@qxbyte/muse 0.1.1 → 0.1.3

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/dist/cli.js CHANGED
@@ -5,17 +5,177 @@ import { Command } from "commander";
5
5
  import { render } from "ink";
6
6
 
7
7
  // src/app.tsx
8
- import { useCallback, useEffect, useMemo as useMemo2, useReducer, useRef, useState as useState2 } from "react";
9
- import { Box as Box9, Text as Text9, useApp, useInput as useInput3, useStdout } from "ink";
10
- import TextInput from "ink-text-input";
11
- import { mkdir as mkdir2, readFile as readFile4, writeFile } from "fs/promises";
12
- import { existsSync as existsSync4 } from "fs";
13
- import { homedir as homedir7 } from "os";
14
- import { dirname as dirname3, join as join5 } from "path";
8
+ import { useCallback, useEffect as useEffect5, useMemo as useMemo2, useReducer, useRef, useState as useState8 } from "react";
9
+ import { Box as Box13, Text as Text15, useApp, useInput as useInput5, useStdout as useStdout2 } from "ink";
15
10
 
16
- // src/components/StartupBanner.tsx
17
- import { Box, Text } from "ink";
11
+ // src/components/BgTextInput.tsx
12
+ import { useState, useEffect } from "react";
13
+ import { Text, useInput } from "ink";
18
14
  import { jsx, jsxs } from "react/jsx-runtime";
15
+ var BLINK_MS = 530;
16
+ var PASTE_CHAR_THRESHOLD = 200;
17
+ function looksLikePaste(input) {
18
+ return input.includes("\n") || input.includes("\r") || input.length > PASTE_CHAR_THRESHOLD;
19
+ }
20
+ function stripBracketedPaste(s) {
21
+ return s.replace(/\x1b\[20[01]~/g, "");
22
+ }
23
+ function normalizeLineEndings(s) {
24
+ return s.replace(/\r\n?/g, "\n");
25
+ }
26
+ function BgTextInput({
27
+ value,
28
+ onChange,
29
+ onSubmit,
30
+ width,
31
+ backgroundColor,
32
+ color,
33
+ isActive = true,
34
+ onPaste,
35
+ placeholder
36
+ }) {
37
+ const [cursor, setCursor] = useState(value.length);
38
+ const [blinkOn, setBlinkOn] = useState(true);
39
+ useEffect(() => {
40
+ setCursor((c) => Math.min(c, value.length));
41
+ }, [value]);
42
+ useEffect(() => {
43
+ if (!isActive) return;
44
+ setBlinkOn(true);
45
+ const id = setInterval(() => setBlinkOn((b) => !b), BLINK_MS);
46
+ return () => clearInterval(id);
47
+ }, [isActive, cursor, value]);
48
+ useInput(
49
+ (input, key) => {
50
+ if (key.return) {
51
+ onSubmit?.(value);
52
+ return;
53
+ }
54
+ if (key.backspace || key.delete) {
55
+ if (cursor === 0) return;
56
+ const next = value.slice(0, cursor - 1) + value.slice(cursor);
57
+ onChange(next);
58
+ setCursor((c) => Math.max(0, c - 1));
59
+ return;
60
+ }
61
+ if (key.leftArrow) {
62
+ setCursor((c) => Math.max(0, c - 1));
63
+ return;
64
+ }
65
+ if (key.rightArrow) {
66
+ setCursor((c) => Math.min(value.length, c + 1));
67
+ return;
68
+ }
69
+ if (key.ctrl && input === "a") {
70
+ setCursor(0);
71
+ return;
72
+ }
73
+ if (key.ctrl && input === "e") {
74
+ setCursor(value.length);
75
+ return;
76
+ }
77
+ if (key.ctrl || key.shift || key.tab || key.escape || key.upArrow || key.downArrow || key.meta) {
78
+ return;
79
+ }
80
+ if (input && !key.return) {
81
+ const cleaned = normalizeLineEndings(stripBracketedPaste(input));
82
+ if (!cleaned) return;
83
+ const insertion = onPaste && looksLikePaste(cleaned) ? onPaste(cleaned) : cleaned;
84
+ const next = value.slice(0, cursor) + insertion + value.slice(cursor);
85
+ onChange(next);
86
+ setCursor((c) => c + insertion.length);
87
+ }
88
+ },
89
+ { isActive }
90
+ );
91
+ const showCursor = isActive && blinkOn;
92
+ if (value.length === 0 && placeholder) {
93
+ const maxW = Math.max(0, width - 1);
94
+ let truncated = placeholder;
95
+ while (stringWidth(truncated) > maxW && truncated.length > 0) {
96
+ truncated = truncated.slice(0, -1);
97
+ }
98
+ const usedW = 1 + stringWidth(truncated);
99
+ const padLen2 = Math.max(0, width - usedW);
100
+ return /* @__PURE__ */ jsxs(Text, { backgroundColor, color, children: [
101
+ showCursor ? /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color, dimColor: true, children: " " }) : /* @__PURE__ */ jsx(Text, { backgroundColor, color, children: " " }),
102
+ /* @__PURE__ */ jsx(Text, { backgroundColor, dimColor: true, children: truncated }),
103
+ " ".repeat(padLen2)
104
+ ] });
105
+ }
106
+ const displayValue = value.replace(/[\n\r]/g, "\u21B5");
107
+ const view = computeViewport(displayValue, cursor, width);
108
+ const at = view.atChar;
109
+ const padLen = Math.max(0, width - view.consumedWidth);
110
+ return /* @__PURE__ */ jsxs(Text, { backgroundColor, color, children: [
111
+ view.before,
112
+ showCursor ? /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color, dimColor: true, children: at }) : /* @__PURE__ */ jsx(Text, { backgroundColor, color, children: at }),
113
+ view.after,
114
+ " ".repeat(padLen)
115
+ ] });
116
+ }
117
+ function charWidth(ch) {
118
+ const cp = ch.codePointAt(0);
119
+ if (cp === void 0) return 0;
120
+ if (cp < 32 || cp === 127) return 0;
121
+ if (cp >= 4352 && cp <= 4447 || // Hangul Jamo
122
+ cp >= 11904 && cp <= 12350 || // CJK Radicals
123
+ cp >= 12353 && cp <= 13311 || // Hiragana / Katakana / CJK Symbols
124
+ cp >= 13312 && cp <= 19903 || // CJK Ext A
125
+ cp >= 19968 && cp <= 40959 || // CJK Unified
126
+ cp >= 40960 && cp <= 42191 || // Yi
127
+ cp >= 44032 && cp <= 55203 || // Hangul Syllables
128
+ cp >= 63744 && cp <= 64255 || // CJK Compat
129
+ cp >= 65072 && cp <= 65103 || // CJK Compat Forms
130
+ cp >= 65280 && cp <= 65376 || // Fullwidth ASCII / Punctuation
131
+ cp >= 65504 && cp <= 65510 || // Fullwidth Sign
132
+ cp >= 131072 && cp <= 196605) {
133
+ return 2;
134
+ }
135
+ return 1;
136
+ }
137
+ function stringWidth(s) {
138
+ let w = 0;
139
+ for (const ch of s) w += charWidth(ch);
140
+ return w;
141
+ }
142
+ function computeViewport(value, cursor, width) {
143
+ const cursorAtEnd = cursor >= value.length;
144
+ const atChar = cursorAtEnd ? " " : value[cursor] ?? " ";
145
+ const cursorCellW = charWidth(atChar);
146
+ let beforeStart = 0;
147
+ while (true) {
148
+ const before = value.slice(beforeStart, cursor);
149
+ const after = cursorAtEnd ? "" : value.slice(cursor + 1);
150
+ const total = stringWidth(before) + cursorCellW + stringWidth(after);
151
+ if (total <= width) {
152
+ return { before, atChar, after, consumedWidth: total };
153
+ }
154
+ if (beforeStart >= cursor) {
155
+ let after2 = cursorAtEnd ? "" : value.slice(cursor + 1);
156
+ while (after2.length > 0 && stringWidth("") + cursorCellW + stringWidth(after2) > width) {
157
+ after2 = after2.slice(0, -1);
158
+ }
159
+ return {
160
+ before: "",
161
+ atChar,
162
+ after: after2,
163
+ consumedWidth: cursorCellW + stringWidth(after2)
164
+ };
165
+ }
166
+ beforeStart++;
167
+ }
168
+ }
169
+
170
+ // src/app.tsx
171
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
172
+ import { existsSync as existsSync5 } from "fs";
173
+ import { homedir as homedir8 } from "os";
174
+ import { basename, dirname as dirname3, join as join6 } from "path";
175
+
176
+ // src/components/StartupBanner.tsx
177
+ import { Box, Text as Text2 } from "ink";
178
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
19
179
  var LETTERS = {
20
180
  M: ["\u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588"],
21
181
  U: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588\u2588\u2588 "],
@@ -35,104 +195,610 @@ var LETTER_GAP = 3;
35
195
  var LOGO_WIDTH = 5 * 4 + LETTER_GAP * 3;
36
196
  var GAP_WIDTH = 6;
37
197
  function LogoLine({ row }) {
38
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
39
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: COLORS.M, children: LETTERS.M[row] }) }),
40
- /* @__PURE__ */ jsx(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx(Text, { color: COLORS.U, children: LETTERS.U[row] }) }),
41
- /* @__PURE__ */ jsx(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx(Text, { color: COLORS.S, children: LETTERS.S[row] }) }),
42
- /* @__PURE__ */ jsx(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx(Text, { color: COLORS.E, children: LETTERS.E[row] }) })
198
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
199
+ /* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.M, children: LETTERS.M[row] }) }),
200
+ /* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.U, children: LETTERS.U[row] }) }),
201
+ /* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.S, children: LETTERS.S[row] }) }),
202
+ /* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.E, children: LETTERS.E[row] }) })
43
203
  ] });
44
204
  }
45
205
  function BannerLine({ row, children }) {
46
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
47
- /* @__PURE__ */ jsx(Box, { minWidth: LOGO_WIDTH, children: /* @__PURE__ */ jsx(LogoLine, { row }) }),
48
- /* @__PURE__ */ jsx(Box, { width: GAP_WIDTH }),
206
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
207
+ /* @__PURE__ */ jsx2(Box, { minWidth: LOGO_WIDTH, children: /* @__PURE__ */ jsx2(LogoLine, { row }) }),
208
+ /* @__PURE__ */ jsx2(Box, { width: GAP_WIDTH }),
49
209
  children ?? null
50
210
  ] });
51
211
  }
52
212
  function StartupBanner({ version, model, cwd }) {
53
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 0, children: [
54
- /* @__PURE__ */ jsx(BannerLine, { row: 0 }),
55
- /* @__PURE__ */ jsx(BannerLine, { row: 1, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
56
- /* @__PURE__ */ jsx(Text, { color: COLORS.asterisk, children: "\u273B" }),
57
- /* @__PURE__ */ jsx(Text, { color: COLORS.text, children: " Welcome to Muse " }),
58
- /* @__PURE__ */ jsxs(Text, { color: COLORS.versionAccent, children: [
213
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingY: 0, children: [
214
+ /* @__PURE__ */ jsx2(BannerLine, { row: 0 }),
215
+ /* @__PURE__ */ jsx2(BannerLine, { row: 1, children: /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
216
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.asterisk, children: "\u273B" }),
217
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: " Welcome to Muse " }),
218
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
59
219
  "v",
60
220
  version
61
221
  ] })
62
222
  ] }) }),
63
- /* @__PURE__ */ jsx(BannerLine, { row: 2, children: /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
223
+ /* @__PURE__ */ jsx2(BannerLine, { row: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
64
224
  "model: ",
65
225
  model
66
226
  ] }) }),
67
- /* @__PURE__ */ jsx(BannerLine, { row: 3, children: /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
227
+ /* @__PURE__ */ jsx2(BannerLine, { row: 3, children: /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
68
228
  "cwd: ",
69
229
  cwd
70
230
  ] }) }),
71
- /* @__PURE__ */ jsx(BannerLine, { row: 4 })
231
+ /* @__PURE__ */ jsx2(BannerLine, { row: 4 })
72
232
  ] });
73
233
  }
74
234
  function CompactBanner({ version, model, cwd }) {
75
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingY: 0, children: [
76
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
77
- /* @__PURE__ */ jsx(Text, { color: COLORS.asterisk, children: "\u273B" }),
78
- /* @__PURE__ */ jsx(Text, { color: COLORS.text, children: " Welcome to Muse " }),
79
- /* @__PURE__ */ jsxs(Text, { color: COLORS.versionAccent, children: [
235
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingY: 0, children: [
236
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
237
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.asterisk, children: "\u273B" }),
238
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: " Welcome to Muse " }),
239
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
80
240
  "v",
81
241
  version
82
242
  ] })
83
243
  ] }),
84
- /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
244
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
85
245
  "model: ",
86
246
  model
87
247
  ] }),
88
- /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
248
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
89
249
  "cwd: ",
90
250
  cwd
91
251
  ] })
92
252
  ] });
93
253
  }
94
254
  function SingleLineBanner({ version, model }) {
95
- return /* @__PURE__ */ jsxs(Text, { children: [
96
- /* @__PURE__ */ jsx(Text, { color: COLORS.text, children: "Muse " }),
97
- /* @__PURE__ */ jsxs(Text, { color: COLORS.versionAccent, children: [
255
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
256
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: "Muse " }),
257
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
98
258
  "v",
99
259
  version
100
260
  ] }),
101
- /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
261
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
102
262
  " \xB7 ",
103
263
  model
104
264
  ] })
105
265
  ] });
106
266
  }
107
267
  function pickBanner(width, props) {
108
- if (width >= 60) return /* @__PURE__ */ jsx(StartupBanner, { ...props });
109
- if (width >= 40) return /* @__PURE__ */ jsx(CompactBanner, { ...props });
110
- return /* @__PURE__ */ jsx(SingleLineBanner, { version: props.version, model: props.model });
268
+ if (width >= 60) return /* @__PURE__ */ jsx2(StartupBanner, { ...props });
269
+ if (width >= 40) return /* @__PURE__ */ jsx2(CompactBanner, { ...props });
270
+ return /* @__PURE__ */ jsx2(SingleLineBanner, { version: props.version, model: props.model });
111
271
  }
112
272
 
113
273
  // src/components/MessageView.tsx
114
274
  import { useMemo } from "react";
115
- import { Box as Box2, Text as Text2 } from "ink";
275
+ import { Box as Box2, Text as Text3, useStdout } from "ink";
276
+
277
+ // node_modules/chalk/source/vendor/ansi-styles/index.js
278
+ var ANSI_BACKGROUND_OFFSET = 10;
279
+ var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
280
+ var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
281
+ var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
282
+ var styles = {
283
+ modifier: {
284
+ reset: [0, 0],
285
+ // 21 isn't widely supported and 22 does the same thing
286
+ bold: [1, 22],
287
+ dim: [2, 22],
288
+ italic: [3, 23],
289
+ underline: [4, 24],
290
+ overline: [53, 55],
291
+ inverse: [7, 27],
292
+ hidden: [8, 28],
293
+ strikethrough: [9, 29]
294
+ },
295
+ color: {
296
+ black: [30, 39],
297
+ red: [31, 39],
298
+ green: [32, 39],
299
+ yellow: [33, 39],
300
+ blue: [34, 39],
301
+ magenta: [35, 39],
302
+ cyan: [36, 39],
303
+ white: [37, 39],
304
+ // Bright color
305
+ blackBright: [90, 39],
306
+ gray: [90, 39],
307
+ // Alias of `blackBright`
308
+ grey: [90, 39],
309
+ // Alias of `blackBright`
310
+ redBright: [91, 39],
311
+ greenBright: [92, 39],
312
+ yellowBright: [93, 39],
313
+ blueBright: [94, 39],
314
+ magentaBright: [95, 39],
315
+ cyanBright: [96, 39],
316
+ whiteBright: [97, 39]
317
+ },
318
+ bgColor: {
319
+ bgBlack: [40, 49],
320
+ bgRed: [41, 49],
321
+ bgGreen: [42, 49],
322
+ bgYellow: [43, 49],
323
+ bgBlue: [44, 49],
324
+ bgMagenta: [45, 49],
325
+ bgCyan: [46, 49],
326
+ bgWhite: [47, 49],
327
+ // Bright color
328
+ bgBlackBright: [100, 49],
329
+ bgGray: [100, 49],
330
+ // Alias of `bgBlackBright`
331
+ bgGrey: [100, 49],
332
+ // Alias of `bgBlackBright`
333
+ bgRedBright: [101, 49],
334
+ bgGreenBright: [102, 49],
335
+ bgYellowBright: [103, 49],
336
+ bgBlueBright: [104, 49],
337
+ bgMagentaBright: [105, 49],
338
+ bgCyanBright: [106, 49],
339
+ bgWhiteBright: [107, 49]
340
+ }
341
+ };
342
+ var modifierNames = Object.keys(styles.modifier);
343
+ var foregroundColorNames = Object.keys(styles.color);
344
+ var backgroundColorNames = Object.keys(styles.bgColor);
345
+ var colorNames = [...foregroundColorNames, ...backgroundColorNames];
346
+ function assembleStyles() {
347
+ const codes = /* @__PURE__ */ new Map();
348
+ for (const [groupName, group] of Object.entries(styles)) {
349
+ for (const [styleName, style] of Object.entries(group)) {
350
+ styles[styleName] = {
351
+ open: `\x1B[${style[0]}m`,
352
+ close: `\x1B[${style[1]}m`
353
+ };
354
+ group[styleName] = styles[styleName];
355
+ codes.set(style[0], style[1]);
356
+ }
357
+ Object.defineProperty(styles, groupName, {
358
+ value: group,
359
+ enumerable: false
360
+ });
361
+ }
362
+ Object.defineProperty(styles, "codes", {
363
+ value: codes,
364
+ enumerable: false
365
+ });
366
+ styles.color.close = "\x1B[39m";
367
+ styles.bgColor.close = "\x1B[49m";
368
+ styles.color.ansi = wrapAnsi16();
369
+ styles.color.ansi256 = wrapAnsi256();
370
+ styles.color.ansi16m = wrapAnsi16m();
371
+ styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
372
+ styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
373
+ styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
374
+ Object.defineProperties(styles, {
375
+ rgbToAnsi256: {
376
+ value(red, green, blue) {
377
+ if (red === green && green === blue) {
378
+ if (red < 8) {
379
+ return 16;
380
+ }
381
+ if (red > 248) {
382
+ return 231;
383
+ }
384
+ return Math.round((red - 8) / 247 * 24) + 232;
385
+ }
386
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
387
+ },
388
+ enumerable: false
389
+ },
390
+ hexToRgb: {
391
+ value(hex) {
392
+ const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
393
+ if (!matches) {
394
+ return [0, 0, 0];
395
+ }
396
+ let [colorString] = matches;
397
+ if (colorString.length === 3) {
398
+ colorString = [...colorString].map((character) => character + character).join("");
399
+ }
400
+ const integer = Number.parseInt(colorString, 16);
401
+ return [
402
+ /* eslint-disable no-bitwise */
403
+ integer >> 16 & 255,
404
+ integer >> 8 & 255,
405
+ integer & 255
406
+ /* eslint-enable no-bitwise */
407
+ ];
408
+ },
409
+ enumerable: false
410
+ },
411
+ hexToAnsi256: {
412
+ value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
413
+ enumerable: false
414
+ },
415
+ ansi256ToAnsi: {
416
+ value(code) {
417
+ if (code < 8) {
418
+ return 30 + code;
419
+ }
420
+ if (code < 16) {
421
+ return 90 + (code - 8);
422
+ }
423
+ let red;
424
+ let green;
425
+ let blue;
426
+ if (code >= 232) {
427
+ red = ((code - 232) * 10 + 8) / 255;
428
+ green = red;
429
+ blue = red;
430
+ } else {
431
+ code -= 16;
432
+ const remainder = code % 36;
433
+ red = Math.floor(code / 36) / 5;
434
+ green = Math.floor(remainder / 6) / 5;
435
+ blue = remainder % 6 / 5;
436
+ }
437
+ const value = Math.max(red, green, blue) * 2;
438
+ if (value === 0) {
439
+ return 30;
440
+ }
441
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
442
+ if (value === 2) {
443
+ result += 60;
444
+ }
445
+ return result;
446
+ },
447
+ enumerable: false
448
+ },
449
+ rgbToAnsi: {
450
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
451
+ enumerable: false
452
+ },
453
+ hexToAnsi: {
454
+ value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
455
+ enumerable: false
456
+ }
457
+ });
458
+ return styles;
459
+ }
460
+ var ansiStyles = assembleStyles();
461
+ var ansi_styles_default = ansiStyles;
462
+
463
+ // node_modules/chalk/source/vendor/supports-color/index.js
464
+ import process2 from "process";
465
+ import os from "os";
466
+ import tty from "tty";
467
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
468
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
469
+ const position = argv.indexOf(prefix + flag);
470
+ const terminatorPosition = argv.indexOf("--");
471
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
472
+ }
473
+ var { env } = process2;
474
+ var flagForceColor;
475
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
476
+ flagForceColor = 0;
477
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
478
+ flagForceColor = 1;
479
+ }
480
+ function envForceColor() {
481
+ if ("FORCE_COLOR" in env) {
482
+ if (env.FORCE_COLOR === "true") {
483
+ return 1;
484
+ }
485
+ if (env.FORCE_COLOR === "false") {
486
+ return 0;
487
+ }
488
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
489
+ }
490
+ }
491
+ function translateLevel(level) {
492
+ if (level === 0) {
493
+ return false;
494
+ }
495
+ return {
496
+ level,
497
+ hasBasic: true,
498
+ has256: level >= 2,
499
+ has16m: level >= 3
500
+ };
501
+ }
502
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
503
+ const noFlagForceColor = envForceColor();
504
+ if (noFlagForceColor !== void 0) {
505
+ flagForceColor = noFlagForceColor;
506
+ }
507
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
508
+ if (forceColor === 0) {
509
+ return 0;
510
+ }
511
+ if (sniffFlags) {
512
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
513
+ return 3;
514
+ }
515
+ if (hasFlag("color=256")) {
516
+ return 2;
517
+ }
518
+ }
519
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) {
520
+ return 1;
521
+ }
522
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
523
+ return 0;
524
+ }
525
+ const min = forceColor || 0;
526
+ if (env.TERM === "dumb") {
527
+ return min;
528
+ }
529
+ if (process2.platform === "win32") {
530
+ const osRelease = os.release().split(".");
531
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
532
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
533
+ }
534
+ return 1;
535
+ }
536
+ if ("CI" in env) {
537
+ if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
538
+ return 3;
539
+ }
540
+ if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
541
+ return 1;
542
+ }
543
+ return min;
544
+ }
545
+ if ("TEAMCITY_VERSION" in env) {
546
+ return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
547
+ }
548
+ if (env.COLORTERM === "truecolor") {
549
+ return 3;
550
+ }
551
+ if (env.TERM === "xterm-kitty") {
552
+ return 3;
553
+ }
554
+ if (env.TERM === "xterm-ghostty") {
555
+ return 3;
556
+ }
557
+ if (env.TERM === "wezterm") {
558
+ return 3;
559
+ }
560
+ if ("TERM_PROGRAM" in env) {
561
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
562
+ switch (env.TERM_PROGRAM) {
563
+ case "iTerm.app": {
564
+ return version >= 3 ? 3 : 2;
565
+ }
566
+ case "Apple_Terminal": {
567
+ return 2;
568
+ }
569
+ }
570
+ }
571
+ if (/-256(color)?$/i.test(env.TERM)) {
572
+ return 2;
573
+ }
574
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
575
+ return 1;
576
+ }
577
+ if ("COLORTERM" in env) {
578
+ return 1;
579
+ }
580
+ return min;
581
+ }
582
+ function createSupportsColor(stream, options = {}) {
583
+ const level = _supportsColor(stream, {
584
+ streamIsTTY: stream && stream.isTTY,
585
+ ...options
586
+ });
587
+ return translateLevel(level);
588
+ }
589
+ var supportsColor = {
590
+ stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
591
+ stderr: createSupportsColor({ isTTY: tty.isatty(2) })
592
+ };
593
+ var supports_color_default = supportsColor;
594
+
595
+ // node_modules/chalk/source/utilities.js
596
+ function stringReplaceAll(string, substring, replacer) {
597
+ let index = string.indexOf(substring);
598
+ if (index === -1) {
599
+ return string;
600
+ }
601
+ const substringLength = substring.length;
602
+ let endIndex = 0;
603
+ let returnValue = "";
604
+ do {
605
+ returnValue += string.slice(endIndex, index) + substring + replacer;
606
+ endIndex = index + substringLength;
607
+ index = string.indexOf(substring, endIndex);
608
+ } while (index !== -1);
609
+ returnValue += string.slice(endIndex);
610
+ return returnValue;
611
+ }
612
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
613
+ let endIndex = 0;
614
+ let returnValue = "";
615
+ do {
616
+ const gotCR = string[index - 1] === "\r";
617
+ returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
618
+ endIndex = index + 1;
619
+ index = string.indexOf("\n", endIndex);
620
+ } while (index !== -1);
621
+ returnValue += string.slice(endIndex);
622
+ return returnValue;
623
+ }
624
+
625
+ // node_modules/chalk/source/index.js
626
+ var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
627
+ var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
628
+ var STYLER = /* @__PURE__ */ Symbol("STYLER");
629
+ var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
630
+ var levelMapping = [
631
+ "ansi",
632
+ "ansi",
633
+ "ansi256",
634
+ "ansi16m"
635
+ ];
636
+ var styles2 = /* @__PURE__ */ Object.create(null);
637
+ var applyOptions = (object, options = {}) => {
638
+ if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
639
+ throw new Error("The `level` option should be an integer from 0 to 3");
640
+ }
641
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
642
+ object.level = options.level === void 0 ? colorLevel : options.level;
643
+ };
644
+ var chalkFactory = (options) => {
645
+ const chalk2 = (...strings) => strings.join(" ");
646
+ applyOptions(chalk2, options);
647
+ Object.setPrototypeOf(chalk2, createChalk.prototype);
648
+ return chalk2;
649
+ };
650
+ function createChalk(options) {
651
+ return chalkFactory(options);
652
+ }
653
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
654
+ for (const [styleName, style] of Object.entries(ansi_styles_default)) {
655
+ styles2[styleName] = {
656
+ get() {
657
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
658
+ Object.defineProperty(this, styleName, { value: builder });
659
+ return builder;
660
+ }
661
+ };
662
+ }
663
+ styles2.visible = {
664
+ get() {
665
+ const builder = createBuilder(this, this[STYLER], true);
666
+ Object.defineProperty(this, "visible", { value: builder });
667
+ return builder;
668
+ }
669
+ };
670
+ var getModelAnsi = (model, level, type, ...arguments_) => {
671
+ if (model === "rgb") {
672
+ if (level === "ansi16m") {
673
+ return ansi_styles_default[type].ansi16m(...arguments_);
674
+ }
675
+ if (level === "ansi256") {
676
+ return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
677
+ }
678
+ return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
679
+ }
680
+ if (model === "hex") {
681
+ return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
682
+ }
683
+ return ansi_styles_default[type][model](...arguments_);
684
+ };
685
+ var usedModels = ["rgb", "hex", "ansi256"];
686
+ for (const model of usedModels) {
687
+ styles2[model] = {
688
+ get() {
689
+ const { level } = this;
690
+ return function(...arguments_) {
691
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
692
+ return createBuilder(this, styler, this[IS_EMPTY]);
693
+ };
694
+ }
695
+ };
696
+ const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
697
+ styles2[bgModel] = {
698
+ get() {
699
+ const { level } = this;
700
+ return function(...arguments_) {
701
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
702
+ return createBuilder(this, styler, this[IS_EMPTY]);
703
+ };
704
+ }
705
+ };
706
+ }
707
+ var proto = Object.defineProperties(() => {
708
+ }, {
709
+ ...styles2,
710
+ level: {
711
+ enumerable: true,
712
+ get() {
713
+ return this[GENERATOR].level;
714
+ },
715
+ set(level) {
716
+ this[GENERATOR].level = level;
717
+ }
718
+ }
719
+ });
720
+ var createStyler = (open, close, parent) => {
721
+ let openAll;
722
+ let closeAll;
723
+ if (parent === void 0) {
724
+ openAll = open;
725
+ closeAll = close;
726
+ } else {
727
+ openAll = parent.openAll + open;
728
+ closeAll = close + parent.closeAll;
729
+ }
730
+ return {
731
+ open,
732
+ close,
733
+ openAll,
734
+ closeAll,
735
+ parent
736
+ };
737
+ };
738
+ var createBuilder = (self, _styler, _isEmpty) => {
739
+ const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
740
+ Object.setPrototypeOf(builder, proto);
741
+ builder[GENERATOR] = self;
742
+ builder[STYLER] = _styler;
743
+ builder[IS_EMPTY] = _isEmpty;
744
+ return builder;
745
+ };
746
+ var applyStyle = (self, string) => {
747
+ if (self.level <= 0 || !string) {
748
+ return self[IS_EMPTY] ? "" : string;
749
+ }
750
+ let styler = self[STYLER];
751
+ if (styler === void 0) {
752
+ return string;
753
+ }
754
+ const { openAll, closeAll } = styler;
755
+ if (string.includes("\x1B")) {
756
+ while (styler !== void 0) {
757
+ string = stringReplaceAll(string, styler.close, styler.open);
758
+ styler = styler.parent;
759
+ }
760
+ }
761
+ const lfIndex = string.indexOf("\n");
762
+ if (lfIndex !== -1) {
763
+ string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
764
+ }
765
+ return openAll + string + closeAll;
766
+ };
767
+ Object.defineProperties(createChalk.prototype, styles2);
768
+ var chalk = createChalk();
769
+ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
770
+ var source_default = chalk;
771
+
772
+ // src/components/MessageView.tsx
116
773
  import { marked } from "marked";
117
774
  import { markedTerminal } from "marked-terminal";
118
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
775
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
776
+ if (source_default.level === 0) source_default.level = 3;
119
777
  marked.use(markedTerminal());
120
778
  function renderMarkdown(text) {
121
779
  try {
122
- const out = marked.parse(text);
123
- return out.replace(/\n+$/, "");
780
+ let out = marked.parse(text);
781
+ out = out.replace(/\n+$/, "");
782
+ out = out.replace(/\*\*([^\n*]+?)\*\*/g, (_, body) => `\x1B[1m${body}\x1B[22m`);
783
+ out = out.replace(/(?<![*\\\x1b])\*([^\n*]+?)\*(?!\*)/g, (_, body) => `\x1B[3m${body}\x1B[23m`);
784
+ out = out.replace(/\x1b\[0m/g, "");
785
+ return out;
124
786
  } catch {
125
787
  return text;
126
788
  }
127
789
  }
128
- function MessageView({ message }) {
790
+ function MessageView({
791
+ message,
792
+ resultsByCallId
793
+ }) {
129
794
  switch (message.role) {
130
795
  case "user":
131
- return /* @__PURE__ */ jsx2(UserMessage, { content: typeof message.content === "string" ? message.content : flattenText(message.content) });
796
+ return /* @__PURE__ */ jsx3(UserMessage, { content: typeof message.content === "string" ? message.content : flattenText(message.content) });
132
797
  case "assistant":
133
- return /* @__PURE__ */ jsx2(AssistantMessage, { content: message.content });
798
+ return /* @__PURE__ */ jsx3(AssistantMessage, { content: message.content, resultsByCallId });
134
799
  case "tool":
135
- return /* @__PURE__ */ jsx2(ToolResultLine, { isError: message.isError ?? false, content: message.content });
800
+ if (message.toolName === "TodoWrite") return null;
801
+ return /* @__PURE__ */ jsx3(ToolResultTree, { result: message, standalone: true });
136
802
  case "system":
137
803
  return null;
138
804
  }
@@ -140,47 +806,174 @@ function MessageView({ message }) {
140
806
  function flattenText(parts) {
141
807
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
142
808
  }
809
+ var DOT = "\u23FA";
810
+ var USER_BG = "#262626";
143
811
  function UserMessage({ content }) {
144
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", marginTop: 1, children: [
145
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "> " }),
146
- /* @__PURE__ */ jsx2(Text2, { children: content })
812
+ const { stdout } = useStdout();
813
+ const termWidth = stdout?.columns ?? 80;
814
+ const bandWidth = Math.max(1, termWidth - 1);
815
+ const PREFIX = " \u203A ";
816
+ const PREFIX_W = 3;
817
+ const rendered = useMemo(() => renderMarkdown(content), [content]);
818
+ const lines = rendered.split("\n");
819
+ const bg = source_default.bgHex(USER_BG);
820
+ const prefixStyle = source_default.gray.bold;
821
+ const padRow = bg(" ".repeat(bandWidth));
822
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
823
+ /* @__PURE__ */ jsx3(Text3, { children: padRow }),
824
+ lines.map((line, i) => {
825
+ const visible = stringWidth(stripAnsi(line));
826
+ const padLen = Math.max(0, bandWidth - PREFIX_W - visible);
827
+ const prefix = i === 0 ? PREFIX : " ";
828
+ const fullLine = bg(prefixStyle(prefix) + line + " ".repeat(padLen));
829
+ return /* @__PURE__ */ jsx3(Text3, { children: fullLine }, i);
830
+ }),
831
+ /* @__PURE__ */ jsx3(Text3, { children: padRow })
147
832
  ] });
148
833
  }
149
- function AssistantMessage({ content }) {
150
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: content.map((part, i) => {
834
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
835
+ function stripAnsi(s) {
836
+ return s.replace(ANSI_RE, "");
837
+ }
838
+ function AssistantMessage({
839
+ content,
840
+ resultsByCallId
841
+ }) {
842
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginTop: 1, children: content.map((part, i) => {
151
843
  if (part.type === "text") {
152
- return /* @__PURE__ */ jsx2(AssistantTextPart, { text: part.text }, i);
844
+ return /* @__PURE__ */ jsx3(AssistantTextPart, { text: part.text }, i);
153
845
  }
154
846
  if (part.type === "tool_use") {
155
- return /* @__PURE__ */ jsx2(ToolCallLine, { name: part.name, args: part.args }, i);
847
+ if (part.name === "TodoWrite") {
848
+ return /* @__PURE__ */ jsx3(TodoList, { todos: extractTodos(part.args) }, i);
849
+ }
850
+ const result = resultsByCallId?.get(part.id);
851
+ return /* @__PURE__ */ jsx3(ToolCallBlock, { name: part.name, args: part.args, result }, i);
156
852
  }
157
853
  return null;
158
854
  }) });
159
855
  }
160
856
  function AssistantTextPart({ text }) {
161
857
  const rendered = useMemo(() => renderMarkdown(text), [text]);
162
- return /* @__PURE__ */ jsx2(Text2, { children: rendered });
858
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
859
+ /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
860
+ DOT,
861
+ " "
862
+ ] }),
863
+ /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx3(Text3, { children: rendered }) })
864
+ ] });
163
865
  }
164
866
  function ToolCallLine({ name, args }) {
165
867
  const argSummary = formatArgs(args);
166
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", marginTop: 1, children: [
167
- /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u2192 " }),
168
- /* @__PURE__ */ jsx2(Text2, { color: "yellow", bold: true, children: name }),
169
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
868
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
869
+ /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
870
+ DOT,
871
+ " "
872
+ ] }),
873
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: name }),
874
+ /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, minWidth: 0, children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, wrap: "truncate-end", children: [
170
875
  "(",
171
876
  argSummary,
172
877
  ")"
173
- ] })
878
+ ] }) })
879
+ ] });
880
+ }
881
+ function extractTodos(args) {
882
+ if (typeof args !== "object" || args === null) return [];
883
+ const todos = args.todos;
884
+ return Array.isArray(todos) ? todos : [];
885
+ }
886
+ function TodoList({ todos }) {
887
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
888
+ /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
889
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u2192 " }),
890
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: "Todos" })
891
+ ] }),
892
+ todos.map((todo, i) => /* @__PURE__ */ jsx3(TodoRow, { todo }, i))
893
+ ] });
894
+ }
895
+ function TodoRow({ todo }) {
896
+ const label = todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content;
897
+ switch (todo.status) {
898
+ case "completed":
899
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
900
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2612 " }),
901
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, strikethrough: true, children: label })
902
+ ] });
903
+ case "in_progress":
904
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
905
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u2610 " }),
906
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: label })
907
+ ] });
908
+ default:
909
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
910
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2610 " }),
911
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label })
912
+ ] });
913
+ }
914
+ }
915
+ function ToolCallBlock({
916
+ name,
917
+ args,
918
+ result
919
+ }) {
920
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
921
+ /* @__PURE__ */ jsx3(ToolCallLine, { name, args }),
922
+ result && /* @__PURE__ */ jsx3(ToolResultTree, { result })
174
923
  ] });
175
924
  }
176
- function ToolResultLine({ isError, content }) {
177
- const preview = content.length > 200 ? content.slice(0, 200) + "..." : content;
178
- const oneLine = preview.split("\n")[0];
179
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", marginLeft: 2, children: [
180
- /* @__PURE__ */ jsx2(Text2, { color: isError ? "red" : "green", children: isError ? "\u2717 " : "\u2713 " }),
181
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: oneLine })
925
+ var MAX_RESULT_LINES = 5;
926
+ function ToolResultTree({ result, standalone = false }) {
927
+ const isError = result.isError ?? false;
928
+ const effective = result.kind ?? (isError ? "error" : "success");
929
+ const dotColor = effective === "error" ? "red" : effective === "warn" ? "yellowBright" : "green";
930
+ const cleaned = stripWrapperTags(result.content);
931
+ const rawLines = cleaned.split("\n");
932
+ while (rawLines.length > 0 && rawLines[rawLines.length - 1].trim() === "") rawLines.pop();
933
+ while (rawLines.length > 0 && rawLines[0].trim() === "") rawLines.shift();
934
+ let displayLines;
935
+ let omitted = 0;
936
+ if (result.summary) {
937
+ const extra = rawLines.length > 1 ? ` (+${rawLines.length - 1} lines)` : "";
938
+ displayLines = [result.summary + extra];
939
+ } else if (rawLines.length === 0) {
940
+ displayLines = ["(no output)"];
941
+ } else if (rawLines.length <= MAX_RESULT_LINES) {
942
+ displayLines = rawLines;
943
+ } else {
944
+ displayLines = rawLines.slice(0, MAX_RESULT_LINES);
945
+ omitted = rawLines.length - MAX_RESULT_LINES;
946
+ }
947
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginLeft: 2, marginTop: standalone ? 1 : 0, children: [
948
+ displayLines.map((line, i) => /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
949
+ /* @__PURE__ */ jsx3(Text3, { color: i === 0 ? dotColor : void 0, children: i === 0 ? "\u2514 " : " " }),
950
+ /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, minWidth: 0, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: line || " " }) })
951
+ ] }, i)),
952
+ omitted > 0 && /* @__PURE__ */ jsx3(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `(+${omitted} more lines)` }) }),
953
+ result.diff && /* @__PURE__ */ jsx3(DiffBlock, { diff: result.diff })
182
954
  ] });
183
955
  }
956
+ function stripWrapperTags(content) {
957
+ return content.split("\n").filter((l) => !/^<\/?(stdout|stderr|timeout|exit_code)>\s*$/.test(l.trim())).join("\n");
958
+ }
959
+ function DiffBlock({ diff }) {
960
+ const lines = diff.split("\n");
961
+ const start = lines.findIndex((l) => l.startsWith("@@"));
962
+ const rendered = start >= 0 ? lines.slice(start) : lines;
963
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: rendered.map((line, i) => {
964
+ let color;
965
+ let dim = false;
966
+ if (line.startsWith("+")) color = "green";
967
+ else if (line.startsWith("-")) color = "red";
968
+ else if (line.startsWith("@@")) {
969
+ color = "cyan";
970
+ dim = true;
971
+ } else {
972
+ dim = true;
973
+ }
974
+ return /* @__PURE__ */ jsx3(Text3, { color, dimColor: dim, children: line || " " }, i);
975
+ }) });
976
+ }
184
977
  function formatArgs(args) {
185
978
  if (typeof args !== "object" || args === null) return String(args);
186
979
  const entries = Object.entries(args);
@@ -198,34 +991,86 @@ function formatArgs(args) {
198
991
  }
199
992
 
200
993
  // src/components/PermissionPrompt.tsx
201
- import { Box as Box3, Text as Text3, useInput } from "ink";
202
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
994
+ import { useState as useState2 } from "react";
995
+ import { Box as Box3, Text as Text4, useInput as useInput2 } from "ink";
996
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
997
+ var OPTIONS = [
998
+ { decision: "yes", labelKey: "yes", shortcut: "y" },
999
+ { decision: "session_allow", labelKey: "session", shortcut: "s" },
1000
+ { decision: "no", labelKey: "no", shortcut: "n" }
1001
+ ];
203
1002
  function PermissionPrompt({ request }) {
204
- useInput((input, key) => {
205
- if (input === "y" || key.return) {
206
- request.resolve(true);
207
- } else if (input === "n" || key.escape) {
208
- request.resolve(false);
1003
+ const [index, setIndex] = useState2(0);
1004
+ useInput2((input, key) => {
1005
+ if (key.upArrow) {
1006
+ setIndex((i) => (i - 1 + OPTIONS.length) % OPTIONS.length);
1007
+ return;
1008
+ }
1009
+ if (key.downArrow) {
1010
+ setIndex((i) => (i + 1) % OPTIONS.length);
1011
+ return;
1012
+ }
1013
+ if (key.return) {
1014
+ request.resolve(OPTIONS[index].decision);
1015
+ return;
1016
+ }
1017
+ if (key.escape) {
1018
+ request.resolve("no");
1019
+ return;
1020
+ }
1021
+ const lower = input?.toLowerCase?.();
1022
+ for (let i = 0; i < OPTIONS.length; i++) {
1023
+ const o = OPTIONS[i];
1024
+ if (lower === o.shortcut || input === String(i + 1)) {
1025
+ request.resolve(o.decision);
1026
+ return;
1027
+ }
209
1028
  }
210
1029
  });
211
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
212
- /* @__PURE__ */ jsxs3(Text3, { color: "yellow", bold: true, children: [
1030
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
1031
+ /* @__PURE__ */ jsxs4(Text4, { color: "yellow", bold: true, children: [
213
1032
  "\u23F5 Approve ",
214
1033
  request.toolName,
215
1034
  "?"
216
1035
  ] }),
217
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: request.summary }),
218
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { children: "(y)es / (n)o / Enter=allow / Esc=reject" }) })
1036
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: request.summary }),
1037
+ /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginTop: 1, children: OPTIONS.map((o, i) => {
1038
+ const focused = i === index;
1039
+ const label = labelFor(o.labelKey, request.toolName);
1040
+ return /* @__PURE__ */ jsxs4(Text4, { color: focused ? "cyan" : void 0, bold: focused, children: [
1041
+ focused ? "\u203A " : " ",
1042
+ i + 1,
1043
+ ". ",
1044
+ label,
1045
+ " ",
1046
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1047
+ "(",
1048
+ o.shortcut,
1049
+ ")"
1050
+ ] })
1051
+ ] }, o.decision);
1052
+ }) }),
1053
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter confirm \xB7 y/s/n shortcut \xB7 Esc=no" }) })
219
1054
  ] });
220
1055
  }
1056
+ function labelFor(key, toolName) {
1057
+ switch (key) {
1058
+ case "yes":
1059
+ return "Yes";
1060
+ case "session":
1061
+ return `Yes, allow ${toolName} for the rest of this session`;
1062
+ case "no":
1063
+ return "No";
1064
+ }
1065
+ }
221
1066
 
222
1067
  // src/components/ModelSelector.tsx
223
- import { Box as Box5, Text as Text5 } from "ink";
1068
+ import { Box as Box5, Text as Text6 } from "ink";
224
1069
 
225
1070
  // src/components/Selector.tsx
226
- import { useState } from "react";
227
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
228
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1071
+ import { useState as useState3 } from "react";
1072
+ import { Box as Box4, Text as Text5, useInput as useInput3 } from "ink";
1073
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
229
1074
  var POINTER_COLOR = "#A855F7";
230
1075
  function Selector({
231
1076
  items,
@@ -238,8 +1083,8 @@ function Selector({
238
1083
  onCancel
239
1084
  }) {
240
1085
  const safeInitial = Math.max(0, Math.min(initialIndex, items.length - 1));
241
- const [index, setIndex] = useState(safeInitial);
242
- useInput2((_, key) => {
1086
+ const [index, setIndex] = useState3(safeInitial);
1087
+ useInput3((_, key) => {
243
1088
  if (key.upArrow) {
244
1089
  setIndex((i) => Math.max(0, i - 1));
245
1090
  } else if (key.downArrow) {
@@ -255,7 +1100,7 @@ function Selector({
255
1100
  const start = Math.max(0, Math.min(index - Math.floor(window / 2), len - window));
256
1101
  const end = Math.min(len, start + window);
257
1102
  const visible = items.slice(start, end);
258
- return /* @__PURE__ */ jsxs4(
1103
+ return /* @__PURE__ */ jsxs5(
259
1104
  Box4,
260
1105
  {
261
1106
  flexDirection: "column",
@@ -264,20 +1109,20 @@ function Selector({
264
1109
  borderStyle: "round",
265
1110
  borderColor: "cyan",
266
1111
  children: [
267
- (title || hint) && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
268
- title && /* @__PURE__ */ jsx4(Text4, { bold: true, children: title }),
269
- title && hint && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " " }),
270
- hint && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: hint })
1112
+ (title || hint) && /* @__PURE__ */ jsxs5(Box4, { marginBottom: 1, children: [
1113
+ title && /* @__PURE__ */ jsx5(Text5, { bold: true, children: title }),
1114
+ title && hint && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " " }),
1115
+ hint && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: hint })
271
1116
  ] }),
272
1117
  visible.map((item, i) => {
273
1118
  const realIndex = start + i;
274
1119
  const focused = realIndex === index;
275
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
276
- /* @__PURE__ */ jsx4(Text4, { color: POINTER_COLOR, bold: true, children: focused ? "\u203A " : " " }),
1120
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "row", children: [
1121
+ /* @__PURE__ */ jsx5(Text5, { color: POINTER_COLOR, bold: true, children: focused ? "\u203A " : " " }),
277
1122
  renderRow(item, focused)
278
1123
  ] }, realIndex);
279
1124
  }),
280
- window < len && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1125
+ window < len && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
281
1126
  "(",
282
1127
  start + 1,
283
1128
  "-",
@@ -292,24 +1137,24 @@ function Selector({
292
1137
  }
293
1138
 
294
1139
  // src/components/ModelSelector.tsx
295
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1140
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
296
1141
  function ModelSelector({ request }) {
297
- const { items, currentId, resolve: resolve5 } = request;
1142
+ const { items, currentId, resolve: resolve6 } = request;
298
1143
  const initialIndex = Math.max(
299
1144
  0,
300
1145
  items.findIndex((m) => m.id === currentId)
301
1146
  );
302
1147
  const labelWidth = Math.max(...items.map((m) => (m.name ?? m.id).length));
303
- return /* @__PURE__ */ jsx5(
1148
+ return /* @__PURE__ */ jsx6(
304
1149
  Selector,
305
1150
  {
306
1151
  items,
307
1152
  initialIndex,
308
1153
  title: "Select model",
309
1154
  hint: "\u2191\u2193 navigate \xB7 Enter confirm \xB7 Esc cancel",
310
- onSubmit: (m) => resolve5(m),
311
- onCancel: () => resolve5(null),
312
- renderRow: (m, _focused) => /* @__PURE__ */ jsx5(ModelRow, { model: m, active: m.id === currentId, labelWidth })
1155
+ onSubmit: (m) => resolve6(m),
1156
+ onCancel: () => resolve6(null),
1157
+ renderRow: (m, _focused) => /* @__PURE__ */ jsx6(ModelRow, { model: m, active: m.id === currentId, labelWidth })
313
1158
  }
314
1159
  );
315
1160
  }
@@ -322,17 +1167,17 @@ function ModelRow({
322
1167
  const label = (model.name ?? model.id).padEnd(labelWidth);
323
1168
  const vendor = model.vendor ? `[${model.vendor}]` : "";
324
1169
  const caps = formatCaps(model);
325
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", children: [
326
- /* @__PURE__ */ jsxs5(Text5, { color: active ? "green" : void 0, children: [
1170
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "row", children: [
1171
+ /* @__PURE__ */ jsxs6(Text6, { color: active ? "green" : void 0, children: [
327
1172
  dot,
328
1173
  " "
329
1174
  ] }),
330
- /* @__PURE__ */ jsx5(Text5, { children: label }),
331
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1175
+ /* @__PURE__ */ jsx6(Text6, { children: label }),
1176
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
332
1177
  " ",
333
1178
  vendor
334
1179
  ] }),
335
- caps && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1180
+ caps && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
336
1181
  " ",
337
1182
  caps
338
1183
  ] })
@@ -346,15 +1191,15 @@ function formatCaps(m) {
346
1191
  }
347
1192
 
348
1193
  // src/components/SessionSelector.tsx
349
- import { Box as Box6, Text as Text6 } from "ink";
350
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1194
+ import { Box as Box6, Text as Text7 } from "ink";
1195
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
351
1196
  function SessionSelector({ request }) {
352
- const { items, currentId, resolve: resolve5 } = request;
1197
+ const { items, currentId, resolve: resolve6 } = request;
353
1198
  const initialIndex = Math.max(
354
1199
  0,
355
1200
  items.findIndex((s) => s.id === currentId)
356
1201
  );
357
- return /* @__PURE__ */ jsx6(
1202
+ return /* @__PURE__ */ jsx7(
358
1203
  Selector,
359
1204
  {
360
1205
  items,
@@ -362,9 +1207,9 @@ function SessionSelector({ request }) {
362
1207
  maxVisible: 12,
363
1208
  title: "Resume session",
364
1209
  hint: "\u2191\u2193 navigate \xB7 Enter load \xB7 Esc cancel",
365
- onSubmit: (s) => resolve5(s),
366
- onCancel: () => resolve5(null),
367
- renderRow: (s) => /* @__PURE__ */ jsx6(SessionRow, { session: s, active: s.id === currentId })
1210
+ onSubmit: (s) => resolve6(s),
1211
+ onCancel: () => resolve6(null),
1212
+ renderRow: (s) => /* @__PURE__ */ jsx7(SessionRow, { session: s, active: s.id === currentId })
368
1213
  }
369
1214
  );
370
1215
  }
@@ -373,18 +1218,18 @@ function SessionRow({ session, active }) {
373
1218
  const time = formatTime(session.createdAt);
374
1219
  const count = `[${String(session.messageCount).padStart(2)} msgs]`;
375
1220
  const preview = session.preview ?? "(empty)";
376
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", children: [
377
- /* @__PURE__ */ jsx6(Text6, { color: active ? "green" : void 0, children: active ? "\u25CF " : " " }),
378
- /* @__PURE__ */ jsx6(Text6, { children: id8 }),
379
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1221
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "row", children: [
1222
+ /* @__PURE__ */ jsx7(Text7, { color: active ? "green" : void 0, children: active ? "\u25CF " : " " }),
1223
+ /* @__PURE__ */ jsx7(Text7, { children: id8 }),
1224
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
380
1225
  " ",
381
1226
  time
382
1227
  ] }),
383
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1228
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
384
1229
  " ",
385
1230
  count
386
1231
  ] }),
387
- /* @__PURE__ */ jsxs6(Text6, { children: [
1232
+ /* @__PURE__ */ jsxs7(Text7, { children: [
388
1233
  " ",
389
1234
  preview
390
1235
  ] })
@@ -397,9 +1242,340 @@ function formatTime(iso) {
397
1242
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
398
1243
  }
399
1244
 
1245
+ // src/components/QuestionPicker.tsx
1246
+ import { useState as useState4 } from "react";
1247
+ import { Box as Box7, Text as Text8, useInput as useInput4 } from "ink";
1248
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1249
+ var POINTER_COLOR2 = "#A855F7";
1250
+ var FOCUS_BG = "#5B5598";
1251
+ var FOCUS_FG = "white";
1252
+ var SUBMIT_COLOR = "green";
1253
+ var NOTES_LABEL_COLOR = "yellow";
1254
+ var PREVIEW_BORDER = "gray";
1255
+ var TAB_GAP = 3;
1256
+ var PREVIEW_MIN_WIDTH = 28;
1257
+ function QuestionPicker({ request }) {
1258
+ const { questions } = request;
1259
+ const N = questions.length;
1260
+ const hasTabs = N > 1;
1261
+ const submitTabIndex = N;
1262
+ const [tabIndex, setTabIndex] = useState4(0);
1263
+ const [states, setStates] = useState4(
1264
+ () => questions.map(() => ({
1265
+ optionIndex: 0,
1266
+ selected: /* @__PURE__ */ new Set(),
1267
+ notes: "",
1268
+ notesEditing: false,
1269
+ notesDraft: ""
1270
+ }))
1271
+ );
1272
+ const currentQ = tabIndex < N ? questions[tabIndex] : null;
1273
+ const isMulti = currentQ?.multiSelect === true;
1274
+ const onSubmitChip = hasTabs && tabIndex === submitTabIndex;
1275
+ const anyPreview = !!currentQ?.options.some((o) => o.preview);
1276
+ const focusedOpt = currentQ?.options[states[tabIndex]?.optionIndex ?? 0];
1277
+ const focusedPreview = anyPreview ? focusedOpt?.preview ?? "" : "";
1278
+ const buildResponses = (cancelled) => {
1279
+ if (cancelled) return questions.map(() => ({ cancelled: true, selections: [] }));
1280
+ return questions.map((q, qi) => ({
1281
+ cancelled: false,
1282
+ selections: Array.from(states[qi].selected).sort((a, b) => a - b).map((i) => q.options[i].label),
1283
+ notes: states[qi].notes
1284
+ }));
1285
+ };
1286
+ const submit = () => request.resolve(buildResponses(false));
1287
+ const cancel = () => request.resolve(buildResponses(true));
1288
+ const updateState = (qi, mut) => {
1289
+ setStates((prev) => {
1290
+ const next = [...prev];
1291
+ next[qi] = mut(prev[qi]);
1292
+ return next;
1293
+ });
1294
+ };
1295
+ const toggleOption = (qi, oi) => {
1296
+ updateState(qi, (s) => {
1297
+ const sel = new Set(s.selected);
1298
+ if (sel.has(oi)) sel.delete(oi);
1299
+ else sel.add(oi);
1300
+ return { ...s, selected: sel };
1301
+ });
1302
+ };
1303
+ const selectSingleOption = (qi, oi) => {
1304
+ updateState(qi, (s) => ({ ...s, selected: /* @__PURE__ */ new Set([oi]) }));
1305
+ if (!hasTabs) {
1306
+ request.resolve([
1307
+ {
1308
+ cancelled: false,
1309
+ selections: [questions[0].options[oi].label],
1310
+ notes: states[0].notes
1311
+ }
1312
+ ]);
1313
+ return;
1314
+ }
1315
+ if (qi < N - 1) setTabIndex(qi + 1);
1316
+ else setTabIndex(submitTabIndex);
1317
+ };
1318
+ const singleQMultiSubmitRowIndex = !hasTabs && isMulti ? currentQ.options.length : -1;
1319
+ const optionRowCount = !hasTabs && isMulti ? currentQ.options.length + 1 : currentQ?.options.length ?? 0;
1320
+ const currentNotesEditing = tabIndex < N ? states[tabIndex].notesEditing : false;
1321
+ useInput4((input, key) => {
1322
+ if (currentNotesEditing) {
1323
+ if (key.escape) {
1324
+ updateState(tabIndex, (s2) => ({ ...s2, notesEditing: false, notesDraft: "" }));
1325
+ return;
1326
+ }
1327
+ if (key.return) {
1328
+ updateState(tabIndex, (s2) => ({
1329
+ ...s2,
1330
+ notesEditing: false,
1331
+ notes: s2.notesDraft,
1332
+ notesDraft: ""
1333
+ }));
1334
+ return;
1335
+ }
1336
+ if (key.backspace || key.delete) {
1337
+ updateState(tabIndex, (s2) => ({
1338
+ ...s2,
1339
+ notesDraft: s2.notesDraft.slice(0, -1)
1340
+ }));
1341
+ return;
1342
+ }
1343
+ if (key.ctrl || key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.meta) {
1344
+ return;
1345
+ }
1346
+ if (input) {
1347
+ updateState(tabIndex, (s2) => ({ ...s2, notesDraft: s2.notesDraft + input }));
1348
+ }
1349
+ return;
1350
+ }
1351
+ if (key.escape) {
1352
+ cancel();
1353
+ return;
1354
+ }
1355
+ if (hasTabs && (key.tab || key.leftArrow || key.rightArrow)) {
1356
+ if (key.leftArrow || key.shift && key.tab) {
1357
+ setTabIndex((t) => Math.max(0, t - 1));
1358
+ } else {
1359
+ setTabIndex((t) => Math.min(submitTabIndex, t + 1));
1360
+ }
1361
+ return;
1362
+ }
1363
+ if (onSubmitChip) {
1364
+ if (key.return) submit();
1365
+ return;
1366
+ }
1367
+ const qi = tabIndex;
1368
+ const s = states[qi];
1369
+ if (key.upArrow) {
1370
+ updateState(qi, (st) => ({ ...st, optionIndex: Math.max(0, st.optionIndex - 1) }));
1371
+ return;
1372
+ }
1373
+ if (key.downArrow) {
1374
+ updateState(qi, (st) => ({
1375
+ ...st,
1376
+ optionIndex: Math.min(optionRowCount - 1, st.optionIndex + 1)
1377
+ }));
1378
+ return;
1379
+ }
1380
+ if (input === "n" && !key.ctrl && !key.meta) {
1381
+ updateState(qi, (st) => ({
1382
+ ...st,
1383
+ notesEditing: true,
1384
+ notesDraft: st.notes
1385
+ }));
1386
+ return;
1387
+ }
1388
+ if (key.return) {
1389
+ if (!hasTabs && isMulti && s.optionIndex === singleQMultiSubmitRowIndex) {
1390
+ submit();
1391
+ return;
1392
+ }
1393
+ if (isMulti) toggleOption(qi, s.optionIndex);
1394
+ else selectSingleOption(qi, s.optionIndex);
1395
+ return;
1396
+ }
1397
+ if (isMulti && input === " " && s.optionIndex !== singleQMultiSubmitRowIndex) {
1398
+ toggleOption(qi, s.optionIndex);
1399
+ }
1400
+ });
1401
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginTop: 1, children: [
1402
+ /* @__PURE__ */ jsx8(Divider, {}),
1403
+ hasTabs && /* @__PURE__ */ jsx8(Box7, { marginTop: 0, children: /* @__PURE__ */ jsx8(TabBar, { questions, states, tabIndex }) }),
1404
+ tabIndex < N && /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginTop: 1, children: [
1405
+ /* @__PURE__ */ jsx8(Text8, { children: questions[tabIndex].question }),
1406
+ /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", marginTop: 1, children: [
1407
+ /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", flexGrow: anyPreview ? 0 : 1, flexShrink: 0, marginRight: anyPreview ? 2 : 0, children: /* @__PURE__ */ jsx8(
1408
+ OptionsList,
1409
+ {
1410
+ options: questions[tabIndex].options,
1411
+ focusedIndex: states[tabIndex].optionIndex,
1412
+ selected: states[tabIndex].selected,
1413
+ isMulti,
1414
+ submitRowIndex: singleQMultiSubmitRowIndex
1415
+ }
1416
+ ) }),
1417
+ anyPreview && /* @__PURE__ */ jsx8(Box7, { flexGrow: 1, minWidth: PREVIEW_MIN_WIDTH, children: /* @__PURE__ */ jsx8(PreviewPanel, { content: focusedPreview }) })
1418
+ ] }),
1419
+ /* @__PURE__ */ jsx8(
1420
+ NotesLine,
1421
+ {
1422
+ notes: states[tabIndex].notes,
1423
+ editing: states[tabIndex].notesEditing,
1424
+ draft: states[tabIndex].notesDraft
1425
+ }
1426
+ )
1427
+ ] }),
1428
+ onSubmitChip && /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx8(SubmitPreview, { questions, states }) }),
1429
+ /* @__PURE__ */ jsx8(Divider, {}),
1430
+ /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: hintLine(hasTabs, isMulti, currentNotesEditing) }) })
1431
+ ] });
1432
+ }
1433
+ function Divider() {
1434
+ return /* @__PURE__ */ jsx8(Box7, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray" });
1435
+ }
1436
+ function TabBar({
1437
+ questions,
1438
+ states,
1439
+ tabIndex
1440
+ }) {
1441
+ const submitIndex = questions.length;
1442
+ const canLeft = tabIndex > 0;
1443
+ const canRight = tabIndex < submitIndex;
1444
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", flexWrap: "wrap", children: [
1445
+ /* @__PURE__ */ jsx8(Box7, { marginRight: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: !canLeft, children: "\u2190" }) }),
1446
+ questions.map((q, i) => {
1447
+ const focused = i === tabIndex;
1448
+ const isMulti = q.multiSelect === true;
1449
+ const count = states[i].selected.size;
1450
+ const answered = count > 0;
1451
+ const mark = answered ? isMulti ? `[${count}]` : "\u2714" : "\u25A1";
1452
+ return /* @__PURE__ */ jsx8(Box7, { marginRight: TAB_GAP, children: /* @__PURE__ */ jsx8(
1453
+ Text8,
1454
+ {
1455
+ backgroundColor: focused ? FOCUS_BG : void 0,
1456
+ color: focused ? FOCUS_FG : void 0,
1457
+ bold: focused,
1458
+ dimColor: !focused,
1459
+ children: ` ${mark} ${q.header} `
1460
+ }
1461
+ ) }, i);
1462
+ }),
1463
+ /* @__PURE__ */ jsx8(Box7, { marginRight: 1, children: /* @__PURE__ */ jsx8(
1464
+ Text8,
1465
+ {
1466
+ backgroundColor: tabIndex === submitIndex ? FOCUS_BG : void 0,
1467
+ color: tabIndex === submitIndex ? FOCUS_FG : SUBMIT_COLOR,
1468
+ bold: tabIndex === submitIndex,
1469
+ dimColor: tabIndex !== submitIndex,
1470
+ children: " \u2714 Submit "
1471
+ }
1472
+ ) }),
1473
+ /* @__PURE__ */ jsx8(Text8, { dimColor: !canRight, children: "\u2192" })
1474
+ ] });
1475
+ }
1476
+ function OptionsList({
1477
+ options,
1478
+ focusedIndex,
1479
+ selected,
1480
+ isMulti,
1481
+ submitRowIndex
1482
+ }) {
1483
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1484
+ options.map((opt, i) => {
1485
+ const focused = i === focusedIndex;
1486
+ const checked = selected.has(i);
1487
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1488
+ /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", children: [
1489
+ /* @__PURE__ */ jsx8(Text8, { color: POINTER_COLOR2, bold: true, children: focused ? "\u203A " : " " }),
1490
+ isMulti && /* @__PURE__ */ jsx8(Text8, { color: checked ? "green" : void 0, children: checked ? "[x] " : "[ ] " }),
1491
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: `${i + 1}. ` }),
1492
+ /* @__PURE__ */ jsx8(Text8, { color: focused ? POINTER_COLOR2 : void 0, bold: focused, children: opt.label })
1493
+ ] }),
1494
+ opt.description && /* @__PURE__ */ jsx8(Box7, { marginLeft: isMulti ? 6 : 5, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, wrap: "truncate-end", children: opt.description }) })
1495
+ ] }, i);
1496
+ }),
1497
+ submitRowIndex >= 0 && /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", marginTop: 1, children: [
1498
+ /* @__PURE__ */ jsx8(Text8, { color: POINTER_COLOR2, bold: true, children: focusedIndex === submitRowIndex ? "\u203A " : " " }),
1499
+ /* @__PURE__ */ jsx8(
1500
+ Text8,
1501
+ {
1502
+ color: focusedIndex === submitRowIndex ? SUBMIT_COLOR : void 0,
1503
+ bold: focusedIndex === submitRowIndex,
1504
+ dimColor: focusedIndex !== submitRowIndex,
1505
+ children: `\u2500\u2500 Submit (${selected.size} selected)`
1506
+ }
1507
+ )
1508
+ ] })
1509
+ ] });
1510
+ }
1511
+ function PreviewPanel({ content }) {
1512
+ const lines = content ? content.split("\n") : ["(no preview)"];
1513
+ return /* @__PURE__ */ jsx8(
1514
+ Box7,
1515
+ {
1516
+ borderStyle: "round",
1517
+ borderColor: PREVIEW_BORDER,
1518
+ flexDirection: "column",
1519
+ paddingX: 1,
1520
+ flexGrow: 1,
1521
+ children: lines.map((line, i) => /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", dimColor: !content, children: line || " " }, i))
1522
+ }
1523
+ );
1524
+ }
1525
+ function NotesLine({
1526
+ notes,
1527
+ editing,
1528
+ draft
1529
+ }) {
1530
+ if (editing) {
1531
+ return /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "row", children: [
1532
+ /* @__PURE__ */ jsx8(Text8, { color: NOTES_LABEL_COLOR, bold: true, children: "Notes: " }),
1533
+ /* @__PURE__ */ jsx8(Text8, { children: draft }),
1534
+ /* @__PURE__ */ jsx8(Text8, { color: POINTER_COLOR2, children: "\u258E" }),
1535
+ /* @__PURE__ */ jsx8(Box7, { flexGrow: 1, marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "(Enter to save \xB7 Esc to discard)" }) })
1536
+ ] });
1537
+ }
1538
+ return /* @__PURE__ */ jsxs8(Box7, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: [
1539
+ /* @__PURE__ */ jsx8(Text8, { color: NOTES_LABEL_COLOR, bold: true, children: "Notes: " }),
1540
+ notes ? /* @__PURE__ */ jsx8(Text8, { children: notes }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "press n to add notes" })
1541
+ ] });
1542
+ }
1543
+ function SubmitPreview({
1544
+ questions,
1545
+ states
1546
+ }) {
1547
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1548
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Review" }),
1549
+ questions.map((q, qi) => {
1550
+ const sel = Array.from(states[qi].selected).sort((a, b) => a - b).map((i) => q.options[i].label);
1551
+ const notes = states[qi].notes.trim();
1552
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginLeft: 2, children: [
1553
+ /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", children: [
1554
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: `${q.header}: ` }),
1555
+ /* @__PURE__ */ jsx8(Text8, { dimColor: sel.length === 0, children: sel.length > 0 ? sel.join(", ") : "(no answer)" })
1556
+ ] }),
1557
+ notes && /* @__PURE__ */ jsxs8(Box7, { marginLeft: 2, flexDirection: "row", children: [
1558
+ /* @__PURE__ */ jsx8(Text8, { color: NOTES_LABEL_COLOR, children: "notes: " }),
1559
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: notes })
1560
+ ] })
1561
+ ] }, qi);
1562
+ })
1563
+ ] });
1564
+ }
1565
+ function hintLine(hasTabs, isMulti, editingNotes) {
1566
+ if (editingNotes) {
1567
+ return "Enter to save \xB7 Esc to discard \xB7 backspace to delete";
1568
+ }
1569
+ const parts = ["Enter to select", "\u2191/\u2193 to navigate", "n to add notes"];
1570
+ if (hasTabs) parts.push("Tab to switch questions");
1571
+ if (isMulti) parts.push("Space to toggle");
1572
+ parts.push("Esc to cancel");
1573
+ return parts.join(" \xB7 ");
1574
+ }
1575
+
400
1576
  // src/components/SlashAutocomplete.tsx
401
- import { Box as Box7, Text as Text7 } from "ink";
402
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1577
+ import { Box as Box8, Text as Text9 } from "ink";
1578
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
403
1579
  var DEFAULT_MAX = 10;
404
1580
  var SLASH_COLOR = "#A855F7";
405
1581
  function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
@@ -407,35 +1583,34 @@ function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
407
1583
  const start = Math.max(0, Math.min(index - Math.floor(maxVisible / 2), matches.length - maxVisible));
408
1584
  const end = Math.min(matches.length, start + maxVisible);
409
1585
  const visible = matches.slice(start, end);
410
- const nameWidth = Math.max(...matches.map((c) => c.name.length + (c.argsHint ? c.argsHint.length + 1 : 0)));
411
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
1586
+ const nameWidth = Math.max(...matches.map((c) => c.name.length));
1587
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginTop: 1, children: [
412
1588
  visible.map((cmd, i) => {
413
1589
  const realIndex = start + i;
414
- return /* @__PURE__ */ jsx7(Row, { cmd, focused: realIndex === index, nameWidth }, cmd.name);
1590
+ return /* @__PURE__ */ jsx9(Row, { cmd, focused: realIndex === index, nameWidth }, cmd.name);
415
1591
  }),
416
- matches.length > visible.length && /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1592
+ matches.length > visible.length && /* @__PURE__ */ jsx9(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
417
1593
  "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel (",
418
1594
  matches.length - visible.length,
419
1595
  " more)"
420
1596
  ] }) }),
421
- matches.length <= visible.length && /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel" }) })
1597
+ matches.length <= visible.length && /* @__PURE__ */ jsx9(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel" }) })
422
1598
  ] });
423
1599
  }
424
1600
  function Row({ cmd, focused, nameWidth }) {
425
- const head = cmd.argsHint ? `${cmd.name} ${cmd.argsHint}` : cmd.name;
426
- const padded = head.padEnd(nameWidth);
427
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "row", children: [
428
- /* @__PURE__ */ jsxs7(Text7, { color: focused ? SLASH_COLOR : void 0, bold: focused, children: [
1601
+ const padded = cmd.name.padEnd(nameWidth);
1602
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "row", children: [
1603
+ /* @__PURE__ */ jsxs9(Text9, { color: focused ? SLASH_COLOR : void 0, bold: focused, children: [
429
1604
  "/",
430
1605
  padded
431
1606
  ] }),
432
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
433
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: cmd.description })
1607
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
1608
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: cmd.description })
434
1609
  ] });
435
1610
  }
436
1611
 
437
1612
  // src/components/PermissionModeBar.tsx
438
- import { Box as Box8, Text as Text8 } from "ink";
1613
+ import { Box as Box9, Text as Text10 } from "ink";
439
1614
 
440
1615
  // src/permission/index.ts
441
1616
  var MODE_CYCLE = [
@@ -459,6 +1634,8 @@ var MODE_COLOR = {
459
1634
  var PermissionGate = class {
460
1635
  rules;
461
1636
  mode = "default";
1637
+ /** Session 级 allow:用户在 PermissionPrompt 选 "yes, for session" 后填充。 */
1638
+ sessionAllow = /* @__PURE__ */ new Set();
462
1639
  constructor(rules = {}) {
463
1640
  this.rules = {
464
1641
  allow: rules.allow ?? [],
@@ -478,8 +1655,16 @@ var PermissionGate = class {
478
1655
  this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
479
1656
  return this.mode;
480
1657
  }
1658
+ /** 用户在 PermissionPrompt 选 "yes, allow for session" 时记下。 */
1659
+ allowForSession(toolName) {
1660
+ this.sessionAllow.add(toolName);
1661
+ }
1662
+ isSessionAllowed(toolName) {
1663
+ return this.sessionAllow.has(toolName);
1664
+ }
481
1665
  decide(input) {
482
1666
  if (this.matches(this.rules.deny, input)) return "deny";
1667
+ if (this.sessionAllow.has(input.toolName)) return "allow";
483
1668
  switch (this.mode) {
484
1669
  case "bypassPermissions":
485
1670
  return "allow";
@@ -533,7 +1718,7 @@ var PermissionGate = class {
533
1718
  };
534
1719
 
535
1720
  // src/components/PermissionModeBar.tsx
536
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1721
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
537
1722
  function PermissionModeBar({ mode, compact }) {
538
1723
  const color = MODE_COLOR[mode];
539
1724
  const label = MODE_LABEL[mode];
@@ -545,20 +1730,235 @@ function PermissionModeBar({ mode, compact }) {
545
1730
  plan: "[plan]",
546
1731
  bypassPermissions: "[bypass]"
547
1732
  };
548
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
549
- /* @__PURE__ */ jsx8(Text8, { color, bold: isBypass, children: short[mode] }),
550
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " shift+tab" })
1733
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
1734
+ /* @__PURE__ */ jsx10(Text10, { color, bold: isBypass, children: short[mode] }),
1735
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " shift+tab" })
551
1736
  ] });
552
1737
  }
553
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
554
- /* @__PURE__ */ jsxs8(Text8, { color, bold: isBypass, children: [
555
- "\u25B6\u25B6 ",
1738
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
1739
+ /* @__PURE__ */ jsxs10(Text10, { color, bold: isBypass, children: [
1740
+ "\u25B8\u25B8 ",
556
1741
  label
557
1742
  ] }),
558
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " (shift+tab to cycle)" })
1743
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " (shift+tab to cycle)" })
559
1744
  ] });
560
1745
  }
561
1746
 
1747
+ // src/components/FooterStatus.tsx
1748
+ import { Box as Box10, Text as Text11 } from "ink";
1749
+ import { Fragment, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1750
+ var BAR_TOTAL_WIDE = 10;
1751
+ var BAR_TOTAL_COMPACT = 6;
1752
+ function FooterStatus({
1753
+ sessionId,
1754
+ model,
1755
+ contextWindow,
1756
+ lastInputTokens,
1757
+ sessionInputTokens,
1758
+ sessionOutputTokens,
1759
+ termWidth
1760
+ }) {
1761
+ const sid = sessionId.slice(0, 8);
1762
+ const hasCtx = contextWindow > 0;
1763
+ const pct = hasCtx ? Math.min(100, Math.round(lastInputTokens / contextWindow * 100)) : 0;
1764
+ const ctxColor = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
1765
+ const SEP = /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: " \u2502 " });
1766
+ const SEP_DOT = /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: " \xB7 " });
1767
+ const renderBar = (barW) => {
1768
+ const filled = Math.round(pct / 100 * barW);
1769
+ const empty = barW - filled;
1770
+ return /* @__PURE__ */ jsxs11(Fragment, { children: [
1771
+ filled > 0 && /* @__PURE__ */ jsx11(Text11, { color: ctxColor, children: "\u2588".repeat(filled) }),
1772
+ empty > 0 && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2591".repeat(empty) })
1773
+ ] });
1774
+ };
1775
+ if (termWidth < 60) {
1776
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "row", children: [
1777
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: sid }),
1778
+ SEP_DOT,
1779
+ /* @__PURE__ */ jsx11(Text11, { color: "magenta", children: model }),
1780
+ hasCtx && /* @__PURE__ */ jsxs11(Fragment, { children: [
1781
+ SEP_DOT,
1782
+ renderBar(BAR_TOTAL_COMPACT),
1783
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` ${pct}%` })
1784
+ ] })
1785
+ ] });
1786
+ }
1787
+ if (termWidth < 100) {
1788
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "row", children: [
1789
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: `@${sid}` }),
1790
+ SEP,
1791
+ /* @__PURE__ */ jsx11(Text11, { color: "magenta", children: model }),
1792
+ hasCtx && /* @__PURE__ */ jsxs11(Fragment, { children: [
1793
+ SEP,
1794
+ /* @__PURE__ */ jsx11(Text11, { children: "ctx: " }),
1795
+ renderBar(BAR_TOTAL_COMPACT),
1796
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` ${pct}%` })
1797
+ ] })
1798
+ ] });
1799
+ }
1800
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "row", children: [
1801
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: `@${sid}` }),
1802
+ SEP,
1803
+ /* @__PURE__ */ jsx11(Text11, { color: "magenta", children: model }),
1804
+ hasCtx && /* @__PURE__ */ jsxs11(Fragment, { children: [
1805
+ SEP,
1806
+ /* @__PURE__ */ jsx11(Text11, { children: "ctx: " }),
1807
+ renderBar(BAR_TOTAL_WIDE),
1808
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` ${pct}%` }),
1809
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` ${formatTokens(lastInputTokens)}/${formatTokens(contextWindow)}` })
1810
+ ] }),
1811
+ SEP,
1812
+ /* @__PURE__ */ jsx11(Text11, { children: "tok: " }),
1813
+ /* @__PURE__ */ jsx11(Text11, { color: "green", children: `\u2191${formatTokens(sessionInputTokens)}` }),
1814
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
1815
+ /* @__PURE__ */ jsx11(Text11, { color: "blueBright", children: `\u2193${formatTokens(sessionOutputTokens)}` })
1816
+ ] });
1817
+ }
1818
+ function formatTokens(n) {
1819
+ if (n < 1e3) return String(n);
1820
+ if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
1821
+ if (n < 1e6) return Math.round(n / 1e3) + "k";
1822
+ return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
1823
+ }
1824
+
1825
+ // src/components/ProgressBanner.tsx
1826
+ import { useEffect as useEffect2, useState as useState5 } from "react";
1827
+ import { Box as Box11, Text as Text12 } from "ink";
1828
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1829
+ var BAR_WIDTH = 42;
1830
+ var TICK_MS = 400;
1831
+ var TIP_ROTATE_SEC = 5;
1832
+ function ProgressBanner({ state }) {
1833
+ const [now, setNow] = useState5(Date.now());
1834
+ useEffect2(() => {
1835
+ const t = setInterval(() => setNow(Date.now()), TICK_MS);
1836
+ return () => clearInterval(t);
1837
+ }, []);
1838
+ const elapsedSec = Math.max(0, Math.floor((now - state.startTime) / 1e3));
1839
+ const percent = Math.max(0, Math.min(99, Math.floor(state.getPercent())));
1840
+ const filled = Math.floor(percent / 100 * BAR_WIDTH);
1841
+ const empty = BAR_WIDTH - filled;
1842
+ const bar = "\u25B0".repeat(filled) + "\u25B1".repeat(empty);
1843
+ const tip = state.tips.length ? state.tips[Math.floor(elapsedSec / TIP_ROTATE_SEC) % state.tips.length] : "";
1844
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", marginTop: 1, children: [
1845
+ /* @__PURE__ */ jsxs12(Box11, { children: [
1846
+ /* @__PURE__ */ jsx12(Text12, { color: "cyan", bold: true, children: "\u2726 " }),
1847
+ /* @__PURE__ */ jsxs12(Text12, { color: "cyan", children: [
1848
+ state.title,
1849
+ "..."
1850
+ ] }),
1851
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: ` (${elapsedSec}s)` })
1852
+ ] }),
1853
+ /* @__PURE__ */ jsxs12(Box11, { marginLeft: 2, children: [
1854
+ /* @__PURE__ */ jsx12(Text12, { color: "cyan", children: bar }),
1855
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: ` ${percent}%` })
1856
+ ] }),
1857
+ tip && /* @__PURE__ */ jsx12(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2514 Tip: ${tip}` }) })
1858
+ ] });
1859
+ }
1860
+
1861
+ // src/components/StatusLine.tsx
1862
+ import { useEffect as useEffect4, useState as useState7 } from "react";
1863
+ import { Box as Box12, Text as Text14 } from "ink";
1864
+
1865
+ // src/components/Shimmer.tsx
1866
+ import { useEffect as useEffect3, useState as useState6 } from "react";
1867
+ import { Text as Text13 } from "ink";
1868
+ import { jsx as jsx13 } from "react/jsx-runtime";
1869
+ var FRAME_MS = 100;
1870
+ var TRAIL = 4;
1871
+ function Shimmer({ text, bold = true }) {
1872
+ const chars = Array.from(text);
1873
+ const cycle = chars.length + TRAIL;
1874
+ const [phase, setPhase] = useState6(0);
1875
+ useEffect3(() => {
1876
+ const id = setInterval(() => {
1877
+ setPhase((p) => (p + 1) % cycle);
1878
+ }, FRAME_MS);
1879
+ return () => clearInterval(id);
1880
+ }, [cycle]);
1881
+ return /* @__PURE__ */ jsx13(Text13, { children: chars.map((ch, i) => {
1882
+ const d = Math.abs(i - phase);
1883
+ if (d === 0) {
1884
+ return /* @__PURE__ */ jsx13(Text13, { color: "white", bold, children: ch }, i);
1885
+ }
1886
+ if (d === 1) {
1887
+ return /* @__PURE__ */ jsx13(Text13, { color: "white", children: ch }, i);
1888
+ }
1889
+ return /* @__PURE__ */ jsx13(Text13, { color: "gray", dimColor: true, children: ch }, i);
1890
+ }) });
1891
+ }
1892
+
1893
+ // src/components/StatusLine.tsx
1894
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1895
+ var TICK_MS2 = 400;
1896
+ function StatusLine({ startTime, firstTextTime, inputTokens, runningTool, lang }) {
1897
+ const [now, setNow] = useState7(Date.now());
1898
+ useEffect4(() => {
1899
+ const t = setInterval(() => setNow(Date.now()), TICK_MS2);
1900
+ return () => clearInterval(t);
1901
+ }, []);
1902
+ const elapsedSec = Math.max(0, Math.floor((now - startTime) / 1e3));
1903
+ const mainLabel = lang === "zh-CN" ? "\u5DE5\u4F5C\u4E2D" : "Working";
1904
+ const parts = [formatDuration(elapsedSec)];
1905
+ if (inputTokens > 0) {
1906
+ parts.push(`\u2191 ${formatTokens2(inputTokens)} tokens`);
1907
+ }
1908
+ if (firstTextTime !== null) {
1909
+ const thinkSec = Math.max(0, Math.floor((firstTextTime - startTime) / 1e3));
1910
+ parts.push(
1911
+ lang === "zh-CN" ? `\u601D\u8003 ${formatDuration(thinkSec)}` : `thought for ${formatDuration(thinkSec)}`
1912
+ );
1913
+ }
1914
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginTop: 1, children: [
1915
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", children: [
1916
+ /* @__PURE__ */ jsx14(Text14, { color: "gray", children: "\u25CF " }),
1917
+ /* @__PURE__ */ jsx14(Shimmer, { text: mainLabel }),
1918
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` (${parts.join(" \xB7 ")})` })
1919
+ ] }),
1920
+ runningTool && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", marginLeft: 2, marginTop: 0, children: [
1921
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "\u21B3 " }),
1922
+ /* @__PURE__ */ jsx14(Text14, { color: "cyan", children: runningTool })
1923
+ ] })
1924
+ ] });
1925
+ }
1926
+ function formatTokens2(n) {
1927
+ if (n < 1e3) return String(n);
1928
+ if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
1929
+ if (n < 1e6) return Math.round(n / 1e3) + "k";
1930
+ return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
1931
+ }
1932
+ function formatDuration(sec) {
1933
+ if (sec < 60) return `${sec}s`;
1934
+ const m = Math.floor(sec / 60);
1935
+ const s = sec % 60;
1936
+ return s === 0 ? `${m}m` : `${m}m${s}s`;
1937
+ }
1938
+
1939
+ // src/ui/termTitle.ts
1940
+ var ENABLED = (() => {
1941
+ if (!process.stdout.isTTY) return false;
1942
+ if (process.env.MUSE_NO_TITLE === "1") return false;
1943
+ return true;
1944
+ })();
1945
+ var lastTitle = "";
1946
+ function sanitize(s) {
1947
+ return s.replace(/[\x00-\x1f\x7f]/g, "");
1948
+ }
1949
+ function setTerminalTitle(title) {
1950
+ if (!ENABLED) return;
1951
+ const clean = sanitize(title);
1952
+ if (clean === lastTitle) return;
1953
+ lastTitle = clean;
1954
+ process.stdout.write(`\x1B]0;${clean}\x07`);
1955
+ }
1956
+ function resetTerminalTitle() {
1957
+ if (!ENABLED) return;
1958
+ lastTitle = "";
1959
+ process.stdout.write(`\x1B]0;\x07`);
1960
+ }
1961
+
562
1962
  // src/llm/providers/openai-compatible.ts
563
1963
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
564
1964
  import { streamText, jsonSchema, tool } from "ai";
@@ -668,17 +2068,45 @@ var OpenAICompatibleClient = class {
668
2068
  const { messages, tools, systemPrompt, temperature, maxTokens, abortSignal } = opts;
669
2069
  const aiMessages = convertMessages(messages, systemPrompt);
670
2070
  const aiTools = tools ? convertTools(tools) : void 0;
2071
+ let attempt = 0;
2072
+ const maxAttempts = 3;
2073
+ let result;
2074
+ while (true) {
2075
+ try {
2076
+ result = streamText({
2077
+ model: this.modelProvider,
2078
+ messages: aiMessages,
2079
+ tools: aiTools,
2080
+ temperature,
2081
+ maxTokens,
2082
+ abortSignal
2083
+ });
2084
+ break;
2085
+ } catch (err) {
2086
+ if (abortSignal?.aborted) {
2087
+ yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
2088
+ return;
2089
+ }
2090
+ if (!isRetryable(err) || attempt >= maxAttempts - 1) {
2091
+ yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
2092
+ return;
2093
+ }
2094
+ const delay = 1e3 * Math.pow(2, attempt);
2095
+ log.warn(`LLM connect failed (attempt ${attempt + 1}/${maxAttempts}); retrying in ${delay}ms`, {
2096
+ msg: err instanceof Error ? err.message : String(err)
2097
+ });
2098
+ await sleep(delay, abortSignal);
2099
+ attempt += 1;
2100
+ }
2101
+ }
2102
+ if (!result) {
2103
+ yield { type: "error", error: new Error("Internal: stream result is undefined after retry loop.") };
2104
+ return;
2105
+ }
2106
+ const stream = result.fullStream;
671
2107
  try {
672
- const result = streamText({
673
- model: this.modelProvider,
674
- messages: aiMessages,
675
- tools: aiTools,
676
- temperature,
677
- maxTokens,
678
- abortSignal
679
- });
680
2108
  const seenToolCalls = /* @__PURE__ */ new Set();
681
- for await (const part of result.fullStream) {
2109
+ for await (const part of stream) {
682
2110
  switch (part.type) {
683
2111
  case "text-delta":
684
2112
  yield { type: "text", delta: part.textDelta };
@@ -784,6 +2212,33 @@ function convertTools(tools) {
784
2212
  }
785
2213
  return result;
786
2214
  }
2215
+ function isRetryable(err) {
2216
+ if (!(err instanceof Error)) return false;
2217
+ const msg = err.message.toLowerCase();
2218
+ const code = err.code ?? "";
2219
+ if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "ECONNREFUSED" || code === "EAI_AGAIN") {
2220
+ return true;
2221
+ }
2222
+ if (msg.includes("fetch failed") || msg.includes("network") || msg.includes("socket hang up") || msg.includes("under maintenance") || msg.includes("rate limit") || msg.includes("429") || msg.includes("502") || msg.includes("503") || msg.includes("504")) {
2223
+ return true;
2224
+ }
2225
+ return false;
2226
+ }
2227
+ async function sleep(ms, abortSignal) {
2228
+ await new Promise((resolve6, reject) => {
2229
+ if (abortSignal?.aborted) return reject(new Error("aborted"));
2230
+ const t = setTimeout(() => {
2231
+ abortSignal?.removeEventListener("abort", onAbort);
2232
+ resolve6();
2233
+ }, ms);
2234
+ const onAbort = () => {
2235
+ clearTimeout(t);
2236
+ abortSignal?.removeEventListener("abort", onAbort);
2237
+ reject(new Error("aborted"));
2238
+ };
2239
+ abortSignal?.addEventListener("abort", onAbort);
2240
+ });
2241
+ }
787
2242
  function mapFinishReason(reason) {
788
2243
  switch (reason) {
789
2244
  case "stop":
@@ -884,10 +2339,7 @@ function setActiveModelEnv(entry) {
884
2339
  function createLLMClientFromModelEntry(entry) {
885
2340
  const apiKey = process.env[ACTIVE_API_KEY_ENV] ?? "";
886
2341
  if (!apiKey && !entry.baseUrl.includes("localhost")) {
887
- throw new MuseError(
888
- `Model "${entry.id}" has no apiKey in env ${ACTIVE_API_KEY_ENV}. Check models.json (or models.local.json) and ensure setActiveModelEnv() was called.`,
889
- "MISSING_API_KEY"
890
- );
2342
+ throw new MuseError(buildMissingKeyMessage(entry), "MISSING_API_KEY");
891
2343
  }
892
2344
  const capabilities = {};
893
2345
  if (entry.supportsToolCall !== void 0) capabilities.toolCalling = entry.supportsToolCall;
@@ -901,6 +2353,34 @@ function createLLMClientFromModelEntry(entry) {
901
2353
  capabilities
902
2354
  });
903
2355
  }
2356
+ function buildMissingKeyMessage(entry) {
2357
+ const envVars = (entry._apiKeyEnvVars ?? []).filter(
2358
+ (v) => !process.env[v]
2359
+ );
2360
+ const head = `Model "${entry.id}" needs an API key but none was found.`;
2361
+ if (envVars.length > 0) {
2362
+ const list = envVars.map((v) => `$${v}`).join(", ");
2363
+ const fixVar = envVars[0];
2364
+ return [
2365
+ head,
2366
+ ``,
2367
+ `Cause: ~/.muse/models.local.json sets apiKey to a placeholder referencing ${list},`,
2368
+ ` but the shell environment does not have ${envVars.length > 1 ? "those variables" : "that variable"} set.`,
2369
+ ``,
2370
+ `Fix (pick one):`,
2371
+ ` 1. Replace the \${${fixVar}} placeholder in ~/.muse/models.local.json with the literal key`,
2372
+ ` (recommended \u2014 the file is local-only and never enters git).`,
2373
+ ` 2. Export the variable in your shell:`,
2374
+ ` export ${fixVar}=<your-key>`
2375
+ ].join("\n");
2376
+ }
2377
+ return [
2378
+ head,
2379
+ ``,
2380
+ `Edit ~/.muse/models.local.json and set "apiKey" on the "${entry.id}" entry`,
2381
+ `(plain text is fine \u2014 the file stays local-only).`
2382
+ ].join("\n");
2383
+ }
904
2384
  function createLLMClient(opts) {
905
2385
  const { provider, model, providers } = opts;
906
2386
  const config = providers[provider];
@@ -933,6 +2413,32 @@ function createLLMClient(opts) {
933
2413
  );
934
2414
  }
935
2415
 
2416
+ // src/loop/todos.ts
2417
+ var TodoStore = class {
2418
+ items = [];
2419
+ list() {
2420
+ return this.items.slice();
2421
+ }
2422
+ set(items) {
2423
+ this.items = items.slice();
2424
+ }
2425
+ clear() {
2426
+ this.items = [];
2427
+ }
2428
+ /** 把当前清单格式化为 system prompt 段落;无任务时返回空串。 */
2429
+ toPromptSection() {
2430
+ if (this.items.length === 0) return "";
2431
+ const lines = this.items.map((t, i) => {
2432
+ const marker = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
2433
+ return ` ${i + 1}. ${marker} ${t.content}`;
2434
+ });
2435
+ return `# Current todos
2436
+ ${lines.join("\n")}
2437
+
2438
+ Update via TodoWrite as you make progress. Keep exactly one item in_progress at a time.`;
2439
+ }
2440
+ };
2441
+
936
2442
  // src/loop/agent.ts
937
2443
  var Agent = class {
938
2444
  constructor(ctx) {
@@ -940,6 +2446,7 @@ var Agent = class {
940
2446
  }
941
2447
  ctx;
942
2448
  messages = [];
2449
+ todos = new TodoStore();
943
2450
  getMessages() {
944
2451
  return this.messages;
945
2452
  }
@@ -956,10 +2463,14 @@ var Agent = class {
956
2463
  const tools = this.ctx.tools.toLLMDefinitions(
957
2464
  mode === "plan" ? (t) => t.permission === "read" : void 0
958
2465
  );
2466
+ const todoSection = this.todos.toPromptSection();
2467
+ const systemPrompt = todoSection ? `${this.ctx.systemPrompt}
2468
+
2469
+ ${todoSection}` : this.ctx.systemPrompt;
959
2470
  const stream = this.ctx.llm.stream({
960
2471
  messages: this.messages,
961
2472
  tools,
962
- systemPrompt: this.ctx.systemPrompt,
2473
+ systemPrompt,
963
2474
  abortSignal: this.ctx.abortSignal
964
2475
  });
965
2476
  const assistantParts = [];
@@ -983,6 +2494,7 @@ var Agent = class {
983
2494
  this.ctx.events?.onTurnEnd?.();
984
2495
  return;
985
2496
  }
2497
+ this.ctx.events?.onAssistantTurn?.();
986
2498
  for (const call of toolCallsToRun) {
987
2499
  await this.runToolCall(call);
988
2500
  }
@@ -1048,27 +2560,37 @@ var Agent = class {
1048
2560
  return;
1049
2561
  }
1050
2562
  if (decision === "ask") {
1051
- approved = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? false;
1052
- if (!approved) {
2563
+ const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
2564
+ if (userDecision === "no") {
1053
2565
  this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
1054
2566
  return;
1055
2567
  }
2568
+ if (userDecision === "session_allow") {
2569
+ this.ctx.permissions.allowForSession(call.name);
2570
+ }
2571
+ approved = true;
1056
2572
  }
1057
2573
  const toolCtx = {
1058
2574
  cwd: this.ctx.cwd,
1059
2575
  abortSignal: this.ctx.abortSignal,
1060
- askPermission: async () => true
2576
+ askPermission: async () => true,
1061
2577
  // 已在外层处理
2578
+ todos: this.todos,
2579
+ askQuestions: this.ctx.events?.onAskQuestions ? (qs) => this.ctx.events.onAskQuestions(qs) : void 0
1062
2580
  };
1063
2581
  const result = await this.ctx.tools.execute(call.name, call.args, toolCtx);
1064
- this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary);
2582
+ this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary, result.diff, result.kind);
1065
2583
  }
1066
- recordToolResult(id, name, content, isError, summary) {
2584
+ recordToolResult(id, name, content, isError, summary, diff, kind) {
1067
2585
  const toolMsg = {
1068
2586
  role: "tool",
1069
2587
  toolUseId: id,
1070
2588
  content,
1071
- isError
2589
+ isError,
2590
+ toolName: name,
2591
+ ...diff ? { diff } : {},
2592
+ ...summary ? { summary } : {},
2593
+ ...kind ? { kind } : {}
1072
2594
  };
1073
2595
  this.messages.push(toolMsg);
1074
2596
  this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
@@ -1079,7 +2601,7 @@ var Agent = class {
1079
2601
  // src/loop/system-prompt.ts
1080
2602
  import { homedir as homedir2 } from "os";
1081
2603
  function buildSystemPrompt(opts) {
1082
- const { cwd, model, provider, lang, toolNames } = opts;
2604
+ const { cwd, model, provider, lang, toolNames, memoryIndex } = opts;
1083
2605
  const home = homedir2();
1084
2606
  const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
1085
2607
  const sections = [];
@@ -1104,18 +2626,113 @@ Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit f
1104
2626
  - If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
1105
2627
  - When the user asks a question that does not need tools, just answer.`
1106
2628
  );
2629
+ if (toolNames.includes("TodoWrite")) {
2630
+ sections.push(
2631
+ `# Task management
2632
+ - For non-trivial, multi-step work, use TodoWrite to plan and track progress.
2633
+ - Keep exactly one task in_progress; mark a task completed immediately when done.
2634
+ - Skip it for trivial single-step requests.`
2635
+ );
2636
+ }
1107
2637
  if (lang === "zh-CN") {
1108
2638
  sections.push(`# Output language
1109
2639
  Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
1110
2640
  }
1111
- return sections.join("\n\n");
2641
+ if (memoryIndex && memoryIndex.trim()) {
2642
+ sections.push(
2643
+ `# Memory (long-term)
2644
+ Below is MEMORY.md \u2014 your index of persistent facts about the user, project, and prior feedback. Each line points at a file you can MemoryRead. Use MemoryWrite to record new durable knowledge (user role/preferences, validated decisions, project facts, external references). Do NOT save things derivable from the repo or git history.
2645
+
2646
+ ` + memoryIndex
2647
+ );
2648
+ }
2649
+ return sections.join("\n\n");
2650
+ }
2651
+
2652
+ // src/loop/memory.ts
2653
+ import { mkdir, readFile, writeFile } from "fs/promises";
2654
+ import { existsSync } from "fs";
2655
+ import { homedir as homedir3 } from "os";
2656
+ import { join as join2 } from "path";
2657
+ import { createHash } from "crypto";
2658
+ function projectHash(cwd) {
2659
+ return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
2660
+ }
2661
+ function memoryDir(cwd) {
2662
+ return join2(homedir3(), ".muse", "projects", projectHash(cwd), "memory");
2663
+ }
2664
+ function memoryIndexPath(cwd) {
2665
+ return join2(memoryDir(cwd), "MEMORY.md");
2666
+ }
2667
+ function memoryFilePath(cwd, name) {
2668
+ return join2(memoryDir(cwd), `${name}.md`);
2669
+ }
2670
+ async function loadMemoryIndex(cwd, maxLines = 200) {
2671
+ const path = memoryIndexPath(cwd);
2672
+ if (!existsSync(path)) return "";
2673
+ try {
2674
+ const raw = await readFile(path, "utf-8");
2675
+ const lines = raw.split("\n");
2676
+ if (lines.length <= maxLines) return raw.trim();
2677
+ return lines.slice(0, maxLines).join("\n").trim() + `
2678
+ ... [truncated; ${lines.length - maxLines} more lines]`;
2679
+ } catch {
2680
+ return "";
2681
+ }
2682
+ }
2683
+ async function readMemoryFile(cwd, name) {
2684
+ const path = memoryFilePath(cwd, name);
2685
+ if (!existsSync(path)) {
2686
+ throw new Error(`Memory "${name}" does not exist at ${path}.`);
2687
+ }
2688
+ return readFile(path, "utf-8");
2689
+ }
2690
+ async function writeMemory(cwd, opts) {
2691
+ const dir = memoryDir(cwd);
2692
+ await mkdir(dir, { recursive: true });
2693
+ const filePath = memoryFilePath(cwd, opts.name);
2694
+ const frontmatter = [
2695
+ "---",
2696
+ `name: ${opts.name}`,
2697
+ `description: ${opts.description.replace(/\n/g, " ").trim()}`,
2698
+ `metadata:`,
2699
+ ` type: ${opts.type}`,
2700
+ "---"
2701
+ ].join("\n");
2702
+ const content = `${frontmatter}
2703
+
2704
+ ${opts.body.trim()}
2705
+ `;
2706
+ await writeFile(filePath, content, "utf-8");
2707
+ const indexPath = memoryIndexPath(cwd);
2708
+ let index = "";
2709
+ if (existsSync(indexPath)) index = await readFile(indexPath, "utf-8");
2710
+ const lines = index ? index.split("\n") : [];
2711
+ const linePrefix = `- [${opts.name}](${opts.name}.md)`;
2712
+ const newLine = `${linePrefix} \u2014 ${opts.description.replace(/\n/g, " ").trim()}`;
2713
+ const existing = lines.findIndex((l) => l.startsWith(linePrefix));
2714
+ let indexUpdated = false;
2715
+ if (existing >= 0) {
2716
+ if (lines[existing] !== newLine) {
2717
+ lines[existing] = newLine;
2718
+ indexUpdated = true;
2719
+ }
2720
+ } else {
2721
+ lines.push(newLine);
2722
+ indexUpdated = true;
2723
+ }
2724
+ if (indexUpdated) {
2725
+ const out = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
2726
+ await writeFile(indexPath, out, "utf-8");
2727
+ }
2728
+ return { filePath, indexUpdated };
1112
2729
  }
1113
2730
 
1114
2731
  // src/config/loader.ts
1115
- import { readFile } from "fs/promises";
1116
- import { existsSync } from "fs";
1117
- import { homedir as homedir3 } from "os";
1118
- import { join as join2, resolve } from "path";
2732
+ import { readFile as readFile2 } from "fs/promises";
2733
+ import { existsSync as existsSync2 } from "fs";
2734
+ import { homedir as homedir4 } from "os";
2735
+ import { join as join3, resolve } from "path";
1119
2736
 
1120
2737
  // src/config/types.ts
1121
2738
  import { z } from "zod";
@@ -1125,8 +2742,8 @@ var ProviderConfigSchema = z.object({
1125
2742
  extraHeaders: z.record(z.string()).optional()
1126
2743
  }).passthrough();
1127
2744
  var LLMConfigSchema = z.object({
1128
- provider: z.string().optional().describe("Fallback provider preset (only used when no models.json entry matches)."),
1129
- model: z.string().optional().describe("Active model id; should match an id in models.json."),
2745
+ provider: z.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
2746
+ model: z.string().optional().describe("Active model id; should match an id in models.local.json."),
1130
2747
  temperature: z.number().min(0).max(2).optional(),
1131
2748
  maxTokens: z.number().int().positive().optional()
1132
2749
  });
@@ -1191,7 +2808,7 @@ var DEFAULTS = {
1191
2808
  ollama: { baseUrl: "http://localhost:11434/v1" }
1192
2809
  },
1193
2810
  permissions: {
1194
- allow: ["Read", "Grep", "Glob"],
2811
+ allow: ["Read", "Grep", "Glob", "TodoWrite"],
1195
2812
  ask: ["Write", "Edit", "Bash"],
1196
2813
  deny: [],
1197
2814
  defaultMode: "ask"
@@ -1202,9 +2819,9 @@ var DEFAULTS = {
1202
2819
  }
1203
2820
  };
1204
2821
  async function readJsonIfExists(path) {
1205
- if (!existsSync(path)) return void 0;
2822
+ if (!existsSync2(path)) return void 0;
1206
2823
  try {
1207
- const raw = await readFile(path, "utf-8");
2824
+ const raw = await readFile2(path, "utf-8");
1208
2825
  return JSON.parse(raw);
1209
2826
  } catch (err) {
1210
2827
  log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
@@ -1232,9 +2849,9 @@ async function loadSettings(cwd = process.cwd()) {
1232
2849
  const sources = ["<defaults>"];
1233
2850
  let merged = DEFAULTS;
1234
2851
  const candidates = [
1235
- join2(homedir3(), ".muse", "settings.json"),
1236
- join2(cwd, ".muse", "settings.json"),
1237
- join2(cwd, ".muse", "settings.local.json")
2852
+ join3(homedir4(), ".muse", "settings.json"),
2853
+ join3(cwd, ".muse", "settings.json"),
2854
+ join3(cwd, ".muse", "settings.local.json")
1238
2855
  ];
1239
2856
  for (const path of candidates) {
1240
2857
  const raw = await readJsonIfExists(path);
@@ -1261,10 +2878,10 @@ async function loadSettings(cwd = process.cwd()) {
1261
2878
  }
1262
2879
 
1263
2880
  // src/config/models.ts
1264
- import { readFile as readFile2 } from "fs/promises";
1265
- import { existsSync as existsSync2 } from "fs";
1266
- import { homedir as homedir4 } from "os";
1267
- import { join as join3 } from "path";
2881
+ import { readFile as readFile3 } from "fs/promises";
2882
+ import { existsSync as existsSync3 } from "fs";
2883
+ import { homedir as homedir5 } from "os";
2884
+ import { join as join4 } from "path";
1268
2885
  import { z as z2 } from "zod";
1269
2886
  var ModelEntryInputSchema = z2.object({
1270
2887
  id: z2.string().min(1),
@@ -1285,65 +2902,61 @@ var ModelsRegistryInputSchema = z2.object({
1285
2902
  /** 不填 = 全部 models 都进 selector;填了就是 selector 子集(按顺序)。 */
1286
2903
  availableModels: z2.array(z2.string()).optional()
1287
2904
  }).passthrough();
1288
- var CANDIDATES = () => [
1289
- join3(homedir4(), ".muse", "models.json"),
1290
- join3(homedir4(), ".muse", "models.local.json")
1291
- ];
2905
+ var MODELS_PATH = () => join4(homedir5(), ".muse", "models.local.json");
1292
2906
  async function loadModelsRegistry() {
1293
2907
  const sources = [];
1294
2908
  const errors = [];
1295
- let merged;
1296
- for (const path of CANDIDATES()) {
1297
- if (!existsSync2(path)) continue;
1298
- let raw;
1299
- try {
1300
- raw = JSON.parse(await readFile2(path, "utf-8"));
1301
- } catch (err) {
1302
- const msg = `JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
1303
- log.warn(`Failed to parse ${path}: ${msg}`);
1304
- errors.push({ path, message: msg });
1305
- continue;
1306
- }
1307
- const parsed = ModelsRegistryInputSchema.safeParse(raw);
1308
- if (!parsed.success) {
1309
- const msg = formatZodIssues2(parsed.error.issues);
1310
- log.warn(`Invalid models registry at ${path}: ${msg}`);
1311
- errors.push({ path, message: msg });
1312
- continue;
1313
- }
1314
- const normalized = {
1315
- ...parsed.data,
1316
- models: parsed.data.models.map(normalizeModelEntry)
1317
- };
1318
- merged = mergeRegistries(merged, normalized);
1319
- sources.push(path);
2909
+ const path = MODELS_PATH();
2910
+ if (!existsSync3(path)) {
2911
+ return { registry: void 0, sources, errors };
2912
+ }
2913
+ let raw;
2914
+ try {
2915
+ raw = JSON.parse(await readFile3(path, "utf-8"));
2916
+ } catch (err) {
2917
+ const msg = `JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
2918
+ log.warn(`Failed to parse ${path}: ${msg}`);
2919
+ errors.push({ path, message: msg });
2920
+ return { registry: void 0, sources, errors };
1320
2921
  }
1321
- if (!merged) return { registry: void 0, sources, errors };
1322
- const expanded = expandEnvVars(merged);
2922
+ const parsed = ModelsRegistryInputSchema.safeParse(raw);
2923
+ if (!parsed.success) {
2924
+ const msg = formatZodIssues2(parsed.error.issues);
2925
+ log.warn(`Invalid models registry at ${path}: ${msg}`);
2926
+ errors.push({ path, message: msg });
2927
+ return { registry: void 0, sources, errors };
2928
+ }
2929
+ const normalized = {
2930
+ ...parsed.data,
2931
+ models: parsed.data.models.map(normalizeModelEntry)
2932
+ };
2933
+ sources.push(path);
2934
+ const expanded = expandEnvVars(normalized);
1323
2935
  return { registry: expanded, sources, errors };
1324
2936
  }
1325
2937
  function formatZodIssues2(issues) {
1326
2938
  return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
1327
2939
  }
1328
- function mergeRegistries(low, high) {
1329
- if (!low) return high;
1330
- const byId = /* @__PURE__ */ new Map();
1331
- for (const m of low.models) byId.set(m.id, m);
1332
- for (const m of high.models) byId.set(m.id, m);
1333
- return {
1334
- ...low,
1335
- ...high,
1336
- models: [...byId.values()],
1337
- availableModels: high.availableModels ?? low.availableModels
1338
- };
1339
- }
1340
2940
  function normalizeModelEntry(entry) {
1341
2941
  let baseUrl = (entry.baseUrl ?? entry.url ?? "").replace(/\/+$/, "");
1342
2942
  if (baseUrl.endsWith("/chat/completions")) {
1343
2943
  baseUrl = baseUrl.slice(0, -"/chat/completions".length);
1344
2944
  }
1345
2945
  const { url: _url, ...rest } = entry;
1346
- return { ...rest, baseUrl };
2946
+ const apiKeyEnvVars = entry.apiKey ? extractEnvVars(entry.apiKey) : [];
2947
+ return {
2948
+ ...rest,
2949
+ baseUrl,
2950
+ ...apiKeyEnvVars.length > 0 ? { _apiKeyEnvVars: apiKeyEnvVars } : {}
2951
+ };
2952
+ }
2953
+ var ENV_PLACEHOLDER = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
2954
+ function extractEnvVars(s) {
2955
+ const out = [];
2956
+ let m;
2957
+ ENV_PLACEHOLDER.lastIndex = 0;
2958
+ while ((m = ENV_PLACEHOLDER.exec(s)) !== null) out.push(m[1]);
2959
+ return out;
1347
2960
  }
1348
2961
  function findEntry(registry, modelId) {
1349
2962
  return registry.models.find((m) => m.id === modelId);
@@ -1452,7 +3065,7 @@ async function compactMessages(messages, opts) {
1452
3065
  }
1453
3066
  const older = messages.slice(0, cutoff);
1454
3067
  const recent = messages.slice(cutoff);
1455
- const summary = await summarizeConversation(older, opts.llm, opts.abortSignal);
3068
+ const summary = await summarizeConversation(older, opts.llm, opts.abortSignal, opts.onProgress);
1456
3069
  const summaryMessage = {
1457
3070
  role: "user",
1458
3071
  content: `[Previous conversation summary]
@@ -1497,7 +3110,7 @@ function hasUnresolvedToolUse(older) {
1497
3110
  }
1498
3111
  return false;
1499
3112
  }
1500
- async function summarizeConversation(older, llm, abortSignal) {
3113
+ async function summarizeConversation(older, llm, abortSignal, onProgress) {
1501
3114
  const transcript = renderTranscript(older);
1502
3115
  const prompt = [
1503
3116
  {
@@ -1517,8 +3130,10 @@ ${transcript}
1517
3130
  ];
1518
3131
  let text = "";
1519
3132
  for await (const ev of llm.stream({ messages: prompt, abortSignal })) {
1520
- if (ev.type === "text") text += ev.delta;
1521
- else if (ev.type === "error") throw ev.error;
3133
+ if (ev.type === "text") {
3134
+ text += ev.delta;
3135
+ onProgress?.(text.length);
3136
+ } else if (ev.type === "error") throw ev.error;
1522
3137
  }
1523
3138
  return text.trim() || "(empty summary)";
1524
3139
  }
@@ -1579,16 +3194,16 @@ function getMCPStatus(settings) {
1579
3194
  }
1580
3195
 
1581
3196
  // src/session/jsonl.ts
1582
- import { appendFile, mkdir, readdir, readFile as readFile3, stat } from "fs/promises";
1583
- import { existsSync as existsSync3 } from "fs";
1584
- import { homedir as homedir5 } from "os";
1585
- import { dirname as dirname2, join as join4 } from "path";
1586
- import { createHash, randomUUID } from "crypto";
1587
- function projectHash(cwd) {
1588
- return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
3197
+ import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4, stat } from "fs/promises";
3198
+ import { existsSync as existsSync4 } from "fs";
3199
+ import { homedir as homedir6 } from "os";
3200
+ import { dirname as dirname2, join as join5 } from "path";
3201
+ import { createHash as createHash2, randomUUID } from "crypto";
3202
+ function projectHash2(cwd) {
3203
+ return createHash2("sha256").update(cwd).digest("hex").slice(0, 16);
1589
3204
  }
1590
3205
  function sessionsDir(cwd) {
1591
- return join4(homedir5(), ".muse", "projects", projectHash(cwd), "sessions");
3206
+ return join5(homedir6(), ".muse", "projects", projectHash2(cwd), "sessions");
1592
3207
  }
1593
3208
  var Session = class _Session {
1594
3209
  meta;
@@ -1599,8 +3214,8 @@ var Session = class _Session {
1599
3214
  static async create(cwd) {
1600
3215
  const id = randomUUID();
1601
3216
  const dir = sessionsDir(cwd);
1602
- await mkdir(dir, { recursive: true });
1603
- const path = join4(dir, `${id}.jsonl`);
3217
+ await mkdir2(dir, { recursive: true });
3218
+ const path = join5(dir, `${id}.jsonl`);
1604
3219
  const meta = {
1605
3220
  id,
1606
3221
  cwd,
@@ -1616,7 +3231,7 @@ var Session = class _Session {
1616
3231
  }
1617
3232
  static async resolve(cwd, idOrPrefix) {
1618
3233
  const dir = sessionsDir(cwd);
1619
- if (!existsSync3(dir)) return void 0;
3234
+ if (!existsSync4(dir)) return void 0;
1620
3235
  const entries = await readdir(dir);
1621
3236
  const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
1622
3237
  if (matches.length === 0) return void 0;
@@ -1624,12 +3239,12 @@ var Session = class _Session {
1624
3239
  throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
1625
3240
  }
1626
3241
  const top = matches[0];
1627
- const st = await stat(join4(dir, top));
3242
+ const st = await stat(join5(dir, top));
1628
3243
  return {
1629
3244
  id: top.replace(/\.jsonl$/, ""),
1630
3245
  cwd,
1631
3246
  createdAt: st.mtime.toISOString(),
1632
- path: join4(dir, top)
3247
+ path: join5(dir, top)
1633
3248
  };
1634
3249
  }
1635
3250
  /**
@@ -1638,13 +3253,13 @@ var Session = class _Session {
1638
3253
  */
1639
3254
  static async listAll(cwd, limit) {
1640
3255
  const dir = sessionsDir(cwd);
1641
- if (!existsSync3(dir)) return [];
3256
+ if (!existsSync4(dir)) return [];
1642
3257
  const entries = await readdir(dir);
1643
3258
  const files = entries.filter((e) => e.endsWith(".jsonl"));
1644
3259
  if (files.length === 0) return [];
1645
3260
  const stats = await Promise.all(
1646
3261
  files.map(async (f) => {
1647
- const path = join4(dir, f);
3262
+ const path = join5(dir, f);
1648
3263
  const st = await stat(path);
1649
3264
  return { file: f, path, mtime: st.mtime };
1650
3265
  })
@@ -1681,7 +3296,7 @@ var Session = class _Session {
1681
3296
  const line = JSON.stringify(event) + "\n";
1682
3297
  this.writeQueue = this.writeQueue.then(async () => {
1683
3298
  try {
1684
- await mkdir(dirname2(this.meta.path), { recursive: true });
3299
+ await mkdir2(dirname2(this.meta.path), { recursive: true });
1685
3300
  await appendFile(this.meta.path, line, "utf-8");
1686
3301
  } catch (err) {
1687
3302
  log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -1690,8 +3305,8 @@ var Session = class _Session {
1690
3305
  return this.writeQueue;
1691
3306
  }
1692
3307
  async readAll() {
1693
- if (!existsSync3(this.meta.path)) return [];
1694
- const raw = await readFile3(this.meta.path, "utf-8");
3308
+ if (!existsSync4(this.meta.path)) return [];
3309
+ const raw = await readFile4(this.meta.path, "utf-8");
1695
3310
  const events = [];
1696
3311
  for (const line of raw.split("\n")) {
1697
3312
  if (!line.trim()) continue;
@@ -1706,7 +3321,7 @@ var Session = class _Session {
1706
3321
  async function readSummary(meta) {
1707
3322
  let events = [];
1708
3323
  try {
1709
- const raw = await readFile3(meta.path, "utf-8");
3324
+ const raw = await readFile4(meta.path, "utf-8");
1710
3325
  for (const line of raw.split("\n")) {
1711
3326
  if (!line.trim()) continue;
1712
3327
  try {
@@ -1728,9 +3343,9 @@ async function readSummary(meta) {
1728
3343
  }
1729
3344
 
1730
3345
  // src/slash/_format.ts
1731
- import { homedir as homedir6 } from "os";
3346
+ import { homedir as homedir7 } from "os";
1732
3347
  function shortPath(p) {
1733
- const home = homedir6();
3348
+ const home = homedir7();
1734
3349
  if (p === home) return "~";
1735
3350
  if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
1736
3351
  return p;
@@ -1828,6 +3443,18 @@ var COST = {
1828
3443
  return { display: lines.join("\n") };
1829
3444
  }
1830
3445
  };
3446
+ var COMPACT_TIPS = [
3447
+ "Shift+Tab cycles permission modes (default / acceptEdits / plan / bypass)",
3448
+ "/mode plan drafts changes without executing them",
3449
+ "/cost shows token usage and estimated spend",
3450
+ "/resume picks up a previous session in this directory",
3451
+ "muse --continue resumes the last session on startup",
3452
+ "MemoryWrite saves persistent knowledge across sessions",
3453
+ "TodoWrite keeps the model honest on multi-step tasks",
3454
+ 'Pipe to muse: cat bug.log | muse "explain this"',
3455
+ "Ctrl+C exits immediately; Esc rejects a pending tool"
3456
+ ];
3457
+ var COMPACT_ESTIMATED_CHARS = 1800;
1831
3458
  var COMPACT = {
1832
3459
  name: "compact",
1833
3460
  description: "summarize older messages to free up context space",
@@ -1837,23 +3464,39 @@ var COMPACT = {
1837
3464
  const { flags } = parseArgs(ctx.args);
1838
3465
  const keepRecent = typeof flags.keep === "string" ? Math.max(1, parseInt(flags.keep, 10)) : 4;
1839
3466
  if (Number.isNaN(keepRecent)) return { display: `Invalid --keep value: ${flags.keep}` };
1840
- const result = await compactMessages(ctx.history, { llm: ctx.llm, keepRecent });
1841
- if (result.noop) {
1842
- return { display: `(history has ${result.originalCount} messages; not enough to compact with --keep ${keepRecent})` };
1843
- }
1844
- ctx.actions.setMessages(result.newMessages);
1845
- const preview = result.summary.length > 240 ? result.summary.slice(0, 240) + "\u2026" : result.summary;
1846
- return {
1847
- display: `Compacted ${result.originalCount} \u2192 ${result.newCount} messages (kept last ${keepRecent}).
3467
+ const progressRef = { chars: 0 };
3468
+ ctx.actions.showProgress({
3469
+ title: "Compacting conversation",
3470
+ tips: COMPACT_TIPS,
3471
+ getPercent: () => progressRef.chars / COMPACT_ESTIMATED_CHARS * 100
3472
+ });
3473
+ try {
3474
+ const result = await compactMessages(ctx.history, {
3475
+ llm: ctx.llm,
3476
+ keepRecent,
3477
+ onProgress: (chars) => {
3478
+ progressRef.chars = chars;
3479
+ }
3480
+ });
3481
+ if (result.noop) {
3482
+ return { display: `(history has ${result.originalCount} messages; not enough to compact with --keep ${keepRecent})` };
3483
+ }
3484
+ ctx.actions.setMessages(result.newMessages);
3485
+ const preview = result.summary.length > 240 ? result.summary.slice(0, 240) + "\u2026" : result.summary;
3486
+ return {
3487
+ display: `Compacted ${result.originalCount} \u2192 ${result.newCount} messages (kept last ${keepRecent}).
1848
3488
 
1849
3489
  Summary:
1850
3490
  ${preview}`
1851
- };
3491
+ };
3492
+ } finally {
3493
+ ctx.actions.hideProgress();
3494
+ }
1852
3495
  }
1853
3496
  };
1854
3497
  var MODELS = {
1855
3498
  name: "models",
1856
- description: "pick a model from ~/.muse/models.json (\u2191\u2193 to navigate)",
3499
+ description: "pick a model from ~/.muse/models.local.json (\u2191\u2193 to navigate)",
1857
3500
  async execute(ctx) {
1858
3501
  let registry = ctx.modelsRegistry;
1859
3502
  let errors = [];
@@ -1871,7 +3514,7 @@ var MODELS = {
1871
3514
  const visible = visibleEntries(registry);
1872
3515
  if (visible.length === 0) {
1873
3516
  return {
1874
- display: `models.json has no available models.
3517
+ display: `models.local.json has no available models.
1875
3518
  Check that "availableModels" lists at least one id present in "models".`
1876
3519
  };
1877
3520
  }
@@ -1889,7 +3532,7 @@ Check that "availableModels" lists at least one id present in "models".`
1889
3532
  };
1890
3533
  function renderLoadErrors(errors) {
1891
3534
  return [
1892
- `models.json was found but failed to load:`,
3535
+ `models.local.json was found but failed to load:`,
1893
3536
  ``,
1894
3537
  ...errors.flatMap((e) => [` ${shortPath(e.path)}`, ` ${e.message}`]),
1895
3538
  ``,
@@ -1900,7 +3543,7 @@ function renderLoadErrors(errors) {
1900
3543
  function renderEmptyRegistryHint() {
1901
3544
  return [
1902
3545
  `No models registry found.`,
1903
- `Create ~/.muse/models.json with a "models" array. Example:`,
3546
+ `Create ~/.muse/models.local.json with a "models" array. Example:`,
1904
3547
  ``,
1905
3548
  `{`,
1906
3549
  ` "models": [`,
@@ -2038,6 +3681,44 @@ async function loadAndReport(meta, ctx) {
2038
3681
  display: `Resumed session ${meta.id.slice(0, 8)} (${messages.length} messages from ${formatTime2(meta.createdAt)}).`
2039
3682
  };
2040
3683
  }
3684
+ var MODE_ALIASES = {
3685
+ default: "default",
3686
+ normal: "default",
3687
+ acceptedits: "acceptEdits",
3688
+ "accept-edits": "acceptEdits",
3689
+ accept: "acceptEdits",
3690
+ edits: "acceptEdits",
3691
+ plan: "plan",
3692
+ bypass: "bypassPermissions",
3693
+ bypasspermissions: "bypassPermissions"
3694
+ };
3695
+ var MODE_CMD = {
3696
+ name: "mode",
3697
+ description: "show or switch the permission mode (alternative to Shift+Tab)",
3698
+ argsHint: "[default|acceptEdits|plan|bypassPermissions]",
3699
+ execute(ctx) {
3700
+ const arg = ctx.args.trim().toLowerCase();
3701
+ if (!arg) {
3702
+ const cur = ctx.actions.getMode();
3703
+ const lines = [`Current permission mode: ${cur} \u2014 ${MODE_LABEL[cur]}`, ``, `Available modes:`];
3704
+ for (const m of MODE_CYCLE) {
3705
+ const marker = m === cur ? "\u25CF" : " ";
3706
+ lines.push(` ${marker} ${m.padEnd(20)} ${MODE_LABEL[m]}`);
3707
+ }
3708
+ lines.push(``, `Switch: /mode <name> or Shift+Tab to cycle`);
3709
+ return { display: lines.join("\n") };
3710
+ }
3711
+ const target = MODE_ALIASES[arg];
3712
+ if (!target) {
3713
+ return {
3714
+ display: `Unknown mode "${ctx.args.trim()}". Valid: ${MODE_CYCLE.join(" | ")}`
3715
+ };
3716
+ }
3717
+ if (target === ctx.actions.getMode()) return { display: `Already in ${target} mode.` };
3718
+ ctx.actions.setMode(target);
3719
+ return { display: `Switched to ${target} \u2014 ${MODE_LABEL[target]}` };
3720
+ }
3721
+ };
2041
3722
  var BUILTIN_SLASH_COMMANDS = [
2042
3723
  HELP,
2043
3724
  CLEAR,
@@ -2045,31 +3726,53 @@ var BUILTIN_SLASH_COMMANDS = [
2045
3726
  MODELS,
2046
3727
  CONFIG,
2047
3728
  MCP,
3729
+ MODE_CMD,
2048
3730
  COST,
2049
3731
  RESUME,
2050
3732
  QUIT
2051
3733
  ];
2052
3734
 
2053
3735
  // src/app.tsx
2054
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3736
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2055
3737
  function reducer(state, action) {
2056
3738
  switch (action.type) {
2057
3739
  case "user_submit":
2058
- return { ...state, streamingText: "", status: "streaming" };
3740
+ return {
3741
+ ...state,
3742
+ streamingText: "",
3743
+ status: "streaming",
3744
+ runningTool: null,
3745
+ turnStartTime: Date.now(),
3746
+ turnFirstTextTime: null,
3747
+ turnInputTokens: 0
3748
+ };
2059
3749
  case "history_set":
2060
3750
  return { ...state, history: action.messages };
2061
3751
  case "stream_delta":
2062
- return { ...state, streamingText: state.streamingText + action.delta };
3752
+ return {
3753
+ ...state,
3754
+ streamingText: state.streamingText + action.delta,
3755
+ status: state.status === "tool" ? "streaming" : state.status,
3756
+ runningTool: null,
3757
+ turnFirstTextTime: state.turnFirstTextTime ?? Date.now()
3758
+ };
2063
3759
  case "stream_reset":
2064
3760
  return { ...state, streamingText: "" };
2065
3761
  case "set_status":
2066
- return { ...state, status: action.status };
3762
+ return {
3763
+ ...state,
3764
+ status: action.status,
3765
+ runningTool: action.status === "tool" ? state.runningTool : null
3766
+ };
3767
+ case "tool_start":
3768
+ return { ...state, status: "tool", runningTool: action.name };
2067
3769
  case "add_usage":
2068
3770
  return {
2069
3771
  ...state,
2070
3772
  inputTokens: state.inputTokens + action.usage.inputTokens,
2071
3773
  outputTokens: state.outputTokens + action.usage.outputTokens,
2072
- totalTokens: state.totalTokens + action.usage.totalTokens
3774
+ totalTokens: state.totalTokens + action.usage.totalTokens,
3775
+ turnInputTokens: state.turnInputTokens + action.usage.inputTokens
2073
3776
  };
2074
3777
  }
2075
3778
  }
@@ -2087,34 +3790,66 @@ function App({
2087
3790
  initialMessages
2088
3791
  }) {
2089
3792
  const { exit } = useApp();
2090
- const { stdout } = useStdout();
3793
+ const { stdout } = useStdout2();
2091
3794
  const termWidth = stdout?.columns ?? 80;
2092
- const [llm, setLLM] = useState2(initialLLM);
2093
- const [permissions, setPermissions] = useState2(initialPermissions);
2094
- const [settings, setSettings] = useState2(initialSettings);
2095
- const [settingsSources, setSettingsSources] = useState2(initialSources);
2096
- const [modelsRegistry, setModelsRegistry] = useState2(initialModelsRegistry);
2097
- const [mode, setMode] = useState2(initialPermissions.getMode());
3795
+ const [llm, setLLM] = useState8(initialLLM);
3796
+ const [permissions, setPermissions] = useState8(initialPermissions);
3797
+ const [settings, setSettings] = useState8(initialSettings);
3798
+ const [settingsSources, setSettingsSources] = useState8(initialSources);
3799
+ const [modelsRegistry, setModelsRegistry] = useState8(initialModelsRegistry);
3800
+ const [mode, setMode] = useState8(initialPermissions.getMode());
2098
3801
  const [state, dispatch] = useReducer(reducer, {
2099
3802
  history: initialMessages ?? [],
2100
3803
  streamingText: "",
2101
3804
  status: "idle",
3805
+ runningTool: null,
2102
3806
  inputTokens: 0,
2103
3807
  outputTokens: 0,
2104
- totalTokens: 0
3808
+ totalTokens: 0,
3809
+ turnStartTime: 0,
3810
+ turnFirstTextTime: null,
3811
+ turnInputTokens: 0
2105
3812
  });
2106
3813
  const messagesRef = useRef(initialMessages ?? []);
2107
- const [input, setInput] = useState2("");
2108
- const [inputRemountKey, setInputRemountKey] = useState2(0);
3814
+ const [input, setInput] = useState8("");
3815
+ const [inputRemountKey, setInputRemountKey] = useState8(0);
2109
3816
  const commitInput = (value) => {
2110
3817
  setInput(value);
2111
3818
  setInputRemountKey((k) => k + 1);
2112
3819
  };
2113
- const [pending, setPending] = useState2(null);
2114
- const [picker, setPicker] = useState2(null);
2115
- const [sessionPicker, setSessionPicker] = useState2(null);
2116
- const [autocompleteIndex, setAutocompleteIndex] = useState2(0);
3820
+ const pasteRegistryRef = useRef({
3821
+ map: /* @__PURE__ */ new Map(),
3822
+ nextId: 1
3823
+ });
3824
+ const handlePaste = useCallback((chunk) => {
3825
+ const reg = pasteRegistryRef.current;
3826
+ const id = reg.nextId++;
3827
+ reg.map.set(id, chunk);
3828
+ const lines = chunk.split("\n").length;
3829
+ return `[Pasted text #${id} +${lines} lines]`;
3830
+ }, []);
3831
+ const [pending, setPending] = useState8(null);
3832
+ const [picker, setPicker] = useState8(null);
3833
+ const [sessionPicker, setSessionPicker] = useState8(null);
3834
+ const [questionPicker, setQuestionPicker] = useState8(null);
3835
+ const [autocompleteIndex, setAutocompleteIndex] = useState8(0);
3836
+ const [progress, setProgress] = useState8(null);
2117
3837
  const agentRef = useRef(null);
3838
+ const queuedInputsRef = useRef([]);
3839
+ const [queuedInputs, setQueuedInputs] = useState8([]);
3840
+ const enqueueInput = (text) => {
3841
+ queuedInputsRef.current.push(text);
3842
+ setQueuedInputs([...queuedInputsRef.current]);
3843
+ };
3844
+ const dequeueInput = () => {
3845
+ if (queuedInputsRef.current.length === 0) return null;
3846
+ const front = queuedInputsRef.current.shift();
3847
+ setQueuedInputs([...queuedInputsRef.current]);
3848
+ return front;
3849
+ };
3850
+ const inputHistoryRef = useRef(extractUserInputs(initialMessages ?? []));
3851
+ const historyIndexRef = useRef(-1);
3852
+ const savedDraftRef = useRef("");
2118
3853
  const slash = useMemo2(() => {
2119
3854
  const r = new SlashRegistry();
2120
3855
  r.registerAll(BUILTIN_SLASH_COMMANDS);
@@ -2131,17 +3866,48 @@ function App({
2131
3866
  ) : all;
2132
3867
  return { matches, query };
2133
3868
  }, [input, slash]);
2134
- useEffect(() => {
3869
+ useEffect5(() => {
2135
3870
  const len = autocomplete?.matches.length ?? 0;
2136
3871
  if (autocompleteIndex >= len) setAutocompleteIndex(0);
2137
3872
  }, [autocomplete, autocompleteIndex]);
2138
- useEffect(() => {
3873
+ useEffect5(() => {
3874
+ const project = basename(cwd) || "muse";
3875
+ const baseIdle = `muse \xB7 ${project}`;
3876
+ if (state.status === "idle") {
3877
+ setTerminalTitle(baseIdle);
3878
+ return;
3879
+ }
3880
+ const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3881
+ let i = 0;
3882
+ const id = setInterval(() => {
3883
+ const frame = FRAMES[i % FRAMES.length];
3884
+ const tail = state.runningTool ? ` \xB7 ${state.runningTool}` : "";
3885
+ setTerminalTitle(`${frame} muse \xB7 ${project}${tail}`);
3886
+ i++;
3887
+ }, 100);
3888
+ return () => clearInterval(id);
3889
+ }, [state.status, state.runningTool, cwd]);
3890
+ useEffect5(() => {
3891
+ return () => resetTerminalTitle();
3892
+ }, []);
3893
+ const [memoryIndex, setMemoryIndex] = useState8("");
3894
+ useEffect5(() => {
3895
+ let cancelled = false;
3896
+ loadMemoryIndex(cwd).then((idx) => {
3897
+ if (!cancelled) setMemoryIndex(idx);
3898
+ });
3899
+ return () => {
3900
+ cancelled = true;
3901
+ };
3902
+ }, [cwd]);
3903
+ useEffect5(() => {
2139
3904
  const systemPrompt = buildSystemPrompt({
2140
3905
  cwd,
2141
3906
  model: llm.model,
2142
3907
  provider: llm.providerName,
2143
3908
  lang,
2144
- toolNames: tools.list().map((t) => t.name)
3909
+ toolNames: tools.list().map((t) => t.name),
3910
+ memoryIndex
2145
3911
  });
2146
3912
  const agent = new Agent({
2147
3913
  llm,
@@ -2152,7 +3918,23 @@ function App({
2152
3918
  systemPrompt,
2153
3919
  events: {
2154
3920
  onText: (delta) => dispatch({ type: "stream_delta", delta }),
2155
- onToolCallStart: () => dispatch({ type: "set_status", status: "tool" }),
3921
+ onToolCallStart: (_id, name) => dispatch({ type: "tool_start", name }),
3922
+ // assistant 流刚结束、这一批 calls 已落到 messages 但 tool 还没开始执行:
3923
+ // 立刻同步 history,让所有 ⏺ Tool(...) 调用头一次性显示出来(之前要等第一个
3924
+ // result 才能见到任何东西,看起来像"卡死了")
3925
+ onAssistantTurn: () => {
3926
+ const msgs = [...agent.getMessages()];
3927
+ messagesRef.current = msgs;
3928
+ dispatch({ type: "history_set", messages: msgs });
3929
+ dispatch({ type: "stream_reset" });
3930
+ },
3931
+ // 每个 tool result 到位就同步:result 立刻挂到对应 ⏺ 调用的 └ 树枝下方
3932
+ onToolResult: () => {
3933
+ const msgs = [...agent.getMessages()];
3934
+ messagesRef.current = msgs;
3935
+ dispatch({ type: "history_set", messages: msgs });
3936
+ dispatch({ type: "set_status", status: "streaming" });
3937
+ },
2156
3938
  onUsage: (usage) => dispatch({ type: "add_usage", usage }),
2157
3939
  onTurnEnd: () => {
2158
3940
  const msgs = [...agent.getMessages()];
@@ -2160,6 +3942,20 @@ function App({
2160
3942
  dispatch({ type: "history_set", messages: msgs });
2161
3943
  dispatch({ type: "stream_reset" });
2162
3944
  dispatch({ type: "set_status", status: "idle" });
3945
+ const next = dequeueInput();
3946
+ if (next) {
3947
+ setTimeout(() => {
3948
+ dispatch({ type: "user_submit" });
3949
+ const expanded = expandPastes(next, pasteRegistryRef.current.map);
3950
+ agent.runTurn(expanded).catch((err) => {
3951
+ const m = err instanceof Error ? err.message : String(err);
3952
+ dispatch({ type: "stream_delta", delta: `
3953
+ [error] ${m}
3954
+ ` });
3955
+ dispatch({ type: "set_status", status: "idle" });
3956
+ });
3957
+ }, 0);
3958
+ }
2163
3959
  },
2164
3960
  onError: (err) => {
2165
3961
  dispatch({ type: "stream_delta", delta: `
@@ -2167,15 +3963,18 @@ function App({
2167
3963
  ` });
2168
3964
  dispatch({ type: "set_status", status: "idle" });
2169
3965
  },
2170
- onPermissionRequest: (toolName, args, summary) => new Promise((resolve5) => {
2171
- setPending({ toolName, args, summary, resolve: resolve5 });
3966
+ onPermissionRequest: (toolName, args, summary) => new Promise((resolve6) => {
3967
+ setPending({ toolName, args, summary, resolve: resolve6 });
3968
+ }),
3969
+ onAskQuestions: (questions) => new Promise((resolve6) => {
3970
+ setQuestionPicker({ questions, resolve: resolve6 });
2172
3971
  })
2173
3972
  }
2174
3973
  });
2175
3974
  agent.setMessages(messagesRef.current);
2176
3975
  agentRef.current = agent;
2177
- }, [llm, tools, permissions, session, cwd, lang]);
2178
- useInput3(
3976
+ }, [llm, tools, permissions, session, cwd, lang, memoryIndex]);
3977
+ useInput5(
2179
3978
  (inputKey, key) => {
2180
3979
  if (key.ctrl && inputKey === "c") {
2181
3980
  exit();
@@ -2186,22 +3985,43 @@ function App({
2186
3985
  setMode(next);
2187
3986
  return;
2188
3987
  }
2189
- if (!autocomplete || autocomplete.matches.length === 0) return;
2190
- const len = autocomplete.matches.length;
2191
- if (key.upArrow) {
2192
- setAutocompleteIndex((i) => (i - 1 + len) % len);
3988
+ if (autocomplete && autocomplete.matches.length > 0) {
3989
+ const len = autocomplete.matches.length;
3990
+ if (key.upArrow) {
3991
+ setAutocompleteIndex((i) => (i - 1 + len) % len);
3992
+ } else if (key.downArrow) {
3993
+ setAutocompleteIndex((i) => (i + 1) % len);
3994
+ } else if (key.tab) {
3995
+ const picked = autocomplete.matches[autocompleteIndex];
3996
+ if (picked) commitInput(`/${picked.name}`);
3997
+ } else if (key.escape) {
3998
+ commitInput("");
3999
+ }
4000
+ return;
4001
+ }
4002
+ const hist = inputHistoryRef.current;
4003
+ if (key.upArrow && hist.length > 0) {
4004
+ const cur = historyIndexRef.current;
4005
+ if (cur === -1) savedDraftRef.current = input;
4006
+ const next = Math.min(cur + 1, hist.length - 1);
4007
+ historyIndexRef.current = next;
4008
+ commitInput(hist[hist.length - 1 - next] ?? "");
2193
4009
  } else if (key.downArrow) {
2194
- setAutocompleteIndex((i) => (i + 1) % len);
2195
- } else if (key.tab) {
2196
- const picked = autocomplete.matches[autocompleteIndex];
2197
- if (picked) commitInput(`/${picked.name}`);
2198
- } else if (key.escape) {
2199
- commitInput("");
4010
+ const cur = historyIndexRef.current;
4011
+ if (cur === -1) return;
4012
+ const next = cur - 1;
4013
+ historyIndexRef.current = next;
4014
+ if (next === -1) commitInput(savedDraftRef.current);
4015
+ else commitInput(hist[hist.length - 1 - next] ?? "");
2200
4016
  }
2201
4017
  },
2202
- { isActive: state.status === "idle" && !pending && !picker && !sessionPicker }
4018
+ // 模型在跑时也要响应键盘(让用户能 Ctrl+C / Shift+Tab / autocomplete 导航);
4019
+ // 仅模态弹起时让出键盘所有权
4020
+ { isActive: !pending && !picker && !sessionPicker && !questionPicker }
2203
4021
  );
2204
- const acceptingInput = state.status === "idle" && pending === null && picker === null && sessionPicker === null;
4022
+ const acceptingInput = pending === null && picker === null && sessionPicker === null && questionPicker === null;
4023
+ const inputVisible = pending === null && picker === null && sessionPicker === null;
4024
+ const inputPlaceholder = questionPicker ? "Chat about this" : void 0;
2205
4025
  const actions = useMemo2(
2206
4026
  () => ({
2207
4027
  setMessages: (msgs) => {
@@ -2209,11 +4029,11 @@ function App({
2209
4029
  agentRef.current?.setMessages(msgs);
2210
4030
  dispatch({ type: "history_set", messages: msgs });
2211
4031
  },
2212
- pickModel: (items, currentId) => new Promise((resolve5) => {
2213
- setPicker({ items, currentId, resolve: resolve5 });
4032
+ pickModel: (items, currentId) => new Promise((resolve6) => {
4033
+ setPicker({ items, currentId, resolve: resolve6 });
2214
4034
  }),
2215
- pickSession: (items, currentId) => new Promise((resolve5) => {
2216
- setSessionPicker({ items, currentId, resolve: resolve5 });
4035
+ pickSession: (items, currentId) => new Promise((resolve6) => {
4036
+ setSessionPicker({ items, currentId, resolve: resolve6 });
2217
4037
  }),
2218
4038
  switchModel: async (modelId) => {
2219
4039
  if (!modelsRegistry) throw new Error("No models registry loaded.");
@@ -2224,6 +4044,20 @@ function App({
2224
4044
  setLLM(next);
2225
4045
  await persistActiveModel(modelId);
2226
4046
  },
4047
+ getMode: () => permissions.getMode(),
4048
+ setMode: (m) => {
4049
+ permissions.setMode(m);
4050
+ setMode(m);
4051
+ },
4052
+ showProgress: (opts) => {
4053
+ setProgress({
4054
+ title: opts.title,
4055
+ tips: opts.tips ?? [],
4056
+ getPercent: opts.getPercent ?? (() => 0),
4057
+ startTime: Date.now()
4058
+ });
4059
+ },
4060
+ hideProgress: () => setProgress(null),
2227
4061
  reloadSettings: async () => {
2228
4062
  const { settings: nextSettings, sources } = await loadSettings(cwd);
2229
4063
  const { registry: nextModels } = await loadModelsRegistry();
@@ -2253,7 +4087,7 @@ function App({
2253
4087
  return { settings: nextSettings, sources };
2254
4088
  }
2255
4089
  }),
2256
- [cwd, modelsRegistry, llm.model]
4090
+ [cwd, modelsRegistry, llm.model, permissions]
2257
4091
  );
2258
4092
  const handleSubmit = useCallback(
2259
4093
  async (value) => {
@@ -2271,6 +4105,10 @@ function App({
2271
4105
  }
2272
4106
  const parsed = parseSlash(trimmed);
2273
4107
  if (parsed) {
4108
+ if (state.status !== "idle") {
4109
+ commitInput("");
4110
+ return;
4111
+ }
2274
4112
  const cmd = slash.get(parsed.name);
2275
4113
  commitInput("");
2276
4114
  if (!cmd) {
@@ -2305,9 +4143,25 @@ function App({
2305
4143
  return;
2306
4144
  }
2307
4145
  commitInput("");
4146
+ const hist = inputHistoryRef.current;
4147
+ if (hist[hist.length - 1] !== trimmed) hist.push(trimmed);
4148
+ if (hist.length > 200) hist.shift();
4149
+ historyIndexRef.current = -1;
4150
+ savedDraftRef.current = "";
4151
+ if (state.status !== "idle") {
4152
+ enqueueInput(trimmed);
4153
+ return;
4154
+ }
2308
4155
  dispatch({ type: "user_submit" });
4156
+ const expanded = expandPastes(trimmed, pasteRegistryRef.current.map);
4157
+ {
4158
+ const userMsg = { role: "user", content: expanded };
4159
+ const next = [...messagesRef.current, userMsg];
4160
+ messagesRef.current = next;
4161
+ dispatch({ type: "history_set", messages: next });
4162
+ }
2309
4163
  try {
2310
- await agentRef.current?.runTurn(trimmed);
4164
+ await agentRef.current?.runTurn(expanded);
2311
4165
  } catch (err) {
2312
4166
  const msg = err instanceof Error ? err.message : String(err);
2313
4167
  dispatch({ type: "stream_delta", delta: `
@@ -2316,7 +4170,7 @@ function App({
2316
4170
  dispatch({ type: "set_status", status: "idle" });
2317
4171
  }
2318
4172
  },
2319
- [slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, actions, autocomplete, autocompleteIndex]
4173
+ [slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, state.status, actions, autocomplete, autocompleteIndex]
2320
4174
  );
2321
4175
  function appendAssistantText(text) {
2322
4176
  const msg = { role: "assistant", content: [{ type: "text", text }] };
@@ -2334,25 +4188,47 @@ function App({
2334
4188
  }
2335
4189
  }
2336
4190
  const banner = !showBanner ? null : pickBanner(termWidth, { version: "0.1.0", model: llm.model, cwd: shortCwd(cwd) });
2337
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
4191
+ const { resultsByCallId, inlinedIds } = useMemo2(() => {
4192
+ const byId = /* @__PURE__ */ new Map();
4193
+ const used = /* @__PURE__ */ new Set();
4194
+ for (const m of state.history) {
4195
+ if (m.role === "tool" && m.toolUseId) byId.set(m.toolUseId, m);
4196
+ if (m.role === "assistant" && Array.isArray(m.content)) {
4197
+ for (const p of m.content) {
4198
+ if (p.type === "tool_use") used.add(p.id);
4199
+ }
4200
+ }
4201
+ }
4202
+ return { resultsByCallId: byId, inlinedIds: used };
4203
+ }, [state.history]);
4204
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
2338
4205
  banner,
2339
- /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, children: [
2340
- state.history.map((msg, i) => /* @__PURE__ */ jsx9(MessageView, { message: msg }, i)),
2341
- state.streamingText && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { children: state.streamingText }) })
4206
+ /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", marginTop: 1, children: [
4207
+ state.history.map((msg, i) => {
4208
+ if (msg.role === "tool" && inlinedIds.has(msg.toolUseId)) return null;
4209
+ return /* @__PURE__ */ jsx15(MessageView, { message: msg, resultsByCallId }, i);
4210
+ }),
4211
+ state.streamingText && /* @__PURE__ */ jsxs14(Box13, { flexDirection: "row", marginTop: 1, children: [
4212
+ /* @__PURE__ */ jsxs14(Text15, { color: "cyan", children: [
4213
+ DOT,
4214
+ " "
4215
+ ] }),
4216
+ /* @__PURE__ */ jsx15(Box13, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx15(Text15, { children: state.streamingText }) })
4217
+ ] })
2342
4218
  ] }),
2343
- pending && /* @__PURE__ */ jsx9(
4219
+ pending && /* @__PURE__ */ jsx15(
2344
4220
  PermissionPrompt,
2345
4221
  {
2346
4222
  request: {
2347
4223
  ...pending,
2348
- resolve: (ok) => {
2349
- pending.resolve(ok);
4224
+ resolve: (decision) => {
4225
+ pending.resolve(decision);
2350
4226
  setPending(null);
2351
4227
  }
2352
4228
  }
2353
4229
  }
2354
4230
  ),
2355
- picker && /* @__PURE__ */ jsx9(
4231
+ picker && /* @__PURE__ */ jsx15(
2356
4232
  ModelSelector,
2357
4233
  {
2358
4234
  request: {
@@ -2364,7 +4240,7 @@ function App({
2364
4240
  }
2365
4241
  }
2366
4242
  ),
2367
- sessionPicker && /* @__PURE__ */ jsx9(
4243
+ sessionPicker && /* @__PURE__ */ jsx15(
2368
4244
  SessionSelector,
2369
4245
  {
2370
4246
  request: {
@@ -2376,38 +4252,111 @@ function App({
2376
4252
  }
2377
4253
  }
2378
4254
  ),
2379
- acceptingInput && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2380
- /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
2381
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "> " }),
2382
- /* @__PURE__ */ jsx9(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit }, inputRemountKey)
4255
+ questionPicker && /* @__PURE__ */ jsx15(
4256
+ QuestionPicker,
4257
+ {
4258
+ request: {
4259
+ questions: questionPicker.questions,
4260
+ resolve: (responses) => {
4261
+ questionPicker.resolve(responses);
4262
+ setQuestionPicker(null);
4263
+ }
4264
+ }
4265
+ }
4266
+ ),
4267
+ state.status !== "idle" && /* @__PURE__ */ jsx15(
4268
+ StatusLine,
4269
+ {
4270
+ startTime: state.turnStartTime,
4271
+ firstTextTime: state.turnFirstTextTime,
4272
+ inputTokens: state.turnInputTokens,
4273
+ runningTool: state.runningTool,
4274
+ lang
4275
+ }
4276
+ ),
4277
+ progress && /* @__PURE__ */ jsx15(ProgressBanner, { state: progress }),
4278
+ inputVisible && /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
4279
+ queuedInputs.length > 0 && /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [
4280
+ queuedInputs.map((q, i) => /* @__PURE__ */ jsx15(Text15, { color: "yellow", dimColor: true, children: `\u21B3 queued: ${q.length > 60 ? q.slice(0, 60) + "\u2026" : q}` }, i)),
4281
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` (will send after current turn \xB7 ${queuedInputs.length} pending)` })
4282
+ ] }),
4283
+ /* @__PURE__ */ jsxs14(Box13, { marginTop: 1, flexDirection: "column", children: [
4284
+ /* @__PURE__ */ jsx15(Text15, { backgroundColor: "#1c1c1c", children: " ".repeat(Math.max(1, termWidth - 1)) }),
4285
+ /* @__PURE__ */ jsxs14(Box13, { flexDirection: "row", children: [
4286
+ /* @__PURE__ */ jsx15(Text15, { backgroundColor: "#1c1c1c", color: "gray", bold: true, children: " \u203A " }),
4287
+ /* @__PURE__ */ jsx15(
4288
+ BgTextInput,
4289
+ {
4290
+ value: input,
4291
+ onChange: setInput,
4292
+ onSubmit: handleSubmit,
4293
+ width: Math.max(10, termWidth - 4),
4294
+ backgroundColor: "#1c1c1c",
4295
+ isActive: acceptingInput,
4296
+ onPaste: handlePaste,
4297
+ placeholder: inputPlaceholder
4298
+ },
4299
+ inputRemountKey
4300
+ )
4301
+ ] }),
4302
+ /* @__PURE__ */ jsx15(Text15, { backgroundColor: "#1c1c1c", children: " ".repeat(Math.max(1, termWidth - 1)) })
2383
4303
  ] }),
2384
- autocomplete && autocomplete.matches.length > 0 && /* @__PURE__ */ jsx9(SlashAutocomplete, { matches: autocomplete.matches, index: autocompleteIndex })
4304
+ autocomplete && autocomplete.matches.length > 0 && /* @__PURE__ */ jsx15(SlashAutocomplete, { matches: autocomplete.matches, index: autocompleteIndex })
2385
4305
  ] }),
2386
- state.status === "streaming" && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "... (streaming)" }) }),
2387
- state.status === "tool" && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "... (running tool)" }) }),
2388
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(PermissionModeBar, { mode, compact: termWidth < 60 }) })
4306
+ /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
4307
+ /* @__PURE__ */ jsx15(
4308
+ FooterStatus,
4309
+ {
4310
+ sessionId: session.meta.id,
4311
+ model: llm.model,
4312
+ contextWindow: llm.capabilities.maxContextWindow,
4313
+ lastInputTokens: state.turnInputTokens,
4314
+ sessionInputTokens: state.inputTokens,
4315
+ sessionOutputTokens: state.outputTokens,
4316
+ termWidth
4317
+ }
4318
+ ),
4319
+ /* @__PURE__ */ jsx15(PermissionModeBar, { mode, compact: termWidth < 60 })
4320
+ ] })
2389
4321
  ] });
2390
4322
  }
4323
+ function extractUserInputs(messages) {
4324
+ const out = [];
4325
+ for (const m of messages) {
4326
+ if (m.role !== "user") continue;
4327
+ const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
4328
+ if (text.startsWith("[Previous conversation summary]")) continue;
4329
+ if (text.trim()) out.push(text);
4330
+ }
4331
+ return out;
4332
+ }
4333
+ var PASTE_PLACEHOLDER_RE = /\[Pasted text #(\d+) \+\d+ lines\]/g;
4334
+ function expandPastes(value, map) {
4335
+ return value.replace(PASTE_PLACEHOLDER_RE, (full, id) => {
4336
+ const text = map.get(Number(id));
4337
+ return text ?? full;
4338
+ });
4339
+ }
2391
4340
  function shortCwd(cwd) {
2392
- const home = homedir7();
4341
+ const home = homedir8();
2393
4342
  if (cwd === home) return "~";
2394
4343
  if (cwd.startsWith(home + "/")) return "~" + cwd.slice(home.length);
2395
4344
  return cwd;
2396
4345
  }
2397
4346
  async function persistActiveModel(modelId) {
2398
- const path = join5(homedir7(), ".muse", "settings.json");
4347
+ const path = join6(homedir8(), ".muse", "settings.json");
2399
4348
  let current = {};
2400
- if (existsSync4(path)) {
4349
+ if (existsSync5(path)) {
2401
4350
  try {
2402
- current = JSON.parse(await readFile4(path, "utf-8"));
4351
+ current = JSON.parse(await readFile5(path, "utf-8"));
2403
4352
  } catch {
2404
4353
  current = {};
2405
4354
  }
2406
4355
  }
2407
4356
  const llm = current.llm ?? {};
2408
4357
  const next = { ...current, llm: { ...llm, model: modelId } };
2409
- await mkdir2(dirname3(path), { recursive: true });
2410
- await writeFile(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
4358
+ await mkdir3(dirname3(path), { recursive: true });
4359
+ await writeFile2(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
2411
4360
  }
2412
4361
 
2413
4362
  // src/tools/registry.ts
@@ -2521,8 +4470,8 @@ var ToolRegistry = class {
2521
4470
  };
2522
4471
 
2523
4472
  // src/tools/builtin/read.ts
2524
- import { readFile as readFile5, stat as stat2 } from "fs/promises";
2525
- import { resolve as resolve2, isAbsolute } from "path";
4473
+ import { readFile as readFile6, stat as stat2 } from "fs/promises";
4474
+ import { resolve as resolve3, isAbsolute } from "path";
2526
4475
  import { z as z3 } from "zod";
2527
4476
 
2528
4477
  // src/tools/types.ts
@@ -2537,6 +4486,48 @@ function defineTool(def) {
2537
4486
  };
2538
4487
  }
2539
4488
 
4489
+ // src/tools/_sensitive.ts
4490
+ import { homedir as homedir9 } from "os";
4491
+ import { basename as basename2, resolve as resolve2 } from "path";
4492
+ var HOME = homedir9();
4493
+ var SENSITIVE_DIRS = [
4494
+ resolve2(HOME, ".ssh"),
4495
+ resolve2(HOME, ".aws"),
4496
+ resolve2(HOME, ".gnupg"),
4497
+ resolve2(HOME, ".config", "gh")
4498
+ ];
4499
+ var SENSITIVE_FILES = [
4500
+ resolve2(HOME, ".kube", "config"),
4501
+ resolve2(HOME, ".netrc"),
4502
+ resolve2(HOME, ".pypirc")
4503
+ ];
4504
+ var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
4505
+ "id_rsa",
4506
+ "id_ed25519",
4507
+ "id_ecdsa",
4508
+ "id_dsa"
4509
+ ]);
4510
+ var ENV_PATTERN2 = /(?:^|\/)\.env(\..+)?$/;
4511
+ function checkSensitivePath(path) {
4512
+ const abs = resolve2(path);
4513
+ for (const dir of SENSITIVE_DIRS) {
4514
+ if (abs === dir || abs.startsWith(dir + "/")) {
4515
+ return { blocked: true, reason: `sensitive directory ${dir.replace(HOME, "~")}` };
4516
+ }
4517
+ }
4518
+ for (const f of SENSITIVE_FILES) {
4519
+ if (abs === f) return { blocked: true, reason: `sensitive file ${f.replace(HOME, "~")}` };
4520
+ }
4521
+ const base = basename2(abs);
4522
+ if (SENSITIVE_BASENAMES.has(base)) {
4523
+ return { blocked: true, reason: `private key filename ${base}` };
4524
+ }
4525
+ if (ENV_PATTERN2.test(abs)) {
4526
+ return { blocked: true, reason: `.env file (may contain secrets)` };
4527
+ }
4528
+ return { blocked: false };
4529
+ }
4530
+
2540
4531
  // src/tools/builtin/read.ts
2541
4532
  var ReadArgs = z3.object({
2542
4533
  file_path: z3.string().describe("Absolute or cwd-relative path to the file."),
@@ -2552,7 +4543,11 @@ var ReadTool = defineTool({
2552
4543
  permission: "read",
2553
4544
  summarize: (args) => `Read(${args.file_path}${args.offset != null ? `, offset=${args.offset}` : ""}${args.limit != null ? `, limit=${args.limit}` : ""})`,
2554
4545
  async execute(args, ctx) {
2555
- const path = isAbsolute(args.file_path) ? args.file_path : resolve2(ctx.cwd, args.file_path);
4546
+ const path = isAbsolute(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
4547
+ const sensitive = checkSensitivePath(path);
4548
+ if (sensitive.blocked) {
4549
+ return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
4550
+ }
2556
4551
  let info;
2557
4552
  try {
2558
4553
  info = await stat2(path);
@@ -2562,7 +4557,7 @@ var ReadTool = defineTool({
2562
4557
  if (!info.isFile()) {
2563
4558
  throw new ToolError(`Not a regular file: ${path}`, "Read");
2564
4559
  }
2565
- const content = await readFile5(path, "utf-8");
4560
+ const content = await readFile6(path, "utf-8");
2566
4561
  const lines = content.split(/\r?\n/);
2567
4562
  const offset = args.offset ?? 0;
2568
4563
  const limit = args.limit ?? DEFAULT_LIMIT;
@@ -2585,9 +4580,26 @@ var ReadTool = defineTool({
2585
4580
  });
2586
4581
 
2587
4582
  // src/tools/builtin/write.ts
2588
- import { writeFile as writeFile2, mkdir as mkdir3, stat as stat3 } from "fs/promises";
2589
- import { resolve as resolve3, isAbsolute as isAbsolute2, dirname as dirname4 } from "path";
4583
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir4, stat as stat3 } from "fs/promises";
4584
+ import { resolve as resolve4, isAbsolute as isAbsolute2, dirname as dirname4 } from "path";
2590
4585
  import { z as z4 } from "zod";
4586
+
4587
+ // src/tools/_diff.ts
4588
+ import { createPatch } from "diff";
4589
+ var MAX_DIFF_LINES = 200;
4590
+ function makeUnifiedDiff(filePath, oldContent, newContent) {
4591
+ if (oldContent === newContent) return "";
4592
+ const patch = createPatch(filePath, oldContent, newContent, "before", "after", { context: 3 });
4593
+ return truncate(patch);
4594
+ }
4595
+ function truncate(diff) {
4596
+ const lines = diff.split("\n");
4597
+ if (lines.length <= MAX_DIFF_LINES) return diff;
4598
+ return lines.slice(0, MAX_DIFF_LINES).join("\n") + `
4599
+ ... [${lines.length - MAX_DIFF_LINES} more diff lines truncated]`;
4600
+ }
4601
+
4602
+ // src/tools/builtin/write.ts
2591
4603
  var WriteArgs = z4.object({
2592
4604
  file_path: z4.string().describe("Absolute or cwd-relative path to the file."),
2593
4605
  content: z4.string().describe("Full content of the file.")
@@ -2599,25 +4611,33 @@ var WriteTool = defineTool({
2599
4611
  permission: "write",
2600
4612
  summarize: (args) => `Write(${args.file_path}, ${args.content.length} chars)`,
2601
4613
  async execute(args, ctx) {
2602
- const path = isAbsolute2(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
4614
+ const path = isAbsolute2(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
4615
+ const sensitive = checkSensitivePath(path);
4616
+ if (sensitive.blocked) {
4617
+ return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
4618
+ }
2603
4619
  let existed = false;
4620
+ let oldContent = "";
2604
4621
  try {
2605
4622
  const info = await stat3(path);
2606
4623
  existed = info.isFile();
4624
+ if (existed) oldContent = await readFile7(path, "utf-8");
2607
4625
  } catch {
2608
4626
  }
2609
- await mkdir3(dirname4(path), { recursive: true });
2610
- await writeFile2(path, args.content, "utf-8");
4627
+ await mkdir4(dirname4(path), { recursive: true });
4628
+ await writeFile3(path, args.content, "utf-8");
4629
+ const diff = makeUnifiedDiff(args.file_path, oldContent, args.content);
2611
4630
  return {
2612
4631
  content: existed ? `Overwrote ${path} (${args.content.length} bytes).` : `Created ${path} (${args.content.length} bytes).`,
2613
- summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`
4632
+ summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`,
4633
+ diff: diff || void 0
2614
4634
  };
2615
4635
  }
2616
4636
  });
2617
4637
 
2618
4638
  // src/tools/builtin/edit.ts
2619
- import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
2620
- import { resolve as resolve4, isAbsolute as isAbsolute3 } from "path";
4639
+ import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
4640
+ import { resolve as resolve5, isAbsolute as isAbsolute3 } from "path";
2621
4641
  import { z as z5 } from "zod";
2622
4642
  var EditArgs = z5.object({
2623
4643
  file_path: z5.string().describe("Absolute or cwd-relative path to the file."),
@@ -2632,10 +4652,14 @@ var EditTool = defineTool({
2632
4652
  permission: "write",
2633
4653
  summarize: (args) => `Edit(${args.file_path})`,
2634
4654
  async execute(args, ctx) {
2635
- const path = isAbsolute3(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
4655
+ const path = isAbsolute3(args.file_path) ? args.file_path : resolve5(ctx.cwd, args.file_path);
4656
+ const sensitive = checkSensitivePath(path);
4657
+ if (sensitive.blocked) {
4658
+ return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
4659
+ }
2636
4660
  let content;
2637
4661
  try {
2638
- content = await readFile6(path, "utf-8");
4662
+ content = await readFile8(path, "utf-8");
2639
4663
  } catch (err) {
2640
4664
  throw new ToolError(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`, "Edit", err);
2641
4665
  }
@@ -2656,10 +4680,12 @@ var EditTool = defineTool({
2656
4680
  };
2657
4681
  }
2658
4682
  const newContent = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
2659
- await writeFile3(path, newContent, "utf-8");
4683
+ await writeFile4(path, newContent, "utf-8");
4684
+ const diff = makeUnifiedDiff(args.file_path, content, newContent);
2660
4685
  return {
2661
4686
  content: `Edited ${path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s).`,
2662
- summary: `Edited ${args.file_path}`
4687
+ summary: `Edited ${args.file_path}`,
4688
+ diff: diff || void 0
2663
4689
  };
2664
4690
  }
2665
4691
  });
@@ -2738,8 +4764,8 @@ var BashTool = defineTool({
2738
4764
  maxBuffer: MAX_OUTPUT_BYTES * 2,
2739
4765
  cancelSignal: ctx.abortSignal
2740
4766
  });
2741
- const stdout = truncate(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
2742
- const stderr = truncate(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
4767
+ const stdout = truncate2(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
4768
+ const stderr = truncate2(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
2743
4769
  const parts = [];
2744
4770
  if (stdout) parts.push(`<stdout>
2745
4771
  ${stdout}
@@ -2763,7 +4789,7 @@ ${stderr}
2763
4789
  }
2764
4790
  }
2765
4791
  });
2766
- function truncate(text, max, label) {
4792
+ function truncate2(text, max, label) {
2767
4793
  if (text.length <= max) return text;
2768
4794
  return text.slice(0, max) + `
2769
4795
  ... [${label} truncated, original ${text.length} bytes]`;
@@ -2872,6 +4898,298 @@ var GlobTool = defineTool({
2872
4898
  }
2873
4899
  });
2874
4900
 
4901
+ // src/tools/builtin/todo.ts
4902
+ import { z as z9 } from "zod";
4903
+ var TodoSchema = z9.object({
4904
+ content: z9.string().describe("Imperative one-line task description (e.g. 'Run the test suite')."),
4905
+ status: z9.enum(["pending", "in_progress", "completed"]).describe("Current status."),
4906
+ activeForm: z9.string().optional().describe("Present-continuous form for the spinner (e.g. 'Running the test suite').")
4907
+ });
4908
+ var TodoWriteArgs = z9.object({
4909
+ todos: z9.array(TodoSchema).describe("Full list. Replaces the current store.")
4910
+ });
4911
+ var TodoWriteTool = defineTool({
4912
+ name: "TodoWrite",
4913
+ description: "Maintain a structured task list for the current session. Pass the FULL list every call (it replaces the store). Mark exactly one task in_progress at a time; mark completed immediately when done; do not batch completions. Use when the task has 3+ distinct steps or is non-trivial. Skip for single trivial actions.",
4914
+ parameters: TodoWriteArgs,
4915
+ permission: "read",
4916
+ summarize: (args) => `TodoWrite(${args.todos.length} items)`,
4917
+ async execute(args, ctx) {
4918
+ if (!ctx.todos) {
4919
+ return {
4920
+ content: "TodoWrite is unavailable: this agent run has no todo store. (Internal bug; tell the user.)",
4921
+ isError: true
4922
+ };
4923
+ }
4924
+ ctx.todos.set(args.todos);
4925
+ const summary = args.todos.map((t, i) => `${i + 1}. ${t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]"} ${t.content}`).join("\n");
4926
+ return {
4927
+ content: `Updated todos (${args.todos.length} items):
4928
+ ${summary}`,
4929
+ summary: `Todos: ${args.todos.filter((t) => t.status === "completed").length}/${args.todos.length} done`
4930
+ };
4931
+ }
4932
+ });
4933
+
4934
+ // src/tools/builtin/webfetch.ts
4935
+ import { z as z10 } from "zod";
4936
+ var WebFetchArgs = z10.object({
4937
+ url: z10.string().describe("Fully-qualified URL. http will be upgraded to https."),
4938
+ prompt: z10.string().optional().describe(
4939
+ "What information to look for. The host returns the page content; the LLM should then read it to answer the prompt."
4940
+ )
4941
+ });
4942
+ var MAX_RESPONSE_BYTES = 1e6;
4943
+ var FETCH_TIMEOUT_MS = 3e4;
4944
+ var PRIVATE_HOST_PATTERNS = [
4945
+ /^localhost$/i,
4946
+ /^127\./,
4947
+ /^0\.0\.0\.0$/,
4948
+ /^169\.254\./,
4949
+ /^10\./,
4950
+ /^192\.168\./,
4951
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
4952
+ /^::1$/,
4953
+ /^fc[0-9a-f]{2}:/i,
4954
+ /^fe80:/i
4955
+ ];
4956
+ function isPrivateHost(hostname) {
4957
+ return PRIVATE_HOST_PATTERNS.some((p) => p.test(hostname));
4958
+ }
4959
+ var WebFetchTool = defineTool({
4960
+ name: "WebFetch",
4961
+ description: "Fetch a URL and return its textual content (HTML stripped to a markdown-ish form). Use for reading documentation, blog posts, or API specs. Private/loopback hosts are blocked. If the URL redirects to a different host, the redirect target is returned for you to re-fetch.",
4962
+ parameters: WebFetchArgs,
4963
+ permission: "network",
4964
+ summarize: (args) => `WebFetch(${args.url})`,
4965
+ async execute(args, ctx) {
4966
+ let target;
4967
+ try {
4968
+ target = new URL(args.url);
4969
+ } catch {
4970
+ return { content: `Invalid URL: ${args.url}`, isError: true };
4971
+ }
4972
+ if (target.protocol === "http:") {
4973
+ target.protocol = "https:";
4974
+ }
4975
+ if (target.protocol !== "https:") {
4976
+ return { content: `Refused: only http(s) URLs are allowed.`, isError: true };
4977
+ }
4978
+ if (isPrivateHost(target.hostname)) {
4979
+ return { content: `Refused: ${target.hostname} is a private/loopback host (SSRF guard).`, isError: true };
4980
+ }
4981
+ const controller = new AbortController();
4982
+ const onAbort = () => controller.abort();
4983
+ ctx.abortSignal?.addEventListener("abort", onAbort);
4984
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
4985
+ try {
4986
+ const resp = await fetch(target.toString(), {
4987
+ redirect: "manual",
4988
+ signal: controller.signal,
4989
+ headers: { "user-agent": "muse-cli/0.1" }
4990
+ });
4991
+ if (resp.status >= 300 && resp.status < 400) {
4992
+ const loc = resp.headers.get("location");
4993
+ if (loc) {
4994
+ try {
4995
+ const redirectURL = new URL(loc, target);
4996
+ if (redirectURL.hostname !== target.hostname) {
4997
+ return {
4998
+ content: `Redirect to a different host: ${redirectURL.toString()}
4999
+ Re-fetch the new URL explicitly if you trust it.`,
5000
+ summary: `Redirect to a different host: ${redirectURL.toString()}`,
5001
+ kind: "warn"
5002
+ };
5003
+ }
5004
+ return {
5005
+ content: `Redirect (same host): ${redirectURL.toString()}
5006
+ Re-fetch the new URL to continue.`,
5007
+ summary: `Redirect \u2192 ${redirectURL.pathname}`,
5008
+ kind: "warn"
5009
+ };
5010
+ } catch {
5011
+ return { content: `Redirect with unparseable location: ${loc}`, isError: true };
5012
+ }
5013
+ }
5014
+ }
5015
+ if (!resp.ok) {
5016
+ return { content: `HTTP ${resp.status} ${resp.statusText} for ${target.toString()}`, isError: true };
5017
+ }
5018
+ const contentType = resp.headers.get("content-type") ?? "";
5019
+ const reader = resp.body?.getReader();
5020
+ if (!reader) return { content: `Empty response body.`, isError: true };
5021
+ const chunks = [];
5022
+ let total = 0;
5023
+ while (true) {
5024
+ const { value, done } = await reader.read();
5025
+ if (done) break;
5026
+ if (value) {
5027
+ total += value.byteLength;
5028
+ if (total > MAX_RESPONSE_BYTES) {
5029
+ await reader.cancel();
5030
+ chunks.push(value.slice(0, value.byteLength - (total - MAX_RESPONSE_BYTES)));
5031
+ break;
5032
+ }
5033
+ chunks.push(value);
5034
+ }
5035
+ }
5036
+ const body = new TextDecoder("utf-8", { fatal: false }).decode(Buffer.concat(chunks.map((c) => Buffer.from(c))));
5037
+ let processed = body;
5038
+ if (/^text\/html|application\/xhtml/i.test(contentType)) {
5039
+ processed = htmlToText(body);
5040
+ }
5041
+ const summary = args.prompt ? `# WebFetch result for: ${args.prompt}` : `Fetched ${target.hostname} (${total} bytes${total >= MAX_RESPONSE_BYTES ? ", truncated" : ""})`;
5042
+ const truncated = processed.length > 2e5 ? processed.slice(0, 2e5) + "\n\n... [truncated]" : processed;
5043
+ const preface = args.prompt ? `# WebFetch result for: ${args.prompt}
5044
+
5045
+ Source: ${target.toString()}
5046
+
5047
+ ` : `Source: ${target.toString()}
5048
+
5049
+ `;
5050
+ return { content: preface + truncated, summary };
5051
+ } catch (err) {
5052
+ if (err.name === "AbortError") {
5053
+ return { content: `WebFetch aborted (timeout or user cancel).`, isError: true };
5054
+ }
5055
+ return { content: `WebFetch failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
5056
+ } finally {
5057
+ clearTimeout(timer);
5058
+ ctx.abortSignal?.removeEventListener("abort", onAbort);
5059
+ }
5060
+ }
5061
+ });
5062
+ function htmlToText(html) {
5063
+ let s = html;
5064
+ s = s.replace(/<(script|style|svg|noscript)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
5065
+ s = s.replace(/<!--[\s\S]*?-->/g, "");
5066
+ s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, txt) => {
5067
+ return `
5068
+
5069
+ ${"#".repeat(parseInt(lvl, 10))} ${stripTags(txt).trim()}
5070
+
5071
+ `;
5072
+ });
5073
+ s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, txt) => {
5074
+ const label = stripTags(txt).trim();
5075
+ return label ? `[${label}](${href})` : href;
5076
+ });
5077
+ s = s.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_m, txt) => `
5078
+ - ${stripTags(txt).trim()}`);
5079
+ s = s.replace(/<(p|div|section|article|header|footer|main|aside|nav|pre|blockquote|br|hr)\b[^>]*>/gi, "\n");
5080
+ s = s.replace(/<\/(p|div|section|article|header|footer|main|aside|nav|pre|blockquote)>/gi, "\n");
5081
+ s = s.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_m, txt) => `\`${stripTags(txt)}\``);
5082
+ s = stripTags(s);
5083
+ s = s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
5084
+ s = s.replace(/\n{3,}/g, "\n\n").trim();
5085
+ return s;
5086
+ }
5087
+ function stripTags(s) {
5088
+ return s.replace(/<[^>]+>/g, "");
5089
+ }
5090
+
5091
+ // src/tools/builtin/memory.ts
5092
+ import { z as z11 } from "zod";
5093
+ var TYPES = ["user", "feedback", "project", "reference"];
5094
+ var MemoryWriteArgs = z11.object({
5095
+ name: z11.string().regex(/^[a-z0-9][a-z0-9-_]*$/i, "must be a kebab- or snake-style slug").describe("Short kebab/snake slug; used as filename (<name>.md) and index key."),
5096
+ description: z11.string().describe("One-line summary used in MEMORY.md index (decides future relevance)."),
5097
+ type: z11.enum(TYPES).describe("user | feedback | project | reference"),
5098
+ body: z11.string().describe("Memory content (markdown). For feedback/project, lead with the rule/fact then **Why:** and **How to apply:** lines.")
5099
+ });
5100
+ var MemoryWriteTool = defineTool({
5101
+ name: "MemoryWrite",
5102
+ description: "Save a long-term memory file under ~/.muse/projects/<hash>/memory/<name>.md and update MEMORY.md index. Use for: user role/preferences, validated approach decisions (feedback), project facts (auto-convert relative dates), external system references. Do NOT save: code patterns derivable from the repo, git history, fix recipes, ephemeral task state.",
5103
+ parameters: MemoryWriteArgs,
5104
+ permission: "write",
5105
+ summarize: (args) => `MemoryWrite(${args.name}, type=${args.type})`,
5106
+ async execute(args, ctx) {
5107
+ const { filePath, indexUpdated } = await writeMemory(ctx.cwd, {
5108
+ name: args.name,
5109
+ description: args.description,
5110
+ type: args.type,
5111
+ body: args.body
5112
+ });
5113
+ return {
5114
+ content: `Saved memory "${args.name}" (${args.type}) \u2192 ${filePath}${indexUpdated ? "\nMEMORY.md updated." : ""}`,
5115
+ summary: `MemoryWrite ${args.name}`
5116
+ };
5117
+ }
5118
+ });
5119
+ var MemoryReadArgs = z11.object({
5120
+ name: z11.string().describe("Memory slug to read (no .md extension).")
5121
+ });
5122
+ var MemoryReadTool = defineTool({
5123
+ name: "MemoryRead",
5124
+ description: "Read a specific long-term memory file by name. Use after seeing it referenced in MEMORY.md (which is auto-injected into the system prompt).",
5125
+ parameters: MemoryReadArgs,
5126
+ permission: "read",
5127
+ summarize: (args) => `MemoryRead(${args.name})`,
5128
+ async execute(args, ctx) {
5129
+ try {
5130
+ const content = await readMemoryFile(ctx.cwd, args.name);
5131
+ return { content, summary: `MemoryRead ${args.name}` };
5132
+ } catch (err) {
5133
+ return { content: err instanceof Error ? err.message : String(err), isError: true };
5134
+ }
5135
+ }
5136
+ });
5137
+
5138
+ // src/tools/builtin/ask-user-question.ts
5139
+ import { z as z12 } from "zod";
5140
+ var AskQuestionOptionSchema = z12.object({
5141
+ label: z12.string().min(1).describe("Option text shown to the user. Concise (1-5 words)."),
5142
+ description: z12.string().optional().describe("Optional one-line explanation of what this option means."),
5143
+ preview: z12.string().optional().describe(
5144
+ "Optional rich preview rendered in a right-side panel when this option is focused. Use for code/diagram/config snippets that help compare options visually. Multi-line text supported."
5145
+ )
5146
+ });
5147
+ var AskQuestionSchema = z12.object({
5148
+ question: z12.string().min(1).describe("Full question text (end with ?)."),
5149
+ header: z12.string().min(1).max(16).describe("Very short label (chip), max 12 chars. E.g. 'Auth method'."),
5150
+ options: z12.array(AskQuestionOptionSchema).min(2).max(4).describe("2-4 options. Mutually exclusive unless multiSelect=true."),
5151
+ multiSelect: z12.boolean().optional().describe("Allow multiple selections. Default false.")
5152
+ });
5153
+ var AskUserQuestionArgs = z12.object({
5154
+ questions: z12.array(AskQuestionSchema).min(1).max(4).describe("1-4 questions to ask the user sequentially.")
5155
+ });
5156
+ var AskUserQuestionTool = defineTool({
5157
+ name: "AskUserQuestion",
5158
+ description: "Ask the user one or more multiple-choice questions when their input is needed to proceed. Each question has 2-4 options. Use multiSelect=true for non-mutually-exclusive choices. Prefer this over plain-text questions when the answer space is bounded. If the user presses Esc, the entire batch is treated as cancelled.",
5159
+ parameters: AskUserQuestionArgs,
5160
+ permission: "read",
5161
+ summarize: (args) => `AskUserQuestion(${args.questions.length} question${args.questions.length === 1 ? "" : "s"})`,
5162
+ async execute(args, ctx) {
5163
+ if (!ctx.askQuestions) {
5164
+ return {
5165
+ content: "AskUserQuestion is unavailable: this agent run has no question handler. (Internal bug; tell the user.)",
5166
+ isError: true
5167
+ };
5168
+ }
5169
+ const responses = await ctx.askQuestions(args.questions);
5170
+ if (responses.length > 0 && responses[0].cancelled) {
5171
+ return {
5172
+ content: "User cancelled (Esc). No answers were collected.",
5173
+ isError: false
5174
+ };
5175
+ }
5176
+ const blocks = args.questions.map((q, qi) => {
5177
+ const r = responses[qi];
5178
+ const sel = r?.selections ?? [];
5179
+ const ans = sel.length === 0 ? "(no answer)" : sel.join(", ");
5180
+ const notes = r?.notes?.trim();
5181
+ return notes ? `Q: ${q.question}
5182
+ A: ${ans}
5183
+ Notes: ${notes}` : `Q: ${q.question}
5184
+ A: ${ans}`;
5185
+ });
5186
+ return {
5187
+ content: blocks.join("\n\n"),
5188
+ summary: `Asked ${args.questions.length} question${args.questions.length === 1 ? "" : "s"}`
5189
+ };
5190
+ }
5191
+ });
5192
+
2875
5193
  // src/tools/builtin/index.ts
2876
5194
  var BUILTIN_TOOLS = [
2877
5195
  ReadTool,
@@ -2879,16 +5197,21 @@ var BUILTIN_TOOLS = [
2879
5197
  EditTool,
2880
5198
  BashTool,
2881
5199
  GrepTool,
2882
- GlobTool
5200
+ GlobTool,
5201
+ TodoWriteTool,
5202
+ WebFetchTool,
5203
+ MemoryReadTool,
5204
+ MemoryWriteTool,
5205
+ AskUserQuestionTool
2883
5206
  ];
2884
5207
 
2885
5208
  // src/cli.tsx
2886
- import { jsx as jsx10 } from "react/jsx-runtime";
5209
+ import { jsx as jsx16 } from "react/jsx-runtime";
2887
5210
  var VERSION = "0.1.0";
2888
5211
  async function main() {
2889
5212
  const program = new Command();
2890
5213
  program.name("muse").description("A TypeScript agent CLI built around OpenAI-compatible APIs. First-class support for self-hostable and Chinese LLMs.").version(VERSION, "-v, --version", "print version");
2891
- program.argument("[prompt...]", "one-shot prompt (omit for interactive mode)").option("-m, --model <model>", "override model").option("-p, --provider <provider>", "override provider").option("--no-banner", "skip startup banner").option("--quiet", "minimal output (implies --no-banner)").option("--continue", "resume last session in this directory").option("--debug", "verbose logging").action(async (promptArgs, opts) => {
5214
+ program.argument("[prompt...]", "one-shot prompt (omit for interactive mode)").option("-m, --model <model>", "override model").option("-p, --provider <provider>", "override provider").option("--no-banner", "skip startup banner").option("--quiet", "minimal output (implies --no-banner)").option("--continue", "resume last session in this directory").option("--mode <mode>", "initial permission mode (default|acceptEdits|plan|bypassPermissions)").option("--debug", "verbose logging").action(async (promptArgs, opts) => {
2892
5215
  if (opts.debug) log.setLevel("debug");
2893
5216
  const cwd = process.cwd();
2894
5217
  const { settings, sources } = await loadSettings(cwd);
@@ -2908,7 +5231,7 @@ async function main() {
2908
5231
  llmModelName = llm.model;
2909
5232
  } else {
2910
5233
  if (!provider || !model) {
2911
- die("No model configured. Either define one in ~/.muse/models.json or set llm.provider+llm.model in settings.json.");
5234
+ die("No model configured. Either define one in ~/.muse/models.local.json or set llm.provider+llm.model in settings.json.");
2912
5235
  }
2913
5236
  llm = createLLMClient({ provider, model, providers: settings.providers ?? {} });
2914
5237
  llmProviderName = provider;
@@ -2921,24 +5244,62 @@ async function main() {
2921
5244
  const tools = new ToolRegistry();
2922
5245
  tools.registerAll(BUILTIN_TOOLS);
2923
5246
  const permissions = new PermissionGate(settings.permissions);
2924
- const session = await Session.create(cwd);
2925
- await session.append({
2926
- type: "session_start",
2927
- time: (/* @__PURE__ */ new Date()).toISOString(),
2928
- cwd,
2929
- provider: llmProviderName,
2930
- model: llmModelName
2931
- });
5247
+ if (opts.mode) {
5248
+ const valid = ["default", "acceptEdits", "plan", "bypassPermissions"];
5249
+ if (!valid.includes(opts.mode)) {
5250
+ die(`Invalid --mode "${opts.mode}". Valid: ${valid.join(", ")}`);
5251
+ }
5252
+ permissions.setMode(opts.mode);
5253
+ }
5254
+ let session;
5255
+ let initialMessages;
5256
+ if (opts.continue) {
5257
+ const latest = await Session.findLatest(cwd);
5258
+ if (latest) {
5259
+ const opened = await Session.open(latest);
5260
+ session = opened.session;
5261
+ initialMessages = Session.messagesFromEvents(opened.events);
5262
+ log.debug("resumed session", { id: latest.id, messages: initialMessages.length });
5263
+ } else {
5264
+ session = await Session.create(cwd);
5265
+ await session.append({
5266
+ type: "session_start",
5267
+ time: (/* @__PURE__ */ new Date()).toISOString(),
5268
+ cwd,
5269
+ provider: llmProviderName,
5270
+ model: llmModelName
5271
+ });
5272
+ }
5273
+ } else {
5274
+ session = await Session.create(cwd);
5275
+ await session.append({
5276
+ type: "session_start",
5277
+ time: (/* @__PURE__ */ new Date()).toISOString(),
5278
+ cwd,
5279
+ provider: llmProviderName,
5280
+ model: llmModelName
5281
+ });
5282
+ }
2932
5283
  const showBanner = !opts.quiet && opts.banner !== false;
2933
5284
  const lang = settings.ui?.lang ?? "en";
2934
5285
  const pipedInput = await readStdinIfPiped();
2935
5286
  const oneShotPrompt = [...promptArgs ?? [], pipedInput].filter(Boolean).join("\n").trim();
2936
5287
  if (oneShotPrompt) {
2937
- await runOneShot({ llm, tools, permissions, session, cwd, lang, prompt: oneShotPrompt, quiet: opts.quiet ?? false });
5288
+ await runOneShot({
5289
+ llm,
5290
+ tools,
5291
+ permissions,
5292
+ session,
5293
+ cwd,
5294
+ lang,
5295
+ prompt: oneShotPrompt,
5296
+ quiet: opts.quiet ?? false,
5297
+ initialMessages
5298
+ });
2938
5299
  return;
2939
5300
  }
2940
5301
  const { waitUntilExit } = render(
2941
- /* @__PURE__ */ jsx10(
5302
+ /* @__PURE__ */ jsx16(
2942
5303
  App,
2943
5304
  {
2944
5305
  llm,
@@ -2951,7 +5312,8 @@ async function main() {
2951
5312
  modelsSources,
2952
5313
  cwd,
2953
5314
  lang,
2954
- showBanner
5315
+ showBanner,
5316
+ initialMessages
2955
5317
  }
2956
5318
  )
2957
5319
  );
@@ -2966,12 +5328,14 @@ async function readStdinIfPiped() {
2966
5328
  return Buffer.concat(chunks).toString("utf-8").trim();
2967
5329
  }
2968
5330
  async function runOneShot(opts) {
5331
+ const memoryIndex = await loadMemoryIndex(opts.cwd);
2969
5332
  const systemPrompt = buildSystemPrompt({
2970
5333
  cwd: opts.cwd,
2971
5334
  model: opts.llm.model,
2972
5335
  provider: opts.llm.providerName,
2973
5336
  lang: opts.lang,
2974
- toolNames: opts.tools.list().map((t) => t.name)
5337
+ toolNames: opts.tools.list().map((t) => t.name),
5338
+ memoryIndex
2975
5339
  });
2976
5340
  const agent = new Agent({
2977
5341
  llm: opts.llm,
@@ -2994,10 +5358,11 @@ async function runOneShot(opts) {
2994
5358
  if (!opts.quiet) process.stderr.write(`
2995
5359
  [denied: ${toolName} \u2014 ${summary}; run in interactive mode to approve]
2996
5360
  `);
2997
- return false;
5361
+ return "no";
2998
5362
  }
2999
5363
  }
3000
5364
  });
5365
+ if (opts.initialMessages?.length) agent.setMessages(opts.initialMessages);
3001
5366
  await agent.runTurn(opts.prompt);
3002
5367
  process.stdout.write("\n");
3003
5368
  }