thincoder 0.12.58 → 0.12.60
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/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tui/index.mjs
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* config-helpers.mjs — persistRaw / syncProviderField / maskKey
|
|
15
15
|
* clipboard.mjs — clipboard image paste
|
|
16
16
|
* distill-cmd.mjs — /distill command
|
|
17
|
+
* mouse.mjs — SGR mouse parsing + dispatch assembly (createMouseDispatch)
|
|
18
|
+
* update-notice.mjs — background update notice + startup check
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import { emitKeypressEvents } from "node:readline"
|
|
@@ -25,28 +27,20 @@ import { createRenderLoop } from "./render-loop.mjs"
|
|
|
25
27
|
import { makeDimsState } from "./dims.mjs"
|
|
26
28
|
import { SLASH_COMMANDS, SLASH_ALIASES, createSlashCommands } from "./slash-commands.mjs"
|
|
27
29
|
import { createWizard } from "./wizard.mjs"
|
|
28
|
-
import { writeStartupSequence, createExitCleanup } from "./tui-lifecycle.mjs"
|
|
30
|
+
import { writeStartupSequence, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
|
|
29
31
|
import { createPickers } from "./pickers.mjs"
|
|
30
32
|
import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
|
|
31
33
|
import { createInteraction } from "./interaction.mjs"
|
|
32
34
|
import { pasteClipboardImage as pasteClipboardImageImpl, insertPastedText, translateShiftEnter, stripKeyboardProtocol } from "./clipboard.mjs"
|
|
33
|
-
import { parseMouseClicks,
|
|
35
|
+
import { parseMouseClicks, handleWheel, createMouseDispatch } from "./mouse.mjs"
|
|
34
36
|
import { runAgentTurn } from "./agent-turn.mjs"
|
|
35
37
|
import { createKeyHandler, convMaxScroll } from "./key-handler.mjs"
|
|
36
|
-
import { showStartup, backgroundIndex,
|
|
37
|
-
import {
|
|
38
|
+
import { showStartup, backgroundIndex, createLoadOlder } from "./startup.mjs"
|
|
39
|
+
import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
|
|
38
40
|
import { createConfigHelpers } from "./config-helpers.mjs"
|
|
41
|
+
import { createUpdateNotice, pendingNoticeReady } from "./update-notice.mjs"
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
export function upgradeFailureText(code, output) {
|
|
42
|
-
const tail = (output ?? "").trimEnd().split("\n").slice(-3).join("\n")
|
|
43
|
-
return `✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.${tail ? `\n${tail}` : ""}`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** 后台更新提示可弹出的条件:无任何交互弹层(picker/permission/question)激活。 */
|
|
47
|
-
export function pendingNoticeReady(state) {
|
|
48
|
-
return Boolean(state.pendingNotice && !state.picker && !state.permission && !state.question)
|
|
49
|
-
}
|
|
43
|
+
export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
|
|
50
44
|
|
|
51
45
|
/**
|
|
52
46
|
* SESSION.md §8 D-S2 — TUI 启动首帧前的 provider 重选流程:
|
|
@@ -112,7 +106,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
112
106
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
113
107
|
completion: null, // Tab completion state { candidates, index }
|
|
114
108
|
|
|
115
|
-
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 } } — 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
|
|
109
|
+
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: [](§27 R23 嵌套子代理子块载体——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(R23:子块计入外层同配额——树级 trim)
|
|
116
110
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
117
111
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
118
112
|
status: "Ready",
|
|
@@ -127,7 +121,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
127
121
|
_historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
|
|
128
122
|
_historyTotal: 0, // total messages in the restored session
|
|
129
123
|
_hasOlder: false, // more earlier messages remain unloaded
|
|
124
|
+
_agent: null, // §19.6 D-P1 面板镜像挂载点:startTUI 装配时置 agent 引用——subagent-blocks
|
|
125
|
+
// syncPanelSnapshot 经此写 agent._panelSnapshot(块级状态变更即刷——subagent
|
|
126
|
+
// action:"panel" 读——模型可查面板视图 + 门控补发冻结)。headless/VS Code 无此
|
|
127
|
+
// 挂载 = 无镜像 → panel 动作恒降级池视图(CLI-only 完整能力——AC-P4)。
|
|
130
128
|
}
|
|
129
|
+
state._agent = agent
|
|
130
|
+
agent._panelSnapshot = [] // 镜像初始态 = 空面板(非"无镜像"——[] 与 undefined 区分)
|
|
131
131
|
|
|
132
132
|
// On session restore, if all tasks are completed, auto-collapse the todo panel (match runtime behavior)
|
|
133
133
|
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
@@ -147,44 +147,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
147
147
|
// modifyOtherKeys lvl 2 (\x1b[>4;2m): Shift+Enter → \x1b[27;2;13~ (mintty / Git Bash)
|
|
148
148
|
// translateShiftEnter (stdin layer) maps both to \x1b\r → meta+return → multiline branch.
|
|
149
149
|
writeStartupSequence()
|
|
150
|
+
setTuiActive(true) // R25(F-R25a):终端接管完成——置 TUI 活动态(崩溃钩子恢复判定源)
|
|
150
151
|
|
|
151
152
|
const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
|
|
152
153
|
|
|
153
154
|
let pasteMode = false
|
|
154
155
|
let pasteAccum = ""
|
|
155
156
|
|
|
156
|
-
/** 懒加载更早历史(2026-08-31 用户约定:"滚动到头自动加载"——滚轮/PgUp 到顶皆触发;
|
|
157
|
-
* 2026-08-31 前只挂 PgUp 键 = 违约,滚轮到头无反应)。加载后滚动补偿保持锚定。
|
|
158
|
-
* 外层作用域:data 回调(滚轮分支)与 createKeyHandler ctx(PgUp 分支)共用。 */
|
|
159
|
-
const loadOlder = () => {
|
|
160
|
-
if (!state._hasOlder) return
|
|
161
|
-
const full = agent._fullHistory ?? []
|
|
162
|
-
const loaded = state._historyLoaded
|
|
163
|
-
const start = Math.max(0, full.length - loaded - HISTORY_PAGE_MESSAGES)
|
|
164
|
-
const end = full.length - loaded
|
|
165
|
-
if (start >= end) return
|
|
166
|
-
|
|
167
|
-
const d = state.dims ? state.dims.get() : {}
|
|
168
|
-
const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
|
|
169
|
-
const before = countConvLines(state, cols, d.rows ?? (process.stdout.rows || 24))
|
|
170
|
-
|
|
171
|
-
if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
|
|
172
|
-
state._lineIdCounter = state._lineIdCounter ?? 0
|
|
173
|
-
const older = historyToLines(full, start, end)
|
|
174
|
-
for (const l of older) l._lineId = ++state._lineIdCounter
|
|
175
|
-
state.lines.unshift(...older)
|
|
176
|
-
state._historyLoaded += end - start
|
|
177
|
-
state._hasOlder = start > 0
|
|
178
|
-
if (state._hasOlder) {
|
|
179
|
-
state.lines.unshift({ text: `… ${start} more earlier messages (scroll to top to load)`, color: C.dim })
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const after = countConvLines(state, cols, (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24))
|
|
183
|
-
state.scroll += Math.max(0, after - before)
|
|
184
|
-
render()
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
157
|
process.stdin.on("data", (chunk) => {
|
|
189
158
|
try {
|
|
190
159
|
|
|
@@ -237,7 +206,6 @@ export async function startTUI(agent, opts = {}) {
|
|
|
237
206
|
|
|
238
207
|
// Scroll wheel: \x1b[<64;col;rowM = up, \x1b[<65;col;rowM = down(3 lines each)
|
|
239
208
|
// 2026-08-31:坐标命中展开块内容行 → 块内滚动(handleWheel);未命中 → 会话滚动(现状)
|
|
240
|
-
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
241
209
|
for (const m of text.matchAll(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g)) {
|
|
242
210
|
const button = Number(m[1])
|
|
243
211
|
if (button === 64 || button === 65) {
|
|
@@ -267,9 +235,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
267
235
|
}
|
|
268
236
|
|
|
269
237
|
// Strip complete mouse sequences; keep incomplete tail for reassembly with next chunk
|
|
270
|
-
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
271
238
|
text = text.replace(/\x1b\[<\d+;\d+;\d+[Mm]/g, "")
|
|
272
|
-
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
273
239
|
const tail = text.match(/\x1b\[<[\d;]*$/)
|
|
274
240
|
if (tail) {
|
|
275
241
|
mousePending = tail[0]
|
|
@@ -298,6 +264,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
298
264
|
if (state.lines.length > 5000) {
|
|
299
265
|
state.lines.splice(0, 1000)
|
|
300
266
|
state.lines.unshift({ text: `... [earlier messages trimmed — ${state.lines.length} lines remaining]`, color: C.dim })
|
|
267
|
+
// 2026-09-03 修复轮(冻结锚点):头裁对在途 settled 锚点整体前移(锚点是绝对
|
|
268
|
+
// 流位置)——不校正则池空补发冻结(freezeSubTaskLines splice)落点漂移;校正量
|
|
269
|
+
// = 净位移(裁 1000 补 1 标记行 → −999,code review round1 #3)。
|
|
270
|
+
shiftFreezeAnchors(state, 1000)
|
|
301
271
|
}
|
|
302
272
|
render()
|
|
303
273
|
}
|
|
@@ -326,6 +296,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
326
296
|
pushLine)
|
|
327
297
|
const { render, scheduleRender } = renderLoop
|
|
328
298
|
|
|
299
|
+
// 懒加载更早历史(startup.mjs createLoadOlder——D-S1b):滚轮/PgUp 到顶自动加载;
|
|
300
|
+
// 声明在此(render 已可用)——data 回调(滚轮分支)与 createKeyHandler ctx(PgUp)共用。
|
|
301
|
+
const loadOlder = createLoadOlder({ agent, state, render })
|
|
302
|
+
|
|
329
303
|
// Resize events are genuine dimension changes on every terminal (2026-08-31
|
|
330
304
|
// simplification: the earlier settle-timer/double-confirm machinery was built
|
|
331
305
|
// on the misdiagnosed ConPTY-stale hypothesis and even stalled drag-shrink).
|
|
@@ -453,9 +427,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
453
427
|
}
|
|
454
428
|
})
|
|
455
429
|
|
|
456
|
-
//
|
|
457
|
-
const onMouseClick
|
|
458
|
-
const mouseCtx = () => ({ state, render })
|
|
430
|
+
// 鼠标点击/滚轮 ctx 装配(mouse.mjs createMouseDispatch——D-S1a:cancelSubagent/onMouseClick/mouseCtx)
|
|
431
|
+
const { onMouseClick, mouseCtx } = createMouseDispatch({ agent, state, pushLine, render, popPicker })
|
|
459
432
|
|
|
460
433
|
// ---------------------------------------------------------- Startup screen + background indexing
|
|
461
434
|
|
|
@@ -466,49 +439,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
466
439
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
467
440
|
backgroundIndex({ agent, state, render })
|
|
468
441
|
|
|
469
|
-
// Check for updates (non-blocking, after startup screen)
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
{ type: "item", text: "Upgrade now", action: "upgrade" },
|
|
475
|
-
{ type: "item", text: "Later", action: "later" },
|
|
476
|
-
])
|
|
477
|
-
if (sel?.action !== "upgrade") return
|
|
478
|
-
pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
|
|
479
|
-
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
480
|
-
const { exec } = await import("node:child_process")
|
|
481
|
-
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
482
|
-
let stdout = ""
|
|
483
|
-
cp.stdout?.on("data", (d) => { stdout += d })
|
|
484
|
-
cp.stderr?.on("data", (d) => { stdout += d })
|
|
485
|
-
cp.on("close", (code) => {
|
|
486
|
-
if (code === 0) {
|
|
487
|
-
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
488
|
-
} else {
|
|
489
|
-
pushLine(upgradeFailureText(code, stdout), C.error)
|
|
490
|
-
}
|
|
491
|
-
render()
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
|
-
;(async () => {
|
|
495
|
-
try {
|
|
496
|
-
const { readFileSync } = await import("node:fs")
|
|
497
|
-
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
|
|
498
|
-
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
499
|
-
const result = await checkForUpdate(pkg.version)
|
|
500
|
-
if (result?.newer) {
|
|
501
|
-
// Defer: if wizard is still active, just show a dim line
|
|
502
|
-
if (state.wizard) {
|
|
503
|
-
pushLine(`Tip: thincoder ${result.latest} is available (run /upgrade later or restart)`, C.dim)
|
|
504
|
-
render()
|
|
505
|
-
} else {
|
|
506
|
-
state.pendingNotice = result
|
|
507
|
-
render()
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
} catch { /* network error or timeout — silently skip */ }
|
|
511
|
-
})()
|
|
442
|
+
// Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
|
|
443
|
+
// (D-S1c):有 picker 打开时不硬抢——挂到 state.pendingNotice,picker 全部关闭后由
|
|
444
|
+
// doRender 弹出(showUpdateNotice 经 renderLoop ctx getter 懒引用)。
|
|
445
|
+
const { showUpdateNotice, checkUpdates } = createUpdateNotice({ state, showPicker, pushLine, pushLabel, render })
|
|
446
|
+
checkUpdates()
|
|
512
447
|
}
|
|
513
448
|
|
|
514
449
|
function summarize(obj) {
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -42,7 +42,16 @@ export function createInteraction(ctx) {
|
|
|
42
42
|
}
|
|
43
43
|
if (base === "delete") return [`${args.path}${args.force ? " (force: also delete tracked files)" : ""}`]
|
|
44
44
|
if (base === "subagent") return cap(args.task ?? "", 500).split("\n")
|
|
45
|
-
if (base === "
|
|
45
|
+
if (base === "memory") {
|
|
46
|
+
// §6 action-routed preview (D-M5): put shows content, batch delete/clear show gate args
|
|
47
|
+
const action = String(args.action ?? "")
|
|
48
|
+
if (action === "put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
|
|
49
|
+
if (action === "delete") return args.id
|
|
50
|
+
? [`delete single: ${args.id}${args.layer ? ` (layer ${args.layer})` : ""}`]
|
|
51
|
+
: [`batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""}`, `confirm=${args.confirm}`]
|
|
52
|
+
if (action === "clear") return [`clear personal memory`, `layer=${args.layer ?? ""} confirm=${args.confirm}`]
|
|
53
|
+
return [cap(summarize(args), 300)]
|
|
54
|
+
}
|
|
46
55
|
return [cap(summarize(args), 300)]
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -98,10 +107,12 @@ export function createInteraction(ctx) {
|
|
|
98
107
|
}
|
|
99
108
|
if (!options.length) {
|
|
100
109
|
// free text: open input mode for user typing, Enter to submit
|
|
110
|
+
// q.cursor 装配(TUI-INPUT-BOX.md §7.2/§7.5 #10b):answer 存 codepoint 数组(同
|
|
111
|
+
// state.input 语义——emoji 不劈半),进入自由文本态即 cursor = answer.length(= 0)。
|
|
101
112
|
pushLabel(`❯ Question`, ansi.bold + C.tool)
|
|
102
113
|
for (const line of text.split("\n")) pushLine(` ${line}`, C.text)
|
|
103
114
|
return new Promise((resolve) => {
|
|
104
|
-
state.question = { text, options: [], resolve }
|
|
115
|
+
state.question = { text, options: [], answer: [], cursor: 0, resolve }
|
|
105
116
|
state.status = "Waiting for answer..."
|
|
106
117
|
render()
|
|
107
118
|
})
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -3,12 +3,11 @@ import { readClipboardText, insertPastedText } from "./clipboard.mjs"
|
|
|
3
3
|
import { computeLayout } from "./layout.mjs"
|
|
4
4
|
import { handleSearchKey } from "./key-handler-search.mjs"
|
|
5
5
|
import { countConvLines } from "./render-conversation.mjs"
|
|
6
|
-
import {
|
|
6
|
+
import { handlePermissionMode, handleQuestionMode, handleInterruptMode } from "./key-modes.mjs"
|
|
7
7
|
|
|
8
8
|
/** Current conversation max scroll offset (display lines beyond the visible panel). */
|
|
9
9
|
export function convMaxScroll(state) {
|
|
10
|
-
// Single source (
|
|
11
|
-
const d = state.dims ? state.dims.get() : {}
|
|
10
|
+
const d = state.dims ? state.dims.get() : {} // Single source (ConPTY instability, 2026-08-30) — cached dims
|
|
12
11
|
const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
|
|
13
12
|
const rows = d.rows ?? (process.stdout.rows || 24)
|
|
14
13
|
const layout = computeLayout(state, { cols, rows })
|
|
@@ -25,131 +24,99 @@ export function createKeyHandler(ctx) {
|
|
|
25
24
|
const { agent, state, render, popPicker, renderPickerLines, handleSlash, handleTab, submit, pasteClipboardImage, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems, renderWizard, pushLine, cleanup, showPicker, loadOlder } = ctx
|
|
26
25
|
|
|
27
26
|
return function onKeypress(str, key = {}) {
|
|
28
|
-
// permission confirm
|
|
29
|
-
//
|
|
30
|
-
if (state
|
|
31
|
-
const answer = (str || "").toLowerCase()
|
|
32
|
-
const isContinue = state.permission.name === "continue"
|
|
33
|
-
const isBatch = Boolean(state.permission.batch)
|
|
34
|
-
const validKeys = isContinue ? ["y", "n"] : isBatch ? ["a", "o", "n"] : ["y", "n", "a"]
|
|
35
|
-
if (validKeys.includes(answer) || key.name === "escape") {
|
|
36
|
-
const { resolve, name } = state.permission
|
|
37
|
-
state.permission = null
|
|
38
|
-
state.permissionPreview = []
|
|
39
|
-
state.status = "Processing..."
|
|
40
|
-
if (isBatch) {
|
|
41
|
-
// Merged batch ask: resolve the verdict string; dispatch applies it
|
|
42
|
-
// (approveAll = batch-scope allowance only, NOT the persistent AUTO flag).
|
|
43
|
-
const verdict = answer === "a" ? "approveAll" : answer === "o" ? "oneByOne" : "deny"
|
|
44
|
-
const tone = verdict === "approveAll" ? C.dim : C.error
|
|
45
|
-
pushLine(` [${verdict === "approveAll" ? "approved" : verdict === "deny" ? "denied" : "one by one"}] ${name}`, tone)
|
|
46
|
-
resolve(verdict)
|
|
47
|
-
render()
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
if (answer === "a" && !isContinue) {
|
|
51
|
-
agent.autoApprove = true
|
|
52
|
-
agent._pendingReminders = agent._pendingReminders ?? []
|
|
53
|
-
agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
|
|
54
|
-
pushLine(` [auto] AUTO ON: tool calls no longer prompt for approval (/auto to disable)`, C.warn)
|
|
55
|
-
}
|
|
56
|
-
const approved = answer === "y" || (answer === "a" && !isContinue)
|
|
57
|
-
// leave trail: record approval/denial in conversation (continue prompt has its own output, don't duplicate)
|
|
58
|
-
if (!isContinue) {
|
|
59
|
-
pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)
|
|
60
|
-
}
|
|
61
|
-
resolve(approved)
|
|
62
|
-
render()
|
|
63
|
-
}
|
|
64
|
-
return
|
|
65
|
-
}
|
|
27
|
+
// permission confirm: y/n/a (a = approve + AUTO ON); batch (§16 D-B1): a/o/n (Esc = deny)
|
|
28
|
+
// ——模态实现 key-modes.mjs handlePermissionMode(2026-09-03 D-S4)
|
|
29
|
+
if (handlePermissionMode(str, key, { state, agent, pushLine, render })) return
|
|
66
30
|
|
|
67
|
-
// question tool callback: free text / option selection
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (q.options.length > 0) {
|
|
71
|
-
// options mode: ↑↓ select, Enter confirm, Esc cancel
|
|
72
|
-
if (key.name === "escape") {
|
|
73
|
-
q.resolve("")
|
|
74
|
-
state.question = null
|
|
75
|
-
state.status = "Processing..."
|
|
76
|
-
render()
|
|
77
|
-
} else if (key.name === "up") {
|
|
78
|
-
q.selected = Math.max(0, (q.selected ?? 0) - 1)
|
|
79
|
-
render()
|
|
80
|
-
} else if (key.name === "down") {
|
|
81
|
-
q.selected = Math.min(q.options.length - 1, (q.selected ?? 0) + 1)
|
|
82
|
-
render()
|
|
83
|
-
} else if (key.name === "return") {
|
|
84
|
-
const answer = q.options[q.selected ?? 0]
|
|
85
|
-
if (answer === QUESTION_CUSTOM) {
|
|
86
|
-
// Switch to free-text mode — the user wants to type their own answer.
|
|
87
|
-
q.options = []
|
|
88
|
-
q.answer = ""
|
|
89
|
-
q.selected = undefined
|
|
90
|
-
state.status = "Waiting for answer..."
|
|
91
|
-
render()
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
q.resolve(answer)
|
|
95
|
-
state.question = null
|
|
96
|
-
state.status = "Processing..."
|
|
97
|
-
pushLine(` → ${answer}`, C.tool)
|
|
98
|
-
render()
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
// free text: type answer, Enter submit, Esc cancel
|
|
102
|
-
if (key.name === "escape") {
|
|
103
|
-
q.resolve("")
|
|
104
|
-
state.question = null
|
|
105
|
-
state.status = "Processing..."
|
|
106
|
-
render()
|
|
107
|
-
} else if (key.name === "return") {
|
|
108
|
-
if (q._pasting) return // block Enter while paste is in flight
|
|
109
|
-
const answer = (q.answer ?? "").trim()
|
|
110
|
-
q.resolve(answer || "")
|
|
111
|
-
state.question = null
|
|
112
|
-
state.status = "Processing..."
|
|
113
|
-
pushLine(` → ${answer || "(empty)"}`, C.tool)
|
|
114
|
-
render()
|
|
115
|
-
} else if (key.name === "backspace") {
|
|
116
|
-
q.answer = (q.answer ?? "").slice(0, -1)
|
|
117
|
-
render()
|
|
118
|
-
} else if (key.ctrl && !key.alt && key.name === "v") {
|
|
119
|
-
// Ctrl+V paste: read clipboard text (fires when the terminal passes Ctrl+V through
|
|
120
|
-
// as a key event; bracketed-paste terminals are handled upstream in the stdin handler)
|
|
121
|
-
if (q._pasting) return
|
|
122
|
-
q._pasting = true
|
|
123
|
-
readClipboardText().then((text) => {
|
|
124
|
-
q._pasting = false
|
|
125
|
-
if (text) {
|
|
126
|
-
insertPastedText(state, text)
|
|
127
|
-
render()
|
|
128
|
-
}
|
|
129
|
-
}).catch((e) => {
|
|
130
|
-
q._pasting = false
|
|
131
|
-
console.error(`[tui] clipboard paste failed: ${e.message}`)
|
|
132
|
-
})
|
|
133
|
-
} else if (str && !key.ctrl && !key.meta) {
|
|
134
|
-
q.answer = (q.answer ?? "") + str
|
|
135
|
-
render()
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return
|
|
139
|
-
}
|
|
31
|
+
// question tool callback: free text / option selection——模态实现
|
|
32
|
+
// key-modes.mjs handleQuestionMode(D-S4)
|
|
33
|
+
if (handleQuestionMode(str, key, { state, pushLine, render })) return
|
|
140
34
|
|
|
141
35
|
// Search mode: Ctrl+F to enter, Ctrl+N/Ctrl+P (or Ctrl+G/Ctrl+R) navigate, Esc exit
|
|
142
36
|
if (handleSearchKey(str, key, state, render)) return
|
|
143
37
|
|
|
144
|
-
|
|
38
|
+
if (key.ctrl && key.name === "c") {
|
|
145
39
|
// picker 打开时 Ctrl+C = 取消当前 picker(等同 Esc),不杀进程
|
|
146
40
|
if (state.picker) {
|
|
147
41
|
popPicker(null)
|
|
148
42
|
return
|
|
149
43
|
}
|
|
44
|
+
// §17.6(2026-09-03):武装窗口内第二次 Ctrl+C = 显式全停(D-C4 /abort 语义)。
|
|
45
|
+
// 在状态路由前检查(picker 取消分支例外——picker 打开时 Ctrl+C 语义 = 取消
|
|
46
|
+
// picker,武装让位;场景极窄——3s 窗口内需另有 picker 被打开)——两次按下之间
|
|
47
|
+
// 状态会迁移(首按停回合 → 释放窗口/挂起会话启动),武装必须跨态桥接:只查挂起
|
|
48
|
+
// 分支会让非挂起 processing 首按后的二按落入空闲退出分支(三按误退出应用)。
|
|
49
|
+
if (state.suspAbortArmed) {
|
|
50
|
+
if (ctx.suspArmTimer) clearTimeout(ctx.suspArmTimer)
|
|
51
|
+
state.suspAbortArmed = false
|
|
52
|
+
// 无可停目标(无 processing 回合 / 无会话 abort 集合 / 未挂起)时不吞键:
|
|
53
|
+
// 首按停的是普通回合(无后台池)——二按落回下方空闲退出分支(exitArmed 双
|
|
54
|
+
// 确认),不打印误导性的 "[Aborting background subagents…]"(advisor round1
|
|
55
|
+
// 🟡——停回合后连按退出的第二次按下被空转吞掉 → 退出从 3 按变 4 按)。
|
|
56
|
+
const hasStopTarget = (state.processing && state.controller) ||
|
|
57
|
+
(agent._sessionAbortAll?.length ?? 0) > 0 ||
|
|
58
|
+
(agent._sessionAbort != null && !agent._sessionAbort.signal.aborted) ||
|
|
59
|
+
state.suspended
|
|
60
|
+
if (hasStopTarget) {
|
|
61
|
+
// 当前回合平 abort(无 interrupt——命中 agent.mjs 回合收尾清池分支——全停)
|
|
62
|
+
if (state.processing && state.controller) state.controller.abort()
|
|
63
|
+
// 会话 abort 集合 = 链条内全部 controller(含 Ctrl+I/ContinueError 重建的旧
|
|
64
|
+
// controller——children 不逃逸,round1 偏差 #3)。挂起态才标记 _suspAborted +
|
|
65
|
+
// 唤醒 driver(driver 收尾清池)——非挂起语境置位会粘滞阻塞未来挂起会话重入
|
|
66
|
+
// (round2 偏差 #1 语义)。
|
|
67
|
+
for (const c of agent._sessionAbortAll ?? (agent._sessionAbort ? [agent._sessionAbort] : [])) c?.abort()
|
|
68
|
+
if (state.suspended) {
|
|
69
|
+
state._suspAborted = true
|
|
70
|
+
state._suspWake?.()
|
|
71
|
+
}
|
|
72
|
+
pushLine("[Aborting background subagents…]", C.warn)
|
|
73
|
+
render()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
// 无目标可停 → 落空继续到下方分支(processing 已停 → 空闲态 exitArmed 双确认)
|
|
77
|
+
}
|
|
78
|
+
if (state.suspended) {
|
|
79
|
+
// §17 D-S9 + round2 偏差 #4(2026-09-02)+ §17.6 修订(2026-09-03):挂起态
|
|
80
|
+
// Ctrl+C = 武装窗口两级中止——一次按键直接清池中止全部后台子代理的误触代价高
|
|
81
|
+
// (digest 刷屏时用户可能只想停住当前回合);仿空闲态退出武装语义(同下方空闲态
|
|
82
|
+
// exitArmed 双确认分支)。§17.6 修订 ①:中止当前回合的 abort 带 interrupt
|
|
83
|
+
// (无 message)——此前平 abort 命中 agent.mjs 回合收尾清池分支(aborted &&
|
|
84
|
+
// !reason.interrupt → 无条件 _asyncSubagents.clear()),一次首按即误杀全部后台
|
|
85
|
+
// 子代理(2026-09-03 用户两次实测——提示"再按才杀"与实际"一次就全杀"不符);
|
|
86
|
+
// interrupt 排除清池分支——会话与后台保留(agent-turn 区分:无 message 停回合
|
|
87
|
+
// 不续跑):
|
|
88
|
+
// ① 未武装 + 有回合在跑(digest 消化/会话内回合):仅中止当前回合
|
|
89
|
+
// (controller.abort({ interrupt: true }))——会话继续、后台子代理不受
|
|
90
|
+
// 影响,回挂起等待;
|
|
91
|
+
// ② 未武装 + 纯挂起等待:仅提示不清池;
|
|
92
|
+
// ③ 3s 武装窗口内再次 Ctrl+C:彻底中止——上方统一块(二按语义不变)。
|
|
93
|
+
if (state.processing && state.controller) {
|
|
94
|
+
state.controller.abort({ interrupt: true }) // ① 仅中止当前回合(digest/会话内回合),不碰后台池
|
|
95
|
+
pushLine("[stopped current turn — press Ctrl+C again within 3s to abort all background subagents]", C.warn)
|
|
96
|
+
} else {
|
|
97
|
+
const bgActive = [...(agent._asyncSubagents?.values() ?? [])].filter((e) => e.status === "running").length + (agent._asyncQueue?.length ?? 0)
|
|
98
|
+
pushLine(`[abort] Press Ctrl+C again within 3s to abort all background subagents (${bgActive} running)`, C.warn)
|
|
99
|
+
}
|
|
100
|
+
state.suspAbortArmed = true
|
|
101
|
+
if (ctx.suspArmTimer) clearTimeout(ctx.suspArmTimer)
|
|
102
|
+
ctx.suspArmTimer = setTimeout(() => { state.suspAbortArmed = false }, ctx.exitArmDelay ?? 3000)
|
|
103
|
+
ctx.suspArmTimer.unref?.()
|
|
104
|
+
render()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
150
107
|
if (state.processing && state.controller) {
|
|
151
|
-
|
|
152
|
-
|
|
108
|
+
// §17.6 D-C1(2026-09-03 紧急修复——processing 态曾无武装窗口,第一按直接平
|
|
109
|
+
// abort() → 命中 agent.mjs 回合收尾清池分支 → 一次 Ctrl+C 误杀全部后台子代理,
|
|
110
|
+
// 用户两次实测被坑):首按 = interrupt 语义(无 message——停当前回合不续跑——
|
|
111
|
+
// interrupt 排除 agent.mjs 清池分支——后台池保留——agent-turn 区分不重建续跑)
|
|
112
|
+
// + 武装 3s(复用 suspArmTimer/exitArmDelay——与挂起态同构——过期自动复位);
|
|
113
|
+
// 窗口内再按 = 上方统一块全停(平 abort → agent.mjs 清池)。
|
|
114
|
+
state.controller.abort({ interrupt: true })
|
|
115
|
+
pushLine("[stopped current turn — press Ctrl+C again within 3s to abort all background subagents]", C.warn)
|
|
116
|
+
state.suspAbortArmed = true
|
|
117
|
+
if (ctx.suspArmTimer) clearTimeout(ctx.suspArmTimer)
|
|
118
|
+
ctx.suspArmTimer = setTimeout(() => { state.suspAbortArmed = false }, ctx.exitArmDelay ?? 3000)
|
|
119
|
+
ctx.suspArmTimer.unref?.()
|
|
153
120
|
render()
|
|
154
121
|
return
|
|
155
122
|
}
|
|
@@ -174,7 +141,7 @@ export function createKeyHandler(ctx) {
|
|
|
174
141
|
// F1: 显示快捷键帮助
|
|
175
142
|
if (key.name === "f1" && !state.picker && !state.permission && !state.question) {
|
|
176
143
|
showPicker("Keyboard Shortcuts", [
|
|
177
|
-
{ type: "item", text: "Ctrl+C — Cancel/Abort;
|
|
144
|
+
{ type: "item", text: "Ctrl+C — Cancel/Abort; first press stops the turn (or arms), twice within 3s stops all / exits" },
|
|
178
145
|
{ type: "item", text: "Ctrl+I — Interrupt and inject message" },
|
|
179
146
|
{ type: "item", text: "Ctrl+F — Search conversation history" },
|
|
180
147
|
{ type: "item", text: "Shift+Enter / Ctrl+J — Insert newline (multiline input)" },
|
|
@@ -198,34 +165,9 @@ export function createKeyHandler(ctx) {
|
|
|
198
165
|
return
|
|
199
166
|
}
|
|
200
167
|
|
|
201
|
-
// Interrupt prompt mode: type message, Enter to inject, Esc to cancel
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
state.interruptPrompt = null
|
|
205
|
-
render()
|
|
206
|
-
} else if (key.name === "return") {
|
|
207
|
-
const msg = (state.interruptPrompt.text ?? "").trim()
|
|
208
|
-
state.interruptPrompt = null
|
|
209
|
-
if (msg) {
|
|
210
|
-
// Guard: if the turn already finished while the user was typing, the controller
|
|
211
|
-
// may have been replaced or already aborted — don't abort a live turn by mistake.
|
|
212
|
-
if (state.processing && state.controller && !state.controller.signal.aborted) {
|
|
213
|
-
pushLine(` [inject] ${msg}`, C.warn)
|
|
214
|
-
state.controller.abort({ interrupt: true, message: msg })
|
|
215
|
-
} else {
|
|
216
|
-
pushLine(` [inject — turn ended, message queued] ${msg}`, C.dim)
|
|
217
|
-
}
|
|
218
|
-
render()
|
|
219
|
-
}
|
|
220
|
-
} else if (key.name === "backspace") {
|
|
221
|
-
state.interruptPrompt.text = state.interruptPrompt.text.slice(0, -1)
|
|
222
|
-
render()
|
|
223
|
-
} else if (str && !key.ctrl && !key.meta) {
|
|
224
|
-
state.interruptPrompt.text += str.replace(/[\r\n]+/g, "")
|
|
225
|
-
render()
|
|
226
|
-
}
|
|
227
|
-
return
|
|
228
|
-
}
|
|
168
|
+
// Interrupt prompt mode: type message, Enter to inject, Esc to cancel——模态实现
|
|
169
|
+
// key-modes.mjs handleInterruptMode(D-S4)
|
|
170
|
+
if (handleInterruptMode(str, key, { state, pushLine, render })) return
|
|
229
171
|
|
|
230
172
|
// generic list picker: ↑↓/PgUp/PgDn/Home/End 导航,输入即过滤,Enter 选中,Esc 取消
|
|
231
173
|
if (state.picker) {
|
|
@@ -267,7 +209,6 @@ export function createKeyHandler(ctx) {
|
|
|
267
209
|
} else if (str && !key.ctrl && !key.meta) {
|
|
268
210
|
// 输入即过滤;粘贴的多行文本先去换行(与输入框清洗口径一致),仍含控制字符则整段丢弃
|
|
269
211
|
const text = str.replace(/[\r\n]+/g, "")
|
|
270
|
-
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
271
212
|
if (text && !/[\x00-\x1f\x7f]/.test(text)) applyFilter(p.filter + text)
|
|
272
213
|
}
|
|
273
214
|
return
|
|
@@ -423,19 +364,28 @@ export function createKeyHandler(ctx) {
|
|
|
423
364
|
return
|
|
424
365
|
}
|
|
425
366
|
if (key.name === "return" || key.name === "enter" || str === "\r") {
|
|
426
|
-
// Multiline newline
|
|
427
|
-
//
|
|
428
|
-
// 2. Shift+Enter: keyboard-enhanced terminals send \x1b[13;2u / \x1b[27;2;13~,
|
|
429
|
-
// translateShiftEnter maps them to \x1b\r → also meta+return
|
|
430
|
-
// 3. Ctrl+J: sends \n (0x0A), readline parses as name:"enter" — the universal
|
|
431
|
-
// fallback: \n and \r are distinct bytes in EVERY terminal, no protocol needed
|
|
432
|
-
// (legacy conhost users: Shift+Enter is a bare \r there, physically
|
|
433
|
-
// indistinguishable from Enter — Ctrl+J is their newline key)
|
|
367
|
+
// Multiline newline (docs/design/TUI-INPUT-BOX.md §1.5): Alt+Enter / Shift+Enter
|
|
368
|
+
// (translateShiftEnter) / Ctrl+J — the universal no-protocol fallback.
|
|
434
369
|
if ((key.name === "return" && key.meta) || key.name === "enter") {
|
|
435
370
|
state.input.splice(state.cursor, 0, "\n")
|
|
436
371
|
state.cursor++
|
|
437
372
|
render()
|
|
438
373
|
} else {
|
|
374
|
+
const text = state.input.join("").trim()
|
|
375
|
+
// §17 D-S5/F3/F7 + 偏差 #1:挂起态(suspended 或释放窗口 _suspPending)Enter =
|
|
376
|
+
// 新回合输入(非打断)——入 pendingInput 由挂起会话调度;输入框零干扰(F3)。
|
|
377
|
+
if ((state.suspended || state._suspPending) && text && !text.startsWith("/")) {
|
|
378
|
+
state.input = []
|
|
379
|
+
state.cursor = 0
|
|
380
|
+
state.history.push(text)
|
|
381
|
+
state.historyIndex = -1
|
|
382
|
+
state._draft = null
|
|
383
|
+
state.pendingInput ??= []
|
|
384
|
+
state.pendingInput.push(text)
|
|
385
|
+
state._suspWake?.()
|
|
386
|
+
render()
|
|
387
|
+
return
|
|
388
|
+
}
|
|
439
389
|
submit().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
440
390
|
}
|
|
441
391
|
return
|