Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts: # apps/cli/tests/web-agent-presets.e2e.ts # packages/client/connection/README.i18n.yaml # packages/client/connection/README.md # packages/client/connection/README.zh.md # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-primitives/tests/icons.spec.tsx # packages/client/ui-settings/src/client/contract/slots.ts # packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
|
||||
README.md: d894d3e578f065e9cb2c45676224efd1f4f76fa8
|
||||
README.zh.md: ec99a8d7daa593cdf3014f292a095fb2fd6f39d3
|
||||
README.md: b57f88b5a030a6c20c957e26ea32fb125f106ab4
|
||||
README.zh.md: a8a8c4814086cad02c5416078f242ec92a7503d7
|
||||
|
||||
@@ -38,7 +38,7 @@ Per-session UI state for selection and the active view lives in the declared cha
|
||||
|
||||
The composer bar declares session-scoped single seats for `'conversation.input.plan'` (right of the local access-mode control) and `'conversation.input.model'` (immediately before the pending indicator and send/stop controls), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the `locked` owner prop, and the standard slot shares. The leading plus button is a Command launcher, not an attachment surface: it asks the session's `SlashController` to open only the `/` trigger's `command` source over the current textarea selection, while ui-slash's existing `MenuView` remains the sole floating menu and pick path. No file row, file input, upload protocol, or second menu component is introduced. While the `plan` projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording, localized through the `conversation` locale namespace this package registers (the `placeholder.plan` / `hint.plan` keys) and shared verbatim with the claimed `/plan` command hint (a host-folded value read through the standard-kit `useProjection`; owner-supplied placeholders win). A pending composer takeover remains mounted when another conversation view is active so the blocked agent can still receive its answer; without a pending interaction, the active-session composer belongs to Chat. The composer-bar slot itself is `session-maybe`: with no current session the same bar renders inert (machine faces absent, `disabled` owner prop) instead of swapping in a parallel disabled tree, so the textarea DOM survives the workspace pick; the strict-session control seats simply stay empty until a session exists.
|
||||
|
||||
The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. Visible nodes supply only the turn and step counts plus the LLM and tool wall times, which are window-scoped facts about what is on screen rather than accounting; durable token and context groups remain visible when compaction leaves no assistant node in the loaded window. The same window fold averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy moved off the row onto the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation.
|
||||
The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. Visible nodes supply only the turn and step counts plus the LLM and tool wall times, which are window-scoped facts about what is on screen rather than accounting; durable token and context groups remain visible when compaction leaves no assistant node in the loaded window. The same window fold averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation.
|
||||
|
||||
`src/client/` is organized by domain. `contract/` is the shared face for slot declarations, composed props, and cross-domain types; `skeleton/`, `chat/`, `input/`, `queue/`, and `settings/` keep their implementations internal, while `apply.ts` is their assembly point. The `/client` export surface contains only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations.
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu
|
||||
|
||||
输入栏为 `'conversation.input.plan'`(位于本地 access 模式控件右侧)和 `'conversation.input.model'`(渲染在 pending 指示器与发送/停止控件之前)声明会话作用域的单实例 seat,并为 overlay、dock、left 和 right 输入扩展声明列表 slot。各功能包拥有相应控件及其状态;ui-conversation 提供放置位置、`locked` owner prop 和标准 slot share。前置加号按钮是 Command launcher,而非附件入口:它要求当前会话的 `SlashController` 基于 textarea 当前 selection,只打开 `/` trigger 的 `command` source,同时 ui-slash 既有的 `MenuView` 仍是唯一的浮层菜单与 pick 路径。不引入 File 行、file input、上传协议或第二套菜单组件。当 `plan` 投影的有效目标为 plan mode 时,InputBar 将文本框 placeholder 切换为 plan 任务措辞,经本包注册的 `conversation` locale 命名空间(`placeholder.plan` / `hint.plan` 键)本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(经标准套件 `useProjection` 读取的 host 折叠值;owner 提供的 placeholder 优先)。另一个会话视图活跃时,待处理的 composer 接管仍保持挂载,使被阻塞的 agent(智能体)仍能收到回答;没有待处理交互时,活跃会话的 composer 归 Chat 所有。composer bar slot 本身为 `session-maybe`:没有当前会话时,同一个 bar 以不可交互状态渲染(machine face 均缺席、`disabled` owner prop),而不是换入一棵平行的 disabled 树,因此选择 workspace 时 textarea DOM 不会被销毁;严格会话作用域的控件 seat 在会话存在之前保持为空。
|
||||
|
||||
聊天统计行的 token 账目来自经标准套件 `useProjection` 读取的通用 token-meter 投影 `tokenUsage`:计费输入为未缓存输入、缓存读取与缓存写入之和;缓存命中率以缓存读取除以该总量。可见节点只提供轮次与步骤计数,以及 LLM(大语言模型)和工具的墙钟时间:这些是关于「屏幕上有什么」的窗口作用域事实,而非账目;压缩(compaction)使已加载窗口不再包含 assistant 节点时,持久 token 与上下文分组仍保持可见。同一次窗口折算还会把每个有完整记录的步骤的 TTFT(首 token 延迟)取平均,并用采样到的输出 token 数除以其解码时长之和,得到经 `conversation` locale 命名空间本地化的延迟/吞吐分组(中文为 `首 token 平均 … · … tok/s`);缺少某个 timing 边界或 usage 采样的步骤会直接退出这些数字,而不是让它们失真。轮次计数、步骤计数、耗时、缓存与 token 各项的标签也使用同一命名空间。每个已结算轮次还会在其 assistant footer 的 `用时` 之后追加 hover 才显示的 `首 token {s}秒 · {tps} tok/s` 标签——即该轮次首个步骤的 TTFT 与轮次聚合的解码吞吐——仅当该轮次的 timing 位于已加载窗口内才显示(窗口是日志的连续后缀,因此窗口内的轮次必然带着它的全部步骤),未记录的数字会各自省略。未组合 token-meter 的部署会整组省略 token 分组;统计行过长时以省略号截断,仅在内容真的被裁切时由延迟 hover tooltip 承载完整文本。上下文占用率从统计行移到了 composer 尾部的 ContextMeter:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,仅当分子与路由容量都已知时才渲染;点击弹出的面板把「已用百分比」标题与 `~已用 / 容量` 数字,与来自 `contextBreakdown` 投影、带 `~` 前缀的启发式组成明细行(系统提示词、工具、对话消息)及分色分段进度条并列。圆环与标题读取 `projectedTokens`——把提供方样本沿此后表层的增减推进到当下——因此压缩会立刻反映出来,而不必再等一整轮;组成明细行仍是纯启发式,因此加起来依然不等于标题数字([原理](../../llm/token-meter/README.md))。占用率是刻意为之的近似值:分子与容量是两个相互独立的「后写覆盖」投影字段,并非同一次请求的原子观测。
|
||||
聊天统计行的 token 账目来自经标准套件 `useProjection` 读取的通用 token-meter 投影 `tokenUsage`:计费输入为未缓存输入、缓存读取与缓存写入之和;缓存命中率以缓存读取除以该总量。可见节点只提供轮次与步骤计数,以及 LLM(大语言模型)和工具的墙钟时间:这些是关于「屏幕上有什么」的窗口作用域事实,而非账目;压缩(compaction)使已加载窗口不再包含 assistant 节点时,持久 token 与上下文分组仍保持可见。同一次窗口折算还会把每个有完整记录的步骤的 TTFT(首 token 延迟)取平均,并用采样到的输出 token 数除以其解码时长之和,得到经 `conversation` locale 命名空间本地化的延迟/吞吐分组(中文为 `首 token 平均 … · … tok/s`);缺少某个 timing 边界或 usage 采样的步骤会直接退出这些数字,而不是让它们失真。轮次计数、步骤计数、耗时、缓存与 token 各项的标签也使用同一命名空间。每个已结算轮次还会在其 assistant footer 的 `用时` 之后追加 hover 才显示的 `首 token {s}秒 · {tps} tok/s` 标签——即该轮次首个步骤的 TTFT 与轮次聚合的解码吞吐——仅当该轮次的 timing 位于已加载窗口内才显示(窗口是日志的连续后缀,因此窗口内的轮次必然带着它的全部步骤),未记录的数字会各自省略。未组合 token-meter 的部署会整组省略 token 分组;统计行过长时以省略号截断,仅在内容真的被裁切时由延迟 hover tooltip 承载完整文本。上下文占用率渲染为 composer 尾部的 ContextMeter:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,仅当分子与路由容量都已知时才渲染;点击弹出的面板把「已用百分比」标题与 `~已用 / 容量` 数字,与来自 `contextBreakdown` 投影、带 `~` 前缀的启发式组成明细行(系统提示词、工具、对话消息)及分色分段进度条并列。圆环与标题读取 `projectedTokens`——把提供方样本沿此后表层的增减推进到当下——因此压缩会立刻反映出来,而不必再等一整轮;组成明细行仍是纯启发式,因此加起来依然不等于标题数字([原理](../../llm/token-meter/README.md))。占用率是刻意为之的近似值:分子与容量是两个相互独立的「后写覆盖」投影字段,并非同一次请求的原子观测。
|
||||
|
||||
`src/client/` 按领域组织。`contract/` 是 slot 声明、组合 props 与跨领域类型的共享表层;`skeleton/`、`chat/`、`input/`、`queue/` 和 `settings/` 保持内部实现,`apply.ts` 是它们的组装点。`/client` 导出表层只包含 loader entry、service class 和 contract 类型;组件与 store factory 经 slot 注册抵达页面。
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ export function apply(ctx: Context): void {
|
||||
// way: this package must not import the plugins that would know.
|
||||
const composerBlocks = new ComposerBlockRegistry()
|
||||
|
||||
// Decision 19/20: the input machine feeds every session-scope slot
|
||||
// The input machine feeds every session-scope slot
|
||||
// component through the standard provide channel — the 'input' hook plus
|
||||
// the two public actions. Materialization is the shell creation trigger
|
||||
// (per-session lazy; scope disposer tears down).
|
||||
@@ -242,7 +242,7 @@ export function apply(ctx: Context): void {
|
||||
}, ConversationSessionHeader)
|
||||
|
||||
// The default composer body: its own single slot inside the composer
|
||||
// chain's fallback (decision 20). Public machine surface arrives via the
|
||||
// chain's fallback. Public machine surface arrives via the
|
||||
// provide channel above; the keyboard command face and the stop/retry
|
||||
// verbs ride this inject (package-internal — hub and bar are one plugin).
|
||||
// Session-maybe: with no current session the machine faces are absent and
|
||||
@@ -253,7 +253,7 @@ export function apply(ctx: Context): void {
|
||||
locale: NS,
|
||||
// The two named control seats in the bar's tool row (plan beside the
|
||||
// access control, model right); empty until their owning plugins
|
||||
// register (B ruling).
|
||||
// register.
|
||||
children: {
|
||||
'conversation.input.plan': { kind: 'single', scope: 'session' },
|
||||
'conversation.input.model': { kind: 'single', scope: 'session' },
|
||||
@@ -383,7 +383,7 @@ export function apply(ctx: Context): void {
|
||||
// The plan strip rides the input dock above the queue rows (same posture).
|
||||
ctx.plugin(todoDockEntry)
|
||||
|
||||
// The read-only queue dock entry (T9 file territory) rides the same
|
||||
// The read-only queue dock entry rides the same
|
||||
// registration path into the input dock declared above.
|
||||
ctx.plugin(queueDockEntry)
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ export function ChatView({
|
||||
const lastKeyRef = useRef<string | null>(null)
|
||||
const lastSteeringIdRef = useRef<string | null>(null)
|
||||
/** Flow tip signature — follow-scroll only when this moves, never on a
|
||||
* scroll-driven at-bottom chrome re-render (that was snapping inertial
|
||||
* scroll-driven at-bottom chrome re-render (which would snap inertial
|
||||
* scrolls the rest of the way to the floor). */
|
||||
const followSigRef = useRef<string | null>(null)
|
||||
|
||||
|
||||
@@ -123,8 +123,8 @@ function TurnErrorItem({ node, t }: {
|
||||
* Display projection of reference forms in a user bubble (free geometry — no
|
||||
* textarea alignment constraint here); everything else stays plain text. The
|
||||
* logged model text remains the single truth; this is presentation only.
|
||||
* Plain-text `/name` / `@name` word-boundary tokens decorate (decision 21:
|
||||
* the sent text IS the reference — the bubble uses the same plainest token
|
||||
* Plain-text `/name` / `@name` word-boundary tokens decorate (the sent text
|
||||
* IS the reference — the bubble uses the same plainest token
|
||||
* scan as the composer, minus the lexicon: sent tokens were validated at
|
||||
* compose time, so shape alone decorates).
|
||||
*/
|
||||
|
||||
@@ -85,14 +85,14 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
* the next one rather than applied to a current one.
|
||||
*/
|
||||
'conversation.hero.agentPreset': { kind: 'single'; scope: 'root'; owner: HeroAgentPresetOwnerProps }
|
||||
// 'conversation.input.overlay' merges in ui-slash (dedup ruling: the
|
||||
// dependency direction is the hard constraint — ui-slash cannot import
|
||||
// 'conversation.input.overlay' merges in ui-slash (the dependency
|
||||
// direction is the hard constraint — ui-slash cannot import
|
||||
// this package, while this package's input contract already imports
|
||||
// ui-slash, so the type arrives transitively). The runtime declaration
|
||||
// (children table in apply.ts) stays here with the other input slots.
|
||||
/**
|
||||
* Stacked strip above the input (queue rows / GoalBar / attachments;
|
||||
* design §6 MIX evidence: entries coexist in fixed order).
|
||||
* entries coexist in fixed order).
|
||||
*/
|
||||
'conversation.input.dock': { kind: 'list'; scope: 'session'; owner: InputZone }
|
||||
/** The band under the composer card (stats line family), rendered inside the bar's width column via the `footer` owner prop. */
|
||||
@@ -103,7 +103,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
'conversation.input.right': { kind: 'list'; scope: 'session'; owner: InputZone }
|
||||
/**
|
||||
* The default composer body: a single slot rendered as the composer
|
||||
* chain's fallback (decision 20 — a real entry, not a chain rider, so a
|
||||
* chain's fallback (a real entry, not a chain rider, so a
|
||||
* takeover election hides rather than unmounts it and the textarea DOM
|
||||
* survives). Session-maybe: the bar stays mounted across the
|
||||
* no-session/session transition — the no-workspace hero renders the SAME
|
||||
@@ -117,7 +117,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
/**
|
||||
* The Plan-mode status seat in the composer tool row (left group,
|
||||
* right of the access-mode control). Declared by the composer-bar
|
||||
* entry; empty until a plan plugin registers (B ruling: no placeholder
|
||||
* entry; empty until a plan plugin registers (no placeholder
|
||||
* fallback).
|
||||
*/
|
||||
'conversation.input.plan': { kind: 'single'; scope: 'session'; owner: InputControlOwnerProps }
|
||||
@@ -130,7 +130,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
|
||||
/**
|
||||
* ui-conversation's members of the session standard kit, provided through
|
||||
* `sessions.provide` (decision 19/20): every session-scope slot component
|
||||
* `sessions.provide`: every session-scope slot component
|
||||
* receives the input machine's state hook and the two public actions.
|
||||
*/
|
||||
interface SessionStandardProps {
|
||||
@@ -173,7 +173,7 @@ export interface ConversationSessionOwnerProps {
|
||||
export interface ConversationHeaderActionOwnerProps {}
|
||||
|
||||
/**
|
||||
* The input-region slot currency (plan §1.4): dock/left/right entries read
|
||||
* The input-region slot currency: dock/left/right entries read
|
||||
* the conversation snapshot and the live input state as owner props (both
|
||||
* are point-in-time snapshots — the dispatching skeleton re-renders on
|
||||
* either store's change, so entries stay current without subscribing).
|
||||
@@ -381,7 +381,7 @@ export interface ComposerBarOwnerProps {
|
||||
|
||||
/** Injected share of the composer-bar entry (package-internal faces). */
|
||||
export interface ComposerBarInjected {
|
||||
/** The InputBar-exclusive keyboard/DOM command face (decision 20 private plane); absent with the session. */
|
||||
/** The InputBar-exclusive keyboard/DOM command face (private plane); absent with the session. */
|
||||
keyboard: ComposerKeyboard | undefined
|
||||
/** Resolve one keyboard submission gesture against the current running state and persisted preference. */
|
||||
resolveSubmitMode: (
|
||||
@@ -408,7 +408,8 @@ export interface ComposerBarInjected {
|
||||
hooks: {
|
||||
/** Latest surfaced notice (null after none; seq keys re-render of repeats). */
|
||||
notices: ObservableSnapshot<InputNotice | null>
|
||||
/** Hot plain-text reference lexicon for the decoration scan (decision 21). */
|
||||
/** Hot plain-text reference lexicon for the decoration scan (plain-text-reference decision;
|
||||
* see .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md). */
|
||||
lexicon: ObservableSnapshot<ReadonlyMap<'/' | '@', readonly string[]>>
|
||||
/** Source name opened by the programmatic menu launcher, or null. */
|
||||
menuLauncher: ObservableSnapshot<string | null>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Frozen input-machine contract (design §9.1, eng. plan §3.9-3.12). Types
|
||||
* Frozen input-machine contract. Types
|
||||
* only. Three-tier visibility: business packages see InputState via the
|
||||
* InputZone currency; the scoped input events carry the mutation verbs; the
|
||||
* conversation wiring layer alone sees the full SessionInput. InputMachine
|
||||
@@ -56,7 +56,7 @@ export interface InputService {
|
||||
|
||||
/**
|
||||
* The public input action face provided to every session-scope slot
|
||||
* component (decision 20): two stable-identity void callbacks, mirroring the
|
||||
* component: two stable-identity void callbacks, mirroring the
|
||||
* useStore+actions convention. Command-style handles (track/arbitrate/space/
|
||||
* undo/paste/…) stay InputBar-private and never ride this face.
|
||||
*/
|
||||
@@ -75,7 +75,7 @@ export interface InputNotice {
|
||||
}
|
||||
|
||||
/**
|
||||
* The InputBar-exclusive keyboard/DOM command face (decision 20): synchronous
|
||||
* The InputBar-exclusive keyboard/DOM command face: synchronous
|
||||
* returns and event-handler semantics that must not enter the public provide
|
||||
* channel. Handed to the composer-bar entry through its own inject —
|
||||
* package-internal, never across a plugin boundary. The session shell
|
||||
@@ -128,7 +128,7 @@ export interface EditRange extends EditSelection {
|
||||
|
||||
/**
|
||||
* One reference chip occurrence, backing exactly one U+FFFC placeholder in
|
||||
* the draft (design §9.1 底层表示). Identity is occurrenceId — same-named
|
||||
* the draft. Identity is occurrenceId — same-named
|
||||
* references stay independently addressable. label/clipboardText are the
|
||||
* owner's insert-time projections, cached so the chip survives owner loss
|
||||
* (invalid flips instead of dropping the occurrence).
|
||||
@@ -157,7 +157,7 @@ export interface PasteComponent extends EditSelection {
|
||||
|
||||
/**
|
||||
* Live paste-match attempt published while async matching may still upgrade
|
||||
* pasted tokens (design §9.1 剪贴板 round-trip). Any non-paste transaction,
|
||||
* pasted tokens (the clipboard round-trip). Any non-paste transaction,
|
||||
* submit start, invalidate-paste, or release ends it; a paste-upgrade keeps
|
||||
* it current (later tokens re-CAS against the advanced draftRev).
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Draft decoration pure core (design §9.1: chips render from the occurrence
|
||||
* Draft decoration pure core (chips render from the occurrence
|
||||
* table at placeholder offsets; the claim token renders as a mirror-layer
|
||||
* highlight, the claim hint as ghost text). Zero React — the skeleton renders
|
||||
* the instructions; tests drive this directly.
|
||||
@@ -24,7 +24,9 @@ export interface ChipRender {
|
||||
}
|
||||
|
||||
/**
|
||||
* One plain-text reference range (decision 21): a `/name` or `@name` token
|
||||
* One plain-text reference range (the plain-text-reference decision;
|
||||
* see .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md):
|
||||
* a `/name` or `@name` token
|
||||
* whose name is on the trigger's lexicon. Pure derivation — editing the text
|
||||
* out of match shape simply drops the range next scan.
|
||||
*/
|
||||
@@ -50,8 +52,8 @@ export interface DraftDecorations {
|
||||
const TEXT_REF_RE = /(^|\s)([/@])([\w-]+)/g
|
||||
|
||||
/**
|
||||
* Scan the draft for plain-text reference tokens against the hot lexicons
|
||||
* (decision 21). Word-boundary discipline: the trigger must sit at the draft
|
||||
* Scan the draft for plain-text reference tokens against the hot lexicons.
|
||||
* Word-boundary discipline: the trigger must sit at the draft
|
||||
* start or after whitespace ('x/name' never matches); the name must be an
|
||||
* exact lexicon member.
|
||||
* @param draft - draft text.
|
||||
@@ -82,7 +84,7 @@ const EMPTY_LEXICON: ReadonlyMap<'/' | '@', readonly string[]> = new Map()
|
||||
/**
|
||||
* Derive the mirror-layer decorations from the input state.
|
||||
* @param state - published input state.
|
||||
* @param lexicon - optional per-trigger reference lexicons (decision 21 scan).
|
||||
* @param lexicon - optional per-trigger reference lexicons (plain-text-reference scan).
|
||||
* @returns token range, chip instructions, text-ref ranges, and the ghost hint.
|
||||
*/
|
||||
export function deriveDecorations(
|
||||
|
||||
@@ -66,7 +66,7 @@ export class SessionInputShell implements SessionInput {
|
||||
readonly state: SnapshotStore<InputState>
|
||||
/** Latest surfaced notice (null after clear); the wiring renders it beside the error strip. */
|
||||
readonly notices: SnapshotStore<InputNotice | null> = createSnapshotStore<InputNotice | null>(null)
|
||||
/** The public provide-channel action face (one stable identity per session — decision 20). */
|
||||
/** The public provide-channel action face (one stable identity per session). */
|
||||
readonly actions: InputActions = {
|
||||
setDraft: (text) => { this.setDraft(text) },
|
||||
submit: () => { this.submit('queue') },
|
||||
@@ -198,7 +198,9 @@ export class SessionInputShell implements SessionInput {
|
||||
|
||||
/**
|
||||
* Hot plain-text reference lexicon source for the decoration scan
|
||||
* (decision 21): delegates to the controller's aggregated store. Stable
|
||||
* (the plain-text-reference decision;
|
||||
* see .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md):
|
||||
* delegates to the controller's aggregated store. Stable
|
||||
* identity per shell; without a pipeline the snapshot is the empty Map and
|
||||
* subscribers never fire.
|
||||
*/
|
||||
@@ -253,7 +255,8 @@ export class SessionInputShell implements SessionInput {
|
||||
|
||||
/**
|
||||
* Insert plain reference text over the pick-time span (scoped insert-text
|
||||
* event listener body, decision 21). Same CAS-then-splice shape as the
|
||||
* event listener body; plain-text-reference decision, web-input-machine
|
||||
* note). Same CAS-then-splice shape as the
|
||||
* consume-token span branch: the machine sees an ordinary draft-changed
|
||||
* transaction (one undo step), no occurrence is minted — the chip look is
|
||||
* a scan-derived decoration, never state.
|
||||
@@ -339,7 +342,7 @@ export class SessionInputShell implements SessionInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt serialization before the sink (design §3.12): expand each
|
||||
* Prompt serialization before the sink: expand each
|
||||
* placeholder to its owner's model form via the session controller's
|
||||
* codec routing. Owner missing / serialize failure / disposal blocks the
|
||||
* send — notice + draft and chips retained, never a silent downgrade to
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* InputHub: the InputService implementation (`ctx.conversation.input`) — one
|
||||
* SessionInputShell per session, created inside the sessions provide
|
||||
* materialization (decision 19: the 'input' standard-kit entry IS the
|
||||
* materialization (the 'input' standard-kit entry IS the
|
||||
* creation trigger) and torn down by the scope disposer (instance-and-scope
|
||||
* share one lifecycle). The hub registers the three scoped input-mutation
|
||||
* listeners on each session's actx (the sole consumer side of the ui-slash
|
||||
@@ -61,7 +61,7 @@ export class InputHub implements InputService {
|
||||
})
|
||||
this.shells.set(id, shell)
|
||||
// The one teardown axis: listeners, shell, and map entries all ride the
|
||||
// scope fiber (decision 12 — nothing here outlives the scope).
|
||||
// scope fiber (nothing here outlives the scope).
|
||||
actx.effect(() => {
|
||||
const offs = [
|
||||
actx.on('slash/input-begin-command', req =>
|
||||
@@ -97,7 +97,7 @@ export class InputHub implements InputService {
|
||||
}
|
||||
|
||||
/**
|
||||
* The InputBar-exclusive keyboard command face (decision 20): the shell
|
||||
* The InputBar-exclusive keyboard command face: the shell
|
||||
* satisfies it structurally; package-internal — handed through the
|
||||
* composer-bar entry's inject, never across a plugin boundary.
|
||||
* @param id - session id.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* InputMachine: the pure per-session input state machine (design §9.1, eng.
|
||||
* plan §3.9-3.12). Events in, effects out; zero React / DOM / cordis / ambient
|
||||
* InputMachine: the pure per-session input state machine.
|
||||
* Events in, effects out; zero React / DOM / cordis / ambient
|
||||
* clock. Package-private — the SessionInput shell is the only caller and the
|
||||
* sole executor of the returned effects.
|
||||
*
|
||||
@@ -23,10 +23,10 @@ import type {
|
||||
/** The object-replacement character backing every chip occurrence in the draft. */
|
||||
export const PLACEHOLDER = ''
|
||||
|
||||
/** The machine never writes the queue; the wiring layer overlays the T9 store projection. */
|
||||
/** The machine never writes the queue; the wiring layer overlays the queue store's projection. */
|
||||
const EMPTY_QUEUE: InputState['queue'] = []
|
||||
|
||||
/** Undo ring depth (design §9.1: bounded self-managed transaction log). */
|
||||
/** Undo ring depth (bounded self-managed transaction log). */
|
||||
const LOG_LIMIT = 100
|
||||
|
||||
/** Exhaustiveness backstop for the closed InputEvent / guard unions. */
|
||||
@@ -68,7 +68,7 @@ function diffEdit(prev: string, next: string): EditRange {
|
||||
|
||||
/**
|
||||
* Expand the draft's placeholders into their occurrences' clipboard text
|
||||
* (decision 16: the persistence mirror and clipboard both write this
|
||||
* (the persistence mirror and clipboard both write this
|
||||
* projection — U+FFFC never leaves the machine). Table order is offset
|
||||
* order, so one linear walk pairs placeholders with entries.
|
||||
* @param state - published input state.
|
||||
@@ -194,7 +194,7 @@ export class InputMachine {
|
||||
/**
|
||||
* Reconcile the occurrence table with one edit (old-draft coordinates):
|
||||
* entries past the range shift by the length delta; entries whose
|
||||
* placeholder sits inside the replaced range go away whole (design §9.1: a
|
||||
* placeholder sits inside the replaced range go away whole (a
|
||||
* deletion/replacement intersecting a placeholder acts on the whole chip).
|
||||
*/
|
||||
private reconcile(range: EditRange): void {
|
||||
@@ -338,7 +338,7 @@ export class InputMachine {
|
||||
/**
|
||||
* Owner-resolution style bits: exactly the listed occurrences render
|
||||
* invalid. Not a transaction — the draft, revision, and undo log are
|
||||
* untouched (design §9.1: invalidation never deletes or rewrites chips).
|
||||
* untouched (invalidation never deletes or rewrites chips).
|
||||
*/
|
||||
private onSetInvalid(invalidIds: readonly number[]): InputEffect[] {
|
||||
const ids = new Set(invalidIds)
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { QueuedMessage } from '../input/contract.ts'
|
||||
|
||||
/**
|
||||
* Project a session's transient inbox rows as a bare observable (subscribe/getSnapshot).
|
||||
* The wiring layer (T5) overlays this onto InputState.queue; the runtime
|
||||
* The wiring layer overlays this onto InputState.queue; the runtime
|
||||
* QueuedMessage and the input-contract QueuedMessage are structurally
|
||||
* identical.
|
||||
* @param session - the resident session face.
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface IConversation {
|
||||
|
||||
/** Scope-addressed conversation service (root singleton, provided as `conversation`). */
|
||||
export class ConversationService extends Service implements IConversation {
|
||||
/** The per-session input machine registry (InputService face, design §5.2). */
|
||||
/** The per-session input machine registry (InputService face). */
|
||||
readonly input: InputService
|
||||
/** The per-session composer-block registry. */
|
||||
readonly blocks: ComposerBlocks
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
// buttons must be reachable no matter how long the command is.
|
||||
// One-shot: the buttons disable
|
||||
// after a click and the panel leaves (the InputBar returns) on the broadcast
|
||||
// resolved frame. The draft's "Always allow this type" is deferred with
|
||||
// grant storage.
|
||||
// resolved frame.
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** Composer context-occupancy meter: a ring beside the send button fed by the
|
||||
* `contextPressure` projection, with a click-open panel of the heuristic
|
||||
* `contextBreakdown` composition (system prompt, tools, conversation).
|
||||
* Renders nothing until a provider reports both pressure and a route capacity
|
||||
* (same gate as the stats row used). */
|
||||
* Renders nothing until a provider reports both pressure and a route
|
||||
* capacity. */
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { UseProjection } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -81,7 +81,6 @@ export function ConversationRoot({
|
||||
const zone: InputZone | undefined =
|
||||
session === undefined || inputState === undefined ? undefined : { session, input: inputState }
|
||||
|
||||
// Flow optimization — worth a close PR review for code/boundary issues.
|
||||
// The chip is a selector; label resolution walks the flow top-down:
|
||||
// 1. a just-picked workspace (pending) → its title;
|
||||
// 2. cold start, no session yet → placeholder ("Choose workspace");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Details third column, minimal P-I fill: header (name + close) over a
|
||||
/* Details third column, minimal fill: header (name + close) over a
|
||||
scrolling body of Input/Output code sections. Panel width/squeeze belongs
|
||||
to layout; this fills whatever the column gives. */
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// DetailsPanel, P-I minimal form: close button + the selected call's args and
|
||||
// DetailsPanel: close button + the selected call's args and
|
||||
// result — args as JSON, the result raw except for a terminal-card call, whose
|
||||
// Output section is the command's terminal card. The three-段 Switch /
|
||||
// Prev-Next stepping / See-in-trajectory are deferred (ledger). Reads the
|
||||
// Output section is the command's terminal card. Reads the
|
||||
// selection from the shared chat
|
||||
// store (conversation writes, this panel reads — the cross-registration
|
||||
// share the store seat exists for) and derives the call material from the
|
||||
|
||||
@@ -225,9 +225,9 @@
|
||||
so by construction now that all three sit INSIDE .scroll — a scrollbar
|
||||
that consumes layout space narrows the scrollport, which is their shared
|
||||
containing block, so it costs all three the same width on every engine.
|
||||
Scrolling the textarea itself is what used to break this, and no property
|
||||
fixed it: WebKit reserved gutter space for the overflow-y:auto textarea
|
||||
and not for the overflow:hidden layers beside it, leaving them 8px apart
|
||||
A textarea that scrolls itself would break this, and no property fixes
|
||||
it: WebKit reserves gutter space for an overflow-y:auto textarea and not
|
||||
for the overflow:hidden layers beside it, leaving them 8px apart
|
||||
(768 against 776) — worth 2 to 5 wrapped lines on a long draft. */
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Plain-text reference highlight (decision 21): a pure range mark over the
|
||||
/* Plain-text reference highlight: a pure range mark over the
|
||||
draft's own glyphs — advance untouched, so the two layers cannot drift.
|
||||
Chip family colors; clone keeps rounded ends on soft-wrap fragments. */
|
||||
.textRef {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** The default composer body: the 'conversation.composer.bar' slot entry
|
||||
* (decision 20). Machine state arrives through the standard provide channel
|
||||
/** The default composer body: the 'conversation.composer.bar' slot entry.
|
||||
* Machine state arrives through the standard provide channel
|
||||
* (useInput + inputActions); the keyboard/DOM command face and stop arrive
|
||||
* through this entry's own inject, whose hooks compartment binds
|
||||
* useNotices/useLexicon; layout-phase inputs (variant, placeholder,
|
||||
@@ -87,7 +87,7 @@ export function InputBar({
|
||||
// but its independent Stop below stays available while it runs.
|
||||
const continuable = subagent?.address.mode === 'continuable'
|
||||
const parentOffline = continuable && !subagent.parentAvailable
|
||||
// Queue cut 1: running input stays free; locked = session removed, the
|
||||
// Running input stays free; locked = session removed, the
|
||||
// inert no-workspace state, the machine faces absent (no session), or a
|
||||
// parent-offline continuable child. An owner block also disables input;
|
||||
// adjudicating and submitting render read-only so the draft stays visible.
|
||||
@@ -385,7 +385,7 @@ export function InputBar({
|
||||
const backdrop: ReactNode[] = []
|
||||
{
|
||||
// Segment boundaries: the token range end, every chip offset, and every
|
||||
// text-ref range (decision 21) — merged in draft order (the sources never
|
||||
// text-ref range — merged in draft order (the sources never
|
||||
// overlap: chips sit on placeholders, text-refs on plain tokens, the
|
||||
// claim token only leads).
|
||||
let cursor = 0
|
||||
@@ -430,7 +430,7 @@ export function InputBar({
|
||||
)
|
||||
cursor = chip.offset + 1 // the placeholder char the chip stands for
|
||||
} else {
|
||||
// Plain-range highlight (decision 21): the glyphs stay the
|
||||
// Plain-range highlight: the glyphs stay the
|
||||
// textarea's (advance untouched); the mark paints the chip look.
|
||||
backdrop.push(
|
||||
<mark key={`ref-${b.ref.start}`} className={css.textRef} data-decoration="text-ref">
|
||||
@@ -529,7 +529,6 @@ export function InputBar({
|
||||
{rightItems}
|
||||
{renderSlot('conversation.input.model', { locked: modelSeatLocked })}
|
||||
<ContextMeter useProjection={useProjection} t={t} />
|
||||
{/* {machineBusy && <span className={css.pending} data-input-pending aria-label="处理中" />} */}
|
||||
{interruptible && (
|
||||
<Tooltip label={t('input.stop')} side="top" delayMs={500}>
|
||||
<button
|
||||
|
||||
@@ -226,10 +226,10 @@ describe('Enter semantics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('running and lock semantics (queue cut 1)', () => {
|
||||
describe('running and lock semantics', () => {
|
||||
it('running keeps the input free (typing + Enter queue) while the primary turns stop', () => {
|
||||
const { textarea, button, stop, sink } = bench({ running: true, draft: '排队消息' })
|
||||
expect(textarea.disabled).toBe(false) // running no longer locks
|
||||
expect(textarea.disabled).toBe(false)
|
||||
fireEvent.change(textarea, { target: { value: '排队消息2' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2', 'queue')
|
||||
@@ -429,8 +429,8 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
// scrollport element holds both layers.
|
||||
expect(scroll.contains(textarea)).toBe(true)
|
||||
expect(scroll.contains(backdrop)).toBe(true)
|
||||
// The glyph layer carries the draft and nothing else: with one scrollport
|
||||
// it no longer pads its own height to match a second box's scroll extent.
|
||||
// The glyph layer carries the draft and nothing else — no height padding
|
||||
// to a second box's scroll extent.
|
||||
expect(backdrop.textContent).toBe('line\n'.repeat(40))
|
||||
})
|
||||
|
||||
@@ -659,7 +659,7 @@ describe('decorations', () => {
|
||||
expect(shell.snapshot.draft).toBe('参考 \uFFFC 内容')
|
||||
})
|
||||
|
||||
it('a lexicon-matched plain token renders the text-ref mark (decision 21)', () => {
|
||||
it('a lexicon-matched plain token renders the text-ref mark', () => {
|
||||
const lexicon = new Map<'/' | '@', readonly string[]>([['/', ['fixture-demo']]])
|
||||
const { view, shell } = bench({ lexicon })
|
||||
act(() => { shell.setDraft('use /fixture-demo now') })
|
||||
@@ -671,7 +671,7 @@ describe('decorations', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertText (decision 21 scoped event body)', () => {
|
||||
describe('insertText (scoped event body)', () => {
|
||||
it('splices plain text over the span and reports success as true', () => {
|
||||
const { shell } = bench({ draft: '/fix' })
|
||||
const ok = shell.insertText('/fixture-demo ', { start: 0, end: 4, draftRev: shell.snapshot.draftRev })
|
||||
@@ -721,7 +721,7 @@ describe('strips and variants', () => {
|
||||
})
|
||||
|
||||
describe('command launcher chrome and control seats', () => {
|
||||
it('renders the command launcher; the Access chip is absent without the permissions projection; the control seats render EMPTY without entries (B ruling)', () => {
|
||||
it('renders the command launcher; the Access chip is absent without the permissions projection; the control seats render EMPTY without entries', () => {
|
||||
const { view, slotCalls } = bench()
|
||||
expect(view.getByLabelText('命令')).toBeTruthy()
|
||||
// Capability absent (no projection value): the chip renders nothing.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* InputMachine unit account (design §9.1, eng. plan §3.9-3.12): the submit
|
||||
* plane carried over from the InputCore era (adjudication, span CAS, drift
|
||||
* InputMachine unit account: the submit
|
||||
* plane (adjudication, span CAS, drift
|
||||
* guard, anti-backwash), plus the occurrence table (shift / whole-chip
|
||||
* deletion / same-name independence), the self-managed undo log (typing
|
||||
* coalescing, paste two-stage undo, redo chain), consume-token guards, the
|
||||
@@ -625,7 +625,7 @@ describe('input-machine: projectClipboard', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('decorations: scanTextRefs (decision 21)', () => {
|
||||
describe('decorations: scanTextRefs', () => {
|
||||
const LEX: ReadonlyMap<'/' | '@', readonly string[]> = new Map([
|
||||
['/', ['commit-helper', 'fixture-demo']],
|
||||
['@', ['worker-1']],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Impact-matrix projection tests (design §5.2 影响矩阵, row by row): what each
|
||||
* Impact-matrix projection tests (row by row): what each
|
||||
* phase projects onto the InputBar — enter routing, visuals (token color /
|
||||
* hint / pending), edit freedom, and the published currency's claim seat.
|
||||
* React over jsdom per the client testing discipline; the machine is real.
|
||||
@@ -184,7 +184,7 @@ describe('matrix row: locked (session disabled)', () => {
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
|
||||
it('running does NOT lock (queue cut 1): typing and enter-queue stay live', () => {
|
||||
it('running does NOT lock: typing and enter-queue stay live', () => {
|
||||
const { textarea, sink } = bench({ running: true })
|
||||
expect((textarea).disabled).toBe(false)
|
||||
fireEvent.change(textarea, { target: { value: '排队' } })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Scenario-chain integration (design §8 A/C/D/H/I): the real per-session
|
||||
* Scenario-chain integration (scenarios A/C/D/H/I): the real per-session
|
||||
* SlashController pipeline over a real session scope (SessionsService over
|
||||
* a listed host session) + a command source implementing the decision
|
||||
* table's relevant cells + the real SessionInput machine (scoped-event
|
||||
@@ -34,7 +34,7 @@ interface FakeCommand {
|
||||
input?: { hint: string }
|
||||
}
|
||||
|
||||
/** T6 decision-table source over an in-memory directory (menu/space/enter columns for leadingInput + execute). */
|
||||
/** Decision-table source over an in-memory directory (menu/space/enter columns for leadingInput + execute). */
|
||||
function commandSource(commands: FakeCommand[], execute: (line: string) => Promise<SubmitOutcome>) {
|
||||
const resolve = (name: string): FakeCommand | undefined => commands.find(c => c.name === name)
|
||||
const leadingClaim = (desc: FakeCommand): CommandClaim => ({
|
||||
|
||||
@@ -86,8 +86,8 @@ describe('TodoPanel', () => {
|
||||
it('marks every parallel active item, and counts them all in the header', () => {
|
||||
render(<TodoPanel todos={PARALLEL} t={t} />)
|
||||
fireEvent.click(screen.getByRole('button', { expanded: false }))
|
||||
// The old unconditional cap made this list unreachable: three items carry
|
||||
// the in-progress glyph at once, and the header counts all three.
|
||||
// An unconditional in-progress cap would make this list unreachable: three
|
||||
// items carry the in-progress glyph at once, and the header counts all three.
|
||||
const statuses = screen.getAllByRole('listitem').map(li => li.getAttribute('data-status'))
|
||||
expect(statuses.filter(s => s === 'in_progress')).toHaveLength(3)
|
||||
expect(screen.getByText('跑后台构建')).toBeTruthy()
|
||||
|
||||
Reference in New Issue
Block a user