Web GUI changes close the loop on the existing URL
Web GUI 改动在现有 URL 上闭环
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 vali
English
Problem
The Web agent could identify neither the GUI hosting its session nor the URL the user was viewing. The runtime-context decision 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 post-mortem owns the event-log timeline and why the original checks accepted the wrong page, process, and port.
Decision
The ordinary dsh web composition mounts the Web bundle's web-runtime plugin, which 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 and names the URL; DSH_WEB_URL carries the same fact into every foreground or managed background bash call. 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. A complete-prompt profile sets the row's surfaceContext to false and receives neither the prompt section nor the managed variable; the Web launcher uses the same setting to suppress its source-checkout prompt section.
The prompt makes the agent, rather than the user, own the hidden startup contract. The client-plugin HMR receiver is always mounted, but automatic client-plugin reload additionally requires a same-checkout pnpm run dev:web watcher, which the agent verifies before promising no-refresh updates. Shell and other plain-package changes still require rebuilding the affected artifacts and refreshing the existing URL. The agent does not launch a replacement GUI unless asked.
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-job 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 matches the actual bound runtime. The real CLI smoke launches dsh web and captures the provider request, pinning the complete two-command development contract. The dev:web watcher test rebuilds an isolated client bundle after a source change; the browser HMR scenario launches dsh web, changes an initial roster bundle, and observes the new DOM under the same page identity. A real Vite subprocess test requires serve mode to exit naturally with the full-host correction and instruments Server.listen() to prove it was never called. 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, DOM identity, 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 client-plugin reload is owned by the always-mounted HMR chain plus the pnpm run dev:web watcher.
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
Ordinary Web prompts gain a dynamic URL paragraph, so provider prefix reuse now varies by bound port. Their 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 agent can teach the user the exact update behavior of the process actually serving their session, and the unsupported startup path fails before a white screen. The URL contract guides the agent away from replacement ports; it does not prohibit arbitrary shell commands from starting one. Profiles that disable surfaceContext also give up this feedback-loop guidance and shell context.
中文
问题
Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道用户正在查看哪个 URL。运行时上下文决策提供前一项事实,但 GUI 编辑仍然没有可执行的验收目标:源码编辑、产物构建、监听中的进程与用户已打开的页面只是互不关联的观察结果。仓库提供的入口让错误的替代方案显得合理,因为 apps/web/package.json 将 vite 暴露为 dev 脚本,而裸 Vite 即使无法注入 window.__DSH_BOOT__,仍会返回 HTTP 200。
事故复盘集中记录事件日志时间线,并解释原有检查为何会接受错误的页面、进程和端口。
决策
常规 dsh web 组合会挂载 Web 组合包的 web-runtime 插件,由它发布一个规范的回环 URL,同时将其作为模型可见的界面定位信息和受管 shell 事实。app:web-surface 提示词段说明:未加限定的指代指向此 GUI,并给出 URL;DSH_WEB_URL 会把同样的事实传入每次前台或受管后台 bash 调用。该段保留「不会隐式获得 DOM、路由或截图」这一边界,也不声称局域网别名等于浏览器中的实际地址。拥有完整提示词的 profile 会把该配置行的 surfaceContext 设为 false,并且不会收到该提示词段和该受管变量;Web 启动器也会使用同一项设置来抑制其源码 checkout 提示词段。
提示词让 agent 而非用户负责隐藏的启动约定。HMR(热模块替换)接收端始终挂载,但客户端插件要自动重新加载,还需要在同一检出中运行 pnpm run dev:web 监听进程,agent 会在承诺无需刷新即可更新前验证这一点。外壳和其他普通包的变更仍然需要重新构建受影响的产物并刷新现有 URL。除非用户要求,agent 不会启动替代 GUI。
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 与实际绑定的运行时一致。真实 CLI 冒烟测试会启动 dsh web 并捕获模型提供方请求,从而固定完整的双命令开发约定。dev:web watcher 测试会在源码发生变化后重新构建隔离的客户端 bundle;浏览器 HMR 场景会启动 dsh web,修改初始 roster 中的 bundle,并在页面 identity 不变的情况下观察新 DOM。真实 Vite 子进程测试要求服务模式在给出改用完整宿主的纠正信息后自然退出,并通过插桩 Server.listen() 证明它从未被调用。真实 loader Web 服务器测试会在进程完成绑定后改写静态资源,并证明同一端口返回新的字节。这些断言检查提示词状态、进程退出状态、shell 输出、DOM identity 和 HTTP 字节,而不是 agent 的成功声明。
考虑过的替代方案
仅扩展系统提示词。 不予采纳,因为这样会让工具仍然无法获得目标,保留具有误导性的裸 Vite 路径,并且无法证明现有进程如何观察重新构建的产物。
删除 apps/web 开发脚本,但不为 Vite 添加防护。 不予采纳,因为事故中实际使用的命令 npx vite 会绕过包脚本。服务模式本身必须失败。
每次编辑后自动重启或替换当前 Web 进程。 不予采纳,因为静态服务器本就会在每次请求时读取当前产物,重启还会中断发起编辑请求的会话,而客户端插件重载由始终挂载的 HMR 链路加 pnpm run dev:web watcher 负责。
每次请求都发送 DOM、路由或截图。 推迟到另行设计的已记录输入机制。稳定的 URL 身份足以闭合本次反馈循环,同时不会声称宿主掌握其未接收的浏览器状态。
影响
常规 Web 提示词会增加一个动态 URL 段落,因此模型提供方的前缀复用会随绑定端口变化。相应的 Bash 进程会增加一个非敏感的受管环境变量。裸 Vite 不再能用作只依赖 shell 的视觉沙箱;开发者应改用完整宿主或构建模式。作为交换,GUI 工作有了一个可由机制观察的唯一目标,agent 可以向用户说明实际承载其会话的进程究竟如何更新,不受支持的启动路径也会在出现白屏前失败。URL 约定会引导 agent 避免使用替代端口,但不会禁止任意 shell 命令启动替代服务。禁用 surfaceContext 的 profile 也会放弃这项反馈闭环指引与 shell 上下文。