@zhushanwen/pi-ask-user 7.0.15 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ARCHITECTURE.md CHANGED
@@ -69,14 +69,14 @@ Encoding is **one-way** — the old `parseAnswerParts` text reverse-parsing was
69
69
 
70
70
  | Step | Check | Returns | Why this order |
71
71
  |------|-------|---------|----------------|
72
- | 1 | `validateInput(questions)` | `isError:true` + fix hint | Pure function, no side effects — cheapest gate. Reject before any UI/state work. |
73
- | 2 | `!ctx.hasUI` (headless) | `isError:true` + **`setActiveTools` removes ask_user** | Must run before the agent can retry. Physically removing the tool breaks a function-calling retry loop that plain `isError` cannot. |
72
+ | 1 | `validateInput(questions)` | `throw` (Pi marks `isError:true`, empty `details`) + fix hint | Pure function, no side effects — cheapest gate. Reject before any UI/state work. |
73
+ | 2 | `ctx.mode !== "tui" && ctx.mode !== "rpc"` (headless) | `throw` + **`setActiveTools` removes ask_user** | Must run before the agent can retry. Physically removing the tool breaks a function-calling retry loop that a thrown error cannot. |
74
74
  | 3 | `signal?.aborted` | `cancelled:true` | O(1) short-circuit before the expensive blocking `ctx.ui.custom` call. |
75
- | 4 | `try { ctx.ui.custom(...) } catch` | `isError:true` + `{ error }` | `ctx.ui.custom` is the only call that runs user interaction / editor construction / theme reads — the largest blast radius, so it is the only thing wrapped. |
75
+ | 4 | `try { ctx.ui.custom(...) } catch` | `throw` (Pi marks `isError:true`, empty `details`) | `ctx.ui.custom` is the only call that runs user interaction / editor construction / theme reads — the largest blast radius, so it is the only thing wrapped. |
76
76
  | 5 | `result === null \|\| result.cancelled` | `cancelled:true` | Component resolved to cancel. |
77
77
  | 6 | normal | `{ answers }` | Compose the summary. |
78
78
 
79
- **The order is load-bearing**: swapping 1↔2 wastes a UI check on invalid params; swapping 2↔3 lets an aborted agent enter a blocking UI; moving 4's try/catch wider catches nothing extra. The headless branch's `setActiveTools` is the key insight — returning `isError` alone does not stop an LLM from calling the tool again in the same turn, so the tool is removed from the session's active set and the error text says "do not retry".
79
+ **The order is load-bearing**: swapping 1↔2 wastes a UI check on invalid params; swapping 2↔3 lets an aborted agent enter a blocking UI; moving 4's try/catch wider catches nothing extra. Business outcomes (answers / cancellation) are returned as normal results — only validation failures and unexpected exceptions `throw` (Pi converts a throw into an `isError:true` tool result with empty `details`). The headless branch's `setActiveTools` is the key insight — even an `isError` tool result does not stop an LLM from calling the tool again in the same turn, so the tool is removed from the session's active set and the error text says "do not retry".
80
80
 
81
81
  ## `QuestionState` machine
82
82
 
@@ -122,7 +122,7 @@ If you add a new path that changes the answer set, audit both directions of this
122
122
 
123
123
  ### `autoConfirmIfAnswered` trigger
124
124
 
125
- Called only from `gotoTab()` — when the user navigates between tabs via Tab/Shift+Tab without pressing Enter. It promotes an implicitly-answered tab (toggled but not confirmed) to `confirmed`. A Tab navigation intent should not force a confirm prompt — only the Enter path confirms via `afterConfirm`.
125
+ Called only from `gotoTab()` — when the user navigates between tabs via `←/→` (Tab is not a tab-navigation key: on the Submit tab it toggles Submit/Cancel focus, and `Shift+Tab` is deliberately unused because Pi's global `app.thinking.cycle` intercepts it). It promotes an implicitly-answered tab (toggled but not confirmed) to `confirmed`. A navigation intent should not force a confirm prompt — only the Enter path confirms via `afterConfirm`.
126
126
 
127
127
  ## Race guards
128
128
 
@@ -132,7 +132,7 @@ Three independent guards protect against three different races. They are dimensi
132
132
  |-------|------|----------|----------|-----------|
133
133
  | `_resolved` | `boolean` field | `component.ts` | **Double `done()`**: user already submitted/cancelled, then a signal-abort listener or a late keypress fires `done` again → Pi receives two resolves. | `submit()`/`cancel()` set `_resolved = true` before `done(...)`; **both `handleInput` and `cancel()` itself early-return if already set** — so a signal-abort firing after resolution (the listener calls `comp.cancel()`) is a no-op (see `execute` step 4). |
134
134
  | `pendingCancel` | `boolean` field | `component.ts` | **Accidental cancel losing answers**: Esc on the first question (or single question) cancelling outright would discard everything. | Two-step confirm: first Esc sets `pendingCancel = true` and shows an overlay; a second Esc truly cancels; any other key exits the overlay and keeps the form. The Submit-tab Cancel button bypasses this (already at the terminus). |
135
- | `autoConfirmIfAnswered` | **method** (not a field) | `component.ts` | **Zombie unanswered tab**: in multi-question mode, toggling an option then Tab-ing away leaves a tab "answered but not confirmed", so the Submit gate (`allConfirmed()`) stays false and the user cannot tell why Submit is blocked. | `gotoTab()` calls it before switching; if the current state has an answer but `!confirmed`, it sets `confirmed = true`. |
135
+ | `autoConfirmIfAnswered` | **method** (not a field) | `component.ts` | **Zombie unanswered tab**: in multi-question mode, toggling an option then navigating away (`←/→`) leaves a tab "answered but not confirmed", so the Submit gate (`allConfirmed()`) stays false and the user cannot tell why Submit is blocked. | `gotoTab()` calls it before switching; if the current state has an answer but `!confirmed`, it sets `confirmed = true`. |
136
136
 
137
137
  ## Three-layer rendering
138
138
 
@@ -164,7 +164,7 @@ Constants: `SPLIT_PANE_MIN_WIDTH = 84`, `SPLIT_PANE_LEFT_MIN = 32`, `SPLIT_PANE_
164
164
 
165
165
  ## Spec cross-reference
166
166
 
167
- Design spec: `.xyz-harness/2026-06-15-ask-user/spec.md` (FR = functional requirement, AC = acceptance criterion). Implementation anchors:
167
+ The original spec files (`.xyz-harness/2026-06-15-ask-user/`) are no longer in the repo — this table is self-contained (FR = functional requirement from the original spec). Implementation anchors:
168
168
 
169
169
  | Spec | Implemented in |
170
170
  |------|----------------|
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zhushanwen/pi-ask-user
2
2
 
3
- Inline adaptive `ask_user` tool for the Pi coding agent. Resolves ambiguity the agent cannot resolve itself — a single question (no tab bar) or 1-4 questions (tabbed view + submit), with split-pane option preview on wide terminals, an inline free-text editor, and optional comments.
3
+ Inline adaptive `ask_user` tool for the Pi coding agent. Resolves ambiguity the agent cannot resolve itself — a single question (no tab bar) or 1-4 questions (tabbed view + submit), with split-pane option preview on wide terminals and an inline free-text editor. There is no comment feature: the free-text "Other" editor is the only free-form input.
4
4
 
5
5
  The tool's primary caller is the LLM. This README covers both **how an agent should use it** (top sections) and **how a maintainer reads the code** (File structure → Design notes).
6
6
 
@@ -46,7 +46,6 @@ If you recommend an option, prefix its label with `(Recommended)` and list it fi
46
46
  description?: string; // short rationale shown under the label and in the preview pane
47
47
  }>;
48
48
  multiSelect?: boolean; // default false; true only when several options can validly apply
49
- allowComment?: boolean; // default false; lets the user append a free-text note after selecting
50
49
  }>
51
50
  } // questions: 1-4 entries
52
51
  ```
@@ -61,7 +60,7 @@ If you recommend an option, prefix its label with `(Recommended)` and list it fi
61
60
  | `header` | ≤12 chars; required when `questions.length > 1` | `validate.ts` (length + non-empty) |
62
61
  | `options[].label` | non-empty, unique within the question | `validate.ts` |
63
62
 
64
- Validation errors are returned as `isError: true` with a message that names the violation and tells you how to fix it — correct the parameters and retry.
63
+ Validation failures make the tool `throw` with a message that names the violation and tells you how to fix it — correct the parameters and retry. (Pi converts a thrown error into an `isError: true` tool result; business outcomes like answers and cancellation are returned normally.)
65
64
 
66
65
  ## Result format
67
66
 
@@ -69,8 +68,7 @@ On success the tool returns the answers joined as `"question" = "answer"` lines.
69
68
 
70
69
  - **Single-select**: the chosen `label`.
71
70
  - **Multi-select**: selected labels joined with `, ` (e.g. `A, B`).
72
- - **Free-text (Other)**: whatever the user typed.
73
- - **Comment**: if `allowComment` was set, the user's note is appended after ` — ` (e.g. `Postgres — needs TLS`).
71
+ - **Free-text (Other)**: whatever the user typed (appended to the selection with `, ` when both exist).
74
72
 
75
73
  A question with no answer reports as `(no answer)`.
76
74
 
@@ -78,11 +76,13 @@ A question with no answer reports as `(no answer)`.
78
76
 
79
77
  | Situation | Return | What the agent should do |
80
78
  |-----------|--------|--------------------------|
81
- | Parameter validation fails | `isError: true` + fix hint | Correct params and retry |
82
- | No interactive UI (headless) | `isError: true`, tool **disabled for the session** | Proceed with a defensible decision stated in text, or wait for the user — **do not retry** |
79
+ | Parameter validation fails | `throw` (Pi shows it as `isError: true`) + fix hint | Correct params and retry |
80
+ | No interactive UI (headless) | `throw`, tool **disabled for the session** | Proceed with a defensible decision stated in text, or wait for the user — **do not retry** |
83
81
  | Agent aborted (goal cancelled / context compacted) | `cancelled: true` | The text identifies it as an agent abort, not a user cancel. Do not assume an answer; do not retry ask_user — propagate the abort, or wait for new instructions if the decision is still required. |
84
82
  | User cancels (Esc → confirm, or Cancel button) | `cancelled: true` | Wait for new instructions, or re-ask with refined options if the decision is still required |
85
- | Unexpected error | `isError: true` + `{ error }` | Retry once with corrected parameters, or proceed with a defensible decision |
83
+ | Unexpected error during interaction | `throw` (Pi shows it as `isError: true`) | Retry once with corrected parameters, or proceed with a defensible decision |
84
+
85
+ Business outcomes (answers / cancellation) are returned as normal results; only validation failures and unexpected exceptions `throw` — Pi marks a thrown error `isError: true` with empty `details`.
86
86
 
87
87
  The headless branch physically removes the tool from the session (`setActiveTools`) — this is deliberate, so a function-calling loop cannot keep retrying `ask_user` in a non-interactive context.
88
88
 
@@ -90,11 +90,10 @@ The headless branch physically removes the tool from the session (`setActiveTool
90
90
 
91
91
  - **Adaptive layout**: single question → no tab bar; 1-4 questions → tabbed view + Submit tab.
92
92
  - **Split-pane preview** (≥84 cols): option list left, selected option detail right. The right pane is **plain-text** option detail (label + description), not a Markdown renderer.
93
- - **Inline free-text editor**: select "Other" → Enter → type a custom answer. Multi-line aware, soft-wrapped.
94
- - **Optional comments**: `allowComment: true` → after selecting, the user may append a short note.
93
+ - **Inline free-text editor**: select "Other" → Enter → type a custom answer. Multi-line aware, soft-wrapped. No comment mode — this editor is the only free-form input.
95
94
  - **Multi-select**: `multiSelect: true` → toggle checkboxes with Space, Enter to confirm.
96
95
  - **Esc confirm-to-cancel**: Esc on the first question opens a confirm overlay (a second Esc cancels; any other key stays).
97
- - **Headless-safe**: disables the tool and returns `isError` when no UI is available.
96
+ - **Headless-safe**: disables the tool and throws when no UI is available.
98
97
 
99
98
  ## File structure
100
99
 
@@ -111,7 +110,11 @@ extensions/universal/ask-user/
111
110
  ├── validate.ts # pure parameter validation; error messages aimed at LLM fixability
112
111
  ├── component.ts # AskUserComponent: state machine, input routing, race guards
113
112
  ├── question-view.ts # pure render: option list, split-pane, inline editor
114
- └── submit-view.ts # pure render: Submit tab, answer summary, buildResult
113
+ ├── submit-view.ts # pure render: Submit tab, answer summary, buildResult
114
+ ├── answer-codec.ts # pure encode: AnswerValue → proto answers entries (protocol boundary)
115
+ ├── channel-handler.ts # subagent channel passthru: RPC forward / TUI re-render → proto answers
116
+ ├── channel-registry-register.ts # globalThis Symbol slot handshake (handler registration)
117
+ └── editor-ops.ts # pure editor ops: insert/delete/cursor/paste (UTF-16 surrogate aware)
115
118
  ```
116
119
 
117
120
  `types.ts` is intentionally the shared dependency leaf — it holds `QuestionState`/`ThemeLike` (not `component.ts`) so the two pure-render views depend only on the leaf, breaking a would-be `component → view → component` cycle. See ARCHITECTURE.md for the full graph.
@@ -130,18 +133,12 @@ All three are consistent and point the same direction. If you tune behavior, edi
130
133
 
131
134
  - **Why inline, not overlay** (`execute` → `ctx.ui.custom` without `options`): the question belongs in the conversation flow, not a modal that obscures context.
132
135
  - **Why `Other` is auto-appended, not in the schema**: free-text input is the user's escape hatch and must not be something the LLM can omit or mislabel. Keeping it out of `options` guarantees it is always present and always last.
133
- - **Why `←/→` does not switch tabs**: left/right is reserved for the Submit tab's Submit/Cancel focus toggle, so it does not yank focus away while navigating an option list.
136
+ - **Why `←/→` switches tabs but `Tab` does not**: arrow keys move between question tabs (and wrap on the Submit tab), so tab navigation never conflicts with the option-list cursor (`↑/↓`) or text editing. On the Submit tab, `Tab` alone toggles focus between Submit and Cancel a deliberate single-key bidirectional toggle; `Shift+Tab` is intentionally unused because Pi's global `app.thinking.cycle` intercepts it.
134
137
  - **Why validation messages are verbose**: every message names the violation and gives a fix path, because the reader is an LLM that will retry.
135
138
 
136
139
  ## Spec reference
137
140
 
138
- The original design spec, acceptance criteria (FR-x / AC-x), and E2E test cases live under `.xyz-harness/2026-06-15-ask-user/`:
139
-
140
- - `spec.md` — requirements + functional/acceptance criteria
141
- - `e2e-test-cases.md` — end-to-end scenarios
142
- - `clarification.md` / `plan.md` — design rationale
143
-
144
- Cross-references between these and the implementation are in ARCHITECTURE.md.
141
+ The original design spec (requirements, functional requirements FR-x, acceptance criteria AC-x, E2E test cases) predates this repository — the original spec files are no longer in the repo. The FR cross-reference table in ARCHITECTURE.md is self-contained: each entry names the behavior and where it is implemented in this codebase.
145
142
 
146
143
  ## License
147
144
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-ask-user",
3
- "version": "7.0.15",
4
- "description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview, inline editor and optional comments.",
3
+ "version": "7.1.0",
4
+ "description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview and an inline free-text editor.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "xyz-agent": {
@@ -29,11 +29,11 @@
29
29
  "ARCHITECTURE.md"
30
30
  ],
31
31
  "dependencies": {
32
- "@xyz-agent/extension-protocol": "0.7.0",
33
- "@zhushanwen/pi-extension-logger": "0.3.0"
32
+ "@xyz-agent/extension-protocol": "0.8.0",
33
+ "@zhushanwen/pi-extension-logger": "0.4.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@earendil-works/pi-tui": "^0.84.2",
36
+ "@earendil-works/pi-tui": "^0.84.4",
37
37
  "@types/node": "^24.0.0",
38
38
  "@vitest/coverage-v8": "^4.1.9",
39
39
  "fast-check": "^4.9.0",
@@ -41,8 +41,8 @@
41
41
  "vitest": "^4.1.8"
42
42
  },
43
43
  "peerDependencies": {
44
- "@earendil-works/pi-coding-agent": "^0.84.1",
45
- "@earendil-works/pi-tui": "^0.84.1",
44
+ "@earendil-works/pi-coding-agent": "^0.84.4",
45
+ "@earendil-works/pi-tui": "^0.84.4",
46
46
  "typebox": "*"
47
47
  },
48
48
  "peerDependenciesMeta": {
@@ -1,7 +1,7 @@
1
1
  // src/__tests__/e2e.test.ts
2
2
  // E2E test cases for ask_user. Drives tool.execute() → real AskUserComponent
3
3
  // → simulated keypresses → asserts on final execute() result contract.
4
- // Spec: .xyz-harness/2026-06-15-ask-user/e2e-test-cases.md
4
+ // (原始 spec 文件已不在仓内——用例自包含,编号 E2E-x 沿用原始 spec 命名)
5
5
 
6
6
  import { describe, expect, it } from "vitest";
7
7
 
@@ -453,17 +453,8 @@ describe("renderCall / renderResult (FR-9)", () => {
453
453
  expect(renderText(node)).toContain("Cancelled");
454
454
  });
455
455
 
456
- it("I-19: renderResult error shows <error>", () => {
457
- const tool = getTool();
458
- const node = tool.renderResult(
459
- { details: { error: "something broke" } },
460
- { expanded: false },
461
- stubTheme,
462
- ) as unknown as { render(width: number): string[] };
463
- const text = renderText(node);
464
- expect(text).toContain("✗");
465
- expect(text).toContain("something broke");
466
- });
456
+ // (I-19 已随 ErrorDetails 死类型删除:错误路径全部 throw(W4),pi isError
457
+ // details 为空对象,renderResult 不存在 {error} details 分支可渲染)
467
458
 
468
459
  // S-3: options.expanded 展开 —— 显示全部选项 + ●/○ 选中标记(spec FR-9)
469
460
  it("I-20: renderResult expanded shows all options with ●/○ marks", () => {
@@ -13,7 +13,8 @@
13
13
  // (key=header/question,单选=string,多选=JSON 数组,Other→__other),让子进程 decode 一致。
14
14
  //
15
15
  // handler 收到的 req.channelPayload = {questions: AskUserQuestion[], allowCancel}(proto 格式,
16
- // 由子进程 askUserInteract 编码、subagent-workflow parseChannel 解析 options[0] JSON 得到)。
16
+ // 由子进程 askUserInteract 编码、packages/subagent-core parseChannel(execution/ui-channels.ts)
17
+ // 解析 options[0] JSON 得到)。
17
18
 
18
19
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
19
20
  import {
@@ -27,20 +28,20 @@ import { encodeAnswer } from "./answer-codec";
27
28
  import { type AnswerValue, type Option, type Question, type Result, type ThemeLike } from "./types";
28
29
 
29
30
  /**
30
- * channel handler 签名——与 subagent-workflow 的 UiChannelRegistry.ChannelHandler 一致
31
- *((req: unknown) => Promise<unknown>)。本文件不静态 import subagent-workflow(它是可选
32
- * peerDep,未安装时静态 import 会导致整个 ask-user 加载失败);注册时通过动态 import 拿
33
- * registry,handler 签名用本地等价类型,运行时结构兼容。
31
+ * channel handler 签名——与 packages/subagent-core 的 UiChannelRegistry.ChannelHandler 一致
32
+ *(execution/ui-channels.ts 定义,(req: unknown) => Promise<unknown>)。本文件不静态 import
33
+ * packages/subagent-core(host 侧包,非本包依赖——两侧扩展经 globalThis slot 握手协作,
34
+ * 见 index.ts 工厂注释);handler 签名用本地等价类型,运行时结构兼容。
34
35
  */
35
36
  export type ChannelHandler = (req: unknown) => Promise<unknown>;
36
37
 
37
- /** handler 返回给 subagent-workflow 的 UiResponse 形状(dialog-queue.ts 定义)。
38
+ /** handler 返回给 packages/subagent-core 的 UiResponse 形状(execution/dialog-queue.ts 定义)。
38
39
  * - {value}: select 的回传值(子进程 JSON.parse(value) 得 answers)
39
40
  * - {cancelled}: 用户取消 / 子进程 close / handler 抛错 */
40
41
  type ChannelResponse = { value: string } | { cancelled: true };
41
42
 
42
43
  /** handler 收到的 req 形状收窄(ChannelHandler 签名是 unknown,按形状 as 收窄)。
43
- * channelPayload 由 subagent-workflow parseChannel 填充。 */
44
+ * channelPayload 由 packages/subagent-core parseChannel 填充。 */
44
45
  interface ChannelRequest {
45
46
  channelPayload?: { questions?: AskUserQuestion[]; allowCancel?: boolean };
46
47
  }
@@ -138,7 +139,7 @@ async function runTuiProtoInteraction(
138
139
  */
139
140
  export function createAskUserChannelHandler(ctx: ExtensionContext): ChannelHandler {
140
141
  return async (req: unknown): Promise<unknown> => {
141
- // req 正常是 subagent-workflow 构造的 UiRequest 对象;防御性收窄 null/undefined/
142
+ // req 正常是 packages/subagent-core 构造的 UiRequest 对象;防御性收窄 null/undefined/
142
143
  // 非 object(handler 抛错会被 dialog-queue 兜底为 {cancelled:true},但这里直接返回更干净)
143
144
  if (req === null || typeof req !== "object") {
144
145
  return { cancelled: true } satisfies ChannelResponse;
@@ -3,14 +3,16 @@
3
3
  // ask-user 侧的 channel handler 握手注册纯函数。
4
4
  //
5
5
  // 设计动机(PR #85 #M4 修复):原实现 ask-user 先 session_start 时会自建简化 Map-based
6
- // registry 占据 canonical 槽位,劫持 subagent-workflow 后续 getOrCreateChannelRegistry 拿到的
7
- // 实例(subagent-workflow 的 createUiChannelRegistry 才是 canonical——它带排队、dialog 队列
8
- // 等完整能力)。修复后的握手协议改为「带 version 的 slot」:
9
- // - subagent-workflow session_start 时往 slot 写 {version, registry, pending:[]}
6
+ // registry 占据 canonical 槽位,劫持后续 getOrCreateChannelRegistry 拿到的实例
7
+ // (packages/subagent-core 的 createUiChannelRegistry(execution/ui-channels.ts)才是
8
+ // canonical——它带排队、dialog 队列等完整能力)。修复后的握手协议改为「带 version 的 slot」:
9
+ // - 承载 subagent-core 的 subagent-workflow 扩展 session_start 时往 slot 写
10
+ // {version, registry, pending:[]}(registry 由 subagent-core 创建)
10
11
  // - ask-user session_start 时只读 slot,registry 就绪则调 registry.register,未就绪则 push pending
11
12
  // - ask-user **永不**创建 registry 实例,**永不**写 slot.registry
12
13
  //
13
- // 这是 ask-user 侧的注册入口;canonical registry 由 subagent-workflow 创建。本模块永不创建
14
+ // 这是 ask-user 侧的注册入口;canonical registry 由 packages/subagent-core 创建(经
15
+ // channel-registry-access.ts 的 getOrCreateChannelRegistry + flush)。本模块永不创建
14
16
  // registry 实例——仅往 slot 写 pending 或调 slot.registry.register。
15
17
 
16
18
  import type { ChannelHandler } from "./channel-handler";
@@ -21,7 +23,7 @@ const logger = getLogger("ask-user");
21
23
  /**
22
24
  * 进程级 channel registry 握手的 globalThis key(Symbol.for 跨模块共享)。
23
25
  *
24
- * ⚠️ 必须与 extensions/universal/subagent-workflow/src/execution/channel-registry-access.ts 的字面量
26
+ * ⚠️ 必须与 packages/subagent-core/src/execution/channel-registry-access.ts 的字面量
25
27
  * 完全一致——两边用同一字符串确保拿到同一 slot 实例。改名必须两侧同步。
26
28
  */
27
29
  export const CHANNEL_HANDSHAKE_KEY = Symbol.for(
@@ -35,9 +37,9 @@ const HANDSHAKE_VERSION = 1;
35
37
  const ASK_USER_CHANNEL = "ask_user";
36
38
 
37
39
  /**
38
- * channel registry 的本地等价接口(与 subagent-workflow UiChannelRegistry 形状一致)。
39
- * 本模块不静态 import subagent-workflow(它是可选 peerDep,未安装时静态 import 会致整个
40
- * ask-user 加载失败);运行时结构兼容即可。
40
+ * channel registry 的本地等价接口(与 packages/subagent-core UiChannelRegistry 形状一致,
41
+ * execution/ui-channels.ts 定义)。本模块不静态 import packages/subagent-core(host 侧包,
42
+ * 非本包依赖——两侧经 globalThis slot 握手协作);运行时结构兼容即可。
41
43
  */
42
44
  interface ChannelRegistry {
43
45
  register(channel: string, handler: ChannelHandler): void;
@@ -45,7 +47,7 @@ interface ChannelRegistry {
45
47
  list(): string[];
46
48
  }
47
49
 
48
- /** pending 队列元素:channel + handler。subagent-workflow flush 时遍历调用 registry.register。 */
50
+ /** pending 队列元素:channel + handler。packages/subagent-core flush(channel-registry-access.ts)时遍历调用 registry.register。 */
49
51
  interface PendingEntry {
50
52
  channel: string;
51
53
  handler: ChannelHandler;
@@ -55,9 +57,9 @@ interface PendingEntry {
55
57
  * globalThis slot 的形状。
56
58
  *
57
59
  * - `version`:握手协议版本(运行时校验,不兼容则丢弃重建)
58
- * - `registry`:canonical 实例,**仅 subagent-workflow 创建**;缺失表示 registry 未就绪,
60
+ * - `registry`:canonical 实例,**仅 packages/subagent-core 创建**;缺失表示 registry 未就绪,
59
61
  * ask-user 把 handler 入 pending 队列等待 flush
60
- * - `pending`:未消费的注册请求(registry 就绪后由 subagent-workflow 一次性 flush)
62
+ * - `pending`:未消费的注册请求(registry 就绪后由 packages/subagent-core 一次性 flush)
61
63
  */
62
64
  export interface ChannelRegistryHandshake {
63
65
  version: 1;
@@ -98,7 +100,7 @@ function ensureSlot(): ChannelRegistryHandshake {
98
100
  * 3. slot 存在且 registry 就绪 → 直接调 registry.register("ask_user", handler)
99
101
  *
100
102
  * 多次调用幂等:registry 就绪时 register 同名覆盖;未就绪时 pending.length 增长
101
- * (subagent-workflow flush 时一次性消费所有 pending)。
103
+ * (packages/subagent-core flush 时一次性消费所有 pending)。
102
104
  *
103
105
  * @param handler ask_user channel handler(createAskUserChannelHandler 产出)
104
106
  */
package/src/editor-ops.ts CHANGED
@@ -10,8 +10,6 @@ import { isHighSurrogate, SURROGATE_PAIR_LEN, type QuestionState } from "./types
10
10
 
11
11
  // ── 编辑器纯操作 ──
12
12
 
13
- // ── 编辑器纯操作 ──
14
-
15
13
  /** 在光标处插入文本,光标前移 text.length。 */
16
14
  export function insertAtCursor(state: QuestionState, text: string): void {
17
15
  state.draftText = state.draftText.slice(0, state.cursorIndex) + text + state.draftText.slice(state.cursorIndex);
package/src/index.ts CHANGED
@@ -211,15 +211,16 @@ export default function (pi: ExtensionAPI): void {
211
211
  // 注册 ask_user channel handler:把 subagent 子进程的 ask_user 请求透传到主进程 UI。
212
212
  //
213
213
  // 跨扩展握手协议(PR #85 #M4):通过 globalThis Symbol.for 约定 slot 形状
214
- //(CHANNEL_HANDSHAKE_KEY,与 subagent-workflow/src/execution/channel-registry-access.ts
214
+ //(CHANNEL_HANDSHAKE_KEY,与 packages/subagent-core/src/execution/channel-registry-access.ts
215
215
  // 用同一字符串 key),不依赖 dynamic import npm 包名(两个扩展都通过
216
216
  // ~/.pi/agent/extensions/ symlink 加载,互相之间无法用 npm 包名 import)。
217
217
  //
218
218
  // 握手流程(registerAskUserChannelHandler 内部完成):
219
219
  // 1. 读 slot;不存在或 version 不兼容 → 建 slot(仅 pending,**永不建 registry**)
220
- // 2. slot.registry 就绪(subagent-workflow 先到)→ 直接调 registry.register
221
- // 3. slot.registry 未就绪 → handler 入 pending,等 subagent-workflow flush
222
- // ask-user 永不创建 registry 实例——canonical registry 仅 subagent-workflow 创建。
220
+ // 2. slot.registry 就绪(承载 packages/subagent-core 的 subagent-workflow 扩展先到)→ 直接调 registry.register
221
+ // 3. slot.registry 未就绪 → handler 入 pending,等 subagent-core flush
222
+ // ask-user 永不创建 registry 实例——canonical registry 仅 packages/subagent-core 创建
223
+ //(execution/ui-channels.ts 的 createUiChannelRegistry)。
223
224
  pi.on("session_start", (_event, ctx) => {
224
225
  registerAskUserChannelHandler(createAskUserChannelHandler(ctx));
225
226
  });
@@ -351,10 +352,8 @@ Don't:
351
352
  theme: ThemeLike,
352
353
  ) {
353
354
  const details = result.details;
354
- if (details && "error" in details && details.error) {
355
- return new Text(theme.fg("error", `✗ ${details.error}`), 0, 0);
356
- }
357
- // details 现已排除 ErrorDetails 分支,收窄为 Result | undefined
355
+ // 错误路径已全部改 throw(W4):pi isError:true details 为空对象,
356
+ // 不存在 ErrorDetails 形态——这里 details 要么 undefined 要么 Result
358
357
  const d = details as Result | undefined;
359
358
  if (!d || d.cancelled) {
360
359
  return new Text(theme.fg("warning", "Cancelled"), 0, 0);
@@ -18,6 +18,8 @@ const SPLIT_PANE_LEFT_RATIO = 0.42;
18
18
  const DESCRIPTION_INDENT_MULTI = 10;
19
19
  const DESCRIPTION_INDENT_SINGLE = 8;
20
20
  const PREVIEW_MIN_WIDTH = 10;
21
+ /** 分屏右列(详情预览)最小行数(左列较短时占位,避免预览区域过矮)。 */
22
+ const PREVIEW_MIN_LINES = 8;
21
23
  const QUESTION_TEXT_MARGIN = 2;
22
24
 
23
25
  export interface DisplayOption {
@@ -238,7 +240,7 @@ function buildSplitPane(
238
240
  const leftCtx: RenderContext = { ...ctx, width: split.left };
239
241
  const rightCtx: RenderContext = { ...ctx, width: split.right };
240
242
  const leftLines = buildOptionLines(leftCtx, true);
241
- const rightLines = buildPreviewLines(rightCtx, Math.max(leftLines.length, 8));
243
+ const rightLines = buildPreviewLines(rightCtx, Math.max(leftLines.length, PREVIEW_MIN_LINES));
242
244
  const rowCount = Math.max(leftLines.length, rightLines.length);
243
245
  const sep = t.fg("dim", SPLIT_PANE_SEPARATOR);
244
246
  for (let i = 0; i < rowCount; i++) {
package/src/types.ts CHANGED
@@ -109,13 +109,12 @@ export const ResultSchema = Type.Object({
109
109
 
110
110
  export type Result = Static<typeof ResultSchema>;
111
111
 
112
- /** execute 意外异常时返回的错误 details(区别于 Result.cancelled 的业务取消) */
113
- export interface ErrorDetails {
114
- error: string;
115
- }
116
-
117
- /** execute 返回的 details 联合:正常/取消/校验失败走 Result,意外异常走 ErrorDetails */
118
- export type AskUserDetails = Result | ErrorDetails;
112
+ /**
113
+ * execute 返回的 details 形状:正常/取消/校验失败都复用 Result。
114
+ * 错误路径已全部改为 throw(W4)——pi 对 throw 生成 isError:true 且 details 为空对象,
115
+ * 不存在「错误 details」形态;曾有的 ErrorDetails 死类型随不可达分支一并删除。
116
+ */
117
+ export type AskUserDetails = Result;
119
118
 
120
119
  // ── 跨模块共享的交互状态类型 ─────────────────────────
121
120
  // 放这里(而非 component.ts)是为了让 question-view.ts / submit-view.ts