diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml index b4b1fb5110..1c04f72d28 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md -2026-07-30-web-config-plane.md: 0b18cee414df23a2ed8a8b43b76dc06403804691 -2026-07-30-web-config-plane.zh.md: e70c2a47970f943e49393b099c4fcea58dc0fbdc +2026-07-30-web-config-plane.md: 483058c8a24ad8c414278eba84f4226e973f2923 +2026-07-30-web-config-plane.zh.md: 9247aac52401cd570d37f4aad47d9da6bb194d19 diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md index 0b18cee414..483058c8a2 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md @@ -16,7 +16,7 @@ PR1 made LLM adapter configuration restart-free at the seam, but the only writer **`describe()` grows layers and structural secret redaction.** `SettingsDescriptor` carries `base`/`user` beside the effective value, so the form marks "overridden" by presence in the user layer, not value inequality (an override *equal* to the base is still an override). `describe({ redactSecrets: true })` — mandatory at every wire face — strips `role('secret')` subtrees from all three layers via a pure structural walk of the schema (object/dict/array containers; a secret-role subtree is one opaque leaf) and enumerates the stripped slots as `{path, set}`, so a page can render write-only inputs without ever receiving a value. -**The Host identifies and opens the local settings document.** The settings seam exposes optional `documentPath` provider metadata and a `prepareDocument()` operation; `settings-local` returns its fully resolved custom or `$DSH_HOME/settings.yaml` filename and exclusively creates an absent empty document with owner-only permissions, while non-file providers retain the base `undefined`. The loopback-only `settings.describe` response carries only the boolean `hasDocument` capability beside the redacted namespace views. `ui-settings-general` registers a `settings.action` entry only on loopback pages, shows it only after the metadata confirms that a provider-owned local document can be prepared, and invokes pathless `settings.openDocument`; the Host resolves the provider path again before a text-document handoff (`open -t` on macOS so an arbitrary YAML file association cannot redirect the gesture, `xdg-open` on desktop Linux, `Invoke-Item` on Windows, and `wslpath -w` followed by that Windows handoff on WSL). Generic workspace paths retain the existing default-application handoff. The browser neither derives `$DSH_HOME` nor receives a filesystem target; remote pages make no privileged settings read for this action. +**The Host identifies and opens the local settings document.** The settings seam exposes optional `documentPath` provider metadata and a `prepareDocument()` operation; `settings-local` returns its fully resolved custom or `$DSH_HOME/settings.yaml` filename and exclusively creates an absent empty document with owner-only permissions, while non-file providers retain the base `undefined`. The loopback-only `settings.describe` response carries only the boolean `hasDocument` capability beside the redacted namespace views. `ui-settings-general` registers a `settings.action` entry only on loopback pages, shows it only after the metadata confirms that a provider-owned local document can be prepared, and invokes pathless `settings.openDocument`; the Host resolves the provider path again before a text-document handoff (`open -t` on macOS so an arbitrary YAML file association cannot redirect the gesture, `xdg-open` on desktop Linux, `Invoke-Item` on Windows, and `wslpath -w` followed by that Windows handoff on WSL). Generic workspace paths retain the default intent, including its browser preference for browser-renderable documents. The browser neither derives `$DSH_HOME` nor receives a filesystem target; remote pages make no privileged settings read for this action. **The llm seam declares configurability and announces topology.** `registerConfigurableProviders()` is an all-or-nothing, fiber-scoped directory of `{provider, displayName, settingsNs, settingsPath}` — the addressing a config page needs to open the right settings subtree for a route that may not exist yet; `listConfigurableProviders()` merges with live routes in the wire handler so undeclared live routes still report active. The zero-payload `'llm/adapters-updated'` event fires from all four registration/unregistration commit points with contained listener dispatch (INVARIANT rethrow), following the settings/commands precedent. `llm-deepseek`'s route renamed to `deepseek-official` because the pi-ai catalog legitimately owns `deepseek` as an aggregator entry; pre-release stance, no alias. diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md index e70c2a4797..9247aac524 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md @@ -16,7 +16,7 @@ PR1 让 LLM(大语言模型)适配器配置在 seam 层面免重启,但唯 **`describe()` 增加分层与结构化 secret 脱敏。**`SettingsDescriptor` 在生效值之外携带 `base`/`user`,表单据此按「字段是否出现在用户层」来标记「已覆盖」,而非按值是否不等(与 base *相等*的覆盖仍然是覆盖)。`describe({ redactSecrets: true })`——在每个 wire 面都强制启用——经由对 schema 的纯结构遍历(object/dict/array 容器;secret 角色子树整体是一个不透明叶节点)从全部三层剥除 `role('secret')` 子树,并把剥除的槽位枚举为 `{path, set}`,页面因此不必收到任何值就能渲染只写输入框。 -**Host 识别并打开本地设置文档。** settings seam 暴露可选的 `documentPath` 提供方元数据和 `prepareDocument()` 操作;`settings-local` 返回已完全解析的自定义文件名或 `$DSH_HOME/settings.yaml` 文件名,并在文档缺失时以仅属主可访问的权限独占创建空文档,非文件提供方则保留基类的 `undefined`。仅限回环访问的 `settings.describe` 响应会在脱敏 namespace 视图旁只携带布尔型 `hasDocument` 能力。`ui-settings-general` 只在回环页面注册一条 `settings.action` 条目,只有元数据确认可准备好一份由提供方持有的本地文档后才显示,并调用无路径参数的 `settings.openDocument`;Host 会在文本文档交接前再次解析提供方路径(macOS 上使用 `open -t`,使任意 YAML 文件关联无法重定向这次操作;桌面 Linux 上使用 `xdg-open`;Windows 上使用 `Invoke-Item`;WSL 上先执行 `wslpath -w`,再使用同一 Windows 交接)。通用 Workspace 路径仍保留现有的默认应用交接。浏览器既不推导 `$DSH_HOME`,也不会收到文件系统目标;远程页面不会为这项操作发起特权 settings 读取。 +**Host 识别并打开本地设置文档。** settings seam 暴露可选的 `documentPath` 提供方元数据和 `prepareDocument()` 操作;`settings-local` 返回已完全解析的自定义文件名或 `$DSH_HOME/settings.yaml` 文件名,并在文档缺失时以仅属主可访问的权限独占创建空文档,非文件提供方则保留基类的 `undefined`。仅限回环访问的 `settings.describe` 响应会在脱敏 namespace 视图旁只携带布尔型 `hasDocument` 能力。`ui-settings-general` 只在回环页面注册一条 `settings.action` 条目,只有元数据确认可准备好一份由提供方持有的本地文档后才显示,并调用无路径参数的 `settings.openDocument`;Host 会在文本文档交接前再次解析提供方路径(macOS 上使用 `open -t`,使任意 YAML 文件关联无法重定向这次操作;桌面 Linux 上使用 `xdg-open`;Windows 上使用 `Invoke-Item`;WSL 上先执行 `wslpath -w`,再使用同一 Windows 交接)。通用 Workspace 路径仍保留默认意图,包括针对浏览器可渲染文档的浏览器偏好。浏览器既不推导 `$DSH_HOME`,也不会收到文件系统目标;远程页面不会为这项操作发起特权 settings 读取。 **llm seam 声明可配置性并公布拓扑。**`registerConfigurableProviders()` 是一个全有或全无、以 fiber 为作用域的目录,条目为 `{provider, displayName, settingsNs, settingsPath}`——这正是配置页要为一条可能尚不存在的路由打开正确设置子树时所需要的寻址;`listConfigurableProviders()` 在 wire 处理器里与存活路由合并,未声明的存活路由因此仍报告为激活。零负载的 `'llm/adapters-updated'` 事件从全部四个注册/注销提交点触发,listener 派发带异常隔离(INVARIANT 重抛),沿用 settings/commands 的先例。`llm-deepseek` 的路由重命名为 `deepseek-official`,因为 pi-ai catalog 名正言顺地拥有 `deepseek` 这个聚合器条目;依预发布立场,不设别名。 diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml index a702f1511f..5f2f7bc9ae 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md -2026-07-28-tool-call-file-open-in-os.md: 73f5091888ab2506eab50b827e74c5120394b127 -2026-07-28-tool-call-file-open-in-os.zh.md: c1bc93c472aae8be8cee3bab6cd02f556abca494 +2026-07-28-tool-call-file-open-in-os.md: 499d3caa11e1f2c17d96e43b7846df72a31c0eb1 +2026-07-28-tool-call-file-open-in-os.zh.md: a658a7b55d2575ad1a5d8baccb80f0a8444d6db9 diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md index 73f5091888..499d3caa11 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md @@ -10,9 +10,9 @@ Chat tool rows treated the whole summary line as a click target that opened the ## Decision -File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as hover-underline links with a pointer cursor. Clicking the path calls `host.openPath` through `WorkspacesService.openPath`, resolving relative paths against the session cwd. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them. +File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `host.openPath` through `WorkspacesService.openPath`, resolving relative paths against the session cwd. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them. -`host.openPath` is a privileged unary RPC accepted only from loopback, same-origin browser requests (same carrier guard as `host.pickDirectory`). Platform adapters open without a shell: `open` on macOS, PowerShell `Invoke-Item` on Windows, and `xdg-open` on desktop Linux. WSL is a separate host shape despite Node reporting `linux`: the adapter recognizes its environment or Microsoft kernel release, translates the Linux path with `wslpath -w`, and passes the resulting Windows/UNC path to the same PowerShell handoff. The opener's platform facts and command runner are injectable for tests. URL-only read args (`web_fetch`) are not file links. +`host.openPath` is a privileged unary RPC accepted only from loopback, same-origin browser requests (same carrier guard as `host.pickDirectory`). Platform adapters open without a shell: `open` on macOS, PowerShell `Invoke-Item` on Windows, and `xdg-open` on desktop Linux; browser-renderable documents prefer the named default browser on macOS and desktop Linux. WSL is a separate host shape despite Node reporting `linux`: the adapter recognizes its environment or Microsoft kernel release, translates the Linux path with `wslpath -w`, and passes the resulting Windows/UNC path to the same PowerShell handoff. The opener's platform facts and command runner are injectable for tests. URL-only read args (`web_fetch`) are not file links. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md index c1bc93c472..a658a7b55d 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md @@ -10,9 +10,9 @@ Status: implemented ## 决策 -文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为悬停下划线链接并使用 pointer 光标。点击路径会经 `WorkspacesService.openPath` 调用 `host.openPath`,相对路径以会话 cwd 为基准解析。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。 +文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经 `WorkspacesService.openPath` 调用 `host.openPath`,相对路径以会话 cwd 为基准解析。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。 -`host.openPath` 是特权一元 RPC,仅接受来自回环地址且同源的浏览器请求(与 `host.pickDirectory` 相同的载体守卫)。平台适配器不经 shell 打开:macOS 为 `open`,Windows 为 PowerShell `Invoke-Item`,桌面 Linux 为 `xdg-open`。尽管 Node 将 WSL 报告为 `linux`,WSL 仍是一种独立的宿主形态:适配器根据其环境或 Microsoft 内核 release 识别它,用 `wslpath -w` 转换 Linux 路径,并将所得 Windows/UNC 路径交给同一 PowerShell 交接。打开器的平台信息和命令运行器可在测试中注入。仅含 URL 的 read 参数(`web_fetch`)不是文件链接。 +`host.openPath` 是特权一元 RPC,仅接受来自回环地址且同源的浏览器请求(与 `host.pickDirectory` 相同的载体守卫)。平台适配器不经 shell 打开:macOS 为 `open`,Windows 为 PowerShell `Invoke-Item`,桌面 Linux 为 `xdg-open`;浏览器可渲染的文档会在 macOS 与桌面 Linux 上优先使用能够确定的默认浏览器。尽管 Node 将 WSL 报告为 `linux`,WSL 仍是一种独立的宿主形态:适配器根据其环境或 Microsoft 内核 release 识别它,用 `wslpath -w` 转换 Linux 路径,并将所得 Windows/UNC 路径交给同一 PowerShell 交接。打开器的平台信息和命令运行器可在测试中注入。仅含 URL 的 read 参数(`web_fetch`)不是文件链接。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml new file mode 100644 index 0000000000..c1cc57b233 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml @@ -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/feature/2026-07-31-web-workspace-file-links.md +2026-07-31-web-workspace-file-links.md: fe71a2d8caa4dc9cc732a250d7e388d1a3b44182 +2026-07-31-web-workspace-file-links.zh.md: 0f8c87822087169aad9ac4283f706fd8a8748be9 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md new file mode 100644 index 0000000000..fe71a2d8ca --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md @@ -0,0 +1,35 @@ +# Agent Note: opening a produced file from the web UI + +Status: implemented + +English | [中文](2026-07-31-web-workspace-file-links.zh.md) + +> Scope: the produced-files row a finished turn ends with, the file-path link that reads as one, and the Host opener preferring the default browser for documents a browser renders. Not in scope, by decision: serving workspace files over HTTP, and previews for a client that is not on the Host machine. + +## Problem + +A web session that produced a file had no way to look at it. The agent wrote `deepseek-homepage.html`, said so, and the user's only recourse was to copy an absolute path like `/private/tmp/dsh-client-hotplug.ygPvsm/workspaces/plugin-hotplug/deepseek-homepage.html` into a terminal. + +Two distinct defects sat behind that. The transcript never said what a turn had produced: `ToolCallView.locations` — the follow-along vocabulary the file tools already populate — had no consumer in the client, so a reader's only account of the output was whatever the closing message happened to spell. And the affordance that did exist was invisible: `ToolRow` already renders a mutation or read row's path as a real button wired to `host.openPath`, but styled exactly like the surrounding prose and underlined only on hover, so nobody found it. The reported "I can't open what it made" was a discoverability failure sitting on top of a working capability. + +## Decision + +**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. One cordis.yml line composes the surface in or out; the unregistered hole renders nothing. + +**The path link reads as a link.** Underlined at rest, not only on hover. This is the smaller half of the diff and the larger half of the fix. + +**Opening stays the Host's job, and prefers the default browser.** `host.openPath` hands the path to the operating system, which yields a `file://` document in a real browser: full page capabilities, and no reachability into `/api`, because a `file://` document is not same-origin with it. Measured on the reported artifact: `localStorage` works, the theme toggle flips, the tabs switch, and `fetch` to the API fails. For documents a browser renders — `.html`, `.htm`, `.xhtml`, `.svg` — the opener resolves the default *browser* rather than the type's default application when the platform can name one, because a developer who binds `.html` to an editor would otherwise click a produced page and get source code. macOS reads the LaunchServices `https` handler and desktop Linux reads `$BROWSER`; either falls back to the default application when no browser can be named. Windows uses its registered association, and WSL first translates the path before using that same Windows handoff. + +**Serving workspace files over HTTP is out of scope, and so are non-local clients.** An earlier revision served files from the harness itself — first same-origin with `/api`, then behind `CSP: sandbox`, then from a second listener whose own port gave served documents their own origin. Each step answered a real problem, and the whole line was retired once the product scope was settled: previews for a browser that is not on the Host machine are not supported. With that decided, the Host opener answers the supported case completely and the HTTP machinery answered only the unsupported one. + +## Alternatives considered + +- **Serving `/f//` from the harness** — built and working, including confinement by dual `realpath`, the browser-trust fence, streamed reads, and a separate listener whose port gave served documents their own origin. It is the only design that shows a preview to a client on another machine, which is exactly the case ruled out of scope. Retired for that reason, not because it failed; its cost was a second socket with its own lifecycle, a port published into the page, and a URL-shape contract shared across two packages. +- **Same-origin HTTP serving without isolation** — measurably unsafe, and recorded so nobody retries it: a document served beside `/api` drove `settings.describe` to a `200` with full data and `session.list` to 35 KB of every session's transcript, from a page that need not be agent-authored at all (a read row makes every file in a cloned repository openable). +- **`Content-Security-Policy: sandbox` over that same-origin serving** — closes the hole by taking the document's origin away, which measurably breaks the pages this feature exists to show: the reported artifact throws `SecurityError` on load, and because an uncaught exception aborts the rest of its `