min-agent 0.2.0 → 0.3.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/README.md +146 -18
- package/dist/agent.js +293 -408
- package/dist/assistant-stream.js +11 -7
- package/dist/cli.js +403 -140
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +3 -3
- package/dist/compaction.js +182 -81
- package/dist/config.js +186 -35
- package/dist/confirm.js +55 -6
- package/dist/context-window.js +67 -54
- package/dist/doom-loop.js +19 -12
- package/dist/http.js +119 -0
- package/dist/instructions.js +51 -33
- package/dist/logger.js +66 -0
- package/dist/markdown.js +3 -44
- package/dist/mcp.js +547 -100
- package/dist/memory.js +48 -6
- package/dist/output.js +36 -27
- package/dist/paste-handler.js +3 -3
- package/dist/plugins.js +33 -6
- package/dist/pricing.js +119 -0
- package/dist/provider.js +17 -15
- package/dist/serve.js +658 -369
- package/dist/sessions.js +151 -13
- package/dist/skills.js +466 -76
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +2 -1
- package/dist/tool-display.js +173 -0
- package/dist/tool-output.js +54 -45
- package/dist/tools/apply_patch.js +191 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +147 -70
- package/dist/tools/code_search.js +6 -5
- package/dist/tools/edit.js +23 -7
- package/dist/tools/explore.js +80 -12
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +146 -14
- package/dist/tools/index.js +7 -7
- package/dist/tools/question.js +4 -22
- package/dist/tools/read.js +71 -11
- package/dist/tools/task.js +33 -20
- package/dist/tools/todo.js +83 -73
- package/dist/tools/web_fetch.js +150 -46
- package/dist/tools/web_search.js +706 -28
- package/dist/tools/write.js +13 -7
- package/dist/tui/App.js +40 -6
- package/dist/tui/ConfirmBar.js +24 -3
- package/dist/tui/InputBar.js +390 -45
- package/dist/tui/MessageList.js +533 -20
- package/dist/tui/ModelPicker.js +108 -0
- package/dist/tui/QuestionBar.js +104 -0
- package/dist/tui/StatusBar.js +19 -11
- package/dist/tui/agent-runner.js +103 -0
- package/dist/tui/caret-pos.js +134 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +44 -0
- package/dist/tui/index.js +153 -24
- package/dist/tui/input-history.js +44 -0
- package/dist/tui/layout.js +17 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/selection.js +134 -0
- package/dist/tui/slash-commands.js +90 -0
- package/dist/tui/slash-handler.js +370 -0
- package/dist/tui/text-width.js +91 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +27 -0
- package/dist/tui-chat.js +111 -331
- package/dist/updater.js +57 -0
- package/docs/API.md +160 -14
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/package.json +7 -8
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# 输入框光标编辑实现计划
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 输入框支持左右键移动光标、点击定位光标、光标处插入/删除文本。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 编辑与坐标换算逻辑抽为纯函数模块(`src/tui/mouse.ts`、`src/tui/caret-pos.ts`),InputBar 只做状态与渲染集成。光标 `caretIndex` 基于 code point 数组索引(兼容 CJK/emoji),视觉行由 `visualRows()` 统一派生,渲染与点击换算共用同一份行映射,保证一致。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, React 18 + Ink 5, bun:test, SGR 鼠标协议 (1006)
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-08-16-input-caret-design.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 文件结构
|
|
16
|
+
|
|
17
|
+
| 文件 | 职责 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `src/tui/mouse.ts`(新) | SGR 鼠标协议:启用/禁用序列常量、`parseSgrMouse()` 事件解析 |
|
|
20
|
+
| `src/tui/caret-pos.ts`(新) | 光标纯函数:`visualRows()` 视觉行映射、`moveCaretHorizontal()`、`caretIndexFromClick()`、`insertAt()`/`backspaceAt()`/`deleteAt()` |
|
|
21
|
+
| `src/tui/InputBar.tsx`(改) | 集成:`caretIndex` state、左右键/插入/退格、光标渲染、硬件光标位置、鼠标协议启用与事件处理 |
|
|
22
|
+
| `src/tui/App.tsx`(改) | 给 InputBar 传 `terminalRows` prop |
|
|
23
|
+
| `tests/mouse.test.ts`(新) | 鼠标序列解析 |
|
|
24
|
+
| `tests/caret-pos.test.ts`(新) | 视觉行/移动/点击换算/编辑函数 |
|
|
25
|
+
|
|
26
|
+
测试命令(在 `packages/min-agent` 下):`bun test tests/<file>`、全量 `bun test`、类型 `./node_modules/.bin/tsc --noEmit`。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Task 1: mouse.ts — SGR 鼠标序列解析
|
|
31
|
+
|
|
32
|
+
**Files:**
|
|
33
|
+
- Create: `src/tui/mouse.ts`
|
|
34
|
+
- Test: `tests/mouse.test.ts`
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 1: 写失败测试**
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { expect, test } from "bun:test"
|
|
40
|
+
import { parseSgrMouse, MOUSE_ENABLE, MOUSE_DISABLE } from "../src/tui/mouse.js"
|
|
41
|
+
|
|
42
|
+
test("parseSgrMouse extracts row and col from a press event", () => {
|
|
43
|
+
expect(parseSgrMouse("\x1b[<0;10;5M")).toEqual({ row: 5, col: 10 })
|
|
44
|
+
expect(parseSgrMouse("\x1b[<2;3;1M")).toEqual({ row: 1, col: 3 })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("parseSgrMouse finds the event inside a larger chunk", () => {
|
|
48
|
+
expect(parseSgrMouse("abc\x1b[<0;1;2Mxyz")).toEqual({ row: 2, col: 1 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("parseSgrMouse returns null for non-mouse input", () => {
|
|
52
|
+
expect(parseSgrMouse("hello")).toBeNull()
|
|
53
|
+
expect(parseSgrMouse("\x1b[13;2u")).toBeNull()
|
|
54
|
+
expect(parseSgrMouse("\x1b[<0;3;4m")).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("mouse enable/disable sequences are well-formed", () => {
|
|
58
|
+
expect(MOUSE_ENABLE).toContain("?1000h")
|
|
59
|
+
expect(MOUSE_ENABLE).toContain("?1006h")
|
|
60
|
+
expect(MOUSE_DISABLE).toContain("?1000l")
|
|
61
|
+
expect(MOUSE_DISABLE).toContain("?1006l")
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 2: 运行确认失败**
|
|
66
|
+
|
|
67
|
+
Run: `bun test tests/mouse.test.ts`
|
|
68
|
+
Expected: FAIL(模块不存在)
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 3: 实现**
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
/** Enable button-event tracking (1000) with SGR coordinates (1006). */
|
|
74
|
+
export const MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h"
|
|
75
|
+
/** Disable button-event tracking. */
|
|
76
|
+
export const MOUSE_DISABLE = "\x1b[?1000l\x1b[?1006l"
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse an SGR mouse press event from raw stdin bytes:
|
|
80
|
+
* CSI < b ; Px ; Py M — button; column; row (b is 0 for left, rows/cols 1-based).
|
|
81
|
+
* Only press events (uppercase M) are handled; release events (lowercase m)
|
|
82
|
+
* are ignored. Returns null if no event found.
|
|
83
|
+
*/
|
|
84
|
+
export function parseSgrMouse(input: string): { row: number; col: number } | null {
|
|
85
|
+
const match = input.match(/\x1b\[<(\d+);(\d+);(\d+)M/)
|
|
86
|
+
if (!match) return null
|
|
87
|
+
return { row: parseInt(match[3]!, 10), col: parseInt(match[2]!, 10) }
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
注意:真实 xterm SGR 鼠标序列是三字段 `CSI < b ; Px ; Py M`(按钮;列;行),不是两字段——测试必须用真实终端会发出的格式。
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 4: 运行确认通过**
|
|
94
|
+
|
|
95
|
+
Run: `bun test tests/mouse.test.ts`
|
|
96
|
+
Expected: PASS(4 tests)
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 5: 提交**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git add src/tui/mouse.ts tests/mouse.test.ts
|
|
102
|
+
git commit -m "feat: SGR 鼠标序列解析"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Task 2: caret-pos.ts — 视觉行映射与光标移动
|
|
108
|
+
|
|
109
|
+
**Files:**
|
|
110
|
+
- Create: `src/tui/caret-pos.ts`
|
|
111
|
+
- Test: `tests/caret-pos.test.ts`
|
|
112
|
+
|
|
113
|
+
- [ ] **Step 1: 写失败测试**
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { expect, test } from "bun:test"
|
|
117
|
+
import { visualRows, moveCaretHorizontal } from "../src/tui/caret-pos.js"
|
|
118
|
+
|
|
119
|
+
test("visualRows maps a single line", () => {
|
|
120
|
+
expect(visualRows("ab", 10)).toEqual([{ text: "ab", startIndex: 0 }])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("visualRows counts newlines as one character", () => {
|
|
124
|
+
expect(visualRows("ab\ncd", 10)).toEqual([
|
|
125
|
+
{ text: "ab", startIndex: 0 },
|
|
126
|
+
{ text: "cd", startIndex: 3 },
|
|
127
|
+
])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("visualRows wraps wide lines and tracks fullwidth widths", () => {
|
|
131
|
+
const rows = visualRows("你好ab", 4)
|
|
132
|
+
expect(rows[0]!.text).toBe("你好")
|
|
133
|
+
expect(rows[0]!.startIndex).toBe(0)
|
|
134
|
+
expect(rows[1]!.text).toBe("ab")
|
|
135
|
+
expect(rows[1]!.startIndex).toBe(2)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("visualRows handles an empty value", () => {
|
|
139
|
+
expect(visualRows("", 10)).toEqual([{ text: "", startIndex: 0 }])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("moveCaretHorizontal clamps at both ends", () => {
|
|
143
|
+
expect(moveCaretHorizontal("ab", 0, -1)).toBe(0)
|
|
144
|
+
expect(moveCaretHorizontal("ab", 2, 1)).toBe(2)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("moveCaretHorizontal steps by code point, not UTF-16 unit", () => {
|
|
148
|
+
expect(moveCaretHorizontal("a你b", 1, 1)).toBe(2)
|
|
149
|
+
expect(moveCaretHorizontal("a你b", 2, -1)).toBe(1)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("moveCaretHorizontal crosses visual rows naturally", () => {
|
|
153
|
+
expect(moveCaretHorizontal("ab\ncd", 2, 1)).toBe(3)
|
|
154
|
+
expect(moveCaretHorizontal("ab\ncd", 3, -1)).toBe(2)
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- [ ] **Step 2: 运行确认失败**
|
|
159
|
+
|
|
160
|
+
Run: `bun test tests/caret-pos.test.ts`
|
|
161
|
+
Expected: FAIL(模块不存在)
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 3: 实现**
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { charWidth, wrapByWidth } from "./text-width.js"
|
|
167
|
+
|
|
168
|
+
/** One visual row of the input value: the text and its first character's index in Array.from(value). */
|
|
169
|
+
export interface VisualRow {
|
|
170
|
+
text: string
|
|
171
|
+
startIndex: number
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Split the value into visual rows, tracking each row's character start index (newlines count as one). */
|
|
175
|
+
export function visualRows(value: string, maxWidth: number): VisualRow[] {
|
|
176
|
+
const rows: VisualRow[] = []
|
|
177
|
+
let start = 0
|
|
178
|
+
const lines = value.split("\n")
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
for (const row of wrapByWidth(lines[i]!, maxWidth)) {
|
|
181
|
+
rows.push({ text: row, startIndex: start })
|
|
182
|
+
start += Array.from(row).length
|
|
183
|
+
}
|
|
184
|
+
if (i < lines.length - 1) start += 1
|
|
185
|
+
}
|
|
186
|
+
return rows
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Move the caret by one code point; clamps at the start/end of the value. */
|
|
190
|
+
export function moveCaretHorizontal(value: string, caretIndex: number, direction: 1 | -1): number {
|
|
191
|
+
const len = Array.from(value).length
|
|
192
|
+
return Math.min(Math.max(caretIndex + direction, 0), len)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- [ ] **Step 4: 运行确认通过**
|
|
197
|
+
|
|
198
|
+
Run: `bun test tests/caret-pos.test.ts`
|
|
199
|
+
Expected: PASS(7 tests)
|
|
200
|
+
|
|
201
|
+
- [ ] **Step 5: 提交**
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git add src/tui/caret-pos.ts tests/caret-pos.test.ts
|
|
205
|
+
git commit -m "feat: 输入框视觉行映射与光标移动"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Task 3: caret-pos.ts — 点击换算与编辑函数
|
|
211
|
+
|
|
212
|
+
**Files:**
|
|
213
|
+
- Modify: `src/tui/caret-pos.ts`
|
|
214
|
+
- Test: `tests/caret-pos.test.ts`
|
|
215
|
+
|
|
216
|
+
- [ ] **Step 1: 写失败测试(追加)**
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { caretIndexFromClick, insertAt, backspaceAt, deleteAt } from "../src/tui/caret-pos.js"
|
|
220
|
+
|
|
221
|
+
test("caretIndexFromClick maps column to character index", () => {
|
|
222
|
+
const rows = visualRows("abcd", 10)
|
|
223
|
+
expect(caretIndexFromClick(rows, 0, 5, 5)).toBe(0)
|
|
224
|
+
expect(caretIndexFromClick(rows, 0, 6, 5)).toBe(1)
|
|
225
|
+
expect(caretIndexFromClick(rows, 0, 9, 5)).toBe(4)
|
|
226
|
+
expect(caretIndexFromClick(rows, 0, 99, 5)).toBe(4)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test("caretIndexFromClick snaps fullwidth characters to the nearest boundary", () => {
|
|
230
|
+
const rows = visualRows("你ab", 10)
|
|
231
|
+
// "你" spans columns 5-6; clicking past it snaps to index 1
|
|
232
|
+
expect(caretIndexFromClick(rows, 0, 7, 5)).toBe(1)
|
|
233
|
+
// clicking just before it stays at index 0
|
|
234
|
+
expect(caretIndexFromClick(rows, 0, 5, 5)).toBe(0)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("caretIndexFromClick picks the right visual row", () => {
|
|
238
|
+
const rows = visualRows("ab\ncd", 10)
|
|
239
|
+
// row 1 is "cd" starting at text column 4: 'c' occupies col 4, 'd' col 5;
|
|
240
|
+
// clicking 'd' snaps to the boundary before it (index 4)
|
|
241
|
+
expect(caretIndexFromClick(rows, 1, 5, 4)).toBe(4)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("caretIndexFromClick with an out-of-range row returns 0", () => {
|
|
245
|
+
const rows = visualRows("ab", 10)
|
|
246
|
+
expect(caretIndexFromClick(rows, 5, 5, 4)).toBe(0)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test("insertAt inserts at the caret and advances it", () => {
|
|
250
|
+
expect(insertAt("ab", 1, "X")).toEqual({ value: "aXb", caretIndex: 2 })
|
|
251
|
+
expect(insertAt("ab", 0, "你")).toEqual({ value: "你ab", caretIndex: 1 })
|
|
252
|
+
expect(insertAt("", 0, "x")).toEqual({ value: "x", caretIndex: 1 })
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test("backspaceAt removes the code point before the caret", () => {
|
|
256
|
+
expect(backspaceAt("ab", 2)).toEqual({ value: "a", caretIndex: 1 })
|
|
257
|
+
expect(backspaceAt("a你b", 2)).toEqual({ value: "ab", caretIndex: 1 })
|
|
258
|
+
expect(backspaceAt("ab", 0)).toEqual({ value: "ab", caretIndex: 0 })
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test("deleteAt removes the code point at the caret", () => {
|
|
262
|
+
expect(deleteAt("ab", 0)).toEqual({ value: "b", caretIndex: 0 })
|
|
263
|
+
expect(deleteAt("a你b", 1)).toEqual({ value: "ab", caretIndex: 1 })
|
|
264
|
+
expect(deleteAt("ab", 2)).toEqual({ value: "ab", caretIndex: 2 })
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- [ ] **Step 2: 运行确认失败**
|
|
269
|
+
|
|
270
|
+
Run: `bun test tests/caret-pos.test.ts`
|
|
271
|
+
Expected: FAIL(caretIndexFromClick/insertAt 等未定义)
|
|
272
|
+
|
|
273
|
+
- [ ] **Step 3: 实现(追加到 caret-pos.ts)**
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
/**
|
|
277
|
+
* Map a click column (1-based terminal column) on a visual row to a character
|
|
278
|
+
* index in the value. Fullwidth characters snap to the nearer boundary.
|
|
279
|
+
*/
|
|
280
|
+
export function caretIndexFromClick(
|
|
281
|
+
rows: VisualRow[],
|
|
282
|
+
rowIndex: number,
|
|
283
|
+
clickColumn: number,
|
|
284
|
+
textStartColumn: number,
|
|
285
|
+
): number {
|
|
286
|
+
const row = rows[rowIndex]
|
|
287
|
+
if (!row) return 0
|
|
288
|
+
const target = clickColumn - textStartColumn
|
|
289
|
+
if (target <= 0) return row.startIndex
|
|
290
|
+
let width = 0
|
|
291
|
+
let idx = 0
|
|
292
|
+
for (const char of row.text) {
|
|
293
|
+
const w = charWidth(char)
|
|
294
|
+
if (width + w > target) {
|
|
295
|
+
return row.startIndex + idx + (target - width < w / 2 ? 0 : 1)
|
|
296
|
+
}
|
|
297
|
+
width += w
|
|
298
|
+
idx++
|
|
299
|
+
}
|
|
300
|
+
return row.startIndex + idx
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Insert text at the caret; returns the new value and caret index. */
|
|
304
|
+
export function insertAt(value: string, caretIndex: number, text: string): { value: string; caretIndex: number } {
|
|
305
|
+
const chars = Array.from(value)
|
|
306
|
+
chars.splice(caretIndex, 0, ...Array.from(text))
|
|
307
|
+
return { value: chars.join(""), caretIndex: caretIndex + Array.from(text).length }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Remove the code point before the caret. */
|
|
311
|
+
export function backspaceAt(value: string, caretIndex: number): { value: string; caretIndex: number } {
|
|
312
|
+
if (caretIndex <= 0) return { value, caretIndex }
|
|
313
|
+
const chars = Array.from(value)
|
|
314
|
+
chars.splice(caretIndex - 1, 1)
|
|
315
|
+
return { value: chars.join(""), caretIndex: caretIndex - 1 }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Remove the code point at the caret. */
|
|
319
|
+
export function deleteAt(value: string, caretIndex: number): { value: string; caretIndex: number } {
|
|
320
|
+
const chars = Array.from(value)
|
|
321
|
+
if (caretIndex >= chars.length) return { value, caretIndex }
|
|
322
|
+
chars.splice(caretIndex, 1)
|
|
323
|
+
return { value: chars.join(""), caretIndex }
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
注意:`chars` 是 code point 数组,`splice` 索引即 code point 索引,与 `caretIndex` 语义一致,无需 UTF-16 换算。
|
|
328
|
+
|
|
329
|
+
- [ ] **Step 4: 运行确认通过**
|
|
330
|
+
|
|
331
|
+
Run: `bun test tests/caret-pos.test.ts`
|
|
332
|
+
Expected: PASS(14 tests)
|
|
333
|
+
|
|
334
|
+
- [ ] **Step 5: 提交**
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
git add src/tui/caret-pos.ts tests/caret-pos.test.ts
|
|
338
|
+
git commit -m "feat: 点击坐标换算与光标编辑函数"
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Task 4: InputBar 集成 — 光标状态、左右键与编辑
|
|
344
|
+
|
|
345
|
+
**Files:**
|
|
346
|
+
- Modify: `src/tui/InputBar.tsx`
|
|
347
|
+
|
|
348
|
+
先读取当前文件确认行号(`packages/min-agent/src/tui/InputBar.tsx`,以下基于 HEAD 版本)。
|
|
349
|
+
|
|
350
|
+
- [ ] **Step 1: 替换 import 与光标状态**
|
|
351
|
+
|
|
352
|
+
将:
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
import { displayWidth, wrapByWidth } from "./text-width.js"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
替换为:
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
import { displayWidth } from "./text-width.js"
|
|
362
|
+
import { visualRows, moveCaretHorizontal, insertAt, backspaceAt, deleteAt } from "./caret-pos.js"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
在 `const [value, setValue] = useState("")` 后加:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
const [caretIndex, setCaretIndex] = useState(0)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
更新 `acceptCommand`:
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
const acceptCommand = (cmd: SlashCommand) => {
|
|
375
|
+
if (cmd.argHint) {
|
|
376
|
+
setValue(`/${cmd.name} `)
|
|
377
|
+
setCaretIndex(cmd.name.length + 2)
|
|
378
|
+
setMenuDismissed(true)
|
|
379
|
+
} else {
|
|
380
|
+
onSubmit(`/${cmd.name}`)
|
|
381
|
+
setValue("")
|
|
382
|
+
setCaretIndex(0)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
- [ ] **Step 2: 重写 useInput 回调**
|
|
388
|
+
|
|
389
|
+
将 `useInput((input, key) => { ... })` 回调整体替换为:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
useInput((input, key) => {
|
|
393
|
+
if (disabled) return
|
|
394
|
+
|
|
395
|
+
// Fast typing or paste can deliver "text\r" as a single chunk — treat a
|
|
396
|
+
// trailing Enter as Enter and strip it from the value.
|
|
397
|
+
const cleanInput = input.replace(/[\r\n]+$/, "")
|
|
398
|
+
const pressedEnter = key.return || /[\r\n]+$/.test(input)
|
|
399
|
+
|
|
400
|
+
// Esc closes the slash menu
|
|
401
|
+
if (key.escape) {
|
|
402
|
+
if (menuOpen) setMenuDismissed(true)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Any other key re-opens the menu
|
|
407
|
+
setMenuDismissed(false)
|
|
408
|
+
|
|
409
|
+
// Slash menu navigation
|
|
410
|
+
if (menuOpen) {
|
|
411
|
+
if (key.downArrow) {
|
|
412
|
+
setMenuIndex((i) => (i + 1) % matches.length)
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
if (key.upArrow) {
|
|
416
|
+
setMenuIndex((i) => (i - 1 + matches.length) % matches.length)
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
if (pressedEnter || key.tab) {
|
|
420
|
+
if (selected) acceptCommand(selected)
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
// Left/right keep their normal caret behavior even with the menu open
|
|
424
|
+
if (key.leftArrow || key.rightArrow) return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Ctrl+J: insert newline
|
|
428
|
+
if (key.ctrl && input === "j") {
|
|
429
|
+
const after = insertAt(value, caretIndex, "\n")
|
|
430
|
+
setValue(after.value)
|
|
431
|
+
setCaretIndex(after.caretIndex)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Enter: submit or continue if ends with backslash
|
|
436
|
+
if (pressedEnter) {
|
|
437
|
+
if (value.endsWith("\\")) {
|
|
438
|
+
const after = insertAt(value.slice(0, -1), caretIndex, "\n")
|
|
439
|
+
setValue(after.value)
|
|
440
|
+
setCaretIndex(after.caretIndex)
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
const text = value.trim()
|
|
444
|
+
if (text) {
|
|
445
|
+
onSubmit(text)
|
|
446
|
+
setValue("")
|
|
447
|
+
setCaretIndex(0)
|
|
448
|
+
}
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Left / right move the caret through the text
|
|
453
|
+
if (key.leftArrow) {
|
|
454
|
+
setCaretIndex((i) => moveCaretHorizontal(value, i, -1))
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
if (key.rightArrow) {
|
|
458
|
+
setCaretIndex((i) => moveCaretHorizontal(value, i, 1))
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Backspace / Delete — remove a whole code point, not a UTF-16 unit,
|
|
463
|
+
// otherwise CJK and emoji leave broken surrogate halves behind.
|
|
464
|
+
if (key.backspace || key.delete) {
|
|
465
|
+
const after = key.delete ? deleteAt(value, caretIndex) : backspaceAt(value, caretIndex)
|
|
466
|
+
setValue(after.value)
|
|
467
|
+
setCaretIndex(after.caretIndex)
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Escape, up/down arrows, etc. — ignore (handled elsewhere)
|
|
472
|
+
if (key.escape || key.upArrow || key.downArrow || key.pageUp || key.pageDown) return
|
|
473
|
+
if (key.ctrl || key.meta) return
|
|
474
|
+
|
|
475
|
+
// Kitty Shift+Enter sequence (or a fragment of it) — handled by the raw
|
|
476
|
+
// stdin listener, never as text
|
|
477
|
+
if (isKittySequenceFragment(input)) return
|
|
478
|
+
|
|
479
|
+
// SGR mouse events are handled by the raw stdin listener (Task 5)
|
|
480
|
+
if (/^\[<.*M$/.test(input)) return
|
|
481
|
+
|
|
482
|
+
// Tab → 2 spaces
|
|
483
|
+
if (key.tab) {
|
|
484
|
+
const after = insertAt(value, caretIndex, " ")
|
|
485
|
+
setValue(after.value)
|
|
486
|
+
setCaretIndex(after.caretIndex)
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Normal character input (including CJK)
|
|
491
|
+
if (input) {
|
|
492
|
+
const after = insertAt(value, caretIndex, cleanInput)
|
|
493
|
+
setValue(after.value)
|
|
494
|
+
setCaretIndex(after.caretIndex)
|
|
495
|
+
}
|
|
496
|
+
}, { isActive: !disabled })
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
同时将 Kitty 监听中的 `setValue((v) => v + "\n")` 改为:
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
if (shiftEnter) setValue((v) => v + "\n")
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
(保持原样——Shift+Enter 在行尾追加,与光标位置无关;若后续需要光标感知可再改,本次不涉及。)
|
|
506
|
+
|
|
507
|
+
注意:原回调中的 `if (key.pageUp || key.pageDown) return` 与 `key.ctrl || key.meta` 已合并进新代码。
|
|
508
|
+
|
|
509
|
+
- [ ] **Step 3: 更新渲染与硬件光标位置**
|
|
510
|
+
|
|
511
|
+
将:
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
const isEmpty = value === ""
|
|
515
|
+
const logicalLines = value.split("\n")
|
|
516
|
+
const isMultiline = logicalLines.length > 1
|
|
517
|
+
const borderColor = disabled ? "gray" : "cyan"
|
|
518
|
+
|
|
519
|
+
// Available cells for text on a single visual row.
|
|
520
|
+
const textWidth = Math.max(8, width - BOX_CHROME - GUTTER_WIDTH)
|
|
521
|
+
|
|
522
|
+
// Visual rows of the value, plus the caret position inside them.
|
|
523
|
+
const rows: string[] = []
|
|
524
|
+
for (const line of logicalLines) rows.push(...wrapByWidth(line, textWidth))
|
|
525
|
+
let caretColumnOffset = displayWidth(rows[rows.length - 1]!)
|
|
526
|
+
// The caret block itself needs a cell; if the row is full it moves down.
|
|
527
|
+
if (caretColumnOffset + 1 > textWidth) {
|
|
528
|
+
rows.push("")
|
|
529
|
+
caretColumnOffset = 0
|
|
530
|
+
}
|
|
531
|
+
const caretRow = rows.length - 1
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
替换为:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
const isEmpty = value === ""
|
|
538
|
+
const logicalLines = value.split("\n")
|
|
539
|
+
const isMultiline = logicalLines.length > 1
|
|
540
|
+
const borderColor = disabled ? "gray" : "cyan"
|
|
541
|
+
|
|
542
|
+
// Available cells for text on a single visual row.
|
|
543
|
+
const textWidth = Math.max(8, width - BOX_CHROME - GUTTER_WIDTH)
|
|
544
|
+
|
|
545
|
+
// Visual rows of the value, plus the caret position inside them.
|
|
546
|
+
const rows = visualRows(value, textWidth)
|
|
547
|
+
const clampedCaret = Math.min(caretIndex, Array.from(value).length)
|
|
548
|
+
// Locate the caret's visual row and its column offset inside that row.
|
|
549
|
+
let caretRowIndex = rows.length - 1
|
|
550
|
+
let caretColumnOffset = displayWidth(rows[caretRowIndex]!.text)
|
|
551
|
+
for (let i = 0; i < rows.length; i++) {
|
|
552
|
+
const row = rows[i]!
|
|
553
|
+
const end = row.startIndex + Array.from(row.text).length
|
|
554
|
+
if (clampedCaret >= row.startIndex && clampedCaret <= end) {
|
|
555
|
+
caretRowIndex = i
|
|
556
|
+
caretColumnOffset = displayWidth(Array.from(row.text).slice(0, clampedCaret - row.startIndex).join(""))
|
|
557
|
+
break
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// A caret at the end of a full row has no cell left — move it to the start
|
|
561
|
+
// of the next visual row (pushing an empty one when it is the last row).
|
|
562
|
+
if (caretColumnOffset >= textWidth) {
|
|
563
|
+
if (caretRowIndex === rows.length - 1) {
|
|
564
|
+
rows.push({
|
|
565
|
+
text: "",
|
|
566
|
+
startIndex: rows[caretRowIndex]!.startIndex + Array.from(rows[caretRowIndex]!.text).length,
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
caretRowIndex++
|
|
570
|
+
caretColumnOffset = 0
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
将 `setCaretPosition` 段:
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
setCaretPosition({
|
|
578
|
+
rowsAbove: 1 /* bottom border */ + hintRows + menuRows,
|
|
579
|
+
column: TEXT_START_COLUMN + caretColumnOffset,
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
替换为:
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
const rowsBelowCaret = rows.length - 1 - caretRowIndex
|
|
587
|
+
setCaretPosition({
|
|
588
|
+
rowsAbove: rowsBelowCaret + 1 /* bottom border */ + hintRows + menuRows,
|
|
589
|
+
column: TEXT_START_COLUMN + caretColumnOffset,
|
|
590
|
+
})
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
将 `rows.map((row, i) => ...)` 渲染段:
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
rows.map((row, i) => (
|
|
597
|
+
<Box key={i}>
|
|
598
|
+
<Text color={disabled ? "gray" : "cyan"}>{i === 0 ? GUTTER : " "}</Text>
|
|
599
|
+
<Text wrap="truncate-end">{i === caretRow ? `${row}█` : row}</Text>
|
|
600
|
+
</Box>
|
|
601
|
+
))
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
替换为:
|
|
605
|
+
|
|
606
|
+
```tsx
|
|
607
|
+
rows.map((row, i) => {
|
|
608
|
+
const chars = Array.from(row.text)
|
|
609
|
+
const rowEnd = row.startIndex + chars.length
|
|
610
|
+
const caretHere = clampedCaret >= row.startIndex && clampedCaret <= rowEnd
|
|
611
|
+
const offset = caretHere ? clampedCaret - row.startIndex : chars.length
|
|
612
|
+
const before = chars.slice(0, offset).join("")
|
|
613
|
+
const after = chars.slice(offset).join("")
|
|
614
|
+
return (
|
|
615
|
+
<Box key={i}>
|
|
616
|
+
<Text color={disabled ? "gray" : "cyan"}>{i === 0 ? GUTTER : " "}</Text>
|
|
617
|
+
<Text wrap="truncate-end">
|
|
618
|
+
{before}
|
|
619
|
+
{caretHere && <Text inverse> </Text>}
|
|
620
|
+
{after}
|
|
621
|
+
</Text>
|
|
622
|
+
</Box>
|
|
623
|
+
)
|
|
624
|
+
})
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
注意:光标用 `<Text inverse> </Text>`(反色空格,1 个单元格)替代原行尾 `█`,与硬件光标位置一致;空值时 placeholder 分支保持原样,硬件光标由 setCaretPosition 停在文本起始列。
|
|
628
|
+
|
|
629
|
+
- [ ] **Step 4: 类型检查与测试**
|
|
630
|
+
|
|
631
|
+
Run: `./node_modules/.bin/tsc --noEmit`
|
|
632
|
+
Expected: 无错误
|
|
633
|
+
|
|
634
|
+
Run: `bun test`
|
|
635
|
+
Expected: 全部通过(现有 153 tests 不受影响)
|
|
636
|
+
|
|
637
|
+
- [ ] **Step 5: 手动验证(可选)**
|
|
638
|
+
|
|
639
|
+
Run: `bun run src/cli.ts`,输入文本后按 ← → 移动光标;把光标移到中间再输入/退格,观察插入位置正确;退格删除光标前字符。
|
|
640
|
+
|
|
641
|
+
- [ ] **Step 6: 提交**
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
git add src/tui/InputBar.tsx
|
|
645
|
+
git commit -m "feat: 输入框光标移动与光标处编辑"
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Task 5: 鼠标点击定位
|
|
651
|
+
|
|
652
|
+
**Files:**
|
|
653
|
+
- Modify: `src/tui/InputBar.tsx`
|
|
654
|
+
- Modify: `src/tui/App.tsx`
|
|
655
|
+
|
|
656
|
+
- [ ] **Step 1: App 传入 terminalRows**
|
|
657
|
+
|
|
658
|
+
在 `App.tsx` 的 InputBar 调用处加 `terminalRows={terminalRows}`:
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
<InputBar
|
|
662
|
+
onSubmit={onSubmit}
|
|
663
|
+
disabled={initialState.isRunning}
|
|
664
|
+
placeholder={initialState.isRunning ? "按 Esc 取消运行..." : "输入消息... (Ctrl+J 换行)"}
|
|
665
|
+
columns={columns}
|
|
666
|
+
terminalRows={terminalRows}
|
|
667
|
+
/>
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
在 `InputBarProps` 接口(InputBar.tsx)加:
|
|
671
|
+
|
|
672
|
+
```tsx
|
|
673
|
+
/** Terminal height (rows), kept fresh by the App-level resize listener. */
|
|
674
|
+
terminalRows: number
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
并在组件解构处加 `terminalRows`。
|
|
678
|
+
|
|
679
|
+
- [ ] **Step 2: InputBar 启用鼠标协议并解析点击**
|
|
680
|
+
|
|
681
|
+
InputBar.tsx 顶部 import 改为:
|
|
682
|
+
|
|
683
|
+
```tsx
|
|
684
|
+
import { Box, Text, useInput, useStdin, useStdout } from "ink"
|
|
685
|
+
import { setCaretPosition } from "./caret.js"
|
|
686
|
+
import { displayWidth } from "./text-width.js"
|
|
687
|
+
import { visualRows, moveCaretHorizontal, insertAt, backspaceAt, deleteAt, caretIndexFromClick } from "./caret-pos.js"
|
|
688
|
+
import { MOUSE_ENABLE, MOUSE_DISABLE, parseSgrMouse } from "./mouse.js"
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
在 `const { stdin } = useStdin()` 后加 `const { stdout } = useStdout()`。
|
|
692
|
+
|
|
693
|
+
在 `const width = columns || 80` 后加输入框几何计算(依赖渲染期值,放组件体内、`rows` 计算之后;下面的 effect 通过依赖数组拿到最新值):
|
|
694
|
+
|
|
695
|
+
```tsx
|
|
696
|
+
// Input bar sits at the bottom of App's footer: its content area starts
|
|
697
|
+
// `terminalRows` rows up minus the bar's own height.
|
|
698
|
+
const inputTopRow = terminalRows - rows.length - 2 /* border */ - hintRows - menuRows + 1
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
将 Kitty 监听 effect 替换为(同时解析鼠标;依赖数组包含几何值,每次输入变化重建监听,成本可忽略):
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
useEffect(() => {
|
|
705
|
+
if (!stdin || disabled) return
|
|
706
|
+
|
|
707
|
+
const handleData = (data: Buffer) => {
|
|
708
|
+
const chunk = data.toString("utf-8")
|
|
709
|
+
const { buffer, shiftEnter } = accumulateKittyInput(kittyBuffer.current, chunk)
|
|
710
|
+
kittyBuffer.current = buffer
|
|
711
|
+
if (shiftEnter) setValue((v) => v + "\n")
|
|
712
|
+
const click = parseSgrMouse(chunk)
|
|
713
|
+
if (click) {
|
|
714
|
+
const contentRow = click.row - inputTopRow
|
|
715
|
+
if (contentRow >= 0 && contentRow < rows.length) {
|
|
716
|
+
setCaretIndex(caretIndexFromClick(rows, contentRow, click.col, TEXT_START_COLUMN))
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
stdin.on("data", handleData)
|
|
722
|
+
return () => { stdin.off("data", handleData) }
|
|
723
|
+
}, [stdin, disabled, rows, inputTopRow])
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
新增鼠标协议启用的 effect(放在 Kitty effect 之后):
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
// Enable the SGR mouse protocol so clicks on the input bar can position the
|
|
730
|
+
// caret. Events arrive on raw stdin; parseSgrMouse picks them out of the
|
|
731
|
+
// same chunk stream as the Kitty sequence. Terminals without mouse support
|
|
732
|
+
// simply never send events — keyboard navigation still works.
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
stdout.write(MOUSE_ENABLE)
|
|
735
|
+
return () => { stdout.write(MOUSE_DISABLE) }
|
|
736
|
+
}, [stdout])
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
注意:依赖数组包含 `rows`(每次渲染新数组)→ Kitty effect 每次按键重建。`accumulateKittyInput` 的 `kittyBuffer` 是 ref,不受影响。若担心频繁重建,可将 `rows`/`inputTopRow` 改用 ref(见下方"替代实现"注释)——保持简单,直接依赖即可。
|
|
740
|
+
|
|
741
|
+
- [ ] **Step 3: 类型检查与测试**
|
|
742
|
+
|
|
743
|
+
Run: `./node_modules/.bin/tsc --noEmit`
|
|
744
|
+
Expected: 无错误
|
|
745
|
+
|
|
746
|
+
Run: `bun test`
|
|
747
|
+
Expected: 全部通过
|
|
748
|
+
|
|
749
|
+
- [ ] **Step 4: 手动验证**
|
|
750
|
+
|
|
751
|
+
Run: `bun run src/cli.ts`:
|
|
752
|
+
1. 输入多行文本(含中文),鼠标点击输入框任意位置,光标应跳到点击处
|
|
753
|
+
2. 点击中间后输入/退格,在光标处生效
|
|
754
|
+
3. 点击输入框上方区域不应移动光标
|
|
755
|
+
4. 退出 TUI(Ctrl+C / /exit)后终端鼠标恢复默认(可正常选中文本)
|
|
756
|
+
|
|
757
|
+
- [ ] **Step 5: 提交**
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
git add src/tui/InputBar.tsx src/tui/App.tsx
|
|
761
|
+
git commit -m "feat: 输入框鼠标点击定位光标"
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## Task 6: 收尾验证
|
|
767
|
+
|
|
768
|
+
- [ ] **Step 1: 全量验证**
|
|
769
|
+
|
|
770
|
+
Run: `bun test && ./node_modules/.bin/tsc --noEmit`
|
|
771
|
+
Expected: 全部通过、无类型错误
|
|
772
|
+
|
|
773
|
+
- [ ] **Step 2: 更新文档(如涉及)**
|
|
774
|
+
|
|
775
|
+
检查 `packages/min-agent/README.md` 与交互模式 `/help` 是否描述输入框交互;若描述与现状不符,同步更新。
|
|
776
|
+
|
|
777
|
+
- [ ] **Step 3: 提交**
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
git add -A
|
|
781
|
+
git commit -m "chore: 输入框光标编辑收尾"
|
|
782
|
+
```
|