thincoder 0.12.63 → 0.12.65

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.
@@ -6,12 +6,14 @@
6
6
  *
7
7
  * §17(2026-09-02,AGENT-LOOP.md §9 D-S1..S9):回合尾后台池非空 → 不阻塞等待,
8
8
  * 进入挂起态——挂起空闲输入开放(Enter = 新回合填单槽 + 唤醒)、busy(processing 含
9
- * digest)输入禁用(INPUT-LOCK-ASYNC C'——提交吞——2026-09-09)、settle 事件驱动
9
+ * digest)提交亦入同槽(busy-extend 批 2026-09-22——`TUI-INPUT-BOX.md` §4.1)、settle 事件驱动
10
10
  * auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无待处理输入 → 补发
11
11
  * done 冻结自然退出。状态机行表见 AGENT-LOOP.md §9.2。
12
- * F-UC7(2026-09-19 批,AGENT-LOOP-SUBAGENT.md §6.27.12):第二开轮源 = 未 drain 的上行
12
+ * F-UC7(2026-09-19 批,AGENT-LOOP-UPSTREAM.md §6.27.12):第二开轮源 = 未 drain 的上行
13
13
  * ask(`upstreamWaiting` 谓词——ask 入队即唤醒本驱动);唤醒轮同走 auto-turn(旗标
14
- * `upstreamTurn` 供核域文本选择,提示行走第三档)。
14
+ * `upstreamTurn` 供核域文本选择)。
15
+ * F-UC8(2026-09-21 批,§6.27.12.13):可见提示面**按因两档**(ask 携「谁 + 啥」——
16
+ * 核单点 `upstreamAskLabelVars` / digest)+ 起跑数行与轮尾收尾行同守 `pend0 > 0`。
15
17
  */
16
18
 
17
19
  // 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
@@ -23,16 +25,17 @@ import { logEvent } from "@thincoder/core/log.mjs"
23
25
  import { C } from "./ansi.mjs"
24
26
  // ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
25
27
  import { getAsyncPool, parkAsyncPending, releaseSettledEntry } from "@thincoder/core/agent-tools/async-settle.mjs"
26
- // F-UC7(AGENT-LOOP-SUBAGENT.md §6.27.12——2026-09-19 批):上行 ask 开轮谓词单点(核导出)
27
- import { upstreamWaiting } from "@thincoder/core/agent-tools/parent-channel.mjs"
28
+ // F-UC7(AGENT-LOOP-UPSTREAM.md §6.27.12——2026-09-19 批):上行 ask 开轮谓词单点(核导出)
29
+ // F-UC8(§6.27.12.13 ②——2026-09-21 批):ask 提示行携参单点(同档导出——显示面单源)
30
+ import { upstreamAskLabelVars, upstreamWaiting } from "@thincoder/core/agent-tools/parent-channel.mjs"
28
31
  // X9(2026-09-20 端差·显示面消差批):消化收尾文案单源 = 核 i18n 容器(禁第三份字面)
29
32
  import { t } from "@thincoder/core/i18n.mjs"
30
- // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-SUBAGENT.md §6.20):中止分支「只清已死」收尾单点
33
+ // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-ASYNC-POOL.md §6.20):中止分支「只清已死」收尾单点
31
34
  import { discardAbortedPool, discardAbortedAdvisors } from "@thincoder/core/agent-tools/async-discard.mjs"
32
35
 
33
36
  // INPUT-LOCK-ASYNC(C'——2026-09-09,本档 INPUT-LOCK-ASYNC.md):R15 排队
34
37
  // 用户指令合并(§11.3 D-24c——攒批计划/合并文案/上限常量)整批废弃
35
- // ——busy 提交吞 + pendingInput 单槽化(至多一条待交接——单消息逐发不攒批)。
38
+ // ——pendingInput 单槽化(至多一条待交接——单消息逐发不攒批;busy 提交入槽 = busy-extend 批 2026-09-22 扩面)。
36
39
 
37
40
  /** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
38
41
  * poolN = _asyncSubagents + _asyncAdvisors(§11.2 D-24b advisor 池同面板计数——queued
@@ -77,7 +80,7 @@ export function allPendingEntries(agent) {
77
80
  return [...(agent?._pendingAsyncResults ?? [])]
78
81
  }
79
82
 
80
- // ─── §17 挂起会话(AGENT-LOOP.md §17 D-S2/D-S9 状态机行表)────────────────
83
+ // ─── 挂起会话(AGENT-LOOP-ASYNC-POOL.md §6.8 D-S2/D-S9 状态机行表)────────────────
81
84
 
82
85
  /** 后台池存活判据(D-S2/F5 口径):running/queued 子代理或后台评审,或已 settle 未注入结果
83
86
  * (pending 单容器非空 = D-S3 "未注入"),或 running consult 会话(会诊跨回合——
@@ -162,40 +165,47 @@ function backgroundStatusText(agent) {
162
165
  * 契约——denied 不弹面板、不悬挂);AUTO 档沿用普通回调(autoApprove 短路自动
163
166
  * 执行)。_suspended 保持 true:消化中 settle 延迟冻结 + 移交 pending。R17:pending
164
167
  * 计数/消化触发 = 任一 pending 族(T-R17j——consult/escalate 空闲 settle 也触发消化轮)。
165
- * F-UC7(§6.27.12.5 D):`upstream` = 未 drain 的 ask 在场(唤醒轮)——提示行走第三
166
- * 档、旗标随 auto 轮贯通到核 `runAgent`(域文本选择面)、`digest:*` 载荷条件携带
167
- * `upstream: true`(宿主日志区分唤醒轮与 digest 轮——两端同规)。 */
168
+ * F-UC7(§6.27.12.5 D):`upstream` = 未 drain 的 ask 在场(唤醒轮)——旗标随 auto 轮
169
+ * 贯通到核 `runAgent`(域文本选择面)、`digest:*` 载荷条件携带 `upstream: true`(宿主
170
+ * 日志区分唤醒轮与 digest 轮——两端同规)。
171
+ * F-UC8(§6.27.12.13 ①–③):可见提示面**按因两档 × 全档**——标签行 ask 携「谁 + 啥」
172
+ * (核容器键,CLI 零自持字面)/ digest(manual 与 AUTO 同判——泛句退场);起跑数行
173
+ * 与轮尾收尾行同守 `pend0 > 0`(ask-only 轮两行皆不出)。 */
168
174
  async function digestTurn(ctx, upstream = false) {
169
175
  const { agent, pushLine } = ctx
170
176
  const manual = !agent.autoApprove
171
- // 提示行三分(§6.27.12.5 D):manual 档 ask 轮 = 第三档(「消化已完成的报告」字面在
172
- // ask 轮相抵——本轮主事 = 答复子代理在飞提问);既有两档字面零改。
173
- const label = manual
174
- ? (upstream ? "[auto-turn: answering a subagent's in-flight message…]" : "[auto-turn: digesting finished subagent reports…]")
175
- : "[auto-turn: continuing background work…]"
176
- pushLine(label, C.dim)
177
+ // 起跑数前置(§6.27.12.13 ③):起跑行与轮尾收尾行同源(皆 = 起跑口径)
178
+ const pend0 = pendingFamilyCount(agent)
179
+ // 标签两档(§6.27.12.13 ①):ask 因恒优先——携参单源 = 核 `upstreamAskLabelVars`
180
+ // (队首 ask 的 `from` + 单行归一截断 `msg`);digest 因沿用既有字面;AUTO 档同判
181
+ // (泛句无生产者——开轮因穷尽;模式可见性另有 `AUTO│` 横幅载体)。
182
+ const ask = upstream ? upstreamAskLabelVars(agent) : null
183
+ pushLine(ask ? t("digest.turnLabelAsk", ask) : t("digest.turnLabel"), C.dim)
184
+ // 起跑数行(对位 VSC `.digest-status` 元素):`n > 0` 规则——ask-only 轮(`n = 0`)零行
185
+ if (pend0 > 0) pushLine(t("digest.start", { n: pend0 }), C.dim)
177
186
  const digestCtx = manual
178
187
  ? { ...ctx, askPermission: null, askBatchPermission: null, askQuestion: null }
179
188
  : ctx
180
189
  // LOGGING:digest:* 事件(D-S9 消化轮边界——LOGGING.md F-L4 挂起态覆盖)
181
190
  const d0 = Date.now()
182
- const pend0 = pendingFamilyCount(agent)
183
191
  logEvent("digest:start", { pendingN: pend0, ...(upstream ? { upstream: true } : {}) })
184
192
  const outcome = await runAgentTurn(digestCtx, "", { autoTurn: true, upstreamTurn: upstream, skipSession: true })
185
193
  logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms: Date.now() - d0, ...(upstream ? { upstream: true } : {}) })
186
194
  // X9(2026-09-20 端差·显示面消差批):轮尾**可见**收尾行(修前只有 `digest:end` 日志事件——
187
- // 用户不可见;对位 VSC `webview/chat.js:394-401`)。计数口径 = **起跑数** `pend0`
188
- // (与 VSC `suspension.mjs:302/:314` 同源);文案单源 = 核 i18n `digest.done` /
195
+ // 用户不可见;对位 VSC `webview/chat.js` `showDigestStatus` end 段)。计数口径 = **起跑数**
196
+ // `pend0`(与 VSC `suspension.mjs` 起跑 post 同源);文案单源 = 核 i18n `digest.done` /
189
197
  // `digest.aborted`;终态 ≠ ok(中止/失败)⇒ aborted 形态(VSC ok 旗标同口径)。
198
+ // F-UC8 守卫(§6.27.12.13 ③):`pend0 = 0`(ask-only 轮)⇒ **零收尾行**——与起跑行
199
+ // 同规则(两行成对);done / aborted 两形态同判(VSC 零动作守卫先于 ok 判)。
190
200
  const seconds = ((Date.now() - d0) / 1000).toFixed(1)
191
- pushLine(t(outcome === "ok" ? "digest.done" : "digest.aborted", { n: pend0, seconds }), C.dim)
201
+ if (pend0 > 0) pushLine(t(outcome === "ok" ? "digest.done" : "digest.aborted", { n: pend0, seconds }), C.dim)
192
202
  }
193
203
 
194
204
  /**
195
205
  * §17 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
196
206
  * - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
197
207
  * 上行 ask 入队 → 唤醒 + 谓词 → 开唤醒轮(F-UC7——谓词先于池空退出判,见第 2 步);
198
- * 挂起空闲用户 Enter → pendingInput 单槽(busy 含 digest 提交吞——INPUT-LOCK-ASYNC)
208
+ * 挂起空闲用户 Enter、会话内 busy Enter → pendingInput 单槽(busy-extend 2026-09-22 同判据)
199
209
  * ——用户输入优先于 digest;
200
210
  * - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
201
211
  * 续开合并消化轮或回挂起;pendingInput 非空 → 以该消息开新回合(不触发新 digest);
@@ -230,11 +240,12 @@ export async function suspensionSession(ctx) {
230
240
  while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
231
241
  sweepSettledToPending(agent)
232
242
  // 1. 用户输入优先(D-S5):pendingInput 单槽(INPUT-LOCK-ASYNC C'——busy(processing
233
- // 含 digest)提交吞——Enter 只可能落在挂起空闲/释放窗口——至多一条待交接——driver
243
+ // 含 digest)提交亦入本槽——TUI-INPUT-BOX.md §4.1;至多一条待交接——driver
234
244
  // 消费清槽即开新回合)。R15 攒批/queue 双源已随排队机制废弃收敛(2026-09-09——
235
245
  // 单消息交接——无合并无 /cmd 分流——key-handler 只收非斜杠文本——单消息逐发)。
236
246
  if ((state.pendingInput?.length ?? 0) > 0) {
237
247
  const head = String(state.pendingInput.shift())
248
+ pushLine("[sending queued message]", C.tool) // F16 消费回执(TUI.md §7.5——driver 消费点)
238
249
  agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
239
250
  await runAgentTurn(ctx, head, { skipSession: true })
240
251
  agent._suspended = true
@@ -280,7 +291,7 @@ export async function suspensionSession(ctx) {
280
291
  state._suspWake = null
281
292
  state.suspAbortArmed = false // round2 偏差 #4:会话退出即解除挂起中止武装(防跨会话粘滞)
282
293
  if (aborted) {
283
- // §15 abort 语义:只清已死条目(AGENT-LOOP-SUBAGENT.md §6.20——墓碑/出池/队列剔除/
294
+ // §15 abort 语义:只清已死条目(AGENT-LOOP-ASYNC-POOL.md §6.20——墓碑/出池/队列剔除/
284
295
  // 整批一次提醒;存活与已 settle 者留池消化——不注入陈旧错误)。
285
296
  discardAbortedPool(agent)
286
297
  discardAbortedAdvisors(agent)
@@ -15,6 +15,15 @@ export function writeStartupSequence(write = (s) => process.stdout.write(s)) {
15
15
  write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn + ansi.bracketedPasteOn + ansi.keyboardPush + ansi.modifyOtherKeysOn + ansi.wrapOff)
16
16
  }
17
17
 
18
+ // 启动加载画面(2026-09-21 用户):双进程冷启动(wrapped spawn + 全链 import)期间 alt buffer
19
+ // 全黑 → 用户以为死了。序列后立即写一行 loading(首帧 render 自然覆盖 ✗ 无需清除逻辑 ✗)。
20
+ // 版本号由调用方传入(bin 入口已读 package.json——零重复 IO);wrapped 父进程同款(spawn 前写——
21
+ // 父子两段冷启动都非黑)。纯文本行 ✗ 无 ANSI 位置控制 ✗ 首帧覆盖依赖 render-loop 全屏重绘 ✓
22
+ export function writeLoadingLine(write = (s) => process.stdout.write(s), version = "") {
23
+ const v = version ? ` v${version}` : ""
24
+ try { write(`\n\n ◌ thincoder loading...${v}\n`) } catch { /* 尽力面——TTY 不可写不阻断启动 */ }
25
+ }
26
+
18
27
  /** TUI 清理序列 = 异常退出恢复序列(单一来源——§9.2 表 2 候选 1):清屏 + 关闭鼠标/
19
28
  * 粘贴/键盘增强 + 退出 alt buffer + 显示光标 + 恢复环绕。
20
29
  * TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §9.3):提取为导出常量 `RECOVERY_SEQUENCE`——
@@ -0,0 +1,65 @@
1
+ import { makeDimsState } from "./dims.mjs"
2
+
3
+ /** TUI state 工厂(2026-09-22 structure-debt §2.2 · #159):state 字面量自 index.mjs startTUI
4
+ * 逐字搬移(仅去包裹);`dims` 种子以调用侧采样值入参(Cols/Rows 名保持字面量原样),
5
+ * `tasks` 由 agent 承接(`agent.tasks ?? []` 原地保留)。
6
+ * @param {{ cols: number, rows: number, agent: object }} deps
7
+ * @returns {object} TUI state(会话面字段 / dims 单源 / 面板回指位) */
8
+ export function createTuiState({ cols, rows, agent }) {
9
+ // 名字桥:字面量内沿用调用侧采样名(逐字搬移所需)
10
+ const startupCols = cols
11
+ const startupRows = rows
12
+ return {
13
+ lines: [], // conversation lines: { text, color }
14
+ streaming: "", // current streaming buffer
15
+ _advisorBlocks: [], // advisor ordered blocks: [{ kind: "think"|"text", text }] — preserves emission order (think ↔ tool interleaving)
16
+ input: [], // input buffer (codepoint array)
17
+ cursor: 0,
18
+ history: [],
19
+ historyIndex: -1,
20
+ _draft: null, // stashed unsent input while navigating history (restored on down past newest)
21
+ scroll: 0, // scroll lines from bottom upward
22
+ _foldScroll: new Map(), // 2026-08-31 块内滚动:foldKey → 窗口 offset(展开块 ▲▼ 翻窗)
23
+ _followTail: true, // 2026-08-31 流式跟随:渲染前 scroll=0;用户上滚暂停、到底/新消息恢复
24
+ processing: false,
25
+ controller: null, // AbortController for current agent run
26
+ permission: null, // { name, args, resolve }
27
+ permissionPreview: [], // content preview lines for permission approval (rendered above input box, without separation)
28
+ question: null, // { text, options, resolve } — agent question tool callback
29
+ picker: null, // active picker (stack top) { title, entries, lines, index, scroll, selectedLine, filter }
30
+ pickerStack: [], // picker 栈:showPicker push,Enter/Esc pop;state.picker 始终指向栈顶
31
+ pendingNotice: null, // 后台更新提示:有 picker 打开时挂起,picker 全部关闭后再弹
32
+ wizard: null, // first-launch config wizard { step, index, scroll, selectedLine, fields, error, lines }
33
+ tasks: agent.tasks ?? [], // task list from task tool (progress shown in status bar); carried over on session restore, auto-collapsed when all done
34
+ dims: makeDimsState({ cols: startupCols, rows: startupRows }), // terminal dims single source (Windows ConPTY instability, 2026-08-30) — seeded pre-raw-mode, re-sampled by event hooks only (startup retry / resize / idle watchdog)
35
+ tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0, reasoningTokens: 0 }, // cumulative token usage (shown in status bar)
36
+ ctxCache: { len: -1, tokens: 0 }, // context utilization estimate cache (estimateTokens is O(n), only recompute when history grows)
37
+ reasoning: "", // thinking stream buffer (dimmed display)
38
+ completion: null, // Tab completion state { candidates, index }
39
+
40
+ subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容行并入本块 blocks——subagent-children.mjs) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer(SUBAGENT-TAIL 单环:内层行同环计数、单载体最旧先行——TUI.md §6)
41
+ currentTool: null, // currently executing tool name (shown in status bar)
42
+ processingStarted: 0, // current turn start time (status bar timer)
43
+ status: "Ready",
44
+ ledger: { marker: null, warn: false, scannedAt: 0 }, // LEDGER-SURFACE(§2.30.3.4):L1 标记位——ledger-surface 写 / render-frame 读
45
+ queue: [], // 交接残项单容器 [{ text }](INPUT-LOCK:submit 不再排队——仅释放窗口兜底/挂起中止残余——回合尾队列循环续发,至多一条)
46
+ interruptPrompt: null, // Ctrl+I inject box(第 31 批——TUI-INPUT-BOX.md §8): { chars: string[], cursor: number } or null
47
+ attentionAwaiting: false, // 第 33 批(TUI §14.3(f)):回合结束等待输入——链尾置位 / 用户输入清位;不落盘、不进会话
48
+ search: null, // Ctrl+F search mode: { query: "", matches: [{lineIndex, charIndex}], index: 0 } or null
49
+ expandedBlocks: new Set(), // block hashes that are expanded (Enter toggles)
50
+ foldEnabled: true, // global fold toggle — /fold on|off
51
+ exitArmed: false, // Ctrl+C double-confirm: first press arms, second (within window) exits
52
+ // Lazy history window (parity with VS Code): only the latest messages are
53
+ // materialized on restore; PgUp-at-top loads earlier pages via loadOlder.
54
+ _historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
55
+ _historyTotal: 0, // total messages in the restored session
56
+ _hasOlder: false, // more earlier messages remain unloaded
57
+ _linesChars: 0, // TUI-OOM-ROOTCAUSE(§15.3.2):state.lines 全部行与载体文本总量(字符账——唯一新增状态位)
58
+ _agent: null, // TUI state → agent 回指挂载点:startTUI 装配时置 agent 引用——
59
+ // ① SYNC-CANCEL ⏹ 门控读 state._agent._syncChildAborts(subagent-panel.mjs,2026-09-09);
60
+ // ② CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):agent._tuiState = state 反向挂载——
61
+ // action:"panel" 经 ctx.state(= agent._tuiState)**读时现算**面板块(computePanelBlocks
62
+ // ——subTasks 活值纯推导——单账本)。headless/VS Code 无此装配 = 无面板 → panel 动作
63
+ // 恒降级池视图(CLI-only 完整能力——AC-P4)。
64
+ }
65
+ }
@@ -0,0 +1,64 @@
1
+ import { createInteraction } from "./interaction.mjs"
2
+ import { runAgentTurn } from "./agent-turn.mjs"
3
+ import { pasteClipboardImage as pasteClipboardImageImpl } from "./clipboard.mjs"
4
+
5
+ /** 回合面(2026-09-22 structure-debt §2.2 · #159):submit 门禁 / interaction 装配(权限 + 提问)/
6
+ * 剪贴板图片粘贴 / turnCtx / turn——块体自 index.mjs startTUI 逐字搬移;唯一改指 = turnCtx 的
7
+ * `assistantLabeled` 存取器两行改经 ctx.conversation(写入面单源)承接。
8
+ * `handleSlash` 由调用侧以惰性转发承接(命令层后定义——循环依赖语义逐字保持),turnCtx 由
9
+ * index.mjs 在命令层之后原址回填 `turnCtx.handleSlash`。
10
+ * @param {object} ctx
11
+ * @returns {{ submit: Function, turn: Function, turnCtx: object, askPermission: Function,
12
+ * askQuestion: Function, askBatchPermission: Function, pasteClipboardImage: Function }} */
13
+ export function createTurnFace(ctx) {
14
+ const { agent, state, pushLine, pushLabel, render, ensureAssistantLabel, summarize, handleSlash, conversation } = ctx
15
+ async function submit() {
16
+ const text = state.input.join("").trim()
17
+ if (!text) return
18
+ // INPUT-LOCK 防御(C'——2026-09-09 + F16 busy 单槽——2026-09-21):门禁在 key-handler
19
+ // (busy Enter 非模态 = 单槽注入 / 模态·斜杠·空·槽满 = 吞 + 提示,文本保留——TUI-INPUT-BOX.md
20
+ // §4.1);submit 只在非 busy 期达此;防御直呼/上游改动:busy 期拒绝(不清输入框不吞内容)。
21
+ if (state.processing) {
22
+ pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
23
+ render()
24
+ return
25
+ }
26
+ state.input = []
27
+ state.cursor = 0
28
+ const wasInHistory = state.historyIndex !== -1
29
+ state.history.push(text) // review #3 fix: single push (was duplicated — every submit appeared twice in ↑/↓ history)
30
+ state.historyIndex = -1
31
+ if (!wasInHistory) state._draft = null // submitted — the draft is now history. Keep draft when submitting from history mode (↓ can recover)
32
+ state.scroll = 0
33
+ state._followTail = true // 2026-08-31 会诊 deepseek:新消息恢复跟随(注释曾承诺、实现缺漏)
34
+
35
+ // Slash commands: handled locally, don't enter agent loop. 斜杠 busy 期提交在 key-handler
36
+ // 门禁被吞(白名单已删——斜杠同吞——INPUT-LOCK-BEHAVIOR-REVISED)——submit 仅非 busy 期可达。
37
+ if (text.startsWith("/")) {
38
+ await handleSlash(text)
39
+ render()
40
+ return
41
+ }
42
+
43
+ await turn(text)
44
+ }
45
+
46
+ // Interaction primitives: permission approval + Q&A input, implemented in interaction.mjs
47
+ const { askPermission, askQuestion, askBatchPermission } = createInteraction({
48
+ agent, state, pushLine, pushLabel, render, summarize,
49
+ })
50
+
51
+ // Clipboard image paste: implemented in clipboard.mjs
52
+ const pasteClipboardImage = () => pasteClipboardImageImpl({ agent, state, pushLine, render })
53
+
54
+ // Agent loop: implemented in agent-turn.mjs
55
+ const turnCtx = {
56
+ agent, state, pushLine, pushLabel, render, scheduleRender: render, ensureAssistantLabel,
57
+ askPermission, askQuestion, askBatchPermission, handleSlash: null,
58
+ get assistantLabeled() { return conversation.assistantLabeled },
59
+ set assistantLabeled(v) { conversation.assistantLabeled = v },
60
+ }
61
+ const turn = (text) => runAgentTurn(turnCtx, text)
62
+
63
+ return { submit, turn, turnCtx, askPermission, askQuestion, askBatchPermission, pasteClipboardImage }
64
+ }
@@ -2,11 +2,11 @@
2
2
  * spawn 子(自身 bin)tee stderr → 终端 + crash-reports/tui-stderr-<ts>-<pid>.log(外部终止/
3
3
  * native abort——fd 2 进程内不可改——诊断唯一默认捕获路)。子死 → 日志收尾 → 同码退(null 映射
4
4
  * code??(signal?1:0)——评审 #1);spawn error → 注日志 + exit 1(评审 #5——不挂死)。 */
5
- import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"
5
+ import { appendFileSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"
6
6
  import { spawn } from "node:child_process"
7
7
  import { fileURLToPath } from "node:url"
8
8
  import { crashReportsDir } from "../crash-reports.mjs"
9
- import { RECOVERY_SEQUENCE } from "./tui-lifecycle.mjs"
9
+ import { RECOVERY_SEQUENCE, writeLoadingLine } from "./tui-lifecycle.mjs"
10
10
 
11
11
  // F-3 信号语义(raw mode 既有 key-handler 双按语义——Ctrl+C = stdin 字节不产生信号 → 子正常退 → 父收
12
12
  // exit 同码退):父忽略 SIGINT/SIGTERM——tee 不被打断。2026-09-09 实测:Windows process.kill(SIGINT)
@@ -21,6 +21,10 @@ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPat
21
21
  logPath = `${dir}/tui-stderr-${Date.now()}-${process.pid}.log`
22
22
  writeFileSync(logPath, `# tui-stderr ${new Date().toISOString()} pid=${process.pid} argv=${JSON.stringify(process.argv)}\n`, { flag: "a", mode: 0o600 })
23
23
  } catch { return false }
24
+ // ①⸸ 启动加载行(spawn 前——回车后立刻可见 ✗ 冷启动期非黑):复用 tui-lifecycle.writeLoadingLine(修正轮 F-A ✗ 父段副本删)
25
+ let v = ""
26
+ try { v = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version } catch { /* 尽力面——读失败 → 无版本尾 */ }
27
+ writeLoadingLine(writeImpl, v)
24
28
  process.on("SIGINT", ignoreSignal); process.on("SIGTERM", ignoreSignal) // 父忽略信号(F-3)——tee 不被打断
25
29
  process.stderr.on("error", ignoreSignal) // 父 stderr 异步 EPIPE(终端/管道已关)→ 不炸父——tee 继续
26
30
  const note = (s) => { try { appendFileSync(logPath, s) } catch { /* 落盘尽力面——不阻断 tee */ } }