mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #733 from deepseek-harness/worktree/web-file-session-references
feat(web): add file and session references
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 .agents/notes/implemented/feature/2026-07-21-cross-session-references.md
|
||||
2026-07-21-cross-session-references.md: f49c75a9d4e3304b44d12a3df85430ffb494b8a9
|
||||
2026-07-21-cross-session-references.zh.md: 81a1970497bf175948a70bb1982f336f19d4d19e
|
||||
2026-07-21-cross-session-references.md: 774a948345e3d45adbba47ef6a7edd3e6f0740b2
|
||||
2026-07-21-cross-session-references.zh.md: fd864203d17954167646edf3b1a62946f8fa3f23
|
||||
|
||||
@@ -6,33 +6,35 @@ English | [中文](2026-07-21-cross-session-references.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
TUI users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, filtering by cited source-event seqs, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
|
||||
Web users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, provenance filtering, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferenceResolver`. Hosts normalize their protocol into `SessionReferenceInput[]` and call `prepare()` before delivery. The service returns detached readable content plus an optional identified, frozen `UserMessage` snapshot; core agent packages do not parse session URIs or read another log.
|
||||
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferenceResolver`. Its outer `agent/pre-step` listener parses canonical mentions in accepted direct user messages and calls `prepare()` without adding reference behavior to a host gateway. The service returns detached readable content plus an optional identified, frozen `UserMessage` snapshot; core agent packages do not parse session URIs or read another log.
|
||||
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)`; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. Web receives that URI inside the Host-produced `@[label](uri)` mention and keeps it behind an atomic session chip; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
|
||||
|
||||
The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live-preferred corpus observation, folds it with the session package's canonical surface algorithm, and returns a detached header, capture seq, and current nodes. FTS is not a dependency: discovery matches id, cwd, or the latest folded title, while message bodies remain outside the candidate layer. Non-empty queries batch title observations across the visible corpus with bounded persisted-log concurrency and cancellation; a dedicated title index can replace that discovery path without changing reference identity or preparation.
|
||||
|
||||
## Snapshot and projection
|
||||
|
||||
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partially prepared context: any read, cancellation, validation, or budget error rejects the operation before `followup()` or `steer()`. Cancellation races in-flight discovery and exact reads, so a host settles promptly even when a persistence backend cannot interrupt its pending operation; any late backend settlement is observed but cannot enqueue the message. A source is read before enqueue, so later source messages, compaction, deletion, or persistence replacement cannot change the target session.
|
||||
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partial context: any read, cancellation, validation, or budget error ends the turn before the accepted messages enter model-visible history. Cancellation races in-flight discovery and exact reads, so the listener settles promptly even when a persistence backend cannot interrupt its pending operation. A queued message captures each source when it reaches `agent/pre-step`; later source messages, compaction, deletion, or persistence replacement cannot change the context recorded in the target session.
|
||||
|
||||
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compaction`. That marker is part of the compaction capability contract rather than a backend package name. When a source prompt already contains baked prefix context, projection reads only its model-hidden display content, so referencing that target later does not recursively propagate an earlier snapshot. Projection excludes shadowed pre-compaction nodes, tools and results, reasoning, injected context, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
|
||||
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compaction`. That marker is part of the compaction capability contract rather than a backend package name. Reference snapshots remain separate sourced `user/message` events, so projection excludes them as injected context and never recursively propagates an earlier snapshot. Projection also excludes shadowed pre-compaction nodes, tools and results, reasoning, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
|
||||
|
||||
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags or escape the data region. The same serializer drives each source's independent byte accounting. AgentLoop persists the snapshot as a sourced `user/message` immediately before the direct `user/message`; target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type, placement mode, or prompt envelope.
|
||||
|
||||
## Message ownership
|
||||
|
||||
TUI owns the snapshot/direct-message transaction without extending the generic inbox record. While the agent is idle, it installs a one-shot outer `agent/pre-step` listener before `followup()`; an enter decision receives the snapshot as another message, while rejection or an earlier ordinary discard releases the listener and writes neither message. While the agent is running, TUI calls `inject(snapshot)` then `steer(prompt)`, placing both in the next-step inbox for the same later claim. A rejecting or failed pre-step leaves that claimed pair removed; messages inserted after the claim remain pending. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this generic delivery boundary.
|
||||
The service's outer `agent/pre-step` listener calls downstream listeners first and processes only an `enter` decision. It parses each accepted direct user message, preserves that message's id while replacing canonical mentions with readable labels, and inserts the frozen snapshot immediately before that message. Queue edits and queue-to-steer relocation need no reference-specific state because the final claimed messages are the input to preparation. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this context ordering.
|
||||
|
||||
Reference preparation is not a new steering protocol and does not create a turn by itself. Idle delivery uses `followup()` and pre-step entry; running delivery uses the shared next-step inbox while retaining snapshot order.
|
||||
Reference preparation is not a new delivery protocol and does not create a turn by itself. A preparation failure terminates the already accepted turn through the agent loop's existing plugin-failure path.
|
||||
|
||||
## Host adapters
|
||||
|
||||
TUI combines session candidates with the existing `@` file provider. Candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the readable direct content as the user message, and renders session-reference source metadata as a compact source list instead of exposing the complete JSON in the terminal.
|
||||
The unified Web `@` source combines session candidates with Host-backed file discovery. Session candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the request's cancellation signal, and session id, cwd, and mention labels escape external control characters while the canonical URI retains the original id.
|
||||
|
||||
Web exposes file and session discovery through generated Remote methods on their owning services, as detailed in [Web file and session references](2026-07-27-web-file-and-session-references.md). Session picks are atomic chips backed by the Host-produced canonical mention. Ordinary `session.prompt` delivery carries that mention without a reference-specific API Proxy route. Replay associates the separate session-reference context with its neighboring direct message and renders a compact source summary instead of exposing the snapshot JSON.
|
||||
|
||||
The [automation-only ACP transport](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately does not mount session-query or session-reference services.
|
||||
|
||||
@@ -45,16 +47,16 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
|
||||
- **Wait for SQLite FTS5** — rejected because snapshot correctness requires exact id reads and canonical surface folding, not content search. FTS improves discovery only.
|
||||
- **Put mention syntax in agent delivery methods** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
|
||||
- **Implement references separately in each host** — rejected because projection, security warning, retention, and persistence would drift across hosts.
|
||||
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. A domain-specific admission wrapper and the existing next-step inbox preserve the required pairing without enlarging every direct prompt.
|
||||
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets TUI hide background bytes from the direct user bubble.
|
||||
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. The domain listener can prepare the final claimed message without enlarging every direct prompt.
|
||||
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets Web hide background bytes from the direct user bubble.
|
||||
- **Replay the raw source log or restore shadowed events** — rejected because compact defines the current model surface and may intentionally retire sensitive or expensive history.
|
||||
- **Resume or fork the source** — rejected because the feature supplies read-only background for one target message, not identity or lifecycle continuity.
|
||||
- **Inject at request time by rereading the source** — rejected because the reference would become nondeterministic, cancellation races could alter its bytes, and target replay would depend on external mutable state.
|
||||
- **Reread the source after the model step enters** — rejected because target replay would depend on external mutable state instead of the logged snapshot.
|
||||
|
||||
## Verification
|
||||
|
||||
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, terminal-control escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, prompt blocking, admission-time staging, send/steer placement, title isolation, missing capability, and compact TUI replay. One keyless terminal snapshot types a title-only substring against an opaque session id and pins the rendered candidate. Another keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains a sourced snapshot message followed by the readable current prompt, without either shadowed string.
|
||||
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, control-character escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, cancellation against a non-settling storage read, independent per-source byte retention, frozen message ownership, pre-step parsing and insertion, downstream rejection, node-owned replay association, title isolation, and the generated Remote discovery faces. A keyless Web snapshot pins the assembled reference selection path.
|
||||
|
||||
## Consequences
|
||||
|
||||
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard TUI demo bundle mounts it explicitly and exposes its count and per-source byte limits in its config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
|
||||
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard CLI composition mounts it explicitly for Web and exposes its count and per-source byte limits in config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
|
||||
|
||||
@@ -6,33 +6,35 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
TUI 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、按被引用来源事件 seq 过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
|
||||
Web 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、来源过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferenceResolver` 上的单一上下文消费服务。宿主先把各自的协议规范化为 `SessionReferenceInput[]`,并在交付前调用 `prepare()`。该服务返回分离的可读内容和一份可选的、带标识且冻结的 `UserMessage` 快照;核心 agent 包既不解析会话 URI,也不读取其他日志。
|
||||
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferenceResolver` 上的单一上下文消费服务。它的外层 `agent/pre-step` 监听器会解析已接受直接用户消息中的规范 mention,并调用 `prepare()`,宿主网关无需添加引用行为。该服务返回分离的可读内容和一份可选的、带标识且冻结的 `UserMessage` 快照;核心 agent 包既不解析会话 URI,也不读取其他日志。
|
||||
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` 中;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。Web 接收由 Host 生成、包含该 URI 的 `@[label](uri)` 提及标记,并把它封装为原子 session chip;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
|
||||
|
||||
该服务使用 `ctx.sessionQuery.readSurface(sessionId)`:它优先从实时会话加载一次语料观察结果,使用会话包的规范表层算法执行折叠,并返回与源数据分离的会话头、捕获序号和当前节点。FTS 不是依赖项:候选发现会匹配 id、cwd 或最新折叠后的标题,而消息主体不进入候选层。非空查询会对可见语料中的标题观察结果执行批处理,以有界并发读取持久化日志,并支持取消;专用标题索引可以替换这条发现路径,而无需改变引用标识或准备过程。
|
||||
|
||||
## 快照与投影
|
||||
|
||||
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分完成的上下文:任何读取、取消、校验或预算错误都会在调用 `followup()` 或 `steer()` 前拒绝本次操作。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,宿主也能及时结束等待;后端迟到的完成结果仍会被观察,但不能让消息入队。源会话在入队前完成读取,因此源会话后续新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中的快照。
|
||||
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分上下文:任何读取、取消、校验或预算错误都会在已接受消息进入面向模型的历史之前结束该轮次。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,监听器也能及时结束等待。queued 消息到达 `agent/pre-step` 时会捕获每个源;此后源会话新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中记录的上下文。
|
||||
|
||||
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compaction` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力约定的一部分,而非某个后端包名称。当源提示词已包含合并写入的前缀上下文时,投影只读取其模型不可见的显示内容,因此后续引用该目标不会递归传播先前的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、注入的上下文、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
|
||||
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compaction` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力约定的一部分,而非某个后端包名称。引用快照始终是独立且带来源的 `user/message` 事件,因此投影会把它们作为注入上下文排除,绝不递归传播早先的快照。投影还会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
|
||||
|
||||
系统把一个聚合上下文序列化为 JSON,并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求,除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。AgentLoop 会把快照持久化为一条带来源信息的 `user/message`,紧接在直接 `user/message` 之前。因此,目标回放无需新增事件类型、放置模式或提示词封套,也能满足「模型可见/日志可重建」不变量。
|
||||
|
||||
## 消息所有权
|
||||
|
||||
TUI 负责快照/直接消息事务,不扩展通用收件箱记录。agent 空闲时,它会在调用 `followup()` 前安装一次性的外层 `agent/pre-step` 监听器;enter 决策会把快照作为另一条消息接收,而 reject 或更早的普通丢弃会释放监听器,并且不写入任何消息。agent 运行时,TUI 会依次调用 `inject(snapshot)` 和 `steer(prompt)`,把两者放入 next-step inbox,等待后续同一次领取。pre-step reject 或失败会使这对已领取消息维持已移除状态;领取后插入的消息继续等待。这一通用交付边界由[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定。
|
||||
该服务的外层 `agent/pre-step` 监听器会先调用下游监听器,并且只处理 `enter` 决策。它会解析每条已接受的直接用户消息,在把规范 mention 替换为可读标签时保留消息 id,并把冻结快照插入到该消息紧前。最终领取的消息是准备过程的输入,因此队列编辑和从 queue 移动到 steer 不需要引用专用状态。[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定了这一上下文顺序。
|
||||
|
||||
引用准备过程不是新的 steering 协议,本身也不会创建轮次。空闲交付使用 `followup()` 和 pre-step 进入决策;运行期间的交付使用共享 next-step inbox,并保持快照顺序。
|
||||
引用准备过程不是新的投递协议,本身也不会创建轮次。准备失败会通过 agent loop 的现有插件失败路径终止已经接受的轮次。
|
||||
|
||||
## 宿主适配器
|
||||
|
||||
TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。候选查询遵循编辑器的取消信号;session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把可读的直接内容渲染为用户消息,并把会话引用来源元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
|
||||
统一的 Web `@` source 把会话候选与 Host 支持的文件发现组合在一起。会话候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。查询遵循请求的取消信号;session id、cwd 和提及标签中的外部控制字符会被转义,但规范 URI 仍保留原始 id。
|
||||
|
||||
Web 通过所属服务上的生成 Remote 方法提供文件与会话发现,详见 [Web 文件与会话引用](2026-07-27-web-file-and-session-references.md)。session 选择项是由 Host 生成的规范 mention 支撑的原子 chip。普通 `session.prompt` 投递会携带该 mention,无需引用专用 API Proxy 路由。回放会把独立的 session-reference 上下文与相邻直接消息关联起来,并渲染精简来源摘要,而不暴露快照 JSON。
|
||||
|
||||
[仅面向自动化的 ACP(Agent Client Protocol)传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。
|
||||
|
||||
@@ -45,16 +47,16 @@ TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询
|
||||
- **等待 SQLite FTS5**:不予采纳,因为快照正确性依赖按准确 id 读取和规范表层折叠,而不是内容搜索。FTS 只改进候选发现。
|
||||
- **把提及标记语法放入 agent 投递方法**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
|
||||
- **在每个宿主中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
|
||||
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域专用的准入包装层和现有 next-step inbox 可以保持所需配对,而无需扩大每条直接提示词。
|
||||
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 TUI 从直接用户气泡中隐藏背景字节。
|
||||
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域监听器可以准备最终领取的消息,无需扩大每条直接提示词。
|
||||
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 Web 从直接用户气泡中隐藏背景字节。
|
||||
- **回放原始源日志或恢复被遮蔽的事件**:不予采纳,因为压缩定义了当前模型表层,并且可能有意淘汰敏感或开销高昂的历史内容。
|
||||
- **恢复或 fork 源会话**:不予采纳,因为本功能只为一条目标消息提供只读背景,不提供身份或生命周期连续性。
|
||||
- **在请求时重新读取源会话并注入**:不予采纳,因为这会让引用变得不确定,取消竞态可能改变其字节内容,目标回放也会依赖可变的外部状态。
|
||||
- **模型步骤进入后重新读取源会话**:不予采纳,因为目标回放会依赖可变的外部状态,而不是已记录的快照。
|
||||
|
||||
## 验证
|
||||
|
||||
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 id/cwd/标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、终端控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、提示词阻止、准入期间的暂存、send/steer 放置方式、标题隔离、能力缺失和压缩场景下的 TUI 回放。一个无密钥终端快照会在会话 id 不透明的情况下输入一个只与标题匹配的子串,并固定渲染出的候选项。另一个无密钥 TUI 快照会运行真实的 agent loop(智能体循环):源表层用一个压缩检查点替换旧的用户/assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条带来源的快照消息,后面跟随可读的当前提示词,并且不包含任一被遮蔽的字符串。
|
||||
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 id/cwd/标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时的取消、逐源独立字节保留、冻结消息所有权、pre-step 解析和插入、下游拒绝、节点负责的回放关联、标题隔离,以及生成的 Remote 发现接口。一个无密钥 Web 快照会固定组装后的引用选择路径。
|
||||
|
||||
## 后果
|
||||
|
||||
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 TUI 演示组合包会显式挂载它,并在自身配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
|
||||
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 CLI 组合会为 Web 显式挂载它,并在配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
|
||||
|
||||
@@ -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-27-web-file-and-session-references.md
|
||||
2026-07-27-web-file-and-session-references.md: ad8e5c53832a567bd38d1d1e560122cb8b630daa
|
||||
2026-07-27-web-file-and-session-references.zh.md: acb016866efc42ef3ea9f661cb10ee1459cf1a6b
|
||||
@@ -0,0 +1,49 @@
|
||||
# Agent Note: Web file and session references
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-web-file-and-session-references.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web composer had a reusable slash/reference trigger pipeline, but its `@` source was inert subagent-label text. Web needed Host-backed workspace-path discovery and structured cross-session snapshots without scanning the Host filesystem in the browser or binding session identity to a display label.
|
||||
|
||||
## Decision
|
||||
|
||||
Web exposes one combined `@file` and `@session` menu through `@deepseek-ai/dsh-client-ui-reference`. For each unquoted query it starts both Remote discovery calls concurrently and deterministically orders files before sessions with locale-registered labels; non-selectable file and session section headings distinguish the two contiguous candidate sections without entering the keyboard-selection index. An open quoted token searches files only. Either candidate domain may fail independently without hiding successful rows from the other.
|
||||
|
||||
The file capability follows the three-package seam: `@deepseek-ai/dsh-file-reference` owns `ctx.fileReferences`, the shared `@path` token grammar, candidate shape, and stable model guidance; `@deepseek-ai/dsh-file-reference-local` owns bounded per-agent Host-filesystem indexes, invalidation, and scoped prompt installation; `dsh-client-ui-reference` consumes the generated Remote namespaces and shared grammar. A file pick remains path-only prompt text and a directory pick retriggers completion below its trailing slash.
|
||||
|
||||
A session pick is an atomic composer reference. Its visible label is presentation, while its hidden value and clipboard form are the canonical `@[label](dsh-session:…)` mention produced by the Host. Ordinary `session.prompt` delivery carries that mention unchanged. The session-reference service parses accepted direct user messages at `agent/pre-step`, captures every source, replaces the canonical mention with readable text while preserving the direct message id, and inserts the frozen snapshot immediately before that message. The API Proxy contains no reference-specific route, dependency, or error code.
|
||||
|
||||
The input machine keeps ordinary draft text and atomic references until the default sink reports Host acceptance. Serialization or prompt transport failure returns the same draft to editing. After acceptance, reference preparation belongs to the agent turn; a malformed mention, failed source read, cancellation, or budget failure terminates that turn. The logged prompt remains the replay authority. The concrete user and steering chat-node definition associates labels from an immediately preceding session-reference context, so the renderer receives the association from its own node data and shows a compact source summary instead of snapshot JSON.
|
||||
|
||||
## Reference transaction
|
||||
|
||||
```text
|
||||
type @ → parallel file/session Remote calls → pick path text or canonical session chip
|
||||
→ serialize draft → ordinary session.prompt enqueue
|
||||
→ agent/pre-step parses mentions → capture sources → context + readable prompt
|
||||
```
|
||||
|
||||
File lookup is advisory and cancellable; selection itself performs no read. Session preparation is all-or-nothing for one accepted model step. A queued message captures each source when the message is claimed, so queue edits and queue-to-steer relocation use the same path without gateway coordination.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Implement file discovery and grammar inside the Web client.** Rejected because browser-side code cannot safely access the Host workspace, while duplicating grammar, ranking, bounds, and invalidation would drift from the Host provider.
|
||||
|
||||
**Scan files through ordinary filesystem-tool RPCs.** Rejected because recursive fuzzy discovery is editor latency work, not a model-facing exact filesystem operation, and would couple the menu to tool policy and provider round trips.
|
||||
|
||||
**Eagerly attach selected file contents.** Rejected because selection would spend context before relevance is known and bypass the logged, auditable `read` call/result sequence.
|
||||
|
||||
**Represent sessions as plain `@label` text.** Rejected because labels are neither stable nor unique and cannot identify the source snapshot. Canonical Host-produced mentions preserve opaque session identity while keeping a readable display.
|
||||
|
||||
**Clear the composer before prompt admission settles.** Rejected because a transport or admission failure would lose the only editable copy of the request and visually claim acceptance that never occurred.
|
||||
|
||||
## Verification
|
||||
|
||||
Package tests pin shared file grammar and ranking, cache invalidation and lifecycle cleanup, parallel Web lookup, quoted paths, independent candidate failure, cancellation, grouped headings that do not alter option indexes, file/directory continuation, canonical session chips, adjacent-reference and adjacent-text reference projection, codec round-trip, generated Remote type inference, pre-step preparation, downstream rejection, and chat-node-owned label association. The keyless assembled Web snapshot renders the available reference sections, selects a file, then selects a session reference through the real client composition.
|
||||
|
||||
## Consequences
|
||||
|
||||
Web now uses the shared `@file` discovery seam and structured session-reference identity, while Host services remain the authority for filesystem and session access. File and session discovery are unary Remote contracts on the owning services, so generated client types replace handwritten RPC interfaces and browser bundles remain free of Node APIs. Candidate lookup failures remain quiet menu degradation. Reference preparation failures occur after prompt acceptance and end the agent turn. File references cost only path text plus stable conditional guidance, whereas session references retain the bounded snapshot cost and trust framing owned by `dsh-session-reference`.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Agent Note: Web 文件与会话引用
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-web-file-and-session-references.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 输入框已有可复用的斜杠命令/引用触发流水线,但它的 `@` source 只是不会产生实际作用的 subagent 标签文本。Web 需要由宿主提供工作区路径发现和结构化跨会话快照,同时避免在浏览器中扫描宿主文件系统或把会话身份绑定到显示标签。
|
||||
|
||||
## 决策
|
||||
|
||||
Web 通过 `@deepseek-ai/dsh-client-ui-reference` 暴露一个合并的 `@file` 与 `@session` 菜单。每次处理未加引号的查询时,它会并发启动两项 Remote 发现调用,以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的标签;不可选择的文件与会话分组标题会区分两个连续的候选分组,且不会进入键盘选择索引。尚未闭合的带引号 token 只搜索文件。任一候选领域都可以独立失败,不会隐藏另一领域成功返回的行。
|
||||
|
||||
文件功能遵循由三个包构成的 seam:`@deepseek-ai/dsh-file-reference` 拥有 `ctx.fileReferences`、共享 `@path` token 语法、候选形状和稳定的模型指引;`@deepseek-ai/dsh-file-reference-local` 拥有每个 agent(智能体)有界的宿主文件系统索引、失效处理和作用域内的提示词安装;`dsh-client-ui-reference` 消费生成的 Remote 命名空间与共享语法。选择文件后仍只会把路径文本写入提示词,选择目录则会在其尾部斜杠后重新触发补全。
|
||||
|
||||
选择会话会创建一个原子的输入框引用。可见标签只用于呈现,隐藏值和剪贴板形式则是宿主生成的规范 `@[label](dsh-session:…)` mention。普通 `session.prompt` 投递会原样携带该 mention。session-reference 服务会在 `agent/pre-step` 解析已接受的直接用户消息,捕获每个源,在保留直接消息 id 的同时把规范 mention 替换为可读文本,并把冻结快照插入到该消息紧前。API Proxy 不包含引用专用路由、依赖或错误码。
|
||||
|
||||
输入状态机在默认 sink 报告宿主已接受前,会保留普通草稿文本和原子引用。序列化或提示词传输失败后,同一草稿会回到可编辑状态。接受后,引用准备属于 agent 轮次;格式错误的 mention、源读取失败、取消或预算失败会终止该轮次。已记录的提示词仍是回放权威。具体的 user 和 steering chat-node 定义会关联紧邻前一条 session-reference 上下文中的标签,因此渲染器会从自身节点数据接收关联信息,并显示精简的来源摘要,而不是快照 JSON。
|
||||
|
||||
## 引用事务
|
||||
|
||||
```text
|
||||
type @ → parallel file/session Remote calls → pick path text or canonical session chip
|
||||
→ serialize draft → ordinary session.prompt enqueue
|
||||
→ agent/pre-step parses mentions → capture sources → context + readable prompt
|
||||
```
|
||||
|
||||
文件查询仅供参考且可取消;选择操作本身不会读取文件。会话准备针对一个已接受的模型步骤保持全有或全无。queued 消息被领取时会捕获每个源,因此队列编辑和从 queue 移动到 steer 使用同一路径,无需网关协调。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**在 Web 客户端内部实现文件发现与语法。** 不予采纳,因为浏览器侧代码无法安全访问宿主工作区,而且重复的语法、排序、边界和失效处理会与宿主提供方产生偏差。
|
||||
|
||||
**通过普通文件系统工具 RPC 扫描文件。** 不予采纳,因为递归模糊发现属于编辑器低延迟工作,而不是面向模型的精确文件系统操作;该方案还会把菜单与工具策略及提供方往返绑定。
|
||||
|
||||
**选择文件时立即附加其内容。** 不予采纳,因为该方案会在尚未确定相关性时消耗上下文,并绕过可从日志重建、可审计的 `read` 调用/结果序列。
|
||||
|
||||
**用普通 `@label` 文本表示会话。** 不予采纳,因为标签既不稳定也不唯一,无法标识源快照。宿主生成的规范提及标记既能保留不透明会话身份,也能保持显示内容易读。
|
||||
|
||||
**提示词准入结算前清空输入框。** 不予采纳,因为传输或准入失败会丢失请求唯一可编辑的副本,并在视觉上错误表示一个从未成功的接受操作。
|
||||
|
||||
## 验证
|
||||
|
||||
包(package)测试固定共享文件语法和排序、缓存失效及生命周期清理、Web 并行查询、带引号的路径、候选项独立失败、取消、不改变候选项索引的分组标题、文件/目录继续补全、规范会话 chip、相邻引用及相邻文本条件下的引用投影、codec 无损往返、生成的 Remote 类型推断、pre-step 准备、下游拒绝,以及 chat node 自有的标签关联。无密钥的装配 Web 快照会渲染可用的引用分组,并通过真实客户端组合依次选择文件和会话引用。
|
||||
|
||||
## 后果
|
||||
|
||||
Web 现在使用共享的 `@file` 发现 seam 和结构化会话引用身份,宿主服务仍然是文件系统与会话访问的权威来源。文件和会话发现都是所属服务上的一元 Remote 契约,因此生成的客户端类型会替代手写 RPC 接口,浏览器 bundle 中也不包含 Node API。候选查询失败仍会让菜单静默降级。引用准备失败发生在提示词已接受之后,并会结束 agent 轮次。文件引用只产生路径文本和稳定的条件式指引成本,而会话引用仍保留 `dsh-session-reference` 所拥有的有界快照开销与信任限定文本。
|
||||
@@ -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-27-web-subagent-conversations.md
|
||||
2026-07-27-web-subagent-conversations.md: cdf2b12e69b359b8ab00d4b107aa78a21691ac4e
|
||||
2026-07-27-web-subagent-conversations.zh.md: 697bfeef86e2ef456aed6ee6ad8eae1173c96a6f
|
||||
2026-07-27-web-subagent-conversations.md: d20e55429d04d4308ae26d001575e664058c7dea
|
||||
2026-07-27-web-subagent-conversations.zh.md: 67c9b5224abea6c0710303ae56e315ba34c7ac09
|
||||
|
||||
@@ -85,7 +85,7 @@ The shipped Web composition mounts SQLite session query beside JSONL persistence
|
||||
|
||||
**Put the adapter in the webserver.** Rejected because catalog and continuation are channel-independent client capabilities; the webserver only carries validated messages.
|
||||
|
||||
**Create a new UI package.** Rejected because `ui-subagent` already owns Web subagent references and is the coherent owner for catalog and addressed-child presentation.
|
||||
**Put Host-backed file and session references in this package.** Rejected because catalog and addressed-child presentation depend on subagent lineage, while combined reference discovery is a separate Host capability consumed by [`ui-reference`](../../../../packages/client/ui-reference/README.md).
|
||||
|
||||
**Auto-resume an absent parent.** Rejected because continuation requires the exact live direct parent. Child navigation must not mutate the parent lifecycle.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ one-shot 行始终会用文案替代输入框,说明执行记录为只读。
|
||||
|
||||
**将适配器放入 webserver。** 不予采纳,因为目录与继续执行是通道无关的客户端能力;webserver 只承载已校验的消息。
|
||||
|
||||
**新建 UI 包。** 不予采纳,因为 `ui-subagent` 已经负责 Web subagent 引用,也是目录与已寻址 child 呈现的统一 owner。
|
||||
**把由 Host 支撑的文件与会话引用放进本包。** 不予采纳,因为目录与已寻址 child 呈现依赖 subagent 谱系,而组合引用发现是独立的 Host 功能,由 [`ui-reference`](../../../../packages/client/ui-reference/README.md) 消费。
|
||||
|
||||
**自动恢复缺失的 parent。** 不予采纳,因为继续执行要求确切的存活直接 parent。child 导航不得改变 parent 生命周期。
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Web e2e scenario: the shipped composition discovers local files and cold
|
||||
// sessions through the real Host, groups both domains in the shared @ menu,
|
||||
// and projects each pick back into the composer without issuing a model call.
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
SESSION_FORMAT_VERSION,
|
||||
Session,
|
||||
SessionId,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-session-title'
|
||||
import {
|
||||
assertFixtureInventory,
|
||||
captureStableAria,
|
||||
compareOrRefreshGolden,
|
||||
launchWebScaffold,
|
||||
seedSession,
|
||||
watchConsole,
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/reference-composer', import.meta.url))
|
||||
const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SOURCE_SESSION_ID = 'reference-source-session'
|
||||
|
||||
/** Build one closed source session with a stable title for reference discovery. */
|
||||
function sourceSessionFixture(): string {
|
||||
const session = Session.create(SessionId(SOURCE_SESSION_ID))
|
||||
session.append('turn/start', {
|
||||
turn: 1,
|
||||
})
|
||||
const user = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'Research context for the reference menu.' }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
session.append('session/title', {
|
||||
title: 'Research notes',
|
||||
messageSeqs: [user.seq],
|
||||
source: { kind: 'fallback' },
|
||||
})
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
return [
|
||||
JSON.stringify({
|
||||
type: 'session',
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: '{{sessionId}}',
|
||||
createdAt: 0,
|
||||
cwd: '{{cwd}}',
|
||||
}),
|
||||
...session.events.map(event => JSON.stringify(event)),
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: file and session references through the real host', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSession(scaffold, sourceSessionFixture(), SOURCE_SESSION_ID)
|
||||
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)
|
||||
await writeFile(join(scaffold.workspaceCwd, 'workspace', 'reference.txt'), 'reference fixture\n')
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('groups both sources and projects file text plus an atomic session chip', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-reference-composer'))
|
||||
const input = page.locator('textarea').first()
|
||||
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
|
||||
|
||||
await input.fill('@')
|
||||
await expect.poll(() => menu.getByRole('option').count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(2)
|
||||
const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
|
||||
expect(snapshot).toContain('Files & folders')
|
||||
expect(snapshot).toContain('Session conversations')
|
||||
expect(snapshot).toContain('File \u00b7 reference.txt')
|
||||
expect(snapshot).toContain('Session \u00b7 Research notes')
|
||||
expect(snapshot).not.toContain('text: Subagents')
|
||||
|
||||
await input.fill('@reference')
|
||||
await menu.getByRole('option', { name: /File \u00b7 reference\.txt/ }).click()
|
||||
await expect.poll(() => input.inputValue()).toBe('@reference.txt ')
|
||||
|
||||
await input.fill('@Research')
|
||||
await menu.getByRole('option', { name: /Session \u00b7 Research notes/ }).click()
|
||||
await expect.poll(() => page.locator('[data-decoration="chip"]').textContent()).toBe('@Research notes')
|
||||
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['menu.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -856,8 +856,9 @@ function normalizeAria(snapshot: string, workspaceCwd: string): string {
|
||||
// Seeded compaction prices realized file paths, whose length differs
|
||||
// between local worktrees and CI scratch directories.
|
||||
.replace(/(Compacted \d+ history items \(~)\d+( tokens\))/g, '$1{{tokens}}$2')
|
||||
// Message IconActions clocks widen by calendar day/year; collapse every
|
||||
// format so goldens stay stable across midnight and year changes.
|
||||
// Session summaries and Message IconActions clocks cross calendar
|
||||
// boundaries; collapse every shape so goldens stay stable across them.
|
||||
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, '{{timestamp}}')
|
||||
.replace(/\d{4}年\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
|
||||
.replace(/\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
|
||||
.replace(/(?<!\d)\d{1,2}:\d{2}:\d{2}(?:\.\d+)?(?:\s*[AP]M)?(?!\d)/gi, '{{clock}}')
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- listbox "Trigger suggestions":
|
||||
- text: reference Files & folders
|
||||
- option "File · reference.txt reference.txt" [selected]
|
||||
- text: Session conversations
|
||||
- option "Session · Research notes reference-source-session · {{cwd}} · {{timestamp}}"
|
||||
@@ -106,9 +106,8 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
// this exact occurrence into the current turn's steering outbox.
|
||||
await input.fill(STEER)
|
||||
await input.press('Enter')
|
||||
const queued = page.getByText(STEER, { exact: true })
|
||||
await queued.waitFor({ timeout: 10_000 })
|
||||
const queuedRow = page.getByRole('listitem').filter({ hasText: STEER })
|
||||
await queuedRow.waitFor({ timeout: 10_000 })
|
||||
const steerButton = queuedRow.getByRole('button', { name: 'Steer queued message' })
|
||||
await expect.poll(() => steerButton.isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
await steerButton.click({ timeout: 10_000 })
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"tests/message-feedback.e2e.ts",
|
||||
"tests/message-feedback-layout.e2e.ts",
|
||||
"tests/markdown-images.e2e.ts",
|
||||
"tests/reference-composer.e2e.ts",
|
||||
"tests/math-rendering.e2e.ts",
|
||||
"tests/markdown-cjk-strong.e2e.ts",
|
||||
"tests/markdown-inline-code-links.e2e.ts",
|
||||
|
||||
@@ -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 docs/capability-seams.md
|
||||
capability-seams.md: 710c399510b6b123123e0a9586d86bfc3a96dff9
|
||||
capability-seams.zh.md: e0559d464ba1ecd2160eaba40c24bfd829e4a0d6
|
||||
capability-seams.md: 23fc498cc280518489aadbe752a1702d43f17972
|
||||
capability-seams.zh.md: 0a8822bc8d1f0296ac6698b59fda60666faaf54a
|
||||
|
||||
@@ -66,6 +66,9 @@ flowchart LR
|
||||
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
|
||||
pkg_session_reference["session-reference"]
|
||||
pkg_tool_session_query["tool-session-query"]
|
||||
pkg_file_reference["file-reference"]
|
||||
svc_fileReferences["ctx.fileReferences<br/>File reference discovery"]
|
||||
pkg_file_reference_local["file-reference-local"]
|
||||
svc_sessionReferenceResolver["ctx.sessionReferenceResolver<br/>Cross-session snapshot preparation"]
|
||||
pkg_session_title["session-title"]
|
||||
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
|
||||
@@ -222,6 +225,8 @@ flowchart LR
|
||||
pkg_directory_picker_browse --> svc_directoryPicker
|
||||
pkg_directory_picker_native --> svc_directoryPicker
|
||||
pkg_e2b --> svc_e2b
|
||||
pkg_file_reference --> svc_fileReferences
|
||||
pkg_file_reference_local --> svc_fileReferences
|
||||
pkg_fs --> svc_fs
|
||||
pkg_fs_e2b --> svc_fs
|
||||
pkg_fs_local --> svc_fs
|
||||
@@ -433,6 +438,7 @@ flowchart LR
|
||||
| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. |
|
||||
| `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. |
|
||||
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. |
|
||||
| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | The interface returns path-only completion candidates within the addressed Agent cwd through its unary Remote contract; providers own namespace access and ranking without reading file contents. |
|
||||
| `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
|
||||
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. |
|
||||
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
|
||||
|
||||
@@ -68,6 +68,9 @@ flowchart LR
|
||||
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
|
||||
pkg_session_reference["session-reference"]
|
||||
pkg_tool_session_query["tool-session-query"]
|
||||
pkg_file_reference["file-reference"]
|
||||
svc_fileReferences["ctx.fileReferences<br/>File reference discovery"]
|
||||
pkg_file_reference_local["file-reference-local"]
|
||||
svc_sessionReferenceResolver["ctx.sessionReferenceResolver<br/>Cross-session snapshot preparation"]
|
||||
pkg_session_title["session-title"]
|
||||
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
|
||||
@@ -224,6 +227,8 @@ flowchart LR
|
||||
pkg_directory_picker_browse --> svc_directoryPicker
|
||||
pkg_directory_picker_native --> svc_directoryPicker
|
||||
pkg_e2b --> svc_e2b
|
||||
pkg_file_reference --> svc_fileReferences
|
||||
pkg_file_reference_local --> svc_fileReferences
|
||||
pkg_fs --> svc_fs
|
||||
pkg_fs_e2b --> svc_fs
|
||||
pkg_fs_local --> svc_fs
|
||||
@@ -435,6 +440,7 @@ flowchart LR
|
||||
| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 |
|
||||
| `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 |
|
||||
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 |
|
||||
| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | 该接口通过其一元 Remote 契约返回指定 Agent cwd 内仅含路径的补全候选;提供方负责命名空间访问和排序,但不会读取文件内容。 |
|
||||
| `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 |
|
||||
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 |
|
||||
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 |
|
||||
|
||||
@@ -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 docs/config-catalog.md
|
||||
config-catalog.md: 659fe4a89bd36a6b1e7446be07e3e72022fa5d0f
|
||||
config-catalog.zh.md: 8b180c96d7d22f13253bc28c580343ccec051a1c
|
||||
config-catalog.md: 09ad73fa708fc526060598421286223d3ef4c955
|
||||
config-catalog.zh.md: 38155b91b92902fd0d19e2769e37225f894f2302
|
||||
|
||||
@@ -587,6 +587,26 @@ export interface Config {
|
||||
|
||||
Source: [`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-file-reference-local"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-file-reference-local`
|
||||
|
||||
Requires: `agents`
|
||||
|
||||
```ts config-catalog
|
||||
/** Local file-reference discovery configuration. */
|
||||
export interface Config {
|
||||
/** Maximum ranked candidates returned for one query. */
|
||||
maxResults?: number
|
||||
/** Maximum indexed files and directories per agent workspace. */
|
||||
maxEntries?: number
|
||||
/** Directory basenames never traversed or offered. */
|
||||
excludedDirectories?: string[]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-fs-local"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-fs-local`
|
||||
@@ -3179,6 +3199,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
- `@deepseek-ai/dsh-client-ui-model-selection` ([`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-permission-presets` ([`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-reference` ([`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-renderer` ([`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
|
||||
@@ -3233,6 +3254,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
|
||||
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
||||
- `@deepseek-ai/dsh-compaction` — abstract `CompactionEngine` ([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
|
||||
- `@deepseek-ai/dsh-credentials` — abstract `CredentialProvider` ([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
|
||||
- `@deepseek-ai/dsh-file-reference` — abstract `FileReferenceService` ([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
|
||||
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
|
||||
- `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
|
||||
- `@deepseek-ai/dsh-jobs` — abstract `JobRegistry` ([`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts))
|
||||
|
||||
@@ -589,6 +589,26 @@ export interface Config {
|
||||
|
||||
来源:[`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-file-reference-local"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-file-reference-local`
|
||||
|
||||
需要:`agents`
|
||||
|
||||
```ts config-catalog
|
||||
/** Local file-reference discovery configuration. */
|
||||
export interface Config {
|
||||
/** Maximum ranked candidates returned for one query. */
|
||||
maxResults?: number
|
||||
/** Maximum indexed files and directories per agent workspace. */
|
||||
maxEntries?: number
|
||||
/** Directory basenames never traversed or offered. */
|
||||
excludedDirectories?: string[]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-fs-local"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-fs-local`
|
||||
@@ -3183,6 +3203,7 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-client-ui-model-selection`([`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-permission-presets`([`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-plan`([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-reference`([`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-renderer`([`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings`([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings-general`([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
|
||||
@@ -3237,6 +3258,7 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-code-runtime` — 抽象 `CodeRuntime`([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
||||
- `@deepseek-ai/dsh-compaction` — 抽象 `CompactionEngine`([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
|
||||
- `@deepseek-ai/dsh-credentials` — 抽象 `Credentials`([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
|
||||
- `@deepseek-ai/dsh-file-reference` — 抽象 `FileReferenceService`([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
|
||||
- `@deepseek-ai/dsh-fs` — 抽象 `FileSystem`([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
|
||||
- `@deepseek-ai/dsh-host-directory-picker` — 抽象 `DirectoryPicker`([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
|
||||
- `@deepseek-ai/dsh-jobs` — 抽象 `JobRegistry`([`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts))
|
||||
|
||||
@@ -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 docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: c906ee6329fac66e7391c266213dd150dd5b8e09
|
||||
event-producer-consumer.zh.md: 77bf401b7215bd263c0d84f04e0eabe6b28b7915
|
||||
event-producer-consumer.md: 2e4bec93f5b9f68ab885d50bab27e9a9a6036028
|
||||
event-producer-consumer.zh.md: e3f5602287d7b688a2191937871e1639dc271e65
|
||||
|
||||
@@ -9,13 +9,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
|
||||
@@ -40,7 +40,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
|
||||
@@ -42,7 +42,7 @@
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
|
||||
@@ -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 docs/module-graph.md
|
||||
module-graph.md: 38d3acc73175d504dadf22e4a5317e5f84ff5be8
|
||||
module-graph.zh.md: 76880c34aef07f877147a90f62dacd2b6877de41
|
||||
module-graph.md: 46a8ac8380692fb57ad7234e4509a36a8851aae9
|
||||
module-graph.zh.md: 68e89b7375b68c1cda35a72db31add4c71dd6e39
|
||||
|
||||
+29
-3
@@ -139,6 +139,7 @@ flowchart TD
|
||||
pkg_client_ui_permission_presets["client-ui-permission-presets"]
|
||||
pkg_client_ui_plan["client-ui-plan"]
|
||||
pkg_client_ui_primitives["client-ui-primitives"]
|
||||
pkg_client_ui_reference["client-ui-reference"]
|
||||
pkg_client_ui_renderer["client-ui-renderer"]
|
||||
pkg_client_ui_settings["client-ui-settings"]
|
||||
pkg_client_ui_settings_general["client-ui-settings-general"]
|
||||
@@ -170,6 +171,8 @@ flowchart TD
|
||||
end
|
||||
subgraph group_context["packages/context"]
|
||||
pkg_agent_instructions["agent-instructions"]
|
||||
pkg_file_reference["file-reference"]
|
||||
pkg_file_reference_local["file-reference-local"]
|
||||
pkg_session_reference["session-reference"]
|
||||
pkg_time_context["time-context"]
|
||||
pkg_tmux_context["tmux-context"]
|
||||
@@ -510,6 +513,9 @@ flowchart TD
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_spill_local --> pkg_invariants
|
||||
pkg_spill_local --> pkg_spill
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_file_reference --> pkg_invariants
|
||||
pkg_file_reference --> pkg_typert_protocol
|
||||
pkg_time_context --> pkg_agent
|
||||
pkg_time_context --> pkg_invariants
|
||||
pkg_time_context --> pkg_session
|
||||
@@ -834,6 +840,11 @@ flowchart TD
|
||||
pkg_agent_instructions --> pkg_llm
|
||||
pkg_agent_instructions --> pkg_session
|
||||
pkg_agent_instructions --> pkg_tools
|
||||
pkg_file_reference_local --> pkg_agent
|
||||
pkg_file_reference_local --> pkg_file_reference
|
||||
pkg_file_reference_local --> pkg_invariants
|
||||
pkg_file_reference_local --> pkg_system_prompt
|
||||
pkg_file_reference_local --> pkg_tools
|
||||
pkg_session_reference --> pkg_agent
|
||||
pkg_session_reference --> pkg_compaction
|
||||
pkg_session_reference --> pkg_invariants
|
||||
@@ -841,6 +852,7 @@ flowchart TD
|
||||
pkg_session_reference --> pkg_output_retention
|
||||
pkg_session_reference --> pkg_session
|
||||
pkg_session_reference --> pkg_session_query
|
||||
pkg_session_reference --> pkg_typert_protocol
|
||||
pkg_cordis_host_runner --> pkg_agent
|
||||
pkg_cordis_host_runner --> pkg_brand
|
||||
pkg_cordis_host_runner --> pkg_invariants
|
||||
@@ -1161,6 +1173,7 @@ flowchart TD
|
||||
pkg_api_remotes --> pkg_commands
|
||||
pkg_api_remotes --> pkg_cordis_host_runner
|
||||
pkg_api_remotes --> pkg_credentials
|
||||
pkg_api_remotes --> pkg_file_reference
|
||||
pkg_api_remotes --> pkg_goal
|
||||
pkg_api_remotes --> pkg_host_plugin_inventory
|
||||
pkg_api_remotes --> pkg_invariants
|
||||
@@ -1168,6 +1181,7 @@ flowchart TD
|
||||
pkg_api_remotes --> pkg_message_feedback
|
||||
pkg_api_remotes --> pkg_session
|
||||
pkg_api_remotes --> pkg_session_persistence
|
||||
pkg_api_remotes --> pkg_session_reference
|
||||
pkg_api_remotes --> pkg_settings
|
||||
pkg_api_remotes --> pkg_typert_registry
|
||||
pkg_client_runtime --> pkg_agent
|
||||
@@ -1205,6 +1219,7 @@ flowchart TD
|
||||
pkg_client_test_runtime --> pkg_invariants
|
||||
pkg_client_ui_input_trigger --> pkg_client_locale
|
||||
pkg_client_ui_input_trigger --> pkg_client_runtime
|
||||
pkg_client_ui_input_trigger --> pkg_file_reference
|
||||
pkg_client_ui_input_trigger --> pkg_invariants
|
||||
pkg_client_ui_settings_models --> pkg_api_remotes
|
||||
pkg_client_ui_settings_models --> pkg_client_connection
|
||||
@@ -1234,6 +1249,14 @@ flowchart TD
|
||||
pkg_client_ui_layout --> pkg_client_runtime
|
||||
pkg_client_ui_layout --> pkg_client_ui_theme
|
||||
pkg_client_ui_layout --> pkg_invariants
|
||||
pkg_client_ui_reference --> pkg_api_remotes
|
||||
pkg_client_ui_reference --> pkg_client_locale
|
||||
pkg_client_ui_reference --> pkg_client_runtime
|
||||
pkg_client_ui_reference --> pkg_client_ui_input_trigger
|
||||
pkg_client_ui_reference --> pkg_file_reference
|
||||
pkg_client_ui_reference --> pkg_invariants
|
||||
pkg_client_ui_reference --> pkg_session_reference
|
||||
pkg_client_ui_reference --> pkg_typert_protocol
|
||||
pkg_cordis_client_runner --> pkg_api_remotes
|
||||
pkg_cordis_client_runner --> pkg_client_connection
|
||||
pkg_cordis_client_runner --> pkg_client_modules
|
||||
@@ -1492,6 +1515,7 @@ flowchart TD
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1552,7 +1576,8 @@ flowchart TD
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
|
||||
| [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
@@ -1602,18 +1627,19 @@ flowchart TD
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
|
||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
|
||||
+29
-3
@@ -141,6 +141,7 @@ flowchart TD
|
||||
pkg_client_ui_permission_presets["client-ui-permission-presets"]
|
||||
pkg_client_ui_plan["client-ui-plan"]
|
||||
pkg_client_ui_primitives["client-ui-primitives"]
|
||||
pkg_client_ui_reference["client-ui-reference"]
|
||||
pkg_client_ui_renderer["client-ui-renderer"]
|
||||
pkg_client_ui_settings["client-ui-settings"]
|
||||
pkg_client_ui_settings_general["client-ui-settings-general"]
|
||||
@@ -172,6 +173,8 @@ flowchart TD
|
||||
end
|
||||
subgraph group_context["packages/context"]
|
||||
pkg_agent_instructions["agent-instructions"]
|
||||
pkg_file_reference["file-reference"]
|
||||
pkg_file_reference_local["file-reference-local"]
|
||||
pkg_session_reference["session-reference"]
|
||||
pkg_time_context["time-context"]
|
||||
pkg_tmux_context["tmux-context"]
|
||||
@@ -512,6 +515,9 @@ flowchart TD
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_spill_local --> pkg_invariants
|
||||
pkg_spill_local --> pkg_spill
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_file_reference --> pkg_invariants
|
||||
pkg_file_reference --> pkg_typert_protocol
|
||||
pkg_time_context --> pkg_agent
|
||||
pkg_time_context --> pkg_invariants
|
||||
pkg_time_context --> pkg_session
|
||||
@@ -836,6 +842,11 @@ flowchart TD
|
||||
pkg_agent_instructions --> pkg_llm
|
||||
pkg_agent_instructions --> pkg_session
|
||||
pkg_agent_instructions --> pkg_tools
|
||||
pkg_file_reference_local --> pkg_agent
|
||||
pkg_file_reference_local --> pkg_file_reference
|
||||
pkg_file_reference_local --> pkg_invariants
|
||||
pkg_file_reference_local --> pkg_system_prompt
|
||||
pkg_file_reference_local --> pkg_tools
|
||||
pkg_session_reference --> pkg_agent
|
||||
pkg_session_reference --> pkg_compaction
|
||||
pkg_session_reference --> pkg_invariants
|
||||
@@ -843,6 +854,7 @@ flowchart TD
|
||||
pkg_session_reference --> pkg_output_retention
|
||||
pkg_session_reference --> pkg_session
|
||||
pkg_session_reference --> pkg_session_query
|
||||
pkg_session_reference --> pkg_typert_protocol
|
||||
pkg_cordis_host_runner --> pkg_agent
|
||||
pkg_cordis_host_runner --> pkg_brand
|
||||
pkg_cordis_host_runner --> pkg_invariants
|
||||
@@ -1163,6 +1175,7 @@ flowchart TD
|
||||
pkg_api_remotes --> pkg_commands
|
||||
pkg_api_remotes --> pkg_cordis_host_runner
|
||||
pkg_api_remotes --> pkg_credentials
|
||||
pkg_api_remotes --> pkg_file_reference
|
||||
pkg_api_remotes --> pkg_goal
|
||||
pkg_api_remotes --> pkg_host_plugin_inventory
|
||||
pkg_api_remotes --> pkg_invariants
|
||||
@@ -1170,6 +1183,7 @@ flowchart TD
|
||||
pkg_api_remotes --> pkg_message_feedback
|
||||
pkg_api_remotes --> pkg_session
|
||||
pkg_api_remotes --> pkg_session_persistence
|
||||
pkg_api_remotes --> pkg_session_reference
|
||||
pkg_api_remotes --> pkg_settings
|
||||
pkg_api_remotes --> pkg_typert_registry
|
||||
pkg_client_runtime --> pkg_agent
|
||||
@@ -1207,6 +1221,7 @@ flowchart TD
|
||||
pkg_client_test_runtime --> pkg_invariants
|
||||
pkg_client_ui_input_trigger --> pkg_client_locale
|
||||
pkg_client_ui_input_trigger --> pkg_client_runtime
|
||||
pkg_client_ui_input_trigger --> pkg_file_reference
|
||||
pkg_client_ui_input_trigger --> pkg_invariants
|
||||
pkg_client_ui_settings_models --> pkg_api_remotes
|
||||
pkg_client_ui_settings_models --> pkg_client_connection
|
||||
@@ -1236,6 +1251,14 @@ flowchart TD
|
||||
pkg_client_ui_layout --> pkg_client_runtime
|
||||
pkg_client_ui_layout --> pkg_client_ui_theme
|
||||
pkg_client_ui_layout --> pkg_invariants
|
||||
pkg_client_ui_reference --> pkg_api_remotes
|
||||
pkg_client_ui_reference --> pkg_client_locale
|
||||
pkg_client_ui_reference --> pkg_client_runtime
|
||||
pkg_client_ui_reference --> pkg_client_ui_input_trigger
|
||||
pkg_client_ui_reference --> pkg_file_reference
|
||||
pkg_client_ui_reference --> pkg_invariants
|
||||
pkg_client_ui_reference --> pkg_session_reference
|
||||
pkg_client_ui_reference --> pkg_typert_protocol
|
||||
pkg_cordis_client_runner --> pkg_api_remotes
|
||||
pkg_cordis_client_runner --> pkg_client_connection
|
||||
pkg_cordis_client_runner --> pkg_client_modules
|
||||
@@ -1494,6 +1517,7 @@ flowchart TD
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1554,7 +1578,8 @@ flowchart TD
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
|
||||
| [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
@@ -1604,18 +1629,19 @@ flowchart TD
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
|
||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
|
||||
@@ -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 docs/subsystems/session-reference.md
|
||||
session-reference.md: 4140eb2a27d79ee0975d97ae9e96a1b502d74187
|
||||
session-reference.zh.md: 9dcfdbd07d4df1c54c51dc06d36af2f052969783
|
||||
session-reference.md: e3ef2c00b7ddbeed9fa3f9d074df436d8c0cc7b5
|
||||
session-reference.zh.md: 74063c1525a1f7f67663e9b11148e18902b22748
|
||||
|
||||
@@ -2,9 +2,23 @@
|
||||
|
||||
English | [中文](session-reference.zh.md)
|
||||
|
||||
Structured cross-session reference requests and prepared message contexts. The [package contract](../../packages/context/session-reference) defines canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
|
||||
Host-backed file discovery plus structured cross-session reference requests and prepared message contexts. The [file-reference contract](../../packages/context/file-reference) owns path-only completion records and grammar; the [session-reference contract](../../packages/context/session-reference) defines canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
|
||||
|
||||
Source: [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
Sources: [`packages/context/file-reference/src/types.ts`](../../packages/context/file-reference/src/types.ts) · [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
|
||||
## File candidates
|
||||
|
||||
`FileReferenceCandidate` is the path-only discovery result. The addressed agent supplies the working-directory scope; providers decide ranking and namespace access without reading file contents.
|
||||
|
||||
```ts type-equiv
|
||||
/** One path-only completion candidate inside the target session cwd. */
|
||||
interface FileReferenceCandidate {
|
||||
/** User-facing path accepted by normal prompts and filesystem tools. */
|
||||
path: string
|
||||
/** Directories keep completion open; files finish the mention. */
|
||||
kind: 'file' | 'directory'
|
||||
}
|
||||
```
|
||||
|
||||
## Inputs and candidates
|
||||
|
||||
@@ -36,6 +50,16 @@ interface SessionReferenceCandidate {
|
||||
}
|
||||
```
|
||||
|
||||
The `sessionReferenceResolver/candidates` Remote method serves the same discovery to browser consumers and attaches each candidate's canonical prompt mention.
|
||||
|
||||
```ts type-equiv
|
||||
/** One discovery candidate carrying its canonical prompt mention. */
|
||||
interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {
|
||||
/** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */
|
||||
mention: string
|
||||
}
|
||||
```
|
||||
|
||||
## Prepared messages
|
||||
|
||||
Preparation preserves readable current-message content and returns at most one aggregated context.
|
||||
@@ -74,6 +98,37 @@ type SessionReferenceErrorCode =
|
||||
|
||||
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxfilereferences--filereferenceservice-abstract-seam"></a>
|
||||
|
||||
### `ctx.fileReferences` — `FileReferenceService` (abstract seam)
|
||||
|
||||
Host capability for cancellable file-reference discovery.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* List file and directory candidates for one agent's working directory.
|
||||
* @param agent - target agent whose session cwd bounds discovery.
|
||||
* @param query - path text following `@` or `@"`.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns deterministic path-only candidates.
|
||||
*/
|
||||
abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
|
||||
|
||||
/**
|
||||
* Remote face of {@link list}; the decorator cannot mark the abstract
|
||||
* member, so this concrete adapter carries the identical contract.
|
||||
* @param agent - target agent whose session cwd bounds discovery.
|
||||
* @param query - path text following `@` or `@"`.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns deterministic path-only candidates.
|
||||
*/
|
||||
@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
|
||||
```
|
||||
|
||||
Types: [Agent](core.md)
|
||||
|
||||
Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts)
|
||||
|
||||
<a id="ctxsessionreferenceresolver--sessionreferenceresolver"></a>
|
||||
|
||||
### `ctx.sessionReferenceResolver` — `SessionReferenceResolver`
|
||||
@@ -92,11 +147,22 @@ Exact-read consumer that prepares immutable cross-session message context.
|
||||
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
|
||||
|
||||
/**
|
||||
* Snapshot all references before enqueue and return one aggregated durable context.
|
||||
* Remote face of {@link listCandidates}: the configured candidate limit
|
||||
* applies, and every candidate carries the canonical mention a host inserts
|
||||
* into the prompt draft.
|
||||
* @param agent - target agent; self is excluded and its cwd drives ranking.
|
||||
* @param query - optional case-insensitive session-id/cwd/title substring.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns mention-carrying candidates in rank order.
|
||||
*/
|
||||
@Remote('candidates') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise<SessionReferenceMentionCandidate[]>
|
||||
|
||||
/**
|
||||
* Snapshot all references for one accepted direct message and return one aggregated durable context.
|
||||
* @param agent - target agent; references to it are rejected.
|
||||
* @param content - already host-normalized readable message content.
|
||||
* @param references - structured source sessions in mention order.
|
||||
* @param signal - optional cancellation boundary for host request teardown.
|
||||
* @param signal - optional cancellation boundary for the active turn.
|
||||
* @returns detached content and optional referenced-session context.
|
||||
*/
|
||||
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
|
||||
@@ -104,5 +170,5 @@ async prepare( agent: Agent, content: ContentBlock[], references: SessionReferen
|
||||
|
||||
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
|
||||
|
||||
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
|
||||
Source: [`packages/context/session-reference/src/index.ts:75`](../../packages/context/session-reference/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,9 +2,23 @@
|
||||
|
||||
[English](session-reference.md) | 中文
|
||||
|
||||
结构化的跨会话引用请求与准备后的消息上下文。[包约定](../../packages/context/session-reference) 定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
|
||||
由 Host 支撑的文件发现,以及结构化的跨会话引用请求与准备后的消息上下文。[文件引用约定](../../packages/context/file-reference)负责仅含路径的补全记录与语法;[会话引用约定](../../packages/context/session-reference)定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
|
||||
|
||||
来源:[`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
来源:[`packages/context/file-reference/src/types.ts`](../../packages/context/file-reference/src/types.ts) · [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
|
||||
## 文件候选项
|
||||
|
||||
`FileReferenceCandidate` 是仅含路径的发现结果。被寻址的 agent 提供工作目录范围;提供方负责排序和命名空间访问,但不会读取文件内容。
|
||||
|
||||
```ts type-equiv
|
||||
/** One path-only completion candidate inside the target session cwd. */
|
||||
interface FileReferenceCandidate {
|
||||
/** User-facing path accepted by normal prompts and filesystem tools. */
|
||||
path: string
|
||||
/** Directories keep completion open; files finish the mention. */
|
||||
kind: 'file' | 'directory'
|
||||
}
|
||||
```
|
||||
|
||||
## 输入与候选项
|
||||
|
||||
@@ -36,6 +50,16 @@ interface SessionReferenceCandidate {
|
||||
}
|
||||
```
|
||||
|
||||
`sessionReferenceResolver/candidates` Remote 方法向浏览器消费方提供同一发现能力,并为每个候选附上规范提示词 mention。
|
||||
|
||||
```ts type-equiv
|
||||
/** One discovery candidate carrying its canonical prompt mention. */
|
||||
interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {
|
||||
/** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */
|
||||
mention: string
|
||||
}
|
||||
```
|
||||
|
||||
## 准备后的消息
|
||||
|
||||
准备过程保留可读的当前消息内容,并最多返回一个聚合上下文。
|
||||
@@ -74,6 +98,37 @@ type SessionReferenceErrorCode =
|
||||
|
||||
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxfilereferences--filereferenceservice-abstract-seam"></a>
|
||||
|
||||
### `ctx.fileReferences` — `FileReferenceService` (abstract seam)
|
||||
|
||||
Host capability for cancellable file-reference discovery.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* List file and directory candidates for one agent's working directory.
|
||||
* @param agent - target agent whose session cwd bounds discovery.
|
||||
* @param query - path text following `@` or `@"`.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns deterministic path-only candidates.
|
||||
*/
|
||||
abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
|
||||
|
||||
/**
|
||||
* Remote face of {@link list}; the decorator cannot mark the abstract
|
||||
* member, so this concrete adapter carries the identical contract.
|
||||
* @param agent - target agent whose session cwd bounds discovery.
|
||||
* @param query - path text following `@` or `@"`.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns deterministic path-only candidates.
|
||||
*/
|
||||
@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
|
||||
```
|
||||
|
||||
Types: [Agent](core.md)
|
||||
|
||||
Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts)
|
||||
|
||||
<a id="ctxsessionreferenceresolver--sessionreferenceresolver"></a>
|
||||
|
||||
### `ctx.sessionReferenceResolver` — `SessionReferenceResolver`
|
||||
@@ -92,11 +147,22 @@ Exact-read consumer that prepares immutable cross-session message context.
|
||||
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
|
||||
|
||||
/**
|
||||
* Snapshot all references before enqueue and return one aggregated durable context.
|
||||
* Remote face of {@link listCandidates}: the configured candidate limit
|
||||
* applies, and every candidate carries the canonical mention a host inserts
|
||||
* into the prompt draft.
|
||||
* @param agent - target agent; self is excluded and its cwd drives ranking.
|
||||
* @param query - optional case-insensitive session-id/cwd/title substring.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns mention-carrying candidates in rank order.
|
||||
*/
|
||||
@Remote('candidates') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise<SessionReferenceMentionCandidate[]>
|
||||
|
||||
/**
|
||||
* Snapshot all references for one accepted direct message and return one aggregated durable context.
|
||||
* @param agent - target agent; references to it are rejected.
|
||||
* @param content - already host-normalized readable message content.
|
||||
* @param references - structured source sessions in mention order.
|
||||
* @param signal - optional cancellation boundary for host request teardown.
|
||||
* @param signal - optional cancellation boundary for the active turn.
|
||||
* @returns detached content and optional referenced-session context.
|
||||
*/
|
||||
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
|
||||
@@ -104,5 +170,5 @@ async prepare( agent: Agent, content: ContentBlock[], references: SessionReferen
|
||||
|
||||
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
|
||||
|
||||
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
|
||||
Source: [`packages/context/session-reference/src/index.ts:75`](../../packages/context/session-reference/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -502,6 +502,16 @@
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/context/file-reference": {
|
||||
"ignoreDependencies": [
|
||||
"zod"
|
||||
]
|
||||
},
|
||||
"packages/context/session-reference": {
|
||||
"ignoreDependencies": [
|
||||
"zod"
|
||||
]
|
||||
},
|
||||
"packages/session/session-checkpoint-policy": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
|
||||
@@ -58,37 +58,41 @@
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-gateway": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-message-feedback": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^"
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-gateway": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-message-feedback": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
|
||||
@@ -4,16 +4,20 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
|
||||
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
|
||||
import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
|
||||
import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
|
||||
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
|
||||
import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
|
||||
import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote'
|
||||
import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
export type { TypertClientRemote as ClientRemote } from '@deepseek-ai/dsh-typert-protocol'
|
||||
export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
|
||||
export type {} from '@deepseek-ai/dsh-commands/remote'
|
||||
export type {} from '@deepseek-ai/dsh-file-reference/remote'
|
||||
export type {} from '@deepseek-ai/dsh-goal/remote'
|
||||
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
|
||||
export type {} from '@deepseek-ai/dsh-message-feedback/remote'
|
||||
export type {} from '@deepseek-ai/dsh-session-reference/remote'
|
||||
// The forwarded-event allowlist's selection seat: without it in the consumer's
|
||||
// compilation face `TypertRemoteEvent` is `never` and every `$on` call fails.
|
||||
export type { ApiRemoteForwardedEvent } from '../types.ts'
|
||||
@@ -86,6 +90,10 @@ export type {
|
||||
// reason: a Client contribution names what it sends without importing a Host
|
||||
// package, and this assembly is where both planes legitimately meet.
|
||||
export type { JsonValue } from '@deepseek-ai/dsh-session/types'
|
||||
// Reference-discovery result vocabulary for the fileReferences and
|
||||
// sessionReferenceResolver namespaces.
|
||||
export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
export type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
@@ -106,7 +114,8 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
|
||||
const disposers: Array<() => Promise<void>> = []
|
||||
try {
|
||||
for (const contribution of [
|
||||
commandsRemote, goalsRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote,
|
||||
commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
|
||||
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
|
||||
]) {
|
||||
disposers.push(await ctx.remote.$mount(contribution))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
"path": "../../credentials/credentials"
|
||||
|
||||
},
|
||||
{
|
||||
"path": "../../context/file-reference"
|
||||
},
|
||||
{
|
||||
"path": "../../context/session-reference"
|
||||
},
|
||||
{
|
||||
"path": "../../extensions/cordis-host-runner"
|
||||
},
|
||||
|
||||
@@ -79,6 +79,12 @@
|
||||
writeEveryEvents: 200
|
||||
writeIntervalMs: 5000
|
||||
|
||||
- id: session-reference
|
||||
name: '@deepseek-ai/dsh-session-reference'
|
||||
|
||||
- id: file-reference-local
|
||||
name: '@deepseek-ai/dsh-file-reference-local'
|
||||
|
||||
# Whole-log turn/step counts for the chat stats strip (the sessionStats
|
||||
# projection key); the projection registry itself is a base-layer row.
|
||||
- id: session-stats
|
||||
@@ -226,7 +232,7 @@
|
||||
name: '@deepseek-ai/dsh-client-ui-workspace'
|
||||
|
||||
# Input triggers: the '/' | '@' pipeline (ui-input-trigger), the command surface over
|
||||
# it (ui-commands), and the two reference sources (ui-skill / ui-subagent).
|
||||
# it (ui-commands), and the reference sources (ui-skill / ui-reference).
|
||||
- id: ui-input-trigger
|
||||
name: '@deepseek-ai/dsh-client-ui-input-trigger'
|
||||
|
||||
@@ -239,6 +245,9 @@
|
||||
- id: ui-subagent
|
||||
name: '@deepseek-ai/dsh-client-ui-subagent'
|
||||
|
||||
- id: ui-reference
|
||||
name: '@deepseek-ai/dsh-client-ui-reference'
|
||||
|
||||
# Background jobs: the session-header list over the jobsBySession mirror.
|
||||
- id: ui-jobs
|
||||
name: '@deepseek-ai/dsh-client-ui-jobs'
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-jobs": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
|
||||
@@ -95,8 +96,11 @@
|
||||
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-message-feedback": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-log-export": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-stats": "workspace:^",
|
||||
"@deepseek-ai/dsh-storage": "workspace:^",
|
||||
|
||||
@@ -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 packages/client/README.md
|
||||
README.md: cff74ddb048df8b37643f4c44f01e2bda4160a77
|
||||
README.zh.md: 5e5d623509b9152fb7b59ea228cfa42720b737af
|
||||
README.md: fe57d58a0f4fa1c9bff2699ffb363c80197fc5ed
|
||||
README.zh.md: c721710c9f20ba10f20392c207e1de4169ec0e12
|
||||
|
||||
@@ -29,6 +29,7 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
|
||||
| [`ui-commands/`](ui-commands/README.md) | Provides session-aware command discovery and dispatch. |
|
||||
| [`ui-input-trigger/`](ui-input-trigger/README.md) | Coordinates inline command and reference suggestions. |
|
||||
| [`ui-skill/`](ui-skill/README.md) | Adds skill references to inline suggestions. |
|
||||
| [`ui-reference/`](ui-reference/README.md) | Unified Web `@file` / `@session` reference source. |
|
||||
| [`ui-subagent/`](ui-subagent/README.md) | Provides subagent navigation, child transcript states, and inline references. |
|
||||
| [`ui-jobs/`](ui-jobs/README.md) | Lists this session's background jobs in the conversation header. |
|
||||
| [`ui-model-selection/`](ui-model-selection/README.md) | Provides model selection in conversation surfaces. |
|
||||
|
||||
@@ -29,6 +29,7 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U
|
||||
| [`ui-commands/`](ui-commands/README.md) | 提供会话感知的命令发现与分发。 |
|
||||
| [`ui-input-trigger/`](ui-input-trigger/README.md) | 协调内联命令和引用建议。 |
|
||||
| [`ui-skill/`](ui-skill/README.md) | 向内联建议添加 skill(技能)引用。 |
|
||||
| [`ui-reference/`](ui-reference/README.md) | 统一的 Web `@file` / `@session` 引用 source。 |
|
||||
| [`ui-subagent/`](ui-subagent/README.md) | 提供 subagent(子 agent)导航、子级 transcript(文本记录)的状态和内联引用。 |
|
||||
| [`ui-jobs/`](ui-jobs/README.md) | 在会话标题栏列出当前会话的后台任务。 |
|
||||
| [`ui-model-selection/`](ui-model-selection/README.md) | 在对话界面中提供模型选择。 |
|
||||
|
||||
@@ -1866,6 +1866,51 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
})
|
||||
|
||||
/** Canonical fixture implementation of the generated Goal Remote contract. */
|
||||
/** Canonical fixture implementation of the generated reference-discovery Remote contracts. */
|
||||
const referenceRemotes = {
|
||||
files(id: SessionId, query: string): RpcResult<{ path: string; kind: 'file' | 'directory' }[]> {
|
||||
const missing = requireGoalSession(id)
|
||||
if (missing !== undefined) return missing
|
||||
const needle = query.toLocaleLowerCase()
|
||||
const items = [
|
||||
{ path: 'notes', kind: 'directory' as const },
|
||||
{ path: 'README.md', kind: 'file' as const },
|
||||
{ path: 'notes/demo.txt', kind: 'file' as const },
|
||||
].filter(item => item.path.toLocaleLowerCase().includes(needle))
|
||||
return { ok: true, value: items }
|
||||
},
|
||||
sessions(id: SessionId, query: string): RpcResult<{
|
||||
sessionId: SessionId
|
||||
label: string
|
||||
cwd?: string
|
||||
createdAt: number
|
||||
mention: string
|
||||
}[]> {
|
||||
const missing = requireGoalSession(id)
|
||||
if (missing !== undefined) return missing
|
||||
const needle = query.toLocaleLowerCase()
|
||||
const value = sessions
|
||||
.filter(item => item.sessionId !== id)
|
||||
.filter(item => String(item.sessionId).toLocaleLowerCase().includes(needle)
|
||||
|| item.cwd?.toLocaleLowerCase().includes(needle) === true)
|
||||
.map((item) => {
|
||||
const label = item.sessionId === sid('fx-beta') ? 'Fixture child session' : String(item.sessionId)
|
||||
const encoded = btoa(JSON.stringify(item.sessionId))
|
||||
.replaceAll('+', '-')
|
||||
.replaceAll('/', '_')
|
||||
.replace(/=+$/u, '')
|
||||
return {
|
||||
sessionId: item.sessionId,
|
||||
label,
|
||||
...item.cwd === undefined ? {} : { cwd: item.cwd },
|
||||
createdAt: item.updatedAt,
|
||||
mention: `@[${label}](dsh-session:${encoded})`,
|
||||
}
|
||||
})
|
||||
return { ok: true, value }
|
||||
},
|
||||
}
|
||||
|
||||
const goalRemotes = {
|
||||
create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> {
|
||||
const missing = requireGoalSession(id)
|
||||
@@ -3053,6 +3098,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
args: {
|
||||
agentId: SessionId
|
||||
line?: string
|
||||
query?: string
|
||||
images?: readonly unknown[]
|
||||
ref?: { id: string; revision: number }
|
||||
request?: { objective?: string; maxGoalRounds?: number }
|
||||
@@ -3062,6 +3108,8 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
switch (endpoint) {
|
||||
case 'commands/list': return Promise.resolve(commandRemotes.list(sessionId))
|
||||
case 'commands/execute': return Promise.resolve(commandRemotes.execute(sessionId, args.line as string, args.images ?? []))
|
||||
case 'fileReferences/list': return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? ''))
|
||||
case 'sessionReferenceResolver/candidates': return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? ''))
|
||||
case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, {
|
||||
objective: args.request?.objective as string,
|
||||
...args.request?.maxGoalRounds === undefined ? {} : { maxGoalRounds: args.request.maxGoalRounds },
|
||||
|
||||
@@ -38,7 +38,11 @@ export interface ISession {
|
||||
* @param mode - 'queue' appends a turn; 'steer' interrupts the running one.
|
||||
* @returns acceptance, or the business error (also mirrored into snapshot.promptError).
|
||||
*/
|
||||
prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
|
||||
prompt(
|
||||
content: PromptContentPart[],
|
||||
mode: 'queue' | 'steer',
|
||||
signal?: AbortSignal,
|
||||
): Promise<RpcResult<{ accepted: true }>>
|
||||
/**
|
||||
* Resolve one durable image referenced by this session.
|
||||
* @param attachmentId - opaque id found in the folded session log.
|
||||
|
||||
@@ -85,7 +85,7 @@ export {
|
||||
} from './sessions/conversation.ts'
|
||||
export { emptyAssistantBlock } from './sessions/partial.ts'
|
||||
export { isTokenDelta } from './sessions/assistant-timing.ts'
|
||||
export { contextForm, contextProvenance } from './sessions/context-provenance.ts'
|
||||
export { contextForm, contextProvenance, sessionRecallLabels } from './sessions/context-provenance.ts'
|
||||
export { displayFailureMessage } from './sessions/failure-display.ts'
|
||||
export type {
|
||||
ConversationContext, ConversationContextOriginKind,
|
||||
|
||||
@@ -58,6 +58,19 @@ function joined(names: string[]): string | null {
|
||||
return names.length > 0 ? names.join(', ') : null
|
||||
}
|
||||
|
||||
/**
|
||||
* The referenced-session labels of one durable `session-reference` recall
|
||||
* source, in first-seen order; empty for every other source shape, including
|
||||
* a foreign or older log whose reference entries carry no readable label.
|
||||
* @param source - the logged `user/message` source, exactly as recorded.
|
||||
* @returns distinct non-empty reference labels.
|
||||
*/
|
||||
export function sessionRecallLabels(source: unknown): string[] {
|
||||
const record = asRecord(source)
|
||||
if (record === null || readString(record, 'kind') !== 'session-reference') return []
|
||||
return collect(record, 'references', 'label')
|
||||
}
|
||||
|
||||
/**
|
||||
* Project one durable message source onto its transcript role and producer name.
|
||||
*
|
||||
|
||||
@@ -187,7 +187,11 @@ export class Session implements SessionFace {
|
||||
* @param mode - queue appends after the current turn; steer interrupts it.
|
||||
* @returns the prompt result (also mirrored into promptError on failure).
|
||||
*/
|
||||
async prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
|
||||
async prompt(
|
||||
content: PromptContentPart[],
|
||||
mode: 'queue' | 'steer',
|
||||
signal?: AbortSignal,
|
||||
): Promise<RpcResult<{ accepted: true }>> {
|
||||
this.promptError = null
|
||||
this.lastAgentError = null
|
||||
// Synchronous, before the first await: the blank → engaging edge must be
|
||||
@@ -204,7 +208,7 @@ export class Session implements SessionFace {
|
||||
mode,
|
||||
content,
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
})).result
|
||||
}, signal)).result
|
||||
} else if (this.address.mode === 'one-shot') {
|
||||
result = {
|
||||
ok: false,
|
||||
@@ -231,7 +235,7 @@ export class Session implements SessionFace {
|
||||
? [{ type: 'text' as const, text: part.text }]
|
||||
: []),
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
})).result
|
||||
}, signal)).result
|
||||
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -57,7 +57,7 @@ function styleInjectionModule(
|
||||
* Everything else under @deepseek-ai/* is either a module-table entry
|
||||
* (external) or a leak the purity gate rejects.
|
||||
*/
|
||||
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/
|
||||
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand)(\/|$)/
|
||||
|
||||
/**
|
||||
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
|
||||
|
||||
@@ -337,6 +337,7 @@ export function apply(ctx: Context): void {
|
||||
inputTriggers.toggleSource('command', {
|
||||
trigger: '/',
|
||||
query: '',
|
||||
quoted: false,
|
||||
position: snapshot.draft.slice(0, selection.start).trim() === '' ? 'leading' : 'inline',
|
||||
span: { ...selection, draftRev: snapshot.draftRev },
|
||||
})
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.referenceSummary {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.contextRow {
|
||||
padding: 2px 0;
|
||||
}
|
||||
@@ -269,7 +274,8 @@
|
||||
}
|
||||
|
||||
/* Reference chip projection inside a user bubble (`<skill>name</skill>` model
|
||||
spans render as chips; free geometry — no textarea pairing here). */
|
||||
spans and metadata-confirmed sessions render as chips; free geometry means
|
||||
the textarea overlay's metric pairing does not apply here). */
|
||||
.refChip {
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
|
||||
@@ -152,21 +152,36 @@ function TurnMaxTokensItem({ t }: {
|
||||
* scan as the composer, minus the lexicon: sent tokens were validated at
|
||||
* compose time, so shape alone decorates).
|
||||
*/
|
||||
function projectUserText(text: string): ReactNode {
|
||||
function projectUserText(text: string, sessionLabels: readonly string[]): ReactNode {
|
||||
const ranges: { start: number; end: number; label: string; kind: 'session' | 'plain' }[] = []
|
||||
for (const rawLabel of [...new Set(sessionLabels)].sort((a, b) => b.length - a.length)) {
|
||||
const label = `@${rawLabel}`
|
||||
let start = text.indexOf(label)
|
||||
while (start >= 0) {
|
||||
ranges.push({ start, end: start + label.length, label, kind: 'session' })
|
||||
start = text.indexOf(label, start + label.length)
|
||||
}
|
||||
}
|
||||
const re = /(^|\s)([/@][\w-]+)(?=\s|$)/g
|
||||
const parts: ReactNode[] = []
|
||||
let cursor = 0
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const tokenStart = m.index + (m[1]?.length ?? 0)
|
||||
const label = m[2] ?? ''
|
||||
ranges.push({ start: tokenStart, end: tokenStart + label.length, label, kind: 'plain' })
|
||||
}
|
||||
ranges.sort((a, b) => a.start - b.start || b.end - a.end)
|
||||
const parts: ReactNode[] = []
|
||||
let cursor = 0
|
||||
for (const range of ranges) {
|
||||
if (range.start < cursor) continue
|
||||
const { start: tokenStart, end, label, kind } = range
|
||||
if (tokenStart > cursor) parts.push(<MessageText key={cursor} text={text.slice(cursor, tokenStart)} />)
|
||||
parts.push(
|
||||
<span key={tokenStart} className={css.refChip} data-ref-chip={label.startsWith('@') ? 'subagent' : 'skill'}>
|
||||
<span key={tokenStart} className={css.refChip} data-ref-chip={kind === 'session' ? 'session' : label.startsWith('@') ? 'subagent' : 'skill'}>
|
||||
{label}
|
||||
</span>,
|
||||
)
|
||||
cursor = tokenStart + label.length
|
||||
cursor = end
|
||||
}
|
||||
if (parts.length === 0) return <MessageText text={text} />
|
||||
if (cursor < text.length) parts.push(<MessageText key={cursor} text={text.slice(cursor)} />)
|
||||
@@ -175,7 +190,7 @@ function projectUserText(text: string): ReactNode {
|
||||
|
||||
/** Right-aligned bubble shared by user and steering rows. */
|
||||
function UserStyleBubble({
|
||||
content, renderMessageImages, actions, pending = false, t,
|
||||
content, renderMessageImages, actions, pending = false, referenceLabels = [], t,
|
||||
}: {
|
||||
content: readonly unknown[]
|
||||
renderMessageImages: ChatNodeOwnerProps['renderMessageImages']
|
||||
@@ -183,6 +198,8 @@ function UserStyleBubble({
|
||||
actions?: (text: string) => ReactNode
|
||||
/** Whether this is the Host-authoritative pre-admission steering projection. */
|
||||
pending?: boolean
|
||||
/** Exact session mention labels associated by the adjacent recall node. */
|
||||
referenceLabels?: readonly string[]
|
||||
t: ChatViewSlotProps['t']
|
||||
}): ReactNode {
|
||||
const { text, images, rest } = contentParts(content)
|
||||
@@ -193,9 +210,14 @@ function UserStyleBubble({
|
||||
<div className={css.userStack}>
|
||||
{renderMessageImages({ images, align: 'end' })}
|
||||
{showBubble && <div className={css.bubble}>
|
||||
{projectUserText(text)}
|
||||
{projectUserText(text, referenceLabels)}
|
||||
{rest.map((block, i) => <JsonBlock key={i} label={t('message.extraBlock')} payload={block} truncatedLabel={truncated} />)}
|
||||
</div>}
|
||||
{referenceLabels.length > 0 && (
|
||||
<div className={css.referenceSummary}>
|
||||
{t('message.referenceSummary', { labels: referenceLabels.join(t('message.referenceSeparator')) })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{actions?.(text)}
|
||||
</div>
|
||||
@@ -240,6 +262,7 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
||||
<UserStyleBubble
|
||||
content={data.content}
|
||||
renderMessageImages={renderMessageImages}
|
||||
{...data.referenceLabels === undefined ? {} : { referenceLabels: data.referenceLabels }}
|
||||
t={t}
|
||||
actions={text => (
|
||||
<MessageIconActions
|
||||
|
||||
@@ -3,19 +3,29 @@ import type {
|
||||
ContextMessageNode, ConversationNodeDefinition, SteeringMessageNode, UserMessageNode,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
contextForm, contextProvenance, isAppendSurfaceEvent, isReplacementSurfaceEvent,
|
||||
contextForm, contextProvenance, isAppendSurfaceEvent, isReplacementSurfaceEvent, sessionRecallLabels,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InboxState } from './inbox.ts'
|
||||
import { chatNode } from './common.ts'
|
||||
|
||||
type MessageNode = UserMessageNode | SteeringMessageNode | ContextMessageNode
|
||||
interface ReferencedUserMessageNode extends UserMessageNode {
|
||||
/** Labels cited by the immediately preceding session-reference context. */
|
||||
readonly referenceLabels?: readonly string[]
|
||||
}
|
||||
|
||||
interface ReferencedSteeringMessageNode extends SteeringMessageNode {
|
||||
/** Labels cited by the immediately preceding session-reference context. */
|
||||
readonly referenceLabels?: readonly string[]
|
||||
}
|
||||
|
||||
type MessageNode = ReferencedUserMessageNode | ReferencedSteeringMessageNode | ContextMessageNode
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
|
||||
interface ChatNodeDataMap {
|
||||
/** Ordinary turn-opening user message. */
|
||||
user: UserMessageNode
|
||||
user: ReferencedUserMessageNode
|
||||
/** User message admitted into an active turn. */
|
||||
steering: SteeringMessageNode
|
||||
steering: ReferencedSteeringMessageNode
|
||||
/** Non-user context injected into model history. */
|
||||
context: ContextMessageNode
|
||||
}
|
||||
@@ -51,6 +61,11 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
|
||||
}
|
||||
}
|
||||
const claimed = reader.previous<InboxState>('inbox-next-step')?.state.claimed.has(String(event.data.id)) === true
|
||||
const previous = reader.previous<MessageNode>('input-message')
|
||||
const labels = previous?.state.kind === 'context' && previous.state.seq + 1 === event.seq
|
||||
? sessionRecallLabels(previous.state.source)
|
||||
: []
|
||||
const referenceLabels = labels.length === 0 ? {} : { referenceLabels: labels }
|
||||
return claimed
|
||||
? {
|
||||
kind: 'steering',
|
||||
@@ -59,6 +74,7 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
|
||||
time: event.time,
|
||||
content: event.data.content,
|
||||
source: event.data.source,
|
||||
...referenceLabels,
|
||||
}
|
||||
: {
|
||||
kind: 'user',
|
||||
@@ -66,6 +82,7 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
|
||||
time: event.time,
|
||||
content: event.data.content,
|
||||
source: event.data.source,
|
||||
...referenceLabels,
|
||||
}
|
||||
},
|
||||
update: context => context.state,
|
||||
|
||||
@@ -232,7 +232,7 @@ export interface InputState {
|
||||
export interface SubmitAttempt {
|
||||
readonly seq: number
|
||||
readonly signal: AbortSignal
|
||||
/** Draft at enter time; rollback restores it only while the live draft still equals it. */
|
||||
/** Draft at enter time; settlement clears it only after acceptance. */
|
||||
readonly draftSnapshot: string
|
||||
/** Default-message delivery intent retained while slash adjudication is pending. */
|
||||
readonly mode: InputSubmitMode
|
||||
@@ -271,10 +271,7 @@ export type InputEvent =
|
||||
| { readonly type: 'adjudicated'; readonly attempt: SubmitAttempt; readonly outcome: PickOutcome }
|
||||
| { readonly type: 'adjudication-failed'; readonly attempt: SubmitAttempt; readonly message: string }
|
||||
| { readonly type: 'submit-settled'; readonly attempt: SubmitAttempt; readonly ok: boolean; readonly outcome?: SubmitOutcome; readonly message?: string }
|
||||
/**
|
||||
* An ordinary (default-sink) send was accepted: clear the draft as a COMMIT —
|
||||
* undo must not resurrect sent content (mirrors submit-settled's success arm).
|
||||
*/
|
||||
/** Commit an image-only send whose empty draft did not need an attempt. */
|
||||
| { readonly type: 'send-committed' }
|
||||
| { readonly type: 'release' }
|
||||
|
||||
@@ -286,5 +283,5 @@ export type InputEvent =
|
||||
export type InputEffect =
|
||||
| { readonly type: 'adjudicate'; readonly attempt: SubmitAttempt; readonly draft: string }
|
||||
| { readonly type: 'begin-submit'; readonly attempt: SubmitAttempt; readonly claim: CommandClaim; readonly args: string }
|
||||
| { readonly type: 'default-sink'; readonly draft: string; readonly mode: InputSubmitMode }
|
||||
| { readonly type: 'default-sink'; readonly attempt: SubmitAttempt; readonly draft: string; readonly mode: InputSubmitMode }
|
||||
| { readonly type: 'notice'; readonly level: 'info' | 'error'; readonly text: string }
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { ClientContext, ObservableSnapshot, SnapshotStore } from '@deepseek
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ArbitrateKey, ArbitrateOutcome, CommandClaim, ConsumeTokenRequest, PickOutcome,
|
||||
ReferenceInsert, InputTriggerController, SubmitImageAttachment, TokenSpan,
|
||||
ReferenceInsert, InputTriggerController, SubmitImageAttachment, SubmitOutcome, TokenSpan,
|
||||
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type {
|
||||
DraftAttachmentId, EditRange, EditSelection, InputActions, InputEffect, InputNotice, InputState,
|
||||
@@ -45,7 +45,12 @@ export interface SessionInputDeps {
|
||||
*/
|
||||
steerQueue?: (() => void) | undefined
|
||||
/** The plain-message sink (send choreography / materialize fork — the hub owns it). */
|
||||
defaultSink(text: string, imageIds: readonly DraftAttachmentId[], mode: InputSubmitMode): void
|
||||
defaultSink(
|
||||
text: string,
|
||||
imageIds: readonly DraftAttachmentId[],
|
||||
mode: InputSubmitMode,
|
||||
signal: AbortSignal,
|
||||
): Promise<SubmitOutcome>
|
||||
/** Command-plane image plumbing (the hub owns the conversation face and the copy). */
|
||||
commandImages: {
|
||||
/** Resolve ordered draft ids to wire payloads without sending them; rejects when an id no longer resolves. */
|
||||
@@ -95,6 +100,8 @@ export class SessionInputShell implements SessionInput {
|
||||
private noticeSeq = 0
|
||||
private lastDraft = ''
|
||||
private imageIds: readonly DraftAttachmentId[] = []
|
||||
/** One image-only send at a time: Enter during the Host round-trip is a no-op. */
|
||||
private imageSendInFlight = false
|
||||
private disposed = false
|
||||
/** Draft persistence mirror (chat store write; receives the clipboard projection, never raw placeholders). */
|
||||
private mirrorFn: ((text: string) => void) | undefined
|
||||
@@ -150,16 +157,6 @@ export class SessionInputShell implements SessionInput {
|
||||
this.publish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a failed attempt before any images added after its admission.
|
||||
* @param ids - failed attempt image ids.
|
||||
*/
|
||||
restoreImages(ids: readonly DraftAttachmentId[]): void {
|
||||
const current = new Set(this.imageIds)
|
||||
this.imageIds = [...ids.filter(id => !current.has(id)), ...this.imageIds]
|
||||
this.publish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the draft as a successful-send commit: no undo unit is recorded and
|
||||
* the undo history is cut, so Ctrl/Cmd-Z cannot resurrect sent content
|
||||
@@ -211,7 +208,19 @@ export class SessionInputShell implements SessionInput {
|
||||
*/
|
||||
submit(mode: InputSubmitMode = 'queue'): void {
|
||||
if (this.snapshot.draft.trim() === '' && this.imageIds.length > 0) {
|
||||
if (this.snapshot.phase === 'plain') this.deps.defaultSink('', [...this.imageIds], mode)
|
||||
if (this.snapshot.phase === 'plain' && !this.imageSendInFlight) {
|
||||
const imageIds = [...this.imageIds]
|
||||
this.imageSendInFlight = true
|
||||
void this.deps.defaultSink('', imageIds, mode, new AbortController().signal).then((outcome) => {
|
||||
this.imageSendInFlight = false
|
||||
if (this.disposed) return
|
||||
if (outcome.kind === 'success') this.commitSend(imageIds)
|
||||
else if (outcome.text !== undefined) this.notify('error', outcome.text)
|
||||
}, (error: unknown) => {
|
||||
this.imageSendInFlight = false
|
||||
if (!this.disposed) this.notify('error', error instanceof Error ? error.message : String(error))
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
// Claimed pre-gate: a claim that does not declare image acceptance never
|
||||
@@ -350,13 +359,21 @@ export class SessionInputShell implements SessionInput {
|
||||
* a scan-derived decoration, never state.
|
||||
* @param text - the plain reference text to splice in (e.g. `/name `).
|
||||
* @param span - pick-time span snapshot (draftRev CAS).
|
||||
* @param keepCompleting - re-track at the caret after the splice so an open
|
||||
* token (a directory pick's trailing slash) reopens the menu.
|
||||
* @returns whether the text was applied.
|
||||
*/
|
||||
insertText(text: string, span: TokenSpan): boolean {
|
||||
insertText(text: string, span: TokenSpan, keepCompleting = false): boolean {
|
||||
const snapshot = this.core.state
|
||||
if (span.draftRev !== snapshot.draftRev) return false
|
||||
const draft = snapshot.draft
|
||||
this.setDraft(draft.slice(0, span.start) + text + draft.slice(span.end))
|
||||
if (keepCompleting) {
|
||||
// Machine-driven draft replacement never passes through onChange, so
|
||||
// re-track at the caret inside the still-open token (see space()).
|
||||
const next = this.snapshot
|
||||
this.deps.inputTriggers?.()?.track(next.draft, span.start + text.length, { tier: guardOf(next.phase) }, next.draftRev)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -421,7 +438,7 @@ export class SessionInputShell implements SessionInput {
|
||||
return
|
||||
}
|
||||
case 'default-sink': {
|
||||
this.sinkSerialized(fx.draft, fx.mode)
|
||||
this.sinkSerialized(fx.attempt, fx.draft, fx.mode)
|
||||
return
|
||||
}
|
||||
default:
|
||||
@@ -436,11 +453,11 @@ export class SessionInputShell implements SessionInput {
|
||||
* send — notice + draft and chips retained, never a silent downgrade to
|
||||
* the clipboard text. Chip-free drafts skip the async detour.
|
||||
*/
|
||||
private sinkSerialized(draft: string, mode: InputSubmitMode): void {
|
||||
private sinkSerialized(attempt: SubmitAttempt, draft: string, mode: InputSubmitMode): void {
|
||||
const imageIds = [...this.imageIds]
|
||||
const occurrences = this.core.state.occurrences
|
||||
if (occurrences.length === 0) {
|
||||
this.deps.defaultSink(draft.trim(), imageIds, mode)
|
||||
this.settleSubmit(attempt, this.deps.defaultSink(draft.trim(), imageIds, mode, attempt.signal), imageIds)
|
||||
return
|
||||
}
|
||||
const inputTriggers = this.deps.inputTriggers?.()
|
||||
@@ -460,13 +477,45 @@ export class SessionInputShell implements SessionInput {
|
||||
cursor = part.offset + 1
|
||||
}
|
||||
out += draft.slice(cursor)
|
||||
this.deps.defaultSink(out.trim(), imageIds, mode)
|
||||
this.settleSubmit(attempt, this.deps.defaultSink(out.trim(), imageIds, mode, attempt.signal), imageIds)
|
||||
},
|
||||
(error: unknown) => {
|
||||
controller.abort()
|
||||
if (this.disposed) return
|
||||
if (this.dead(attempt)) return
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
this.notify('error', message)
|
||||
this.run(this.core.dispatch({ type: 'submit-settled', attempt, ok: false, message }))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Settle one admission attempt; successful sends consume only their captured images. */
|
||||
private settleSubmit(
|
||||
attempt: SubmitAttempt,
|
||||
pending: Promise<SubmitOutcome>,
|
||||
imageIds: readonly DraftAttachmentId[] = [],
|
||||
): void {
|
||||
pending.then(
|
||||
(outcome) => {
|
||||
if (this.dead(attempt)) return
|
||||
if (outcome.kind === 'success' && imageIds.length > 0) {
|
||||
const submitted = new Set(imageIds)
|
||||
this.imageIds = this.imageIds.filter(id => !submitted.has(id))
|
||||
}
|
||||
this.run(this.core.dispatch({
|
||||
type: 'submit-settled',
|
||||
attempt,
|
||||
ok: outcome.kind === 'success',
|
||||
outcome,
|
||||
}))
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (this.dead(attempt)) return
|
||||
this.run(this.core.dispatch({
|
||||
type: 'submit-settled',
|
||||
attempt,
|
||||
ok: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
}))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -519,6 +568,7 @@ export class SessionInputShell implements SessionInput {
|
||||
}
|
||||
this.run(this.core.dispatch({
|
||||
type: 'submit-settled', attempt, ok: outcome.kind === 'success', outcome,
|
||||
...(outcome.kind === 'error' && outcome.text === undefined ? { message: 'command failed' } : {}),
|
||||
}))
|
||||
},
|
||||
(error: unknown) => {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* real host entity, so the sink is one unconditional prompt path.
|
||||
*/
|
||||
import type { ClientContext, ISessions, SessionBinding, SessionFace, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InputTriggerController, SubmitImageAttachment } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type { InputTriggerController, SubmitImageAttachment, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { queueReadFaceOf } from '../queue/store.ts'
|
||||
import type { ComposerKeyboard, DraftAttachmentId, SessionInputResolver, SessionInput } from './contract.ts'
|
||||
@@ -29,7 +29,8 @@ interface ConversationAttachmentFace {
|
||||
text: string,
|
||||
imageIds: readonly DraftAttachmentId[],
|
||||
mode: InputSubmitMode,
|
||||
): Promise<void>
|
||||
signal?: AbortSignal,
|
||||
): Promise<SubmitOutcome>
|
||||
serializeDraftImages(imageIds: readonly DraftAttachmentId[]): Promise<readonly SubmitImageAttachment[]>
|
||||
releaseDraftImage(id: DraftAttachmentId): void
|
||||
}
|
||||
@@ -76,7 +77,7 @@ export class InputHub implements SessionInputResolver {
|
||||
inputTriggers: () => this.controller(actx),
|
||||
popup: () => this.popup(actx),
|
||||
queue: queueReadFaceOf(session),
|
||||
defaultSink: (text, imageIds, mode) => { this.sink(session, text, imageIds, mode) },
|
||||
defaultSink: (text, imageIds, mode, signal) => this.sink(session, text, imageIds, mode, signal),
|
||||
steerQueue: () => { void this.steerQueue(session, shell) },
|
||||
commandImages: {
|
||||
serialize: ids => this.conversation().serializeDraftImages(ids),
|
||||
@@ -105,7 +106,7 @@ export class InputHub implements SessionInputResolver {
|
||||
actx.on('slash/input-consume-token', req =>
|
||||
shell.consumeToken(req.guard) ? true : undefined),
|
||||
actx.on('slash/input-insert-text', req =>
|
||||
shell.insertText(req.text, req.span) ? true : undefined),
|
||||
shell.insertText(req.text, req.span, req.continue === true) ? true : undefined),
|
||||
]
|
||||
return () => {
|
||||
for (const off of offs) off()
|
||||
@@ -166,20 +167,10 @@ export class InputHub implements SessionInputResolver {
|
||||
text: string,
|
||||
imageIds: readonly DraftAttachmentId[],
|
||||
mode: InputSubmitMode,
|
||||
): void {
|
||||
if (text === '' && imageIds.length === 0) return
|
||||
const shell = this.shells.get(session.sessionId)
|
||||
// Commit, not an editable clear: undo must not resurrect sent content.
|
||||
shell?.commitSend(imageIds)
|
||||
void this.conversation().sendSession(session, text, imageIds, mode).catch(() => {
|
||||
if (this.shells.get(session.sessionId) === shell) {
|
||||
shell?.restoreImages(imageIds)
|
||||
if (shell?.snapshot.draft === '') shell.setDraft(text)
|
||||
return
|
||||
}
|
||||
const conversation = this.rootCtx.get('conversation') as ConversationAttachmentFace | undefined
|
||||
for (const id of imageIds) conversation?.releaseDraftImage(id)
|
||||
})
|
||||
signal: AbortSignal,
|
||||
): Promise<SubmitOutcome> {
|
||||
if (text === '' && imageIds.length === 0) return Promise.resolve({ kind: 'success' })
|
||||
return this.conversation().sendSession(session, text, imageIds, mode, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -481,7 +481,9 @@ export class InputMachine {
|
||||
this.phase = 'adjudicating'
|
||||
return [{ type: 'adjudicate', attempt, draft: this.draft }]
|
||||
}
|
||||
return [{ type: 'default-sink', draft: this.draft, mode }]
|
||||
const attempt = this.beginAttempt(mode)
|
||||
this.phase = 'submitting'
|
||||
return [{ type: 'default-sink', attempt, draft: this.draft, mode }]
|
||||
}
|
||||
|
||||
private onAdjudicated(attempt: SubmitAttempt, outcome: Extract<InputEvent, { type: 'adjudicated' }>['outcome']): InputEffect[] {
|
||||
@@ -499,11 +501,18 @@ export class InputMachine {
|
||||
}
|
||||
// 'handled' (source dealt internally), {insert} (no enter-time span
|
||||
// semantics), or a miss: all land plain; only the miss flows to the sink.
|
||||
if (outcome === undefined) {
|
||||
this.phase = 'submitting'
|
||||
return [{
|
||||
type: 'default-sink',
|
||||
attempt,
|
||||
draft: attempt.draftSnapshot,
|
||||
mode: attempt.mode,
|
||||
}]
|
||||
}
|
||||
this.inflight = undefined
|
||||
this.phase = 'plain'
|
||||
return outcome === undefined
|
||||
? [{ type: 'default-sink', draft: attempt.draftSnapshot, mode: attempt.mode }]
|
||||
: []
|
||||
return []
|
||||
}
|
||||
|
||||
private onAdjudicationFailed(attempt: SubmitAttempt, message: string): InputEffect[] {
|
||||
@@ -522,7 +531,13 @@ export class InputMachine {
|
||||
this.phase = 'plain'
|
||||
this.claim = undefined
|
||||
this.occurrences = []
|
||||
this.adopt('')
|
||||
// Text appended after the sent snapshot during the Host round-trip
|
||||
// survives the commit; edits interleaved with committed content cannot
|
||||
// be separated from it, so only a pure suffix is retained.
|
||||
const snapshot = flight.attempt.draftSnapshot
|
||||
this.adopt(this.draft !== snapshot && this.draft.startsWith(snapshot)
|
||||
? this.draft.slice(snapshot.length)
|
||||
: '')
|
||||
// Committed content is gone for good: undo must not resurrect a sent draft.
|
||||
this.log = []
|
||||
this.redoStack = []
|
||||
@@ -532,24 +547,24 @@ export class InputMachine {
|
||||
? [{ type: 'notice', level: ev.outcome.kind === 'error' ? 'error' : 'info', text: ev.outcome.text }]
|
||||
: []
|
||||
}
|
||||
const text = ev.message ?? ev.outcome?.text ?? 'command failed'
|
||||
// Drift guard: keep the enter-time draft (same claim) only while the
|
||||
// live draft still equals it; user input typed during flight wins.
|
||||
const text = ev.message ?? ev.outcome?.text
|
||||
// Keep the same command claim only while the live draft still equals the
|
||||
// enter-time draft; user input typed during flight wins.
|
||||
// Claimed re-entry additionally requires the watch to hold — an
|
||||
// enter-path snapshot may carry leading whitespace the token never had.
|
||||
if (this.draft === flight.attempt.draftSnapshot
|
||||
&& this.claim !== undefined && this.draft.startsWith(this.claim.token)) {
|
||||
this.phase = 'claimed'
|
||||
return [{ type: 'notice', level: 'error', text }]
|
||||
return text === undefined ? [] : [{ type: 'notice', level: 'error', text }]
|
||||
}
|
||||
this.phase = 'plain'
|
||||
this.claim = undefined
|
||||
return [{ type: 'notice', level: 'error', text }]
|
||||
return text === undefined ? [] : [{ type: 'notice', level: 'error', text }]
|
||||
}
|
||||
|
||||
/** Ordinary send accepted: clear as a commit (no undo unit; sent content
|
||||
* must not be resurrectable — same discipline as submit-settled success). */
|
||||
/** Cut undo state after an accepted image-only send. */
|
||||
private onSendCommitted(): InputEffect[] {
|
||||
if (this.phase !== 'plain') return []
|
||||
this.claim = undefined
|
||||
this.occurrences = []
|
||||
this.adopt('')
|
||||
|
||||
@@ -99,6 +99,8 @@ export const zh = {
|
||||
'message.extraBlock': '附加内容块',
|
||||
'message.contextInjection': '上下文注入',
|
||||
'message.contextRecall': '跨会话召回',
|
||||
'message.referenceSummary': '引用会话 · {labels}',
|
||||
'message.referenceSeparator': '、',
|
||||
'message.context.instructions.loaded': '已载入',
|
||||
'message.context.instructions.added': '已新增',
|
||||
'message.context.instructions.updated': '已更新',
|
||||
@@ -274,6 +276,8 @@ export const en = {
|
||||
'message.extraBlock': 'Extra content block',
|
||||
'message.contextInjection': 'Context injection',
|
||||
'message.contextRecall': 'Session recall',
|
||||
'message.referenceSummary': 'Referenced session · {labels}',
|
||||
'message.referenceSeparator': ', ',
|
||||
'message.context.instructions.loaded': 'loaded',
|
||||
'message.context.instructions.added': 'added',
|
||||
'message.context.instructions.updated': 'updated',
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
// error, so scope resolution goes through the sessions service (scopeOf
|
||||
// method) instead of the standalone helper.
|
||||
import type { ISessions, SessionFace, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SubmitImageAttachment } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type { SubmitImageAttachment, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ComposerAttachment } from './contract/slots.ts'
|
||||
import type { QueueAction, QueueItemId } from './contract/queue.ts'
|
||||
@@ -139,22 +139,26 @@ export class ConversationController extends Service implements IConversation {
|
||||
* @param text - serialized prompt text.
|
||||
* @param imageIds - ordered draft-local attachment ids.
|
||||
* @param mode - queue or steer delivery selected by composer policy.
|
||||
* @param signal - optional cancellation for the complete Host admission.
|
||||
* @returns the Host admission outcome; local attachment preparation failures reject.
|
||||
*/
|
||||
async sendSession(
|
||||
session: SessionFace,
|
||||
text: string,
|
||||
imageIds: readonly DraftAttachmentId[],
|
||||
mode: InputSubmitMode,
|
||||
): Promise<void> {
|
||||
signal?: AbortSignal,
|
||||
): Promise<SubmitOutcome> {
|
||||
const attachments = this.draftImages(imageIds)
|
||||
if (attachments.length !== imageIds.length) {
|
||||
throw new Error('conversation.sendSession: one or more draft images are no longer available')
|
||||
}
|
||||
const uploaded = await this.serializeImages(attachments.map(attachment => attachment.file))
|
||||
const content = [...uploaded, ...(text === '' ? [] : [{ type: 'text' as const, text }])]
|
||||
const result = await session.prompt(content, mode)
|
||||
if (!result.ok) throw new Error(`conversation.send failed: ${result.error.code}: ${result.error.message}`)
|
||||
const result = await session.prompt(content, mode, signal)
|
||||
if (!result.ok) return { kind: 'error' }
|
||||
this.releaseDraftImages(attachments)
|
||||
return { kind: 'success' }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// apply inject factories exercised end to end against the terminal thin
|
||||
// API: the strict session API (views triple, draft mirror), the
|
||||
// provide-channel input face (machine-sink submit choreography incl.
|
||||
// optimistic clear + failure restore), the resident API (selectWorkspace
|
||||
// transactional clear + failure retention), the resident API (selectWorkspace
|
||||
// draft carrying), the composer-bar stop face, openDetails = select action +
|
||||
// layout orchestration, and the closeDetails details API. Complements
|
||||
// chat-apply.spec.tsx (registration) and selection-survival.spec.tsx (store
|
||||
@@ -150,7 +150,7 @@ describe('conversation slot inject API', () => {
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
it('the provide-channel input face submits through the machine sink: trim, optimistic clear, failure restore without clobber', async () => {
|
||||
it('the provide-channel input face submits through the machine sink: trim, transactional clear, failure retains the draft', async () => {
|
||||
const b = await bench()
|
||||
const { injected } = b.conversationApi(ROOT)
|
||||
const { state, actions } = b.inputApi(ROOT)
|
||||
@@ -159,20 +159,23 @@ describe('conversation slot inject API', () => {
|
||||
actions.submit()
|
||||
expect(b.sessionFake.prompt).not.toHaveBeenCalled()
|
||||
expect(state.getSnapshot().draft).toBe(' ')
|
||||
// Success: cleared and stays cleared.
|
||||
// Success: the draft clears only after the sink settles.
|
||||
actions.setDraft('hello')
|
||||
actions.submit()
|
||||
expect(state.getSnapshot().draft).toBe('')
|
||||
await Promise.resolve()
|
||||
expect(b.sessionFake.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'queue')
|
||||
// Failure: restored (draft still empty when the rejection lands).
|
||||
await vi.waitFor(() => {
|
||||
expect(state.getSnapshot().draft).toBe('')
|
||||
})
|
||||
expect(b.sessionFake.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'queue', expect.any(AbortSignal))
|
||||
// Failure: the draft is retained through the round-trip.
|
||||
b.sessionFake.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'b', details: { reason: 'b' } } })
|
||||
actions.setDraft('retry me')
|
||||
actions.submit()
|
||||
await vi.waitFor(() => {
|
||||
expect(state.getSnapshot().draft).toBe('retry me')
|
||||
expect(b.sessionFake.prompt).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
// Failure landing after new typing: no clobber (restore fills empty only).
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
expect(state.getSnapshot().draft).toBe('retry me')
|
||||
// Failure landing after new typing: no clobber (the interleaved edit wins).
|
||||
b.sessionFake.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'b', details: { reason: 'b' } } })
|
||||
actions.submit()
|
||||
actions.setDraft('typed during flight')
|
||||
|
||||
@@ -48,10 +48,11 @@ const RETRY_ID = 'retry-fixture' as Extract<ConversationNode, { kind: 'model-ret
|
||||
interface MessageItemProps {
|
||||
readonly node: ConversationNode
|
||||
readonly t: ChatNodeViewProps['t']
|
||||
readonly referenceLabels?: readonly string[]
|
||||
}
|
||||
|
||||
/** Legacy-node fixture adapter for the independently registered renderers. */
|
||||
function MessageItem({ node, t: translate }: MessageItemProps) {
|
||||
function MessageItem({ node, t: translate, referenceLabels }: MessageItemProps) {
|
||||
const kind = node.kind === 'assistant' ? 'assistant-step' : node.kind
|
||||
const viewNode: ChatConversationViewNode = {
|
||||
key: `fixture:${node.kind}:${node.seq}`,
|
||||
@@ -61,7 +62,11 @@ function MessageItem({ node, t: translate }: MessageItemProps) {
|
||||
anchorSeq: node.seq,
|
||||
location: { kind: 'session' },
|
||||
visibility: 'visible',
|
||||
data: node.kind === 'model-retry' ? { attempts: [node], current: node } : node,
|
||||
data: node.kind === 'model-retry'
|
||||
? { attempts: [node], current: node }
|
||||
: (node.kind === 'user' || node.kind === 'steering') && referenceLabels !== undefined
|
||||
? { ...node, referenceLabels }
|
||||
: node,
|
||||
}
|
||||
const props = { node: viewNode, t: translate, renderMessageImages } as ChatNodeViewProps
|
||||
switch (node.kind) {
|
||||
@@ -82,6 +87,25 @@ function MessageItem({ node, t: translate }: MessageItemProps) {
|
||||
}
|
||||
|
||||
describe('MessageItem arms', () => {
|
||||
it('renders an adjacent session mention as a chip even without trailing whitespace', () => {
|
||||
const view = render(
|
||||
<MessageItem
|
||||
t={t}
|
||||
referenceLabels={['你好']}
|
||||
node={{
|
||||
kind: 'user',
|
||||
seq: 1,
|
||||
time: 1_000,
|
||||
content: [{ type: 'text', text: '@你好这个在讲啥' }] as never,
|
||||
source: null,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
expect(view.container.querySelector('[data-ref-chip="session"]')?.textContent).toBe('@你好')
|
||||
expect(view.getByText('这个在讲啥')).toBeTruthy()
|
||||
expect(view.getByText('引用会话 · 你好')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('user bubbles expose clock / copy and neither branch nor edit; copy writes the text', () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
|
||||
@@ -514,6 +514,31 @@ describe('built-in conversation node Definitions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('associates session-reference labels inside the adjacent direct-message node', () => {
|
||||
const referenceSource = {
|
||||
kind: 'session-reference',
|
||||
form: 'recall',
|
||||
version: 1,
|
||||
references: [
|
||||
{ sessionId: 'source-a', label: 'Research' },
|
||||
{ sessionId: 'source-b', label: 'Review' },
|
||||
],
|
||||
}
|
||||
const value = assembler([
|
||||
at(1, 'user/message', {
|
||||
...textMessage('reference-context', 'snapshot'),
|
||||
source: referenceSource,
|
||||
}, { surfaceOp: 'append' }),
|
||||
at(2, 'user/message', textMessage('citing-user', '@Research and @Review'), { surfaceOp: 'append' }),
|
||||
at(4, 'user/message', textMessage('later-user', 'unrelated'), { surfaceOp: 'append' }),
|
||||
])
|
||||
|
||||
const current = snapshot(value)
|
||||
const users = [...current.nodes.values()].filter(candidate => candidate.kind === 'user')
|
||||
expect(users[0]?.data).toMatchObject({ referenceLabels: ['Research', 'Review'] })
|
||||
expect(users[1]?.data).not.toHaveProperty('referenceLabels')
|
||||
})
|
||||
|
||||
it('keeps replacement copies out of Chat business nodes', () => {
|
||||
const value = assembler([
|
||||
at(1, 'turn/start', { turn: 1 }),
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import type {
|
||||
ComposerAttachment, ComposerAttachmentsOwnerProps,
|
||||
@@ -104,7 +105,12 @@ function row(id: string): ConversationSnapshot['queue'][number] {
|
||||
|
||||
/** Real machine behind the bar entry: sink spy, no slash pipeline (plain text goes straight to the sink). */
|
||||
function bench(over?: BenchOptions) {
|
||||
const sink = vi.fn()
|
||||
const sink = vi.fn<(
|
||||
text: string,
|
||||
imageIds: readonly DraftAttachmentId[],
|
||||
mode: 'queue' | 'steer',
|
||||
signal: AbortSignal,
|
||||
) => Promise<SubmitOutcome>>(() => Promise.resolve({ kind: 'success' }))
|
||||
const lex = over?.lexicon
|
||||
const session = createSnapshotStore<ConversationSnapshot>(snapshotOf({
|
||||
running: over?.running ?? false,
|
||||
@@ -209,9 +215,11 @@ function bench(over?: BenchOptions) {
|
||||
}
|
||||
|
||||
function attachmentOwner(slotCalls: readonly { key: string; owner: unknown }[]): ComposerAttachmentsOwnerProps {
|
||||
const call = slotCalls.find(candidate => candidate.key === 'conversation.input.attachments')
|
||||
if (call === undefined) throw new Error('attachment slot was not rendered')
|
||||
return call.owner as ComposerAttachmentsOwnerProps
|
||||
for (let i = slotCalls.length - 1; i >= 0; i -= 1) {
|
||||
const call = slotCalls[i]
|
||||
if (call?.key === 'conversation.input.attachments') return call.owner as ComposerAttachmentsOwnerProps
|
||||
}
|
||||
throw new Error('attachment slot was not rendered')
|
||||
}
|
||||
|
||||
describe('image draft rail', () => {
|
||||
@@ -333,18 +341,28 @@ describe('image draft rail', () => {
|
||||
expect(attachmentOwner(result.slotCalls).canAcceptDrop).toBe(false)
|
||||
})
|
||||
|
||||
it('sends an image-only draft and exposes removal through the attachment slot', () => {
|
||||
it('sends an image-only draft and exposes removal through the attachment slot', async () => {
|
||||
const file = new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' })
|
||||
const attachment = { kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file, previewUrl: 'blob:draft-1' }
|
||||
const result = bench({ attachments: [attachment] })
|
||||
const extra = new File([Uint8Array.of(2)], 'extra.png', { type: 'image/png' })
|
||||
const attachments = [
|
||||
{ kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file, previewUrl: 'blob:draft-1' },
|
||||
{ kind: 'image' as const, id: 'draft-2' as DraftAttachmentId, file: extra, previewUrl: 'blob:draft-2' },
|
||||
]
|
||||
const result = bench({ attachments })
|
||||
const { view, textarea, sink, removeImage } = result
|
||||
expect((view.getByRole('button', { name: '发送消息' }) as HTMLButtonElement).disabled).toBe(false)
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('', ['draft-1'], 'queue')
|
||||
const owner = attachmentOwner(result.slotCalls)
|
||||
expect(owner.attachments).toEqual([attachment])
|
||||
owner.onRemoveImage(attachment.id)
|
||||
expect(removeImage).toHaveBeenCalledWith('draft-1')
|
||||
act(() => { owner.onRemoveImage('draft-2' as DraftAttachmentId) })
|
||||
expect(removeImage).toHaveBeenCalledWith('draft-2')
|
||||
let settle!: (outcome: SubmitOutcome) => void
|
||||
sink.mockImplementationOnce(() => new Promise<SubmitOutcome>((resolve) => { settle = resolve }))
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('', ['draft-1'], 'queue', expect.any(AbortSignal))
|
||||
expect(attachmentOwner(result.slotCalls).attachments).toEqual([attachments[0]])
|
||||
await act(async () => { settle({ kind: 'success' }) })
|
||||
await vi.waitFor(() => {
|
||||
expect(attachmentOwner(result.slotCalls).attachments).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
it('announces an image-intake rejection as a fading toast, repeatable for the same reason', () => {
|
||||
@@ -439,8 +457,11 @@ describe('Enter semantics', () => {
|
||||
it('plain Enter submits queue mode through the machine; repeat and empty are suppressed', () => {
|
||||
const { textarea, sink } = bench({ draft: 'hello' })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('hello', [], 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('hello', [], 'queue', expect.any(AbortSignal))
|
||||
// The submitting-phase lock, not draft emptiness, suppresses the repeat:
|
||||
// the draft is still uncleared while the sink round-trip is in flight.
|
||||
fireEvent.keyDown(textarea, { key: 'Enter', repeat: true })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledTimes(1)
|
||||
const empty = bench({ draft: ' ' })
|
||||
fireEvent.keyDown(empty.textarea, { key: 'Enter' })
|
||||
@@ -464,15 +485,15 @@ describe('Enter semantics', () => {
|
||||
it('Ctrl/Meta+Enter sends normally while idle and steers while running', () => {
|
||||
const idle = bench({ draft: 'hello' })
|
||||
fireEvent.keyDown(idle.textarea, { key: 'Enter', metaKey: true })
|
||||
expect(idle.sink).toHaveBeenCalledWith('hello', [], 'queue')
|
||||
expect(idle.sink).toHaveBeenCalledWith('hello', [], 'queue', expect.any(AbortSignal))
|
||||
|
||||
const busyCtrl = bench({ running: true, draft: 'steer with ctrl' })
|
||||
fireEvent.keyDown(busyCtrl.textarea, { key: 'Enter', ctrlKey: true })
|
||||
expect(busyCtrl.sink).toHaveBeenCalledWith('steer with ctrl', [], 'steer')
|
||||
expect(busyCtrl.sink).toHaveBeenCalledWith('steer with ctrl', [], 'steer', expect.any(AbortSignal))
|
||||
|
||||
const busyMeta = bench({ running: true, draft: 'steer with cmd' })
|
||||
fireEvent.keyDown(busyMeta.textarea, { key: 'Enter', metaKey: true })
|
||||
expect(busyMeta.sink).toHaveBeenCalledWith('steer with cmd', [], 'steer')
|
||||
expect(busyMeta.sink).toHaveBeenCalledWith('steer with cmd', [], 'steer', expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
it('empty-draft Cmd/Ctrl+Enter steers the whole queue instead of submitting', () => {
|
||||
@@ -537,7 +558,7 @@ describe('Enter semantics', () => {
|
||||
const steerQueue = vi.fn()
|
||||
const { textarea, sink } = bench({ running: true, queue: [row('q-1')], draft: '插话', steerQueue })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter', ctrlKey: true })
|
||||
expect(sink).toHaveBeenCalledWith('插话', [], 'steer')
|
||||
expect(sink).toHaveBeenCalledWith('插话', [], 'steer', expect.any(AbortSignal))
|
||||
expect(steerQueue).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -585,7 +606,7 @@ describe('running and lock semantics', () => {
|
||||
expect(textarea.disabled).toBe(false)
|
||||
fireEvent.change(textarea, { target: { value: '排队消息2' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue', expect.any(AbortSignal))
|
||||
expect(button.getAttribute('aria-label')).toBe('停止生成')
|
||||
fireEvent.click(button)
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
@@ -594,17 +615,17 @@ describe('running and lock semantics', () => {
|
||||
it('running plain Enter follows the busy-state Steer preference', () => {
|
||||
const { textarea, sink } = bench({ running: true, busyEnter: 'steer', draft: '直接插话' })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('直接插话', [], 'steer')
|
||||
expect(sink).toHaveBeenCalledWith('直接插话', [], 'steer', expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
it('running Cmd/Ctrl+Enter uses the opposite of the busy-state Enter preference', () => {
|
||||
const meta = bench({ running: true, busyEnter: 'steer', draft: '排到下一轮' })
|
||||
fireEvent.keyDown(meta.textarea, { key: 'Enter', metaKey: true })
|
||||
expect(meta.sink).toHaveBeenCalledWith('排到下一轮', [], 'queue')
|
||||
expect(meta.sink).toHaveBeenCalledWith('排到下一轮', [], 'queue', expect.any(AbortSignal))
|
||||
|
||||
const ctrl = bench({ running: true, busyEnter: 'steer', draft: 'also queue' })
|
||||
fireEvent.keyDown(ctrl.textarea, { key: 'Enter', ctrlKey: true })
|
||||
expect(ctrl.sink).toHaveBeenCalledWith('also queue', [], 'queue')
|
||||
expect(ctrl.sink).toHaveBeenCalledWith('also queue', [], 'queue', expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
it('running continuable subagent keeps Send beside an independent Stop', () => {
|
||||
@@ -624,7 +645,7 @@ describe('running and lock semantics', () => {
|
||||
expect(interruptButton).not.toBeNull()
|
||||
expect(textarea.disabled).toBe(false)
|
||||
fireEvent.click(button)
|
||||
expect(sink).toHaveBeenCalledWith('后续消息', [], 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('后续消息', [], 'queue', expect.any(AbortSignal))
|
||||
fireEvent.click(interruptButton!)
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -681,11 +702,11 @@ describe('running and lock semantics', () => {
|
||||
}
|
||||
const plain = bench({ running: true, busyEnter: 'steer', draft: 'plain', subagent })
|
||||
fireEvent.keyDown(plain.textarea, { key: 'Enter' })
|
||||
expect(plain.sink).toHaveBeenCalledWith('plain', [], 'queue')
|
||||
expect(plain.sink).toHaveBeenCalledWith('plain', [], 'queue', expect.any(AbortSignal))
|
||||
|
||||
const accelerated = bench({ running: true, draft: 'accelerated', subagent })
|
||||
fireEvent.keyDown(accelerated.textarea, { key: 'Enter', metaKey: true })
|
||||
expect(accelerated.sink).toHaveBeenCalledWith('accelerated', [], 'queue')
|
||||
expect(accelerated.sink).toHaveBeenCalledWith('accelerated', [], 'queue', expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
it('disabled (session removed) locks the textarea and chrome', () => {
|
||||
@@ -698,7 +719,7 @@ describe('running and lock semantics', () => {
|
||||
it('idle primary sends and disables on empty draft', () => {
|
||||
const { button, sink } = bench({ draft: 'go' })
|
||||
fireEvent.click(button)
|
||||
expect(sink).toHaveBeenCalledWith('go', [], 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('go', [], 'queue', expect.any(AbortSignal))
|
||||
const empty = bench()
|
||||
expect(empty.button.disabled).toBe(true)
|
||||
})
|
||||
|
||||
@@ -72,16 +72,17 @@ describe('input-machine: plain × enter', () => {
|
||||
it('non-command text falls to the default sink', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: 'hello world' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' }))
|
||||
.toEqual([{ type: 'default-sink', draft: 'hello world', mode: 'queue' }])
|
||||
expect(m.state.phase).toBe('plain')
|
||||
const effect = effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink')
|
||||
expect(effect).toMatchObject({ draft: 'hello world', mode: 'queue' })
|
||||
expect(effect.attempt.draftSnapshot).toBe('hello world')
|
||||
expect(m.state.phase).toBe('submitting')
|
||||
})
|
||||
|
||||
it('retains an explicit steer mode on the default sink effect', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: 'steer now' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'steer' }))
|
||||
.toEqual([{ type: 'default-sink', draft: 'steer now', mode: 'steer' }])
|
||||
expect(effectAt(m.dispatch({ type: 'enter', mode: 'steer' }), 0, 'default-sink'))
|
||||
.toMatchObject({ draft: 'steer now', mode: 'steer' })
|
||||
})
|
||||
|
||||
it('leading "/" enters adjudicating with a minted attempt carrying the draft snapshot', () => {
|
||||
@@ -104,8 +105,8 @@ describe('input-machine: plain × enter', () => {
|
||||
it('a non-whitespace prefix before "/" is not leading — default sink', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: '第一行\n/goal x' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' }))
|
||||
.toEqual([{ type: 'default-sink', draft: '第一行\n/goal x', mode: 'queue' }])
|
||||
expect(effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink'))
|
||||
.toMatchObject({ draft: '第一行\n/goal x', mode: 'queue' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -134,9 +135,12 @@ describe('input-machine: adjudication outcomes', () => {
|
||||
it('undefined outcome falls back to the default sink', () => {
|
||||
const m = new InputMachine()
|
||||
const attempt = enterAdjudicating(m, '/unknown thing', 'steer')
|
||||
expect(m.dispatch({ type: 'adjudicated', attempt, outcome: undefined }))
|
||||
.toEqual([{ type: 'default-sink', draft: '/unknown thing', mode: 'steer' }])
|
||||
expect(m.state.phase).toBe('plain')
|
||||
expect(effectAt(
|
||||
m.dispatch({ type: 'adjudicated', attempt, outcome: undefined }),
|
||||
0,
|
||||
'default-sink',
|
||||
)).toMatchObject({ attempt, draft: '/unknown thing', mode: 'steer' })
|
||||
expect(m.state.phase).toBe('submitting')
|
||||
})
|
||||
|
||||
it("'handled' lands plain with zero effects (popup shell path)", () => {
|
||||
@@ -484,6 +488,22 @@ describe('input-machine: undo / redo', () => {
|
||||
expect(m.dispatch({ type: 'undo' })).toEqual([])
|
||||
expect(m.state.draft).toBe('')
|
||||
})
|
||||
|
||||
it('keeps a suffix typed during the round-trip and drops interleaved edits with the commit', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: 'hello' })
|
||||
const effect = effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink')
|
||||
m.dispatch({ type: 'draft-changed', draft: 'hello world' })
|
||||
m.dispatch({ type: 'submit-settled', attempt: effect.attempt, ok: true })
|
||||
expect(m.state.draft).toBe(' world')
|
||||
|
||||
const n = new InputMachine()
|
||||
n.dispatch({ type: 'draft-changed', draft: 'hello' })
|
||||
const second = effectAt(n.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink')
|
||||
n.dispatch({ type: 'draft-changed', draft: 'hXello' })
|
||||
n.dispatch({ type: 'submit-settled', attempt: second.attempt, ok: true })
|
||||
expect(n.state.draft).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('input-machine: paste plane', () => {
|
||||
|
||||
@@ -80,7 +80,7 @@ function bench(over?: {
|
||||
submit?: (args: string) => Promise<SubmitOutcome>
|
||||
serialize?: (ids: readonly DraftAttachmentId[]) => Promise<readonly SubmitImageAttachment[]>
|
||||
}) {
|
||||
const sink = vi.fn()
|
||||
const sink = vi.fn(() => Promise.resolve<SubmitOutcome>({ kind: 'success' }))
|
||||
const serialize = vi.fn(over?.serialize ?? (() => Promise.resolve<readonly SubmitImageAttachment[]>([])))
|
||||
const release = vi.fn()
|
||||
const shell = new SessionInputShell({ actx: SCTX, defaultSink: sink, commandImages: { serialize, release, unsupportedNotice: (token: string) => `${token.trim()} images-unsupported` } })
|
||||
@@ -104,13 +104,15 @@ function bench(over?: {
|
||||
}
|
||||
|
||||
describe('matrix row: plain', () => {
|
||||
it('enter falls to the default sink; no claim on the currency; edits free', () => {
|
||||
it('enter falls to the default sink; no claim on the currency; edits free', async () => {
|
||||
const { textarea, shell, sink } = bench()
|
||||
fireEvent.change(textarea, { target: { value: '普通消息' } })
|
||||
expect(shell.snapshot.claim).toBeUndefined()
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('普通消息', [], 'queue')
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
expect(sink).toHaveBeenCalledWith('普通消息', [], 'queue', expect.any(AbortSignal))
|
||||
expect(shell.snapshot.phase).toBe('submitting')
|
||||
await vi.waitFor(() => { expect(shell.snapshot.phase).toBe('plain') })
|
||||
expect(shell.snapshot.claim).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,7 +301,7 @@ describe('matrix row: locked (session disabled)', () => {
|
||||
expect((textarea).disabled).toBe(false)
|
||||
fireEvent.change(textarea, { target: { value: '排队' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队', [], 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('排队', [], 'queue', expect.any(AbortSignal))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Reference-submit transaction coverage: chips serialize through their
|
||||
* owner, stay resident through Host rejection, and clear only after an
|
||||
* accepted prompt.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InputTriggerController, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import type { DraftAttachmentId } from '../src/client/input/contract.ts'
|
||||
import { PLACEHOLDER } from '../src/client/input/machine.ts'
|
||||
|
||||
const mention = '@[Research](dsh-session:InNvdXJjZSI)'
|
||||
const commandImages = {
|
||||
serialize: () => Promise.resolve([]),
|
||||
release: () => {},
|
||||
unsupportedNotice: (token: string) => `${token.trim()} images-unsupported`,
|
||||
}
|
||||
|
||||
function chip(shell: SessionInputShell): void {
|
||||
shell.setDraft('@res')
|
||||
const accepted = shell.insertReference({
|
||||
source: 'reference',
|
||||
ref: mention,
|
||||
label: '@Research',
|
||||
clipboardText: mention,
|
||||
}, {
|
||||
start: 0,
|
||||
end: 4,
|
||||
draftRev: shell.snapshot.draftRev,
|
||||
})
|
||||
expect(accepted).toBe(true)
|
||||
}
|
||||
|
||||
describe('reference submission', () => {
|
||||
it('retains the chip on Host failure and clears it only after a later accepted retry', async () => {
|
||||
const serializeReference = vi.fn(() => Promise.resolve(mention))
|
||||
const sink = vi.fn<(
|
||||
_text: string,
|
||||
_imageIds: readonly DraftAttachmentId[],
|
||||
_mode: 'queue' | 'steer',
|
||||
_signal: AbortSignal,
|
||||
) => Promise<SubmitOutcome>>()
|
||||
.mockResolvedValueOnce({ kind: 'error', text: 'snapshot unavailable' })
|
||||
.mockResolvedValueOnce({ kind: 'success' })
|
||||
const inputTriggers = {
|
||||
serializeReference,
|
||||
track: vi.fn(),
|
||||
} as unknown as InputTriggerController
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
inputTriggers: () => inputTriggers,
|
||||
defaultSink: sink,
|
||||
commandImages,
|
||||
})
|
||||
chip(shell)
|
||||
expect(shell.snapshot).toMatchObject({
|
||||
draft: `${PLACEHOLDER} `,
|
||||
occurrences: [{ source: 'reference', ref: mention, label: '@Research' }],
|
||||
})
|
||||
|
||||
shell.submit('queue')
|
||||
expect(shell.snapshot.phase).toBe('submitting')
|
||||
await vi.waitFor(() => {
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
expect(sink).toHaveBeenNthCalledWith(1, mention, [], 'queue', expect.any(AbortSignal))
|
||||
expect(shell.snapshot).toMatchObject({
|
||||
draft: `${PLACEHOLDER} `,
|
||||
occurrences: [{ source: 'reference', ref: mention, label: '@Research' }],
|
||||
})
|
||||
expect(shell.notices.getSnapshot()).toMatchObject({
|
||||
level: 'error',
|
||||
text: 'snapshot unavailable',
|
||||
})
|
||||
|
||||
shell.submit('queue')
|
||||
await vi.waitFor(() => {
|
||||
expect(shell.snapshot.draft).toBe('')
|
||||
})
|
||||
expect(sink).toHaveBeenNthCalledWith(2, mention, [], 'queue', expect.any(AbortSignal))
|
||||
expect(shell.snapshot.occurrences).toEqual([])
|
||||
expect(serializeReference).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('blocks submission and retains the chip when its owner cannot serialize it', async () => {
|
||||
const sink = vi.fn()
|
||||
const inputTriggers = {
|
||||
serializeReference: () => Promise.reject(new Error('reference codec unavailable')),
|
||||
track: vi.fn(),
|
||||
} as unknown as InputTriggerController
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
inputTriggers: () => inputTriggers,
|
||||
defaultSink: sink,
|
||||
commandImages,
|
||||
})
|
||||
chip(shell)
|
||||
shell.submit()
|
||||
await vi.waitFor(() => {
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
expect(sink).not.toHaveBeenCalled()
|
||||
expect(shell.snapshot.draft).toBe(`${PLACEHOLDER} `)
|
||||
expect(shell.snapshot.occurrences).toHaveLength(1)
|
||||
expect(shell.notices.getSnapshot()).toMatchObject({
|
||||
level: 'error',
|
||||
text: 'reference codec unavailable',
|
||||
})
|
||||
})
|
||||
|
||||
it('aborts Host-side preparation when the input shell is disposed', () => {
|
||||
let signal: AbortSignal | undefined
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
defaultSink: (_text, _imageIds, _mode, received) => {
|
||||
signal = received
|
||||
return new Promise<SubmitOutcome>(() => {})
|
||||
},
|
||||
commandImages,
|
||||
})
|
||||
shell.setDraft('send this')
|
||||
shell.submit()
|
||||
expect(signal?.aborted).toBe(false)
|
||||
shell.dispose()
|
||||
expect(signal?.aborted).toBe(true)
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
expect(shell.snapshot.draft).toBe('send this')
|
||||
})
|
||||
|
||||
it('retains a rejected default message without duplicating its prompt error notice', async () => {
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
defaultSink: () => Promise.resolve({ kind: 'error' }),
|
||||
commandImages,
|
||||
})
|
||||
shell.setDraft('retry this')
|
||||
shell.submit()
|
||||
await vi.waitFor(() => {
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
expect(shell.snapshot.draft).toBe('retry this')
|
||||
expect(shell.notices.getSnapshot()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('submit transaction hardening', () => {
|
||||
it('sends one image-only prompt per settlement, ignoring Enter during the round-trip', async () => {
|
||||
let settle!: (outcome: SubmitOutcome) => void
|
||||
const sink = vi.fn(() => new Promise<SubmitOutcome>((resolve) => { settle = resolve }))
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
defaultSink: sink,
|
||||
commandImages,
|
||||
})
|
||||
expect(shell.addImages(['img-1' as DraftAttachmentId])).toBe(true)
|
||||
shell.submit('queue')
|
||||
shell.submit('queue')
|
||||
expect(sink).toHaveBeenCalledTimes(1)
|
||||
settle({ kind: 'success' })
|
||||
await vi.waitFor(() => {
|
||||
expect(shell.snapshot.imageIds).toEqual([])
|
||||
})
|
||||
|
||||
expect(shell.addImages(['img-2' as DraftAttachmentId])).toBe(true)
|
||||
shell.submit('queue')
|
||||
expect(sink).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('retains an image-only rejection without duplicating its prompt error notice', async () => {
|
||||
const sink = vi.fn(() => Promise.resolve<SubmitOutcome>({ kind: 'error' }))
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
defaultSink: sink,
|
||||
commandImages,
|
||||
})
|
||||
const imageId = 'img-1' as DraftAttachmentId
|
||||
shell.addImages([imageId])
|
||||
shell.submit()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(shell.snapshot.imageIds).toEqual([imageId])
|
||||
expect(shell.notices.getSnapshot()).toBeNull()
|
||||
})
|
||||
|
||||
it('re-tracks at the caret when a continuing insert-text splice lands (directory descent)', () => {
|
||||
const track = vi.fn()
|
||||
const shell = new SessionInputShell({
|
||||
actx: {} as ClientContext,
|
||||
inputTriggers: () => ({ track } as unknown as InputTriggerController),
|
||||
defaultSink: vi.fn(),
|
||||
commandImages,
|
||||
})
|
||||
shell.setDraft('@sr')
|
||||
const applied = shell.insertText('@src/', { start: 0, end: 3, draftRev: shell.snapshot.draftRev }, true)
|
||||
expect(applied).toBe(true)
|
||||
expect(shell.snapshot.draft).toBe('@src/')
|
||||
expect(track).toHaveBeenCalledWith('@src/', 5, { tier: 'plain' }, shell.snapshot.draftRev)
|
||||
|
||||
track.mockClear()
|
||||
shell.insertText(' plain ', { start: 0, end: 0, draftRev: shell.snapshot.draftRev })
|
||||
expect(track).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -119,7 +119,7 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo
|
||||
register?.(inputTriggers)
|
||||
const actx = sessions.scope(sessionId)!
|
||||
const controller = inputTriggers.sessionOf(actx)
|
||||
const sink = vi.fn()
|
||||
const sink = vi.fn(() => Promise.resolve<SubmitOutcome>({ kind: 'success' }))
|
||||
const serialize = vi.fn((ids: readonly DraftAttachmentId[]) => Promise.resolve(ids.map(() => PNG)))
|
||||
const release = vi.fn()
|
||||
const shell = new SessionInputShell({ actx, inputTriggers: () => controller, defaultSink: sink, commandImages: { serialize, release, unsupportedNotice: (token: string) => `${token.trim()} images-unsupported` } })
|
||||
@@ -165,6 +165,7 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo
|
||||
controller.toggleSource('command', {
|
||||
trigger: '/',
|
||||
query: '',
|
||||
quoted: false,
|
||||
position: snapshot.draft.slice(0, selection.start).trim() === '' ? 'leading' : 'inline',
|
||||
span: { ...selection, draftRev: snapshot.draftRev },
|
||||
})
|
||||
@@ -262,7 +263,7 @@ describe('scenario D: execute-kind /compact', () => {
|
||||
act(() => { b2.shell.setDraft('/compact 现在') })
|
||||
fireEvent.keyDown(b2.textarea, { key: 'Enter' })
|
||||
// execute with trailing → matchEnter answers undefined → default sink.
|
||||
await vi.waitFor(() => { expect(b2.sink).toHaveBeenCalledWith('/compact 现在', [], 'queue') })
|
||||
await vi.waitFor(() => { expect(b2.sink).toHaveBeenCalledWith('/compact 现在', [], 'queue', expect.any(AbortSignal)) })
|
||||
expect(b2.executed).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -343,8 +344,8 @@ describe('scenario I: unknown /xyz + enter', () => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft('/xyz 干点啥') })
|
||||
fireEvent.keyDown(b.textarea, { key: 'Enter' })
|
||||
await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledWith('/xyz 干点啥', [], 'queue') })
|
||||
expect(b.shell.snapshot.phase).toBe('plain')
|
||||
await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledWith('/xyz 干点啥', [], 'queue', expect.any(AbortSignal)) })
|
||||
await vi.waitFor(() => { expect(b.shell.snapshot.phase).toBe('plain') })
|
||||
expect(b.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import type { ViewTab } from '../src/client/contract/views.ts'
|
||||
|
||||
/** Machine-backed wiring over a sink spy. */
|
||||
function fakeWiring() {
|
||||
const sink = vi.fn()
|
||||
const sink = vi.fn(() => Promise.resolve({ kind: 'success' as const }))
|
||||
const shell = new SessionInputShell({ actx: {} as ClientContext, defaultSink: sink, commandImages: { serialize: () => Promise.resolve([]), release: () => {}, unsupportedNotice: (token: string) => `${token.trim()} images-unsupported` } })
|
||||
return { wiring: shell, sink, shell }
|
||||
}
|
||||
@@ -309,7 +309,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
fireEvent.change(box, { target: { value: 'ordinary revised' } })
|
||||
expect(b.chat.store.getSnapshot().draft).toBe('ordinary revised')
|
||||
fireEvent.keyDown(box, { key: 'Enter' })
|
||||
expect(b.sink).toHaveBeenCalledWith('ordinary revised', [], 'queue')
|
||||
expect(b.sink).toHaveBeenCalledWith('ordinary revised', [], 'queue', expect.any(AbortSignal))
|
||||
expect((b.view.getByRole('button', { name: 'Child' }) as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(b.view.queryByText('Root')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
@@ -61,7 +62,8 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.2.0",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -60,6 +60,20 @@
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
flex: none;
|
||||
min-height: 26px;
|
||||
padding: 6px 10px 2px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.sectionTitle:not(:first-child) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.itemIcon {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
|
||||
@@ -87,25 +87,29 @@ export function MenuView({ menu, onPick, onDismiss, t }: MenuViewProps) {
|
||||
: group.items.map((item, index) => {
|
||||
const active = highlight !== null && highlight.source === group.source && highlight.index === index
|
||||
return (
|
||||
<button
|
||||
key={`${group.source}:${item.name}`}
|
||||
id={optionId(group.source, index)}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
className={clsx(css.item, active && css.active)}
|
||||
// mousedown, not click: the textarea keeps focus (combobox
|
||||
// pattern) — preventing default stops the focus steal, and the
|
||||
// pick runs before any blur-driven teardown.
|
||||
onMouseDown={(ev) => {
|
||||
ev.preventDefault()
|
||||
onPick(group.source, index)
|
||||
}}
|
||||
>
|
||||
{item.icon !== undefined && <span className={css.itemIcon} aria-hidden>{item.icon}</span>}
|
||||
<span className={css.itemName}>{item.name}</span>
|
||||
{item.description !== undefined && <span className={css.itemDescription}>{item.description}</span>}
|
||||
</button>
|
||||
<Fragment key={optionId(group.source, index)}>
|
||||
{item.section !== undefined && item.section !== group.items[index - 1]?.section
|
||||
? <div className={css.sectionTitle} role="presentation">{item.section}</div>
|
||||
: null}
|
||||
<button
|
||||
id={optionId(group.source, index)}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
className={clsx(css.item, active && css.active)}
|
||||
// mousedown, not click: the textarea keeps focus (combobox
|
||||
// pattern) — preventing default stops the focus steal, and the
|
||||
// pick runs before any blur-driven teardown.
|
||||
onMouseDown={(ev) => {
|
||||
ev.preventDefault()
|
||||
onPick(group.source, index)
|
||||
}}
|
||||
>
|
||||
{item.icon !== undefined && <span className={css.itemIcon} aria-hidden>{item.icon}</span>}
|
||||
<span className={css.itemName}>{item.name}</span>
|
||||
{item.description !== undefined && <span className={css.itemDescription}>{item.description}</span>}
|
||||
</button>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Fragment>
|
||||
|
||||
@@ -101,6 +101,7 @@ export class InputTriggerController {
|
||||
const prev = this.menu.getSnapshot()
|
||||
const same = !launched && prev.open && prev.hit !== null
|
||||
&& prev.hit.trigger === hit.trigger && prev.hit.query === hit.query
|
||||
&& prev.hit.quoted === hit.quoted
|
||||
&& prev.hit.span.start === hit.span.start && prev.hit.span.end === hit.span.end
|
||||
this.hit = hit
|
||||
if (same) return
|
||||
@@ -324,7 +325,11 @@ export class InputTriggerController {
|
||||
return actx.bail(actx, 'slash/input-begin-command', { claim: outcome.claim, span }) === true
|
||||
}
|
||||
if ('text' in outcome) {
|
||||
return actx.bail(actx, 'slash/input-insert-text', { text: outcome.text, span }) === true
|
||||
return actx.bail(actx, 'slash/input-insert-text', {
|
||||
text: outcome.text,
|
||||
span,
|
||||
...outcome.continue === true ? { continue: true } : {},
|
||||
}) === true
|
||||
}
|
||||
return actx.bail(actx, 'slash/input-insert-reference', { reference: outcome.insert, span }) === true
|
||||
}
|
||||
@@ -367,7 +372,12 @@ export class InputTriggerController {
|
||||
const projection = this.project()
|
||||
for (const source of roster) {
|
||||
void source
|
||||
.candidates(projection, { query: hit.query, position: hit.position, signal: controller.signal })
|
||||
.candidates(projection, {
|
||||
query: hit.query,
|
||||
quoted: hit.quoted,
|
||||
position: hit.position,
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(
|
||||
(items) => {
|
||||
if (controller.signal.aborted) return
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface TriggerHit {
|
||||
readonly trigger: TriggerChar
|
||||
/** Text between the trigger char and the caret, live-filtered. */
|
||||
readonly query: string
|
||||
/** True only for an open quoted `@file` token. */
|
||||
readonly quoted: boolean
|
||||
/** leading = draft trimmed (whitespace incl. newlines) starts with the token. */
|
||||
readonly position: TriggerPosition
|
||||
/** Token span; draftRev injected by the caller. */
|
||||
@@ -19,8 +21,9 @@ export interface TriggerHit {
|
||||
|
||||
/**
|
||||
* Detect a trigger token at the caret under the given guard tier.
|
||||
* Word-boundary rule: the char before the trigger is start-of-line,
|
||||
* whitespace, or punctuation; `user@host` and URL '/' do not trigger.
|
||||
* `@` uses the shared file-reference start/whitespace grammar; `/` accepts
|
||||
* punctuation boundaries with URL carve-outs. `user@host` and URL `/` do not
|
||||
* trigger.
|
||||
* Returns null when no trigger is live at the caret.
|
||||
*/
|
||||
export type DetectTrigger = (draft: string, caret: number, guard: TriggerGuard) => TriggerHit | null
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* the caret for a live trigger char under the guard tier and applies the
|
||||
* word-boundary rules. Zero React / DOM / cordis.
|
||||
*/
|
||||
import { activeAtToken } from '@deepseek-ai/dsh-file-reference/grammar'
|
||||
import type { TriggerChar } from '../types.ts'
|
||||
import type { DetectTrigger } from './contract.ts'
|
||||
|
||||
@@ -29,10 +30,10 @@ function boundaryOk(draft: string, index: number, char: TriggerChar): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a trigger token at the caret. Scans left from the caret and stops
|
||||
* at the first whitespace (the token under edit never spans whitespace);
|
||||
* trigger chars failing the guard tier or the word boundary are treated as
|
||||
* ordinary token chars and the scan continues (`user@host`, URL slashes).
|
||||
* Detect a trigger token at the caret. `@` first uses the shared grammar,
|
||||
* including an open quoted token that may span whitespace. Slash detection
|
||||
* scans left to the first whitespace; slashes failing the word boundary are
|
||||
* treated as ordinary token chars and the scan continues (URL slashes).
|
||||
* Guard tiers: plain = both chars live; claimed = '/' fully suppressed,
|
||||
* '@' live; frozen = none.
|
||||
*
|
||||
@@ -46,15 +47,27 @@ function boundaryOk(draft: string, index: number, char: TriggerChar): boolean {
|
||||
*/
|
||||
export const detectTrigger: DetectTrigger = (draft, caret, guard) => {
|
||||
if (guard.tier === 'frozen') return null
|
||||
const at = activeAtToken(draft, caret)
|
||||
if (at !== undefined) {
|
||||
const start = caret - at.prefix.length
|
||||
return {
|
||||
trigger: '@',
|
||||
query: at.query,
|
||||
quoted: at.quoted,
|
||||
position: draft.search(/\S/) === start ? 'leading' : 'inline',
|
||||
span: { start, end: caret, draftRev: 0 },
|
||||
}
|
||||
}
|
||||
for (let i = caret - 1; i >= 0; i--) {
|
||||
const ch = draft.charAt(i)
|
||||
if (WHITESPACE.test(ch)) return null
|
||||
if (ch !== '/' && ch !== '@') continue
|
||||
if (guard.tier === 'claimed' && ch === '/') continue
|
||||
if (ch !== '/') continue
|
||||
if (guard.tier === 'claimed') continue
|
||||
if (!boundaryOk(draft, i, ch)) continue
|
||||
return {
|
||||
trigger: ch,
|
||||
query: draft.slice(i + 1, caret),
|
||||
quoted: false,
|
||||
position: draft.search(/\S/) === i ? 'leading' : 'inline',
|
||||
span: { start: i, end: caret, draftRev: 0 },
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Frozen cross-package contract for the input trigger pipeline. Types only —
|
||||
* no runtime code. Sources (ui-commands / ui-skill / ui-subagent) and the
|
||||
* no runtime code. Sources (ui-commands / ui-skill / ui-reference) and the
|
||||
* conversation input layer import from here; changes require main-thread
|
||||
* arbitration.
|
||||
*
|
||||
@@ -35,6 +35,10 @@ export interface InputTriggerCandidate {
|
||||
readonly description?: string
|
||||
readonly icon?: string
|
||||
readonly hint?: string
|
||||
/** Optional visual group heading shared by adjacent candidates. */
|
||||
readonly section?: string
|
||||
/** Opaque source-owned pick payload. */
|
||||
readonly value?: string
|
||||
}
|
||||
|
||||
/** Pick-moment snapshot of the trigger token span. CAS: stale draftRev ⇒ the whole action no-ops. */
|
||||
@@ -110,7 +114,7 @@ export interface SubmitOutcome {
|
||||
export type PickOutcome =
|
||||
| { readonly claim: CommandClaim }
|
||||
| { readonly insert: ReferenceInsert }
|
||||
| { readonly text: string }
|
||||
| { readonly text: string; readonly continue?: boolean }
|
||||
| 'handled'
|
||||
| undefined
|
||||
|
||||
@@ -127,6 +131,8 @@ export interface SubmitEnvelope {
|
||||
/** Candidate request passed to a source. The signal is superseded on query change / menu close. */
|
||||
export interface CandidateRequest {
|
||||
readonly query: string
|
||||
/** Whether the active @file token is an open quoted path. */
|
||||
readonly quoted?: boolean
|
||||
readonly position: TriggerPosition
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
@@ -256,6 +262,8 @@ export interface InsertTextRequest {
|
||||
/** Literal replacement for the trigger token span (e.g. `/name `). */
|
||||
readonly text: string
|
||||
readonly span: TokenSpan
|
||||
/** Keep completion open after the splice (directory descent): the input re-tracks at the caret. */
|
||||
readonly continue?: boolean
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
|
||||
@@ -91,6 +91,17 @@ describe('detectTrigger guard tiers', () => {
|
||||
})
|
||||
|
||||
describe('detectTrigger span and query', () => {
|
||||
it('keeps an open quoted @file token active across spaces', () => {
|
||||
const draft = 'read @"docs/design notes'
|
||||
expect(atEnd(draft)).toMatchObject({
|
||||
trigger: '@',
|
||||
query: 'docs/design notes',
|
||||
quoted: true,
|
||||
position: 'inline',
|
||||
span: { start: 5, end: draft.length },
|
||||
})
|
||||
})
|
||||
|
||||
it('spans trigger char to caret with a placeholder draftRev', () => {
|
||||
const hit = detectTrigger('say /goal', 9, plain)
|
||||
expect(hit?.span).toEqual({ start: 4, end: 9, draftRev: 0 })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { exactMatch, MENU_CLOSED, menuReduce, seedGroups } from '../src/core/men
|
||||
const hit = (query = ''): TriggerHit => ({
|
||||
trigger: '/',
|
||||
query,
|
||||
quoted: false,
|
||||
position: 'leading',
|
||||
span: { start: 0, end: 1 + query.length, draftRev: 1 },
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MenuView } from '../src/client/MenuView.tsx'
|
||||
const hit: TriggerHit = {
|
||||
trigger: '/',
|
||||
query: 'g',
|
||||
quoted: false,
|
||||
position: 'leading',
|
||||
span: { start: 0, end: 2, draftRev: 1 },
|
||||
}
|
||||
@@ -99,6 +100,31 @@ describe('MenuView', () => {
|
||||
expect(titles(view.container)).toEqual(['命令', 'mystery', '技能'])
|
||||
})
|
||||
|
||||
it('renders contiguous candidate sections once without changing option indexes', () => {
|
||||
const { onPick } = mount(openState({
|
||||
groups: [{
|
||||
source: 'reference',
|
||||
status: 'ready',
|
||||
items: [
|
||||
{ name: 'Folder · src/', section: '文件与文件夹' },
|
||||
{ name: 'File · README.md', section: '文件与文件夹' },
|
||||
{ name: 'Session · Research', section: 'Session 对话' },
|
||||
],
|
||||
}],
|
||||
highlight: { source: 'reference', index: 0 },
|
||||
}))
|
||||
expect(screen.getAllByText('文件与文件夹')).toHaveLength(1)
|
||||
expect(screen.getAllByText('Session 对话')).toHaveLength(1)
|
||||
const options = screen.getAllByRole('option')
|
||||
expect(options.map(option => option.textContent)).toEqual([
|
||||
'Folder · src/',
|
||||
'File · README.md',
|
||||
'Session · Research',
|
||||
])
|
||||
fireEvent.mouseDown(options[2]!)
|
||||
expect(onPick).toHaveBeenCalledWith('reference', 2)
|
||||
})
|
||||
|
||||
it('exposes the highlight via aria-activedescendant and aria-selected', () => {
|
||||
mount(openState({ highlight: { source: 'command', index: 1 } }))
|
||||
const listbox = screen.getByRole('listbox')
|
||||
|
||||
@@ -359,6 +359,7 @@ describe('programmatic source launcher', () => {
|
||||
const hit = {
|
||||
trigger: '/' as const,
|
||||
query: '',
|
||||
quoted: false,
|
||||
position: 'leading' as const,
|
||||
span: { start: 2, end: 5, draftRev: 7 },
|
||||
}
|
||||
@@ -385,6 +386,7 @@ describe('programmatic source launcher', () => {
|
||||
const hit = {
|
||||
trigger: '/' as const,
|
||||
query: '',
|
||||
quoted: false,
|
||||
position: 'leading' as const,
|
||||
span: { start: 0, end: 0, draftRev: 1 },
|
||||
}
|
||||
@@ -496,6 +498,18 @@ describe('pick / scoped input events', () => {
|
||||
expect(controller.menu.getSnapshot().open).toBe(false)
|
||||
})
|
||||
|
||||
it('forwards a continuing text outcome so a directory pick keeps completion open', async () => {
|
||||
const { controller, actx } = pickBench(() => ({ text: '@src/', continue: true }))
|
||||
const texts: Array<{ text: string; continue?: boolean }> = []
|
||||
actx.on('slash/input-insert-text', (req) => {
|
||||
texts.push(req)
|
||||
return true
|
||||
})
|
||||
await tick()
|
||||
controller.pick('command', 0)
|
||||
expect(texts).toEqual([{ text: '@src/', continue: true, span: { start: 0, end: 2, draftRev: 3 } }])
|
||||
})
|
||||
|
||||
it('a text outcome the input declines answers false on the space path', async () => {
|
||||
const src: InputTriggerSource = {
|
||||
trigger: '/',
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
{
|
||||
"path": "../runtime"
|
||||
},
|
||||
{
|
||||
"path": "../../context/file-reference"
|
||||
},
|
||||
{
|
||||
"path": "../ui-primitives"
|
||||
},
|
||||
|
||||
@@ -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 packages/client/ui-reference/README.md
|
||||
README.md: 12a8e69624c0d7fe28c10ae708466fdda4a8480d
|
||||
README.zh.md: bfaee51af3947fe794afab3a99df194c543b0082
|
||||
@@ -0,0 +1,25 @@
|
||||
# `@deepseek-ai/dsh-client-ui-reference`
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Unified Web `@file` and `@session` source. The browser starts the `fileReferences/list` and `sessionReferenceResolver/candidates` Remote calls together for an unquoted token, deterministically orders files before sessions with locale-registered folder/file/session labels, renders the rows under non-selectable file and session section headings, and degrades either failed candidate domain independently. An open `@"…` token searches files only.
|
||||
|
||||
File picks insert the natural text defined by the shared `@path` grammar. A file closes completion and adds a trailing space; a directory keeps the menu active at its trailing slash so the user can descend another level. Paths containing whitespace use `@"path with spaces"`, and a quote the user opened explicitly remains quoted.
|
||||
|
||||
Session picks insert an atomic composer chip whose hidden `ref` and clipboard representation are the canonical `@[label](dsh-session:…)` mention returned by the Host. The visible chip uses `@label`; serialization never reconstructs identity from that label. Ordinary send carries the canonical mention through `session.prompt`; the session-reference service validates it and captures model context at `agent/pre-step`.
|
||||
|
||||
The `/client` export is the plugin body (`apply`/`inject`) only; candidate encoding stays internal to the registration effect.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `@deepseek-ai/dsh-file-reference-local` for path guidance and `@deepseek-ai/dsh-session-reference` for prepared session snapshots.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Candidate browsing has no model effect. A selected file or session changes only the new user-message suffix and any Host-prepared session-reference prefix attached to that message; earlier target history remains unchanged.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Candidate failure is intentionally quiet** — one unavailable or failed Remote discovery call yields no rows for that domain. A session-reference preparation failure occurs after prompt acceptance and terminates that agent turn.
|
||||
- **No browser-side file scan** — Web completion requires a mounted Host `ctx.fileReferences` provider; the browser cannot fall back to its own filesystem.
|
||||
- **Session search remains metadata-only** — discovery filters session id, cwd, and the latest log-backed title through `ctx.sessionReferenceResolver`; message bodies and full transcripts are not searched.
|
||||
@@ -0,0 +1,25 @@
|
||||
# `@deepseek-ai/dsh-client-ui-reference`
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
统一的 Web `@file` 与 `@session` source。对于未加引号的 token,浏览器会同时启动 `fileReferences/list` 和 `sessionReferenceResolver/candidates` Remote 调用,以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的文件夹、文件与会话标签;各行分别渲染在不可选择的文件与会话分组标题下,任一候选领域的失败都会独立降级。尚未闭合的 `@"…` token 只搜索文件。
|
||||
|
||||
选择文件会插入共享 `@path` 语法所定义的自然文本。文件会关闭补全并追加一个尾随空格;目录则让菜单在尾部斜杠处保持活跃,用户可以继续进入下一层。包含空白的路径使用 `@"path with spaces"`,用户显式打开的引号会继续保留。
|
||||
|
||||
选择会话会插入一个原子的输入框 chip,其隐藏 `ref` 与剪贴板表示均为宿主返回的规范 `@[label](dsh-session:…)` mention。可见 chip 使用 `@label`;序列化永远不会根据该标签重建身份。普通发送会通过 `session.prompt` 携带规范 mention,session-reference 服务会在 `agent/pre-step` 校验它并捕获模型上下文。
|
||||
|
||||
`/client` 只导出插件主体(`apply`/`inject`);候选编码保留在注册 effect 内部。
|
||||
|
||||
## 模型体验
|
||||
|
||||
间接影响模型体验:路径指引由 `@deepseek-ai/dsh-file-reference-local` 提供,准备后的会话快照由 `@deepseek-ai/dsh-session-reference` 提供。
|
||||
|
||||
#### KV 缓存影响
|
||||
|
||||
浏览候选项不会影响模型。选择文件或会话只会改变新用户消息的后缀,以及附加到该消息、由宿主准备的会话引用前缀;目标会话更早的历史保持不变。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **候选失败有意保持静默**:Remote 发现调用不可用或失败时,该领域不产生候选行。会话引用准备失败发生在提示词接受后,并会终止该 agent 轮次。
|
||||
- **浏览器侧不扫描文件**:Web 补全需要挂载宿主 `ctx.fileReferences` 提供方;浏览器无法回退到自身文件系统。
|
||||
- **会话搜索仍仅使用元数据**:发现流程通过 `ctx.sessionReferenceResolver` 筛选 session id、cwd 和以日志为依据的最新标题;不搜索消息主体或完整 transcript(文本记录)。
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-reference",
|
||||
"description": "Unified Web @file and @session reference source",
|
||||
"version": "0.1.0-rc.7",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-reference"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./lib/types/client/index.d.ts",
|
||||
"default": "./lib/client.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dsh": {
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-api-remotes",
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger"
|
||||
],
|
||||
"platform": "web"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "tsdown",
|
||||
"watch": "tsdown --watch"
|
||||
},
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/client.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Unified Web `@` reference source. File and session discovery run through
|
||||
* the cancellable generated Remote namespaces in parallel with deterministic
|
||||
* ordering and labels.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-client-ui-reference/client
|
||||
*/
|
||||
// Type-only: pulls the generated Remote API and ctx.remote merge through the Client assembly boundary.
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ClientSessionContext, InputTriggerServiceContract, InputTriggerSource,
|
||||
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar'
|
||||
import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
|
||||
import { en, NS, zh, type ReferenceKey } from './locales.ts'
|
||||
|
||||
/** Required services: the trigger registry, the Remote namespaces, and the copy. */
|
||||
export const inject = [
|
||||
'inputTriggers', 'locale', 'remote', 'remote.fileReferences', 'remote.sessionReferenceResolver',
|
||||
]
|
||||
|
||||
/**
|
||||
* Register the combined `@file` / `@session` source.
|
||||
* @param ctx - client root context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-reference: dictionaries')
|
||||
const t = ctx.locale.bind(NS)
|
||||
const source: InputTriggerSource = {
|
||||
trigger: '@',
|
||||
name: 'reference',
|
||||
async candidates(session: ClientSessionContext, { query, quoted, signal }) {
|
||||
const files = ctx.remote.fileReferences.list(session.sessionId, query, signal).then(
|
||||
result => result.ok ? result.value : [],
|
||||
() => [],
|
||||
)
|
||||
const sessions = quoted === true
|
||||
? Promise.resolve([] as SessionReferenceMentionCandidate[])
|
||||
: ctx.remote.sessionReferenceResolver.candidates(session.sessionId, query, signal).then(
|
||||
result => result.ok ? result.value : [],
|
||||
() => [],
|
||||
)
|
||||
const [fileItems, sessionItems] = await Promise.all([files, sessions])
|
||||
if (signal.aborted) return []
|
||||
return [
|
||||
...fileItems.flatMap(candidate => fileCandidate(candidate, quoted === true, t)),
|
||||
...sessionItems.map(candidate => sessionCandidate(candidate, t)),
|
||||
]
|
||||
},
|
||||
onPick({ candidate }) {
|
||||
const value = parseCandidate(candidate.value)
|
||||
if (value?.kind === 'file') {
|
||||
return {
|
||||
text: value.mention + (value.fileKind === 'file' ? ' ' : ''),
|
||||
...value.fileKind === 'directory' ? { continue: true } : {},
|
||||
}
|
||||
}
|
||||
if (value?.kind === 'session') {
|
||||
return {
|
||||
insert: {
|
||||
source: 'reference',
|
||||
ref: value.mention,
|
||||
label: `@${value.label}`,
|
||||
clipboardText: value.mention,
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
codec: {
|
||||
clipboardText: ref => ref,
|
||||
serialize: ref => Promise.resolve(ref),
|
||||
},
|
||||
}
|
||||
const inputTriggers = ctx.get('inputTriggers') as InputTriggerServiceContract
|
||||
ctx.effect(() => inputTriggers.registerSource(source), 'ui-reference: @ source')
|
||||
}
|
||||
|
||||
type Translate = (key: ReferenceKey) => string
|
||||
|
||||
type ReferenceCandidateValue =
|
||||
| { kind: 'file'; fileKind: FileReferenceCandidate['kind']; mention: string }
|
||||
| { kind: 'session'; label: string; mention: string }
|
||||
|
||||
function fileCandidate(candidate: FileReferenceCandidate, preserveQuote: boolean, t: Translate) {
|
||||
const mention = formatFileMention(candidate, preserveQuote)
|
||||
if (mention === undefined) return []
|
||||
const name = candidate.path.slice(candidate.path.lastIndexOf('/') + 1)
|
||||
const directory = candidate.kind === 'directory'
|
||||
const value: ReferenceCandidateValue = {
|
||||
kind: 'file',
|
||||
fileKind: candidate.kind,
|
||||
mention,
|
||||
}
|
||||
return [{
|
||||
name: `${t(directory ? 'candidate.folder' : 'candidate.file')} · ${name}${directory ? '/' : ''}`,
|
||||
description: candidate.path,
|
||||
section: t('section.files'),
|
||||
value: JSON.stringify(value),
|
||||
}]
|
||||
}
|
||||
|
||||
function sessionCandidate(candidate: SessionReferenceMentionCandidate, t: Translate) {
|
||||
const location = candidate.cwd ?? t('candidate.noCwd')
|
||||
const description = `${candidate.label === candidate.sessionId ? '' : `${candidate.sessionId} · `}${location} · ${new Date(candidate.createdAt).toISOString()}`
|
||||
const value: ReferenceCandidateValue = {
|
||||
kind: 'session',
|
||||
label: candidate.label,
|
||||
mention: candidate.mention,
|
||||
}
|
||||
return {
|
||||
name: `${t('candidate.session')} · ${candidate.label}`,
|
||||
description,
|
||||
section: t('section.sessions'),
|
||||
value: JSON.stringify(value),
|
||||
}
|
||||
}
|
||||
|
||||
function parseCandidate(value: string | undefined): ReferenceCandidateValue | undefined {
|
||||
if (value === undefined) return undefined
|
||||
return JSON.parse(value) as ReferenceCandidateValue
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/** `reference` namespace dictionaries for the unified `@` source. */
|
||||
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
/** Dictionary namespace owned by this plugin. */
|
||||
export const NS = 'reference'
|
||||
|
||||
/** Simplified Chinese dictionary (the key-set source of truth). */
|
||||
export const zh = {
|
||||
'section.files': '文件与文件夹',
|
||||
'section.sessions': 'Session 对话',
|
||||
'candidate.file': '文件',
|
||||
'candidate.folder': '文件夹',
|
||||
'candidate.session': 'Session',
|
||||
'candidate.noCwd': '(无工作目录)',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
/** The reference namespace key union. */
|
||||
export type ReferenceKey = keyof typeof zh
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface LocaleNamespaceMap {
|
||||
/** The unified `@` reference menu's copy. */
|
||||
reference: ReferenceKey
|
||||
}
|
||||
}
|
||||
|
||||
/** English dictionary, checked complete against the zh key set. */
|
||||
export const en = {
|
||||
'section.files': 'Files & folders',
|
||||
'section.sessions': 'Session conversations',
|
||||
'candidate.file': 'File',
|
||||
'candidate.folder': 'Folder',
|
||||
'candidate.session': 'Session',
|
||||
'candidate.noCwd': '(no cwd)',
|
||||
} satisfies Record<ReferenceKey, string>
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* File/session reference plugin, node half. Pure UI plugin: the empty apply
|
||||
* exists so the plugin appears in the host cordis.yml / Loader; the browser
|
||||
* half ships via exports["./client"], discovered through the package.json
|
||||
* `dsh.client` declaration.
|
||||
*/
|
||||
|
||||
/** Host plugin body — no host-side behavior for this source plugin. */
|
||||
export function apply(): void {}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-reference`.
|
||||
* @module @deepseek-ai/dsh-client-ui-reference/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-reference'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'client-ui-reference-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: a single slash-source registration whose disposal is
|
||||
* proven by the HMR-safety spec — it emits no cordis events and owns no
|
||||
* cross-plugin mutable state.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Web reference source coverage: Remote-backed file/session discovery,
|
||||
* deterministic ordering and labels, quoted-path suppression, pick projections, codec
|
||||
* round-trip, and registration lifecycle.
|
||||
*/
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
CandidateRequest, ClientSessionContext, InputTriggerCandidate, InputTriggerSource,
|
||||
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
|
||||
import { apply, inject } from '../src/client/index.ts'
|
||||
import { apply as nodeApply } from '../src/index.ts'
|
||||
|
||||
const sid = (value: string): SessionId => value as SessionId
|
||||
const session: ClientSessionContext = { sessionId: sid('target') }
|
||||
|
||||
type RemoteEnvelope<T> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: { code: string; message: string; details: object } }
|
||||
|
||||
type RemoteLookup<T> = (
|
||||
agentId: SessionId,
|
||||
query: string,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<RemoteEnvelope<T[]>>
|
||||
|
||||
function request(
|
||||
query: string,
|
||||
options: { quoted?: boolean; signal?: AbortSignal } = {},
|
||||
): CandidateRequest {
|
||||
return {
|
||||
query,
|
||||
quoted: options.quoted ?? false,
|
||||
position: 'inline',
|
||||
signal: options.signal ?? new AbortController().signal,
|
||||
}
|
||||
}
|
||||
|
||||
async function bench(
|
||||
files: RemoteLookup<FileReferenceCandidate> = vi.fn(() => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: [
|
||||
{ path: 'src', kind: 'directory' as const },
|
||||
{ path: 'docs/a b.md', kind: 'file' as const },
|
||||
],
|
||||
})),
|
||||
sessions: RemoteLookup<SessionReferenceMentionCandidate> = vi.fn(() => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: [{
|
||||
sessionId: sid('source'),
|
||||
label: 'Research',
|
||||
cwd: '/project',
|
||||
createdAt: 1_700_000_000_000,
|
||||
mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
||||
}],
|
||||
})),
|
||||
): Promise<{ ctx: Context; fiber: ReturnType<Context['plugin']>; source: InputTriggerSource }> {
|
||||
const ctx = new Context()
|
||||
let source: InputTriggerSource | undefined
|
||||
ctx.provide('inputTriggers', {
|
||||
registerSource(candidate: InputTriggerSource) {
|
||||
source = candidate
|
||||
return () => { source = undefined }
|
||||
},
|
||||
})
|
||||
class RemoteService extends Service {
|
||||
constructor(serviceCtx: Context) {
|
||||
super(serviceCtx, 'remote')
|
||||
}
|
||||
}
|
||||
new RemoteService(ctx)
|
||||
ctx.provide('remote.fileReferences', { list: files })
|
||||
ctx.provide('remote.sessionReferenceResolver', { candidates: sessions })
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
if (source === undefined) throw new Error('reference source was not registered')
|
||||
return { ctx, fiber, source }
|
||||
}
|
||||
|
||||
describe('apply', () => {
|
||||
it('declares its services and releases the @ reference registration on disposal', async () => {
|
||||
expect(inject).toEqual([
|
||||
'inputTriggers', 'locale', 'remote', 'remote.fileReferences', 'remote.sessionReferenceResolver',
|
||||
])
|
||||
const { fiber } = await bench()
|
||||
let registered: InputTriggerSource | undefined
|
||||
const ctx = new Context()
|
||||
ctx.provide('inputTriggers', {
|
||||
registerSource(source: InputTriggerSource) {
|
||||
registered = source
|
||||
return () => { registered = undefined }
|
||||
},
|
||||
})
|
||||
class RemoteService extends Service {
|
||||
constructor(serviceCtx: Context) {
|
||||
super(serviceCtx, 'remote')
|
||||
}
|
||||
}
|
||||
new RemoteService(ctx)
|
||||
ctx.provide('remote.fileReferences', { list: () => Promise.resolve({ ok: true, value: [] }) })
|
||||
ctx.provide('remote.sessionReferenceResolver', { candidates: () => Promise.resolve({ ok: true, value: [] }) })
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
const ownFiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await ownFiber.await()
|
||||
expect(registered).toMatchObject({ trigger: '@', name: 'reference' })
|
||||
await ownFiber.dispose()
|
||||
expect(registered).toBeUndefined()
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('the node half applies without host-side behavior', () => {
|
||||
expect(() => { nodeApply() }).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('candidates', () => {
|
||||
it('starts both Remote lookups together and renders files before sessions with stable labels', async () => {
|
||||
let releaseFiles!: () => void
|
||||
let releaseSessions!: () => void
|
||||
const files = vi.fn(() => new Promise<{
|
||||
ok: true
|
||||
value: { path: string; kind: 'file' | 'directory' }[]
|
||||
}>((resolve) => {
|
||||
releaseFiles = () => {
|
||||
resolve({
|
||||
ok: true,
|
||||
value: [
|
||||
{ path: 'src', kind: 'directory' },
|
||||
{ path: 'docs/a b.md', kind: 'file' },
|
||||
],
|
||||
})
|
||||
}
|
||||
}))
|
||||
const sessions = vi.fn(() => new Promise<{
|
||||
ok: true
|
||||
value: {
|
||||
sessionId: SessionId
|
||||
label: string
|
||||
cwd: string
|
||||
createdAt: number
|
||||
mention: string
|
||||
}[]
|
||||
}>((resolve) => {
|
||||
releaseSessions = () => {
|
||||
resolve({
|
||||
ok: true,
|
||||
value: [{
|
||||
sessionId: sid('source'),
|
||||
label: 'Research',
|
||||
cwd: '/project',
|
||||
createdAt: 1_700_000_000_000,
|
||||
mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
||||
}],
|
||||
})
|
||||
}
|
||||
}))
|
||||
const { source } = await bench(files, sessions)
|
||||
const pending = source.candidates(session, request('re'))
|
||||
expect(files).toHaveBeenCalledTimes(1)
|
||||
expect(sessions).toHaveBeenCalledTimes(1)
|
||||
releaseSessions()
|
||||
releaseFiles()
|
||||
await expect(pending).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Folder · src/',
|
||||
description: 'src',
|
||||
section: 'Files & folders',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'File · a b.md',
|
||||
description: 'docs/a b.md',
|
||||
section: 'Files & folders',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'Session · Research',
|
||||
description: 'source · /project · 2023-11-14T22:13:20.000Z',
|
||||
section: 'Session conversations',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('suppresses sessions for an open quoted path and degrades each failed domain independently', async () => {
|
||||
const files = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true as const,
|
||||
value: [{ path: 'README.md', kind: 'file' as const }],
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('file scan failed'))
|
||||
const sessions = vi.fn(() => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: [{
|
||||
sessionId: sid('source'),
|
||||
label: 'Research',
|
||||
cwd: '/project',
|
||||
createdAt: 0,
|
||||
mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
||||
}],
|
||||
}))
|
||||
const { source } = await bench(files, sessions)
|
||||
const quoted = await source.candidates(session, request('READ', { quoted: true }))
|
||||
expect(quoted).toEqual([expect.objectContaining({ name: 'File · README.md' })])
|
||||
expect(source.onPick({
|
||||
candidate: quoted[0]!,
|
||||
session,
|
||||
position: 'inline',
|
||||
via: 'menu',
|
||||
span: { start: 0, end: 6, draftRev: 1 },
|
||||
})).toEqual({ text: '@"README.md" ' })
|
||||
expect(sessions).not.toHaveBeenCalled()
|
||||
await expect(source.candidates(session, request('research'))).resolves.toEqual([
|
||||
expect.objectContaining({ name: 'Session · Research' }),
|
||||
])
|
||||
})
|
||||
|
||||
it('drops a completed result when the query signal was superseded', async () => {
|
||||
const controller = new AbortController()
|
||||
const { source } = await bench()
|
||||
const pending = source.candidates(session, request('', { signal: controller.signal }))
|
||||
controller.abort()
|
||||
await expect(pending).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('treats Remote failures as empty domains and filters paths that cannot be mentioned', async () => {
|
||||
const files = vi.fn(() => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: [{ path: 'bad\nname', kind: 'file' as const }],
|
||||
}))
|
||||
const sessions = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('session lookup failed'))
|
||||
.mockResolvedValueOnce({
|
||||
ok: false as const,
|
||||
error: { code: 'internal', message: 'session lookup failed', details: {} },
|
||||
})
|
||||
const { source } = await bench(files, sessions)
|
||||
await expect(source.candidates(session, request('bad'))).resolves.toEqual([])
|
||||
|
||||
files.mockResolvedValueOnce({
|
||||
ok: false as const,
|
||||
error: { code: 'internal', message: 'file lookup failed', details: {} },
|
||||
} as never)
|
||||
await expect(source.candidates(session, request('bad'))).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('omits redundant session ids and labels sessions without a cwd', async () => {
|
||||
const files = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))
|
||||
const sessions = vi.fn(() => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: [{
|
||||
sessionId: sid('same'),
|
||||
label: 'same',
|
||||
createdAt: 0,
|
||||
mention: '@[same](dsh-session:InNhbWUi)',
|
||||
}],
|
||||
}))
|
||||
const { source } = await bench(files, sessions)
|
||||
await expect(source.candidates(session, request('same'))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Session · same',
|
||||
description: '(no cwd) · 1970-01-01T00:00:00.000Z',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('pick and codec', () => {
|
||||
const pick = (source: InputTriggerSource, candidate: InputTriggerCandidate) => source.onPick({
|
||||
candidate,
|
||||
session,
|
||||
position: 'inline',
|
||||
via: 'menu',
|
||||
span: { start: 0, end: 1, draftRev: 1 },
|
||||
})
|
||||
|
||||
it('inserts files as path text, keeping directory completion open', async () => {
|
||||
const { source } = await bench()
|
||||
const [directory, file] = await source.candidates(session, request(''))
|
||||
expect(pick(source, directory!)).toEqual({ text: '@src/', continue: true })
|
||||
expect(pick(source, file!)).toEqual({ text: '@"docs/a b.md" ' })
|
||||
const [quotedDirectory] = await source.candidates(session, request('', { quoted: true }))
|
||||
expect(pick(source, quotedDirectory!)).toEqual({ text: '@"src/', continue: true })
|
||||
})
|
||||
|
||||
it('inserts sessions as atomic chips whose clipboard and model forms are canonical mentions', async () => {
|
||||
const { source } = await bench()
|
||||
const candidates = await source.candidates(session, request(''))
|
||||
const candidate = candidates.find(item => item.name === 'Session · Research')!
|
||||
const mention = '@[Research](dsh-session:InNvdXJjZSI)'
|
||||
expect(pick(source, candidate)).toEqual({
|
||||
insert: {
|
||||
source: 'reference',
|
||||
ref: mention,
|
||||
label: '@Research',
|
||||
clipboardText: mention,
|
||||
},
|
||||
})
|
||||
expect(source.codec?.clipboardText(mention)).toBe(mention)
|
||||
await expect(source.codec?.serialize(mention, new AbortController().signal)).resolves.toBe(mention)
|
||||
})
|
||||
|
||||
it('ignores candidates that do not carry a source-owned value', async () => {
|
||||
const { source } = await bench()
|
||||
expect(pick(source, { name: 'foreign candidate' })).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.client.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../api/remotes/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../../context/file-reference"
|
||||
},
|
||||
{
|
||||
"path": "../../context/session-reference"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../typert/protocol"
|
||||
},
|
||||
{
|
||||
"path": "../locale"
|
||||
},
|
||||
{
|
||||
"path": "../runtime"
|
||||
},
|
||||
{
|
||||
"path": "../ui-input-trigger"
|
||||
},
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { clientBundle } from '../tsdown.client.ts'
|
||||
|
||||
export default clientBundle('@deepseek-ai/dsh-client-ui-reference', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
@@ -61,8 +61,8 @@
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
/**
|
||||
* Subagent reference plugin, browser half: registers the '@' source —
|
||||
* candidates filtered from the session list snapshot's running children
|
||||
* (zero RPC; the list rides the plugin's root-context sessions service, the
|
||||
* scoped session comes from the per-call projection), pick inserts the
|
||||
* literal `@label ` text (plain-text-reference decision, see
|
||||
* .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md:
|
||||
* the draft carries plain text, chip
|
||||
* visuals are derived by scanning against the source lexicon, and the
|
||||
* prompt ships the same literal). Consumption semantics stay with future
|
||||
* business work. No adjudication hooks: subagent
|
||||
* references never enter command adjudication.
|
||||
*/
|
||||
/** Web subagent catalog, navigation, and addressed-session composer owner. */
|
||||
import type {
|
||||
ClientContext, SessionId, SubagentAddress,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ComposerChainProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ClientSessionContext, InputTriggerServiceContract, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import { SubagentCatalogAction, type SubagentCatalogInjected } from './SubagentCatalogAction.tsx'
|
||||
import {
|
||||
SubagentReadOnlyComposer, type SubagentReadOnlyMatch,
|
||||
@@ -37,8 +24,8 @@ export type {
|
||||
SubagentReadOnlyComposerProps, SubagentReadOnlyMatch,
|
||||
} from './SubagentReadOnlyComposer.tsx'
|
||||
|
||||
/** Required services for references, conversation slots, and session navigation. */
|
||||
export const inject = ['inputTriggers', 'sessions', 'slots', 'locale']
|
||||
/** Required services for conversation slots and session navigation. */
|
||||
export const inject = ['sessions', 'slots', 'locale']
|
||||
|
||||
/** Claim the composer for one-shot history or an unavailable continuation owner. */
|
||||
function selectReadOnlySubagent(owner: ComposerChainProps): SubagentReadOnlyMatch | null {
|
||||
@@ -53,49 +40,12 @@ function selectReadOnlySubagent(owner: ComposerChainProps): SubagentReadOnlyMatc
|
||||
}
|
||||
|
||||
/**
|
||||
* Client plugin body: register the '@' subagent source over the root session list.
|
||||
* Client plugin body: register the subagent catalog and read-only composer seats.
|
||||
* @param ctx - client root context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-subagent: dictionaries')
|
||||
const sessions = ctx.sessions
|
||||
// Child labels live on the session list (parentId lineage + displayTitle),
|
||||
// not the conversation snapshot — the list store is the zero-RPC candidate feed.
|
||||
const childLabels = (session: ClientSessionContext, query: string): string[] => {
|
||||
const { byId } = sessions.list.getSnapshot()
|
||||
return Object.values(byId)
|
||||
.filter(child => child.parentId === session.sessionId && child.running && child.displayTitle.includes(query))
|
||||
.map(child => child.displayTitle)
|
||||
}
|
||||
const source: InputTriggerSource = {
|
||||
trigger: '@',
|
||||
name: 'subagent',
|
||||
candidates(session, { query }) {
|
||||
return Promise.resolve(childLabels(session, query).map(name => ({ name })))
|
||||
},
|
||||
lexicon(session) {
|
||||
// The list snapshot is always warm — the full running-children roster.
|
||||
return childLabels(session, '')
|
||||
},
|
||||
subscribeLexicon(_session, listener) {
|
||||
// The roll derives from the list snapshot, so its change feed IS the list's.
|
||||
return sessions.list.subscribe(listener)
|
||||
},
|
||||
onPick({ candidate }) {
|
||||
// Plain-text reference: the literal lands in the draft
|
||||
// and ships to the model verbatim (trailing space closes the token).
|
||||
return { text: `@${candidate.name} ` }
|
||||
},
|
||||
codec: {
|
||||
clipboardText: ref => `@${ref}`,
|
||||
// TODO: serialize returns the raw label until the '@' consumption
|
||||
// feature defines a model representation.
|
||||
serialize: ref => Promise.resolve(`@${ref}`),
|
||||
},
|
||||
}
|
||||
const inputTriggers = ctx.get('inputTriggers') as InputTriggerServiceContract
|
||||
ctx.effect(() => inputTriggers.registerSource(source), 'ui-subagent: @ source')
|
||||
|
||||
const catalogActions = (_parentSessionId: SessionId): SubagentCatalogInjected => ({
|
||||
openChild(address: SubagentAddress) {
|
||||
sessions.openSubagent(address)
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
/**
|
||||
* ui-subagent browser half: source registration (duplicate-name proof) +
|
||||
* fiber-teardown removal (HMR safety) against the real InputTriggerService, then
|
||||
* the source behavior contract driven directly on the captured source with
|
||||
* real ClientSessionContext projections — zero-RPC candidates from the root
|
||||
* session list (running children of the projected session, label-contains
|
||||
* filtering, childless session → empty), the synchronous lexicon roster,
|
||||
* pick → plain-text outcome (the plain-text-reference decision:
|
||||
* .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md),
|
||||
* and the reference codec's two
|
||||
* projections. Direct driving is deliberate: this spec owns only the
|
||||
* source's own contract.
|
||||
*/
|
||||
/** ui-subagent browser half: catalog actions and read-only composer routing. */
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -19,8 +7,6 @@ import {
|
||||
type SessionSummary, type SubagentAddress,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ComposerChainProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { InputTriggerService } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import type { ClientSessionContext, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import {
|
||||
SubagentCatalogAction, type SubagentCatalogInjected,
|
||||
@@ -41,20 +27,17 @@ function summary(partial: Partial<SessionSummary> & { id: SessionId }): SessionS
|
||||
|
||||
const sid = (id: string) => id as SessionId
|
||||
|
||||
/** Fake root sessions face: the list snapshot the source closes over. */
|
||||
/** Fake root sessions face for catalog actions. */
|
||||
function sessionsWith(sessions: SessionSummary[]) {
|
||||
const byId: Record<string, SessionSummary> = {}
|
||||
for (const s of sessions) byId[s.id] = s
|
||||
const snapshot = { ids: sessions.map(s => s.id), byId, current: undefined } as unknown as SessionListState
|
||||
const subs = new Set<() => void>()
|
||||
const actionCalls: { method: string; args: unknown[] }[] = []
|
||||
return {
|
||||
list: {
|
||||
getSnapshot: () => snapshot,
|
||||
subscribe: (fn: () => void) => { subs.add(fn); return () => { subs.delete(fn) } },
|
||||
subscribe: () => () => {},
|
||||
},
|
||||
notify: () => { for (const fn of [...subs]) fn() },
|
||||
listenerCount: () => subs.size,
|
||||
actionCalls,
|
||||
openSubagent: (address: SubagentAddress) => {
|
||||
actionCalls.push({ method: 'openSubagent', args: [address] })
|
||||
@@ -80,26 +63,18 @@ async function provideSlotFaces(ctx: Context): Promise<void> {
|
||||
} as never, () => null)
|
||||
}
|
||||
|
||||
/** Boot the plugin over fake slash/sessions faces; returns the captured source and the list face. */
|
||||
/** Boot the plugin over fake sessions and slot faces. */
|
||||
async function fullBench(sessions: SessionSummary[]) {
|
||||
const ctx = new Context()
|
||||
let captured: InputTriggerSource | undefined
|
||||
const face = sessionsWith(sessions)
|
||||
ctx.provide('inputTriggers', { registerSource: (src: InputTriggerSource) => { captured = src; return () => {} } })
|
||||
ctx.provide('sessions', face)
|
||||
ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
|
||||
// ui-theme's Appearance row binds a durable scope through these two.
|
||||
ctx.provide('remote', { $on: () => () => {} } as never)
|
||||
ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
|
||||
await provideSlotFaces(ctx)
|
||||
await ctx.plugin({ inject: localeInject, apply: applyLocale }).await()
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
return { source: captured!, face, ctx }
|
||||
}
|
||||
|
||||
/** Source-only bench for the behavior-contract suites. */
|
||||
async function bench(sessions: SessionSummary[]): Promise<InputTriggerSource> {
|
||||
return (await fullBench(sessions)).source
|
||||
return { face, ctx }
|
||||
}
|
||||
|
||||
const FAMILY: SessionSummary[] = [
|
||||
@@ -112,40 +87,9 @@ const FAMILY: SessionSummary[] = [
|
||||
summary({ id: sid('c5'), parentId: sid('parent'), displayTitle: 'scout', running: true }),
|
||||
]
|
||||
|
||||
const proj = (id: string): ClientSessionContext => ({ sessionId: sid(id) })
|
||||
|
||||
const req = (query: string) =>
|
||||
({ query, position: 'inline' as const, signal: new AbortController().signal })
|
||||
|
||||
describe('apply', () => {
|
||||
it('declares the services it binds', () => {
|
||||
expect(inject).toEqual(['inputTriggers', 'sessions', 'slots', 'locale'])
|
||||
})
|
||||
|
||||
it('registers the "@" subagent source; disposal frees the name (HMR safety)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(InputTriggerService).await()
|
||||
ctx.provide('sessions', sessionsWith(FAMILY))
|
||||
ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
|
||||
// ui-theme's Appearance row binds a durable scope through these two.
|
||||
ctx.provide('remote', { $on: () => () => {} } as never)
|
||||
ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
|
||||
await provideSlotFaces(ctx)
|
||||
await ctx.plugin({ inject: localeInject, apply: applyLocale }).await()
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
const inputTriggers = ctx.get('inputTriggers') as InputTriggerService
|
||||
const rival = {
|
||||
trigger: '@' as const,
|
||||
name: 'subagent',
|
||||
candidates: () => Promise.resolve([]),
|
||||
onPick: () => undefined,
|
||||
}
|
||||
// Live registration holds the (trigger, name) seat…
|
||||
expect(() => inputTriggers.registerSource(rival)).toThrow(/already registered/)
|
||||
// …and fiber teardown releases it.
|
||||
await fiber.dispose()
|
||||
expect(() => inputTriggers.registerSource(rival)).not.toThrow()
|
||||
expect(inject).toEqual(['sessions', 'slots', 'locale'])
|
||||
})
|
||||
|
||||
it('registers catalog actions and selects read-only subagent composers from session facts', async () => {
|
||||
@@ -194,74 +138,3 @@ describe('apply', () => {
|
||||
expect(select(owner({ address, parentAvailable: false }, true))).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('candidates', () => {
|
||||
it('returns running children of the projected session, filtered by label containment', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
await expect(source.candidates(proj('parent'), req('worker'))).resolves.toEqual([
|
||||
{ name: 'worker-1' }, { name: 'worker-2' },
|
||||
])
|
||||
})
|
||||
|
||||
it('matches every running child on an empty query (containment, not prefix)', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
await expect(source.candidates(proj('parent'), req(''))).resolves.toEqual([
|
||||
{ name: 'worker-1' }, { name: 'worker-2' }, { name: 'scout' },
|
||||
])
|
||||
})
|
||||
|
||||
it('is candidate-less for a session with no children', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
await expect(source.candidates(proj('childless'), req(''))).resolves.toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('lexicon', () => {
|
||||
it('synchronously serves the projected session\'s full running-children roster', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
expect(source.lexicon!(proj('parent'))).toEqual(['worker-1', 'worker-2', 'scout'])
|
||||
expect(source.lexicon!(proj('childless'))).toEqual([])
|
||||
})
|
||||
|
||||
it('subscribeLexicon forwards the session-list change feed and unsubscribes cleanly', async () => {
|
||||
const { source, face } = await fullBench(FAMILY)
|
||||
let notified = 0
|
||||
const off = source.subscribeLexicon!(proj('parent'), () => { notified += 1 })
|
||||
expect(face.listenerCount()).toBe(1)
|
||||
face.notify()
|
||||
expect(notified).toBe(1)
|
||||
off()
|
||||
expect(face.listenerCount()).toBe(0)
|
||||
face.notify()
|
||||
expect(notified).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pick and codec', () => {
|
||||
it('onPick returns the literal @label text with a closing space', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
const outcome = source.onPick({
|
||||
candidate: { name: 'worker-1' },
|
||||
session: proj('parent'),
|
||||
position: 'inline',
|
||||
via: 'menu',
|
||||
span: { start: 4, end: 8, draftRev: 3 },
|
||||
})
|
||||
expect(outcome).toEqual({ text: '@worker-1 ' })
|
||||
})
|
||||
|
||||
it('codec projects clipboard `@label` and serializes the same raw label this phase', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
expect(source.codec!.clipboardText('worker-1')).toBe('@worker-1')
|
||||
await expect(source.codec!.serialize('worker-1', new AbortController().signal))
|
||||
.resolves.toBe('@worker-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('adjudication', () => {
|
||||
it('never participates: no matchSpace/matchEnter hooks on the subagent source', async () => {
|
||||
const source = await bench(FAMILY)
|
||||
expect('matchSpace' in source && source.matchSpace !== undefined).toBe(false)
|
||||
expect('matchEnter' in source && source.matchEnter !== undefined).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
{
|
||||
"path": "../ui-primitives"
|
||||
},
|
||||
{
|
||||
"path": "../ui-input-trigger"
|
||||
},
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
|
||||
@@ -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 packages/context/README.md
|
||||
README.md: fa28751e548dc4aec8e2e2711508816729f5d407
|
||||
README.zh.md: 9d12c7a854c6cb008c3806ea246ec5b5667be941
|
||||
README.md: ac1f6c302ce557adb71974c4d80b3b61e27f049c
|
||||
README.zh.md: 98cc4ab9b3a3f84e9f8dfff0a91025ee697ba240
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Product plugins that add model-visible request context without defining a tool. `agent-instructions` is included by the default `dsh-agent-spine-demo` bundle and can be disabled through bundle config; `time-context`, `tmux-context`, and `session-reference` are opt-in.
|
||||
Product plugins that add model-visible request context without defining a tool. `agent-instructions` is included by the default `dsh-agent-spine-demo` bundle and can be disabled through bundle config; `time-context`, `tmux-context`, `session-reference`, `file-reference`, and `file-reference-local` are opt-in.
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| [`session-reference/`](session-reference/README.md) | Bounded snapshots of other sessions | `ctx.sessionReferenceResolver` |
|
||||
| [`file-reference/`](file-reference/README.md) | File-reference discovery seam and `@file` grammar | `ctx.fileReferences` |
|
||||
| [`file-reference-local/`](file-reference-local/README.md) | Local-filesystem file-reference provider | — |
|
||||
| [`time-context/`](time-context/README.md) | Current-time and elapsed-time context | — |
|
||||
| [`tmux-context/`](tmux-context/README.md) | tmux location context | — |
|
||||
| [`agent-instructions/`](agent-instructions/README.md) | Workspace-instruction context | — |
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
在不定义工具的情况下添加模型可见的请求上下文的产品插件。`agent-instructions` 包含在默认 `dsh-agent-spine-demo` 组合包中,可通过组合包配置禁用;`time-context`、`tmux-context` 和 `session-reference` 需主动启用。
|
||||
在不定义工具的情况下添加模型可见的请求上下文的产品插件。`agent-instructions` 包含在默认 `dsh-agent-spine-demo` 组合包中,可通过组合包配置禁用;`time-context`、`tmux-context`、`session-reference`、`file-reference` 和 `file-reference-local` 需主动启用。
|
||||
|
||||
| 包 | 职责 | ctx key |
|
||||
|---|---|---|
|
||||
| [`session-reference/`](session-reference/README.md) | 其他会话的有界快照 | `ctx.sessionReferenceResolver` |
|
||||
| [`file-reference/`](file-reference/README.md) | 文件引用发现 seam 与 `@file` 语法 | `ctx.fileReferences` |
|
||||
| [`file-reference-local/`](file-reference-local/README.md) | 本地文件系统文件引用提供方 | — |
|
||||
| [`time-context/`](time-context/README.md) | 当前时间与耗时上下文 | — |
|
||||
| [`tmux-context/`](tmux-context/README.md) | tmux 位置上下文 | — |
|
||||
| [`agent-instructions/`](agent-instructions/README.md) | 工作区指令上下文 | — |
|
||||
|
||||
@@ -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 packages/context/file-reference-local/README.md
|
||||
README.md: 67b07eef4b59fdcc5e21104cac4628feb1c15f4e
|
||||
README.zh.md: beded13250daf041294e4e2656d0e1374407ff94
|
||||
@@ -0,0 +1,45 @@
|
||||
# `@deepseek-ai/dsh-file-reference-local`
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Local-filesystem implementation of `ctx.fileReferences`. It maintains one bounded `WorkspaceFileSearch` per agent, rooted at that session's `cwd` and falling back to the host process cwd. The index ranks direct directory listings for queries containing `/`, otherwise fuzzy-ranks a bounded recursive index; it never follows directory symlinks.
|
||||
|
||||
Tool-result events invalidate the addressed agent's reusable index so later completion observes likely workspace mutations. Agent disposal releases that index and its scoped prompt contribution; plugin disposal awaits every prompt fiber and releases all cached searches.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Default | Contract |
|
||||
|---|---:|---|
|
||||
| `maxResults` | `20` | Maximum ranked candidates returned for one query. |
|
||||
| `maxEntries` | `10000` | Maximum files and directories indexed per agent workspace. |
|
||||
| `excludedDirectories` | `[".git", "node_modules"]` | Directory basenames omitted from traversal and candidates. |
|
||||
|
||||
Every numeric value must be a positive safe integer. Excluded names must be non-empty basenames without `/` or `\`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### File-reference guidance when `read` is available
|
||||
|
||||
#### What the model sees
|
||||
|
||||
When the addressed agent has an effective `read` tool, the provider contributes this stable system-prompt section:
|
||||
|
||||
##### File-reference instruction
|
||||
|
||||
```markdown
|
||||
Paths prefixed with @ are files explicitly referenced by the user. Use the read tool when their contents are needed; do not claim to have inspected a file before reading it.
|
||||
```
|
||||
|
||||
#### Token effect
|
||||
|
||||
Conditional and fixed: the one sentence is present while `read` is visible to the addressed agent; candidate lookup itself adds no tokens, and a selected path contributes only its ordinary user-message characters.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The stable sentence joins the system-prompt prefix. Mounting or removing this provider, or changing whether `read` is visible, changes that prefix; queries, candidates, and index invalidations do not.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Host-local namespace** — the provider scans the Harness host filesystem, so remote or virtual `read` implementations require a provider whose namespace matches the tool.
|
||||
- **Bounded advisory index** — very large workspaces may omit paths after `maxEntries`, and excluded or unreadable directories do not appear.
|
||||
- **No ignore-file semantics** — `.gitignore` and other project ignore files do not influence discovery; only configured directory basenames are excluded.
|
||||
@@ -0,0 +1,45 @@
|
||||
# `@deepseek-ai/dsh-file-reference-local`
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`ctx.fileReferences` 的本地文件系统实现。它为每个 agent(智能体)维护一个有界的 `WorkspaceFileSearch`,以该会话的 `cwd` 为根目录;缺少该值时回退到宿主进程的 cwd。查询包含 `/` 时,索引会对直接列出的目录项排序;否则会对有界递归索引进行模糊排序。索引永远不会跟随目录符号链接。
|
||||
|
||||
工具结果事件会使指定 agent 的可复用索引失效,使后续补全能够反映工作区中可能发生的变更。agent 的 dispose(资源释放)会释放该索引及其作用域内的提示词贡献;插件 dispose 会等待所有提示词 fiber,并释放全部缓存的搜索器。
|
||||
|
||||
## 配置
|
||||
|
||||
| 配置键 | 默认值 | 契约 |
|
||||
|---|---:|---|
|
||||
| `maxResults` | `20` | 单次查询返回的候选项最大数量。 |
|
||||
| `maxEntries` | `10000` | 每个 agent 工作区建立索引的文件和目录最大数量。 |
|
||||
| `excludedDirectories` | `[".git", "node_modules"]` | 遍历和候选项中排除的目录基名。 |
|
||||
|
||||
所有数值都必须是正的安全整数。排除名称必须是非空基名,且不能包含 `/` 或 `\`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
### `read` 可用时的文件引用指引
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
当指定 agent 有实际生效的 `read` 工具时,提供方会贡献以下稳定的系统提示词段:
|
||||
|
||||
##### 文件引用指令
|
||||
|
||||
```markdown
|
||||
Paths prefixed with @ are files explicitly referenced by the user. Use the read tool when their contents are needed; do not claim to have inspected a file before reading it.
|
||||
```
|
||||
|
||||
#### Token 影响
|
||||
|
||||
该影响有条件且固定:只要 `read` 对指定 agent 可见,这一句就会存在;候选查询本身不增加 token,所选路径只会贡献普通用户消息中的对应字符。
|
||||
|
||||
#### KV 缓存影响
|
||||
|
||||
该稳定句子会加入系统提示词前缀。挂载或移除此提供方,或者改变 `read` 是否可见,都会改变该前缀;查询、候选项和索引失效不会改变前缀。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **宿主本地命名空间**:提供方扫描 Harness 宿主的文件系统,因此远程或虚拟 `read` 实现需要使用命名空间与该工具一致的提供方。
|
||||
- **有界的提示性索引**:超大型工作区可能省略 `maxEntries` 之后的路径;被排除或无法读取的目录不会出现。
|
||||
- **没有忽略文件语义**:`.gitignore` 和其他项目忽略文件不会影响发现;系统只排除已配置的目录基名。
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-file-reference-local",
|
||||
"description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes",
|
||||
"version": "0.1.0-rc.7",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/context/file-reference-local"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./search": {
|
||||
"types": "./lib/types/search.d.ts",
|
||||
"default": "./lib/types/search.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Local-filesystem implementation of `ctx.fileReferences`.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-file-reference-local
|
||||
*/
|
||||
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import FileReferenceService, {
|
||||
FILE_REFERENCE_PROMPT,
|
||||
type FileReferenceCandidate,
|
||||
} from '@deepseek-ai/dsh-file-reference'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import {
|
||||
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
||||
DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
||||
DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
||||
WorkspaceFileSearch,
|
||||
type FileSearchConfig,
|
||||
} from './search.ts'
|
||||
|
||||
export {
|
||||
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
||||
DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
||||
DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
||||
WorkspaceFileSearch,
|
||||
} from './search.ts'
|
||||
export type { FileSearchConfig } from './search.ts'
|
||||
export { FILE_REFERENCE_PROMPT } from '@deepseek-ai/dsh-file-reference'
|
||||
export { activeAtToken, formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar'
|
||||
|
||||
/** Local file-reference discovery configuration. */
|
||||
export interface Config {
|
||||
/** Maximum ranked candidates returned for one query. */
|
||||
maxResults?: number
|
||||
/** Maximum indexed files and directories per agent workspace. */
|
||||
maxEntries?: number
|
||||
/** Directory basenames never traversed or offered. */
|
||||
excludedDirectories?: string[]
|
||||
}
|
||||
|
||||
/** Local-filesystem owner of the file-reference discovery service. */
|
||||
export class LocalFileReferenceService extends FileReferenceService {
|
||||
static inject = ['agents']
|
||||
static Config: z<Config> = z.object({
|
||||
maxResults: z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_RESULTS),
|
||||
maxEntries: z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_ENTRIES),
|
||||
excludedDirectories: z.array(z.string()).default([...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]),
|
||||
})
|
||||
|
||||
private readonly config: FileSearchConfig
|
||||
private readonly searches = new Map<Agent, WorkspaceFileSearch>()
|
||||
private readonly promptFibers = new Map<Agent, ReturnType<Context['inject']>>()
|
||||
private readonly promptDisposals = new Set<Promise<void>>()
|
||||
|
||||
constructor(ctx: Context, config: Config = {}) {
|
||||
super(ctx)
|
||||
this.config = {
|
||||
maxResults: config.maxResults ?? DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
||||
maxEntries: config.maxEntries ?? DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
||||
excludedDirectories: config.excludedDirectories ?? DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
||||
}
|
||||
validateConfig(this.config)
|
||||
|
||||
const installPrompt = (agent: Agent): void => {
|
||||
if (this.promptFibers.has(agent)) return
|
||||
const fiber = agent.ctx.inject(['systemPrompt', 'tools'], (scope) => {
|
||||
scope.systemPrompt.section({
|
||||
name: 'context:file-reference',
|
||||
order: 99,
|
||||
text: () => agent.ctx.tools.get('read', agent) === undefined ? '' : FILE_REFERENCE_PROMPT,
|
||||
})
|
||||
})
|
||||
this.promptFibers.set(agent, fiber)
|
||||
}
|
||||
const disposePrompt = (agent: Agent): void => {
|
||||
const fiber = this.promptFibers.get(agent)
|
||||
if (fiber === undefined) return
|
||||
this.promptFibers.delete(agent)
|
||||
const task = fiber.dispose().catch((error: unknown) => {
|
||||
ctx.logger.warn(`file-reference-local: prompt cleanup failed: ${error instanceof Error ? error.message : String(error)}`)
|
||||
})
|
||||
this.promptDisposals.add(task)
|
||||
void task.finally(() => {
|
||||
this.promptDisposals.delete(task)
|
||||
})
|
||||
}
|
||||
for (const agent of ctx.agents.list()) installPrompt(agent)
|
||||
ctx.on('agent/created', ({ agent }) => { installPrompt(agent) })
|
||||
ctx.on('agent/disposed', ({ agent }) => {
|
||||
this.searches.get(agent)?.dispose()
|
||||
this.searches.delete(agent)
|
||||
disposePrompt(agent)
|
||||
})
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (event.type !== 'tool/result') return
|
||||
const agent = ctx.agents.get(session.id)
|
||||
if (agent !== undefined) this.searches.get(agent)?.invalidate()
|
||||
})
|
||||
ctx.effect(() => async () => {
|
||||
for (const search of this.searches.values()) search.dispose()
|
||||
this.searches.clear()
|
||||
const promptFibers = [...this.promptFibers.values()]
|
||||
this.promptFibers.clear()
|
||||
await Promise.all([
|
||||
...promptFibers.map(fiber => fiber.dispose()),
|
||||
...this.promptDisposals,
|
||||
])
|
||||
}, 'file-reference-local: search cache')
|
||||
}
|
||||
|
||||
override list(
|
||||
agent: Agent,
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<FileReferenceCandidate[]> {
|
||||
let search = this.searches.get(agent)
|
||||
if (search === undefined) {
|
||||
search = new WorkspaceFileSearch(agent.session.header.cwd ?? process.cwd(), this.config)
|
||||
this.searches.set(agent, search)
|
||||
}
|
||||
return search.list(query, signal)
|
||||
}
|
||||
}
|
||||
|
||||
function validateConfig(config: FileSearchConfig): void {
|
||||
if (!Number.isSafeInteger(config.maxResults) || config.maxResults <= 0) {
|
||||
throw new Error('file-reference-local: maxResults must be a positive safe integer')
|
||||
}
|
||||
if (!Number.isSafeInteger(config.maxEntries) || config.maxEntries <= 0) {
|
||||
throw new Error('file-reference-local: maxEntries must be a positive safe integer')
|
||||
}
|
||||
if (config.excludedDirectories.some(name => name.length === 0 || name.includes('/') || name.includes('\\'))) {
|
||||
throw new Error('file-reference-local: excludedDirectories entries must be non-empty directory basenames')
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalFileReferenceService
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-file-reference-local`.
|
||||
* @module @deepseek-ai/dsh-file-reference-local/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-file-reference-local'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'file-reference-local-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: per-agent indexes are private advisory caches whose
|
||||
* invalidation and disposal are observed directly through service tests.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user