@minko-ai/minko 0.1.0 → 0.1.1

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.
Files changed (189) hide show
  1. package/README.md +577 -2
  2. package/assets/mascot-preview.png +0 -0
  3. package/assets/mascot-preview.txt +46 -0
  4. package/assets/mascot-source.png +0 -0
  5. package/assets/skills/README.md +21 -0
  6. package/assets/skills/brainstorming/LICENSE +21 -0
  7. package/assets/skills/brainstorming/SKILL.md +316 -0
  8. package/assets/skills/buddhist-method/LICENSE +21 -0
  9. package/assets/skills/buddhist-method/SKILL.md +137 -0
  10. package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
  11. package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
  12. package/assets/skills/code-humanizer/LICENSE +21 -0
  13. package/assets/skills/code-humanizer/SKILL.md +174 -0
  14. package/assets/skills/code-review/LICENSE +21 -0
  15. package/assets/skills/code-review/SKILL.md +232 -0
  16. package/assets/skills/code-review/reference/angular.md +768 -0
  17. package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
  18. package/assets/skills/code-review/reference/c.md +890 -0
  19. package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
  20. package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
  21. package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
  22. package/assets/skills/code-review/reference/cpp.md +893 -0
  23. package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  24. package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  25. package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  26. package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  27. package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  28. package/assets/skills/code-review/reference/csharp.md +525 -0
  29. package/assets/skills/code-review/reference/css-less-sass.md +661 -0
  30. package/assets/skills/code-review/reference/dart.md +670 -0
  31. package/assets/skills/code-review/reference/django.md +985 -0
  32. package/assets/skills/code-review/reference/fastapi.md +580 -0
  33. package/assets/skills/code-review/reference/go.md +993 -0
  34. package/assets/skills/code-review/reference/java.md +409 -0
  35. package/assets/skills/code-review/reference/java8.md +586 -0
  36. package/assets/skills/code-review/reference/kotlin.md +1018 -0
  37. package/assets/skills/code-review/reference/nestjs.md +593 -0
  38. package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
  39. package/assets/skills/code-review/reference/php.md +684 -0
  40. package/assets/skills/code-review/reference/python.md +1073 -0
  41. package/assets/skills/code-review/reference/qt.md +757 -0
  42. package/assets/skills/code-review/reference/react.md +871 -0
  43. package/assets/skills/code-review/reference/ruby.md +964 -0
  44. package/assets/skills/code-review/reference/rust.md +846 -0
  45. package/assets/skills/code-review/reference/security-review-guide.md +494 -0
  46. package/assets/skills/code-review/reference/svelte.md +1064 -0
  47. package/assets/skills/code-review/reference/swift.md +936 -0
  48. package/assets/skills/code-review/reference/typescript.md +1016 -0
  49. package/assets/skills/code-review/reference/vue.md +924 -0
  50. package/assets/skills/code-review/reference/zig.md +440 -0
  51. package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
  52. package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
  53. package/assets/skills/five-whys/LICENSE +21 -0
  54. package/assets/skills/five-whys/SKILL.md +247 -0
  55. package/assets/skills/five-whys/examples/performance-regression.md +161 -0
  56. package/assets/skills/five-whys/examples/production-incident.md +134 -0
  57. package/assets/skills/five-whys/references/software-patterns.md +284 -0
  58. package/assets/skills/five-whys/references/toyota-origins.md +257 -0
  59. package/assets/skills/systematic-debugging/LICENSE +21 -0
  60. package/assets/skills/systematic-debugging/SKILL.md +283 -0
  61. package/assets/skills/test-driven-development/LICENSE +21 -0
  62. package/assets/skills/test-driven-development/SKILL.md +330 -0
  63. package/assets/skills/verification-before-completion/LICENSE +21 -0
  64. package/assets/skills/verification-before-completion/SKILL.md +120 -0
  65. package/assets/skills/vibe-sec/LICENSE +201 -0
  66. package/assets/skills/vibe-sec/SKILL.md +758 -0
  67. package/assets/skills/writing-plans/LICENSE +21 -0
  68. package/assets/skills/writing-plans/SKILL.md +204 -0
  69. package/bin/minko.js +2 -2
  70. package/dist/agent/approvalTokens.js +224 -0
  71. package/dist/agent/branchLock.js +315 -0
  72. package/dist/agent/compact.js +182 -0
  73. package/dist/agent/context.js +183 -0
  74. package/dist/agent/cron.js +326 -0
  75. package/dist/agent/hooks.js +211 -0
  76. package/dist/agent/lanes.js +511 -0
  77. package/dist/agent/loop.js +337 -0
  78. package/dist/agent/memory.js +159 -0
  79. package/dist/agent/permissions.js +71 -0
  80. package/dist/agent/plan.js +118 -0
  81. package/dist/agent/prompt.js +120 -0
  82. package/dist/agent/subagent.js +67 -0
  83. package/dist/agent/tasks.js +449 -0
  84. package/dist/agent/transcript.js +62 -0
  85. package/dist/agent/workerStore.js +172 -0
  86. package/dist/agent/workerTypes.js +1 -0
  87. package/dist/agent/workers.js +503 -0
  88. package/dist/cli/app.js +261 -0
  89. package/dist/cli/argv.js +206 -0
  90. package/dist/cli/commandsCommand.js +12 -0
  91. package/dist/cli/configCommand.js +170 -0
  92. package/dist/cli/doctorCommand.js +112 -0
  93. package/dist/cli/exportCommand.js +85 -0
  94. package/dist/cli/forkCommand.js +24 -0
  95. package/dist/cli/linkCommand.js +272 -0
  96. package/dist/cli/loginCommand.js +69 -0
  97. package/dist/cli/mcpLoginCommand.js +68 -0
  98. package/dist/cli/pluginCommand.js +113 -0
  99. package/dist/cli/powerCommand.js +174 -0
  100. package/dist/cli/promptFooter.js +74 -0
  101. package/dist/cli/relayCommand.js +38 -0
  102. package/dist/cli/sandboxCommand.js +31 -0
  103. package/dist/cli/serveCommand.js +104 -0
  104. package/dist/cli/statusCommand.js +120 -0
  105. package/dist/cli/tui.js +1104 -0
  106. package/dist/commands/registry.js +99 -0
  107. package/dist/config/paths.js +81 -0
  108. package/dist/config/store.js +605 -0
  109. package/dist/index.js +199 -0
  110. package/dist/link/protocol.js +159 -0
  111. package/dist/link/relay.js +184 -0
  112. package/dist/link/server.js +566 -0
  113. package/dist/link/tunnel.js +90 -0
  114. package/dist/lsp/client.js +340 -0
  115. package/dist/lsp/tool.js +177 -0
  116. package/dist/mcp/client.js +469 -0
  117. package/dist/mcp/httpClient.js +191 -0
  118. package/dist/mcp/server.js +206 -0
  119. package/dist/mcp/tools.js +173 -0
  120. package/dist/oauth/device.js +111 -0
  121. package/dist/oauth/store.js +56 -0
  122. package/dist/plugins/loader.js +291 -0
  123. package/dist/plugins/market.js +98 -0
  124. package/dist/providers/anthropic.js +279 -0
  125. package/dist/providers/factory.js +109 -0
  126. package/dist/providers/openai.js +182 -0
  127. package/dist/providers/retry.js +95 -0
  128. package/dist/providers/sse.js +67 -0
  129. package/dist/providers/types.js +10 -0
  130. package/dist/rag/embeddings.js +171 -0
  131. package/dist/rag/index.js +373 -0
  132. package/dist/rag/server.js +167 -0
  133. package/dist/rag/tool.js +148 -0
  134. package/dist/sandbox/index.js +55 -0
  135. package/dist/sdk/server.js +150 -0
  136. package/dist/sdk/session.js +104 -0
  137. package/dist/session/liveness.js +148 -0
  138. package/dist/session/recorder.js +49 -0
  139. package/dist/session/store.js +171 -0
  140. package/dist/skills/index.js +428 -0
  141. package/dist/theme.js +30 -0
  142. package/dist/tools/fs.js +195 -0
  143. package/dist/tools/notebook.js +109 -0
  144. package/dist/tools/registry.js +32 -0
  145. package/dist/tools/search.js +130 -0
  146. package/dist/tools/shell.js +200 -0
  147. package/dist/tools/structured.js +83 -0
  148. package/dist/tools/types.js +1 -0
  149. package/dist/ui/ansi.js +135 -0
  150. package/dist/ui/banner.js +103 -0
  151. package/dist/ui/input.js +49 -0
  152. package/dist/ui/inputModel.js +113 -0
  153. package/dist/ui/keyboard.js +142 -0
  154. package/dist/ui/mascot.js +34 -0
  155. package/dist/ui/mascotData.js +96 -0
  156. package/dist/ui/md.js +183 -0
  157. package/dist/ui/screen.js +49 -0
  158. package/dist/ui/spinner.js +52 -0
  159. package/dist/ui/transcript.js +287 -0
  160. package/dist/util/clipboard.js +53 -0
  161. package/dist/util/errors.js +50 -0
  162. package/dist/util/version.js +12 -0
  163. package/dist/web/fetch.js +168 -0
  164. package/dist/web/search.js +97 -0
  165. package/package.json +45 -36
  166. package/src/agent/loop.ts +0 -159
  167. package/src/agent/prompt.ts +0 -45
  168. package/src/cli/app.ts +0 -396
  169. package/src/cli/argv.ts +0 -139
  170. package/src/cli/configCommand.ts +0 -175
  171. package/src/config/paths.ts +0 -20
  172. package/src/config/store.ts +0 -149
  173. package/src/index.ts +0 -120
  174. package/src/providers/anthropic.ts +0 -215
  175. package/src/providers/openai.ts +0 -158
  176. package/src/providers/sse.ts +0 -57
  177. package/src/providers/types.ts +0 -59
  178. package/src/session/store.ts +0 -125
  179. package/src/theme.ts +0 -41
  180. package/src/tools/fs.ts +0 -143
  181. package/src/tools/registry.ts +0 -19
  182. package/src/tools/shell.ts +0 -120
  183. package/src/tools/types.ts +0 -26
  184. package/src/ui/ansi.ts +0 -110
  185. package/src/ui/banner.ts +0 -39
  186. package/src/ui/input.ts +0 -54
  187. package/src/ui/md.ts +0 -160
  188. package/src/ui/spinner.ts +0 -58
  189. package/src/util/errors.ts +0 -48
@@ -0,0 +1,1104 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Painter, stripAnsi, truncateVisible, visibleWidth } from "../ui/ansi.js";
4
+ import { Screen } from "../ui/screen.js";
5
+ import { KeyParser } from "../ui/keyboard.js";
6
+ import { InputModel, wrapInput } from "../ui/inputModel.js";
7
+ import { highlightColumns, renderEntries, selectionByCells, selectionText, Viewport } from "../ui/transcript.js";
8
+ import { copyToClipboard } from "../util/clipboard.js";
9
+ import { bannerLines, compactBanner } from "../ui/banner.js";
10
+ import { mascotFrames } from "../ui/mascot.js";
11
+ import { StreamRenderer } from "../ui/md.js";
12
+ import { runAgent } from "../agent/loop.js";
13
+ import { compactHistory } from "../agent/compact.js";
14
+ import { loadProjectContext } from "../agent/context.js";
15
+ import { createSubagentTool } from "../agent/subagent.js";
16
+ import { createPlanTools, renderTodos } from "../agent/plan.js";
17
+ import { createMemoryTools, defaultMemoryDir, listMemoryIndex, loadMemoryContext, } from "../agent/memory.js";
18
+ import { WorkerRegistry, createWorkerTools } from "../agent/workers.js";
19
+ import { LaneRegistry, createLaneTools } from "../agent/lanes.js";
20
+ import { TaskRegistry, createTaskTools } from "../agent/tasks.js";
21
+ import { CronScheduler, createCronTools } from "../agent/cron.js";
22
+ import { BranchLockStore, createBranchTools } from "../agent/branchLock.js";
23
+ import { ApprovalTokenStore, createApprovalTools } from "../agent/approvalTokens.js";
24
+ import { closeLspClients } from "../lsp/tool.js";
25
+ import { loadPlugins, listPlugins, mergeHooks } from "../plugins/loader.js";
26
+ import { listSkills } from "../skills/index.js";
27
+ import { dockerAvailable, resolveSandbox } from "../sandbox/index.js";
28
+ import { commandHelp, findCommand, parseSlash, suggestCommand } from "../commands/registry.js";
29
+ import { buildStatus, formatStatusPlain } from "./statusCommand.js";
30
+ import { sessionToMarkdown } from "./exportCommand.js";
31
+ import { systemPrompt } from "../agent/prompt.js";
32
+ import { ALL_TOOLS } from "../tools/registry.js";
33
+ import { titleFromMessages } from "../session/store.js";
34
+ import { TurnRecorder } from "../session/recorder.js";
35
+ import { SessionHeartbeat } from "../session/liveness.js";
36
+ import { loadConfig } from "../config/store.js";
37
+ import { friendlyError } from "../util/errors.js";
38
+ import { PURPLE } from "../theme.js";
39
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
40
+ /** Full-screen conversation UI: scrolling transcript above a fixed input box. */
41
+ export class Tui {
42
+ screen = new Screen();
43
+ painter = new Painter();
44
+ parser = new KeyParser();
45
+ input = new InputModel();
46
+ viewport = new Viewport();
47
+ entries = [];
48
+ options;
49
+ provider;
50
+ model;
51
+ meta;
52
+ history;
53
+ running = null;
54
+ status = '';
55
+ statusStarted = 0;
56
+ spinnerFrame = 0;
57
+ approval = null;
58
+ copy = null;
59
+ selection = null;
60
+ expandDetails = false;
61
+ timer = null;
62
+ dirty = true;
63
+ closed = false;
64
+ finish = () => { };
65
+ agentTool;
66
+ planTools;
67
+ memoryDir = defaultMemoryDir();
68
+ memoryTools;
69
+ heartbeat;
70
+ workers;
71
+ workerTools;
72
+ lanes;
73
+ laneTools;
74
+ taskTools;
75
+ tasks;
76
+ crons;
77
+ cronTools;
78
+ branchTools;
79
+ approvals;
80
+ approvalTools;
81
+ pluginTools;
82
+ pluginErrors;
83
+ effectiveHooks;
84
+ usageTotals = { input: 0, output: 0, cached: 0 };
85
+ constructor(options) {
86
+ this.options = options;
87
+ this.provider = options.provider;
88
+ this.model = options.model;
89
+ this.meta = options.session.meta;
90
+ this.history = options.session.history;
91
+ this.agentTool = createSubagentTool({
92
+ getProvider: () => this.provider,
93
+ getConfig: () => this.options.config,
94
+ });
95
+ this.planTools = createPlanTools(this.meta);
96
+ this.memoryTools = createMemoryTools(this.memoryDir);
97
+ this.heartbeat = new SessionHeartbeat(this.options.store.dir, this.meta);
98
+ this.approvals = new ApprovalTokenStore();
99
+ this.workers = new WorkerRegistry({
100
+ getProvider: () => this.provider,
101
+ getConfig: () => this.options.config,
102
+ cwd: this.options.cwd,
103
+ approvalStore: this.approvals,
104
+ approve: async (request) => {
105
+ if (this.approval) {
106
+ return false;
107
+ }
108
+ return await this.askApproval(request);
109
+ },
110
+ });
111
+ this.workerTools = createWorkerTools(this.workers);
112
+ const branchLocks = new BranchLockStore();
113
+ this.branchTools = createBranchTools(branchLocks);
114
+ this.lanes = new LaneRegistry({
115
+ getProvider: () => this.provider,
116
+ getConfig: () => this.options.config,
117
+ cwd: this.options.cwd,
118
+ lockStore: branchLocks,
119
+ approvalStore: this.approvals,
120
+ approve: async (request) => {
121
+ if (this.approval) {
122
+ return false;
123
+ }
124
+ return await this.askApproval(request);
125
+ },
126
+ });
127
+ this.laneTools = createLaneTools(this.lanes);
128
+ this.tasks = new TaskRegistry({ workers: this.workers });
129
+ this.taskTools = createTaskTools(this.tasks);
130
+ this.crons = new CronScheduler({ workers: this.workers });
131
+ this.cronTools = createCronTools(this.crons);
132
+ this.approvalTools = createApprovalTools(this.approvals);
133
+ const pluginLoad = loadPlugins(undefined, options.config?.plugins.disabled ?? []);
134
+ this.pluginTools = pluginLoad.plugins.flatMap((plugin) => plugin.tools);
135
+ this.pluginErrors = pluginLoad.errors;
136
+ this.effectiveHooks = mergeHooks(options.config?.hooks ?? options.toolHooks, ...pluginLoad.plugins.map((plugin) => plugin.hooks));
137
+ }
138
+ async run() {
139
+ this.screen.enter();
140
+ this.heartbeat.start();
141
+ this.crons.start();
142
+ this.pushBanner();
143
+ for (const error of this.pluginErrors) {
144
+ this.entries.push({ kind: 'stats', text: '插件加载失败:' + error });
145
+ }
146
+ if (this.options.store.fallback) {
147
+ this.entries.push({
148
+ kind: 'stats',
149
+ text: '主目录不可写,会话改存到 ' + this.options.store.dir,
150
+ });
151
+ }
152
+ this.attachInput();
153
+ this.timer = setInterval(() => this.tick(), 80);
154
+ this.render();
155
+ const promise = new Promise((resolve) => { this.finish = resolve; });
156
+ if (this.options.initialPrompt && this.options.initialPrompt.trim().length > 0) {
157
+ this.submitText(this.options.initialPrompt.trim());
158
+ }
159
+ return await promise;
160
+ }
161
+ pushBanner() {
162
+ const info = {
163
+ model: this.model,
164
+ cwd: this.options.cwd,
165
+ session: this.meta.id,
166
+ continued: this.history.length > 0,
167
+ version: this.options.version,
168
+ };
169
+ const rows = this.screen.rows;
170
+ if (rows < 12) {
171
+ this.entries.push({ kind: 'banner', text: compactBanner(this.painter, info).join('\n') });
172
+ return;
173
+ }
174
+ const frames = rows < 18 || process.env.MINKO_MASCOT === '0'
175
+ ? []
176
+ : mascotFrames(this.painter);
177
+ const lines = bannerLines(this.painter, info, frames.length > 0 ? frames[0] : null);
178
+ // Breathing room between the mascot and the conversation below it.
179
+ this.entries.push({ kind: 'banner', text: [...lines, '', ''].join('\n') });
180
+ }
181
+ attachInput() {
182
+ if (process.stdin.isTTY) {
183
+ process.stdin.setRawMode(true);
184
+ }
185
+ process.stdin.resume();
186
+ process.stdin.setEncoding('utf8');
187
+ process.stdin.on('data', (chunk) => {
188
+ for (const key of this.parser.push(chunk)) {
189
+ this.handleKey(key);
190
+ }
191
+ });
192
+ process.stdout.on('resize', () => { this.dirty = true; });
193
+ }
194
+ detachInput() {
195
+ process.stdin.removeAllListeners('data');
196
+ process.stdout.removeAllListeners('resize');
197
+ if (process.stdin.isTTY) {
198
+ process.stdin.setRawMode(false);
199
+ }
200
+ process.stdin.pause();
201
+ }
202
+ close(code) {
203
+ if (this.closed) {
204
+ return;
205
+ }
206
+ this.closed = true;
207
+ this.heartbeat.stop();
208
+ this.crons.stop();
209
+ this.workers.stopAll();
210
+ this.lanes.closeAll();
211
+ void closeLspClients();
212
+ if (this.timer) {
213
+ clearInterval(this.timer);
214
+ this.timer = null;
215
+ }
216
+ this.detachInput();
217
+ this.screen.leave();
218
+ this.finish(code);
219
+ }
220
+ tick() {
221
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
222
+ if (this.running || this.approval || this.dirty) {
223
+ this.dirty = false;
224
+ this.render();
225
+ }
226
+ }
227
+ push(entry) {
228
+ this.entries.push(entry);
229
+ if (!this.viewport.atBottom) {
230
+ this.viewport.scroll += 1;
231
+ }
232
+ this.dirty = true;
233
+ }
234
+ handleKey(key) {
235
+ if (key.type === 'mouse') {
236
+ this.handleMouse(key);
237
+ return;
238
+ }
239
+ if (this.approval) {
240
+ this.handleApprovalKey(key);
241
+ return;
242
+ }
243
+ if (this.copy) {
244
+ this.handleCopyKey(key);
245
+ return;
246
+ }
247
+ switch (key.type) {
248
+ case 'ctrl':
249
+ if (key.key === 'c') {
250
+ if (this.selection) {
251
+ this.copySelectionCells();
252
+ return;
253
+ }
254
+ if (this.running) {
255
+ this.running.abort();
256
+ }
257
+ else if (!this.input.empty) {
258
+ this.input.clear();
259
+ this.dirty = true;
260
+ }
261
+ else {
262
+ this.close(0);
263
+ }
264
+ return;
265
+ }
266
+ if (key.key === 'g') {
267
+ this.startCopy();
268
+ return;
269
+ }
270
+ if (key.key === 'd') {
271
+ if (this.input.empty) {
272
+ this.close(0);
273
+ }
274
+ return;
275
+ }
276
+ if (key.key === 'a') {
277
+ this.input.home();
278
+ this.dirty = true;
279
+ return;
280
+ }
281
+ if (key.key === 'e') {
282
+ this.input.end();
283
+ this.dirty = true;
284
+ return;
285
+ }
286
+ if (key.key === 'u') {
287
+ this.input.clear();
288
+ this.dirty = true;
289
+ return;
290
+ }
291
+ if (key.key === 'o') {
292
+ this.expandDetails = !this.expandDetails;
293
+ this.dirty = true;
294
+ return;
295
+ }
296
+ return;
297
+ case 'enter':
298
+ this.submit();
299
+ return;
300
+ case 'backspace':
301
+ this.input.backspace();
302
+ this.dirty = true;
303
+ return;
304
+ case 'delete':
305
+ this.input.deleteForward();
306
+ this.dirty = true;
307
+ return;
308
+ case 'left':
309
+ this.input.move(-1);
310
+ this.dirty = true;
311
+ return;
312
+ case 'right':
313
+ this.input.move(1);
314
+ this.dirty = true;
315
+ return;
316
+ case 'home':
317
+ this.input.home();
318
+ this.dirty = true;
319
+ return;
320
+ case 'end':
321
+ this.input.end();
322
+ this.dirty = true;
323
+ return;
324
+ case 'up':
325
+ this.input.previous();
326
+ this.dirty = true;
327
+ return;
328
+ case 'down':
329
+ this.input.next();
330
+ this.dirty = true;
331
+ return;
332
+ case 'pageup':
333
+ this.viewport.scrollBy(this.viewportHeight(), this.totalLines(), this.viewportHeight());
334
+ this.dirty = true;
335
+ return;
336
+ case 'pagedown':
337
+ this.viewport.scrollBy(-this.viewportHeight(), this.totalLines(), this.viewportHeight());
338
+ this.dirty = true;
339
+ return;
340
+ case 'wheel':
341
+ this.viewport.scrollBy(key.delta, this.totalLines(), this.viewportHeight());
342
+ this.dirty = true;
343
+ return;
344
+ case 'paste':
345
+ this.input.insert(key.text.replace(/\r?\n/g, ' '));
346
+ this.dirty = true;
347
+ return;
348
+ case 'text':
349
+ this.input.insert(key.text);
350
+ this.dirty = true;
351
+ return;
352
+ default:
353
+ return;
354
+ }
355
+ }
356
+ handleMouse(key) {
357
+ const total = this.totalLines();
358
+ if (total === 0) {
359
+ return;
360
+ }
361
+ const height = this.viewportHeight();
362
+ const start = Math.max(0, total - height - this.viewport.scroll);
363
+ const line = Math.max(0, Math.min(total - 1, start + key.row - 1));
364
+ const col = Math.max(0, key.col - 1);
365
+ if (key.kind === 'down') {
366
+ this.copy = null;
367
+ this.selection = { anchor: { line, col }, cursor: { line, col }, dragging: true };
368
+ this.dirty = true;
369
+ return;
370
+ }
371
+ if (key.kind === 'drag' && this.selection) {
372
+ this.selection.cursor = { line, col };
373
+ this.dirty = true;
374
+ return;
375
+ }
376
+ if (key.kind === 'up' && this.selection) {
377
+ // Windows convention: selecting is not copying. Keep the selection until
378
+ // the user presses Ctrl+C.
379
+ this.selection.dragging = false;
380
+ this.dirty = true;
381
+ }
382
+ }
383
+ selectionRangeFor(absolute) {
384
+ if (!this.selection) {
385
+ return null;
386
+ }
387
+ const a = this.selection.anchor;
388
+ const b = this.selection.cursor;
389
+ const startLine = Math.min(a.line, b.line);
390
+ const endLine = Math.max(a.line, b.line);
391
+ if (absolute < startLine || absolute > endLine) {
392
+ return null;
393
+ }
394
+ const first = a.line <= b.line ? a : b;
395
+ const last = a.line <= b.line ? b : a;
396
+ const from = absolute === first.line ? first.col : 0;
397
+ const to = absolute === last.line ? last.col : Number.MAX_SAFE_INTEGER;
398
+ return [from, to];
399
+ }
400
+ copySelectionCells() {
401
+ if (!this.selection) {
402
+ return;
403
+ }
404
+ const { anchor, cursor } = this.selection;
405
+ this.selection = null;
406
+ const lines = renderEntries(this.entries, this.screen.columns - 2, this.painter, this.spinnerFrame, this.expandDetails);
407
+ const text = selectionByCells(lines, anchor, cursor);
408
+ if (text.trim().length > 0) {
409
+ copyToClipboard(text);
410
+ }
411
+ this.dirty = true;
412
+ }
413
+ startCopy() {
414
+ const total = this.totalLines();
415
+ this.copy = { cursor: Math.max(0, total - 1), anchor: null };
416
+ this.dirty = true;
417
+ }
418
+ handleCopyKey(key) {
419
+ if (!this.copy) {
420
+ return;
421
+ }
422
+ const total = this.totalLines();
423
+ switch (key.type) {
424
+ case 'escape':
425
+ this.copy = null;
426
+ this.dirty = true;
427
+ return;
428
+ case 'ctrl':
429
+ if (key.key === 'c' || key.key === 'g') {
430
+ this.copy = null;
431
+ this.dirty = true;
432
+ }
433
+ return;
434
+ case 'enter':
435
+ this.copySelection();
436
+ return;
437
+ case 'up':
438
+ this.copy.cursor = Math.max(0, this.copy.cursor - 1);
439
+ this.dirty = true;
440
+ return;
441
+ case 'down':
442
+ this.copy.cursor = Math.min(Math.max(0, total - 1), this.copy.cursor + 1);
443
+ this.dirty = true;
444
+ return;
445
+ case 'pageup':
446
+ this.copy.cursor = Math.max(0, this.copy.cursor - this.viewportHeight());
447
+ this.dirty = true;
448
+ return;
449
+ case 'pagedown':
450
+ this.copy.cursor = Math.min(Math.max(0, total - 1), this.copy.cursor + this.viewportHeight());
451
+ this.dirty = true;
452
+ return;
453
+ case 'home':
454
+ this.copy.cursor = 0;
455
+ this.dirty = true;
456
+ return;
457
+ case 'end':
458
+ this.copy.cursor = Math.max(0, total - 1);
459
+ this.dirty = true;
460
+ return;
461
+ case 'text':
462
+ if (key.text.trim() === '') {
463
+ this.copy.anchor = this.copy.anchor === null ? this.copy.cursor : null;
464
+ this.dirty = true;
465
+ }
466
+ return;
467
+ default:
468
+ return;
469
+ }
470
+ }
471
+ copySelection() {
472
+ if (!this.copy) {
473
+ return;
474
+ }
475
+ const lines = renderEntries(this.entries, this.screen.columns - 2, this.painter, this.spinnerFrame, this.expandDetails);
476
+ const anchor = this.copy.anchor ?? this.copy.cursor;
477
+ const text = selectionText(lines, anchor, this.copy.cursor);
478
+ this.copy = null;
479
+ if (text.trim().length > 0) {
480
+ copyToClipboard(text);
481
+ }
482
+ this.dirty = true;
483
+ }
484
+ isSelected(index) {
485
+ if (!this.copy) {
486
+ return false;
487
+ }
488
+ const anchor = this.copy.anchor ?? this.copy.cursor;
489
+ return index >= Math.min(anchor, this.copy.cursor) && index <= Math.max(anchor, this.copy.cursor);
490
+ }
491
+ ensureCopyVisible(height, total) {
492
+ if (!this.copy) {
493
+ return;
494
+ }
495
+ const start = Math.max(0, total - height - this.viewport.scroll);
496
+ const end = start + height - 1;
497
+ if (this.copy.cursor < start) {
498
+ this.viewport.scroll = Math.max(0, total - height - this.copy.cursor);
499
+ }
500
+ else if (this.copy.cursor > end) {
501
+ this.viewport.scroll = Math.max(0, total - height - (this.copy.cursor - height + 1));
502
+ }
503
+ }
504
+ handleApprovalKey(key) {
505
+ const approval = this.approval;
506
+ if (!approval) {
507
+ return;
508
+ }
509
+ const entry = approval.entry;
510
+ const answer = (allowed) => {
511
+ this.approval = null;
512
+ this.heartbeat.setStatus('running');
513
+ entry.state = allowed ? 'allowed' : 'denied';
514
+ this.dirty = true;
515
+ approval.resolve(allowed);
516
+ };
517
+ if (key.type === 'escape') {
518
+ answer(false);
519
+ return;
520
+ }
521
+ if (key.type === 'ctrl' && key.key === 'c') {
522
+ answer(false);
523
+ return;
524
+ }
525
+ if (key.type === 'up') {
526
+ entry.choice = Math.max(0, entry.choice - 1);
527
+ this.dirty = true;
528
+ return;
529
+ }
530
+ if (key.type === 'down') {
531
+ entry.choice = Math.min(2, entry.choice + 1);
532
+ this.dirty = true;
533
+ return;
534
+ }
535
+ if (key.type === 'enter') {
536
+ answer(entry.choice !== 2);
537
+ return;
538
+ }
539
+ if (key.type !== 'text') {
540
+ return;
541
+ }
542
+ const text = key.text.trim().toLowerCase();
543
+ if (text === '1') {
544
+ entry.choice = 0;
545
+ answer(true);
546
+ return;
547
+ }
548
+ if (text === '2' || text === 'a') {
549
+ entry.choice = 1;
550
+ answer(true);
551
+ return;
552
+ }
553
+ if (text === '3' || text === 'n') {
554
+ entry.choice = 2;
555
+ answer(false);
556
+ return;
557
+ }
558
+ if (text === 'y') {
559
+ answer(true);
560
+ }
561
+ }
562
+ submit() {
563
+ if (this.running) {
564
+ return;
565
+ }
566
+ const text = this.input.text.trim();
567
+ if (text.length === 0) {
568
+ return;
569
+ }
570
+ this.input.remember();
571
+ this.input.clear();
572
+ this.submitText(text);
573
+ }
574
+ submitText(text) {
575
+ this.viewport.scroll = 0;
576
+ this.push({ kind: 'user', text });
577
+ if (text.startsWith('/')) {
578
+ this.command(text);
579
+ return;
580
+ }
581
+ void this.runTurn(text);
582
+ }
583
+ command(text) {
584
+ const parsed = parseSlash(text);
585
+ if (!parsed) {
586
+ return;
587
+ }
588
+ const definition = findCommand(parsed.name);
589
+ if (!definition) {
590
+ const suggestion = suggestCommand(parsed.name);
591
+ this.push({
592
+ kind: 'stats',
593
+ text: '未知命令 /' + parsed.name
594
+ + (suggestion ? ',你是想 /' + suggestion + ' 吗?' : ',试试 /help'),
595
+ });
596
+ return;
597
+ }
598
+ const name = definition.name;
599
+ const argument = parsed.argument;
600
+ switch (name) {
601
+ case 'exit':
602
+ this.close(0);
603
+ return;
604
+ case 'help':
605
+ this.push({ kind: 'banner', text: commandHelp() });
606
+ return;
607
+ case 'new':
608
+ this.meta = this.options.store.create(this.options.cwd, this.model);
609
+ this.history = [];
610
+ this.planTools = createPlanTools(this.meta);
611
+ this.heartbeat.retarget(this.meta);
612
+ this.push({ kind: 'stats', text: '新会话 ' + this.meta.id });
613
+ return;
614
+ case 'clear':
615
+ this.history = [];
616
+ this.options.store.save(this.meta, this.history);
617
+ this.push({ kind: 'stats', text: '上下文已清空' });
618
+ return;
619
+ case 'compact': {
620
+ const preserve = /^\d+$/.test(argument)
621
+ ? Math.min(Math.max(Number(argument), 0), 50)
622
+ : 4;
623
+ const result = compactHistory(this.history, preserve);
624
+ if (result.removed === 0) {
625
+ this.push({
626
+ kind: 'stats',
627
+ text: '没有需要压缩的内容(约 ' + formatTokens(result.before) + ' token)',
628
+ });
629
+ return;
630
+ }
631
+ this.history = result.history;
632
+ this.options.store.save(this.meta, this.history);
633
+ this.push({
634
+ kind: 'stats',
635
+ text: '已压缩 ' + result.removed + ' 条较早消息 · 约 '
636
+ + formatTokens(result.before) + ' → ' + formatTokens(result.after) + ' token',
637
+ });
638
+ return;
639
+ }
640
+ case 'plan': {
641
+ this.meta.planMode = !this.meta.planMode;
642
+ this.options.store.save(this.meta, this.history);
643
+ this.push({
644
+ kind: 'stats',
645
+ text: this.meta.planMode
646
+ ? '已进入计划模式:只能只读调查,提交计划后才能动手'
647
+ : '已退出计划模式',
648
+ });
649
+ return;
650
+ }
651
+ case 'todos': {
652
+ this.push({
653
+ kind: 'banner',
654
+ text: '待办清单\n' + renderTodos(this.meta.todos ?? []),
655
+ });
656
+ return;
657
+ }
658
+ case 'fork': {
659
+ this.options.store.save(this.meta, this.history);
660
+ const forked = this.options.store.fork(this.meta.id, argument || undefined);
661
+ if (!forked) {
662
+ this.push({ kind: 'stats', text: '创建分支失败:找不到当前会话' });
663
+ return;
664
+ }
665
+ this.meta = forked.meta;
666
+ this.history = forked.messages;
667
+ this.planTools = createPlanTools(this.meta);
668
+ this.heartbeat.retarget(this.meta);
669
+ this.options.store.save(this.meta, this.history);
670
+ this.push({
671
+ kind: 'stats',
672
+ text: '已创建分支 ' + this.meta.id + '(来自 ' + forked.meta.forkedFrom + ')',
673
+ });
674
+ return;
675
+ }
676
+ case 'cwd':
677
+ this.push({ kind: 'stats', text: this.options.cwd });
678
+ return;
679
+ case 'model':
680
+ if (argument.length === 0) {
681
+ this.push({ kind: 'stats', text: '当前模型:' + this.model });
682
+ return;
683
+ }
684
+ try {
685
+ this.provider = this.options.setModel(argument);
686
+ this.model = argument;
687
+ this.push({ kind: 'stats', text: '已切换到 ' + argument });
688
+ }
689
+ catch (error) {
690
+ this.push({ kind: 'stats', text: friendlyError(error) });
691
+ }
692
+ return;
693
+ case 'sessions': {
694
+ const metas = this.options.store.list().slice(0, 10);
695
+ const lines = metas.map((meta) => ' ' + (meta.id === this.meta.id ? '● ' : '○ ')
696
+ + meta.id + ' ' + meta.title);
697
+ this.push({ kind: 'banner', text: lines.length > 0 ? lines.join('\n') : ' 还没有保存的会话' });
698
+ return;
699
+ }
700
+ case 'status': {
701
+ try {
702
+ const config = this.options.config ?? loadConfig();
703
+ const report = buildStatus(config, this.options.store, {
704
+ version: this.options.version,
705
+ });
706
+ this.push({ kind: 'banner', text: formatStatusPlain(report) });
707
+ }
708
+ catch (error) {
709
+ this.push({ kind: 'stats', text: friendlyError(error) });
710
+ }
711
+ return;
712
+ }
713
+ case 'usage': {
714
+ const cached = this.usageTotals.cached > 0
715
+ ? '(缓存 ' + formatTokens(this.usageTotals.cached) + ')'
716
+ : '';
717
+ this.push({
718
+ kind: 'stats',
719
+ text: '本会话:' + formatTokens(this.usageTotals.input) + ' 入' + cached
720
+ + ' / ' + formatTokens(this.usageTotals.output) + ' 出',
721
+ });
722
+ return;
723
+ }
724
+ case 'workers': {
725
+ const workers = this.workers.list();
726
+ this.push({
727
+ kind: 'banner',
728
+ text: workers.length === 0 ? '没有 worker。' : workers
729
+ .map((worker) => worker.id + ' [' + worker.state + '] ' + worker.name).join('\n'),
730
+ });
731
+ return;
732
+ }
733
+ case 'tasks': {
734
+ const tasks = this.tasks.list();
735
+ this.push({
736
+ kind: 'banner',
737
+ text: tasks.length === 0 ? '任务板为空。' : tasks
738
+ .map((task) => task.id + ' [' + task.status + '] ' + task.title).join('\n'),
739
+ });
740
+ return;
741
+ }
742
+ case 'crons': {
743
+ const crons = this.crons.getStore().list();
744
+ this.push({
745
+ kind: 'banner',
746
+ text: crons.length === 0 ? '还没有 cron。' : crons
747
+ .map((cron) => cron.id + ' [' + (cron.enabled ? '启用' : '停用') + '] '
748
+ + cron.name + ' · 每 ' + cron.everyMinutes + ' 分钟').join('\n'),
749
+ });
750
+ return;
751
+ }
752
+ case 'lanes': {
753
+ const lanes = this.lanes.list();
754
+ this.push({
755
+ kind: 'banner',
756
+ text: lanes.length === 0 ? '还没有 lane。' : lanes
757
+ .map((lane) => lane.id + ' [' + lane.state + '] ' + lane.name + ' · ' + lane.branch)
758
+ .join('\n'),
759
+ });
760
+ return;
761
+ }
762
+ case 'approvals': {
763
+ const tokens = this.approvals.list();
764
+ this.push({
765
+ kind: 'banner',
766
+ text: tokens.length === 0 ? '没有审批令牌。' : tokens
767
+ .map((token) => token.id + ' · ' + token.tool
768
+ + (token.pattern ? '(' + token.pattern + ')' : '')
769
+ + ' · ' + token.uses + '/' + token.maxUses + ' · ' + token.expiresAt)
770
+ .join('\n'),
771
+ });
772
+ return;
773
+ }
774
+ case 'plugins': {
775
+ const config = this.options.config ?? loadConfig();
776
+ const result = listPlugins(undefined, config.plugins.disabled);
777
+ const lines = result.plugins.map((plugin) => plugin.name
778
+ + (plugin.enabled ? ' [启用]' : ' [停用]') + ' · ' + plugin.toolCount + ' 个工具');
779
+ if (result.errors.length > 0) {
780
+ lines.push('错误:' + result.errors.join(';'));
781
+ }
782
+ this.push({
783
+ kind: 'banner',
784
+ text: lines.length === 0 ? '还没有安装插件。' : lines.join('\n'),
785
+ });
786
+ return;
787
+ }
788
+ case 'skills': {
789
+ const skills = listSkills(this.options.cwd);
790
+ this.push({
791
+ kind: 'banner',
792
+ text: skills.length === 0 ? '没有技能包。' : skills
793
+ .map((skill) => skill.name + ' — ' + skill.description + '(' + skill.source + ')')
794
+ .join('\n'),
795
+ });
796
+ return;
797
+ }
798
+ case 'memory': {
799
+ const index = listMemoryIndex(this.memoryDir);
800
+ this.push({ kind: 'banner', text: index.length === 0 ? '长期记忆为空。' : index });
801
+ return;
802
+ }
803
+ case 'sandbox': {
804
+ const sandbox = resolveSandbox(this.options.config);
805
+ const docker = dockerAvailable();
806
+ this.push({
807
+ kind: 'banner',
808
+ text: 'sandbox: ' + (sandbox
809
+ ? 'docker · ' + sandbox.image + ' · network ' + (sandbox.network ? '允许' : '禁止')
810
+ : 'off')
811
+ + '\ndocker: ' + (docker ? '可用' : '不可用'),
812
+ });
813
+ return;
814
+ }
815
+ case 'export': {
816
+ const target = path.resolve(this.options.cwd, argument || 'minko-' + this.meta.id + '.md');
817
+ try {
818
+ fs.writeFileSync(target, sessionToMarkdown({
819
+ meta: this.meta,
820
+ messages: this.history,
821
+ }), 'utf8');
822
+ this.push({ kind: 'stats', text: '已导出到 ' + target });
823
+ }
824
+ catch (error) {
825
+ this.push({
826
+ kind: 'stats',
827
+ text: '导出失败:' + (error instanceof Error ? error.message : String(error)),
828
+ });
829
+ }
830
+ return;
831
+ }
832
+ default:
833
+ this.push({ kind: 'stats', text: '未知命令 /' + name + ',试试 /help' });
834
+ }
835
+ }
836
+ async runTurn(text) {
837
+ this.history.push({ role: 'user', content: text });
838
+ this.meta.title = titleFromMessages(this.history);
839
+ this.heartbeat.setStatus('running');
840
+ const recorder = new TurnRecorder(this.options.store, this.meta, this.history);
841
+ const controller = new AbortController();
842
+ this.running = controller;
843
+ this.status = '思考中';
844
+ this.statusStarted = Date.now();
845
+ let assistantEntry = null;
846
+ let reasoningEntry = null;
847
+ let currentTool = null;
848
+ const started = Date.now();
849
+ // Text starts a new bubble after every tool round, so answers and tool calls
850
+ // keep the order they arrived in instead of all text collecting at the top.
851
+ const ensureAssistant = () => {
852
+ if (assistantEntry === null) {
853
+ assistantEntry = { kind: 'assistant', text: '' };
854
+ this.entries.push(assistantEntry);
855
+ }
856
+ return assistantEntry;
857
+ };
858
+ const finishReasoning = () => {
859
+ if (reasoningEntry === null) {
860
+ return;
861
+ }
862
+ reasoningEntry.done = true;
863
+ reasoningEntry.ms = Math.max(0, Date.now() - reasoningEntry.started);
864
+ reasoningEntry = null;
865
+ this.dirty = true;
866
+ };
867
+ const hooks = {
868
+ onText: (delta) => {
869
+ finishReasoning();
870
+ ensureAssistant().text += delta;
871
+ if (this.status === '思考中') {
872
+ this.status = '';
873
+ }
874
+ if (!this.viewport.atBottom) {
875
+ return;
876
+ }
877
+ this.dirty = true;
878
+ },
879
+ onReasoning: (delta) => {
880
+ if (reasoningEntry === null) {
881
+ reasoningEntry = {
882
+ kind: 'reasoning',
883
+ text: '',
884
+ started: Date.now(),
885
+ ms: 0,
886
+ done: false,
887
+ };
888
+ this.entries.push(reasoningEntry);
889
+ }
890
+ reasoningEntry.text += delta;
891
+ if (this.status === '思考中') {
892
+ this.status = '';
893
+ }
894
+ this.dirty = true;
895
+ },
896
+ onToolStart: (call) => {
897
+ finishReasoning();
898
+ assistantEntry = null;
899
+ currentTool = {
900
+ kind: 'tool',
901
+ name: call.name,
902
+ args: describeArguments(call.arguments),
903
+ started: Date.now(),
904
+ status: 'running',
905
+ ms: 0,
906
+ output: '',
907
+ };
908
+ this.status = '';
909
+ this.push(currentTool);
910
+ },
911
+ onToolEnd: (_call, result) => {
912
+ if (currentTool) {
913
+ currentTool.status = result.ok ? 'ok' : 'error';
914
+ currentTool.ms = result.ms;
915
+ currentTool.output = result.output;
916
+ currentTool = null;
917
+ }
918
+ this.dirty = true;
919
+ },
920
+ approve: (request) => this.askApproval(request),
921
+ onNotice: (text) => this.push({ kind: 'stats', text }),
922
+ onAssistantMessage: (message) => recorder.record(message),
923
+ };
924
+ try {
925
+ const outcome = await runAgent({
926
+ provider: this.provider,
927
+ tools: [
928
+ ...ALL_TOOLS,
929
+ ...this.options.mcpTools,
930
+ ...this.planTools,
931
+ ...this.memoryTools,
932
+ ...this.workerTools,
933
+ ...this.pluginTools,
934
+ ...this.laneTools,
935
+ ...this.taskTools,
936
+ ...this.cronTools,
937
+ ...this.branchTools,
938
+ ...this.approvalTools,
939
+ this.agentTool,
940
+ ],
941
+ messages: [{
942
+ role: 'system',
943
+ content: systemPrompt({
944
+ cwd: this.options.cwd,
945
+ project: loadProjectContext(this.options.cwd),
946
+ planMode: this.meta.planMode,
947
+ memory: loadMemoryContext(this.memoryDir),
948
+ }),
949
+ }, ...this.history],
950
+ cwd: this.options.cwd,
951
+ signal: controller.signal,
952
+ approval: this.options.auto ? 'auto' : 'ask',
953
+ power: this.options.power,
954
+ permissions: this.options.permissions,
955
+ toolHooks: this.effectiveHooks,
956
+ planState: this.meta,
957
+ config: this.options.config,
958
+ approvalStore: this.approvals,
959
+ hooks,
960
+ });
961
+ this.history = outcome.messages.filter((message) => message.role !== 'system');
962
+ this.meta.title = titleFromMessages(this.history);
963
+ recorder.commit(this.history);
964
+ const tokens = outcome.usage.input > 0 || outcome.usage.output > 0
965
+ ? ' · ' + formatTokens(outcome.usage.input) + ' 入'
966
+ + (outcome.usage.cached ? '(缓存 ' + formatTokens(outcome.usage.cached) + ')' : '')
967
+ + ' / ' + formatTokens(outcome.usage.output) + ' 出'
968
+ : '';
969
+ this.usageTotals.input += outcome.usage.input;
970
+ this.usageTotals.output += outcome.usage.output;
971
+ this.usageTotals.cached += outcome.usage.cached ?? 0;
972
+ this.push({ kind: 'stats', text: formatMs(Date.now() - started) + tokens });
973
+ }
974
+ catch (error) {
975
+ this.history = recorder.snapshot();
976
+ this.meta.title = titleFromMessages(this.history);
977
+ this.history = recorder.recover();
978
+ let message = error instanceof Error && error.name === 'AbortError'
979
+ ? '已中断'
980
+ : friendlyError(error);
981
+ if (/reasoning_content/i.test(message)) {
982
+ message += ' · 这个会话是旧版本建的,输入 /new 开新会话即可';
983
+ }
984
+ this.push({ kind: 'stats', text: message });
985
+ }
986
+ finally {
987
+ finishReasoning();
988
+ this.running = null;
989
+ this.status = '';
990
+ this.heartbeat.setStatus('idle');
991
+ this.dirty = true;
992
+ }
993
+ }
994
+ askApproval(request) {
995
+ this.heartbeat.setStatus('waiting_approval');
996
+ return new Promise((resolve) => {
997
+ const entry = {
998
+ kind: 'permission',
999
+ title: request.title,
1000
+ detail: request.detail,
1001
+ choice: 0,
1002
+ state: 'pending',
1003
+ };
1004
+ this.push(entry);
1005
+ this.approval = { request, resolve, entry };
1006
+ this.dirty = true;
1007
+ });
1008
+ }
1009
+ viewportHeight() {
1010
+ const inputWidth = Math.max(8, this.screen.columns - 4);
1011
+ const wrapped = wrapInput(this.input.text, this.input.cursor, inputWidth);
1012
+ return Math.max(3, this.screen.rows - (wrapped.lines.length + 2) - 1);
1013
+ }
1014
+ totalLines() {
1015
+ return renderEntries(this.entries, this.screen.columns - 2, this.painter, this.spinnerFrame, this.expandDetails).length;
1016
+ }
1017
+ render() {
1018
+ const columns = this.screen.columns;
1019
+ const rows = this.screen.rows;
1020
+ const width = columns - 2;
1021
+ const inputWidth = Math.max(8, columns - 4);
1022
+ const wrapped = wrapInput(this.input.text, this.input.cursor, inputWidth);
1023
+ const viewportHeight = Math.max(3, rows - (wrapped.lines.length + 2) - 1);
1024
+ const all = renderEntries(this.entries, width, this.painter, this.spinnerFrame, this.expandDetails);
1025
+ if (this.copy) {
1026
+ this.ensureCopyVisible(viewportHeight, all.length);
1027
+ }
1028
+ const visible = this.viewport.visible(all, viewportHeight);
1029
+ const start = Math.max(0, all.length - viewportHeight - this.viewport.scroll);
1030
+ const frame = [];
1031
+ for (let index = 0; index < viewportHeight; index += 1) {
1032
+ const line = visible[index] ?? '';
1033
+ const absolute = start + index;
1034
+ const range = this.selectionRangeFor(absolute);
1035
+ if (range) {
1036
+ frame.push(highlightColumns(line, range[0], range[1]));
1037
+ }
1038
+ else if (this.copy && this.isSelected(absolute)) {
1039
+ frame.push('\x1b[7m' + stripAnsi(line) + '\x1b[27m');
1040
+ }
1041
+ else {
1042
+ frame.push(line);
1043
+ }
1044
+ }
1045
+ const rule = this.painter.fg(PURPLE.border, '─'.repeat(Math.max(10, columns - 1)));
1046
+ frame.push(this.statusLine(columns));
1047
+ frame.push(rule);
1048
+ for (let index = 0; index < wrapped.lines.length; index += 1) {
1049
+ const lead = index === 0 ? this.painter.primary('› ') : ' ';
1050
+ frame.push(lead + wrapped.lines[index]);
1051
+ }
1052
+ frame.push(rule);
1053
+ const cursorRow = viewportHeight + 3 + wrapped.row;
1054
+ const cursorColumn = 3 + wrapped.column;
1055
+ this.screen.draw(frame, cursorRow, Math.min(cursorColumn, columns - 1));
1056
+ }
1057
+ statusLine(columns) {
1058
+ if (this.copy) {
1059
+ const anchor = this.copy.anchor;
1060
+ const count = Math.abs((anchor ?? this.copy.cursor) - this.copy.cursor) + 1;
1061
+ const hint = ' 选择模式 · ↑↓ 移动 · 空格 ' + (anchor === null ? '标记起点' : '取消标记')
1062
+ + ' · Enter 复制 · Esc 取消 · ' + count + ' 行';
1063
+ return this.painter.primary(truncateVisible(hint, columns - 2));
1064
+ }
1065
+ if (this.approval) {
1066
+ return this.painter.dimmed(' 在对话中按 y / a / n 处理确认');
1067
+ }
1068
+ if (!this.running) {
1069
+ if (this.viewport.scroll > 0) {
1070
+ return this.painter.dimmed(' 已向上滚动 · PgDn 回到底部');
1071
+ }
1072
+ return '';
1073
+ }
1074
+ const seconds = Math.max(0, Math.round((Date.now() - this.statusStarted) / 1000));
1075
+ const spinner = SPINNER[this.spinnerFrame];
1076
+ return ' ' + this.painter.primary(spinner) + ' '
1077
+ + this.painter.muted(this.status + (seconds > 0 ? ' ' + seconds + 's' : ''));
1078
+ }
1079
+ }
1080
+ function describeArguments(raw) {
1081
+ try {
1082
+ const args = JSON.parse(raw);
1083
+ if (typeof args.command === 'string') {
1084
+ return args.command;
1085
+ }
1086
+ if (typeof args.path === 'string') {
1087
+ return args.path;
1088
+ }
1089
+ return raw;
1090
+ }
1091
+ catch {
1092
+ return raw;
1093
+ }
1094
+ }
1095
+ function formatMs(ms) {
1096
+ return ms < 1000 ? ms + 'ms' : (ms / 1000).toFixed(1) + 's';
1097
+ }
1098
+ function formatTokens(value) {
1099
+ return value < 1000 ? String(value) : (value / 1000).toFixed(1) + 'k';
1100
+ }
1101
+ export async function runTui(options) {
1102
+ const tui = new Tui(options);
1103
+ return await tui.run();
1104
+ }