@qxbyte/muse 0.1.0 → 0.1.2

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,147 @@ 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 useState7 } from "react";
9
+ import { Box as Box12, Text as Text14, useApp, useInput as useInput4, useStdout } 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
+ function BgTextInput({
17
+ value,
18
+ onChange,
19
+ onSubmit,
20
+ width,
21
+ backgroundColor,
22
+ color,
23
+ isActive = true
24
+ }) {
25
+ const [cursor, setCursor] = useState(value.length);
26
+ const [blinkOn, setBlinkOn] = useState(true);
27
+ useEffect(() => {
28
+ setCursor((c) => Math.min(c, value.length));
29
+ }, [value]);
30
+ useEffect(() => {
31
+ if (!isActive) return;
32
+ setBlinkOn(true);
33
+ const id = setInterval(() => setBlinkOn((b) => !b), BLINK_MS);
34
+ return () => clearInterval(id);
35
+ }, [isActive, cursor, value]);
36
+ useInput(
37
+ (input, key) => {
38
+ if (key.return) {
39
+ onSubmit?.(value);
40
+ return;
41
+ }
42
+ if (key.backspace || key.delete) {
43
+ if (cursor === 0) return;
44
+ const next = value.slice(0, cursor - 1) + value.slice(cursor);
45
+ onChange(next);
46
+ setCursor((c) => Math.max(0, c - 1));
47
+ return;
48
+ }
49
+ if (key.leftArrow) {
50
+ setCursor((c) => Math.max(0, c - 1));
51
+ return;
52
+ }
53
+ if (key.rightArrow) {
54
+ setCursor((c) => Math.min(value.length, c + 1));
55
+ return;
56
+ }
57
+ if (key.ctrl && input === "a") {
58
+ setCursor(0);
59
+ return;
60
+ }
61
+ if (key.ctrl && input === "e") {
62
+ setCursor(value.length);
63
+ return;
64
+ }
65
+ if (key.ctrl || key.shift || key.tab || key.escape || key.upArrow || key.downArrow || key.meta) {
66
+ return;
67
+ }
68
+ if (input && !key.return) {
69
+ const next = value.slice(0, cursor) + input + value.slice(cursor);
70
+ onChange(next);
71
+ setCursor((c) => c + input.length);
72
+ }
73
+ },
74
+ { isActive }
75
+ );
76
+ const view = computeViewport(value, cursor, width);
77
+ const at = view.atChar;
78
+ const padLen = Math.max(0, width - view.consumedWidth);
79
+ const showCursor = isActive && blinkOn;
80
+ return /* @__PURE__ */ jsxs(Text, { backgroundColor, color, children: [
81
+ view.before,
82
+ showCursor ? /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color, dimColor: true, children: at }) : /* @__PURE__ */ jsx(Text, { backgroundColor, color, children: at }),
83
+ view.after,
84
+ " ".repeat(padLen)
85
+ ] });
86
+ }
87
+ function charWidth(ch) {
88
+ const cp = ch.codePointAt(0);
89
+ if (cp === void 0) return 0;
90
+ if (cp < 32 || cp === 127) return 0;
91
+ if (cp >= 4352 && cp <= 4447 || // Hangul Jamo
92
+ cp >= 11904 && cp <= 12350 || // CJK Radicals
93
+ cp >= 12353 && cp <= 13311 || // Hiragana / Katakana / CJK Symbols
94
+ cp >= 13312 && cp <= 19903 || // CJK Ext A
95
+ cp >= 19968 && cp <= 40959 || // CJK Unified
96
+ cp >= 40960 && cp <= 42191 || // Yi
97
+ cp >= 44032 && cp <= 55203 || // Hangul Syllables
98
+ cp >= 63744 && cp <= 64255 || // CJK Compat
99
+ cp >= 65072 && cp <= 65103 || // CJK Compat Forms
100
+ cp >= 65280 && cp <= 65376 || // Fullwidth ASCII / Punctuation
101
+ cp >= 65504 && cp <= 65510 || // Fullwidth Sign
102
+ cp >= 131072 && cp <= 196605) {
103
+ return 2;
104
+ }
105
+ return 1;
106
+ }
107
+ function stringWidth(s) {
108
+ let w = 0;
109
+ for (const ch of s) w += charWidth(ch);
110
+ return w;
111
+ }
112
+ function computeViewport(value, cursor, width) {
113
+ const cursorAtEnd = cursor >= value.length;
114
+ const atChar = cursorAtEnd ? " " : value[cursor] ?? " ";
115
+ const cursorCellW = charWidth(atChar);
116
+ let beforeStart = 0;
117
+ while (true) {
118
+ const before = value.slice(beforeStart, cursor);
119
+ const after = cursorAtEnd ? "" : value.slice(cursor + 1);
120
+ const total = stringWidth(before) + cursorCellW + stringWidth(after);
121
+ if (total <= width) {
122
+ return { before, atChar, after, consumedWidth: total };
123
+ }
124
+ if (beforeStart >= cursor) {
125
+ let after2 = cursorAtEnd ? "" : value.slice(cursor + 1);
126
+ while (after2.length > 0 && stringWidth("") + cursorCellW + stringWidth(after2) > width) {
127
+ after2 = after2.slice(0, -1);
128
+ }
129
+ return {
130
+ before: "",
131
+ atChar,
132
+ after: after2,
133
+ consumedWidth: cursorCellW + stringWidth(after2)
134
+ };
135
+ }
136
+ beforeStart++;
137
+ }
138
+ }
139
+
140
+ // src/app.tsx
141
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
142
+ import { existsSync as existsSync5 } from "fs";
143
+ import { homedir as homedir8 } from "os";
144
+ import { basename, dirname as dirname3, join as join6 } from "path";
145
+
146
+ // src/components/StartupBanner.tsx
147
+ import { Box, Text as Text2 } from "ink";
148
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
19
149
  var LETTERS = {
20
150
  M: ["\u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588"],
21
151
  U: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588\u2588\u2588 "],
@@ -35,87 +165,87 @@ var LETTER_GAP = 3;
35
165
  var LOGO_WIDTH = 5 * 4 + LETTER_GAP * 3;
36
166
  var GAP_WIDTH = 6;
37
167
  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] }) })
168
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
169
+ /* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.M, children: LETTERS.M[row] }) }),
170
+ /* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.U, children: LETTERS.U[row] }) }),
171
+ /* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.S, children: LETTERS.S[row] }) }),
172
+ /* @__PURE__ */ jsx2(Box, { marginLeft: LETTER_GAP, children: /* @__PURE__ */ jsx2(Text2, { color: COLORS.E, children: LETTERS.E[row] }) })
43
173
  ] });
44
174
  }
45
175
  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 }),
176
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
177
+ /* @__PURE__ */ jsx2(Box, { minWidth: LOGO_WIDTH, children: /* @__PURE__ */ jsx2(LogoLine, { row }) }),
178
+ /* @__PURE__ */ jsx2(Box, { width: GAP_WIDTH }),
49
179
  children ?? null
50
180
  ] });
51
181
  }
52
182
  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: [
183
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingY: 0, children: [
184
+ /* @__PURE__ */ jsx2(BannerLine, { row: 0 }),
185
+ /* @__PURE__ */ jsx2(BannerLine, { row: 1, children: /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
186
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.asterisk, children: "\u273B" }),
187
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: " Welcome to Muse " }),
188
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
59
189
  "v",
60
190
  version
61
191
  ] })
62
192
  ] }) }),
63
- /* @__PURE__ */ jsx(BannerLine, { row: 2, children: /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
193
+ /* @__PURE__ */ jsx2(BannerLine, { row: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
64
194
  "model: ",
65
195
  model
66
196
  ] }) }),
67
- /* @__PURE__ */ jsx(BannerLine, { row: 3, children: /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
197
+ /* @__PURE__ */ jsx2(BannerLine, { row: 3, children: /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
68
198
  "cwd: ",
69
199
  cwd
70
200
  ] }) }),
71
- /* @__PURE__ */ jsx(BannerLine, { row: 4 })
201
+ /* @__PURE__ */ jsx2(BannerLine, { row: 4 })
72
202
  ] });
73
203
  }
74
204
  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: [
205
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingY: 0, children: [
206
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", children: [
207
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.asterisk, children: "\u273B" }),
208
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: " Welcome to Muse " }),
209
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
80
210
  "v",
81
211
  version
82
212
  ] })
83
213
  ] }),
84
- /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
214
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
85
215
  "model: ",
86
216
  model
87
217
  ] }),
88
- /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
218
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
89
219
  "cwd: ",
90
220
  cwd
91
221
  ] })
92
222
  ] });
93
223
  }
94
224
  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: [
225
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
226
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.text, children: "Muse " }),
227
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.versionAccent, children: [
98
228
  "v",
99
229
  version
100
230
  ] }),
101
- /* @__PURE__ */ jsxs(Text, { color: COLORS.text, children: [
231
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.text, children: [
102
232
  " \xB7 ",
103
233
  model
104
234
  ] })
105
235
  ] });
106
236
  }
107
237
  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 });
238
+ if (width >= 60) return /* @__PURE__ */ jsx2(StartupBanner, { ...props });
239
+ if (width >= 40) return /* @__PURE__ */ jsx2(CompactBanner, { ...props });
240
+ return /* @__PURE__ */ jsx2(SingleLineBanner, { version: props.version, model: props.model });
111
241
  }
112
242
 
113
243
  // src/components/MessageView.tsx
114
244
  import { useMemo } from "react";
115
- import { Box as Box2, Text as Text2 } from "ink";
245
+ import { Box as Box2, Text as Text3 } from "ink";
116
246
  import { marked } from "marked";
117
247
  import { markedTerminal } from "marked-terminal";
118
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
248
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
119
249
  marked.use(markedTerminal());
120
250
  function renderMarkdown(text) {
121
251
  try {
@@ -128,11 +258,21 @@ function renderMarkdown(text) {
128
258
  function MessageView({ message }) {
129
259
  switch (message.role) {
130
260
  case "user":
131
- return /* @__PURE__ */ jsx2(UserMessage, { content: typeof message.content === "string" ? message.content : flattenText(message.content) });
261
+ return /* @__PURE__ */ jsx3(UserMessage, { content: typeof message.content === "string" ? message.content : flattenText(message.content) });
132
262
  case "assistant":
133
- return /* @__PURE__ */ jsx2(AssistantMessage, { content: message.content });
263
+ return /* @__PURE__ */ jsx3(AssistantMessage, { content: message.content });
134
264
  case "tool":
135
- return /* @__PURE__ */ jsx2(ToolResultLine, { isError: message.isError ?? false, content: message.content });
265
+ if (message.toolName === "TodoWrite") return null;
266
+ return /* @__PURE__ */ jsx3(
267
+ ToolResultLine,
268
+ {
269
+ isError: message.isError ?? false,
270
+ content: message.content,
271
+ diff: message.diff,
272
+ summary: message.summary,
273
+ kind: message.kind
274
+ }
275
+ );
136
276
  case "system":
137
277
  return null;
138
278
  }
@@ -140,47 +280,134 @@ function MessageView({ message }) {
140
280
  function flattenText(parts) {
141
281
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
142
282
  }
283
+ var DOT = "\u23FA";
143
284
  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 })
285
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginTop: 1, children: [
286
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "> " }),
287
+ /* @__PURE__ */ jsx3(Text3, { children: content })
147
288
  ] });
148
289
  }
149
290
  function AssistantMessage({ content }) {
150
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: content.map((part, i) => {
291
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginTop: 1, children: content.map((part, i) => {
151
292
  if (part.type === "text") {
152
- return /* @__PURE__ */ jsx2(AssistantTextPart, { text: part.text }, i);
293
+ return /* @__PURE__ */ jsx3(AssistantTextPart, { text: part.text }, i);
153
294
  }
154
295
  if (part.type === "tool_use") {
155
- return /* @__PURE__ */ jsx2(ToolCallLine, { name: part.name, args: part.args }, i);
296
+ if (part.name === "TodoWrite") {
297
+ return /* @__PURE__ */ jsx3(TodoList, { todos: extractTodos(part.args) }, i);
298
+ }
299
+ return /* @__PURE__ */ jsx3(ToolCallLine, { name: part.name, args: part.args }, i);
156
300
  }
157
301
  return null;
158
302
  }) });
159
303
  }
160
304
  function AssistantTextPart({ text }) {
161
305
  const rendered = useMemo(() => renderMarkdown(text), [text]);
162
- return /* @__PURE__ */ jsx2(Text2, { children: rendered });
306
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
307
+ /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
308
+ DOT,
309
+ " "
310
+ ] }),
311
+ /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx3(Text3, { children: rendered }) })
312
+ ] });
163
313
  }
164
314
  function ToolCallLine({ name, args }) {
165
315
  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: [
316
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
317
+ /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
318
+ DOT,
319
+ " "
320
+ ] }),
321
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: name }),
322
+ /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, minWidth: 0, children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, wrap: "truncate-end", children: [
170
323
  "(",
171
324
  argSummary,
172
325
  ")"
173
- ] })
326
+ ] }) })
174
327
  ] });
175
328
  }
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 })
329
+ function extractTodos(args) {
330
+ if (typeof args !== "object" || args === null) return [];
331
+ const todos = args.todos;
332
+ return Array.isArray(todos) ? todos : [];
333
+ }
334
+ function TodoList({ todos }) {
335
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
336
+ /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
337
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u2192 " }),
338
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: "Todos" })
339
+ ] }),
340
+ todos.map((todo, i) => /* @__PURE__ */ jsx3(TodoRow, { todo }, i))
182
341
  ] });
183
342
  }
343
+ function TodoRow({ todo }) {
344
+ const label = todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content;
345
+ switch (todo.status) {
346
+ case "completed":
347
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
348
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2612 " }),
349
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, strikethrough: true, children: label })
350
+ ] });
351
+ case "in_progress":
352
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
353
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u2610 " }),
354
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: label })
355
+ ] });
356
+ default:
357
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", marginLeft: 2, children: [
358
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2610 " }),
359
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label })
360
+ ] });
361
+ }
362
+ }
363
+ function ToolResultLine({
364
+ isError,
365
+ content,
366
+ diff,
367
+ summary,
368
+ kind
369
+ }) {
370
+ const fallback = (() => {
371
+ const preview = content.length > 200 ? content.slice(0, 200) + "\u2026" : content;
372
+ return preview.split("\n")[0];
373
+ })();
374
+ const headLine = summary ?? fallback;
375
+ const totalLines = content.split("\n").length;
376
+ const extra = totalLines > 1 ? ` (+${totalLines - 1} lines)` : "";
377
+ const effective = kind ?? (isError ? "error" : "success");
378
+ const dotColor = effective === "error" ? "red" : effective === "warn" ? "yellowBright" : "green";
379
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginLeft: 2, children: [
380
+ /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", children: [
381
+ /* @__PURE__ */ jsxs3(Text3, { color: dotColor, children: [
382
+ DOT,
383
+ " "
384
+ ] }),
385
+ /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, minWidth: 0, children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, wrap: "truncate-end", children: [
386
+ headLine,
387
+ extra
388
+ ] }) })
389
+ ] }),
390
+ diff && /* @__PURE__ */ jsx3(DiffBlock, { diff })
391
+ ] });
392
+ }
393
+ function DiffBlock({ diff }) {
394
+ const lines = diff.split("\n");
395
+ const start = lines.findIndex((l) => l.startsWith("@@"));
396
+ const rendered = start >= 0 ? lines.slice(start) : lines;
397
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: rendered.map((line, i) => {
398
+ let color;
399
+ let dim = false;
400
+ if (line.startsWith("+")) color = "green";
401
+ else if (line.startsWith("-")) color = "red";
402
+ else if (line.startsWith("@@")) {
403
+ color = "cyan";
404
+ dim = true;
405
+ } else {
406
+ dim = true;
407
+ }
408
+ return /* @__PURE__ */ jsx3(Text3, { color, dimColor: dim, children: line || " " }, i);
409
+ }) });
410
+ }
184
411
  function formatArgs(args) {
185
412
  if (typeof args !== "object" || args === null) return String(args);
186
413
  const entries = Object.entries(args);
@@ -198,34 +425,86 @@ function formatArgs(args) {
198
425
  }
199
426
 
200
427
  // 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";
428
+ import { useState as useState2 } from "react";
429
+ import { Box as Box3, Text as Text4, useInput as useInput2 } from "ink";
430
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
431
+ var OPTIONS = [
432
+ { decision: "yes", labelKey: "yes", shortcut: "y" },
433
+ { decision: "session_allow", labelKey: "session", shortcut: "s" },
434
+ { decision: "no", labelKey: "no", shortcut: "n" }
435
+ ];
203
436
  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);
437
+ const [index, setIndex] = useState2(0);
438
+ useInput2((input, key) => {
439
+ if (key.upArrow) {
440
+ setIndex((i) => (i - 1 + OPTIONS.length) % OPTIONS.length);
441
+ return;
442
+ }
443
+ if (key.downArrow) {
444
+ setIndex((i) => (i + 1) % OPTIONS.length);
445
+ return;
446
+ }
447
+ if (key.return) {
448
+ request.resolve(OPTIONS[index].decision);
449
+ return;
450
+ }
451
+ if (key.escape) {
452
+ request.resolve("no");
453
+ return;
454
+ }
455
+ const lower = input?.toLowerCase?.();
456
+ for (let i = 0; i < OPTIONS.length; i++) {
457
+ const o = OPTIONS[i];
458
+ if (lower === o.shortcut || input === String(i + 1)) {
459
+ request.resolve(o.decision);
460
+ return;
461
+ }
209
462
  }
210
463
  });
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: [
464
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
465
+ /* @__PURE__ */ jsxs4(Text4, { color: "yellow", bold: true, children: [
213
466
  "\u23F5 Approve ",
214
467
  request.toolName,
215
468
  "?"
216
469
  ] }),
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" }) })
470
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: request.summary }),
471
+ /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginTop: 1, children: OPTIONS.map((o, i) => {
472
+ const focused = i === index;
473
+ const label = labelFor(o.labelKey, request.toolName);
474
+ return /* @__PURE__ */ jsxs4(Text4, { color: focused ? "cyan" : void 0, bold: focused, children: [
475
+ focused ? "\u203A " : " ",
476
+ i + 1,
477
+ ". ",
478
+ label,
479
+ " ",
480
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
481
+ "(",
482
+ o.shortcut,
483
+ ")"
484
+ ] })
485
+ ] }, o.decision);
486
+ }) }),
487
+ /* @__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
488
  ] });
220
489
  }
490
+ function labelFor(key, toolName) {
491
+ switch (key) {
492
+ case "yes":
493
+ return "Yes";
494
+ case "session":
495
+ return `Yes, allow ${toolName} for the rest of this session`;
496
+ case "no":
497
+ return "No";
498
+ }
499
+ }
221
500
 
222
501
  // src/components/ModelSelector.tsx
223
- import { Box as Box5, Text as Text5 } from "ink";
502
+ import { Box as Box5, Text as Text6 } from "ink";
224
503
 
225
504
  // 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";
505
+ import { useState as useState3 } from "react";
506
+ import { Box as Box4, Text as Text5, useInput as useInput3 } from "ink";
507
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
229
508
  var POINTER_COLOR = "#A855F7";
230
509
  function Selector({
231
510
  items,
@@ -238,8 +517,8 @@ function Selector({
238
517
  onCancel
239
518
  }) {
240
519
  const safeInitial = Math.max(0, Math.min(initialIndex, items.length - 1));
241
- const [index, setIndex] = useState(safeInitial);
242
- useInput2((_, key) => {
520
+ const [index, setIndex] = useState3(safeInitial);
521
+ useInput3((_, key) => {
243
522
  if (key.upArrow) {
244
523
  setIndex((i) => Math.max(0, i - 1));
245
524
  } else if (key.downArrow) {
@@ -255,7 +534,7 @@ function Selector({
255
534
  const start = Math.max(0, Math.min(index - Math.floor(window / 2), len - window));
256
535
  const end = Math.min(len, start + window);
257
536
  const visible = items.slice(start, end);
258
- return /* @__PURE__ */ jsxs4(
537
+ return /* @__PURE__ */ jsxs5(
259
538
  Box4,
260
539
  {
261
540
  flexDirection: "column",
@@ -264,20 +543,20 @@ function Selector({
264
543
  borderStyle: "round",
265
544
  borderColor: "cyan",
266
545
  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 })
546
+ (title || hint) && /* @__PURE__ */ jsxs5(Box4, { marginBottom: 1, children: [
547
+ title && /* @__PURE__ */ jsx5(Text5, { bold: true, children: title }),
548
+ title && hint && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " " }),
549
+ hint && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: hint })
271
550
  ] }),
272
551
  visible.map((item, i) => {
273
552
  const realIndex = start + i;
274
553
  const focused = realIndex === index;
275
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
276
- /* @__PURE__ */ jsx4(Text4, { color: POINTER_COLOR, bold: true, children: focused ? "\u203A " : " " }),
554
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "row", children: [
555
+ /* @__PURE__ */ jsx5(Text5, { color: POINTER_COLOR, bold: true, children: focused ? "\u203A " : " " }),
277
556
  renderRow(item, focused)
278
557
  ] }, realIndex);
279
558
  }),
280
- window < len && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
559
+ window < len && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
281
560
  "(",
282
561
  start + 1,
283
562
  "-",
@@ -292,24 +571,24 @@ function Selector({
292
571
  }
293
572
 
294
573
  // src/components/ModelSelector.tsx
295
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
574
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
296
575
  function ModelSelector({ request }) {
297
- const { items, currentId, resolve: resolve5 } = request;
576
+ const { items, currentId, resolve: resolve6 } = request;
298
577
  const initialIndex = Math.max(
299
578
  0,
300
579
  items.findIndex((m) => m.id === currentId)
301
580
  );
302
581
  const labelWidth = Math.max(...items.map((m) => (m.name ?? m.id).length));
303
- return /* @__PURE__ */ jsx5(
582
+ return /* @__PURE__ */ jsx6(
304
583
  Selector,
305
584
  {
306
585
  items,
307
586
  initialIndex,
308
587
  title: "Select model",
309
588
  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 })
589
+ onSubmit: (m) => resolve6(m),
590
+ onCancel: () => resolve6(null),
591
+ renderRow: (m, _focused) => /* @__PURE__ */ jsx6(ModelRow, { model: m, active: m.id === currentId, labelWidth })
313
592
  }
314
593
  );
315
594
  }
@@ -322,17 +601,17 @@ function ModelRow({
322
601
  const label = (model.name ?? model.id).padEnd(labelWidth);
323
602
  const vendor = model.vendor ? `[${model.vendor}]` : "";
324
603
  const caps = formatCaps(model);
325
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", children: [
326
- /* @__PURE__ */ jsxs5(Text5, { color: active ? "green" : void 0, children: [
604
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "row", children: [
605
+ /* @__PURE__ */ jsxs6(Text6, { color: active ? "green" : void 0, children: [
327
606
  dot,
328
607
  " "
329
608
  ] }),
330
- /* @__PURE__ */ jsx5(Text5, { children: label }),
331
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
609
+ /* @__PURE__ */ jsx6(Text6, { children: label }),
610
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
332
611
  " ",
333
612
  vendor
334
613
  ] }),
335
- caps && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
614
+ caps && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
336
615
  " ",
337
616
  caps
338
617
  ] })
@@ -346,15 +625,15 @@ function formatCaps(m) {
346
625
  }
347
626
 
348
627
  // 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";
628
+ import { Box as Box6, Text as Text7 } from "ink";
629
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
351
630
  function SessionSelector({ request }) {
352
- const { items, currentId, resolve: resolve5 } = request;
631
+ const { items, currentId, resolve: resolve6 } = request;
353
632
  const initialIndex = Math.max(
354
633
  0,
355
634
  items.findIndex((s) => s.id === currentId)
356
635
  );
357
- return /* @__PURE__ */ jsx6(
636
+ return /* @__PURE__ */ jsx7(
358
637
  Selector,
359
638
  {
360
639
  items,
@@ -362,9 +641,9 @@ function SessionSelector({ request }) {
362
641
  maxVisible: 12,
363
642
  title: "Resume session",
364
643
  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 })
644
+ onSubmit: (s) => resolve6(s),
645
+ onCancel: () => resolve6(null),
646
+ renderRow: (s) => /* @__PURE__ */ jsx7(SessionRow, { session: s, active: s.id === currentId })
368
647
  }
369
648
  );
370
649
  }
@@ -373,18 +652,18 @@ function SessionRow({ session, active }) {
373
652
  const time = formatTime(session.createdAt);
374
653
  const count = `[${String(session.messageCount).padStart(2)} msgs]`;
375
654
  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: [
655
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "row", children: [
656
+ /* @__PURE__ */ jsx7(Text7, { color: active ? "green" : void 0, children: active ? "\u25CF " : " " }),
657
+ /* @__PURE__ */ jsx7(Text7, { children: id8 }),
658
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
380
659
  " ",
381
660
  time
382
661
  ] }),
383
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
662
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
384
663
  " ",
385
664
  count
386
665
  ] }),
387
- /* @__PURE__ */ jsxs6(Text6, { children: [
666
+ /* @__PURE__ */ jsxs7(Text7, { children: [
388
667
  " ",
389
668
  preview
390
669
  ] })
@@ -398,8 +677,8 @@ function formatTime(iso) {
398
677
  }
399
678
 
400
679
  // 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";
680
+ import { Box as Box7, Text as Text8 } from "ink";
681
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
403
682
  var DEFAULT_MAX = 10;
404
683
  var SLASH_COLOR = "#A855F7";
405
684
  function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
@@ -407,35 +686,34 @@ function SlashAutocomplete({ matches, index, maxVisible = DEFAULT_MAX }) {
407
686
  const start = Math.max(0, Math.min(index - Math.floor(maxVisible / 2), matches.length - maxVisible));
408
687
  const end = Math.min(matches.length, start + maxVisible);
409
688
  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: [
689
+ const nameWidth = Math.max(...matches.map((c) => c.name.length));
690
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginTop: 1, children: [
412
691
  visible.map((cmd, i) => {
413
692
  const realIndex = start + i;
414
- return /* @__PURE__ */ jsx7(Row, { cmd, focused: realIndex === index, nameWidth }, cmd.name);
693
+ return /* @__PURE__ */ jsx8(Row, { cmd, focused: realIndex === index, nameWidth }, cmd.name);
415
694
  }),
416
- matches.length > visible.length && /* @__PURE__ */ jsx7(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
695
+ matches.length > visible.length && /* @__PURE__ */ jsx8(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
417
696
  "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel (",
418
697
  matches.length - visible.length,
419
698
  " more)"
420
699
  ] }) }),
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" }) })
700
+ matches.length <= visible.length && /* @__PURE__ */ jsx8(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191\u2193 select \xB7 Tab/Enter accept \xB7 Esc cancel" }) })
422
701
  ] });
423
702
  }
424
703
  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: [
704
+ const padded = cmd.name.padEnd(nameWidth);
705
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", children: [
706
+ /* @__PURE__ */ jsxs8(Text8, { color: focused ? SLASH_COLOR : void 0, bold: focused, children: [
429
707
  "/",
430
708
  padded
431
709
  ] }),
432
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
433
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: cmd.description })
710
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
711
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: cmd.description })
434
712
  ] });
435
713
  }
436
714
 
437
715
  // src/components/PermissionModeBar.tsx
438
- import { Box as Box8, Text as Text8 } from "ink";
716
+ import { Box as Box8, Text as Text9 } from "ink";
439
717
 
440
718
  // src/permission/index.ts
441
719
  var MODE_CYCLE = [
@@ -459,6 +737,8 @@ var MODE_COLOR = {
459
737
  var PermissionGate = class {
460
738
  rules;
461
739
  mode = "default";
740
+ /** Session 级 allow:用户在 PermissionPrompt 选 "yes, for session" 后填充。 */
741
+ sessionAllow = /* @__PURE__ */ new Set();
462
742
  constructor(rules = {}) {
463
743
  this.rules = {
464
744
  allow: rules.allow ?? [],
@@ -478,8 +758,16 @@ var PermissionGate = class {
478
758
  this.mode = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
479
759
  return this.mode;
480
760
  }
761
+ /** 用户在 PermissionPrompt 选 "yes, allow for session" 时记下。 */
762
+ allowForSession(toolName) {
763
+ this.sessionAllow.add(toolName);
764
+ }
765
+ isSessionAllowed(toolName) {
766
+ return this.sessionAllow.has(toolName);
767
+ }
481
768
  decide(input) {
482
769
  if (this.matches(this.rules.deny, input)) return "deny";
770
+ if (this.sessionAllow.has(input.toolName)) return "allow";
483
771
  switch (this.mode) {
484
772
  case "bypassPermissions":
485
773
  return "allow";
@@ -533,7 +821,7 @@ var PermissionGate = class {
533
821
  };
534
822
 
535
823
  // src/components/PermissionModeBar.tsx
536
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
824
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
537
825
  function PermissionModeBar({ mode, compact }) {
538
826
  const color = MODE_COLOR[mode];
539
827
  const label = MODE_LABEL[mode];
@@ -545,19 +833,234 @@ function PermissionModeBar({ mode, compact }) {
545
833
  plan: "[plan]",
546
834
  bypassPermissions: "[bypass]"
547
835
  };
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" })
836
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "row", children: [
837
+ /* @__PURE__ */ jsx9(Text9, { color, bold: isBypass, children: short[mode] }),
838
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " shift+tab" })
551
839
  ] });
552
840
  }
553
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
554
- /* @__PURE__ */ jsxs8(Text8, { color, bold: isBypass, children: [
555
- "\u25B6\u25B6 ",
841
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "row", children: [
842
+ /* @__PURE__ */ jsxs9(Text9, { color, bold: isBypass, children: [
843
+ "\u25B8\u25B8 ",
556
844
  label
557
845
  ] }),
558
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " (shift+tab to cycle)" })
846
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (shift+tab to cycle)" })
847
+ ] });
848
+ }
849
+
850
+ // src/components/FooterStatus.tsx
851
+ import { Box as Box9, Text as Text10 } from "ink";
852
+ import { Fragment, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
853
+ var BAR_TOTAL_WIDE = 10;
854
+ var BAR_TOTAL_COMPACT = 6;
855
+ function FooterStatus({
856
+ sessionId,
857
+ model,
858
+ contextWindow,
859
+ lastInputTokens,
860
+ sessionInputTokens,
861
+ sessionOutputTokens,
862
+ termWidth
863
+ }) {
864
+ const sid = sessionId.slice(0, 8);
865
+ const hasCtx = contextWindow > 0;
866
+ const pct = hasCtx ? Math.min(100, Math.round(lastInputTokens / contextWindow * 100)) : 0;
867
+ const ctxColor = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
868
+ const SEP = /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " \u2502 " });
869
+ const SEP_DOT = /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " \xB7 " });
870
+ const renderBar = (barW) => {
871
+ const filled = Math.round(pct / 100 * barW);
872
+ const empty = barW - filled;
873
+ return /* @__PURE__ */ jsxs10(Fragment, { children: [
874
+ filled > 0 && /* @__PURE__ */ jsx10(Text10, { color: ctxColor, children: "\u2588".repeat(filled) }),
875
+ empty > 0 && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2591".repeat(empty) })
876
+ ] });
877
+ };
878
+ if (termWidth < 60) {
879
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
880
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: sid }),
881
+ SEP_DOT,
882
+ /* @__PURE__ */ jsx10(Text10, { color: "magenta", children: model }),
883
+ hasCtx && /* @__PURE__ */ jsxs10(Fragment, { children: [
884
+ SEP_DOT,
885
+ renderBar(BAR_TOTAL_COMPACT),
886
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${pct}%` })
887
+ ] })
888
+ ] });
889
+ }
890
+ if (termWidth < 100) {
891
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
892
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `@${sid}` }),
893
+ SEP,
894
+ /* @__PURE__ */ jsx10(Text10, { color: "magenta", children: model }),
895
+ hasCtx && /* @__PURE__ */ jsxs10(Fragment, { children: [
896
+ SEP,
897
+ /* @__PURE__ */ jsx10(Text10, { children: "ctx: " }),
898
+ renderBar(BAR_TOTAL_COMPACT),
899
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${pct}%` })
900
+ ] })
901
+ ] });
902
+ }
903
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "row", children: [
904
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `@${sid}` }),
905
+ SEP,
906
+ /* @__PURE__ */ jsx10(Text10, { color: "magenta", children: model }),
907
+ hasCtx && /* @__PURE__ */ jsxs10(Fragment, { children: [
908
+ SEP,
909
+ /* @__PURE__ */ jsx10(Text10, { children: "ctx: " }),
910
+ renderBar(BAR_TOTAL_WIDE),
911
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${pct}%` }),
912
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: ` ${formatTokens(lastInputTokens)}/${formatTokens(contextWindow)}` })
913
+ ] }),
914
+ SEP,
915
+ /* @__PURE__ */ jsx10(Text10, { children: "tok: " }),
916
+ /* @__PURE__ */ jsx10(Text10, { color: "green", children: `\u2191${formatTokens(sessionInputTokens)}` }),
917
+ /* @__PURE__ */ jsx10(Text10, { children: " " }),
918
+ /* @__PURE__ */ jsx10(Text10, { color: "blueBright", children: `\u2193${formatTokens(sessionOutputTokens)}` })
559
919
  ] });
560
920
  }
921
+ function formatTokens(n) {
922
+ if (n < 1e3) return String(n);
923
+ if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
924
+ if (n < 1e6) return Math.round(n / 1e3) + "k";
925
+ return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
926
+ }
927
+
928
+ // src/components/ProgressBanner.tsx
929
+ import { useEffect as useEffect2, useState as useState4 } from "react";
930
+ import { Box as Box10, Text as Text11 } from "ink";
931
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
932
+ var BAR_WIDTH = 42;
933
+ var TICK_MS = 400;
934
+ var TIP_ROTATE_SEC = 5;
935
+ function ProgressBanner({ state }) {
936
+ const [now, setNow] = useState4(Date.now());
937
+ useEffect2(() => {
938
+ const t = setInterval(() => setNow(Date.now()), TICK_MS);
939
+ return () => clearInterval(t);
940
+ }, []);
941
+ const elapsedSec = Math.max(0, Math.floor((now - state.startTime) / 1e3));
942
+ const percent = Math.max(0, Math.min(99, Math.floor(state.getPercent())));
943
+ const filled = Math.floor(percent / 100 * BAR_WIDTH);
944
+ const empty = BAR_WIDTH - filled;
945
+ const bar = "\u25B0".repeat(filled) + "\u25B1".repeat(empty);
946
+ const tip = state.tips.length ? state.tips[Math.floor(elapsedSec / TIP_ROTATE_SEC) % state.tips.length] : "";
947
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", marginTop: 1, children: [
948
+ /* @__PURE__ */ jsxs11(Box10, { children: [
949
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "\u2726 " }),
950
+ /* @__PURE__ */ jsxs11(Text11, { color: "cyan", children: [
951
+ state.title,
952
+ "..."
953
+ ] }),
954
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` (${elapsedSec}s)` })
955
+ ] }),
956
+ /* @__PURE__ */ jsxs11(Box10, { marginLeft: 2, children: [
957
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: bar }),
958
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` ${percent}%` })
959
+ ] }),
960
+ tip && /* @__PURE__ */ jsx11(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: `\u2514 Tip: ${tip}` }) })
961
+ ] });
962
+ }
963
+
964
+ // src/components/StatusLine.tsx
965
+ import { useEffect as useEffect4, useState as useState6 } from "react";
966
+ import { Box as Box11, Text as Text13 } from "ink";
967
+
968
+ // src/components/Shimmer.tsx
969
+ import { useEffect as useEffect3, useState as useState5 } from "react";
970
+ import { Text as Text12 } from "ink";
971
+ import { jsx as jsx12 } from "react/jsx-runtime";
972
+ var FRAME_MS = 100;
973
+ var TRAIL = 4;
974
+ function Shimmer({ text, bold = true }) {
975
+ const chars = Array.from(text);
976
+ const cycle = chars.length + TRAIL;
977
+ const [phase, setPhase] = useState5(0);
978
+ useEffect3(() => {
979
+ const id = setInterval(() => {
980
+ setPhase((p) => (p + 1) % cycle);
981
+ }, FRAME_MS);
982
+ return () => clearInterval(id);
983
+ }, [cycle]);
984
+ return /* @__PURE__ */ jsx12(Text12, { children: chars.map((ch, i) => {
985
+ const d = Math.abs(i - phase);
986
+ if (d === 0) {
987
+ return /* @__PURE__ */ jsx12(Text12, { color: "white", bold, children: ch }, i);
988
+ }
989
+ if (d === 1) {
990
+ return /* @__PURE__ */ jsx12(Text12, { color: "white", children: ch }, i);
991
+ }
992
+ return /* @__PURE__ */ jsx12(Text12, { color: "gray", dimColor: true, children: ch }, i);
993
+ }) });
994
+ }
995
+
996
+ // src/components/StatusLine.tsx
997
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
998
+ var TICK_MS2 = 400;
999
+ function StatusLine({ startTime, firstTextTime, inputTokens, runningTool, lang }) {
1000
+ const [now, setNow] = useState6(Date.now());
1001
+ useEffect4(() => {
1002
+ const t = setInterval(() => setNow(Date.now()), TICK_MS2);
1003
+ return () => clearInterval(t);
1004
+ }, []);
1005
+ const elapsedSec = Math.max(0, Math.floor((now - startTime) / 1e3));
1006
+ const mainLabel = lang === "zh-CN" ? "\u5DE5\u4F5C\u4E2D" : "Working";
1007
+ const parts = [formatDuration(elapsedSec)];
1008
+ if (inputTokens > 0) {
1009
+ parts.push(`\u2191 ${formatTokens2(inputTokens)} tokens`);
1010
+ }
1011
+ if (firstTextTime !== null) {
1012
+ const thinkSec = Math.max(0, Math.floor((firstTextTime - startTime) / 1e3));
1013
+ parts.push(
1014
+ lang === "zh-CN" ? `\u601D\u8003 ${formatDuration(thinkSec)}` : `thought for ${formatDuration(thinkSec)}`
1015
+ );
1016
+ }
1017
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", marginTop: 1, children: [
1018
+ /* @__PURE__ */ jsxs12(Box11, { flexDirection: "row", children: [
1019
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", children: "\u25CF " }),
1020
+ /* @__PURE__ */ jsx13(Shimmer, { text: mainLabel }),
1021
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: ` (${parts.join(" \xB7 ")})` })
1022
+ ] }),
1023
+ runningTool && /* @__PURE__ */ jsxs12(Box11, { flexDirection: "row", marginLeft: 2, marginTop: 0, children: [
1024
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "\u21B3 " }),
1025
+ /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: runningTool })
1026
+ ] })
1027
+ ] });
1028
+ }
1029
+ function formatTokens2(n) {
1030
+ if (n < 1e3) return String(n);
1031
+ if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
1032
+ if (n < 1e6) return Math.round(n / 1e3) + "k";
1033
+ return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
1034
+ }
1035
+ function formatDuration(sec) {
1036
+ if (sec < 60) return `${sec}s`;
1037
+ const m = Math.floor(sec / 60);
1038
+ const s = sec % 60;
1039
+ return s === 0 ? `${m}m` : `${m}m${s}s`;
1040
+ }
1041
+
1042
+ // src/ui/termTitle.ts
1043
+ var ENABLED = (() => {
1044
+ if (!process.stdout.isTTY) return false;
1045
+ if (process.env.MUSE_NO_TITLE === "1") return false;
1046
+ return true;
1047
+ })();
1048
+ var lastTitle = "";
1049
+ function sanitize(s) {
1050
+ return s.replace(/[\x00-\x1f\x7f]/g, "");
1051
+ }
1052
+ function setTerminalTitle(title) {
1053
+ if (!ENABLED) return;
1054
+ const clean = sanitize(title);
1055
+ if (clean === lastTitle) return;
1056
+ lastTitle = clean;
1057
+ process.stdout.write(`\x1B]0;${clean}\x07`);
1058
+ }
1059
+ function resetTerminalTitle() {
1060
+ if (!ENABLED) return;
1061
+ lastTitle = "";
1062
+ process.stdout.write(`\x1B]0;\x07`);
1063
+ }
561
1064
 
562
1065
  // src/llm/providers/openai-compatible.ts
563
1066
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
@@ -668,17 +1171,45 @@ var OpenAICompatibleClient = class {
668
1171
  const { messages, tools, systemPrompt, temperature, maxTokens, abortSignal } = opts;
669
1172
  const aiMessages = convertMessages(messages, systemPrompt);
670
1173
  const aiTools = tools ? convertTools(tools) : void 0;
1174
+ let attempt = 0;
1175
+ const maxAttempts = 3;
1176
+ let result;
1177
+ while (true) {
1178
+ try {
1179
+ result = streamText({
1180
+ model: this.modelProvider,
1181
+ messages: aiMessages,
1182
+ tools: aiTools,
1183
+ temperature,
1184
+ maxTokens,
1185
+ abortSignal
1186
+ });
1187
+ break;
1188
+ } catch (err) {
1189
+ if (abortSignal?.aborted) {
1190
+ yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
1191
+ return;
1192
+ }
1193
+ if (!isRetryable(err) || attempt >= maxAttempts - 1) {
1194
+ yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
1195
+ return;
1196
+ }
1197
+ const delay = 1e3 * Math.pow(2, attempt);
1198
+ log.warn(`LLM connect failed (attempt ${attempt + 1}/${maxAttempts}); retrying in ${delay}ms`, {
1199
+ msg: err instanceof Error ? err.message : String(err)
1200
+ });
1201
+ await sleep(delay, abortSignal);
1202
+ attempt += 1;
1203
+ }
1204
+ }
1205
+ if (!result) {
1206
+ yield { type: "error", error: new Error("Internal: stream result is undefined after retry loop.") };
1207
+ return;
1208
+ }
1209
+ const stream = result.fullStream;
671
1210
  try {
672
- const result = streamText({
673
- model: this.modelProvider,
674
- messages: aiMessages,
675
- tools: aiTools,
676
- temperature,
677
- maxTokens,
678
- abortSignal
679
- });
680
1211
  const seenToolCalls = /* @__PURE__ */ new Set();
681
- for await (const part of result.fullStream) {
1212
+ for await (const part of stream) {
682
1213
  switch (part.type) {
683
1214
  case "text-delta":
684
1215
  yield { type: "text", delta: part.textDelta };
@@ -784,6 +1315,33 @@ function convertTools(tools) {
784
1315
  }
785
1316
  return result;
786
1317
  }
1318
+ function isRetryable(err) {
1319
+ if (!(err instanceof Error)) return false;
1320
+ const msg = err.message.toLowerCase();
1321
+ const code = err.code ?? "";
1322
+ if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "ECONNREFUSED" || code === "EAI_AGAIN") {
1323
+ return true;
1324
+ }
1325
+ 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")) {
1326
+ return true;
1327
+ }
1328
+ return false;
1329
+ }
1330
+ async function sleep(ms, abortSignal) {
1331
+ await new Promise((resolve6, reject) => {
1332
+ if (abortSignal?.aborted) return reject(new Error("aborted"));
1333
+ const t = setTimeout(() => {
1334
+ abortSignal?.removeEventListener("abort", onAbort);
1335
+ resolve6();
1336
+ }, ms);
1337
+ const onAbort = () => {
1338
+ clearTimeout(t);
1339
+ abortSignal?.removeEventListener("abort", onAbort);
1340
+ reject(new Error("aborted"));
1341
+ };
1342
+ abortSignal?.addEventListener("abort", onAbort);
1343
+ });
1344
+ }
787
1345
  function mapFinishReason(reason) {
788
1346
  switch (reason) {
789
1347
  case "stop":
@@ -884,10 +1442,7 @@ function setActiveModelEnv(entry) {
884
1442
  function createLLMClientFromModelEntry(entry) {
885
1443
  const apiKey = process.env[ACTIVE_API_KEY_ENV] ?? "";
886
1444
  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
- );
1445
+ throw new MuseError(buildMissingKeyMessage(entry), "MISSING_API_KEY");
891
1446
  }
892
1447
  const capabilities = {};
893
1448
  if (entry.supportsToolCall !== void 0) capabilities.toolCalling = entry.supportsToolCall;
@@ -901,6 +1456,34 @@ function createLLMClientFromModelEntry(entry) {
901
1456
  capabilities
902
1457
  });
903
1458
  }
1459
+ function buildMissingKeyMessage(entry) {
1460
+ const envVars = (entry._apiKeyEnvVars ?? []).filter(
1461
+ (v) => !process.env[v]
1462
+ );
1463
+ const head = `Model "${entry.id}" needs an API key but none was found.`;
1464
+ if (envVars.length > 0) {
1465
+ const list = envVars.map((v) => `$${v}`).join(", ");
1466
+ const fixVar = envVars[0];
1467
+ return [
1468
+ head,
1469
+ ``,
1470
+ `Cause: ~/.muse/models.local.json sets apiKey to a placeholder referencing ${list},`,
1471
+ ` but the shell environment does not have ${envVars.length > 1 ? "those variables" : "that variable"} set.`,
1472
+ ``,
1473
+ `Fix (pick one):`,
1474
+ ` 1. Replace the \${${fixVar}} placeholder in ~/.muse/models.local.json with the literal key`,
1475
+ ` (recommended \u2014 the file is local-only and never enters git).`,
1476
+ ` 2. Export the variable in your shell:`,
1477
+ ` export ${fixVar}=<your-key>`
1478
+ ].join("\n");
1479
+ }
1480
+ return [
1481
+ head,
1482
+ ``,
1483
+ `Edit ~/.muse/models.local.json and set "apiKey" on the "${entry.id}" entry`,
1484
+ `(plain text is fine \u2014 the file stays local-only).`
1485
+ ].join("\n");
1486
+ }
904
1487
  function createLLMClient(opts) {
905
1488
  const { provider, model, providers } = opts;
906
1489
  const config = providers[provider];
@@ -933,6 +1516,32 @@ function createLLMClient(opts) {
933
1516
  );
934
1517
  }
935
1518
 
1519
+ // src/loop/todos.ts
1520
+ var TodoStore = class {
1521
+ items = [];
1522
+ list() {
1523
+ return this.items.slice();
1524
+ }
1525
+ set(items) {
1526
+ this.items = items.slice();
1527
+ }
1528
+ clear() {
1529
+ this.items = [];
1530
+ }
1531
+ /** 把当前清单格式化为 system prompt 段落;无任务时返回空串。 */
1532
+ toPromptSection() {
1533
+ if (this.items.length === 0) return "";
1534
+ const lines = this.items.map((t, i) => {
1535
+ const marker = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
1536
+ return ` ${i + 1}. ${marker} ${t.content}`;
1537
+ });
1538
+ return `# Current todos
1539
+ ${lines.join("\n")}
1540
+
1541
+ Update via TodoWrite as you make progress. Keep exactly one item in_progress at a time.`;
1542
+ }
1543
+ };
1544
+
936
1545
  // src/loop/agent.ts
937
1546
  var Agent = class {
938
1547
  constructor(ctx) {
@@ -940,6 +1549,7 @@ var Agent = class {
940
1549
  }
941
1550
  ctx;
942
1551
  messages = [];
1552
+ todos = new TodoStore();
943
1553
  getMessages() {
944
1554
  return this.messages;
945
1555
  }
@@ -956,10 +1566,14 @@ var Agent = class {
956
1566
  const tools = this.ctx.tools.toLLMDefinitions(
957
1567
  mode === "plan" ? (t) => t.permission === "read" : void 0
958
1568
  );
1569
+ const todoSection = this.todos.toPromptSection();
1570
+ const systemPrompt = todoSection ? `${this.ctx.systemPrompt}
1571
+
1572
+ ${todoSection}` : this.ctx.systemPrompt;
959
1573
  const stream = this.ctx.llm.stream({
960
1574
  messages: this.messages,
961
1575
  tools,
962
- systemPrompt: this.ctx.systemPrompt,
1576
+ systemPrompt,
963
1577
  abortSignal: this.ctx.abortSignal
964
1578
  });
965
1579
  const assistantParts = [];
@@ -1048,27 +1662,36 @@ var Agent = class {
1048
1662
  return;
1049
1663
  }
1050
1664
  if (decision === "ask") {
1051
- approved = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? false;
1052
- if (!approved) {
1665
+ const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
1666
+ if (userDecision === "no") {
1053
1667
  this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
1054
1668
  return;
1055
1669
  }
1670
+ if (userDecision === "session_allow") {
1671
+ this.ctx.permissions.allowForSession(call.name);
1672
+ }
1673
+ approved = true;
1056
1674
  }
1057
1675
  const toolCtx = {
1058
1676
  cwd: this.ctx.cwd,
1059
1677
  abortSignal: this.ctx.abortSignal,
1060
- askPermission: async () => true
1678
+ askPermission: async () => true,
1061
1679
  // 已在外层处理
1680
+ todos: this.todos
1062
1681
  };
1063
1682
  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);
1683
+ this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary, result.diff, result.kind);
1065
1684
  }
1066
- recordToolResult(id, name, content, isError, summary) {
1685
+ recordToolResult(id, name, content, isError, summary, diff, kind) {
1067
1686
  const toolMsg = {
1068
1687
  role: "tool",
1069
1688
  toolUseId: id,
1070
1689
  content,
1071
- isError
1690
+ isError,
1691
+ toolName: name,
1692
+ ...diff ? { diff } : {},
1693
+ ...summary ? { summary } : {},
1694
+ ...kind ? { kind } : {}
1072
1695
  };
1073
1696
  this.messages.push(toolMsg);
1074
1697
  this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
@@ -1079,7 +1702,7 @@ var Agent = class {
1079
1702
  // src/loop/system-prompt.ts
1080
1703
  import { homedir as homedir2 } from "os";
1081
1704
  function buildSystemPrompt(opts) {
1082
- const { cwd, model, provider, lang, toolNames } = opts;
1705
+ const { cwd, model, provider, lang, toolNames, memoryIndex } = opts;
1083
1706
  const home = homedir2();
1084
1707
  const displayCwd = cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
1085
1708
  const sections = [];
@@ -1104,18 +1727,113 @@ Prefer the dedicated tool over Bash when one fits (Read for file reading, Edit f
1104
1727
  - If a command may be destructive (rm -rf, force push, drop table, etc.), warn first and let the user run it manually.
1105
1728
  - When the user asks a question that does not need tools, just answer.`
1106
1729
  );
1730
+ if (toolNames.includes("TodoWrite")) {
1731
+ sections.push(
1732
+ `# Task management
1733
+ - For non-trivial, multi-step work, use TodoWrite to plan and track progress.
1734
+ - Keep exactly one task in_progress; mark a task completed immediately when done.
1735
+ - Skip it for trivial single-step requests.`
1736
+ );
1737
+ }
1107
1738
  if (lang === "zh-CN") {
1108
1739
  sections.push(`# Output language
1109
1740
  Reply in Chinese (\u7B80\u4F53\u4E2D\u6587) unless the user writes in English.`);
1110
1741
  }
1742
+ if (memoryIndex && memoryIndex.trim()) {
1743
+ sections.push(
1744
+ `# Memory (long-term)
1745
+ 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.
1746
+
1747
+ ` + memoryIndex
1748
+ );
1749
+ }
1111
1750
  return sections.join("\n\n");
1112
1751
  }
1113
1752
 
1114
- // src/config/loader.ts
1115
- import { readFile } from "fs/promises";
1753
+ // src/loop/memory.ts
1754
+ import { mkdir, readFile, writeFile } from "fs/promises";
1116
1755
  import { existsSync } from "fs";
1117
1756
  import { homedir as homedir3 } from "os";
1118
- import { join as join2, resolve } from "path";
1757
+ import { join as join2 } from "path";
1758
+ import { createHash } from "crypto";
1759
+ function projectHash(cwd) {
1760
+ return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
1761
+ }
1762
+ function memoryDir(cwd) {
1763
+ return join2(homedir3(), ".muse", "projects", projectHash(cwd), "memory");
1764
+ }
1765
+ function memoryIndexPath(cwd) {
1766
+ return join2(memoryDir(cwd), "MEMORY.md");
1767
+ }
1768
+ function memoryFilePath(cwd, name) {
1769
+ return join2(memoryDir(cwd), `${name}.md`);
1770
+ }
1771
+ async function loadMemoryIndex(cwd, maxLines = 200) {
1772
+ const path = memoryIndexPath(cwd);
1773
+ if (!existsSync(path)) return "";
1774
+ try {
1775
+ const raw = await readFile(path, "utf-8");
1776
+ const lines = raw.split("\n");
1777
+ if (lines.length <= maxLines) return raw.trim();
1778
+ return lines.slice(0, maxLines).join("\n").trim() + `
1779
+ ... [truncated; ${lines.length - maxLines} more lines]`;
1780
+ } catch {
1781
+ return "";
1782
+ }
1783
+ }
1784
+ async function readMemoryFile(cwd, name) {
1785
+ const path = memoryFilePath(cwd, name);
1786
+ if (!existsSync(path)) {
1787
+ throw new Error(`Memory "${name}" does not exist at ${path}.`);
1788
+ }
1789
+ return readFile(path, "utf-8");
1790
+ }
1791
+ async function writeMemory(cwd, opts) {
1792
+ const dir = memoryDir(cwd);
1793
+ await mkdir(dir, { recursive: true });
1794
+ const filePath = memoryFilePath(cwd, opts.name);
1795
+ const frontmatter = [
1796
+ "---",
1797
+ `name: ${opts.name}`,
1798
+ `description: ${opts.description.replace(/\n/g, " ").trim()}`,
1799
+ `metadata:`,
1800
+ ` type: ${opts.type}`,
1801
+ "---"
1802
+ ].join("\n");
1803
+ const content = `${frontmatter}
1804
+
1805
+ ${opts.body.trim()}
1806
+ `;
1807
+ await writeFile(filePath, content, "utf-8");
1808
+ const indexPath = memoryIndexPath(cwd);
1809
+ let index = "";
1810
+ if (existsSync(indexPath)) index = await readFile(indexPath, "utf-8");
1811
+ const lines = index ? index.split("\n") : [];
1812
+ const linePrefix = `- [${opts.name}](${opts.name}.md)`;
1813
+ const newLine = `${linePrefix} \u2014 ${opts.description.replace(/\n/g, " ").trim()}`;
1814
+ const existing = lines.findIndex((l) => l.startsWith(linePrefix));
1815
+ let indexUpdated = false;
1816
+ if (existing >= 0) {
1817
+ if (lines[existing] !== newLine) {
1818
+ lines[existing] = newLine;
1819
+ indexUpdated = true;
1820
+ }
1821
+ } else {
1822
+ lines.push(newLine);
1823
+ indexUpdated = true;
1824
+ }
1825
+ if (indexUpdated) {
1826
+ const out = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1827
+ await writeFile(indexPath, out, "utf-8");
1828
+ }
1829
+ return { filePath, indexUpdated };
1830
+ }
1831
+
1832
+ // src/config/loader.ts
1833
+ import { readFile as readFile2 } from "fs/promises";
1834
+ import { existsSync as existsSync2 } from "fs";
1835
+ import { homedir as homedir4 } from "os";
1836
+ import { join as join3, resolve } from "path";
1119
1837
 
1120
1838
  // src/config/types.ts
1121
1839
  import { z } from "zod";
@@ -1125,8 +1843,8 @@ var ProviderConfigSchema = z.object({
1125
1843
  extraHeaders: z.record(z.string()).optional()
1126
1844
  }).passthrough();
1127
1845
  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."),
1846
+ provider: z.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
1847
+ model: z.string().optional().describe("Active model id; should match an id in models.local.json."),
1130
1848
  temperature: z.number().min(0).max(2).optional(),
1131
1849
  maxTokens: z.number().int().positive().optional()
1132
1850
  });
@@ -1191,7 +1909,7 @@ var DEFAULTS = {
1191
1909
  ollama: { baseUrl: "http://localhost:11434/v1" }
1192
1910
  },
1193
1911
  permissions: {
1194
- allow: ["Read", "Grep", "Glob"],
1912
+ allow: ["Read", "Grep", "Glob", "TodoWrite"],
1195
1913
  ask: ["Write", "Edit", "Bash"],
1196
1914
  deny: [],
1197
1915
  defaultMode: "ask"
@@ -1202,9 +1920,9 @@ var DEFAULTS = {
1202
1920
  }
1203
1921
  };
1204
1922
  async function readJsonIfExists(path) {
1205
- if (!existsSync(path)) return void 0;
1923
+ if (!existsSync2(path)) return void 0;
1206
1924
  try {
1207
- const raw = await readFile(path, "utf-8");
1925
+ const raw = await readFile2(path, "utf-8");
1208
1926
  return JSON.parse(raw);
1209
1927
  } catch (err) {
1210
1928
  log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
@@ -1232,9 +1950,9 @@ async function loadSettings(cwd = process.cwd()) {
1232
1950
  const sources = ["<defaults>"];
1233
1951
  let merged = DEFAULTS;
1234
1952
  const candidates = [
1235
- join2(homedir3(), ".muse", "settings.json"),
1236
- join2(cwd, ".muse", "settings.json"),
1237
- join2(cwd, ".muse", "settings.local.json")
1953
+ join3(homedir4(), ".muse", "settings.json"),
1954
+ join3(cwd, ".muse", "settings.json"),
1955
+ join3(cwd, ".muse", "settings.local.json")
1238
1956
  ];
1239
1957
  for (const path of candidates) {
1240
1958
  const raw = await readJsonIfExists(path);
@@ -1261,10 +1979,10 @@ async function loadSettings(cwd = process.cwd()) {
1261
1979
  }
1262
1980
 
1263
1981
  // 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";
1982
+ import { readFile as readFile3 } from "fs/promises";
1983
+ import { existsSync as existsSync3 } from "fs";
1984
+ import { homedir as homedir5 } from "os";
1985
+ import { join as join4 } from "path";
1268
1986
  import { z as z2 } from "zod";
1269
1987
  var ModelEntryInputSchema = z2.object({
1270
1988
  id: z2.string().min(1),
@@ -1285,65 +2003,61 @@ var ModelsRegistryInputSchema = z2.object({
1285
2003
  /** 不填 = 全部 models 都进 selector;填了就是 selector 子集(按顺序)。 */
1286
2004
  availableModels: z2.array(z2.string()).optional()
1287
2005
  }).passthrough();
1288
- var CANDIDATES = () => [
1289
- join3(homedir4(), ".muse", "models.json"),
1290
- join3(homedir4(), ".muse", "models.local.json")
1291
- ];
2006
+ var MODELS_PATH = () => join4(homedir5(), ".muse", "models.local.json");
1292
2007
  async function loadModelsRegistry() {
1293
2008
  const sources = [];
1294
2009
  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);
2010
+ const path = MODELS_PATH();
2011
+ if (!existsSync3(path)) {
2012
+ return { registry: void 0, sources, errors };
2013
+ }
2014
+ let raw;
2015
+ try {
2016
+ raw = JSON.parse(await readFile3(path, "utf-8"));
2017
+ } catch (err) {
2018
+ const msg = `JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
2019
+ log.warn(`Failed to parse ${path}: ${msg}`);
2020
+ errors.push({ path, message: msg });
2021
+ return { registry: void 0, sources, errors };
2022
+ }
2023
+ const parsed = ModelsRegistryInputSchema.safeParse(raw);
2024
+ if (!parsed.success) {
2025
+ const msg = formatZodIssues2(parsed.error.issues);
2026
+ log.warn(`Invalid models registry at ${path}: ${msg}`);
2027
+ errors.push({ path, message: msg });
2028
+ return { registry: void 0, sources, errors };
1320
2029
  }
1321
- if (!merged) return { registry: void 0, sources, errors };
1322
- const expanded = expandEnvVars(merged);
2030
+ const normalized = {
2031
+ ...parsed.data,
2032
+ models: parsed.data.models.map(normalizeModelEntry)
2033
+ };
2034
+ sources.push(path);
2035
+ const expanded = expandEnvVars(normalized);
1323
2036
  return { registry: expanded, sources, errors };
1324
2037
  }
1325
2038
  function formatZodIssues2(issues) {
1326
2039
  return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
1327
2040
  }
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
2041
  function normalizeModelEntry(entry) {
1341
2042
  let baseUrl = (entry.baseUrl ?? entry.url ?? "").replace(/\/+$/, "");
1342
2043
  if (baseUrl.endsWith("/chat/completions")) {
1343
2044
  baseUrl = baseUrl.slice(0, -"/chat/completions".length);
1344
2045
  }
1345
2046
  const { url: _url, ...rest } = entry;
1346
- return { ...rest, baseUrl };
2047
+ const apiKeyEnvVars = entry.apiKey ? extractEnvVars(entry.apiKey) : [];
2048
+ return {
2049
+ ...rest,
2050
+ baseUrl,
2051
+ ...apiKeyEnvVars.length > 0 ? { _apiKeyEnvVars: apiKeyEnvVars } : {}
2052
+ };
2053
+ }
2054
+ var ENV_PLACEHOLDER = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
2055
+ function extractEnvVars(s) {
2056
+ const out = [];
2057
+ let m;
2058
+ ENV_PLACEHOLDER.lastIndex = 0;
2059
+ while ((m = ENV_PLACEHOLDER.exec(s)) !== null) out.push(m[1]);
2060
+ return out;
1347
2061
  }
1348
2062
  function findEntry(registry, modelId) {
1349
2063
  return registry.models.find((m) => m.id === modelId);
@@ -1452,7 +2166,7 @@ async function compactMessages(messages, opts) {
1452
2166
  }
1453
2167
  const older = messages.slice(0, cutoff);
1454
2168
  const recent = messages.slice(cutoff);
1455
- const summary = await summarizeConversation(older, opts.llm, opts.abortSignal);
2169
+ const summary = await summarizeConversation(older, opts.llm, opts.abortSignal, opts.onProgress);
1456
2170
  const summaryMessage = {
1457
2171
  role: "user",
1458
2172
  content: `[Previous conversation summary]
@@ -1497,7 +2211,7 @@ function hasUnresolvedToolUse(older) {
1497
2211
  }
1498
2212
  return false;
1499
2213
  }
1500
- async function summarizeConversation(older, llm, abortSignal) {
2214
+ async function summarizeConversation(older, llm, abortSignal, onProgress) {
1501
2215
  const transcript = renderTranscript(older);
1502
2216
  const prompt = [
1503
2217
  {
@@ -1517,8 +2231,10 @@ ${transcript}
1517
2231
  ];
1518
2232
  let text = "";
1519
2233
  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;
2234
+ if (ev.type === "text") {
2235
+ text += ev.delta;
2236
+ onProgress?.(text.length);
2237
+ } else if (ev.type === "error") throw ev.error;
1522
2238
  }
1523
2239
  return text.trim() || "(empty summary)";
1524
2240
  }
@@ -1579,16 +2295,16 @@ function getMCPStatus(settings) {
1579
2295
  }
1580
2296
 
1581
2297
  // 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);
2298
+ import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4, stat } from "fs/promises";
2299
+ import { existsSync as existsSync4 } from "fs";
2300
+ import { homedir as homedir6 } from "os";
2301
+ import { dirname as dirname2, join as join5 } from "path";
2302
+ import { createHash as createHash2, randomUUID } from "crypto";
2303
+ function projectHash2(cwd) {
2304
+ return createHash2("sha256").update(cwd).digest("hex").slice(0, 16);
1589
2305
  }
1590
2306
  function sessionsDir(cwd) {
1591
- return join4(homedir5(), ".muse", "projects", projectHash(cwd), "sessions");
2307
+ return join5(homedir6(), ".muse", "projects", projectHash2(cwd), "sessions");
1592
2308
  }
1593
2309
  var Session = class _Session {
1594
2310
  meta;
@@ -1599,8 +2315,8 @@ var Session = class _Session {
1599
2315
  static async create(cwd) {
1600
2316
  const id = randomUUID();
1601
2317
  const dir = sessionsDir(cwd);
1602
- await mkdir(dir, { recursive: true });
1603
- const path = join4(dir, `${id}.jsonl`);
2318
+ await mkdir2(dir, { recursive: true });
2319
+ const path = join5(dir, `${id}.jsonl`);
1604
2320
  const meta = {
1605
2321
  id,
1606
2322
  cwd,
@@ -1616,7 +2332,7 @@ var Session = class _Session {
1616
2332
  }
1617
2333
  static async resolve(cwd, idOrPrefix) {
1618
2334
  const dir = sessionsDir(cwd);
1619
- if (!existsSync3(dir)) return void 0;
2335
+ if (!existsSync4(dir)) return void 0;
1620
2336
  const entries = await readdir(dir);
1621
2337
  const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
1622
2338
  if (matches.length === 0) return void 0;
@@ -1624,12 +2340,12 @@ var Session = class _Session {
1624
2340
  throw new Error(`Ambiguous session id "${idOrPrefix}" matches ${matches.length} sessions; use more characters.`);
1625
2341
  }
1626
2342
  const top = matches[0];
1627
- const st = await stat(join4(dir, top));
2343
+ const st = await stat(join5(dir, top));
1628
2344
  return {
1629
2345
  id: top.replace(/\.jsonl$/, ""),
1630
2346
  cwd,
1631
2347
  createdAt: st.mtime.toISOString(),
1632
- path: join4(dir, top)
2348
+ path: join5(dir, top)
1633
2349
  };
1634
2350
  }
1635
2351
  /**
@@ -1638,13 +2354,13 @@ var Session = class _Session {
1638
2354
  */
1639
2355
  static async listAll(cwd, limit) {
1640
2356
  const dir = sessionsDir(cwd);
1641
- if (!existsSync3(dir)) return [];
2357
+ if (!existsSync4(dir)) return [];
1642
2358
  const entries = await readdir(dir);
1643
2359
  const files = entries.filter((e) => e.endsWith(".jsonl"));
1644
2360
  if (files.length === 0) return [];
1645
2361
  const stats = await Promise.all(
1646
2362
  files.map(async (f) => {
1647
- const path = join4(dir, f);
2363
+ const path = join5(dir, f);
1648
2364
  const st = await stat(path);
1649
2365
  return { file: f, path, mtime: st.mtime };
1650
2366
  })
@@ -1681,7 +2397,7 @@ var Session = class _Session {
1681
2397
  const line = JSON.stringify(event) + "\n";
1682
2398
  this.writeQueue = this.writeQueue.then(async () => {
1683
2399
  try {
1684
- await mkdir(dirname2(this.meta.path), { recursive: true });
2400
+ await mkdir2(dirname2(this.meta.path), { recursive: true });
1685
2401
  await appendFile(this.meta.path, line, "utf-8");
1686
2402
  } catch (err) {
1687
2403
  log.warn(`session append failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -1690,8 +2406,8 @@ var Session = class _Session {
1690
2406
  return this.writeQueue;
1691
2407
  }
1692
2408
  async readAll() {
1693
- if (!existsSync3(this.meta.path)) return [];
1694
- const raw = await readFile3(this.meta.path, "utf-8");
2409
+ if (!existsSync4(this.meta.path)) return [];
2410
+ const raw = await readFile4(this.meta.path, "utf-8");
1695
2411
  const events = [];
1696
2412
  for (const line of raw.split("\n")) {
1697
2413
  if (!line.trim()) continue;
@@ -1706,7 +2422,7 @@ var Session = class _Session {
1706
2422
  async function readSummary(meta) {
1707
2423
  let events = [];
1708
2424
  try {
1709
- const raw = await readFile3(meta.path, "utf-8");
2425
+ const raw = await readFile4(meta.path, "utf-8");
1710
2426
  for (const line of raw.split("\n")) {
1711
2427
  if (!line.trim()) continue;
1712
2428
  try {
@@ -1728,9 +2444,9 @@ async function readSummary(meta) {
1728
2444
  }
1729
2445
 
1730
2446
  // src/slash/_format.ts
1731
- import { homedir as homedir6 } from "os";
2447
+ import { homedir as homedir7 } from "os";
1732
2448
  function shortPath(p) {
1733
- const home = homedir6();
2449
+ const home = homedir7();
1734
2450
  if (p === home) return "~";
1735
2451
  if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
1736
2452
  return p;
@@ -1828,6 +2544,18 @@ var COST = {
1828
2544
  return { display: lines.join("\n") };
1829
2545
  }
1830
2546
  };
2547
+ var COMPACT_TIPS = [
2548
+ "Shift+Tab cycles permission modes (default / acceptEdits / plan / bypass)",
2549
+ "/mode plan drafts changes without executing them",
2550
+ "/cost shows token usage and estimated spend",
2551
+ "/resume picks up a previous session in this directory",
2552
+ "muse --continue resumes the last session on startup",
2553
+ "MemoryWrite saves persistent knowledge across sessions",
2554
+ "TodoWrite keeps the model honest on multi-step tasks",
2555
+ 'Pipe to muse: cat bug.log | muse "explain this"',
2556
+ "Ctrl+C exits immediately; Esc rejects a pending tool"
2557
+ ];
2558
+ var COMPACT_ESTIMATED_CHARS = 1800;
1831
2559
  var COMPACT = {
1832
2560
  name: "compact",
1833
2561
  description: "summarize older messages to free up context space",
@@ -1837,23 +2565,39 @@ var COMPACT = {
1837
2565
  const { flags } = parseArgs(ctx.args);
1838
2566
  const keepRecent = typeof flags.keep === "string" ? Math.max(1, parseInt(flags.keep, 10)) : 4;
1839
2567
  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}).
2568
+ const progressRef = { chars: 0 };
2569
+ ctx.actions.showProgress({
2570
+ title: "Compacting conversation",
2571
+ tips: COMPACT_TIPS,
2572
+ getPercent: () => progressRef.chars / COMPACT_ESTIMATED_CHARS * 100
2573
+ });
2574
+ try {
2575
+ const result = await compactMessages(ctx.history, {
2576
+ llm: ctx.llm,
2577
+ keepRecent,
2578
+ onProgress: (chars) => {
2579
+ progressRef.chars = chars;
2580
+ }
2581
+ });
2582
+ if (result.noop) {
2583
+ return { display: `(history has ${result.originalCount} messages; not enough to compact with --keep ${keepRecent})` };
2584
+ }
2585
+ ctx.actions.setMessages(result.newMessages);
2586
+ const preview = result.summary.length > 240 ? result.summary.slice(0, 240) + "\u2026" : result.summary;
2587
+ return {
2588
+ display: `Compacted ${result.originalCount} \u2192 ${result.newCount} messages (kept last ${keepRecent}).
1848
2589
 
1849
2590
  Summary:
1850
2591
  ${preview}`
1851
- };
2592
+ };
2593
+ } finally {
2594
+ ctx.actions.hideProgress();
2595
+ }
1852
2596
  }
1853
2597
  };
1854
2598
  var MODELS = {
1855
2599
  name: "models",
1856
- description: "pick a model from ~/.muse/models.json (\u2191\u2193 to navigate)",
2600
+ description: "pick a model from ~/.muse/models.local.json (\u2191\u2193 to navigate)",
1857
2601
  async execute(ctx) {
1858
2602
  let registry = ctx.modelsRegistry;
1859
2603
  let errors = [];
@@ -1871,7 +2615,7 @@ var MODELS = {
1871
2615
  const visible = visibleEntries(registry);
1872
2616
  if (visible.length === 0) {
1873
2617
  return {
1874
- display: `models.json has no available models.
2618
+ display: `models.local.json has no available models.
1875
2619
  Check that "availableModels" lists at least one id present in "models".`
1876
2620
  };
1877
2621
  }
@@ -1889,7 +2633,7 @@ Check that "availableModels" lists at least one id present in "models".`
1889
2633
  };
1890
2634
  function renderLoadErrors(errors) {
1891
2635
  return [
1892
- `models.json was found but failed to load:`,
2636
+ `models.local.json was found but failed to load:`,
1893
2637
  ``,
1894
2638
  ...errors.flatMap((e) => [` ${shortPath(e.path)}`, ` ${e.message}`]),
1895
2639
  ``,
@@ -1900,7 +2644,7 @@ function renderLoadErrors(errors) {
1900
2644
  function renderEmptyRegistryHint() {
1901
2645
  return [
1902
2646
  `No models registry found.`,
1903
- `Create ~/.muse/models.json with a "models" array. Example:`,
2647
+ `Create ~/.muse/models.local.json with a "models" array. Example:`,
1904
2648
  ``,
1905
2649
  `{`,
1906
2650
  ` "models": [`,
@@ -2038,6 +2782,44 @@ async function loadAndReport(meta, ctx) {
2038
2782
  display: `Resumed session ${meta.id.slice(0, 8)} (${messages.length} messages from ${formatTime2(meta.createdAt)}).`
2039
2783
  };
2040
2784
  }
2785
+ var MODE_ALIASES = {
2786
+ default: "default",
2787
+ normal: "default",
2788
+ acceptedits: "acceptEdits",
2789
+ "accept-edits": "acceptEdits",
2790
+ accept: "acceptEdits",
2791
+ edits: "acceptEdits",
2792
+ plan: "plan",
2793
+ bypass: "bypassPermissions",
2794
+ bypasspermissions: "bypassPermissions"
2795
+ };
2796
+ var MODE_CMD = {
2797
+ name: "mode",
2798
+ description: "show or switch the permission mode (alternative to Shift+Tab)",
2799
+ argsHint: "[default|acceptEdits|plan|bypassPermissions]",
2800
+ execute(ctx) {
2801
+ const arg = ctx.args.trim().toLowerCase();
2802
+ if (!arg) {
2803
+ const cur = ctx.actions.getMode();
2804
+ const lines = [`Current permission mode: ${cur} \u2014 ${MODE_LABEL[cur]}`, ``, `Available modes:`];
2805
+ for (const m of MODE_CYCLE) {
2806
+ const marker = m === cur ? "\u25CF" : " ";
2807
+ lines.push(` ${marker} ${m.padEnd(20)} ${MODE_LABEL[m]}`);
2808
+ }
2809
+ lines.push(``, `Switch: /mode <name> or Shift+Tab to cycle`);
2810
+ return { display: lines.join("\n") };
2811
+ }
2812
+ const target = MODE_ALIASES[arg];
2813
+ if (!target) {
2814
+ return {
2815
+ display: `Unknown mode "${ctx.args.trim()}". Valid: ${MODE_CYCLE.join(" | ")}`
2816
+ };
2817
+ }
2818
+ if (target === ctx.actions.getMode()) return { display: `Already in ${target} mode.` };
2819
+ ctx.actions.setMode(target);
2820
+ return { display: `Switched to ${target} \u2014 ${MODE_LABEL[target]}` };
2821
+ }
2822
+ };
2041
2823
  var BUILTIN_SLASH_COMMANDS = [
2042
2824
  HELP,
2043
2825
  CLEAR,
@@ -2045,31 +2827,53 @@ var BUILTIN_SLASH_COMMANDS = [
2045
2827
  MODELS,
2046
2828
  CONFIG,
2047
2829
  MCP,
2830
+ MODE_CMD,
2048
2831
  COST,
2049
2832
  RESUME,
2050
2833
  QUIT
2051
2834
  ];
2052
2835
 
2053
2836
  // src/app.tsx
2054
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2837
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2055
2838
  function reducer(state, action) {
2056
2839
  switch (action.type) {
2057
2840
  case "user_submit":
2058
- return { ...state, streamingText: "", status: "streaming" };
2841
+ return {
2842
+ ...state,
2843
+ streamingText: "",
2844
+ status: "streaming",
2845
+ runningTool: null,
2846
+ turnStartTime: Date.now(),
2847
+ turnFirstTextTime: null,
2848
+ turnInputTokens: 0
2849
+ };
2059
2850
  case "history_set":
2060
2851
  return { ...state, history: action.messages };
2061
2852
  case "stream_delta":
2062
- return { ...state, streamingText: state.streamingText + action.delta };
2853
+ return {
2854
+ ...state,
2855
+ streamingText: state.streamingText + action.delta,
2856
+ status: state.status === "tool" ? "streaming" : state.status,
2857
+ runningTool: null,
2858
+ turnFirstTextTime: state.turnFirstTextTime ?? Date.now()
2859
+ };
2063
2860
  case "stream_reset":
2064
2861
  return { ...state, streamingText: "" };
2065
2862
  case "set_status":
2066
- return { ...state, status: action.status };
2863
+ return {
2864
+ ...state,
2865
+ status: action.status,
2866
+ runningTool: action.status === "tool" ? state.runningTool : null
2867
+ };
2868
+ case "tool_start":
2869
+ return { ...state, status: "tool", runningTool: action.name };
2067
2870
  case "add_usage":
2068
2871
  return {
2069
2872
  ...state,
2070
2873
  inputTokens: state.inputTokens + action.usage.inputTokens,
2071
2874
  outputTokens: state.outputTokens + action.usage.outputTokens,
2072
- totalTokens: state.totalTokens + action.usage.totalTokens
2875
+ totalTokens: state.totalTokens + action.usage.totalTokens,
2876
+ turnInputTokens: state.turnInputTokens + action.usage.inputTokens
2073
2877
  };
2074
2878
  }
2075
2879
  }
@@ -2089,32 +2893,52 @@ function App({
2089
2893
  const { exit } = useApp();
2090
2894
  const { stdout } = useStdout();
2091
2895
  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());
2896
+ const [llm, setLLM] = useState7(initialLLM);
2897
+ const [permissions, setPermissions] = useState7(initialPermissions);
2898
+ const [settings, setSettings] = useState7(initialSettings);
2899
+ const [settingsSources, setSettingsSources] = useState7(initialSources);
2900
+ const [modelsRegistry, setModelsRegistry] = useState7(initialModelsRegistry);
2901
+ const [mode, setMode] = useState7(initialPermissions.getMode());
2098
2902
  const [state, dispatch] = useReducer(reducer, {
2099
2903
  history: initialMessages ?? [],
2100
2904
  streamingText: "",
2101
2905
  status: "idle",
2906
+ runningTool: null,
2102
2907
  inputTokens: 0,
2103
2908
  outputTokens: 0,
2104
- totalTokens: 0
2909
+ totalTokens: 0,
2910
+ turnStartTime: 0,
2911
+ turnFirstTextTime: null,
2912
+ turnInputTokens: 0
2105
2913
  });
2106
2914
  const messagesRef = useRef(initialMessages ?? []);
2107
- const [input, setInput] = useState2("");
2108
- const [inputRemountKey, setInputRemountKey] = useState2(0);
2915
+ const [input, setInput] = useState7("");
2916
+ const [inputRemountKey, setInputRemountKey] = useState7(0);
2109
2917
  const commitInput = (value) => {
2110
2918
  setInput(value);
2111
2919
  setInputRemountKey((k) => k + 1);
2112
2920
  };
2113
- const [pending, setPending] = useState2(null);
2114
- const [picker, setPicker] = useState2(null);
2115
- const [sessionPicker, setSessionPicker] = useState2(null);
2116
- const [autocompleteIndex, setAutocompleteIndex] = useState2(0);
2921
+ const [pending, setPending] = useState7(null);
2922
+ const [picker, setPicker] = useState7(null);
2923
+ const [sessionPicker, setSessionPicker] = useState7(null);
2924
+ const [autocompleteIndex, setAutocompleteIndex] = useState7(0);
2925
+ const [progress, setProgress] = useState7(null);
2117
2926
  const agentRef = useRef(null);
2927
+ const queuedInputsRef = useRef([]);
2928
+ const [queuedInputs, setQueuedInputs] = useState7([]);
2929
+ const enqueueInput = (text) => {
2930
+ queuedInputsRef.current.push(text);
2931
+ setQueuedInputs([...queuedInputsRef.current]);
2932
+ };
2933
+ const dequeueInput = () => {
2934
+ if (queuedInputsRef.current.length === 0) return null;
2935
+ const front = queuedInputsRef.current.shift();
2936
+ setQueuedInputs([...queuedInputsRef.current]);
2937
+ return front;
2938
+ };
2939
+ const inputHistoryRef = useRef(extractUserInputs(initialMessages ?? []));
2940
+ const historyIndexRef = useRef(-1);
2941
+ const savedDraftRef = useRef("");
2118
2942
  const slash = useMemo2(() => {
2119
2943
  const r = new SlashRegistry();
2120
2944
  r.registerAll(BUILTIN_SLASH_COMMANDS);
@@ -2131,17 +2955,48 @@ function App({
2131
2955
  ) : all;
2132
2956
  return { matches, query };
2133
2957
  }, [input, slash]);
2134
- useEffect(() => {
2958
+ useEffect5(() => {
2135
2959
  const len = autocomplete?.matches.length ?? 0;
2136
2960
  if (autocompleteIndex >= len) setAutocompleteIndex(0);
2137
2961
  }, [autocomplete, autocompleteIndex]);
2138
- useEffect(() => {
2962
+ useEffect5(() => {
2963
+ const project = basename(cwd) || "muse";
2964
+ const baseIdle = `muse \xB7 ${project}`;
2965
+ if (state.status === "idle") {
2966
+ setTerminalTitle(baseIdle);
2967
+ return;
2968
+ }
2969
+ const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2970
+ let i = 0;
2971
+ const id = setInterval(() => {
2972
+ const frame = FRAMES[i % FRAMES.length];
2973
+ const tail = state.runningTool ? ` \xB7 ${state.runningTool}` : "";
2974
+ setTerminalTitle(`${frame} muse \xB7 ${project}${tail}`);
2975
+ i++;
2976
+ }, 100);
2977
+ return () => clearInterval(id);
2978
+ }, [state.status, state.runningTool, cwd]);
2979
+ useEffect5(() => {
2980
+ return () => resetTerminalTitle();
2981
+ }, []);
2982
+ const [memoryIndex, setMemoryIndex] = useState7("");
2983
+ useEffect5(() => {
2984
+ let cancelled = false;
2985
+ loadMemoryIndex(cwd).then((idx) => {
2986
+ if (!cancelled) setMemoryIndex(idx);
2987
+ });
2988
+ return () => {
2989
+ cancelled = true;
2990
+ };
2991
+ }, [cwd]);
2992
+ useEffect5(() => {
2139
2993
  const systemPrompt = buildSystemPrompt({
2140
2994
  cwd,
2141
2995
  model: llm.model,
2142
2996
  provider: llm.providerName,
2143
2997
  lang,
2144
- toolNames: tools.list().map((t) => t.name)
2998
+ toolNames: tools.list().map((t) => t.name),
2999
+ memoryIndex
2145
3000
  });
2146
3001
  const agent = new Agent({
2147
3002
  llm,
@@ -2152,7 +3007,8 @@ function App({
2152
3007
  systemPrompt,
2153
3008
  events: {
2154
3009
  onText: (delta) => dispatch({ type: "stream_delta", delta }),
2155
- onToolCallStart: () => dispatch({ type: "set_status", status: "tool" }),
3010
+ onToolCallStart: (_id, name) => dispatch({ type: "tool_start", name }),
3011
+ onToolResult: () => dispatch({ type: "set_status", status: "streaming" }),
2156
3012
  onUsage: (usage) => dispatch({ type: "add_usage", usage }),
2157
3013
  onTurnEnd: () => {
2158
3014
  const msgs = [...agent.getMessages()];
@@ -2160,6 +3016,19 @@ function App({
2160
3016
  dispatch({ type: "history_set", messages: msgs });
2161
3017
  dispatch({ type: "stream_reset" });
2162
3018
  dispatch({ type: "set_status", status: "idle" });
3019
+ const next = dequeueInput();
3020
+ if (next) {
3021
+ setTimeout(() => {
3022
+ dispatch({ type: "user_submit" });
3023
+ agent.runTurn(next).catch((err) => {
3024
+ const m = err instanceof Error ? err.message : String(err);
3025
+ dispatch({ type: "stream_delta", delta: `
3026
+ [error] ${m}
3027
+ ` });
3028
+ dispatch({ type: "set_status", status: "idle" });
3029
+ });
3030
+ }, 0);
3031
+ }
2163
3032
  },
2164
3033
  onError: (err) => {
2165
3034
  dispatch({ type: "stream_delta", delta: `
@@ -2167,15 +3036,15 @@ function App({
2167
3036
  ` });
2168
3037
  dispatch({ type: "set_status", status: "idle" });
2169
3038
  },
2170
- onPermissionRequest: (toolName, args, summary) => new Promise((resolve5) => {
2171
- setPending({ toolName, args, summary, resolve: resolve5 });
3039
+ onPermissionRequest: (toolName, args, summary) => new Promise((resolve6) => {
3040
+ setPending({ toolName, args, summary, resolve: resolve6 });
2172
3041
  })
2173
3042
  }
2174
3043
  });
2175
3044
  agent.setMessages(messagesRef.current);
2176
3045
  agentRef.current = agent;
2177
- }, [llm, tools, permissions, session, cwd, lang]);
2178
- useInput3(
3046
+ }, [llm, tools, permissions, session, cwd, lang, memoryIndex]);
3047
+ useInput4(
2179
3048
  (inputKey, key) => {
2180
3049
  if (key.ctrl && inputKey === "c") {
2181
3050
  exit();
@@ -2186,22 +3055,41 @@ function App({
2186
3055
  setMode(next);
2187
3056
  return;
2188
3057
  }
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);
3058
+ if (autocomplete && autocomplete.matches.length > 0) {
3059
+ const len = autocomplete.matches.length;
3060
+ if (key.upArrow) {
3061
+ setAutocompleteIndex((i) => (i - 1 + len) % len);
3062
+ } else if (key.downArrow) {
3063
+ setAutocompleteIndex((i) => (i + 1) % len);
3064
+ } else if (key.tab) {
3065
+ const picked = autocomplete.matches[autocompleteIndex];
3066
+ if (picked) commitInput(`/${picked.name}`);
3067
+ } else if (key.escape) {
3068
+ commitInput("");
3069
+ }
3070
+ return;
3071
+ }
3072
+ const hist = inputHistoryRef.current;
3073
+ if (key.upArrow && hist.length > 0) {
3074
+ const cur = historyIndexRef.current;
3075
+ if (cur === -1) savedDraftRef.current = input;
3076
+ const next = Math.min(cur + 1, hist.length - 1);
3077
+ historyIndexRef.current = next;
3078
+ commitInput(hist[hist.length - 1 - next] ?? "");
2193
3079
  } 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("");
3080
+ const cur = historyIndexRef.current;
3081
+ if (cur === -1) return;
3082
+ const next = cur - 1;
3083
+ historyIndexRef.current = next;
3084
+ if (next === -1) commitInput(savedDraftRef.current);
3085
+ else commitInput(hist[hist.length - 1 - next] ?? "");
2200
3086
  }
2201
3087
  },
2202
- { isActive: state.status === "idle" && !pending && !picker && !sessionPicker }
3088
+ // 模型在跑时也要响应键盘(让用户能 Ctrl+C / Shift+Tab / autocomplete 导航);
3089
+ // 仅模态弹起时让出键盘所有权
3090
+ { isActive: !pending && !picker && !sessionPicker }
2203
3091
  );
2204
- const acceptingInput = state.status === "idle" && pending === null && picker === null && sessionPicker === null;
3092
+ const acceptingInput = pending === null && picker === null && sessionPicker === null;
2205
3093
  const actions = useMemo2(
2206
3094
  () => ({
2207
3095
  setMessages: (msgs) => {
@@ -2209,11 +3097,11 @@ function App({
2209
3097
  agentRef.current?.setMessages(msgs);
2210
3098
  dispatch({ type: "history_set", messages: msgs });
2211
3099
  },
2212
- pickModel: (items, currentId) => new Promise((resolve5) => {
2213
- setPicker({ items, currentId, resolve: resolve5 });
3100
+ pickModel: (items, currentId) => new Promise((resolve6) => {
3101
+ setPicker({ items, currentId, resolve: resolve6 });
2214
3102
  }),
2215
- pickSession: (items, currentId) => new Promise((resolve5) => {
2216
- setSessionPicker({ items, currentId, resolve: resolve5 });
3103
+ pickSession: (items, currentId) => new Promise((resolve6) => {
3104
+ setSessionPicker({ items, currentId, resolve: resolve6 });
2217
3105
  }),
2218
3106
  switchModel: async (modelId) => {
2219
3107
  if (!modelsRegistry) throw new Error("No models registry loaded.");
@@ -2224,6 +3112,20 @@ function App({
2224
3112
  setLLM(next);
2225
3113
  await persistActiveModel(modelId);
2226
3114
  },
3115
+ getMode: () => permissions.getMode(),
3116
+ setMode: (m) => {
3117
+ permissions.setMode(m);
3118
+ setMode(m);
3119
+ },
3120
+ showProgress: (opts) => {
3121
+ setProgress({
3122
+ title: opts.title,
3123
+ tips: opts.tips ?? [],
3124
+ getPercent: opts.getPercent ?? (() => 0),
3125
+ startTime: Date.now()
3126
+ });
3127
+ },
3128
+ hideProgress: () => setProgress(null),
2227
3129
  reloadSettings: async () => {
2228
3130
  const { settings: nextSettings, sources } = await loadSettings(cwd);
2229
3131
  const { registry: nextModels } = await loadModelsRegistry();
@@ -2253,7 +3155,7 @@ function App({
2253
3155
  return { settings: nextSettings, sources };
2254
3156
  }
2255
3157
  }),
2256
- [cwd, modelsRegistry, llm.model]
3158
+ [cwd, modelsRegistry, llm.model, permissions]
2257
3159
  );
2258
3160
  const handleSubmit = useCallback(
2259
3161
  async (value) => {
@@ -2271,6 +3173,10 @@ function App({
2271
3173
  }
2272
3174
  const parsed = parseSlash(trimmed);
2273
3175
  if (parsed) {
3176
+ if (state.status !== "idle") {
3177
+ commitInput("");
3178
+ return;
3179
+ }
2274
3180
  const cmd = slash.get(parsed.name);
2275
3181
  commitInput("");
2276
3182
  if (!cmd) {
@@ -2305,6 +3211,15 @@ function App({
2305
3211
  return;
2306
3212
  }
2307
3213
  commitInput("");
3214
+ const hist = inputHistoryRef.current;
3215
+ if (hist[hist.length - 1] !== trimmed) hist.push(trimmed);
3216
+ if (hist.length > 200) hist.shift();
3217
+ historyIndexRef.current = -1;
3218
+ savedDraftRef.current = "";
3219
+ if (state.status !== "idle") {
3220
+ enqueueInput(trimmed);
3221
+ return;
3222
+ }
2308
3223
  dispatch({ type: "user_submit" });
2309
3224
  try {
2310
3225
  await agentRef.current?.runTurn(trimmed);
@@ -2316,7 +3231,7 @@ function App({
2316
3231
  dispatch({ type: "set_status", status: "idle" });
2317
3232
  }
2318
3233
  },
2319
- [slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, actions, autocomplete, autocompleteIndex]
3234
+ [slash, cwd, llm, session, settings, settingsSources, modelsRegistry, state.inputTokens, state.outputTokens, state.totalTokens, state.status, actions, autocomplete, autocompleteIndex]
2320
3235
  );
2321
3236
  function appendAssistantText(text) {
2322
3237
  const msg = { role: "assistant", content: [{ type: "text", text }] };
@@ -2334,25 +3249,31 @@ function App({
2334
3249
  }
2335
3250
  }
2336
3251
  const banner = !showBanner ? null : pickBanner(termWidth, { version: "0.1.0", model: llm.model, cwd: shortCwd(cwd) });
2337
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3252
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
2338
3253
  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 }) })
3254
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginTop: 1, children: [
3255
+ state.history.map((msg, i) => /* @__PURE__ */ jsx14(MessageView, { message: msg }, i)),
3256
+ state.streamingText && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", marginTop: 1, children: [
3257
+ /* @__PURE__ */ jsxs13(Text14, { color: "cyan", children: [
3258
+ DOT,
3259
+ " "
3260
+ ] }),
3261
+ /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx14(Text14, { children: state.streamingText }) })
3262
+ ] })
2342
3263
  ] }),
2343
- pending && /* @__PURE__ */ jsx9(
3264
+ pending && /* @__PURE__ */ jsx14(
2344
3265
  PermissionPrompt,
2345
3266
  {
2346
3267
  request: {
2347
3268
  ...pending,
2348
- resolve: (ok) => {
2349
- pending.resolve(ok);
3269
+ resolve: (decision) => {
3270
+ pending.resolve(decision);
2350
3271
  setPending(null);
2351
3272
  }
2352
3273
  }
2353
3274
  }
2354
3275
  ),
2355
- picker && /* @__PURE__ */ jsx9(
3276
+ picker && /* @__PURE__ */ jsx14(
2356
3277
  ModelSelector,
2357
3278
  {
2358
3279
  request: {
@@ -2364,7 +3285,7 @@ function App({
2364
3285
  }
2365
3286
  }
2366
3287
  ),
2367
- sessionPicker && /* @__PURE__ */ jsx9(
3288
+ sessionPicker && /* @__PURE__ */ jsx14(
2368
3289
  SessionSelector,
2369
3290
  {
2370
3291
  request: {
@@ -2376,38 +3297,90 @@ function App({
2376
3297
  }
2377
3298
  }
2378
3299
  ),
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)
3300
+ state.status !== "idle" && /* @__PURE__ */ jsx14(
3301
+ StatusLine,
3302
+ {
3303
+ startTime: state.turnStartTime,
3304
+ firstTextTime: state.turnFirstTextTime,
3305
+ inputTokens: state.turnInputTokens,
3306
+ runningTool: state.runningTool,
3307
+ lang
3308
+ }
3309
+ ),
3310
+ progress && /* @__PURE__ */ jsx14(ProgressBanner, { state: progress }),
3311
+ acceptingInput && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
3312
+ queuedInputs.length > 0 && /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [
3313
+ queuedInputs.map((q, i) => /* @__PURE__ */ jsx14(Text14, { color: "yellow", dimColor: true, children: `\u21B3 queued: ${q.length > 60 ? q.slice(0, 60) + "\u2026" : q}` }, i)),
3314
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` (will send after current turn \xB7 ${queuedInputs.length} pending)` })
2383
3315
  ] }),
2384
- autocomplete && autocomplete.matches.length > 0 && /* @__PURE__ */ jsx9(SlashAutocomplete, { matches: autocomplete.matches, index: autocompleteIndex })
3316
+ /* @__PURE__ */ jsxs13(Box12, { marginTop: 1, flexDirection: "column", children: [
3317
+ /* @__PURE__ */ jsx14(Text14, { backgroundColor: "#1c1c1c", children: " ".repeat(Math.max(1, termWidth - 1)) }),
3318
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", children: [
3319
+ /* @__PURE__ */ jsx14(Text14, { backgroundColor: "#1c1c1c", color: "gray", bold: true, children: " \u276F " }),
3320
+ /* @__PURE__ */ jsx14(
3321
+ BgTextInput,
3322
+ {
3323
+ value: input,
3324
+ onChange: setInput,
3325
+ onSubmit: handleSubmit,
3326
+ width: Math.max(10, termWidth - 4),
3327
+ backgroundColor: "#1c1c1c",
3328
+ isActive: acceptingInput
3329
+ },
3330
+ inputRemountKey
3331
+ )
3332
+ ] }),
3333
+ /* @__PURE__ */ jsx14(Text14, { backgroundColor: "#1c1c1c", children: " ".repeat(Math.max(1, termWidth - 1)) })
3334
+ ] }),
3335
+ autocomplete && autocomplete.matches.length > 0 && /* @__PURE__ */ jsx14(SlashAutocomplete, { matches: autocomplete.matches, index: autocompleteIndex })
2385
3336
  ] }),
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 }) })
3337
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
3338
+ /* @__PURE__ */ jsx14(
3339
+ FooterStatus,
3340
+ {
3341
+ sessionId: session.meta.id,
3342
+ model: llm.model,
3343
+ contextWindow: llm.capabilities.maxContextWindow,
3344
+ lastInputTokens: state.turnInputTokens,
3345
+ sessionInputTokens: state.inputTokens,
3346
+ sessionOutputTokens: state.outputTokens,
3347
+ termWidth
3348
+ }
3349
+ ),
3350
+ /* @__PURE__ */ jsx14(PermissionModeBar, { mode, compact: termWidth < 60 })
3351
+ ] })
2389
3352
  ] });
2390
3353
  }
3354
+ function extractUserInputs(messages) {
3355
+ const out = [];
3356
+ for (const m of messages) {
3357
+ if (m.role !== "user") continue;
3358
+ const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
3359
+ if (text.startsWith("[Previous conversation summary]")) continue;
3360
+ if (text.trim()) out.push(text);
3361
+ }
3362
+ return out;
3363
+ }
2391
3364
  function shortCwd(cwd) {
2392
- const home = homedir7();
3365
+ const home = homedir8();
2393
3366
  if (cwd === home) return "~";
2394
3367
  if (cwd.startsWith(home + "/")) return "~" + cwd.slice(home.length);
2395
3368
  return cwd;
2396
3369
  }
2397
3370
  async function persistActiveModel(modelId) {
2398
- const path = join5(homedir7(), ".muse", "settings.json");
3371
+ const path = join6(homedir8(), ".muse", "settings.json");
2399
3372
  let current = {};
2400
- if (existsSync4(path)) {
3373
+ if (existsSync5(path)) {
2401
3374
  try {
2402
- current = JSON.parse(await readFile4(path, "utf-8"));
3375
+ current = JSON.parse(await readFile5(path, "utf-8"));
2403
3376
  } catch {
2404
3377
  current = {};
2405
3378
  }
2406
3379
  }
2407
3380
  const llm = current.llm ?? {};
2408
3381
  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");
3382
+ await mkdir3(dirname3(path), { recursive: true });
3383
+ await writeFile2(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
2411
3384
  }
2412
3385
 
2413
3386
  // src/tools/registry.ts
@@ -2521,8 +3494,8 @@ var ToolRegistry = class {
2521
3494
  };
2522
3495
 
2523
3496
  // src/tools/builtin/read.ts
2524
- import { readFile as readFile5, stat as stat2 } from "fs/promises";
2525
- import { resolve as resolve2, isAbsolute } from "path";
3497
+ import { readFile as readFile6, stat as stat2 } from "fs/promises";
3498
+ import { resolve as resolve3, isAbsolute } from "path";
2526
3499
  import { z as z3 } from "zod";
2527
3500
 
2528
3501
  // src/tools/types.ts
@@ -2537,6 +3510,48 @@ function defineTool(def) {
2537
3510
  };
2538
3511
  }
2539
3512
 
3513
+ // src/tools/_sensitive.ts
3514
+ import { homedir as homedir9 } from "os";
3515
+ import { basename as basename2, resolve as resolve2 } from "path";
3516
+ var HOME = homedir9();
3517
+ var SENSITIVE_DIRS = [
3518
+ resolve2(HOME, ".ssh"),
3519
+ resolve2(HOME, ".aws"),
3520
+ resolve2(HOME, ".gnupg"),
3521
+ resolve2(HOME, ".config", "gh")
3522
+ ];
3523
+ var SENSITIVE_FILES = [
3524
+ resolve2(HOME, ".kube", "config"),
3525
+ resolve2(HOME, ".netrc"),
3526
+ resolve2(HOME, ".pypirc")
3527
+ ];
3528
+ var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
3529
+ "id_rsa",
3530
+ "id_ed25519",
3531
+ "id_ecdsa",
3532
+ "id_dsa"
3533
+ ]);
3534
+ var ENV_PATTERN2 = /(?:^|\/)\.env(\..+)?$/;
3535
+ function checkSensitivePath(path) {
3536
+ const abs = resolve2(path);
3537
+ for (const dir of SENSITIVE_DIRS) {
3538
+ if (abs === dir || abs.startsWith(dir + "/")) {
3539
+ return { blocked: true, reason: `sensitive directory ${dir.replace(HOME, "~")}` };
3540
+ }
3541
+ }
3542
+ for (const f of SENSITIVE_FILES) {
3543
+ if (abs === f) return { blocked: true, reason: `sensitive file ${f.replace(HOME, "~")}` };
3544
+ }
3545
+ const base = basename2(abs);
3546
+ if (SENSITIVE_BASENAMES.has(base)) {
3547
+ return { blocked: true, reason: `private key filename ${base}` };
3548
+ }
3549
+ if (ENV_PATTERN2.test(abs)) {
3550
+ return { blocked: true, reason: `.env file (may contain secrets)` };
3551
+ }
3552
+ return { blocked: false };
3553
+ }
3554
+
2540
3555
  // src/tools/builtin/read.ts
2541
3556
  var ReadArgs = z3.object({
2542
3557
  file_path: z3.string().describe("Absolute or cwd-relative path to the file."),
@@ -2552,7 +3567,11 @@ var ReadTool = defineTool({
2552
3567
  permission: "read",
2553
3568
  summarize: (args) => `Read(${args.file_path}${args.offset != null ? `, offset=${args.offset}` : ""}${args.limit != null ? `, limit=${args.limit}` : ""})`,
2554
3569
  async execute(args, ctx) {
2555
- const path = isAbsolute(args.file_path) ? args.file_path : resolve2(ctx.cwd, args.file_path);
3570
+ const path = isAbsolute(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
3571
+ const sensitive = checkSensitivePath(path);
3572
+ if (sensitive.blocked) {
3573
+ return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
3574
+ }
2556
3575
  let info;
2557
3576
  try {
2558
3577
  info = await stat2(path);
@@ -2562,7 +3581,7 @@ var ReadTool = defineTool({
2562
3581
  if (!info.isFile()) {
2563
3582
  throw new ToolError(`Not a regular file: ${path}`, "Read");
2564
3583
  }
2565
- const content = await readFile5(path, "utf-8");
3584
+ const content = await readFile6(path, "utf-8");
2566
3585
  const lines = content.split(/\r?\n/);
2567
3586
  const offset = args.offset ?? 0;
2568
3587
  const limit = args.limit ?? DEFAULT_LIMIT;
@@ -2585,9 +3604,26 @@ var ReadTool = defineTool({
2585
3604
  });
2586
3605
 
2587
3606
  // 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";
3607
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir4, stat as stat3 } from "fs/promises";
3608
+ import { resolve as resolve4, isAbsolute as isAbsolute2, dirname as dirname4 } from "path";
2590
3609
  import { z as z4 } from "zod";
3610
+
3611
+ // src/tools/_diff.ts
3612
+ import { createPatch } from "diff";
3613
+ var MAX_DIFF_LINES = 200;
3614
+ function makeUnifiedDiff(filePath, oldContent, newContent) {
3615
+ if (oldContent === newContent) return "";
3616
+ const patch = createPatch(filePath, oldContent, newContent, "before", "after", { context: 3 });
3617
+ return truncate(patch);
3618
+ }
3619
+ function truncate(diff) {
3620
+ const lines = diff.split("\n");
3621
+ if (lines.length <= MAX_DIFF_LINES) return diff;
3622
+ return lines.slice(0, MAX_DIFF_LINES).join("\n") + `
3623
+ ... [${lines.length - MAX_DIFF_LINES} more diff lines truncated]`;
3624
+ }
3625
+
3626
+ // src/tools/builtin/write.ts
2591
3627
  var WriteArgs = z4.object({
2592
3628
  file_path: z4.string().describe("Absolute or cwd-relative path to the file."),
2593
3629
  content: z4.string().describe("Full content of the file.")
@@ -2599,25 +3635,33 @@ var WriteTool = defineTool({
2599
3635
  permission: "write",
2600
3636
  summarize: (args) => `Write(${args.file_path}, ${args.content.length} chars)`,
2601
3637
  async execute(args, ctx) {
2602
- const path = isAbsolute2(args.file_path) ? args.file_path : resolve3(ctx.cwd, args.file_path);
3638
+ const path = isAbsolute2(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
3639
+ const sensitive = checkSensitivePath(path);
3640
+ if (sensitive.blocked) {
3641
+ return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
3642
+ }
2603
3643
  let existed = false;
3644
+ let oldContent = "";
2604
3645
  try {
2605
3646
  const info = await stat3(path);
2606
3647
  existed = info.isFile();
3648
+ if (existed) oldContent = await readFile7(path, "utf-8");
2607
3649
  } catch {
2608
3650
  }
2609
- await mkdir3(dirname4(path), { recursive: true });
2610
- await writeFile2(path, args.content, "utf-8");
3651
+ await mkdir4(dirname4(path), { recursive: true });
3652
+ await writeFile3(path, args.content, "utf-8");
3653
+ const diff = makeUnifiedDiff(args.file_path, oldContent, args.content);
2611
3654
  return {
2612
3655
  content: existed ? `Overwrote ${path} (${args.content.length} bytes).` : `Created ${path} (${args.content.length} bytes).`,
2613
- summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`
3656
+ summary: `${existed ? "Overwrote" : "Created"} ${args.file_path}`,
3657
+ diff: diff || void 0
2614
3658
  };
2615
3659
  }
2616
3660
  });
2617
3661
 
2618
3662
  // 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";
3663
+ import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
3664
+ import { resolve as resolve5, isAbsolute as isAbsolute3 } from "path";
2621
3665
  import { z as z5 } from "zod";
2622
3666
  var EditArgs = z5.object({
2623
3667
  file_path: z5.string().describe("Absolute or cwd-relative path to the file."),
@@ -2632,10 +3676,14 @@ var EditTool = defineTool({
2632
3676
  permission: "write",
2633
3677
  summarize: (args) => `Edit(${args.file_path})`,
2634
3678
  async execute(args, ctx) {
2635
- const path = isAbsolute3(args.file_path) ? args.file_path : resolve4(ctx.cwd, args.file_path);
3679
+ const path = isAbsolute3(args.file_path) ? args.file_path : resolve5(ctx.cwd, args.file_path);
3680
+ const sensitive = checkSensitivePath(path);
3681
+ if (sensitive.blocked) {
3682
+ return { content: `Refused: ${path} matches sensitive path policy (${sensitive.reason}).`, isError: true };
3683
+ }
2636
3684
  let content;
2637
3685
  try {
2638
- content = await readFile6(path, "utf-8");
3686
+ content = await readFile8(path, "utf-8");
2639
3687
  } catch (err) {
2640
3688
  throw new ToolError(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`, "Edit", err);
2641
3689
  }
@@ -2656,10 +3704,12 @@ var EditTool = defineTool({
2656
3704
  };
2657
3705
  }
2658
3706
  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");
3707
+ await writeFile4(path, newContent, "utf-8");
3708
+ const diff = makeUnifiedDiff(args.file_path, content, newContent);
2660
3709
  return {
2661
3710
  content: `Edited ${path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s).`,
2662
- summary: `Edited ${args.file_path}`
3711
+ summary: `Edited ${args.file_path}`,
3712
+ diff: diff || void 0
2663
3713
  };
2664
3714
  }
2665
3715
  });
@@ -2738,8 +3788,8 @@ var BashTool = defineTool({
2738
3788
  maxBuffer: MAX_OUTPUT_BYTES * 2,
2739
3789
  cancelSignal: ctx.abortSignal
2740
3790
  });
2741
- const stdout = truncate(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
2742
- const stderr = truncate(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
3791
+ const stdout = truncate2(result.stdout ?? "", MAX_OUTPUT_BYTES, "stdout");
3792
+ const stderr = truncate2(result.stderr ?? "", MAX_OUTPUT_BYTES, "stderr");
2743
3793
  const parts = [];
2744
3794
  if (stdout) parts.push(`<stdout>
2745
3795
  ${stdout}
@@ -2763,7 +3813,7 @@ ${stderr}
2763
3813
  }
2764
3814
  }
2765
3815
  });
2766
- function truncate(text, max, label) {
3816
+ function truncate2(text, max, label) {
2767
3817
  if (text.length <= max) return text;
2768
3818
  return text.slice(0, max) + `
2769
3819
  ... [${label} truncated, original ${text.length} bytes]`;
@@ -2872,6 +3922,243 @@ var GlobTool = defineTool({
2872
3922
  }
2873
3923
  });
2874
3924
 
3925
+ // src/tools/builtin/todo.ts
3926
+ import { z as z9 } from "zod";
3927
+ var TodoSchema = z9.object({
3928
+ content: z9.string().describe("Imperative one-line task description (e.g. 'Run the test suite')."),
3929
+ status: z9.enum(["pending", "in_progress", "completed"]).describe("Current status."),
3930
+ activeForm: z9.string().optional().describe("Present-continuous form for the spinner (e.g. 'Running the test suite').")
3931
+ });
3932
+ var TodoWriteArgs = z9.object({
3933
+ todos: z9.array(TodoSchema).describe("Full list. Replaces the current store.")
3934
+ });
3935
+ var TodoWriteTool = defineTool({
3936
+ name: "TodoWrite",
3937
+ 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.",
3938
+ parameters: TodoWriteArgs,
3939
+ permission: "read",
3940
+ summarize: (args) => `TodoWrite(${args.todos.length} items)`,
3941
+ async execute(args, ctx) {
3942
+ if (!ctx.todos) {
3943
+ return {
3944
+ content: "TodoWrite is unavailable: this agent run has no todo store. (Internal bug; tell the user.)",
3945
+ isError: true
3946
+ };
3947
+ }
3948
+ ctx.todos.set(args.todos);
3949
+ const summary = args.todos.map((t, i) => `${i + 1}. ${t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]"} ${t.content}`).join("\n");
3950
+ return {
3951
+ content: `Updated todos (${args.todos.length} items):
3952
+ ${summary}`,
3953
+ summary: `Todos: ${args.todos.filter((t) => t.status === "completed").length}/${args.todos.length} done`
3954
+ };
3955
+ }
3956
+ });
3957
+
3958
+ // src/tools/builtin/webfetch.ts
3959
+ import { z as z10 } from "zod";
3960
+ var WebFetchArgs = z10.object({
3961
+ url: z10.string().describe("Fully-qualified URL. http will be upgraded to https."),
3962
+ prompt: z10.string().optional().describe(
3963
+ "What information to look for. The host returns the page content; the LLM should then read it to answer the prompt."
3964
+ )
3965
+ });
3966
+ var MAX_RESPONSE_BYTES = 1e6;
3967
+ var FETCH_TIMEOUT_MS = 3e4;
3968
+ var PRIVATE_HOST_PATTERNS = [
3969
+ /^localhost$/i,
3970
+ /^127\./,
3971
+ /^0\.0\.0\.0$/,
3972
+ /^169\.254\./,
3973
+ /^10\./,
3974
+ /^192\.168\./,
3975
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
3976
+ /^::1$/,
3977
+ /^fc[0-9a-f]{2}:/i,
3978
+ /^fe80:/i
3979
+ ];
3980
+ function isPrivateHost(hostname) {
3981
+ return PRIVATE_HOST_PATTERNS.some((p) => p.test(hostname));
3982
+ }
3983
+ var WebFetchTool = defineTool({
3984
+ name: "WebFetch",
3985
+ 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.",
3986
+ parameters: WebFetchArgs,
3987
+ permission: "network",
3988
+ summarize: (args) => `WebFetch(${args.url})`,
3989
+ async execute(args, ctx) {
3990
+ let target;
3991
+ try {
3992
+ target = new URL(args.url);
3993
+ } catch {
3994
+ return { content: `Invalid URL: ${args.url}`, isError: true };
3995
+ }
3996
+ if (target.protocol === "http:") {
3997
+ target.protocol = "https:";
3998
+ }
3999
+ if (target.protocol !== "https:") {
4000
+ return { content: `Refused: only http(s) URLs are allowed.`, isError: true };
4001
+ }
4002
+ if (isPrivateHost(target.hostname)) {
4003
+ return { content: `Refused: ${target.hostname} is a private/loopback host (SSRF guard).`, isError: true };
4004
+ }
4005
+ const controller = new AbortController();
4006
+ const onAbort = () => controller.abort();
4007
+ ctx.abortSignal?.addEventListener("abort", onAbort);
4008
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
4009
+ try {
4010
+ const resp = await fetch(target.toString(), {
4011
+ redirect: "manual",
4012
+ signal: controller.signal,
4013
+ headers: { "user-agent": "muse-cli/0.1" }
4014
+ });
4015
+ if (resp.status >= 300 && resp.status < 400) {
4016
+ const loc = resp.headers.get("location");
4017
+ if (loc) {
4018
+ try {
4019
+ const redirectURL = new URL(loc, target);
4020
+ if (redirectURL.hostname !== target.hostname) {
4021
+ return {
4022
+ content: `Redirect to a different host: ${redirectURL.toString()}
4023
+ Re-fetch the new URL explicitly if you trust it.`,
4024
+ summary: `Redirect to a different host: ${redirectURL.toString()}`,
4025
+ kind: "warn"
4026
+ };
4027
+ }
4028
+ return {
4029
+ content: `Redirect (same host): ${redirectURL.toString()}
4030
+ Re-fetch the new URL to continue.`,
4031
+ summary: `Redirect \u2192 ${redirectURL.pathname}`,
4032
+ kind: "warn"
4033
+ };
4034
+ } catch {
4035
+ return { content: `Redirect with unparseable location: ${loc}`, isError: true };
4036
+ }
4037
+ }
4038
+ }
4039
+ if (!resp.ok) {
4040
+ return { content: `HTTP ${resp.status} ${resp.statusText} for ${target.toString()}`, isError: true };
4041
+ }
4042
+ const contentType = resp.headers.get("content-type") ?? "";
4043
+ const reader = resp.body?.getReader();
4044
+ if (!reader) return { content: `Empty response body.`, isError: true };
4045
+ const chunks = [];
4046
+ let total = 0;
4047
+ while (true) {
4048
+ const { value, done } = await reader.read();
4049
+ if (done) break;
4050
+ if (value) {
4051
+ total += value.byteLength;
4052
+ if (total > MAX_RESPONSE_BYTES) {
4053
+ await reader.cancel();
4054
+ chunks.push(value.slice(0, value.byteLength - (total - MAX_RESPONSE_BYTES)));
4055
+ break;
4056
+ }
4057
+ chunks.push(value);
4058
+ }
4059
+ }
4060
+ const body = new TextDecoder("utf-8", { fatal: false }).decode(Buffer.concat(chunks.map((c) => Buffer.from(c))));
4061
+ let processed = body;
4062
+ if (/^text\/html|application\/xhtml/i.test(contentType)) {
4063
+ processed = htmlToText(body);
4064
+ }
4065
+ const summary = args.prompt ? `# WebFetch result for: ${args.prompt}` : `Fetched ${target.hostname} (${total} bytes${total >= MAX_RESPONSE_BYTES ? ", truncated" : ""})`;
4066
+ const truncated = processed.length > 2e5 ? processed.slice(0, 2e5) + "\n\n... [truncated]" : processed;
4067
+ const preface = args.prompt ? `# WebFetch result for: ${args.prompt}
4068
+
4069
+ Source: ${target.toString()}
4070
+
4071
+ ` : `Source: ${target.toString()}
4072
+
4073
+ `;
4074
+ return { content: preface + truncated, summary };
4075
+ } catch (err) {
4076
+ if (err.name === "AbortError") {
4077
+ return { content: `WebFetch aborted (timeout or user cancel).`, isError: true };
4078
+ }
4079
+ return { content: `WebFetch failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
4080
+ } finally {
4081
+ clearTimeout(timer);
4082
+ ctx.abortSignal?.removeEventListener("abort", onAbort);
4083
+ }
4084
+ }
4085
+ });
4086
+ function htmlToText(html) {
4087
+ let s = html;
4088
+ s = s.replace(/<(script|style|svg|noscript)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
4089
+ s = s.replace(/<!--[\s\S]*?-->/g, "");
4090
+ s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, txt) => {
4091
+ return `
4092
+
4093
+ ${"#".repeat(parseInt(lvl, 10))} ${stripTags(txt).trim()}
4094
+
4095
+ `;
4096
+ });
4097
+ s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, txt) => {
4098
+ const label = stripTags(txt).trim();
4099
+ return label ? `[${label}](${href})` : href;
4100
+ });
4101
+ s = s.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_m, txt) => `
4102
+ - ${stripTags(txt).trim()}`);
4103
+ s = s.replace(/<(p|div|section|article|header|footer|main|aside|nav|pre|blockquote|br|hr)\b[^>]*>/gi, "\n");
4104
+ s = s.replace(/<\/(p|div|section|article|header|footer|main|aside|nav|pre|blockquote)>/gi, "\n");
4105
+ s = s.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_m, txt) => `\`${stripTags(txt)}\``);
4106
+ s = stripTags(s);
4107
+ s = s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
4108
+ s = s.replace(/\n{3,}/g, "\n\n").trim();
4109
+ return s;
4110
+ }
4111
+ function stripTags(s) {
4112
+ return s.replace(/<[^>]+>/g, "");
4113
+ }
4114
+
4115
+ // src/tools/builtin/memory.ts
4116
+ import { z as z11 } from "zod";
4117
+ var TYPES = ["user", "feedback", "project", "reference"];
4118
+ var MemoryWriteArgs = z11.object({
4119
+ 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."),
4120
+ description: z11.string().describe("One-line summary used in MEMORY.md index (decides future relevance)."),
4121
+ type: z11.enum(TYPES).describe("user | feedback | project | reference"),
4122
+ body: z11.string().describe("Memory content (markdown). For feedback/project, lead with the rule/fact then **Why:** and **How to apply:** lines.")
4123
+ });
4124
+ var MemoryWriteTool = defineTool({
4125
+ name: "MemoryWrite",
4126
+ 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.",
4127
+ parameters: MemoryWriteArgs,
4128
+ permission: "write",
4129
+ summarize: (args) => `MemoryWrite(${args.name}, type=${args.type})`,
4130
+ async execute(args, ctx) {
4131
+ const { filePath, indexUpdated } = await writeMemory(ctx.cwd, {
4132
+ name: args.name,
4133
+ description: args.description,
4134
+ type: args.type,
4135
+ body: args.body
4136
+ });
4137
+ return {
4138
+ content: `Saved memory "${args.name}" (${args.type}) \u2192 ${filePath}${indexUpdated ? "\nMEMORY.md updated." : ""}`,
4139
+ summary: `MemoryWrite ${args.name}`
4140
+ };
4141
+ }
4142
+ });
4143
+ var MemoryReadArgs = z11.object({
4144
+ name: z11.string().describe("Memory slug to read (no .md extension).")
4145
+ });
4146
+ var MemoryReadTool = defineTool({
4147
+ name: "MemoryRead",
4148
+ 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).",
4149
+ parameters: MemoryReadArgs,
4150
+ permission: "read",
4151
+ summarize: (args) => `MemoryRead(${args.name})`,
4152
+ async execute(args, ctx) {
4153
+ try {
4154
+ const content = await readMemoryFile(ctx.cwd, args.name);
4155
+ return { content, summary: `MemoryRead ${args.name}` };
4156
+ } catch (err) {
4157
+ return { content: err instanceof Error ? err.message : String(err), isError: true };
4158
+ }
4159
+ }
4160
+ });
4161
+
2875
4162
  // src/tools/builtin/index.ts
2876
4163
  var BUILTIN_TOOLS = [
2877
4164
  ReadTool,
@@ -2879,16 +4166,20 @@ var BUILTIN_TOOLS = [
2879
4166
  EditTool,
2880
4167
  BashTool,
2881
4168
  GrepTool,
2882
- GlobTool
4169
+ GlobTool,
4170
+ TodoWriteTool,
4171
+ WebFetchTool,
4172
+ MemoryReadTool,
4173
+ MemoryWriteTool
2883
4174
  ];
2884
4175
 
2885
4176
  // src/cli.tsx
2886
- import { jsx as jsx10 } from "react/jsx-runtime";
4177
+ import { jsx as jsx15 } from "react/jsx-runtime";
2887
4178
  var VERSION = "0.1.0";
2888
4179
  async function main() {
2889
4180
  const program = new Command();
2890
- program.name("muse").description("A Claude Code-style agent CLI. Provider-agnostic. First-class support for Chinese / self-hostable 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) => {
4181
+ 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");
4182
+ 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
4183
  if (opts.debug) log.setLevel("debug");
2893
4184
  const cwd = process.cwd();
2894
4185
  const { settings, sources } = await loadSettings(cwd);
@@ -2908,7 +4199,7 @@ async function main() {
2908
4199
  llmModelName = llm.model;
2909
4200
  } else {
2910
4201
  if (!provider || !model) {
2911
- die("No model configured. Either define one in ~/.muse/models.json or set llm.provider+llm.model in settings.json.");
4202
+ die("No model configured. Either define one in ~/.muse/models.local.json or set llm.provider+llm.model in settings.json.");
2912
4203
  }
2913
4204
  llm = createLLMClient({ provider, model, providers: settings.providers ?? {} });
2914
4205
  llmProviderName = provider;
@@ -2921,24 +4212,62 @@ async function main() {
2921
4212
  const tools = new ToolRegistry();
2922
4213
  tools.registerAll(BUILTIN_TOOLS);
2923
4214
  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
- });
4215
+ if (opts.mode) {
4216
+ const valid = ["default", "acceptEdits", "plan", "bypassPermissions"];
4217
+ if (!valid.includes(opts.mode)) {
4218
+ die(`Invalid --mode "${opts.mode}". Valid: ${valid.join(", ")}`);
4219
+ }
4220
+ permissions.setMode(opts.mode);
4221
+ }
4222
+ let session;
4223
+ let initialMessages;
4224
+ if (opts.continue) {
4225
+ const latest = await Session.findLatest(cwd);
4226
+ if (latest) {
4227
+ const opened = await Session.open(latest);
4228
+ session = opened.session;
4229
+ initialMessages = Session.messagesFromEvents(opened.events);
4230
+ log.debug("resumed session", { id: latest.id, messages: initialMessages.length });
4231
+ } else {
4232
+ session = await Session.create(cwd);
4233
+ await session.append({
4234
+ type: "session_start",
4235
+ time: (/* @__PURE__ */ new Date()).toISOString(),
4236
+ cwd,
4237
+ provider: llmProviderName,
4238
+ model: llmModelName
4239
+ });
4240
+ }
4241
+ } else {
4242
+ session = await Session.create(cwd);
4243
+ await session.append({
4244
+ type: "session_start",
4245
+ time: (/* @__PURE__ */ new Date()).toISOString(),
4246
+ cwd,
4247
+ provider: llmProviderName,
4248
+ model: llmModelName
4249
+ });
4250
+ }
2932
4251
  const showBanner = !opts.quiet && opts.banner !== false;
2933
4252
  const lang = settings.ui?.lang ?? "en";
2934
4253
  const pipedInput = await readStdinIfPiped();
2935
4254
  const oneShotPrompt = [...promptArgs ?? [], pipedInput].filter(Boolean).join("\n").trim();
2936
4255
  if (oneShotPrompt) {
2937
- await runOneShot({ llm, tools, permissions, session, cwd, lang, prompt: oneShotPrompt, quiet: opts.quiet ?? false });
4256
+ await runOneShot({
4257
+ llm,
4258
+ tools,
4259
+ permissions,
4260
+ session,
4261
+ cwd,
4262
+ lang,
4263
+ prompt: oneShotPrompt,
4264
+ quiet: opts.quiet ?? false,
4265
+ initialMessages
4266
+ });
2938
4267
  return;
2939
4268
  }
2940
4269
  const { waitUntilExit } = render(
2941
- /* @__PURE__ */ jsx10(
4270
+ /* @__PURE__ */ jsx15(
2942
4271
  App,
2943
4272
  {
2944
4273
  llm,
@@ -2951,7 +4280,8 @@ async function main() {
2951
4280
  modelsSources,
2952
4281
  cwd,
2953
4282
  lang,
2954
- showBanner
4283
+ showBanner,
4284
+ initialMessages
2955
4285
  }
2956
4286
  )
2957
4287
  );
@@ -2966,12 +4296,14 @@ async function readStdinIfPiped() {
2966
4296
  return Buffer.concat(chunks).toString("utf-8").trim();
2967
4297
  }
2968
4298
  async function runOneShot(opts) {
4299
+ const memoryIndex = await loadMemoryIndex(opts.cwd);
2969
4300
  const systemPrompt = buildSystemPrompt({
2970
4301
  cwd: opts.cwd,
2971
4302
  model: opts.llm.model,
2972
4303
  provider: opts.llm.providerName,
2973
4304
  lang: opts.lang,
2974
- toolNames: opts.tools.list().map((t) => t.name)
4305
+ toolNames: opts.tools.list().map((t) => t.name),
4306
+ memoryIndex
2975
4307
  });
2976
4308
  const agent = new Agent({
2977
4309
  llm: opts.llm,
@@ -2994,10 +4326,11 @@ async function runOneShot(opts) {
2994
4326
  if (!opts.quiet) process.stderr.write(`
2995
4327
  [denied: ${toolName} \u2014 ${summary}; run in interactive mode to approve]
2996
4328
  `);
2997
- return false;
4329
+ return "no";
2998
4330
  }
2999
4331
  }
3000
4332
  });
4333
+ if (opts.initialMessages?.length) agent.setMessages(opts.initialMessages);
3001
4334
  await agent.runTurn(opts.prompt);
3002
4335
  process.stdout.write("\n");
3003
4336
  }