Merge remote-tracking branch 'origin/master' into feat/loader-entry-disabled-interpolation
# Conflicts: # vendor/README.md # vendor/loader/src/config/entry.ts
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
|
||||
README.md: cef687dc392f97886ea18b162e8f668a44ce2284
|
||||
README.zh.md: f6721ec256d404e2b602fb2ed921b41c03633a82
|
||||
README.md: 46ea3c241d6775ce90a89c7be58901375a0634a3
|
||||
README.zh.md: f020f46260d6b04b87a4918a671ca6bcbed251d9
|
||||
|
||||
@@ -24,7 +24,7 @@ The shipped apps own these command lines:
|
||||
|
||||
| Profile | Arguments |
|
||||
|---|---|
|
||||
| `web` | `--host`, `--port`, `--dev`, repeatable `--trusted-host` |
|
||||
| `web` | `--host`, `--port`, repeatable `--trusted-host` |
|
||||
| `headless` | the task text, as the positional argument |
|
||||
|
||||
A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
|
||||
@@ -52,7 +52,7 @@ Git-hosted plugins that ship sources build during install through their `prepare
|
||||
|
||||
## Web alias
|
||||
|
||||
`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities), and `--dev` switches the web-runtime row to development mode and enables the client-plugin HMR receiver the bundle ships disabled; it expects a separate `pnpm run dev:web` watcher for no-refresh client bundle updates.
|
||||
`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, and repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities). The client-plugin HMR receiver is always mounted and stays idle until a separate `pnpm run dev:web` watcher rebuilds client bundles.
|
||||
|
||||
```sh
|
||||
dsh web
|
||||
@@ -63,9 +63,9 @@ dsh web --help
|
||||
|
||||
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
|
||||
|
||||
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
|
||||
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
|
||||
|
||||
All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Long-lived surfaces watch valid edits of both `cordis.patch.yml` layers (profile and home) and reapply them transactionally; one-shot runs read the files once at startup.
|
||||
All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Every profile boot watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a one-shot surface exits through its bounded shutdown, which disposes the watchers.
|
||||
|
||||
New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
| Profile | 参数 |
|
||||
|---|---|
|
||||
| `web` | `--host`、`--port`、`--dev`、可重复的 `--trusted-host` |
|
||||
| `web` | `--host`、`--port`、可重复的 `--trusted-host` |
|
||||
| `headless` | 任务文本,作为位置参数 |
|
||||
|
||||
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对 Session 执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
|
||||
@@ -52,7 +52,7 @@ Git 托管、随附源码的插件在安装期间通过其 `prepare` 脚本构
|
||||
|
||||
## Web 别名
|
||||
|
||||
`dsh web` 是 `--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host` 和 `--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority(部署表达式会拼接自己的 authority),`--dev` 把 web-runtime 行切换到开发模式并启用组合包以禁用状态交付的客户端插件 HMR(热模块替换)接收器;若要无刷新更新客户端 bundle,还需单独运行 `pnpm run dev:web` watcher。
|
||||
`dsh web` 是 `--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host` 和 `--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority(部署表达式会拼接自己的 authority),客户端插件 HMR(热模块替换)接收器始终挂载,在单独运行的 `pnpm run dev:web` watcher 重建客户端 bundle 之前保持空闲。
|
||||
|
||||
```sh
|
||||
dsh web
|
||||
@@ -63,9 +63,9 @@ dsh web --help
|
||||
|
||||
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
|
||||
|
||||
进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空;第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose 中,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉。
|
||||
进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空——`SIGTERM` 是监督进程的普通停止请求,在所有 surface 上以 0 退出,`SIGINT` 报告 130;第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose 中,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉。
|
||||
|
||||
所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。常驻 surface 监视两个 `cordis.patch.yml` 层(profile 与 home)的有效编辑并以事务方式重新应用;一次性运行只在启动时读取这些文件一次。
|
||||
所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次 profile 启动都监视两个 `cordis.patch.yml` 层(profile 与 home)的有效编辑并以事务方式重新应用;一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器。
|
||||
|
||||
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', im
|
||||
const USER_PRESET_DIR = '.agent-presets'
|
||||
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import type { HeadlessIo } from '@deepseek-ai/dsh-headless'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
@@ -59,9 +58,6 @@ export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.me
|
||||
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
|
||||
const TELEMETRY_ROW_ID = 'telemetry-otel'
|
||||
|
||||
/** The one-shot runner row: its presence means this composition exits by itself. */
|
||||
const HEADLESS_ROW_ID = 'headless-runner'
|
||||
|
||||
/** The empty root entry list every profile tree patches over. */
|
||||
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
|
||||
# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any
|
||||
@@ -188,9 +184,20 @@ export interface RunProfileOptions {
|
||||
args: readonly string[]
|
||||
}
|
||||
|
||||
/** Re-throw setup failures unless this invocation's signal already owns shutdown. */
|
||||
function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void {
|
||||
if (!signal.aborted) throw error
|
||||
/**
|
||||
* Re-throw a watcher-setup failure unless a shutdown already owns the tree:
|
||||
* a signal aborted this invocation, or an app requested exit (`ctx.appExit`
|
||||
* from a fast one-shot) and the root's disposal rejected the in-flight setup
|
||||
* await. Either way the failure describes a tree that is exiting as asked,
|
||||
* not a broken watch.
|
||||
* @param ctx - the booted root context.
|
||||
* @param signal - this invocation's signal-shutdown fact.
|
||||
* @param error - the setup failure.
|
||||
*/
|
||||
function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown): void {
|
||||
if (signal.aborted) return
|
||||
if (ctx.fiber.state !== FiberState.ACTIVE || ctx.get('loader') === undefined) return
|
||||
throw error
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,11 +208,6 @@ function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void
|
||||
*/
|
||||
export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
|
||||
const composed = composeProfile(options.profile, options.patchFiles)
|
||||
// A one-shot composition ends by itself, which changes what a signal means
|
||||
// and makes watching the user's patch layer pointless.
|
||||
const headlessRow = composed.rows.get(HEADLESS_ROW_ID)
|
||||
const oneShot = headlessRow !== undefined && headlessRow.disabled !== true
|
||||
|
||||
const app: { current?: Context } = {}
|
||||
const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
|
||||
const signalShutdown = new AbortController()
|
||||
@@ -215,7 +217,10 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
}
|
||||
// Signals own teardown throughout the startup window, not only after boot()
|
||||
// settles: an inserted provider can publish before sibling rows finish mounting.
|
||||
process.on('SIGTERM', () => { interrupt(oneShot ? 143 : 0) })
|
||||
// SIGTERM is a supervisor's ordinary stop request and exits 0 on every
|
||||
// surface — the launcher does not know whether the app considered its work
|
||||
// complete; SIGINT is a user interrupt and reports 130.
|
||||
process.on('SIGTERM', () => { interrupt(0) })
|
||||
process.on('SIGINT', () => { interrupt(130) })
|
||||
installFailLoud(NAME, process, async () => {
|
||||
await app.current?.fiber.dispose()
|
||||
@@ -240,9 +245,6 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
|
||||
...composed.overlays,
|
||||
])
|
||||
// One-shot runs exit through the runner; watching would only hold the
|
||||
// process open after its exit request.
|
||||
const watchProfilePatch = !oneShot
|
||||
// Cloned for the same insert-aliasing reason as composeLive: the boot
|
||||
// application must not mutate the objects later reloads recompose from.
|
||||
const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
|
||||
@@ -256,22 +258,16 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
args: options.args,
|
||||
exit: code => void shutdown.shutdown(code),
|
||||
})
|
||||
if (oneShot) {
|
||||
const io: HeadlessIo = {
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
exit: (code) => { void shutdown.shutdown(code) },
|
||||
}
|
||||
hostCtx.provide('headlessIo', io)
|
||||
}
|
||||
})
|
||||
app.current = ctx
|
||||
// A surface can dispose the whole tree while boot or this post-boot watcher
|
||||
// setup is still in flight. Loader presence and fiber state own
|
||||
// liveness; the local signal fact distinguishes that expected exit race
|
||||
// from a real HMR error.
|
||||
if (watchProfilePatch
|
||||
&& !signalShutdown.signal.aborted
|
||||
// setup is still in flight — a signal, or a fast one-shot's appExit. Loader
|
||||
// presence and fiber state own liveness; the initial check skips a tree
|
||||
// that already exited, and the catch below re-checks for an exit that
|
||||
// landed mid-setup. Watching is unconditional: a one-shot surface exits
|
||||
// through its bounded shutdown, which disposes the watchers before the
|
||||
// loop drains.
|
||||
if (!signalShutdown.signal.aborted
|
||||
&& ctx.fiber.state === FiberState.ACTIVE
|
||||
&& ctx.get('loader') !== undefined) {
|
||||
try {
|
||||
@@ -299,7 +295,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
compose: composeLive,
|
||||
})
|
||||
} catch (error) {
|
||||
suppressSignalShutdownError(signalShutdown.signal, error)
|
||||
suppressShutdownError(ctx, signalShutdown.signal, error)
|
||||
}
|
||||
}
|
||||
return { ctx, shutdown }
|
||||
|
||||
@@ -79,6 +79,9 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
{ id: 'skill-badge', disabled: false },
|
||||
{ id: 'modules', disabled: true },
|
||||
{ id: 'connection', disabled: true },
|
||||
// The always-on reload chain waits for the browser roster and bound port
|
||||
// disabled above.
|
||||
{ id: 'client-hmr', disabled: true },
|
||||
// The shipped `-auto` chooser resolves its interaction from a running
|
||||
// host and so waits for the webserver disabled above; the browse variant
|
||||
// supplies `directoryPicker` without one.
|
||||
|
||||
123
apps/web/tests/goal-command-presentation.e2e.ts
Normal file
123
apps/web/tests/goal-command-presentation.e2e.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Web e2e: /goal opts its command input into the human transcript while the
|
||||
// command remains log-only. The shipped composition runs with no model adapter,
|
||||
// so an accidental turn fails loud in addition to the event-level assertions.
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type {} from '@deepseek-ai/dsh-commands/types'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria,
|
||||
compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/goal-command-presentation', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL(
|
||||
'./snapshots/goal-command-presentation/ui.expected.md', import.meta.url,
|
||||
))
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe('web e2e: /goal human transcript presentation', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const events: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { events.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('shows the bare input and result from a fresh session without a model turn', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-command-presentation'))
|
||||
await expect.poll(() => page.getByText('Into the Unknown', { exact: false }).count(), {
|
||||
timeout: 15_000,
|
||||
}).toBe(1)
|
||||
const input = page.locator('textarea').first()
|
||||
await input.fill('/goal')
|
||||
await input.press('Enter')
|
||||
await expect.poll(() => input.inputValue()).toBe('/goal ')
|
||||
await input.press('Enter')
|
||||
|
||||
const commandInput = page.locator('[data-command-input]')
|
||||
await commandInput.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => commandInput.textContent()).toBe('/goal')
|
||||
expect(await commandInput.getAttribute('role')).toBe('group')
|
||||
expect(await commandInput.getAttribute('aria-label')).toBe('Command input')
|
||||
expect(await commandInput.getByRole('button').count()).toBe(0)
|
||||
const typography = await commandInput.evaluate((element) => {
|
||||
const bubble = element.firstElementChild?.firstElementChild
|
||||
if (!(bubble instanceof HTMLElement)) throw new Error('command input bubble is missing')
|
||||
const rootStyle = getComputedStyle(element)
|
||||
const bubbleStyle = getComputedStyle(bubble)
|
||||
return {
|
||||
fontFamily: bubbleStyle.fontFamily,
|
||||
parentFontFamily: rootStyle.fontFamily,
|
||||
fontSize: bubbleStyle.fontSize,
|
||||
lineHeight: bubbleStyle.lineHeight,
|
||||
}
|
||||
})
|
||||
expect(typography).toMatchObject({ fontSize: '14px', lineHeight: '22px' })
|
||||
expect(typography.fontFamily).not.toBe(typography.parentFontFamily)
|
||||
const resultRow = page.locator('[data-variant="others"]').filter({ hasText: 'No goal is currently set.' })
|
||||
await expect.poll(() => resultRow.count(), { timeout: 10_000 }).toBe(1)
|
||||
expect(await resultRow.getByText('goal', { exact: true }).count()).toBe(1)
|
||||
await expect.poll(() => page.locator('[data-phase="active"]').count()).toBe(1)
|
||||
expect(await page.getByText('Into the Unknown', { exact: false }).count()).toBe(0)
|
||||
|
||||
const run = events.find(event => event.type === 'command/run')
|
||||
expect(run).toMatchObject({
|
||||
type: 'command/run',
|
||||
data: { name: 'goal', args: ' ', source: { kind: 'user' } },
|
||||
})
|
||||
expect(events.some(event => event.type === 'command/done')).toBe(true)
|
||||
expect(events.some(event => event.type === 'user/message')).toBe(false)
|
||||
expect(events.some(event => event.type === 'turn/start')).toBe(false)
|
||||
expect(events.some(event => event.type === 'step/start')).toBe(false)
|
||||
expect(events.some(event => event.type === 'request/header')).toBe(false)
|
||||
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
}, 60_000)
|
||||
|
||||
it('reloads the same bubble and result from the persisted command lifecycle', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-command-presentation-reload'))
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
|
||||
await expect.poll(() => page.locator('[data-command-input]').textContent(), { timeout: 15_000 }).toBe('/goal')
|
||||
const resultRow = page.locator('[data-variant="others"]').filter({ hasText: 'No goal is currently set.' })
|
||||
await expect.poll(() => resultRow.count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect.poll(() => page.locator('[data-phase="active"]').count()).toBe(1)
|
||||
|
||||
const sessions = scaffold.ctx.sessions.list()
|
||||
expect(sessions).toHaveLength(1)
|
||||
const persisted = sessions[0]?.events ?? []
|
||||
expect(persisted.filter(event => event.type === 'command/run' || event.type === 'command/done')
|
||||
.map(event => event.type)).toEqual(['command/run', 'command/done'])
|
||||
expect(persisted.some(event => event.type === 'user/message')).toBe(false)
|
||||
expect(persisted.some(event => event.type === 'turn/start')).toBe(false)
|
||||
expect(persisted.some(event => event.type === 'step/start')).toBe(false)
|
||||
expect(persisted.some(event => event.type === 'request/header')).toBe(false)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
|
||||
}, 90_000)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Published dsh web --dev + pnpm dev:web → browser HMR, with no page reload. */
|
||||
/** Published dsh web + pnpm dev:web → browser HMR, with no page reload. */
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
@@ -92,14 +92,14 @@ it('hot-reloads a real client-plugin source edit without refreshing the page', a
|
||||
watcher = subprocessCtx.subprocess.spawn(spawnSpec(['pnpm', 'run', 'dev:web'], REPO_ROOT))
|
||||
await waitForOutput(watcher, /dev-web: watching/, 'pnpm run dev:web')
|
||||
host = subprocessCtx.subprocess.spawn(spawnSpec(
|
||||
[process.execPath, binPath, 'web', '--dev', '--port', '0'],
|
||||
[process.execPath, binPath, 'web', '--port', '0'],
|
||||
world,
|
||||
{
|
||||
DEEPSEEK_API_KEY: 'keyless-hmr-no-call',
|
||||
DSH_HOME: join(world, '.dsh'),
|
||||
},
|
||||
))
|
||||
const baseUrl = await waitForOutput(host, /dsh web: (http:\/\/[^\s]+)/, 'built dsh web --dev')
|
||||
const baseUrl = await waitForOutput(host, /dsh web: (http:\/\/[^\s]+)/, 'built dsh web')
|
||||
browser = await chromium.launch()
|
||||
const page = await browser.newPage()
|
||||
const pageErrors: string[] = []
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
// stores it write-only under the derived reference (`MINIMAX_CN_API_KEY`)
|
||||
// while the settings document records only that reference. Each saved row
|
||||
// appears after route topology invalidation without presenting liveness as
|
||||
// provider status. The customized-settings fold writes the curated
|
||||
// reasoning field as a merge patch. Zero model calls: configuration is pure
|
||||
// provider status. The customized-settings fold writes its curated fields —
|
||||
// the endpoint, and a declared route's own name and protocol — as merge
|
||||
// patches against the stored profile. Zero model calls: configuration is pure
|
||||
// settings/credentials/llm-domain traffic, so there is no fixture and a
|
||||
// stray stream would fail loud because the adapter registry is empty. The provider under test is
|
||||
// minimax-cn so a developer's real ANTHROPIC/OPENAI environment keys can
|
||||
@@ -28,6 +29,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/models-settings', import
|
||||
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
|
||||
const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
|
||||
const DECLARED_EXPECTED = join(SNAPSHOT_DIR, 'declared.expected.md')
|
||||
const DECLARED_EDIT_EXPECTED = join(SNAPSHOT_DIR, 'declared-edit.expected.md')
|
||||
const NATIVE_DELETE_EXPECTED = join(SNAPSHOT_DIR, 'native-delete.expected.md')
|
||||
const DELETE_EXPECTED = join(SNAPSHOT_DIR, 'delete.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -209,6 +211,40 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('reopens the name and protocol a declared route was created with', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-declared-identity'))
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '编辑 Acme Gateway (acme-gateway)' }).click()
|
||||
await dialog.getByText('自定义设置').click()
|
||||
// The create card asked this route for a name and a protocol because
|
||||
// nothing can default them; the editor reaches the same two fields rather
|
||||
// than sending the user to settings.yaml for what only this route names.
|
||||
const protocol = dialog.getByLabel('API 协议')
|
||||
await protocol.waitFor({ timeout: 10_000 })
|
||||
expect(await protocol.inputValue()).toBe('openai-completions')
|
||||
const name = dialog.getByLabel('显示名称', { exact: true })
|
||||
expect(await name.inputValue()).toBe('Acme Gateway')
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(DECLARED_EDIT_EXPECTED, snapshot, MODE)
|
||||
|
||||
await protocol.selectOption('anthropic-messages')
|
||||
await name.fill('Acme 网关')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await expect.poll(async () => dialog.getByLabel('API 协议').count(), { timeout: 10_000 }).toBe(0)
|
||||
// The adapter re-resolved the route under the new protocol and re-registered
|
||||
// it under the new name: an unserviceable profile would have been refused
|
||||
// at the write instead, and a rename that did not re-register would leave
|
||||
// the old label on the row.
|
||||
await dialog.getByText('Acme 网关', { exact: true }).first().waitFor({ timeout: 10_000 })
|
||||
// The status line names the route as the refreshed directory reports it;
|
||||
// the target captured when the card opened still carries the old name.
|
||||
await dialog.getByText('已保存 Acme 网关 (acme-gateway)。', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(document).toContain('api: anthropic-messages')
|
||||
expect(document).toContain('displayName: Acme 网关')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('confirms an identified provider deletion before removing its profile and key', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-delete'))
|
||||
const settingsDialog = page.getByRole('dialog', { name: '设置' })
|
||||
@@ -243,8 +279,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'configured.expected.md', 'declared.expected.md', 'delete.expected.md',
|
||||
'empty.expected.md', 'native-delete.expected.md',
|
||||
'configured.expected.md', 'declared-edit.expected.md', 'declared.expected.md',
|
||||
'delete.expected.md', 'empty.expected.md', 'native-delete.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -101,14 +101,14 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
callId: CallId('web-url-probe'),
|
||||
name: 'bash',
|
||||
arguments: {
|
||||
command: 'printf \'%s\\n%s\\n\' "$DSH_WEB_URL" "$DSH_WEB_MODE"',
|
||||
command: 'printf \'%s\\n\' "$DSH_WEB_URL"',
|
||||
description: 'Print current Web runtime',
|
||||
},
|
||||
agent,
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(result.content.filter(block => block.type === 'text').map(block => block.text).join(''))
|
||||
.toBe(`${scaffold.baseUrl}\nproduction\n`)
|
||||
.toBe(`${scaffold.baseUrl}\n`)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('rendered the settled turn: markdown, tool row, composer restore', async () => {
|
||||
|
||||
@@ -416,7 +416,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// (apps/web IS @deepseek-ai/dsh-frontend); only the URL line is silenced.
|
||||
// Preserve the composed surface-context choice because a patch replaces
|
||||
// the row's complete config.
|
||||
{ id: 'web-runtime', config: { mode: 'production', printUrl: false, surfaceContext } },
|
||||
{ id: 'web-runtime', config: { printUrl: false, surfaceContext } },
|
||||
...options.remoteAuthority === undefined
|
||||
? []
|
||||
: [{ id: 'connection', config: { trustedHosts: [options.remoteAuthority] } }],
|
||||
|
||||
@@ -152,6 +152,67 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('uses the persisted dark preference while plugins are still loading', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme'))
|
||||
await page.emulateMedia({ colorScheme: 'light' })
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const initialDialog = page.getByRole('dialog', { name: '设置' })
|
||||
const darkCube = initialDialog.getByRole('button', { name: '深色' })
|
||||
await darkCube.click()
|
||||
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-theme:\n\s+preference: dark/)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// Hold real plugin bundles so the shell-owned loading page remains observable.
|
||||
const pluginPattern = '**/plugins/**'
|
||||
let releaseBundles = (): void => {}
|
||||
const bundlesReleased = new Promise<void>((resolve) => { releaseBundles = resolve })
|
||||
await page.route(pluginPattern, async (route) => {
|
||||
await bundlesReleased
|
||||
await route.continue()
|
||||
})
|
||||
|
||||
const warningStart = tripwire.warnings.length
|
||||
let reload: ReturnType<Page['reload']> | undefined
|
||||
try {
|
||||
reload = page.reload({ waitUntil: 'domcontentloaded' })
|
||||
const loading = page.getByText('Loading plugins…', { exact: true })
|
||||
await loading.waitFor({ timeout: 10_000 })
|
||||
const state = await loading.evaluate((element) => {
|
||||
const boot = element.parentElement?.parentElement
|
||||
if (boot === undefined || boot === null) throw new Error('loading hint is detached from the boot page')
|
||||
return {
|
||||
attr: document.body.hasAttribute('data-ds-dark-theme'),
|
||||
background: getComputedStyle(boot).backgroundColor,
|
||||
colorScheme: document.documentElement.style.colorScheme,
|
||||
}
|
||||
})
|
||||
expect(state).toEqual({
|
||||
attr: true,
|
||||
background: 'rgb(21, 21, 23)',
|
||||
colorScheme: 'dark',
|
||||
})
|
||||
} finally {
|
||||
releaseBundles()
|
||||
await reload
|
||||
await page.unroute(pluginPattern)
|
||||
}
|
||||
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const restoredDialog = page.getByRole('dialog', { name: '设置' })
|
||||
const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' })
|
||||
await systemCube.click()
|
||||
await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
||||
await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme')), {
|
||||
timeout: 5_000,
|
||||
}).toBe(false)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('flips the theme through the Appearance cubes and persists across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-appearance'))
|
||||
interface ThemeState {
|
||||
|
||||
@@ -145,7 +145,7 @@ it('lets a preset producer reach the background-task registry', async () => {
|
||||
content: [{ type: 'text', text: 'started background task bash-1' }],
|
||||
})
|
||||
|
||||
// The control surface reads what the producer started: same registry, one
|
||||
// The controller reads what the producer started: same registry, one
|
||||
// owner. A per-preset registry would list nothing here even on success.
|
||||
const listed = await ctx.tools.execute({
|
||||
signal,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { REPO_ROOT, connectFreshWorkspace, newEnglishPage, probeFreePort, requireDist, saveFailureShot } from './support.ts'
|
||||
|
||||
const DEVELOPMENT_PROMPT = fileURLToPath(new URL('./snapshots/web-runtime-context/development-prompt.expected.md', import.meta.url))
|
||||
const WEB_SURFACE_PROMPT = fileURLToPath(new URL('./snapshots/web-runtime-context/web-surface-prompt.expected.md', import.meta.url))
|
||||
|
||||
function waitForReadyLine(child: ChildProcess): Promise<string> {
|
||||
return new Promise((resolveReady, reject) => {
|
||||
@@ -187,7 +187,7 @@ describe('dsh web keyless CLI smoke', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('routes --dev runtime context and workspace instructions through the real CLI request', async () => {
|
||||
it('routes web runtime context and workspace instructions through the real CLI request', async () => {
|
||||
requireDist()
|
||||
const workspace = mkdtempSync(join(tmpdir(), 'dsh-web-workspace-'))
|
||||
mkdirSync(join(workspace, '.git'))
|
||||
@@ -226,7 +226,7 @@ describe('dsh web keyless CLI smoke', () => {
|
||||
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0', '--dev'],
|
||||
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
|
||||
{
|
||||
cwd: workspace,
|
||||
env: {
|
||||
@@ -261,7 +261,7 @@ describe('dsh web keyless CLI smoke', () => {
|
||||
const workspaceMessage = captured.messages?.find(message =>
|
||||
message.role === 'user' && message.content?.includes('web-workspace-context-probe'))
|
||||
const systemMessage = captured.messages?.find(message => message.role === 'system')
|
||||
const expectedWebSection = readFileSync(DEVELOPMENT_PROMPT, 'utf8').trimEnd()
|
||||
const expectedWebSection = readFileSync(WEB_SURFACE_PROMPT, 'utf8').trimEnd()
|
||||
.replace('{{webUrl}}', baseUrl)
|
||||
expect(systemMessage?.content).toContain(expectedWebSection)
|
||||
expect(workspaceMessage).toMatchInlineSnapshot(`
|
||||
|
||||
@@ -2,6 +2,6 @@ You are an AI agent powered by the DeepSeek Harness SDK.
|
||||
|
||||
The DeepSeek Harness implementation checkout is at {{sourceRoot}}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.
|
||||
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. This Web process was launched without `--dev`, so HMR is inactive: rebuild the affected Web artifacts and verify this existing URL after a page refresh. If the user wants no-refresh client-plugin updates, explain that this GUI must be restarted with `dsh web --dev` and `pnpm run dev:web` must also run from this same checkout; do not present either command alone as sufficient. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while `pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
|
||||
|
||||
You are a coding agent powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- group "Command input": /goal
|
||||
- 'button "goal No goal is currently set. Usage: /goal [<objective>|clear|edit <objective>|pause|resume]"':
|
||||
- img
|
||||
- img
|
||||
- text: "goal No goal is currently set. Usage: /goal [<objective>|clear|edit <objective>|pause|resume]"
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
@@ -6,6 +6,7 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- group "Command input": /goal 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的
|
||||
- 'button "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit <objective>, /goal pause, /goal clear"':
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
- dialog "设置":
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置":
|
||||
- img
|
||||
- text: 通用设置
|
||||
- button "模型":
|
||||
- img
|
||||
- text: 模型
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: minimax-cn
|
||||
- img "API 密钥已配置"
|
||||
- button "编辑 minimax-cn": 编辑
|
||||
- button "删除 minimax-cn": 删除
|
||||
- listitem:
|
||||
- text: Acme Gateway 自定义
|
||||
- button "编辑 Acme Gateway (acme-gateway)": 编辑
|
||||
- button "删除 Acme Gateway (acme-gateway)": 删除
|
||||
- text: Acme Gateway acme-gateway API 密钥
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 输入 API 密钥,或留空使用环境认证
|
||||
- group:
|
||||
- text: 自定义设置 显示名称
|
||||
- textbox "显示名称":
|
||||
- /placeholder: acme-gateway
|
||||
- text: Acme Gateway
|
||||
- text: API 地址
|
||||
- textbox "API 地址":
|
||||
- /placeholder: https://gateway.acme.example/v1
|
||||
- text: https://gateway.acme.example/v1
|
||||
- text: API 协议
|
||||
- combobox "API 协议":
|
||||
- option "openai-completions" [selected]
|
||||
- option "openai-responses"
|
||||
- option "anthropic-messages"
|
||||
- region "模型目录":
|
||||
- text: 模型目录 已自定义模型目录
|
||||
- button "恢复默认模型"
|
||||
- button "获取可用模型"
|
||||
- textbox "模型 ID 1":
|
||||
- /placeholder: 模型 ID
|
||||
- text: acme-large
|
||||
- textbox "显示名称 1":
|
||||
- /placeholder: 显示名称
|
||||
- button "容量 1"
|
||||
- button "删除模型 1"
|
||||
- button "添加模型"
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
- button "添加提供方":
|
||||
- img
|
||||
- text: 添加提供方
|
||||
- button "添加自定义提供方":
|
||||
- img
|
||||
- text: 添加自定义提供方
|
||||
@@ -6,6 +6,7 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- group "Command input": /goal Keep the composer context panels aligned
|
||||
- 'button "goal Goal created Status: active Objective: Keep the composer context panels aligned Rounds: 0/256 Activation: armed Commands: /goal edit <objective>, /goal pause, /goal clear"':
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. This Web process was launched with `dsh web --dev`, so its client-plugin HMR receiver is active. No-refresh updates occur only when `pnpm run dev:web` is also running from this same checkout to rebuild client-plugin bundles; verify that watcher before promising automatic updates. Client-plugin changes then reload automatically, while apps/web shell and other plain-package changes still require a rebuild and page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
|
||||
@@ -0,0 +1 @@
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while `pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
|
||||
@@ -70,6 +70,7 @@
|
||||
"tests/produced-files.e2e.ts",
|
||||
"tests/produced-file-mentions.e2e.ts",
|
||||
"tests/goal-bar.e2e.ts",
|
||||
"tests/goal-command-presentation.e2e.ts",
|
||||
"tests/subagent-conversation.e2e.ts",
|
||||
"tests/subagent-interrupt.e2e.ts",
|
||||
"tests/subagent-interrupt-ui.e2e.ts",
|
||||
|
||||
@@ -6,7 +6,7 @@ import react from '@vitejs/plugin-react'
|
||||
const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url))
|
||||
const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
|
||||
+ 'From a repository checkout, run `pnpm dsh web`; an installed package uses `dsh web`. '
|
||||
+ 'For client-plugin HMR, run `pnpm dsh web --dev` together with `pnpm run dev:web`.'
|
||||
+ 'For client-plugin HMR, run `pnpm dsh web` together with `pnpm run dev:web`.'
|
||||
|
||||
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
|
||||
function rejectStandaloneServe(): Plugin {
|
||||
@@ -130,7 +130,7 @@ export default defineConfig({
|
||||
// Workspace packages resolve to SOURCE: package.json exports point at lib
|
||||
// for Node/type consumers, but the browser bundle must compile src directly
|
||||
// so CSS rides vite's pipeline instead of the CSS-externalized lib bundle.
|
||||
// Only the shell's normal-package surface is aliased — plugin packages are
|
||||
// Only the shell's normal package entry is aliased — plugin packages are
|
||||
// NEVER bundled here (shell self-sufficiency — see
|
||||
// packages/client/web/README.md); they arrive as runtime
|
||||
// bundles through the client module system. Order matters — subpath
|
||||
|
||||
Reference in New Issue
Block a user