fix(web): close the GUI update feedback loop
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 .agents/notes/implemented/bug-fix/2026-07-28-web-gui-feedback-loop.md
|
||||
2026-07-28-web-gui-feedback-loop.md: 27295704d7b0cde3a46a6a28891545bfe31ed275
|
||||
2026-07-28-web-gui-feedback-loop.zh.md: 06367b09bd889bfb7a4052c46369720c4ec6d358
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Web GUI changes close the loop on the existing URL
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-web-gui-feedback-loop.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web agent could identify neither the GUI hosting its session nor the URL the user was viewing. The [runtime-context decision](2026-07-28-web-agent-runtime-context.md) supplies the first fact, but a GUI edit still had no executable acceptance target: source edits, artifact builds, a listening process, and the user's existing page were unrelated observations. Repository affordances made a wrong substitute look valid because `apps/web/package.json` exposed `vite` as its `dev` script and bare Vite returned HTTP 200 even though it could not inject `window.__DSH_BOOT__`.
|
||||
|
||||
The incident session recorded three consecutive failures. After changing the theme, turn 2 delegated acceptance to the user with `pnpm run demo:tui` or an unspecified browser application and ran no assembled Web check. Turn 3 read the frontend package script, launched bare Vite on port 5173, treated HTTP 200 as readiness, and reported success; the user instead received the expected missing-`__DSH_BOOT__` white screen. Turn 4 found `dsh web`, rebuilt the shell, started an unmanaged shell-background process on port 3334, and checked only that the new page returned 200 with a boot manifest. It never probed the existing port 3081. In fact, the port-3081 process predated the build, and its static host read the rebuilt dist on the next request, so refreshing the original page already showed the change. Only after the user reported that fact did turn 5 inspect port 3081 and remove the redundant server.
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh web` publishes one canonical loopback URL as both model-visible orientation and a managed shell fact. The `app:web-surface` prompt section says that unqualified references identify this GUI, names the URL, and defines acceptance as rebuilding the affected Web artifacts and verifying that existing URL after refresh. `DSH_WEB_URL` carries the same value into every foreground or managed background bash call, so the agent can query the target without parsing prose or process listings. The section preserves the no-implicit-DOM, route, or screenshot boundary and does not claim that a LAN alias equals the browser's literal address.
|
||||
|
||||
The `apps/web` development script and Vite configuration reject serve mode before opening a port. Their diagnostics identify `apps/web` as a build-only shell, explain that only `dsh web` injects `window.__DSH_BOOT__`, and name the production and HMR entry paths. Vite build mode remains unchanged.
|
||||
|
||||
No server restart or replacement is required merely because static artifacts changed. The host reads `index.html` and static assets on each request, while client bundles are also served from their current files with `no-cache`; a refresh of the existing URL is therefore the acceptance path after the relevant shell and plugin bundles are rebuilt. Starting a separate server proves only that a separate server works. If the user explicitly requests another long-running server, the existing managed background-task contract owns its lifecycle and completion notices; shell `&` is not an alternative lifecycle.
|
||||
|
||||
## Verification
|
||||
|
||||
The keyless fresh-round-trip browser scenario boots the shipped Web composition, drives a real replayed session, snapshots the URL-bearing system-prompt prefix, and invokes the assembled bash tool to prove `$DSH_WEB_URL` equals the scaffold's actual bound URL. A real Vite subprocess test requires serve mode to exit nonzero with the full-host correction. The real-Loader webserver test rewrites a static asset after the process binds and proves the same port returns the new bytes. These assertions inspect prompt state, process exit, shell output, and HTTP bytes rather than an agent's success statement.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Extend only the system prompt.** Rejected because it would leave the target unavailable to tools, preserve the misleading bare-Vite path, and fail to prove how an existing process observes rebuilt artifacts.
|
||||
|
||||
**Remove the `apps/web` development script without guarding Vite.** Rejected because `npx vite`, the exact incident command, bypasses package scripts. Serve mode itself must fail.
|
||||
|
||||
**Automatically restart or replace the current Web process after every edit.** Rejected because the static server already reads current artifacts per request, a restart would interrupt the session that requested the edit, and plugin HMR has a separate explicit `dsh web --dev` composition.
|
||||
|
||||
**Send DOM, route, or screenshots with each request.** Deferred to a separate logged-input design. Stable URL identity closes this feedback loop without claiming browser state the host does not receive.
|
||||
|
||||
## Consequences
|
||||
|
||||
Web prompts gain a dynamic URL paragraph, so provider prefix reuse now varies by bound port. Bash processes gain one non-secret managed environment variable. Bare Vite can no longer be used as a shell-only visual sandbox; developers use the full host or build mode instead. In exchange, GUI work has one mechanically observable target, the unsupported startup path fails before a white screen, and a second port can no longer masquerade as proof that the user's current page changed.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Web GUI 改动在现有 URL 上闭环
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-web-gui-feedback-loop.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道用户正在查看哪个 URL。[运行时上下文决策](2026-07-28-web-agent-runtime-context.md)提供前一项事实,但 GUI 编辑仍然没有可执行的验收目标:源码编辑、产物构建、监听中的进程与用户已打开的页面只是互不关联的观察结果。仓库提供的入口让错误的替代方案显得合理,因为 `apps/web/package.json` 将 `vite` 暴露为 `dev` 脚本,而裸 Vite 即使无法注入 `window.__DSH_BOOT__`,仍会返回 HTTP 200。
|
||||
|
||||
事故会话记录了连续三次失败。修改主题后,第 2 轮把验收交给用户,要求用户运行 `pnpm run demo:tui` 或打开某个未指明的浏览器应用,自己没有执行任何真实组装的 Web 验证。第 3 轮读取前端包脚本,在 5173 端口启动裸 Vite,把 HTTP 200 当作就绪并报告成功;用户看到的却是符合预期的缺少 `__DSH_BOOT__` 的白屏。第 4 轮找到 `dsh web`,重新构建 Web 外壳,在 3334 端口启动了一个不受管理的 shell 后台进程,并且只检查新页面是否能返回 200 和启动 manifest(元数据清单),始终没有探测现有的 3081 端口。事实上,3081 端口的进程早于此次构建启动,其静态宿主会在下一次请求时读取重新构建的 dist,因此刷新原页面就已经能看到改动。直到用户报告这一事实,第 5 轮才检查 3081 端口并移除冗余服务。
|
||||
|
||||
## 决策
|
||||
|
||||
`dsh web` 发布一个规范的回环 URL,同时将其作为模型可见的界面定位信息和受管 shell 事实。`app:web-surface` 系统提示词段说明:未加限定的指代指向此 GUI;该段会给出 URL,并把验收定义为重新构建受影响的 Web 产物,然后刷新并验证现有 URL。`DSH_WEB_URL` 会把同一个值传入每次前台或受管后台 bash 调用,使 agent 无需解析提示词或进程列表即可查询目标。该段保留「不会隐式获得 DOM、路由或截图」这一边界,也不声称局域网别名等于浏览器中的实际地址。
|
||||
|
||||
`apps/web` 开发脚本和 Vite 配置都会在打开端口前拒绝服务模式。诊断信息会指出 `apps/web` 只是一个仅供构建的外壳,说明只有 `dsh web` 才会注入 `window.__DSH_BOOT__`,并给出生产入口与 HMR(热模块替换)入口路径。Vite 构建模式保持不变。
|
||||
|
||||
静态产物发生变化时,不需要仅为此重启或替换服务器。宿主会在每次请求时读取 `index.html` 和静态资源,客户端 bundle 也会从当前文件提供,并设置 `no-cache`;因此,重新构建相关外壳与插件 bundle 后,刷新现有 URL 就是验收路径。启动另一个服务器只能证明另一个服务器可用。如果用户明确要求再启动一个长期运行的服务器,则现有受管后台任务契约负责其生命周期和完成通知;shell `&` 不能替代这套生命周期机制。
|
||||
|
||||
## 验证
|
||||
|
||||
无密钥的 fresh-round-trip 浏览器场景会启动已交付的 Web 组合,驱动真实的回放会话,对包含 URL 的系统提示词前缀生成快照,并调用组装后的 bash 工具,证明 `$DSH_WEB_URL` 等于测试脚手架实际绑定的 URL。真实 Vite 子进程测试要求服务模式以非零状态退出,并给出改用完整宿主的纠正信息。真实 loader Web 服务器测试会在进程完成绑定后改写静态资源,并证明同一端口返回新的字节。这些断言检查提示词状态、进程退出状态、shell 输出和 HTTP 字节,而不是 agent 的成功声明。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**仅扩展系统提示词。** 不予采纳,因为这样会让工具仍然无法获得目标,保留具有误导性的裸 Vite 路径,并且无法证明现有进程如何观察重新构建的产物。
|
||||
|
||||
**删除 `apps/web` 开发脚本,但不为 Vite 添加防护。** 不予采纳,因为事故中实际使用的命令 `npx vite` 会绕过包脚本。服务模式本身必须失败。
|
||||
|
||||
**每次编辑后自动重启或替换当前 Web 进程。** 不予采纳,因为静态服务器本就会在每次请求时读取当前产物,重启还会中断发起编辑请求的会话,而插件 HMR 已有独立且显式的 `dsh web --dev` 组合。
|
||||
|
||||
**每次请求都发送 DOM、路由或截图。** 推迟到另行设计的已记录输入机制。稳定的 URL 身份足以闭合本次反馈循环,同时不会声称宿主掌握其未接收的浏览器状态。
|
||||
|
||||
## 影响
|
||||
|
||||
Web 提示词会增加一个动态 URL 段落,因此模型提供方的前缀复用会随绑定端口变化。Bash 进程会增加一个非敏感的受管环境变量。裸 Vite 不再能用作只依赖 shell 的视觉沙箱;开发者应改用完整宿主或构建模式。作为交换,GUI 工作有了一个可由机制观察的唯一目标,不受支持的启动路径会在出现白屏前失败,另一个端口也无法再冒充用户当前页面已经改动的证据。
|
||||
@@ -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/README.md
|
||||
README.md: 87cf1597fbb1ce91a7d61f912c42b0d6a68b2eea
|
||||
README.zh.md: b3743f57a4dafda71e437a04ee0148b32dba2dca
|
||||
README.md: 3ec427a0bca501d70c9bca938692d6ef9557a2dd
|
||||
README.zh.md: e0cf7cd399858822df613a98890c0872554a5085
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ The TUI surface:
|
||||
- tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it;
|
||||
- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree.
|
||||
|
||||
The Web and headless surfaces boot one shared composition (`cordis.yml`): both tell the coding agent its model and session working directory, treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root <path>` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Web additionally names the DeepSeek Harness Web GUI as the interaction surface and this checkout as its own source location; references such as “this page” therefore identify the GUI without claiming access to implicit DOM, route, or screenshot state. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`).
|
||||
The Web and headless surfaces boot one shared composition (`cordis.yml`): both tell the coding agent its model and session working directory, treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root <path>` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Web additionally names the DeepSeek Harness Web GUI as the interaction surface, this checkout as its own source location, and the process's canonical local URL in both the prompt and `$DSH_WEB_URL`; references such as “this page” therefore identify the GUI without claiming access to implicit DOM, route, or screenshot state. The Web host reads frontend dist and client bundles when requests arrive, so rebuilding the affected artifacts and refreshing the existing URL updates that GUI without replacing its process; bare `apps/web` Vite serving fails because it cannot inject `window.__DSH_BOOT__`. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`).
|
||||
|
||||
`DSH_TOOLS_MODE` selects the tool presentation mode for the whole Web/headless process: `native` (the schema default when unset), `code` (the `run_code`-only Code Mode wire), or `both`; any other value fails loud at boot through the `dsh-tools` config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it (its config tree pins its own mode).
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ TUI 界面:
|
||||
- 告知 agent 自身源码所在位置:启动后添加一个命名此 harness checkout 的提示词段。该路径从启动器的真实路径解析,因此在 PATH 符号链接和任意 cwd 下仍然有效,使自指的 `cordis` 工具集可以读取并修改它;
|
||||
- 应用 `~/.dsh` 中的个人覆盖(参见 [app-boot 的个人配置](../../packages/ui/app-boot/README.md#personal-config)):`.env` 填补环境缺口(环境中已有的值 > 项目 `.env` > 个人 `.env`),`config.yaml` 则修补已启动的树。
|
||||
|
||||
Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都会告知编码 agent 所用模型和会话工作目录,将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。Web 还会明确说明交互界面是 DeepSeek Harness Web GUI,并把当前 checkout 标记为自身源码位置;因此,「这个页面」等表述会指向该 GUI,但 agent 不会声称可以访问未显式提供的 DOM、路由或截图状态。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都会告知编码 agent 所用模型和会话工作目录,将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。Web 还会明确说明交互界面是 DeepSeek Harness Web GUI、当前 checkout 是自身源码位置,并在提示词和 `$DSH_WEB_URL` 中提供该进程的规范本地 URL;因此,「这个页面」等表述会指向该 GUI,但 agent 不会声称可以访问未显式提供的 DOM、路由或截图状态。Web 宿主会在收到请求时读取前端 dist 和客户端 bundle,因此重新构建受影响的产物并刷新现有 URL 即可更新该 GUI,无须替换其进程;直接使用裸 `apps/web` Vite 服务会失败,因为它无法注入 `window.__DSH_BOOT__`。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
|
||||
`DSH_TOOLS_MODE` 为整个 Web/无头进程选择工具呈现模式:可选值为 `native`(未设置时的 schema 默认值)、`code`(仅含 `run_code` 的 Code Mode 协议接口)或 `both`;任何其他值都会经由 `dsh-tools` 配置 schema 在启动时明确报错。它是一个临时 seam:Loader 组合是静态的,因此该设置作用于整个进程;待 Web UI 负责逐会话工具模式选择后便会移除。TUI 界面会忽略该变量(其配置树固定了自身模式)。
|
||||
|
||||
|
||||
+30
-11
@@ -11,28 +11,47 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tool-bash'
|
||||
import { AppCLIEntry } from './app-cli-entry.ts'
|
||||
|
||||
const CONFIG_PATH = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
/** Stable model-visible orientation for sessions created through `dsh web`. */
|
||||
const WEB_SURFACE_PROMPT = 'You are interacting with the user through the DeepSeek Harness Web GUI. '
|
||||
+ '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.'
|
||||
const DSH_WEB_URL = 'DSH_WEB_URL' as const
|
||||
|
||||
/** Model-visible orientation and acceptance boundary for sessions created through `dsh web`. */
|
||||
function webSurfacePrompt(webUrl: string): string {
|
||||
return `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. '
|
||||
+ 'For changes to this GUI, rebuild the affected Web artifacts and verify this existing URL after a 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.'
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the launcher-owned source location and Web-surface orientation after the
|
||||
* shared config tree settles. The request header logs both sections with every
|
||||
* model-visible prompt.
|
||||
* Add launcher-owned source, Web-surface orientation, and the shell-visible
|
||||
* canonical URL after the shared config tree settles. The request header logs
|
||||
* the model-visible sections; each bash execution receives the same URL through
|
||||
* the managed environment.
|
||||
* @param ctx - settled Web application context.
|
||||
* @param sourceRoot - absolute checkout root resolved from the launcher module.
|
||||
* @param webUrl - canonical loopback URL printed by this Web process.
|
||||
*/
|
||||
export function installWebPromptContext(ctx: Context, sourceRoot: string): void {
|
||||
export function installWebPromptContext(ctx: Context, sourceRoot: string, webUrl: string): void {
|
||||
const systemPrompt = ctx.get('systemPrompt')
|
||||
if (systemPrompt === undefined) throw new Error('dsh web: systemPrompt service missing after settled boot')
|
||||
const bashEnv = ctx.get('bashEnv')
|
||||
if (bashEnv === undefined) throw new Error('dsh web: bashEnv service missing after settled boot')
|
||||
addHarnessSourceSection(ctx, sourceRoot)
|
||||
systemPrompt.section({ name: 'app:web-surface', order: -98, text: WEB_SURFACE_PROMPT })
|
||||
systemPrompt.section({ name: 'app:web-surface', order: -98, text: webSurfacePrompt(webUrl) })
|
||||
bashEnv.register({
|
||||
name: 'web-runtime',
|
||||
variables: {
|
||||
[DSH_WEB_URL]: { description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' },
|
||||
},
|
||||
resolve: () => ({ [DSH_WEB_URL]: webUrl }),
|
||||
})
|
||||
}
|
||||
|
||||
// Display-only mirrors of the webserver schema's allowed hosts: the loopback
|
||||
@@ -63,7 +82,8 @@ export async function runWeb(
|
||||
...workspaceRoot !== undefined && { workspaceRoot },
|
||||
})
|
||||
const { ctx, port: boundPort } = await entry.run()
|
||||
installWebPromptContext(ctx, SOURCE_ROOT)
|
||||
const localUrl = `http://${LOOPBACK_HOST}:${boundPort}`
|
||||
installWebPromptContext(ctx, SOURCE_ROOT, localUrl)
|
||||
|
||||
let exiting = false
|
||||
const shutdown = (code: number): void => {
|
||||
@@ -76,7 +96,6 @@ export async function runWeb(
|
||||
? Object.values(networkInterfaces()).flat()
|
||||
.find(iface => iface !== undefined && iface.family === 'IPv4' && !iface.internal)
|
||||
: undefined
|
||||
const localUrl = `http://${LOOPBACK_HOST}:${boundPort}`
|
||||
console.log(`dsh web: ${localUrl}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate.address}:${boundPort})`}`)
|
||||
|
||||
process.on('SIGTERM', () => { shutdown(0) })
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
{
|
||||
"path": "../../packages/ui/app-boot"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/bash/tool-bash"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/ui/tui"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"dev": "node -e \"console.error('apps/web is build-only; run dsh web or dsh web --dev with pnpm run dev:web') ; process.exit(1)\"",
|
||||
"watch": "vite build --watch"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
@@ -87,9 +88,29 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
const prefix = system.split('\n\n').slice(0, 4).join('\n\n')
|
||||
.split(REPO_ROOT).join('{{sourceRoot}}')
|
||||
.split(join(scaffold.workspaceCwd, 'workspace')).join('{{cwd}}')
|
||||
.split(scaffold.baseUrl).join('{{webUrl}}')
|
||||
await compareOrRefreshGolden(SYSTEM_PROMPT_EXPECTED, prefix, MODE)
|
||||
})
|
||||
|
||||
it('exposes the assembled Web URL to the real bash tool', async () => {
|
||||
if (settledSessionId === undefined) throw new Error('the drive turn did not publish a session id')
|
||||
const agent = scaffold.ctx.agents.get(settledSessionId)
|
||||
if (agent === undefined) throw new Error(`the settled Web agent ${settledSessionId} is no longer live`)
|
||||
const result = await scaffold.ctx.tools.execute({
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
callId: CallId('web-url-probe'),
|
||||
name: 'bash',
|
||||
arguments: {
|
||||
command: 'printf \'%s\\n\' "$DSH_WEB_URL"',
|
||||
description: 'Print current Web URL',
|
||||
},
|
||||
agent,
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(result.content.filter(block => block.type === 'text').map(block => block.text).join(''))
|
||||
.toBe(`${scaffold.baseUrl}\n`)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('rendered the settled turn: markdown, tool row, composer restore', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-round-trip-settled'))
|
||||
// Browser settled-poll after host completion (host strictly precedes render).
|
||||
|
||||
@@ -203,12 +203,12 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
})
|
||||
await ctx.loader.await()
|
||||
assertEntriesLoaded(ctx, 'web e2e scaffold')
|
||||
installWebPromptContext(ctx, REPO_ROOT)
|
||||
const boundPort = ctx.get('httpServer')?.port
|
||||
if (boundPort === undefined) {
|
||||
throw new Error('web e2e scaffold: httpServer service missing after settled boot')
|
||||
}
|
||||
port = boundPort
|
||||
installWebPromptContext(ctx, REPO_ROOT, `http://127.0.0.1:${String(port)}`)
|
||||
|
||||
// Fill the open llm seam on the settled root ctx (llm-deepseek is disabled
|
||||
// in keyless modes; a scenario with no fixture leaves the seam empty so a
|
||||
|
||||
@@ -2,6 +2,6 @@ You are an AI agent powered by the DeepSeek Harness SDK.
|
||||
|
||||
Your own source code is the checkout at {{sourceRoot}}; you can read it there to learn how dsh works and how to extend it.
|
||||
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI. 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.
|
||||
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. For changes to this GUI, rebuild the affected Web artifacts and verify this existing URL after a 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,29 @@
|
||||
/** Bare Vite must fail before it can present a bootless shell as a working GUI. */
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const WEB_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
describe('Web development entry', () => {
|
||||
it('rejects the package dev alias with the full-host correction', async () => {
|
||||
const result = await execa('pnpm', ['run', 'dev'], { cwd: WEB_ROOT, reject: false })
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toContain('apps/web is build-only')
|
||||
expect(result.stderr).toContain('dsh web')
|
||||
})
|
||||
|
||||
it('rejects the standalone Vite server with the full-host correction', async () => {
|
||||
const result = await execa(join(WEB_ROOT, 'node_modules/.bin/vite'), ['--host', '127.0.0.1', '--port', '0'], {
|
||||
cwd: WEB_ROOT,
|
||||
reject: false,
|
||||
timeout: 2_000,
|
||||
})
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toContain('apps/web is not a standalone application')
|
||||
expect(result.stderr).toContain('dsh web')
|
||||
expect(result.stderr).toContain('window.__DSH_BOOT__')
|
||||
})
|
||||
})
|
||||
+16
-1
@@ -1,11 +1,26 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import type { Plugin } from 'vite'
|
||||
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__. '
|
||||
+ 'Build with `pnpm run build && pnpm run build:web`, then run `dsh web` (repository checkout: `pnpm run dsh -- web`). '
|
||||
+ 'For client-plugin HMR, run `pnpm run dsh -- web --dev` together with `pnpm run dev:web`.'
|
||||
|
||||
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
|
||||
function rejectStandaloneServe(): Plugin {
|
||||
const reject = (): never => { throw new Error(STANDALONE_ERROR) }
|
||||
return {
|
||||
name: 'dsh-reject-standalone-web-serve',
|
||||
apply: 'serve',
|
||||
configureServer: reject,
|
||||
configurePreviewServer: reject,
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [rejectStandaloneServe(), react()],
|
||||
resolve: {
|
||||
// Workspace packages resolve to SOURCE: package.json exports point at lib
|
||||
// for Node/type consumers, but the browser bundle must compile src directly
|
||||
|
||||
@@ -111,6 +111,8 @@ describe('real Loader composition', () => {
|
||||
// Static fallback semantics: real asset served, traversal 403, non-GET/
|
||||
// HEAD without a matching route 405.
|
||||
expect(await request(port, '/app.js')).toMatchObject({ status: 200, body: 'export {}' })
|
||||
await writeFile(join(root!, 'dist', 'app.js'), 'export const rebuilt = true')
|
||||
expect(await request(port, '/app.js')).toMatchObject({ status: 200, body: 'export const rebuilt = true' })
|
||||
expect((await request(port, '/..%2f..%2fetc%2fpasswd')).status).toBe(403)
|
||||
expect((await request(port, '/nowhere', { method: 'POST' })).status).toBe(405)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user