magpie-agent-console 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,15 @@ function initLog(dataDir) {
38
38
  } catch {
39
39
  }
40
40
  }
41
+ function guardConsole() {
42
+ if (guarded) return;
43
+ guarded = true;
44
+ for (const stream of [process.stdout, process.stderr]) {
45
+ stream?.on?.("error", () => {
46
+ consoleGone = true;
47
+ });
48
+ }
49
+ }
41
50
  function log(level, msg, extra) {
42
51
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}${extra !== void 0 ? " " + safeJson(extra) : ""}`;
43
52
  try {
@@ -50,8 +59,19 @@ function log(level, msg, extra) {
50
59
  if (logFile) appendFileSync(logFile, line + "\n", "utf8");
51
60
  } catch {
52
61
  }
62
+ guardConsole();
63
+ if (consoleGone) return;
64
+ const stream = level === "info" ? process.stdout : process.stderr;
65
+ if (stream?.destroyed || stream?.writableEnded) {
66
+ consoleGone = true;
67
+ return;
68
+ }
53
69
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
54
- fn(`[agent-console] ${msg}`, extra !== void 0 ? extra : "");
70
+ try {
71
+ fn(`[agent-console] ${msg}`, extra !== void 0 ? extra : "");
72
+ } catch {
73
+ consoleGone = true;
74
+ }
55
75
  }
56
76
  function safeJson(v) {
57
77
  try {
@@ -60,12 +80,255 @@ function safeJson(v) {
60
80
  return String(v);
61
81
  }
62
82
  }
63
- var logDir, today, logFile;
83
+ var logDir, today, logFile, consoleGone, guarded;
64
84
  var init_log = __esm({
65
85
  "daemon/src/log.ts"() {
66
86
  logDir = "";
67
87
  today = "";
68
88
  logFile = "";
89
+ consoleGone = false;
90
+ guarded = false;
91
+ }
92
+ });
93
+
94
+ // daemon/src/util/i18n.ts
95
+ function normalizeLang(raw) {
96
+ const s = String(raw ?? "").trim().toLowerCase();
97
+ if (!s) return "zh";
98
+ if (s === "zh" || s.startsWith("zh-")) return "zh";
99
+ return "en";
100
+ }
101
+ function setAppLocale(lang) {
102
+ locale = normalizeLang(lang);
103
+ }
104
+ function intlLocale() {
105
+ return locale === "zh" ? "zh-CN" : "en-US";
106
+ }
107
+ function tr(key, params) {
108
+ let s = dict[locale][key] ?? dict.en[key] ?? key;
109
+ if (params) {
110
+ for (const [k, v] of Object.entries(params)) s = s.replaceAll(`{${k}}`, String(v));
111
+ }
112
+ return s;
113
+ }
114
+ var locale, dict;
115
+ var init_i18n = __esm({
116
+ "daemon/src/util/i18n.ts"() {
117
+ locale = "zh";
118
+ dict = {
119
+ zh: {
120
+ "thread.defaultTitle": "\u65B0\u5BF9\u8BDD",
121
+ "notice.daemonRestart": "\u540E\u53F0\u5DF2\u91CD\u542F\uFF0C\u672C\u8F6E\u5DF2\u4E2D\u65AD",
122
+ "notice.sessionStartFailed": "\u4F1A\u8BDD\u542F\u52A8\u5931\u8D25\uFF1A{detail}",
123
+ "notice.sendFailed": "\u53D1\u9001\u5931\u8D25\uFF1A{detail}",
124
+ "notice.effortNextTurn": "\u601D\u8003\u5F3A\u5EA6\u5DF2\u4FDD\u5B58\uFF0C\u5C06\u5728\u4E0B\u4E00\u8F6E\u751F\u6548",
125
+ "notice.effortResetOnModel": "\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301\u8BE5\u601D\u8003\u5F3A\u5EA6\uFF0C\u5DF2\u6062\u590D\u4E3A Default",
126
+ "turn.agentExit": "Agent \u8FDB\u7A0B\u610F\u5916\u9000\u51FA",
127
+ "attachment.missing": "\u9644\u4EF6\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548\uFF08{ref}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u4E0A\u4F20",
128
+ "notice.acpImageByPath": "{agent} \u4E0D\u80FD\u76F4\u63A5\u770B\u56FE\u7247\uFF08\u5B83\u7684 ACP \u521D\u59CB\u5316\u672A\u58F0\u660E\u652F\u6301\u56FE\u7247\u8F93\u5165\uFF09\uFF0C\u5DF2\u6539\u4E3A\u53D1\u9001\u56FE\u7247\u7684\u6587\u4EF6\u8DEF\u5F84\uFF1A{paths}",
129
+ "notice.acpImageByPathSwitched": "{agent} \u662F\u5728\u53E6\u4E00\u4E2A\u6A21\u578B\u4E0A\u542F\u52A8\u7684\uFF0C\u53EA\u5BF9\u90A3\u4E2A\u6A21\u578B\u63A5\u6536\u56FE\u7247\uFF0C\u8FD9\u6B21\u6539\u4E3A\u53D1\u9001\u56FE\u7247\u7684\u6587\u4EF6\u8DEF\u5F84\uFF08\u5F53\u524D\u6A21\u578B\u80FD\u770B\u56FE\u65F6\u5B83\u4F1A\u7528 read_image \u6253\u5F00\uFF09\uFF1A{paths}",
130
+ "notice.acpSessionNotRestored": "{agent} \u4E0D\u80FD\u6062\u590D\u4E0A\u4E00\u8F6E\u5BF9\u8BDD\uFF08\u5B83\u7684 ACP \u65E2\u4E0D\u652F\u6301 session/load \u4E5F\u4E0D\u652F\u6301 session/resume\uFF09\uFF0C\u5DF2\u65B0\u5F00\u4E00\u8F6E\uFF1B\u5148\u524D\u7684\u4E0A\u4E0B\u6587\u4E0D\u4F1A\u5E26\u56DE\u3002",
131
+ "notice.attachmentIssue": "\u9644\u4EF6\u95EE\u9898\uFF1A{detail}",
132
+ "claude.notFound": "\u627E\u4E0D\u5230 claude\uFF08{detail}\uFF09\u3002\u672C\u63D2\u4EF6\u4E0D\u9644\u5E26 Claude Code\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1A{hint}",
133
+ "claude.startFailed": "\u65E0\u6CD5\u542F\u52A8 claude\uFF1A{detail}",
134
+ "claude.exited": "claude \u8FDB\u7A0B\u610F\u5916\u9000\u51FA\uFF08{code}\uFF09",
135
+ "claude.notRunning": "claude \u6CA1\u6709\u5728\u8FD0\u884C\uFF0C\u6D88\u606F\u672A\u53D1\u51FA",
136
+ "claude.closed": "\u4F1A\u8BDD\u5DF2\u5173\u95ED",
137
+ "export.defaultModel": "\u9ED8\u8BA4",
138
+ "bin.notOnPath": "PATH \u91CC\u627E\u4E0D\u5230 {name}",
139
+ "bin.or": " \u6216 ",
140
+ "launch.notFound": "\u627E\u4E0D\u5230\u81EA\u5B9A\u4E49\u7684\u53EF\u6267\u884C\u6587\u4EF6\uFF1A{cmd}\uFF08\u8BF7\u586B\u5199\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u6216 PATH \u91CC\u80FD\u627E\u5230\u7684\u547D\u4EE4\u540D\uFF09",
141
+ "codex.notFound": "\u627E\u4E0D\u5230 codex\uFF08{detail}\uFF09\u3002\u8BF7\u5148\u5B89\u88C5\uFF1A{hint}",
142
+ "provider.disabled": "\u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528",
143
+ "provider.planned": "\u5C1A\u672A\u63A5\u5165",
144
+ "provider.disabledNamed": "{name} \u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528",
145
+ "provider.plannedNamed": "{name} \u5C1A\u672A\u63A5\u5165\uFF0C\u6682\u4E0D\u80FD\u65B0\u5EFA\u5BF9\u8BDD",
146
+ "provider.unavailable": "{name} \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528",
147
+ "provider.unknown": "\u672A\u77E5 Agent\uFF1A{id}",
148
+ "provider.timeout": "\u68C0\u6D4B\u8D85\u65F6",
149
+ "err.projectMissing": "\u9879\u76EE\u4E0D\u5B58\u5728",
150
+ "err.threadMissing": "\u5BF9\u8BDD\u4E0D\u5B58\u5728",
151
+ "err.dirMissing": "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939",
152
+ "err.projectDirGone": "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728",
153
+ "err.terminalMissing": "\u7EC8\u7AEF\u4E0D\u5B58\u5728",
154
+ "err.noManager": "\u4F1A\u8BDD\u7BA1\u7406\u5668\u672A\u542F\u52A8",
155
+ "err.badParams": "\u53C2\u6570\u4E0D\u5BF9\uFF1A{detail}",
156
+ "export.me": "\u6211",
157
+ "export.steered": "\uFF08\u63D2\u8BDD\uFF09",
158
+ "export.reasoning": "\u601D\u8003",
159
+ "export.plan": "\u8BA1\u5212",
160
+ "export.todo": "\u5F85\u529E",
161
+ "err.requestGone": "\u6388\u6743/\u63D0\u95EE\u4E0D\u5B58\u5728\u6216\u5DF2\u56DE\u5E94",
162
+ "err.badOption": "optionId \u975E\u6CD5\uFF1A{id}",
163
+ "err.cursorOnly": "\u4EC5 Cursor \u5BF9\u8BDD\u652F\u6301\u8BE5\u9009\u9879",
164
+ "err.busySwitch": "\u5BF9\u8BDD\u8FD0\u884C\u4E2D\uFF0C\u4E0D\u80FD\u66F4\u6362 Agent",
165
+ "err.hasMessages": "\u5DF2\u6709\u6D88\u606F\uFF0C\u4E0D\u80FD\u66F4\u6362 Agent",
166
+ "tool.read": "\u8BFB\u53D6 {x}",
167
+ "tool.edit": "\u7F16\u8F91 {x}",
168
+ "tool.multiEdit": "\u7F16\u8F91 {x}\uFF08{n} \u5904\uFF09",
169
+ "tool.overwrite": "\u8986\u76D6 {x}",
170
+ "tool.create": "\u65B0\u5EFA {x}",
171
+ "tool.notebook": "\u7F16\u8F91\u7B14\u8BB0\u672C {x}",
172
+ "tool.run": "\u8FD0\u884C {x}",
173
+ "tool.output": "\u67E5\u770B\u8F93\u51FA {x}",
174
+ "tool.kill": "\u7ED3\u675F\u8FDB\u7A0B {x}",
175
+ "tool.grep": '\u641C\u7D22 "{x}"',
176
+ "tool.glob": "\u67E5\u627E {x}",
177
+ "tool.fetch": "\u6253\u5F00 {x}",
178
+ "tool.webSearch": '\u641C\u7D22\u7F51\u9875 "{x}"',
179
+ "tool.todo": "\u66F4\u65B0\u5F85\u529E\uFF08{done}/{n}\uFF09",
180
+ "tool.task": "\u5B50\u4EFB\u52A1\uFF1A{x}",
181
+ "tool.generic": "\u5DE5\u5177",
182
+ "tool.editFiles": "\u4FEE\u6539 {n} \u4E2A\u6587\u4EF6",
183
+ "tool.needsApproval": "\u9700\u8981\u6388\u6743",
184
+ "perm.editFiles": "\u4FEE\u6539\u6587\u4EF6",
185
+ "perm.allow": "\u5141\u8BB8",
186
+ "perm.allowSession": "\u672C\u5BF9\u8BDD\u90FD\u5141\u8BB8",
187
+ "perm.deny": "\u62D2\u7EDD",
188
+ "perm.plan": "Agent \u63D0\u4EA4\u4E86\u8BA1\u5212",
189
+ "perm.implement": "\u5B9E\u65BD",
190
+ "http.internal": "\u670D\u52A1\u5185\u90E8\u9519\u8BEF",
191
+ "http.badOrigin": "\u6765\u6E90\u4E0D\u5141\u8BB8",
192
+ "http.badPreflight": "\u9884\u68C0\u4E0D\u5141\u8BB8",
193
+ "http.badToken": "\u4EE4\u724C\u65E0\u6548",
194
+ "http.noAttachment": "\u9644\u4EF6\u4E0D\u5B58\u5728",
195
+ "http.method": "\u65B9\u6CD5\u4E0D\u5141\u8BB8",
196
+ "http.noApp": "app \u8FD8\u6CA1\u6784\u5EFA\uFF08\u7F3A app/index.html\uFF09",
197
+ "http.badPath": "\u8DEF\u5F84\u4E0D\u5408\u6CD5",
198
+ "http.pathDenied": "\u8DEF\u5F84\u4E0D\u5141\u8BB8",
199
+ "http.noFile": "\u6587\u4EF6\u4E0D\u5B58\u5728",
200
+ "http.badType": "\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B",
201
+ "http.tooLarge": "\u6587\u4EF6\u592A\u5927\uFF08\u4E0A\u9650 50MB\uFF09",
202
+ "http.noRoute": "\u63A5\u53E3\u4E0D\u5B58\u5728",
203
+ "codex.error": "Codex \u9519\u8BEF",
204
+ "codex.closed": "\u4F1A\u8BDD\u5DF2\u5173\u95ED",
205
+ "codex.noTurnSteer": "\u5F53\u524D\u8F6E\u6B21\u8FD8\u6CA1\u5F00\u59CB\uFF0C\u65E0\u6CD5\u63D2\u8BDD",
206
+ "acp.exited": "{name} \u8FDB\u7A0B\u9000\u51FA ({code})",
207
+ "notice.apiRetry": "API \u91CD\u8BD5\u4E2D\uFF08\u7B2C {n} \u6B21\uFF09",
208
+ "notice.compacted": "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29",
209
+ "term.max": "\u6BCF\u4E2A\u9879\u76EE\u6700\u591A {n} \u4E2A\u7EC8\u7AEF",
210
+ "term.title": "\u7EC8\u7AEF {n}",
211
+ "stub.planned": "\u8BE5 Agent \u5C1A\u672A\u5728 Agent Console \u63A5\u5165\uFF0C\u656C\u8BF7\u671F\u5F85",
212
+ "stub.unverified": "\u5DF2\u68C0\u6D4B\u5230 CLI\uFF0C\u4F1A\u8BDD\u9A71\u52A8\u5C1A\u672A\u5B8C\u5168\u9A8C\u8BC1",
213
+ "stub.notFound": "\u672A\u627E\u5230 {bin}",
214
+ "history.untitled": "\uFF08\u65E0\u6807\u9898\uFF09",
215
+ "history.toolCall": "\uFF08\u5386\u53F2\uFF09\u8C03\u7528 {name}",
216
+ "fs.pathDenied": "\u8DEF\u5F84\u4E0D\u5141\u8BB8",
217
+ "fs.dirMissing": "\u76EE\u5F55\u4E0D\u5B58\u5728",
218
+ "fs.dirUnreadable": "\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55",
219
+ "fs.fileMissing": "\u6587\u4EF6\u4E0D\u5B58\u5728",
220
+ "fs.notFile": "\u4E0D\u662F\u6587\u4EF6",
221
+ "fs.unreadable": "\u65E0\u6CD5\u8BFB\u53D6",
222
+ "git.missing": "\u627E\u4E0D\u5230 git\uFF08\u8BF7\u5148\u5B89\u88C5 git \u5E76\u52A0\u5165 PATH\uFF09",
223
+ "git.binary": "\u4E8C\u8FDB\u5236\u6216\u65E0\u6CD5\u8BFB\u53D6"
224
+ },
225
+ en: {
226
+ "thread.defaultTitle": "New chat",
227
+ "notice.daemonRestart": "Daemon restarted \u2014 this turn was interrupted",
228
+ "notice.sessionStartFailed": "Session start failed: {detail}",
229
+ "notice.sendFailed": "Send failed: {detail}",
230
+ "notice.effortNextTurn": "Reasoning effort saved \u2014 takes effect on the next turn",
231
+ "notice.effortResetOnModel": "This model does not support that effort level; reset to Default",
232
+ "turn.agentExit": "Agent process exited unexpectedly",
233
+ "attachment.missing": "Attachment missing or expired ({ref}) \u2014 upload again",
234
+ "notice.acpImageByPath": "{agent} can't see images directly (its ACP initialize doesn't advertise image input); sent the image file path instead: {paths}",
235
+ "notice.acpImageByPathSwitched": "{agent} was started on another model and takes image blocks only for that one, so the image went as its file path (it opens it with read_image when this model sees images): {paths}",
236
+ "notice.acpSessionNotRestored": "{agent} can't restore the previous conversation (its ACP advertises neither session/load nor session/resume); started a new one \u2014 prior context won't come back.",
237
+ "notice.attachmentIssue": "Attachment issue: {detail}",
238
+ "claude.notFound": "claude was not found ({detail}). Claude Code is not bundled with this plugin; install it first: {hint}",
239
+ "claude.startFailed": "Could not start claude: {detail}",
240
+ "claude.exited": "claude exited unexpectedly ({code})",
241
+ "claude.notRunning": "claude is not running; the message was not sent",
242
+ "claude.closed": "The session is closed",
243
+ "export.defaultModel": "Default",
244
+ "bin.notOnPath": "{name} was not found on PATH",
245
+ "bin.or": " or ",
246
+ "launch.notFound": "Custom executable not found: {cmd} (use an absolute path or a command name on PATH)",
247
+ "codex.notFound": "codex was not found ({detail}). Install it first: {hint}",
248
+ "provider.disabled": "Disabled in Settings",
249
+ "provider.planned": "Not supported yet",
250
+ "provider.disabledNamed": "{name} is disabled in Settings",
251
+ "provider.plannedNamed": "{name} is not supported yet; it can't start new conversations",
252
+ "provider.unavailable": "{name} is not installed or not available",
253
+ "provider.unknown": "Unknown agent: {id}",
254
+ "provider.timeout": "Detection timed out",
255
+ "err.projectMissing": "Project not found",
256
+ "err.threadMissing": "Conversation not found",
257
+ "err.dirMissing": "The folder does not exist or is not a folder",
258
+ "err.projectDirGone": "The project folder no longer exists",
259
+ "err.terminalMissing": "Terminal not found",
260
+ "err.noManager": "The session manager is not running",
261
+ "err.badParams": "Invalid parameters: {detail}",
262
+ "export.me": "Me",
263
+ "export.steered": " (steer)",
264
+ "export.reasoning": "Reasoning",
265
+ "export.plan": "Plan",
266
+ "export.todo": "Todos",
267
+ "err.requestGone": "That approval or question no longer exists or was already answered",
268
+ "err.badOption": "Invalid optionId: {id}",
269
+ "err.cursorOnly": "Only Cursor conversations support this option",
270
+ "err.busySwitch": "The conversation is running; the agent can't be changed",
271
+ "err.hasMessages": "The conversation already has messages; the agent can't be changed",
272
+ "tool.read": "Read {x}",
273
+ "tool.edit": "Edit {x}",
274
+ "tool.multiEdit": "Edit {x} ({n} edits)",
275
+ "tool.overwrite": "Overwrite {x}",
276
+ "tool.create": "Create {x}",
277
+ "tool.notebook": "Edit notebook {x}",
278
+ "tool.run": "Run {x}",
279
+ "tool.output": "Read output {x}",
280
+ "tool.kill": "Stop process {x}",
281
+ "tool.grep": 'Search "{x}"',
282
+ "tool.glob": "Find {x}",
283
+ "tool.fetch": "Open {x}",
284
+ "tool.webSearch": 'Web search "{x}"',
285
+ "tool.todo": "Update todos ({done}/{n})",
286
+ "tool.task": "Subtask: {x}",
287
+ "tool.generic": "Tool",
288
+ "tool.editFiles": "Edit {n} file(s)",
289
+ "tool.needsApproval": "Needs approval",
290
+ "perm.editFiles": "Edit files",
291
+ "perm.allow": "Allow",
292
+ "perm.allowSession": "Allow for this chat",
293
+ "perm.deny": "Deny",
294
+ "perm.plan": "The agent proposed a plan",
295
+ "perm.implement": "Implement",
296
+ "http.internal": "Internal error",
297
+ "http.badOrigin": "Origin not allowed",
298
+ "http.badPreflight": "Preflight not allowed",
299
+ "http.badToken": "Invalid token",
300
+ "http.noAttachment": "Attachment not found",
301
+ "http.method": "Method not allowed",
302
+ "http.noApp": "The app is not built (app/index.html is missing)",
303
+ "http.badPath": "Invalid path",
304
+ "http.pathDenied": "Path not allowed",
305
+ "http.noFile": "File not found",
306
+ "http.badType": "Unsupported file type",
307
+ "http.tooLarge": "File too large (50 MB max)",
308
+ "http.noRoute": "No such endpoint",
309
+ "codex.error": "Codex error",
310
+ "codex.closed": "The session is closed",
311
+ "codex.noTurnSteer": "The turn hasn't started yet; can't steer",
312
+ "acp.exited": "{name} exited ({code})",
313
+ "notice.apiRetry": "Retrying the API (attempt {n})",
314
+ "notice.compacted": "Context compacted",
315
+ "term.max": "At most {n} terminals per project",
316
+ "term.title": "Terminal {n}",
317
+ "stub.planned": "This agent isn't supported in Agent Console yet",
318
+ "stub.unverified": "CLI found; its session driver isn't fully verified yet",
319
+ "stub.notFound": "{bin} was not found",
320
+ "history.untitled": "(untitled)",
321
+ "history.toolCall": "(history) called {name}",
322
+ "fs.pathDenied": "Path not allowed",
323
+ "fs.dirMissing": "Folder not found",
324
+ "fs.dirUnreadable": "Can't read the folder",
325
+ "fs.fileMissing": "File not found",
326
+ "fs.notFile": "Not a file",
327
+ "fs.unreadable": "Can't read the file",
328
+ "git.missing": "git was not found (install git and add it to PATH)",
329
+ "git.binary": "Binary or unreadable"
330
+ }
331
+ };
69
332
  }
70
333
  });
71
334
 
@@ -94,8 +357,8 @@ function normPath(p, platform = process.platform) {
94
357
  function samePath(a, b) {
95
358
  return normPath(a) === normPath(b);
96
359
  }
97
- function validId(id) {
98
- return typeof id === "string" && ID_RE.test(id);
360
+ function validId(id2) {
361
+ return typeof id2 === "string" && ID_RE.test(id2);
99
362
  }
100
363
  var ID_RE;
101
364
  var init_paths = __esm({
@@ -394,9 +657,9 @@ async function scriptPerm(ctx) {
394
657
  kind: "tool",
395
658
  detail: { command: "npm test", cwd: ".", toolName: "Bash" },
396
659
  options: [
397
- { id: "accept", label: "\u5141\u8BB8", kind: "allow_once" },
398
- { id: "acceptForSession", label: "\u672C\u5BF9\u8BDD\u90FD\u5141\u8BB8", kind: "allow_always" },
399
- { id: "decline", label: "\u62D2\u7EDD", kind: "reject_once" }
660
+ { id: "accept", label: tr("perm.allow"), kind: "allow_once" },
661
+ { id: "acceptForSession", label: tr("perm.allowSession"), kind: "allow_always" },
662
+ { id: "decline", label: tr("perm.deny"), kind: "reject_once" }
400
663
  ]
401
664
  });
402
665
  const ans = await ctx.waitFor(requestId);
@@ -431,7 +694,7 @@ async function scriptPlan(ctx) {
431
694
  detail: { markdown: "\u89C1\u4E0A\u65B9\u8BA1\u5212" },
432
695
  options: [
433
696
  { id: "implement", label: "\u5F00\u59CB\u5B9E\u65BD", kind: "allow_once" },
434
- { id: "reject", label: "\u62D2\u7EDD", kind: "reject_once" }
697
+ { id: "reject", label: tr("perm.deny"), kind: "reject_once" }
435
698
  ]
436
699
  });
437
700
  const ans = await ctx.waitFor(requestId);
@@ -475,10 +738,10 @@ async function scriptAsk(ctx) {
475
738
  async function scriptSlow(ctx) {
476
739
  const { emit } = ctx;
477
740
  for (let i = 1; i <= 6; i++) {
478
- const id = "s" + i;
479
- emit({ type: "tool_call", itemId: id, kind: "bash", title: `\u6B65\u9AA4 ${i}/6`, status: "running", input: { command: `step ${i}` } });
741
+ const id2 = "s" + i;
742
+ emit({ type: "tool_call", itemId: id2, kind: "bash", title: `\u6B65\u9AA4 ${i}/6`, status: "running", input: { command: `step ${i}` } });
480
743
  if (await sleepAbortable(2e3, ctx)) return;
481
- emit({ type: "tool_call", itemId: id, kind: "bash", title: `\u6B65\u9AA4 ${i}/6`, status: "ok", output: `\u6B65\u9AA4 ${i} \u5B8C\u6210`, durationMs: 2e3 });
744
+ emit({ type: "tool_call", itemId: id2, kind: "bash", title: `\u6B65\u9AA4 ${i}/6`, status: "ok", output: `\u6B65\u9AA4 ${i} \u5B8C\u6210`, durationMs: 2e3 });
482
745
  }
483
746
  emit({ type: "assistant_message", itemId: "sm", text: "6 \u4E2A\u6B65\u9AA4\u5168\u90E8\u8DD1\u5B8C\u3002" });
484
747
  }
@@ -572,6 +835,7 @@ function createMockProvider() {
572
835
  var mockCapabilities, rid, sleep, MockCrash, MockSession, mockProviderMetrics;
573
836
  var init_mock = __esm({
574
837
  "daemon/src/providers/mock.ts"() {
838
+ init_i18n();
575
839
  init_queue();
576
840
  init_diff();
577
841
  init_slash();
@@ -639,7 +903,7 @@ var init_mock = __esm({
639
903
  if (e.itemId) e.itemId = p + e.itemId;
640
904
  this.emit(ev);
641
905
  },
642
- waitFor: (id) => this.waitFor(id),
906
+ waitFor: (id2) => this.waitFor(id2),
643
907
  interrupted: () => this.interruptFlag || this.closed,
644
908
  steerText: () => this.steered.join("\uFF1B"),
645
909
  replyPrefix: () => this.resumePrefix(),
@@ -680,9 +944,9 @@ var init_mock = __esm({
680
944
  }
681
945
  async interrupt() {
682
946
  this.interruptFlag = true;
683
- for (const [id, resolve2] of this.pendingReq) {
947
+ for (const [id2, resolve2] of this.pendingReq) {
684
948
  resolve2({ cancelled: true });
685
- this.pendingReq.delete(id);
949
+ this.pendingReq.delete(id2);
686
950
  }
687
951
  await sleep(120);
688
952
  }
@@ -692,8 +956,8 @@ var init_mock = __esm({
692
956
  async setModel(model) {
693
957
  this.o.model = model;
694
958
  }
695
- async setMode(mode) {
696
- this.o.mode = mode;
959
+ async setMode(mode2) {
960
+ this.o.mode = mode2;
697
961
  }
698
962
  async setEffort(effort) {
699
963
  this.o.effort = effort;
@@ -924,7 +1188,7 @@ async function resolveFirstBin(names) {
924
1188
  if (r.ok) return { ...r, binName };
925
1189
  }
926
1190
  const label = uniq[0] ?? "cli";
927
- return { ok: false, reason: `PATH \u91CC\u627E\u4E0D\u5230 ${uniq.length > 1 ? uniq.join(" \u6216 ") : label}` };
1191
+ return { ok: false, reason: tr("bin.notOnPath", { name: uniq.length > 1 ? uniq.join(tr("bin.or")) : label }) };
928
1192
  }
929
1193
  function clearBinCache(name) {
930
1194
  if (name) cache.delete(name);
@@ -939,7 +1203,7 @@ async function resolveBinWith(name, deps = {}) {
939
1203
  const P = win ? path2.win32 : path2.posix;
940
1204
  const exists = deps.exists ?? existsSync4;
941
1205
  const hits = (deps.find ?? which)(name);
942
- if (!hits.length) return { ok: false, reason: `PATH \u91CC\u627E\u4E0D\u5230 ${name}` };
1206
+ if (!hits.length) return { ok: false, reason: tr("bin.notOnPath", { name }) };
943
1207
  const first = hits[0];
944
1208
  if (!win) return { ok: true, cmd: first, prefixArgs: [], display: first, via: "path" };
945
1209
  const ext = P.extname(first).toLowerCase();
@@ -1031,6 +1295,7 @@ var IS_WIN, cache, CACHE_MS, CMD_META, liveTrees, exitHookInstalled;
1031
1295
  var init_bin = __esm({
1032
1296
  "daemon/src/util/bin.ts"() {
1033
1297
  init_pathscan();
1298
+ init_i18n();
1034
1299
  IS_WIN = process.platform === "win32";
1035
1300
  cache = /* @__PURE__ */ new Map();
1036
1301
  CACHE_MS = 60 * 1e3;
@@ -1058,44 +1323,44 @@ function callInfo(name, input = {}, cwd = "") {
1058
1323
  const file = String(input.file_path ?? input.path ?? input.notebook_path ?? "");
1059
1324
  switch (t) {
1060
1325
  case "Read":
1061
- return f("read", `\u8BFB\u53D6 ${relPath(file, cwd)}`, { path: file, offset: input.offset, limit: input.limit });
1326
+ return f("read", tr("tool.read", { x: relPath(file, cwd) }), { path: file, offset: input.offset, limit: input.limit });
1062
1327
  case "Edit":
1063
- return f("edit", `\u7F16\u8F91 ${relPath(file, cwd)}`, { path: file, old: input.old_string, new: input.new_string });
1328
+ return f("edit", tr("tool.edit", { x: relPath(file, cwd) }), { path: file, old: input.old_string, new: input.new_string });
1064
1329
  case "MultiEdit":
1065
- return f("edit", `\u7F16\u8F91 ${relPath(file, cwd)}\uFF08${(input.edits || []).length} \u5904\uFF09`, { path: file, edits: input.edits ?? [] });
1330
+ return f("edit", tr("tool.multiEdit", { x: relPath(file, cwd), n: (input.edits || []).length }), { path: file, edits: input.edits ?? [] });
1066
1331
  case "Write":
1067
- return f("write", `${input._exists ? "\u8986\u76D6" : "\u65B0\u5EFA"} ${relPath(file, cwd)}`, { path: file, content: input.content });
1332
+ return f("write", tr(input._exists ? "tool.overwrite" : "tool.create", { x: relPath(file, cwd) }), { path: file, content: input.content });
1068
1333
  case "NotebookEdit":
1069
- return f("edit", `\u7F16\u8F91\u7B14\u8BB0\u672C ${relPath(String(input.notebook_path || file), cwd)}`, { path: input.notebook_path ?? file });
1334
+ return f("edit", tr("tool.notebook", { x: relPath(String(input.notebook_path || file), cwd) }), { path: input.notebook_path ?? file });
1070
1335
  case "Bash":
1071
- return f("bash", `\u8FD0\u884C ${trunc(String(input.command ?? ""))}`, { command: input.command, description: input.description });
1336
+ return f("bash", tr("tool.run", { x: trunc(String(input.command ?? "")) }), { command: input.command, description: input.description });
1072
1337
  case "BashOutput":
1073
- return f("bash", `\u67E5\u770B\u8F93\u51FA ${String(input.bash_id ?? "")}`, { bashId: input.bash_id });
1338
+ return f("bash", tr("tool.output", { x: String(input.bash_id ?? "") }), { bashId: input.bash_id });
1074
1339
  case "KillShell":
1075
- return f("bash", `\u7ED3\u675F\u8FDB\u7A0B ${String(input.shell_id ?? "")}`, { shellId: input.shell_id });
1340
+ return f("bash", tr("tool.kill", { x: String(input.shell_id ?? "") }), { shellId: input.shell_id });
1076
1341
  case "Grep":
1077
- return f("search", `\u641C\u7D22 "${trunc(String(input.pattern ?? ""), 40)}"`, { pattern: input.pattern, path: input.path, glob: input.glob });
1342
+ return f("search", tr("tool.grep", { x: trunc(String(input.pattern ?? ""), 40) }), { pattern: input.pattern, path: input.path, glob: input.glob });
1078
1343
  case "Glob":
1079
1344
  case "LS":
1080
- return f("glob", `\u67E5\u627E ${trunc(String(input.pattern ?? input.path ?? ""), 40)}`, { pattern: input.pattern, path: input.path });
1345
+ return f("glob", tr("tool.glob", { x: trunc(String(input.pattern ?? input.path ?? ""), 40) }), { pattern: input.pattern, path: input.path });
1081
1346
  case "WebFetch":
1082
- return f("web", `\u6253\u5F00 ${hostOf(String(input.url ?? ""))}`, { url: input.url });
1347
+ return f("web", tr("tool.fetch", { x: hostOf(String(input.url ?? "")) }), { url: input.url });
1083
1348
  case "WebSearch":
1084
- return f("web", `\u641C\u7D22\u7F51\u9875 "${trunc(String(input.query ?? ""), 40)}"`, { query: input.query });
1349
+ return f("web", tr("tool.webSearch", { x: trunc(String(input.query ?? ""), 40) }), { query: input.query });
1085
1350
  case "TodoWrite": {
1086
1351
  const items = todoItems(input.todos ?? []);
1087
1352
  const done = items.filter((x) => x.status === "done").length;
1088
- return f("todo", `\u66F4\u65B0\u5F85\u529E\uFF08${done}/${items.length}\uFF09`, { items });
1353
+ return f("todo", tr("tool.todo", { done, n: items.length }), { items });
1089
1354
  }
1090
1355
  case "Task":
1091
1356
  case "Agent":
1092
- return f("task", `\u5B50\u4EFB\u52A1\uFF1A${trunc(String(input.description ?? input.prompt ?? ""), 40)}`, { description: input.description });
1357
+ return f("task", tr("tool.task", { x: trunc(String(input.description ?? input.prompt ?? ""), 40) }), { description: input.description });
1093
1358
  default:
1094
1359
  break;
1095
1360
  }
1096
1361
  const m = /^mcp__([^_]+)__(.+)$/.exec(t);
1097
1362
  if (m) return f("mcp", `${m[1]} \xB7 ${m[2]}`, { server: m[1], tool: m[2] });
1098
- return f("other", t || "\u5DE5\u5177", {});
1363
+ return f("other", t || tr("tool.generic"), {});
1099
1364
  }
1100
1365
  function hostOf(url) {
1101
1366
  try {
@@ -1113,6 +1378,7 @@ function todoItems(todos = []) {
1113
1378
  var MAX_CMD, trunc;
1114
1379
  var init_kinds = __esm({
1115
1380
  "daemon/src/agents/kinds.ts"() {
1381
+ init_i18n();
1116
1382
  MAX_CMD = 80;
1117
1383
  trunc = (s, n = MAX_CMD) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
1118
1384
  }
@@ -1126,9 +1392,14 @@ function itemIdFor(st, blockIdx, blkType) {
1126
1392
  const k = blockKind(blkType);
1127
1393
  const n = st.typeSeq.get(k) ?? 0;
1128
1394
  st.typeSeq.set(k, n + 1);
1129
- const id = `${st.msgId}:${k}${n}`;
1130
- st.itemIdByBlock.set(blockIdx, id);
1131
- return id;
1395
+ const id2 = `${st.msgId}:${k}${n}`;
1396
+ st.itemIdByBlock.set(blockIdx, id2);
1397
+ return id2;
1398
+ }
1399
+ function costIsReal(modelUsage) {
1400
+ if (!modelUsage || typeof modelUsage !== "object") return true;
1401
+ const names = Object.keys(modelUsage);
1402
+ return names.every((n) => /claude|opus|sonnet|haiku/i.test(n));
1132
1403
  }
1133
1404
  function mapSdkMessage(m, st, cwd) {
1134
1405
  const out = [];
@@ -1140,15 +1411,15 @@ function mapSdkMessage(m, st, cwd) {
1140
1411
  if (sid) out.push({ type: "provider_session_id", id: sid });
1141
1412
  }
1142
1413
  if (r.subtype === "api_retry") {
1143
- out.push({ type: "notice", level: "warn", message: `API \u91CD\u8BD5\u4E2D\uFF08\u7B2C ${r.attempt ?? "?"} \u6B21\uFF09` });
1414
+ out.push({ type: "notice", level: "warn", message: tr("notice.apiRetry", { n: String(r.attempt ?? "?") }) });
1144
1415
  }
1145
1416
  if (r.subtype === "compact_boundary") {
1146
- out.push({ type: "notice", level: "info", message: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" });
1417
+ out.push({ type: "notice", level: "info", message: tr("notice.compacted") });
1147
1418
  }
1148
1419
  return out;
1149
1420
  }
1150
1421
  if (r.type === "stream_event") {
1151
- const e = rec(m.event);
1422
+ const e = rec(r.event);
1152
1423
  if (e.type === "message_start") {
1153
1424
  const mid = str(rec(e.message).id);
1154
1425
  if (mid) st.msgId = mid;
@@ -1227,7 +1498,7 @@ function mapSdkMessage(m, st, cwd) {
1227
1498
  inputTokens: num("input_tokens"),
1228
1499
  outputTokens: num("output_tokens"),
1229
1500
  cacheReadTokens: num("cache_read_input_tokens"),
1230
- costUsd: typeof r.total_cost_usd === "number" ? r.total_cost_usd : 0
1501
+ costUsd: typeof r.total_cost_usd === "number" && costIsReal(r.modelUsage) ? r.total_cost_usd : 0
1231
1502
  });
1232
1503
  const subtype = str(r.subtype) ?? "";
1233
1504
  const interrupted = st.sawInterrupt || /interrupt/i.test(subtype);
@@ -1249,7 +1520,7 @@ function mapSdkMessage(m, st, cwd) {
1249
1520
  function mapToolUse(blk, cwd) {
1250
1521
  const input = rec(blk.input);
1251
1522
  const name = str(blk.name) ?? "tool";
1252
- const id = str(blk.id) ?? "";
1523
+ const id2 = str(blk.id) ?? "";
1253
1524
  const info = callInfo(name, { ...input, _exists: true }, cwd);
1254
1525
  let detail = info.detail;
1255
1526
  if (info.kind === "edit" && (input.old_string !== void 0 || Array.isArray(input.edits))) {
@@ -1262,7 +1533,7 @@ function mapToolUse(blk, cwd) {
1262
1533
  const out = [
1263
1534
  {
1264
1535
  type: "tool_call",
1265
- itemId: id,
1536
+ itemId: id2,
1266
1537
  kind: info.kind,
1267
1538
  title: info.title,
1268
1539
  status: "running",
@@ -1285,6 +1556,7 @@ var init_map = __esm({
1285
1556
  "daemon/src/providers/claude/map.ts"() {
1286
1557
  init_kinds();
1287
1558
  init_diff();
1559
+ init_i18n();
1288
1560
  MAX_OUTPUT = 20 * 1024;
1289
1561
  ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g;
1290
1562
  clean = (s) => String(s ?? "").replace(ANSI_RE, "");
@@ -1294,54 +1566,6 @@ var init_map = __esm({
1294
1566
  }
1295
1567
  });
1296
1568
 
1297
- // daemon/src/util/i18n.ts
1298
- function normalizeLang(raw) {
1299
- const s = String(raw ?? "").trim().toLowerCase();
1300
- if (!s) return "zh";
1301
- if (s === "zh" || s.startsWith("zh-")) return "zh";
1302
- return "en";
1303
- }
1304
- function setAppLocale(lang) {
1305
- locale = normalizeLang(lang);
1306
- }
1307
- function tr(key, params) {
1308
- let s = dict[locale][key] ?? dict.en[key] ?? key;
1309
- if (params) {
1310
- for (const [k, v] of Object.entries(params)) s = s.replaceAll(`{${k}}`, String(v));
1311
- }
1312
- return s;
1313
- }
1314
- var locale, dict;
1315
- var init_i18n = __esm({
1316
- "daemon/src/util/i18n.ts"() {
1317
- locale = "zh";
1318
- dict = {
1319
- zh: {
1320
- "thread.defaultTitle": "\u65B0\u5BF9\u8BDD",
1321
- "notice.daemonRestart": "\u540E\u53F0\u5DF2\u91CD\u542F\uFF0C\u672C\u8F6E\u5DF2\u4E2D\u65AD",
1322
- "notice.sessionStartFailed": "\u4F1A\u8BDD\u542F\u52A8\u5931\u8D25\uFF1A{detail}",
1323
- "notice.sendFailed": "\u53D1\u9001\u5931\u8D25\uFF1A{detail}",
1324
- "notice.effortNextTurn": "\u601D\u8003\u5F3A\u5EA6\u5DF2\u4FDD\u5B58\uFF0C\u5C06\u5728\u4E0B\u4E00\u8F6E\u751F\u6548",
1325
- "notice.effortResetOnModel": "\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301\u8BE5\u601D\u8003\u5F3A\u5EA6\uFF0C\u5DF2\u6062\u590D\u4E3A Default",
1326
- "turn.agentExit": "Agent \u8FDB\u7A0B\u610F\u5916\u9000\u51FA",
1327
- "attachment.missing": "\u9644\u4EF6\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548\uFF08{ref}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u4E0A\u4F20",
1328
- "export.defaultModel": "\u9ED8\u8BA4"
1329
- },
1330
- en: {
1331
- "thread.defaultTitle": "New chat",
1332
- "notice.daemonRestart": "Daemon restarted \u2014 this turn was interrupted",
1333
- "notice.sessionStartFailed": "Session start failed: {detail}",
1334
- "notice.sendFailed": "Send failed: {detail}",
1335
- "notice.effortNextTurn": "Reasoning effort saved \u2014 takes effect on the next turn",
1336
- "notice.effortResetOnModel": "This model does not support that effort level; reset to Default",
1337
- "turn.agentExit": "Agent process exited unexpectedly",
1338
- "attachment.missing": "Attachment missing or expired ({ref}) \u2014 upload again",
1339
- "export.defaultModel": "Default"
1340
- }
1341
- };
1342
- }
1343
- });
1344
-
1345
1569
  // daemon/src/util/attachmentRefs.ts
1346
1570
  var attachmentRefs_exports = {};
1347
1571
  __export(attachmentRefs_exports, {
@@ -1395,18 +1619,322 @@ var init_attachmentRefs = __esm({
1395
1619
  }
1396
1620
  });
1397
1621
 
1622
+ // daemon/src/util/attachmentKinds.ts
1623
+ function baseMime(mime) {
1624
+ return String(mime ?? "").split(";")[0].trim().toLowerCase();
1625
+ }
1626
+ function attachmentKind(mime) {
1627
+ const m = baseMime(mime);
1628
+ if (MODEL_IMAGE_MIMES.has(m)) return "image";
1629
+ if (m.startsWith("text/") || TEXT_APP.has(m)) return "text";
1630
+ return "other";
1631
+ }
1632
+ function pathNote(kind, file) {
1633
+ return `${kind === "image" ? "Attached image" : "Attached file"}: ${file}`;
1634
+ }
1635
+ var MODEL_IMAGE_MIMES, TEXT_APP;
1636
+ var init_attachmentKinds = __esm({
1637
+ "daemon/src/util/attachmentKinds.ts"() {
1638
+ MODEL_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
1639
+ TEXT_APP = /* @__PURE__ */ new Set(["application/json", "application/xml", "application/yaml", "application/x-yaml", "application/toml", "application/javascript"]);
1640
+ }
1641
+ });
1642
+
1643
+ // daemon/src/providers/claude/content.ts
1644
+ import { readFile as readFile4, stat as stat3 } from "node:fs/promises";
1645
+ import { basename } from "node:path";
1646
+ async function buildClaudeContent(text, attachments) {
1647
+ const media = [];
1648
+ const notes = [];
1649
+ const warnings = [];
1650
+ for (const a of attachments) {
1651
+ let file;
1652
+ let size;
1653
+ try {
1654
+ file = await resolveImagePath(a.path);
1655
+ size = (await stat3(file)).size;
1656
+ } catch (e) {
1657
+ warnings.push(`${a.path}: ${String(e?.message ?? e)}`);
1658
+ continue;
1659
+ }
1660
+ const kind = attachmentKind(a.mime);
1661
+ try {
1662
+ if (kind === "image" && size <= CLAUDE_MAX_IMAGE_BYTES) {
1663
+ const data = (await readFile4(file)).toString("base64");
1664
+ media.push({ type: "image", source: { type: "base64", media_type: baseMime(a.mime), data } });
1665
+ continue;
1666
+ }
1667
+ if (kind === "text" && size <= CLAUDE_MAX_TEXT_BYTES) {
1668
+ const data = await readFile4(file, "utf8");
1669
+ media.push({ type: "document", source: { type: "text", media_type: "text/plain", data }, title: basename(file), context: file });
1670
+ continue;
1671
+ }
1672
+ } catch (e) {
1673
+ warnings.push(`${basename(file)}: ${String(e?.message ?? e)}`);
1674
+ }
1675
+ notes.push(pathNote(kind, file));
1676
+ if (kind === "other") warnings.push(`${basename(file)} (${baseMime(a.mime) || "unknown type"}) is not an image or a text file; sent its path`);
1677
+ else if (kind === "image" && size > CLAUDE_MAX_IMAGE_BYTES) warnings.push(`${basename(file)}: ${(size / 1024 / 1024).toFixed(1)} MB, over Claude's 5 MB image limit; sent its path`);
1678
+ else if (kind === "text" && size > CLAUDE_MAX_TEXT_BYTES) warnings.push(`${basename(file)}: ${(size / 1024 / 1024).toFixed(1)} MB, too large to attach inline; sent its path`);
1679
+ }
1680
+ const fullText = [text, ...notes.length ? [notes.join("\n")] : []].filter(Boolean).join("\n\n");
1681
+ return { blocks: [...media, { type: "text", text: fullText }], warnings };
1682
+ }
1683
+ var CLAUDE_MAX_IMAGE_BYTES, CLAUDE_MAX_TEXT_BYTES;
1684
+ var init_content = __esm({
1685
+ "daemon/src/providers/claude/content.ts"() {
1686
+ init_attachmentRefs();
1687
+ init_attachmentKinds();
1688
+ CLAUDE_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
1689
+ CLAUDE_MAX_TEXT_BYTES = 1024 * 1024;
1690
+ }
1691
+ });
1692
+
1693
+ // daemon/src/providers/launch.ts
1694
+ function launchFromSettings(settings, providerId) {
1695
+ const c = settings.providerCommand?.[providerId];
1696
+ const cmd = typeof c?.executable === "string" ? c.executable.trim() : "";
1697
+ if (!cmd) return void 0;
1698
+ const args = Array.isArray(c?.args) ? c.args.filter((a) => typeof a === "string") : [];
1699
+ return { cmd, args };
1700
+ }
1701
+ function launchArgv(builtin, launch) {
1702
+ return [...launch?.prefixArgs ?? [], ...builtin, ...launch?.args ?? []];
1703
+ }
1704
+ async function resolveLaunch(launch) {
1705
+ const r = await resolveBin(launch.cmd);
1706
+ if (r.ok) return r;
1707
+ return { ok: false, reason: tr("launch.notFound", { cmd: launch.cmd }) };
1708
+ }
1709
+ function launchKey(launch) {
1710
+ return launch ? `${launch.cmd}\0${(launch.prefixArgs ?? []).join("\0")}\0\0${(launch.args ?? []).join("\0")}` : "";
1711
+ }
1712
+ var init_launch = __esm({
1713
+ "daemon/src/providers/launch.ts"() {
1714
+ init_bin();
1715
+ init_i18n();
1716
+ }
1717
+ });
1718
+
1719
+ // daemon/src/providers/claude/cli.ts
1720
+ function claudeArgs(o) {
1721
+ const args = [
1722
+ "-p",
1723
+ "--input-format",
1724
+ "stream-json",
1725
+ "--output-format",
1726
+ "stream-json",
1727
+ "--verbose",
1728
+ "--include-partial-messages",
1729
+ "--permission-prompt-tool",
1730
+ "stdio",
1731
+ "--setting-sources",
1732
+ "user,project,local"
1733
+ ];
1734
+ const put = (flag, v) => {
1735
+ if (!v) return;
1736
+ if (!SAFE_ARG.test(v)) throw new Error(`Refusing to pass an unexpected value to claude ${flag}: ${JSON.stringify(v).slice(0, 80)}`);
1737
+ args.push(flag, v);
1738
+ };
1739
+ put("--permission-mode", o.permissionMode);
1740
+ if (o.permissionMode === "bypassPermissions") args.push("--allow-dangerously-skip-permissions");
1741
+ put("--model", o.model);
1742
+ put("--effort", o.effort);
1743
+ put("--resume", o.resume);
1744
+ return args;
1745
+ }
1746
+ var SAFE_ARG, ClaudeCli;
1747
+ var init_cli = __esm({
1748
+ "daemon/src/providers/claude/cli.ts"() {
1749
+ init_launch();
1750
+ init_bin();
1751
+ init_queue();
1752
+ SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._:\[\]\/@+-]{0,199}$/;
1753
+ ClaudeCli = class {
1754
+ constructor(o, canUseTool) {
1755
+ this.o = o;
1756
+ this.canUseTool = canUseTool;
1757
+ const args = launchArgv(claudeArgs(o), o.launch);
1758
+ this.child = spawnBin(o.bin, args, {
1759
+ cwd: o.cwd,
1760
+ env: { ...o.env ?? process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
1761
+ stdio: ["pipe", "pipe", "pipe"],
1762
+ group: true
1763
+ });
1764
+ this.child.stdout?.setEncoding("utf8");
1765
+ this.child.stdout?.on("data", (d) => this.onData(d));
1766
+ this.child.stderr?.on("data", (d) => {
1767
+ this.stderrTail = (this.stderrTail + d.toString()).slice(-4096);
1768
+ });
1769
+ this.child.stdin?.on("error", () => {
1770
+ });
1771
+ this.exited = new Promise((resolve2) => {
1772
+ this.child.on("error", (e) => {
1773
+ this.spawnError = e;
1774
+ this.finish();
1775
+ resolve2({ code: null, signal: null });
1776
+ });
1777
+ this.child.on("close", (code, signal) => {
1778
+ this.finish();
1779
+ resolve2({ code, signal });
1780
+ });
1781
+ });
1782
+ this.initialized = this.request({ subtype: "initialize" }, 6e4).catch(() => null);
1783
+ }
1784
+ o;
1785
+ canUseTool;
1786
+ /** Conversation messages (everything that is not a control message) */
1787
+ messages = new AsyncQueue();
1788
+ exited;
1789
+ /** The answer to initialize (commands, models, account …), null when it failed */
1790
+ initialized;
1791
+ child;
1792
+ buf = "";
1793
+ seq = 0;
1794
+ pending = /* @__PURE__ */ new Map();
1795
+ cancels = /* @__PURE__ */ new Map();
1796
+ stderrTail = "";
1797
+ dead = false;
1798
+ spawnError = null;
1799
+ get stderr() {
1800
+ return this.stderrTail;
1801
+ }
1802
+ get failure() {
1803
+ return this.spawnError;
1804
+ }
1805
+ get alive() {
1806
+ return !this.dead;
1807
+ }
1808
+ finish() {
1809
+ if (this.dead) return;
1810
+ this.dead = true;
1811
+ for (const [, p] of this.pending) {
1812
+ if (p.timer) clearTimeout(p.timer);
1813
+ p.reject(new Error("claude exited"));
1814
+ }
1815
+ this.pending.clear();
1816
+ for (const [, c] of this.cancels) c.abort();
1817
+ this.cancels.clear();
1818
+ this.messages.close();
1819
+ }
1820
+ onData(d) {
1821
+ this.buf += d;
1822
+ let i;
1823
+ while ((i = this.buf.indexOf("\n")) >= 0) {
1824
+ const line = this.buf.slice(0, i).trim();
1825
+ this.buf = this.buf.slice(i + 1);
1826
+ if (!line) continue;
1827
+ let m;
1828
+ try {
1829
+ m = JSON.parse(line);
1830
+ } catch {
1831
+ continue;
1832
+ }
1833
+ if (m && typeof m === "object") this.onMessage(m);
1834
+ }
1835
+ }
1836
+ onMessage(m) {
1837
+ if (m.type === "control_response") {
1838
+ const r = m.response ?? {};
1839
+ const id2 = String(r.request_id ?? "");
1840
+ const p = this.pending.get(id2);
1841
+ if (!p) return;
1842
+ this.pending.delete(id2);
1843
+ if (p.timer) clearTimeout(p.timer);
1844
+ if (r.subtype === "success") p.resolve(r.response ?? {});
1845
+ else p.reject(new Error(String(r.error ?? "control request failed")));
1846
+ return;
1847
+ }
1848
+ if (m.type === "control_request") {
1849
+ void this.onControlRequest(String(m.request_id ?? ""), m.request ?? {});
1850
+ return;
1851
+ }
1852
+ if (m.type === "control_cancel_request") {
1853
+ const id2 = String(m.request_id ?? "");
1854
+ this.cancels.get(id2)?.abort();
1855
+ this.cancels.delete(id2);
1856
+ return;
1857
+ }
1858
+ if (m.type === "keep_alive") return;
1859
+ this.messages.push(m);
1860
+ }
1861
+ async onControlRequest(id2, req) {
1862
+ if (req.subtype !== "can_use_tool") {
1863
+ this.write({ type: "control_response", response: { subtype: "error", request_id: id2, error: `Unsupported control request: ${String(req.subtype)}` } });
1864
+ return;
1865
+ }
1866
+ const ac = new AbortController();
1867
+ this.cancels.set(id2, ac);
1868
+ try {
1869
+ const decision = await this.canUseTool({
1870
+ toolName: String(req.tool_name ?? ""),
1871
+ input: req.input && typeof req.input === "object" ? req.input : {},
1872
+ toolUseId: typeof req.tool_use_id === "string" ? req.tool_use_id : void 0,
1873
+ signal: ac.signal
1874
+ });
1875
+ if (ac.signal.aborted && !this.cancels.has(id2)) return;
1876
+ this.write({ type: "control_response", response: { subtype: "success", request_id: id2, response: decision } });
1877
+ } catch (e) {
1878
+ this.write({ type: "control_response", response: { subtype: "error", request_id: id2, error: String(e?.message ?? e) } });
1879
+ } finally {
1880
+ this.cancels.delete(id2);
1881
+ }
1882
+ }
1883
+ write(o) {
1884
+ if (this.dead || !this.child.stdin || this.child.stdin.destroyed) return false;
1885
+ try {
1886
+ this.child.stdin.write(JSON.stringify(o) + "\n");
1887
+ return true;
1888
+ } catch {
1889
+ return false;
1890
+ }
1891
+ }
1892
+ /** Send one control request and wait for its answer */
1893
+ request(request, timeoutMs = 1e4) {
1894
+ if (this.dead) return Promise.reject(new Error("claude is not running"));
1895
+ const id2 = `ac_${++this.seq}_${Math.random().toString(36).slice(2, 8)}`;
1896
+ return new Promise((resolve2, reject) => {
1897
+ const p = { resolve: resolve2, reject };
1898
+ p.timer = setTimeout(() => {
1899
+ if (this.pending.delete(id2)) reject(new Error(`claude did not answer ${String(request.subtype)} in ${Math.round(timeoutMs / 1e3)} s`));
1900
+ }, timeoutMs);
1901
+ this.pending.set(id2, p);
1902
+ if (!this.write({ type: "control_request", request_id: id2, request })) {
1903
+ this.pending.delete(id2);
1904
+ clearTimeout(p.timer);
1905
+ reject(new Error("claude is not running"));
1906
+ }
1907
+ });
1908
+ }
1909
+ /** One user message into the conversation */
1910
+ sendUser(content, sessionId) {
1911
+ return this.write({ type: "user", message: { role: "user", content }, parent_tool_use_id: null, session_id: sessionId ?? "" });
1912
+ }
1913
+ interrupt() {
1914
+ return this.request({ subtype: "interrupt" }, 5e3);
1915
+ }
1916
+ setModel(model) {
1917
+ return this.request(model ? { subtype: "set_model", model } : { subtype: "set_model" });
1918
+ }
1919
+ setPermissionMode(mode2) {
1920
+ return this.request({ subtype: "set_permission_mode", mode: mode2 });
1921
+ }
1922
+ /** End stdin (the CLI finishes and exits), then end the process tree if it lingers */
1923
+ async close(graceMs = 2e3) {
1924
+ try {
1925
+ this.child.stdin?.end();
1926
+ } catch {
1927
+ }
1928
+ const done = await Promise.race([this.exited.then(() => true), new Promise((r) => setTimeout(() => r(false), graceMs))]);
1929
+ if (!done) await killTree(this.child, { graceMs: 500 });
1930
+ this.finish();
1931
+ }
1932
+ };
1933
+ }
1934
+ });
1935
+
1398
1936
  // daemon/src/providers/claude/session.ts
1399
- import { query } from "@anthropic-ai/claude-agent-sdk";
1400
- import { readFile as readFile4 } from "node:fs/promises";
1401
- function claudeExecutable(o) {
1402
- if (!o.binCmd) return null;
1403
- if (o.binVia === "cmd-shim" || o.binVia === "ps1") return null;
1404
- const last = o.binPrefixArgs[o.binPrefixArgs.length - 1];
1405
- if (last && /\.(c|m)?js$/i.test(last)) return { path: last, node: true };
1406
- if (o.binPrefixArgs.length) return null;
1407
- return { path: o.binCmd, node: false };
1408
- }
1409
- var EFFORTS, rid2, ClaudeSession;
1937
+ var EFFORTS, MODES, rid2, ClaudeSession;
1410
1938
  var init_session = __esm({
1411
1939
  "daemon/src/providers/claude/session.ts"() {
1412
1940
  init_queue();
@@ -1414,9 +1942,12 @@ var init_session = __esm({
1414
1942
  init_diff();
1415
1943
  init_map();
1416
1944
  init_log();
1417
- init_attachmentRefs();
1945
+ init_content();
1946
+ init_i18n();
1418
1947
  init_slash();
1948
+ init_cli();
1419
1949
  EFFORTS = ["low", "medium", "high", "xhigh", "max"];
1950
+ MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
1420
1951
  rid2 = () => Math.random().toString(36).slice(2, 10);
1421
1952
  ClaudeSession = class {
1422
1953
  constructor(o) {
@@ -1427,9 +1958,7 @@ var init_session = __esm({
1427
1958
  o;
1428
1959
  events;
1429
1960
  q = new AsyncQueue();
1430
- input = new AsyncQueue();
1431
- query = null;
1432
- abort = new AbortController();
1961
+ cli = null;
1433
1962
  sessionId;
1434
1963
  turnId = null;
1435
1964
  turnNum = 0;
@@ -1438,69 +1967,60 @@ var init_session = __esm({
1438
1967
  needsRestart = false;
1439
1968
  restarting = null;
1440
1969
  closed = false;
1441
- stderrTail = "";
1970
+ stopping = /* @__PURE__ */ new WeakSet();
1442
1971
  pumpDone = null;
1443
- // The query is only started before the first send (a failed detection should be reported quickly)
1444
- ensureQuery() {
1445
- if (this.query && !this.needsRestart) return this.query;
1446
- this.abort = new AbortController();
1972
+ /** Mode the running CLI was started with (bypassPermissions has to be allowed at launch) */
1973
+ launchedMode = null;
1974
+ // The CLI is only started before the first send (or a command list request)
1975
+ ensureCli() {
1976
+ if (this.cli && !this.needsRestart) return this.cli;
1447
1977
  this.st = newMapState();
1448
- const options = {
1449
- cwd: this.o.cwd,
1450
- model: this.o.model ?? void 0,
1451
- permissionMode: this.o.mode,
1452
- canUseTool: (toolName, input, opts) => this.canUseTool(toolName, input, opts),
1453
- includePartialMessages: true,
1454
- settingSources: ["user", "project", "local"],
1455
- systemPrompt: { type: "preset", preset: "claude_code" },
1456
- abortController: this.abort,
1457
- ...this.sessionId ? { resume: this.sessionId } : {},
1458
- stderr: (d) => {
1459
- this.stderrTail = (this.stderrTail + d).slice(-4096);
1460
- }
1461
- };
1462
- const exe = claudeExecutable(this.o);
1463
- if (exe) {
1464
- options.pathToClaudeCodeExecutable = exe.path;
1465
- if (exe.node) options.executable = "node";
1466
- }
1467
- if (this.o.effort && EFFORTS.includes(this.o.effort)) {
1468
- options.effort = this.o.effort;
1469
- }
1470
- const q = query({ prompt: this.input, options });
1471
- this.query = q;
1978
+ const effort = this.o.effort && EFFORTS.includes(this.o.effort) ? this.o.effort : null;
1979
+ const cli = new ClaudeCli(
1980
+ {
1981
+ bin: this.o.bin,
1982
+ cwd: this.o.cwd,
1983
+ model: this.o.model,
1984
+ permissionMode: MODES.includes(this.o.mode) ? this.o.mode : "default",
1985
+ effort,
1986
+ resume: this.sessionId,
1987
+ env: this.o.env,
1988
+ launch: this.o.launch
1989
+ },
1990
+ (req) => this.canUseTool(req)
1991
+ );
1992
+ this.cli = cli;
1993
+ this.launchedMode = this.o.mode;
1472
1994
  this.needsRestart = false;
1473
- this.pumpDone = this.pump(q);
1474
- return q;
1995
+ this.pumpDone = this.pump(cli);
1996
+ return cli;
1475
1997
  }
1476
- async pump(q) {
1477
- try {
1478
- for await (const m of q) {
1479
- for (const ev of mapSdkMessage(m, this.st, this.o.cwd)) this.emit(ev);
1480
- }
1481
- } catch (e) {
1482
- if (!this.abort.signal.aborted && !this.closed) {
1483
- this.emit({ type: "notice", level: "error", message: "Claude \u4F1A\u8BDD\u5F02\u5E38\uFF1A" + this.errText(e) });
1484
- }
1998
+ async pump(cli) {
1999
+ for await (const m of cli.messages) {
2000
+ for (const ev of mapSdkMessage(m, this.st, this.o.cwd)) this.emit(ev);
1485
2001
  }
1486
- if (!this.closed && this.query === q) {
2002
+ const exit = await cli.exited;
2003
+ if (this.closed || this.stopping.has(cli)) return;
2004
+ const detail = cli.failure ? tr("claude.startFailed", { detail: String(cli.failure.message) }) : tr("claude.exited", { code: String(exit.code ?? exit.signal ?? "?") });
2005
+ const tail = cli.stderr.trim().slice(-400);
2006
+ this.emit({ type: "notice", level: "error", message: tail ? `${detail} \u2014 stderr: ${tail}` : detail });
2007
+ if (this.turnId) this.emit({ type: "turn_ended", turnId: this.turnId, status: "failed", error: detail });
2008
+ this.turnId = null;
2009
+ if (this.cli === cli) {
1487
2010
  this.needsRestart = true;
1488
2011
  this.q.close();
1489
2012
  }
1490
2013
  }
1491
2014
  emit(ev) {
1492
2015
  if (ev.type === "turn_ended" && !ev.turnId) ev.turnId = this.turnId ?? "unknown";
2016
+ if (ev.type === "turn_ended") this.turnId = null;
1493
2017
  if (ev.type === "provider_session_id" && ev.id) this.sessionId = ev.id;
1494
2018
  this.q.push(ev);
1495
2019
  }
1496
- errText(e) {
1497
- const base = String(e?.message ?? e);
1498
- return this.stderrTail ? `${base}\uFF1Bstderr: ${this.stderrTail.slice(-400)}` : base;
1499
- }
1500
2020
  async send(input) {
1501
- if (this.closed) throw new Error("\u4F1A\u8BDD\u5DF2\u5173\u95ED");
2021
+ if (this.closed) throw new Error(tr("claude.closed"));
1502
2022
  if (this.needsRestart) await this.restart();
1503
- const q = this.ensureQuery();
2023
+ const cli = this.ensureCli();
1504
2024
  this.turnNum++;
1505
2025
  this.turnId = "u_" + Date.now().toString(36) + "_" + this.turnNum;
1506
2026
  this.st.sawInterrupt = false;
@@ -1511,43 +2031,24 @@ var init_session = __esm({
1511
2031
  mode: this.o.mode,
1512
2032
  effort: this.o.effort
1513
2033
  });
1514
- const content = [];
1515
- const blocks = content;
1516
- for (const img of input.images ?? []) {
1517
- try {
1518
- const disk = await resolveImagePath(img.path);
1519
- const data = await readFile4(disk);
1520
- blocks.push({
1521
- type: "image",
1522
- source: { type: "base64", media_type: img.mime, data: data.toString("base64") }
1523
- });
1524
- } catch (e) {
1525
- this.emit({ type: "notice", level: "warn", message: `\u56FE\u7247\u8BFB\u53D6\u5931\u8D25 ${img.path}\uFF1A${String(e)}` });
1526
- }
1527
- }
1528
- blocks.push({ type: "text", text: input.text });
1529
- this.input.push({
1530
- type: "user",
1531
- message: { role: "user", content },
1532
- parent_tool_use_id: null,
1533
- session_id: this.sessionId ?? ""
1534
- });
1535
- void q;
2034
+ const built = await buildClaudeContent(input.text, input.images ?? []);
2035
+ for (const w of built.warnings) this.emit({ type: "notice", level: "warn", message: tr("notice.attachmentIssue", { detail: w }) });
2036
+ if (!cli.sendUser(built.blocks, this.sessionId)) throw new Error(tr("claude.notRunning"));
1536
2037
  }
1537
2038
  async restart() {
1538
2039
  if (this.restarting) return this.restarting;
1539
2040
  this.restarting = (async () => {
1540
- this.query = null;
1541
- try {
1542
- this.input.close();
1543
- this.abort.abort();
1544
- } catch {
2041
+ const old = this.cli;
2042
+ this.cli = null;
2043
+ if (old) {
2044
+ this.stopping.add(old);
2045
+ await old.close().catch(() => {
2046
+ });
1545
2047
  }
1546
2048
  if (this.pumpDone) await this.pumpDone.catch(() => {
1547
2049
  });
1548
- this.input = new AsyncQueue();
1549
2050
  try {
1550
- this.ensureQuery();
2051
+ this.ensureCli();
1551
2052
  } finally {
1552
2053
  this.restarting = null;
1553
2054
  }
@@ -1555,9 +2056,10 @@ var init_session = __esm({
1555
2056
  return this.restarting;
1556
2057
  }
1557
2058
  // ---- permissions (§7.2.4) ----
1558
- canUseTool(toolName, input, opts) {
2059
+ canUseTool(req) {
2060
+ const { toolName, input, signal } = req;
1559
2061
  const requestId = "perm_" + rid2();
1560
- const { signal } = opts;
2062
+ const opts = { toolUseID: req.toolUseId };
1561
2063
  if (toolName === "ExitPlanMode") {
1562
2064
  return this.handlePlanMode(requestId, input, signal, opts.toolUseID);
1563
2065
  }
@@ -1576,24 +2078,24 @@ var init_session = __esm({
1576
2078
  kind: "tool",
1577
2079
  detail,
1578
2080
  options: [
1579
- { id: "allow", label: "\u5141\u8BB8", kind: "allow_once" },
1580
- { id: "allow_always", label: "\u672C\u5BF9\u8BDD\u90FD\u5141\u8BB8", kind: "allow_always" },
1581
- { id: "deny", label: "\u62D2\u7EDD", kind: "reject_once" }
2081
+ { id: "allow", label: tr("perm.allow"), kind: "allow_once" },
2082
+ { id: "allow_always", label: tr("perm.allowSession"), kind: "allow_always" },
2083
+ { id: "deny", label: tr("perm.deny"), kind: "reject_once" }
1582
2084
  ]
1583
2085
  });
1584
2086
  return new Promise((resolve2) => {
1585
- this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(this.toPermissionResult(a, input)) });
2087
+ this.pending.set(requestId, { kind: "permission", resolve: (a) => resolve2(this.toPermissionDecision(a, input)) });
1586
2088
  signal.addEventListener("abort", () => {
1587
2089
  if (this.pending.delete(requestId)) {
1588
2090
  this.emit({ type: "permission_resolved", requestId, cancelled: true });
1589
- resolve2({ behavior: "deny", message: "\u5DF2\u53D6\u6D88", interrupt: true });
2091
+ resolve2({ behavior: "deny", message: "Cancelled by the user", interrupt: true });
1590
2092
  }
1591
2093
  }, { once: true });
1592
2094
  });
1593
2095
  }
1594
- toPermissionResult(a, input) {
2096
+ toPermissionDecision(a, input) {
1595
2097
  if (a.cancelled || a.optionId === "deny") {
1596
- return { behavior: "deny", message: "\u7528\u6237\u62D2\u7EDD\u4E86\u6B64\u64CD\u4F5C", ...a.cancelled ? { interrupt: true } : {} };
2098
+ return { behavior: "deny", message: "The user denied this action", ...a.cancelled ? { interrupt: true } : {} };
1597
2099
  }
1598
2100
  return { behavior: "allow", updatedInput: input };
1599
2101
  }
@@ -1605,12 +2107,12 @@ var init_session = __esm({
1605
2107
  this.emit({
1606
2108
  type: "permission_request",
1607
2109
  requestId,
1608
- title: "Agent \u63D0\u4EA4\u4E86\u8BA1\u5212",
2110
+ title: tr("perm.plan"),
1609
2111
  kind: "plan",
1610
2112
  detail: { plan: planText },
1611
2113
  options: [
1612
- { id: "implement", label: "\u5B9E\u65BD", kind: "allow_once" },
1613
- { id: "reject", label: "\u62D2\u7EDD", kind: "reject_once" }
2114
+ { id: "implement", label: tr("perm.implement"), kind: "allow_once" },
2115
+ { id: "reject", label: tr("perm.deny"), kind: "reject_once" }
1614
2116
  ]
1615
2117
  });
1616
2118
  const answer = await new Promise((resolve2) => {
@@ -1623,10 +2125,10 @@ var init_session = __esm({
1623
2125
  }, { once: true });
1624
2126
  });
1625
2127
  if (answer.cancelled || answer.optionId === "reject") {
1626
- return { behavior: "deny", message: "\u7528\u6237\u62D2\u7EDD\u4E86\u8BA1\u5212\uFF0C\u8BF7\u6839\u636E\u53CD\u9988\u4FEE\u6539", ...answer.cancelled ? { interrupt: true } : {} };
2128
+ return { behavior: "deny", message: "The user rejected the plan; revise it based on their feedback", ...answer.cancelled ? { interrupt: true } : {} };
1627
2129
  }
1628
2130
  try {
1629
- await this.query?.setPermissionMode("default");
2131
+ await this.cli?.setPermissionMode("default");
1630
2132
  this.o.mode = "default";
1631
2133
  this.o.onModeChange?.("default");
1632
2134
  } catch (e) {
@@ -1646,7 +2148,7 @@ var init_session = __esm({
1646
2148
  }, { once: true });
1647
2149
  });
1648
2150
  if (answer.cancelled) {
1649
- return { behavior: "deny", message: "\u7528\u6237\u53D6\u6D88\u4E86\u63D0\u95EE", interrupt: true };
2151
+ return { behavior: "deny", message: "The user cancelled the question", interrupt: true };
1650
2152
  }
1651
2153
  const questions = input.questions ?? [];
1652
2154
  const normalized = {};
@@ -1667,38 +2169,45 @@ var init_session = __esm({
1667
2169
  }
1668
2170
  async interrupt() {
1669
2171
  this.st.sawInterrupt = true;
1670
- for (const [id, p] of this.pending) {
1671
- this.emit({ type: "permission_resolved", requestId: id, cancelled: true });
2172
+ for (const [id2, p] of this.pending) {
2173
+ this.emit({ type: "permission_resolved", requestId: id2, cancelled: true });
1672
2174
  p.resolve({ cancelled: true });
1673
2175
  }
1674
2176
  this.pending.clear();
2177
+ const cli = this.cli;
2178
+ if (!cli || !cli.alive) return;
1675
2179
  try {
1676
- const q = this.query;
1677
- if (q) {
1678
- await Promise.race([
1679
- q.interrupt(),
1680
- new Promise((_, rej) => setTimeout(() => rej(new Error("interrupt \u8D85\u65F6")), 3e3))
1681
- ]);
1682
- }
2180
+ await cli.interrupt();
1683
2181
  } catch (e) {
1684
- log("warn", "interrupt \u5931\u8D25", { err: String(e) });
2182
+ log("warn", "claude interrupt failed", { err: String(e) });
2183
+ this.stopping.add(cli);
1685
2184
  this.needsRestart = true;
2185
+ void cli.close(500);
2186
+ if (this.turnId) this.emit({ type: "turn_ended", turnId: this.turnId, status: "interrupted" });
1686
2187
  }
1687
2188
  }
1688
2189
  async setModel(model) {
1689
2190
  this.o.model = model;
2191
+ if (!this.cli?.alive) return;
1690
2192
  try {
1691
- await this.query?.setModel(model ?? void 0);
2193
+ await this.cli.setModel(model);
1692
2194
  } catch (e) {
1693
- log("warn", "setModel \u5931\u8D25", { err: String(e) });
2195
+ log("warn", "claude set_model failed", { err: String(e) });
2196
+ this.needsRestart = true;
1694
2197
  }
1695
2198
  }
1696
- async setMode(mode) {
1697
- this.o.mode = mode;
2199
+ async setMode(mode2) {
2200
+ this.o.mode = mode2;
2201
+ if (!this.cli?.alive) return;
2202
+ if (mode2 === "bypassPermissions" && this.launchedMode !== "bypassPermissions") {
2203
+ this.needsRestart = true;
2204
+ return;
2205
+ }
1698
2206
  try {
1699
- await this.query?.setPermissionMode(mode);
2207
+ await this.cli.setPermissionMode(mode2);
1700
2208
  } catch (e) {
1701
- log("warn", "setPermissionMode \u5931\u8D25", { err: String(e) });
2209
+ log("warn", "claude set_permission_mode failed", { err: String(e) });
2210
+ this.needsRestart = true;
1702
2211
  }
1703
2212
  }
1704
2213
  async setEffort(effort) {
@@ -1707,14 +2216,14 @@ var init_session = __esm({
1707
2216
  }
1708
2217
  async listCommands() {
1709
2218
  try {
1710
- const q = this.ensureQuery();
1711
- const commands = await q.supportedCommands();
2219
+ const init = await this.ensureCli().initialized;
2220
+ const commands = Array.isArray(init?.commands) ? init.commands : [];
1712
2221
  const map = /* @__PURE__ */ new Map();
1713
2222
  for (const cmd of commands) {
1714
- if (!map.has(cmd.name)) {
2223
+ if (cmd?.name && !map.has(cmd.name)) {
1715
2224
  map.set(cmd.name, {
1716
2225
  name: cmd.name,
1717
- description: cmd.description,
2226
+ description: cmd.description ?? "",
1718
2227
  argumentHint: cmd.argumentHint,
1719
2228
  kind: classifyClaudeSlashCommand(cmd.name)
1720
2229
  });
@@ -1729,10 +2238,11 @@ var init_session = __esm({
1729
2238
  this.closed = true;
1730
2239
  for (const [, p] of this.pending) p.resolve({ cancelled: true });
1731
2240
  this.pending.clear();
1732
- this.input.close();
1733
- try {
1734
- this.abort.abort();
1735
- } catch {
2241
+ const cli = this.cli;
2242
+ if (cli) {
2243
+ this.stopping.add(cli);
2244
+ await cli.close().catch(() => {
2245
+ });
1736
2246
  }
1737
2247
  if (this.pumpDone) {
1738
2248
  await Promise.race([this.pumpDone.catch(() => {
@@ -1754,7 +2264,7 @@ var init_models = __esm({
1754
2264
  });
1755
2265
 
1756
2266
  // daemon/src/history/import.ts
1757
- import { readdir, readFile as readFile5, stat as stat3 } from "node:fs/promises";
2267
+ import { readdir, readFile as readFile5, stat as stat4 } from "node:fs/promises";
1758
2268
  import { existsSync as existsSync6 } from "node:fs";
1759
2269
  import { join as join6 } from "node:path";
1760
2270
  import { homedir as homedir3 } from "node:os";
@@ -1806,10 +2316,10 @@ async function listClaudeHistory(cwd) {
1806
2316
  if (p.userText) count++;
1807
2317
  }
1808
2318
  if (!match) continue;
1809
- const s = await stat3(path3).catch(() => null);
2319
+ const s = await stat4(path3).catch(() => null);
1810
2320
  out.push({
1811
2321
  sessionId: f.replace(/\.jsonl$/, ""),
1812
- title: title || "\uFF08\u65E0\u6807\u9898\uFF09",
2322
+ title: title || tr("history.untitled"),
1813
2323
  updatedAt: s ? Math.round(s.mtimeMs) : 0,
1814
2324
  messageCount: count
1815
2325
  });
@@ -1847,6 +2357,7 @@ var CLAUDE_ROOT, rec2, str2;
1847
2357
  var init_import = __esm({
1848
2358
  "daemon/src/history/import.ts"() {
1849
2359
  init_paths();
2360
+ init_i18n();
1850
2361
  CLAUDE_ROOT = join6(homedir3(), ".claude", "projects");
1851
2362
  rec2 = (v) => v && typeof v === "object" ? v : {};
1852
2363
  str2 = (v) => typeof v === "string" ? v : void 0;
@@ -1875,16 +2386,18 @@ function runVersion(r, args, timeoutMs = 5e3) {
1875
2386
  });
1876
2387
  });
1877
2388
  }
1878
- function createClaudeProvider() {
2389
+ function createClaudeProvider(opts = {}) {
2390
+ const resolve2 = opts.resolve ?? resolveBin;
2391
+ const resolveCustom = opts.resolve ? (l) => opts.resolve(l.cmd) : resolveLaunch;
1879
2392
  return {
1880
2393
  id: "claude",
1881
2394
  name: "Claude Code",
1882
2395
  capabilities: claudeCapabilities,
1883
- async detect() {
1884
- const bin = await resolveBin("claude");
2396
+ async detect(launch) {
2397
+ const bin = launch ? await resolveCustom(launch) : await resolve2("claude");
1885
2398
  if (!bin.ok) return { available: false, reason: bin.reason, installHint: INSTALL_HINT };
1886
2399
  try {
1887
- const version = await runVersion(bin, ["--version"]);
2400
+ const version = await runVersion(bin, [...launch?.prefixArgs ?? [], "--version"]);
1888
2401
  return { available: true, bin: bin.display, version: version.trim().split("\n")[0] || "" };
1889
2402
  } catch {
1890
2403
  return { available: true, bin: bin.display, version: "" };
@@ -1894,8 +2407,8 @@ function createClaudeProvider() {
1894
2407
  return listClaudeModels();
1895
2408
  },
1896
2409
  async createSession(o) {
1897
- const bin = await resolveBin("claude");
1898
- if (!bin.ok) throw new Error(`\u627E\u4E0D\u5230 claude\uFF1A${bin.reason}`);
2410
+ const bin = o.launch ? await resolveCustom(o.launch) : await resolve2("claude");
2411
+ if (!bin.ok) throw new Error(tr("claude.notFound", { detail: bin.reason, hint: INSTALL_HINT }));
1899
2412
  return new ClaudeSession({
1900
2413
  threadId: o.threadId,
1901
2414
  cwd: o.cwd,
@@ -1903,9 +2416,8 @@ function createClaudeProvider() {
1903
2416
  mode: o.mode,
1904
2417
  effort: o.effort,
1905
2418
  resumeId: o.resumeId,
1906
- binCmd: bin.cmd,
1907
- binPrefixArgs: bin.prefixArgs,
1908
- binVia: bin.via
2419
+ bin,
2420
+ launch: o.launch
1909
2421
  });
1910
2422
  },
1911
2423
  // §7.6: jsonl sessions under ~/.claude/projects matched by cwd
@@ -1941,6 +2453,8 @@ var init_provider = __esm({
1941
2453
  init_models();
1942
2454
  init_import();
1943
2455
  init_map();
2456
+ init_i18n();
2457
+ init_launch();
1944
2458
  INSTALL_HINT = "npm i -g @anthropic-ai/claude-code";
1945
2459
  claudeCapabilities = {
1946
2460
  resume: true,
@@ -1958,7 +2472,7 @@ var init_provider = __esm({
1958
2472
  });
1959
2473
 
1960
2474
  // daemon/src/history/codex.ts
1961
- import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "node:fs/promises";
2475
+ import { readdir as readdir2, readFile as readFile6, stat as stat5 } from "node:fs/promises";
1962
2476
  import { join as join7 } from "node:path";
1963
2477
  import { homedir as homedir4 } from "node:os";
1964
2478
  function textOf(content, kind) {
@@ -1969,7 +2483,7 @@ async function* walk(dir, depth = 0) {
1969
2483
  if (depth > 6) return;
1970
2484
  for (const name of await readdir2(dir).catch(() => [])) {
1971
2485
  const p = join7(dir, name);
1972
- const s = await stat4(p).catch(() => null);
2486
+ const s = await stat5(p).catch(() => null);
1973
2487
  if (!s) continue;
1974
2488
  if (s.isDirectory()) yield* walk(p, depth + 1);
1975
2489
  else if (/^rollout-.*\.jsonl$/.test(name)) yield p;
@@ -2012,8 +2526,8 @@ async function listCodexHistory(cwd) {
2012
2526
  }
2013
2527
  }
2014
2528
  if (!match) continue;
2015
- const s = await stat4(path3).catch(() => null);
2016
- out.push({ sessionId, title: title || "\uFF08\u65E0\u6807\u9898\uFF09", updatedAt: s ? Math.round(s.mtimeMs) : 0, messageCount: count });
2529
+ const s = await stat5(path3).catch(() => null);
2530
+ out.push({ sessionId, title: title || tr("history.untitled"), updatedAt: s ? Math.round(s.mtimeMs) : 0, messageCount: count });
2017
2531
  }
2018
2532
  out.sort((a, b) => b.updatedAt - a.updatedAt);
2019
2533
  return out;
@@ -2066,7 +2580,7 @@ async function readCodexHistory(sessionId, cwd) {
2066
2580
  }
2067
2581
  } else if (p.type === "function_call" || p.type === "custom_tool_call") {
2068
2582
  const name = str3(p.name) ?? "tool";
2069
- out.push({ type: "notice", level: "info", message: `\uFF08\u5386\u53F2\uFF09\u8C03\u7528 ${name}` });
2583
+ out.push({ type: "notice", level: "info", message: tr("history.toolCall", { name }) });
2070
2584
  }
2071
2585
  continue;
2072
2586
  }
@@ -2090,7 +2604,7 @@ async function readCodexHistory(sessionId, cwd) {
2090
2604
  const p2 = rec3(r.payload);
2091
2605
  if (p2.type === "function_call" || p2.type === "custom_tool_call") {
2092
2606
  const name = str3(p2.name) ?? "tool";
2093
- out.push({ type: "notice", level: "info", message: `\uFF08\u5386\u53F2\uFF09\u8C03\u7528 ${name}` });
2607
+ out.push({ type: "notice", level: "info", message: tr("history.toolCall", { name }) });
2094
2608
  }
2095
2609
  }
2096
2610
  out.push({ type: "turn_ended", turnId: "import", status: "completed" });
@@ -2100,6 +2614,7 @@ var CODEX_ROOT, rec3, str3, WRAPPERS;
2100
2614
  var init_codex = __esm({
2101
2615
  "daemon/src/history/codex.ts"() {
2102
2616
  init_paths();
2617
+ init_i18n();
2103
2618
  CODEX_ROOT = join7(homedir4(), ".codex", "sessions");
2104
2619
  rec3 = (v) => v && typeof v === "object" ? v : {};
2105
2620
  str3 = (v) => typeof v === "string" ? v : void 0;
@@ -2186,10 +2701,10 @@ var init_rpc = __esm({
2186
2701
  async dispatch(msg) {
2187
2702
  const hasId = msg.id !== void 0 && msg.id !== null;
2188
2703
  if (hasId && ("result" in msg || "error" in msg)) {
2189
- const id = Number(msg.id);
2190
- const p = this.pending.get(id);
2704
+ const id2 = Number(msg.id);
2705
+ const p = this.pending.get(id2);
2191
2706
  if (p) {
2192
- this.pending.delete(id);
2707
+ this.pending.delete(id2);
2193
2708
  if (msg.error) {
2194
2709
  const e = msg.error;
2195
2710
  p.reject(new CodexRpcError(e.message ?? "RPC error", e.code));
@@ -2200,12 +2715,12 @@ var init_rpc = __esm({
2200
2715
  return;
2201
2716
  }
2202
2717
  if (hasId && msg.method) {
2203
- const id = Number(msg.id);
2718
+ const id2 = Number(msg.id);
2204
2719
  try {
2205
2720
  const result = this.onServerRequest ? await this.onServerRequest(String(msg.method), msg.params ?? {}) : void 0;
2206
- this.send({ id, result: result ?? null });
2721
+ this.send({ id: id2, result: result ?? null });
2207
2722
  } catch (e) {
2208
- this.send({ id, error: { code: -32603, message: String(e?.message ?? e) } });
2723
+ this.send({ id: id2, error: { code: -32603, message: String(e?.message ?? e) } });
2209
2724
  }
2210
2725
  return;
2211
2726
  }
@@ -2226,13 +2741,13 @@ var init_rpc = __esm({
2226
2741
  }
2227
2742
  request(method, params, timeoutMs = 6e4) {
2228
2743
  if (this.exited) return Promise.reject(new CodexProcessExitedError());
2229
- const id = this.nextId++;
2744
+ const id2 = this.nextId++;
2230
2745
  return new Promise((resolvePromise, reject) => {
2231
2746
  const timer = setTimeout(() => {
2232
- this.pending.delete(id);
2747
+ this.pending.delete(id2);
2233
2748
  reject(new CodexRpcError(`\u8BF7\u6C42\u8D85\u65F6\uFF1A${method}`));
2234
2749
  }, timeoutMs);
2235
- this.pending.set(id, {
2750
+ this.pending.set(id2, {
2236
2751
  resolve: (v) => {
2237
2752
  clearTimeout(timer);
2238
2753
  resolvePromise(v);
@@ -2242,7 +2757,7 @@ var init_rpc = __esm({
2242
2757
  reject(e);
2243
2758
  }
2244
2759
  });
2245
- this.send({ jsonrpc: "2.0", id, method, params: params ?? {} });
2760
+ this.send({ jsonrpc: "2.0", id: id2, method, params: params ?? {} });
2246
2761
  });
2247
2762
  }
2248
2763
  notify(method, params) {
@@ -2277,9 +2792,41 @@ var init_rpc = __esm({
2277
2792
  }
2278
2793
  });
2279
2794
 
2795
+ // daemon/src/providers/codex/input.ts
2796
+ import { basename as basename2 } from "node:path";
2797
+ async function buildCodexInput(text, attachments) {
2798
+ const images = [];
2799
+ const notes = [];
2800
+ const warnings = [];
2801
+ for (const a of attachments) {
2802
+ let file;
2803
+ try {
2804
+ file = await resolveImagePath(a.path);
2805
+ } catch (e) {
2806
+ warnings.push(`${a.path}: ${String(e?.message ?? e)}`);
2807
+ continue;
2808
+ }
2809
+ const kind = attachmentKind(a.mime);
2810
+ if (kind === "image") {
2811
+ images.push({ type: "localImage", path: file });
2812
+ continue;
2813
+ }
2814
+ notes.push(pathNote(kind, file));
2815
+ if (kind === "other") warnings.push(`${basename2(file)} (${baseMime(a.mime) || "unknown type"}) is not an image or a text file; sent its path`);
2816
+ }
2817
+ const fullText = [text, ...notes.length ? [notes.join("\n")] : []].filter(Boolean).join("\n\n");
2818
+ return { items: [{ type: "text", text: fullText }, ...images], warnings };
2819
+ }
2820
+ var init_input = __esm({
2821
+ "daemon/src/providers/codex/input.ts"() {
2822
+ init_attachmentRefs();
2823
+ init_attachmentKinds();
2824
+ }
2825
+ });
2826
+
2280
2827
  // daemon/src/providers/codex/map.ts
2281
2828
  function newCodexMapState() {
2282
- return { currentTurnId: null, filesChanged: 0, diffText: "" };
2829
+ return { currentTurnId: null, filesChanged: 0, diffText: "", fileChangesByItemId: /* @__PURE__ */ new Map(), reasoningRaw: /* @__PURE__ */ new Map() };
2283
2830
  }
2284
2831
  function mapNotification(method, params, st) {
2285
2832
  const out = [];
@@ -2291,6 +2838,8 @@ function mapNotification(method, params, st) {
2291
2838
  if (tid) st.currentTurnId = tid;
2292
2839
  st.filesChanged = 0;
2293
2840
  st.diffText = "";
2841
+ st.fileChangesByItemId.clear();
2842
+ st.reasoningRaw.clear();
2294
2843
  break;
2295
2844
  }
2296
2845
  case "item/agentMessage/delta": {
@@ -2306,7 +2855,10 @@ function mapNotification(method, params, st) {
2306
2855
  case "item/reasoning/textDelta": {
2307
2856
  const itemId = str4(params.itemId) ?? "";
2308
2857
  const delta = str4(params.delta) ?? "";
2309
- if (itemId && delta) out.push({ type: "reasoning_delta", itemId, text: delta });
2858
+ if (itemId && delta) {
2859
+ if (method === "item/reasoning/textDelta") st.reasoningRaw.set(itemId, (st.reasoningRaw.get(itemId) ?? "") + delta);
2860
+ out.push({ type: "reasoning_delta", itemId, text: delta });
2861
+ }
2310
2862
  break;
2311
2863
  }
2312
2864
  case "item/commandExecution/outputDelta":
@@ -2327,17 +2879,18 @@ function mapNotification(method, params, st) {
2327
2879
  type: "tool_call",
2328
2880
  itemId,
2329
2881
  kind: "bash",
2330
- title: `\u8FD0\u884C ${trunc2(cmd)}`,
2882
+ title: tr("tool.run", { x: trunc2(cmd) }),
2331
2883
  status: "running",
2332
2884
  detail: { command: cmd, cwd: str4(item.cwd) }
2333
2885
  });
2334
2886
  } else if (type === "fileChange") {
2335
2887
  const changes = Array.isArray(item.changes) ? item.changes : [];
2888
+ if (itemId) st.fileChangesByItemId.set(itemId, changes);
2336
2889
  out.push({
2337
2890
  type: "tool_call",
2338
2891
  itemId,
2339
2892
  kind: "edit",
2340
- title: `\u4FEE\u6539 ${changes.length} \u4E2A\u6587\u4EF6`,
2893
+ title: tr("tool.editFiles", { n: changes.length }),
2341
2894
  status: "running",
2342
2895
  detail: { changes }
2343
2896
  });
@@ -2356,7 +2909,7 @@ function mapNotification(method, params, st) {
2356
2909
  type: "tool_call",
2357
2910
  itemId,
2358
2911
  kind: "web",
2359
- title: `\u641C\u7D22\u7F51\u9875 "${trunc2(str4(item.query) ?? "", 40)}"`,
2912
+ title: tr("tool.webSearch", { x: trunc2(str4(item.query) ?? "", 40) }),
2360
2913
  status: "running"
2361
2914
  });
2362
2915
  }
@@ -2371,7 +2924,10 @@ function mapNotification(method, params, st) {
2371
2924
  if (text) out.push({ type: "assistant_message", itemId, text });
2372
2925
  } else if (type === "reasoning") {
2373
2926
  const summary = Array.isArray(item.summary) ? item.summary : [];
2374
- const text = summary.filter((s) => typeof s === "string").join("\n");
2927
+ const content = Array.isArray(item.content) ? item.content : [];
2928
+ const strings = (a) => a.filter((s) => typeof s === "string").join("\n");
2929
+ const text = strings(summary) || strings(content) || st.reasoningRaw.get(itemId) || "";
2930
+ st.reasoningRaw.delete(itemId);
2375
2931
  if (text) out.push({ type: "reasoning", itemId, text });
2376
2932
  } else if (type === "commandExecution") {
2377
2933
  const cmd = str4(item.command) ?? "";
@@ -2381,7 +2937,7 @@ function mapNotification(method, params, st) {
2381
2937
  type: "tool_call",
2382
2938
  itemId,
2383
2939
  kind: "bash",
2384
- title: `\u8FD0\u884C ${trunc2(cmd)}`,
2940
+ title: tr("tool.run", { x: trunc2(cmd) }),
2385
2941
  status: exitCode === 0 ? "ok" : "failed",
2386
2942
  output: output.length > MAX_OUTPUT2 ? output.slice(0, MAX_OUTPUT2) : output,
2387
2943
  exitCode,
@@ -2389,13 +2945,14 @@ function mapNotification(method, params, st) {
2389
2945
  });
2390
2946
  } else if (type === "fileChange") {
2391
2947
  const changes = Array.isArray(item.changes) ? item.changes : [];
2948
+ if (itemId) st.fileChangesByItemId.set(itemId, changes);
2392
2949
  const istatus = str4(item.status);
2393
2950
  st.filesChanged += changes.length;
2394
2951
  out.push({
2395
2952
  type: "tool_call",
2396
2953
  itemId,
2397
2954
  kind: "edit",
2398
- title: `\u4FEE\u6539 ${changes.length} \u4E2A\u6587\u4EF6`,
2955
+ title: tr("tool.editFiles", { n: changes.length }),
2399
2956
  status: istatus === "failed" ? "failed" : "ok",
2400
2957
  detail: { changes }
2401
2958
  });
@@ -2417,7 +2974,7 @@ function mapNotification(method, params, st) {
2417
2974
  type: "tool_call",
2418
2975
  itemId,
2419
2976
  kind: "web",
2420
- title: `\u641C\u7D22\u7F51\u9875 "${trunc2(str4(item.query) ?? "", 40)}"`,
2977
+ title: tr("tool.webSearch", { x: trunc2(str4(item.query) ?? "", 40) }),
2421
2978
  status: "ok"
2422
2979
  });
2423
2980
  } else if (type === "plan") {
@@ -2474,7 +3031,7 @@ function mapNotification(method, params, st) {
2474
3031
  break;
2475
3032
  }
2476
3033
  case "error": {
2477
- const msg = str4(params.message) ?? "Codex \u9519\u8BEF";
3034
+ const msg = str4(params.message) ?? tr("codex.error");
2478
3035
  out.push({ type: "notice", level: "error", message: msg });
2479
3036
  break;
2480
3037
  }
@@ -2486,6 +3043,7 @@ function mapNotification(method, params, st) {
2486
3043
  var MAX_OUTPUT2, trunc2, rec4, str4;
2487
3044
  var init_map2 = __esm({
2488
3045
  "daemon/src/providers/codex/map.ts"() {
3046
+ init_i18n();
2489
3047
  MAX_OUTPUT2 = 20 * 1024;
2490
3048
  trunc2 = (s, n = 80) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
2491
3049
  rec4 = (v) => v && typeof v === "object" ? v : {};
@@ -2494,9 +3052,9 @@ var init_map2 = __esm({
2494
3052
  });
2495
3053
 
2496
3054
  // daemon/src/providers/codex/commands.ts
2497
- import { readdir as readdir3, readFile as readFile7, stat as stat5 } from "node:fs/promises";
3055
+ import { readdir as readdir3, readFile as readFile7, stat as stat6 } from "node:fs/promises";
2498
3056
  import { homedir as homedir5 } from "node:os";
2499
- import { basename, join as join8 } from "node:path";
3057
+ import { basename as basename3, join as join8 } from "node:path";
2500
3058
  function parseFrontMatter(raw) {
2501
3059
  if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
2502
3060
  const end = raw.indexOf("\n---\n", 4);
@@ -2528,11 +3086,11 @@ async function readPromptFiles(dir) {
2528
3086
  if (!f.endsWith(".md")) continue;
2529
3087
  const path3 = join8(dir, f);
2530
3088
  try {
2531
- const st = await stat5(path3);
3089
+ const st = await stat6(path3);
2532
3090
  if (!st.isFile()) continue;
2533
3091
  const raw = await readFile7(path3, "utf8");
2534
3092
  const { meta, body } = parseFrontMatter(raw);
2535
- const name = basename(f, ".md");
3093
+ const name = basename3(f, ".md");
2536
3094
  out.push({
2537
3095
  name,
2538
3096
  description: meta.description || firstLine(body) || name,
@@ -2556,7 +3114,7 @@ async function readSkillDirs(root) {
2556
3114
  for (const d of dirs) {
2557
3115
  const skillPath = join8(root, d, "SKILL.md");
2558
3116
  try {
2559
- const st = await stat5(skillPath);
3117
+ const st = await stat6(skillPath);
2560
3118
  if (!st.isFile()) continue;
2561
3119
  const raw = await readFile7(skillPath, "utf8");
2562
3120
  const { meta, body } = parseFrontMatter(raw);
@@ -2609,13 +3167,15 @@ var init_commands = __esm({
2609
3167
  var rid3, MODE_PRESETS, CodexSession;
2610
3168
  var init_session2 = __esm({
2611
3169
  "daemon/src/providers/codex/session.ts"() {
2612
- init_attachmentRefs();
3170
+ init_input();
3171
+ init_i18n();
2613
3172
  init_queue();
2614
3173
  init_rpc();
2615
3174
  init_bin();
2616
3175
  init_map2();
2617
3176
  init_commands();
2618
3177
  init_log();
3178
+ init_launch();
2619
3179
  rid3 = () => Math.random().toString(36).slice(2, 10);
2620
3180
  MODE_PRESETS = {
2621
3181
  plan: { approvalPolicy: "on-request", sandbox: "read-only", sandboxPolicy: { type: "readOnly" } },
@@ -2659,7 +3219,7 @@ var init_session2 = __esm({
2659
3219
  const rpc = new CodexRpc(
2660
3220
  argvFor(
2661
3221
  { ok: true, cmd: this.o.binCmd, prefixArgs: this.o.binPrefixArgs, display: this.o.binCmd, via: this.o.binVia ?? "path", shim: this.o.binShim },
2662
- ["app-server"]
3222
+ launchArgv(["app-server"], this.o.launch)
2663
3223
  ),
2664
3224
  { cwd: this.o.cwd }
2665
3225
  );
@@ -2705,7 +3265,7 @@ var init_session2 = __esm({
2705
3265
  }
2706
3266
  })();
2707
3267
  this.started = attempt.catch((e) => {
2708
- if (this.started === guarded) {
3268
+ if (this.started === guarded2) {
2709
3269
  this.started = null;
2710
3270
  const rpc = this.rpc;
2711
3271
  this.rpc = null;
@@ -2718,21 +3278,18 @@ var init_session2 = __esm({
2718
3278
  }
2719
3279
  throw e;
2720
3280
  });
2721
- const guarded = this.started;
3281
+ const guarded2 = this.started;
2722
3282
  return this.started;
2723
3283
  }
2724
3284
  async send(input) {
2725
- if (this.closed) throw new Error("\u4F1A\u8BDD\u5DF2\u5173\u95ED");
3285
+ if (this.closed) throw new Error(tr("codex.closed"));
2726
3286
  await this.start();
2727
3287
  const rpc = this.rpc;
2728
3288
  const preset = MODE_PRESETS[this.o.mode];
2729
3289
  const entries = this.codexSlash ?? await loadCodexSlashEntries(this.o.cwd);
2730
3290
  this.codexSlash = entries;
2731
3291
  const wireText = expandCodexSlash(input.text, entries);
2732
- const items = [{ type: "text", text: wireText }];
2733
- for (const img of input.images ?? []) {
2734
- items.push({ type: "localImage", path: await resolveImagePath(img.path) });
2735
- }
3292
+ const { items, warnings } = await buildCodexInput(wireText, input.images ?? []);
2736
3293
  this.interruptWanted = false;
2737
3294
  const turnId = "u_" + Date.now().toString(36) + "_" + rid3();
2738
3295
  if (this.ourTurnId && this.st.currentTurnId) this.staleCodexTurns.add(this.st.currentTurnId);
@@ -2745,6 +3302,7 @@ var init_session2 = __esm({
2745
3302
  mode: this.o.mode,
2746
3303
  effort: this.o.effort
2747
3304
  });
3305
+ for (const w of warnings) this.emit({ type: "notice", level: "warn", message: tr("notice.attachmentIssue", { detail: w }) });
2748
3306
  try {
2749
3307
  await rpc.request("turn/start", {
2750
3308
  threadId: this.sessionThreadId ?? void 0,
@@ -2764,11 +3322,9 @@ var init_session2 = __esm({
2764
3322
  }
2765
3323
  }
2766
3324
  async steer(input) {
2767
- if (!this.rpc || !this.st.currentTurnId) throw new Error("\u5F53\u524D\u8F6E\u6B21\u8FD8\u6CA1\u5F00\u59CB\uFF0C\u65E0\u6CD5\u63D2\u8BDD");
2768
- const items = [{ type: "text", text: input.text }];
2769
- for (const img of input.images ?? []) {
2770
- items.push({ type: "localImage", path: await resolveImagePath(img.path) });
2771
- }
3325
+ if (!this.rpc || !this.st.currentTurnId) throw new Error(tr("codex.noTurnSteer"));
3326
+ const { items, warnings } = await buildCodexInput(input.text, input.images ?? []);
3327
+ for (const w of warnings) this.emit({ type: "notice", level: "warn", message: tr("notice.attachmentIssue", { detail: w }) });
2772
3328
  try {
2773
3329
  await this.rpc.request("turn/steer", {
2774
3330
  threadId: this.threadId(),
@@ -2813,13 +3369,13 @@ var init_session2 = __esm({
2813
3369
  this.emit({
2814
3370
  type: "permission_request",
2815
3371
  requestId,
2816
- title: `\u8FD0\u884C ${command.slice(0, 80)}`,
3372
+ title: tr("tool.run", { x: command.slice(0, 80) }),
2817
3373
  kind: "tool",
2818
3374
  detail: { command, cwd: params.cwd, reason: params.reason, approvalId: params.approvalId, itemId: params.itemId },
2819
3375
  options: [
2820
- { id: "accept", label: "\u5141\u8BB8", kind: "allow_once" },
2821
- { id: "acceptForSession", label: "\u672C\u5BF9\u8BDD\u90FD\u5141\u8BB8", kind: "allow_always" },
2822
- { id: "decline", label: "\u62D2\u7EDD", kind: "reject_once" }
3376
+ { id: "accept", label: tr("perm.allow"), kind: "allow_once" },
3377
+ { id: "acceptForSession", label: tr("perm.allowSession"), kind: "allow_always" },
3378
+ { id: "decline", label: tr("perm.deny"), kind: "reject_once" }
2823
3379
  ]
2824
3380
  });
2825
3381
  const a = await this.waitFor(requestId);
@@ -2828,18 +3384,19 @@ var init_session2 = __esm({
2828
3384
  return { decision: opt };
2829
3385
  }
2830
3386
  case "item/fileChange/requestApproval": {
2831
- const changes = Array.isArray(params.changes) ? params.changes : [];
3387
+ const itemId = typeof params.itemId === "string" ? params.itemId : "";
3388
+ const changes = Array.isArray(params.changes) && params.changes.length ? params.changes : itemId ? this.st.fileChangesByItemId.get(itemId) ?? [] : [];
2832
3389
  const requestId = "perm_" + rid3();
2833
3390
  this.emit({
2834
3391
  type: "permission_request",
2835
3392
  requestId,
2836
- title: "\u4FEE\u6539\u6587\u4EF6",
3393
+ title: tr("perm.editFiles"),
2837
3394
  kind: "tool",
2838
3395
  detail: { changes, approvalId: params.approvalId, itemId: params.itemId, reason: params.reason },
2839
3396
  options: [
2840
- { id: "accept", label: "\u5141\u8BB8", kind: "allow_once" },
2841
- { id: "acceptForSession", label: "\u672C\u5BF9\u8BDD\u90FD\u5141\u8BB8", kind: "allow_always" },
2842
- { id: "decline", label: "\u62D2\u7EDD", kind: "reject_once" }
3397
+ { id: "accept", label: tr("perm.allow"), kind: "allow_once" },
3398
+ { id: "acceptForSession", label: tr("perm.allowSession"), kind: "allow_always" },
3399
+ { id: "decline", label: tr("perm.deny"), kind: "reject_once" }
2843
3400
  ]
2844
3401
  });
2845
3402
  const a = await this.waitFor(requestId);
@@ -2880,8 +3437,8 @@ var init_session2 = __esm({
2880
3437
  async setModel(model) {
2881
3438
  this.o.model = model;
2882
3439
  }
2883
- async setMode(mode) {
2884
- this.o.mode = mode;
3440
+ async setMode(mode2) {
3441
+ this.o.mode = mode2;
2885
3442
  }
2886
3443
  async setEffort(effort) {
2887
3444
  this.o.effort = effort;
@@ -2906,7 +3463,18 @@ var init_session2 = __esm({
2906
3463
  }
2907
3464
  });
2908
3465
 
3466
+ // daemon/src/providers/types.ts
3467
+ var PLACEHOLDER_MODEL_NAME;
3468
+ var init_types = __esm({
3469
+ "daemon/src/providers/types.ts"() {
3470
+ PLACEHOLDER_MODEL_NAME = "Default";
3471
+ }
3472
+ });
3473
+
2909
3474
  // daemon/src/providers/codex/provider.ts
3475
+ import { homedir as homedir6 } from "node:os";
3476
+ import { join as join9 } from "node:path";
3477
+ import { readFile as readFile8 } from "node:fs/promises";
2910
3478
  function runVersion2(r, args, timeoutMs = 5e3) {
2911
3479
  return new Promise((resolvePromise, reject) => {
2912
3480
  let out = "";
@@ -2936,8 +3504,8 @@ function normalizeEfforts(raw) {
2936
3504
  }
2937
3505
  return out;
2938
3506
  }
2939
- async function fetchModels(r, cwd) {
2940
- const rpc = new CodexRpc(argvFor(r, ["app-server"]), { cwd });
3507
+ async function fetchModels(r, cwd, launch) {
3508
+ const rpc = new CodexRpc(argvFor(r, launchArgv(["app-server"], launch)), { cwd });
2941
3509
  try {
2942
3510
  await rpc.request("initialize", {
2943
3511
  clientInfo: { name: "agent-console", title: "Agent Console", version: "0" },
@@ -2963,39 +3531,97 @@ async function fetchModels(r, cwd) {
2963
3531
  await rpc.close();
2964
3532
  }
2965
3533
  }
3534
+ function tomlValue(raw) {
3535
+ const v = raw.trim().replace(/\s+#.*$/, "");
3536
+ const quoted = v.match(/^"([\s\S]*)"$/) ?? v.match(/^'([\s\S]*)'$/);
3537
+ if (quoted) return quoted[1];
3538
+ if (v.startsWith("[") && v.endsWith("]")) {
3539
+ return v.slice(1, -1).split(",").map((x) => tomlValue(x)).filter((x) => typeof x === "string");
3540
+ }
3541
+ return null;
3542
+ }
3543
+ function parseCodexConfig(text) {
3544
+ let section = "";
3545
+ const values = /* @__PURE__ */ new Map();
3546
+ for (const line of text.split(/\r?\n/)) {
3547
+ const s = line.trim();
3548
+ if (!s || s.startsWith("#")) continue;
3549
+ const header = s.match(/^\[([^\]]+)\]$/);
3550
+ if (header) {
3551
+ section = header[1];
3552
+ continue;
3553
+ }
3554
+ const m = s.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
3555
+ if (!m) continue;
3556
+ const value = tomlValue(m[2]);
3557
+ if (typeof value === "string" || Array.isArray(value)) values.set(`${section}\0${m[1]}`, value);
3558
+ }
3559
+ const top = (key) => values.get(`\0${key}`);
3560
+ const profile = top("profile");
3561
+ const profileName = typeof profile === "string" ? profile : null;
3562
+ const profileValue = (key) => profileName ? values.get(`profiles.${profileName}\0${key}`) : void 0;
3563
+ const modelProvider = profileValue("model_provider") ?? top("model_provider");
3564
+ const model = profileValue("model") ?? top("model");
3565
+ const provider = typeof modelProvider === "string" ? modelProvider : null;
3566
+ const models2 = /* @__PURE__ */ new Set();
3567
+ if (typeof model === "string" && model) models2.add(model);
3568
+ for (const [key, value] of values) {
3569
+ if (provider && key === `model_providers.${provider}\0models` && Array.isArray(value)) {
3570
+ for (const m of value) models2.add(m);
3571
+ }
3572
+ }
3573
+ return { modelProvider: provider, model: typeof model === "string" ? model : null, providerModels: [...models2] };
3574
+ }
3575
+ async function readCodexConfig() {
3576
+ const home = process.env.CODEX_HOME?.trim() || join9(homedir6(), ".codex");
3577
+ const file = home.toLowerCase().endsWith(".toml") ? home : join9(home, "config.toml");
3578
+ try {
3579
+ return parseCodexConfig(await readFile8(file, "utf8"));
3580
+ } catch {
3581
+ return { modelProvider: null, model: null, providerModels: [] };
3582
+ }
3583
+ }
3584
+ function isChatGptProvider(id2) {
3585
+ return !id2 || /^(openai|chatgpt)$/i.test(id2);
3586
+ }
2966
3587
  function createCodexProvider(version) {
2967
3588
  return {
2968
3589
  id: "codex",
2969
3590
  name: "Codex",
2970
3591
  capabilities: codexCapabilities,
2971
- async detect() {
2972
- const bin = await resolveBin("codex");
3592
+ async detect(launch) {
3593
+ const bin = launch ? await resolveLaunch(launch) : await resolveBin("codex");
2973
3594
  if (!bin.ok) {
2974
3595
  return { available: false, reason: bin.reason, installHint: INSTALL_HINT2 };
2975
3596
  }
2976
3597
  try {
2977
- const v = await runVersion2(bin, ["--version"]);
3598
+ const v = await runVersion2(bin, [...launch?.prefixArgs ?? [], "--version"]);
2978
3599
  return { available: true, bin: bin.display, version: v.trim().split("\n")[0] || "" };
2979
3600
  } catch (e) {
2980
3601
  return { available: true, bin: bin.display, version: "", reason: String(e) };
2981
3602
  }
2982
3603
  },
2983
- async listModels() {
2984
- if (modelCache && Date.now() - modelCache.at < MODEL_CACHE_MS) return modelCache.models;
2985
- const bin = await resolveBin("codex");
2986
- if (!bin.ok) return [{ id: "", name: "\u9ED8\u8BA4" }];
3604
+ async listModels(launch) {
3605
+ const config = await readCodexConfig();
3606
+ if (!isChatGptProvider(config.modelProvider)) {
3607
+ const rows = config.providerModels.map((id2, i) => ({ id: id2, name: id2, isDefault: i === 0 }));
3608
+ return rows.length ? rows : [{ id: "", name: `${PLACEHOLDER_MODEL_NAME} (${config.modelProvider})`, isDefault: true }];
3609
+ }
3610
+ if (!launch && modelCache && Date.now() - modelCache.at < MODEL_CACHE_MS) return modelCache.models;
3611
+ const bin = launch ? await resolveLaunch(launch) : await resolveBin("codex");
3612
+ if (!bin.ok) return [{ id: "", name: PLACEHOLDER_MODEL_NAME }];
2987
3613
  try {
2988
- const models2 = await fetchModels(bin, process.cwd());
3614
+ const models2 = await fetchModels(bin, process.cwd(), launch);
2989
3615
  modelCache = { at: Date.now(), models: models2 };
2990
3616
  return models2;
2991
3617
  } catch (e) {
2992
3618
  log("warn", "codex model/list \u5931\u8D25", { err: String(e) });
2993
- return [{ id: "", name: "\u9ED8\u8BA4" }];
3619
+ return [{ id: "", name: PLACEHOLDER_MODEL_NAME }];
2994
3620
  }
2995
3621
  },
2996
3622
  async createSession(o) {
2997
- const bin = await resolveBin("codex");
2998
- if (!bin.ok) throw new Error(`\u627E\u4E0D\u5230 codex\uFF1A${bin.reason}`);
3623
+ const bin = o.launch ? await resolveLaunch(o.launch) : await resolveBin("codex");
3624
+ if (!bin.ok) throw new Error(tr("codex.notFound", { detail: bin.reason, hint: INSTALL_HINT2 }));
2999
3625
  return new CodexSession({
3000
3626
  threadId: o.threadId,
3001
3627
  cwd: o.cwd,
@@ -3007,6 +3633,7 @@ function createCodexProvider(version) {
3007
3633
  binPrefixArgs: bin.prefixArgs,
3008
3634
  binVia: bin.via,
3009
3635
  binShim: bin.shim,
3636
+ launch: o.launch,
3010
3637
  version
3011
3638
  });
3012
3639
  },
@@ -3028,6 +3655,9 @@ var init_provider2 = __esm({
3028
3655
  init_rpc();
3029
3656
  init_session2();
3030
3657
  init_log();
3658
+ init_i18n();
3659
+ init_launch();
3660
+ init_types();
3031
3661
  INSTALL_HINT2 = "npm i -g @openai/codex";
3032
3662
  codexCapabilities = {
3033
3663
  resume: true,
@@ -3040,7 +3670,9 @@ var init_provider2 = __esm({
3040
3670
  // tokenUsage yes, cost no
3041
3671
  effort: [],
3042
3672
  // effort values depend on the model — filled from model/list after detect
3043
- modes: ["plan", "default", "acceptEdits", "bypassPermissions"]
3673
+ // No "plan": app-server 0.16x has no plan/collaboration mode on turn/start, and a read-only "plan" was the same preset as default (B9)
3674
+ modes: ["default", "acceptEdits", "bypassPermissions"],
3675
+ modeLabels: { default: "Read Only", acceptEdits: "Auto", bypassPermissions: "Full Access" }
3044
3676
  };
3045
3677
  modelCache = null;
3046
3678
  MODEL_CACHE_MS = 10 * 60 * 1e3;
@@ -3048,8 +3680,8 @@ var init_provider2 = __esm({
3048
3680
  });
3049
3681
 
3050
3682
  // daemon/src/providers/catalog.ts
3051
- function catalogById(id) {
3052
- return PROVIDER_CATALOG.find((p) => p.id === id);
3683
+ function catalogById(id2) {
3684
+ return PROVIDER_CATALOG.find((p) => p.id === id2);
3053
3685
  }
3054
3686
  function providerCreatable(status) {
3055
3687
  return status !== "planned";
@@ -3059,12 +3691,12 @@ var init_catalog = __esm({
3059
3691
  "daemon/src/providers/catalog.ts"() {
3060
3692
  PROVIDER_CATALOG = [
3061
3693
  { id: "claude", name: "Claude Code", bins: ["claude"], installHint: "npm i -g @anthropic-ai/claude-code", status: "verified" },
3062
- { id: "codex", name: "Codex", bins: ["codex"], installHint: "\u89C1 OpenAI Codex CLI \u5B89\u88C5\u8BF4\u660E", status: "verified" },
3063
- { id: "cursor", name: "Cursor", bins: ["cursor-agent", "cursor"], installHint: "\u5B89\u88C5 Cursor \u5E76\u786E\u4FDD CLI \u5728 PATH", status: "verified" },
3064
- { id: "copilot", name: "Copilot", bins: ["copilot"], installHint: "GitHub Copilot CLI \u5B89\u88C5\u8BF4\u660E", status: "verified" },
3065
- { id: "opencode", name: "OpenCode", bins: ["opencode"], installHint: "npm i -g opencode / \u89C1 OpenCode \u6587\u6863", status: "verified" },
3066
- { id: "pi", name: "Pi", bins: ["pi-acp", "pi", "pi-coding-agent"], installHint: "npm i -g pi-acp\uFF08\u6216 Pi coding agent + ACP \u9002\u914D\u5668\uFF09", status: "verified" },
3067
- { id: "devin", name: "Devin CLI", bins: ["devin"], installHint: "Devin CLI \u5B98\u65B9\u5B89\u88C5", status: "verified" },
3694
+ { id: "codex", name: "Codex", bins: ["codex"], installHint: "npm i -g @openai/codex", status: "verified" },
3695
+ { id: "cursor", name: "Cursor", bins: ["cursor-agent", "cursor"], installHint: "curl https://cursor.com/install -fsS | bash", status: "verified" },
3696
+ { id: "copilot", name: "Copilot", bins: ["copilot"], installHint: "npm i -g @github/copilot", status: "verified" },
3697
+ { id: "opencode", name: "OpenCode", bins: ["opencode"], installHint: "npm i -g opencode-ai", status: "verified" },
3698
+ { id: "pi", name: "Pi", bins: ["pi-acp", "pi", "pi-coding-agent"], installHint: "npm i -g pi-acp", status: "verified" },
3699
+ { id: "devin", name: "Devin CLI", bins: ["devin"], installHint: "https://docs.devin.ai/cli", status: "verified" },
3068
3700
  { id: "dsh", name: "dsh", bins: ["dsh"], installHint: "npm i -g @deepseek-ai/dsh", status: "verified" }
3069
3701
  ];
3070
3702
  }
@@ -3101,11 +3733,11 @@ var init_spawn = __esm({
3101
3733
  });
3102
3734
 
3103
3735
  // daemon/src/providers/acp/clientHandlers.ts
3104
- import { readFile as readFile8, writeFile as writeFile2 } from "node:fs/promises";
3736
+ import { readFile as readFile9, writeFile as writeFile2 } from "node:fs/promises";
3105
3737
  import { methods } from "@agentclientprotocol/sdk";
3106
3738
  function registerAcpClientHandlers(app, bridge) {
3107
3739
  app.onRequest(methods.client.fs.readTextFile, async (ctx) => {
3108
- const text = await readFile8(ctx.params.path, "utf8");
3740
+ const text = await readFile9(ctx.params.path, "utf8");
3109
3741
  return { content: text };
3110
3742
  });
3111
3743
  app.onRequest(methods.client.fs.writeTextFile, async (ctx) => {
@@ -3127,7 +3759,7 @@ function registerAcpClientHandlers(app, bridge) {
3127
3759
  bridge.push({
3128
3760
  type: "permission_request",
3129
3761
  requestId,
3130
- title: tc.title ?? tc.name ?? "\u9700\u8981\u6388\u6743",
3762
+ title: tc.title ?? tc.name ?? tr("tool.needsApproval"),
3131
3763
  kind: tc.kind ?? "tool",
3132
3764
  detail: tc.rawInput ?? void 0,
3133
3765
  options
@@ -3141,6 +3773,7 @@ function registerAcpClientHandlers(app, bridge) {
3141
3773
  }
3142
3774
  var init_clientHandlers = __esm({
3143
3775
  "daemon/src/providers/acp/clientHandlers.ts"() {
3776
+ init_i18n();
3144
3777
  }
3145
3778
  });
3146
3779
 
@@ -3169,7 +3802,13 @@ function textFromContent(content) {
3169
3802
  return "";
3170
3803
  }
3171
3804
  function newMapCtx() {
3172
- return { itemPrefix: "a" + rid4() + "_", messageIds: /* @__PURE__ */ new Map() };
3805
+ return { itemPrefix: "a" + rid4() + "_", messageIds: /* @__PURE__ */ new Map(), seg: 0, lastWasText: false };
3806
+ }
3807
+ function endTextSegment(ctx) {
3808
+ if (ctx.lastWasText) {
3809
+ ctx.seg++;
3810
+ ctx.lastWasText = false;
3811
+ }
3173
3812
  }
3174
3813
  function mapSessionUpdate(update, ctx) {
3175
3814
  const out = [];
@@ -3178,19 +3817,22 @@ function mapSessionUpdate(update, ctx) {
3178
3817
  case "agent_message_chunk": {
3179
3818
  const text = textFromContent(update.content);
3180
3819
  if (!text) break;
3181
- const itemId = mid ? ctx.messageIds.get(mid) ?? ctx.itemPrefix + mid : ctx.itemPrefix + "msg";
3820
+ const itemId = `${ctx.itemPrefix}${mid ?? "msg"}_${ctx.seg}`;
3182
3821
  if (mid) ctx.messageIds.set(mid, itemId);
3822
+ ctx.lastWasText = true;
3183
3823
  out.push({ type: "assistant_delta", itemId, text });
3184
3824
  break;
3185
3825
  }
3186
3826
  case "agent_thought_chunk": {
3187
3827
  const text = textFromContent(update.content);
3188
3828
  if (!text) break;
3189
- const itemId = mid ? ctx.itemPrefix + "t_" + mid : ctx.itemPrefix + "think";
3829
+ const itemId = `${ctx.itemPrefix}t_${mid ?? "think"}_${ctx.seg}`;
3830
+ ctx.lastWasText = true;
3190
3831
  out.push({ type: "reasoning_delta", itemId, text });
3191
3832
  break;
3192
3833
  }
3193
3834
  case "tool_call":
3835
+ endTextSegment(ctx);
3194
3836
  out.push({
3195
3837
  type: "tool_call",
3196
3838
  itemId: ctx.itemPrefix + update.toolCallId,
@@ -3201,17 +3843,19 @@ function mapSessionUpdate(update, ctx) {
3201
3843
  });
3202
3844
  break;
3203
3845
  case "tool_call_update":
3846
+ endTextSegment(ctx);
3204
3847
  out.push({
3205
3848
  type: "tool_call",
3206
3849
  itemId: ctx.itemPrefix + update.toolCallId,
3207
3850
  kind: mapToolKind(update.kind),
3208
- title: update.title ?? update.name ?? "tool",
3851
+ title: update.title ?? update.name ?? "",
3209
3852
  status: mapToolStatus(update.status),
3210
3853
  input: update.rawInput,
3211
3854
  output: typeof update.rawOutput === "string" ? update.rawOutput : void 0
3212
3855
  });
3213
3856
  break;
3214
3857
  case "plan":
3858
+ endTextSegment(ctx);
3215
3859
  out.push({
3216
3860
  type: "plan",
3217
3861
  itemId: ctx.itemPrefix + "plan",
@@ -3257,6 +3901,125 @@ var init_mapUpdate = __esm({
3257
3901
  }
3258
3902
  });
3259
3903
 
3904
+ // daemon/src/providers/acp/promptCaps.ts
3905
+ function promptCapsFromInitialize(res) {
3906
+ const pc = res?.agentCapabilities?.promptCapabilities;
3907
+ return {
3908
+ image: pc?.image === true,
3909
+ audio: pc?.audio === true,
3910
+ embeddedContext: pc?.embeddedContext === true
3911
+ };
3912
+ }
3913
+ function rememberPromptCaps(providerId, caps) {
3914
+ if (providerId) learned.set(providerId, caps);
3915
+ }
3916
+ function knownPromptCaps(providerId) {
3917
+ return learned.get(providerId);
3918
+ }
3919
+ var learned;
3920
+ var init_promptCaps = __esm({
3921
+ "daemon/src/providers/acp/promptCaps.ts"() {
3922
+ learned = /* @__PURE__ */ new Map();
3923
+ }
3924
+ });
3925
+
3926
+ // daemon/src/providers/acp/sessionCaps.ts
3927
+ function sessionOpenFromInitialize(res) {
3928
+ const caps = res?.agentCapabilities;
3929
+ const resume = caps?.sessionCapabilities?.resume != null && typeof caps.sessionCapabilities.resume === "object";
3930
+ return { load: caps?.loadSession === true, resume };
3931
+ }
3932
+ function pickReconnect(open) {
3933
+ if (open.load) return "load";
3934
+ if (open.resume) return "resume";
3935
+ return "none";
3936
+ }
3937
+ var init_sessionCaps = __esm({
3938
+ "daemon/src/providers/acp/sessionCaps.ts"() {
3939
+ }
3940
+ });
3941
+
3942
+ // daemon/src/providers/acp/prompt.ts
3943
+ import { readFile as readFile10, stat as stat7 } from "node:fs/promises";
3944
+ import { basename as basename4 } from "node:path";
3945
+ import { pathToFileURL } from "node:url";
3946
+ async function buildAcpPrompt(text, attachments, caps) {
3947
+ const media = [];
3948
+ const links = [];
3949
+ const pathLines = [];
3950
+ const imagesByPath = [];
3951
+ const warnings = [];
3952
+ let inlineBytes = 0;
3953
+ const byPath = (file, mime, size, label) => {
3954
+ links.push({ type: "resource_link", uri: pathToFileURL(file).href, name: basename4(file), mimeType: mime, ...size !== null ? { size } : {} });
3955
+ pathLines.push(`${label}: ${file}`);
3956
+ };
3957
+ for (const a of attachments) {
3958
+ const mime = a.mime || "application/octet-stream";
3959
+ let file;
3960
+ let size = null;
3961
+ try {
3962
+ file = await resolveImagePath(a.path);
3963
+ size = (await stat7(file)).size;
3964
+ } catch (e) {
3965
+ warnings.push(`${a.path}: ${String(e?.message ?? e)}`);
3966
+ continue;
3967
+ }
3968
+ const image = attachmentKind(mime) === "image";
3969
+ const audio = mime.startsWith("audio/");
3970
+ if (image && caps.image || audio && caps.audio) {
3971
+ if (size > MAX_INLINE_MEDIA_BYTES || inlineBytes + size > MAX_INLINE_TOTAL_BYTES) {
3972
+ const limit = size > MAX_INLINE_MEDIA_BYTES ? MAX_INLINE_MEDIA_BYTES : MAX_INLINE_TOTAL_BYTES;
3973
+ warnings.push(`${basename4(file)}: ${(size / 1024 / 1024).toFixed(1)} MB, over the ${limit / 1024 / 1024} MB inline limit; sent as a file path`);
3974
+ byPath(file, mime, size, image ? "Attached image" : "Attached audio");
3975
+ if (image) imagesByPath.push(file);
3976
+ continue;
3977
+ }
3978
+ try {
3979
+ const data = (await readFile10(file)).toString("base64");
3980
+ media.push(image ? { type: "image", data, mimeType: mime } : { type: "audio", data, mimeType: mime });
3981
+ inlineBytes += size;
3982
+ } catch (e) {
3983
+ warnings.push(`${basename4(file)}: ${String(e?.message ?? e)}`);
3984
+ byPath(file, mime, size, image ? "Attached image" : "Attached audio");
3985
+ if (image) imagesByPath.push(file);
3986
+ }
3987
+ continue;
3988
+ }
3989
+ if (image || audio) {
3990
+ byPath(file, mime, size, image ? "Attached image" : "Attached audio");
3991
+ if (image) imagesByPath.push(file);
3992
+ continue;
3993
+ }
3994
+ if (isText(mime) && caps.embeddedContext && size <= MAX_EMBED_TEXT_BYTES && inlineBytes + size <= MAX_INLINE_TOTAL_BYTES) {
3995
+ try {
3996
+ const body = await readFile10(file, "utf8");
3997
+ media.push({ type: "resource", resource: { uri: pathToFileURL(file).href, mimeType: mime, text: body } });
3998
+ inlineBytes += size;
3999
+ continue;
4000
+ } catch {
4001
+ }
4002
+ }
4003
+ byPath(file, mime, size, "Attached file");
4004
+ }
4005
+ const fullText = [text, ...pathLines.length ? [pathLines.join("\n")] : []].filter(Boolean).join("\n\n");
4006
+ const blocks = [];
4007
+ if (fullText) blocks.push({ type: "text", text: fullText });
4008
+ blocks.push(...media, ...links);
4009
+ return { blocks, imagesByPath, warnings };
4010
+ }
4011
+ var MAX_INLINE_MEDIA_BYTES, MAX_INLINE_TOTAL_BYTES, MAX_EMBED_TEXT_BYTES, isText;
4012
+ var init_prompt = __esm({
4013
+ "daemon/src/providers/acp/prompt.ts"() {
4014
+ init_attachmentRefs();
4015
+ init_attachmentKinds();
4016
+ MAX_INLINE_MEDIA_BYTES = 10 * 1024 * 1024;
4017
+ MAX_INLINE_TOTAL_BYTES = 16 * 1024 * 1024;
4018
+ MAX_EMBED_TEXT_BYTES = 1024 * 1024;
4019
+ isText = (mime) => attachmentKind(mime) === "text";
4020
+ }
4021
+ });
4022
+
3260
4023
  // daemon/src/providers/acp/session.ts
3261
4024
  import { client, methods as methods2, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
3262
4025
  var AcpSession;
@@ -3265,14 +4028,20 @@ var init_session3 = __esm({
3265
4028
  init_slash();
3266
4029
  init_queue();
3267
4030
  init_bin();
4031
+ init_launch();
3268
4032
  init_spawn();
3269
4033
  init_clientHandlers();
3270
4034
  init_mapUpdate();
4035
+ init_promptCaps();
4036
+ init_sessionCaps();
4037
+ init_prompt();
4038
+ init_i18n();
3271
4039
  init_log();
3272
4040
  AcpSession = class {
3273
4041
  constructor(o) {
3274
4042
  this.o = o;
3275
4043
  this.events = this.q;
4044
+ this.startModel = o.model;
3276
4045
  this.cursorFast = o.cursorFast ?? false;
3277
4046
  this.cursorAutoReview = false;
3278
4047
  this.commandsReady = new Promise((r) => this.settleCommands = r);
@@ -3300,22 +4069,19 @@ var init_session3 = __esm({
3300
4069
  slashCommands = [];
3301
4070
  activeTurnId = null;
3302
4071
  configOptions = null;
4072
+ // initialize → agentCapabilities.promptCapabilities (ACP baseline until initialize answers: text + resource_link)
4073
+ promptCaps = { image: false, audio: false, embeddedContext: false };
4074
+ initCaps = this.promptCaps;
4075
+ startModel = null;
3303
4076
  wakePump = null;
3304
4077
  // listCommands waits for the first batch of commands (or confirmation there are none), following Paseo's waitForInitialCommands
3305
4078
  commandsReady;
3306
4079
  settleCommands;
3307
4080
  async boot() {
3308
- const bin = this.o.launch ? {
3309
- ok: true,
3310
- cmd: this.o.launch.cmd,
3311
- prefixArgs: this.o.launch.prefixArgs,
3312
- display: this.o.launch.cmd,
3313
- via: "custom",
3314
- binName: "custom"
3315
- } : await resolveFirstBin(this.o.binCandidates?.length ? this.o.binCandidates : [this.o.binName]);
4081
+ const bin = this.o.launch ? await resolveLaunch(this.o.launch).then((r) => r.ok ? { ...r, binName: "custom" } : r) : await resolveFirstBin(this.o.binCandidates?.length ? this.o.binCandidates : [this.o.binName]);
3316
4082
  if (!bin.ok) throw new Error(bin.reason);
3317
4083
  try {
3318
- this.child = spawnResolved(bin, this.o.acpArgs, { cwd: this.o.cwd });
4084
+ this.child = spawnResolved(bin, launchArgv(this.o.acpArgs, this.o.launch), { cwd: this.o.cwd });
3319
4085
  this.child.stderr?.on("data", (d) => {
3320
4086
  const t = d.toString().trim();
3321
4087
  if (t) log("info", `[${this.o.binName} acp] ${t.slice(0, 500)}`);
@@ -3323,37 +4089,50 @@ var init_session3 = __esm({
3323
4089
  this.child.on("error", (e) => this.failInit(e));
3324
4090
  this.child.on("close", (code) => {
3325
4091
  if (!this.closed && code !== 0) {
3326
- this.q.push({ type: "notice", level: "error", message: `${this.o.binName} \u8FDB\u7A0B\u9000\u51FA (${code})` });
4092
+ this.q.push({ type: "notice", level: "error", message: tr("acp.exited", { name: this.o.binName, code: String(code) }) });
3327
4093
  }
3328
4094
  });
3329
4095
  const app = client({ name: "magpie-agent-console" });
3330
4096
  registerAcpClientHandlers(app, {
3331
4097
  push: (ev) => this.q.push(ev),
3332
- waitFor: (id) => this.waitPerm(id),
4098
+ waitFor: (id2) => this.waitPerm(id2),
3333
4099
  autoAcceptPermissions: () => this.cursorAutoReview
3334
4100
  });
3335
4101
  const stream = acpStreamFromChild(this.child);
3336
4102
  this.conn = app.connect(stream);
3337
4103
  this.ctx = this.conn.agent;
3338
- await this.ctx.request(methods2.agent.initialize, {
4104
+ const initRes = await this.ctx.request(methods2.agent.initialize, {
3339
4105
  protocolVersion: PROTOCOL_VERSION,
3340
4106
  // Cursor: declare support for the parameterized model picker (as Paseo does); only then does it expose levels / Fast as separate config options
3341
4107
  clientCapabilities: this.o.flavor === "cursor" ? { _meta: { parameterizedModelPicker: true } } : {},
3342
4108
  clientInfo: { name: "Agent Console", version: this.o.appVersion }
3343
4109
  });
4110
+ this.promptCaps = promptCapsFromInitialize(initRes);
4111
+ this.initCaps = this.promptCaps;
4112
+ rememberPromptCaps(this.o.providerId, this.promptCaps);
3344
4113
  const cwd = absCwd(this.o.cwd);
3345
4114
  const ctx = this.ctx;
3346
4115
  if (!ctx) throw new Error("ACP \u672A\u8FDE\u63A5");
3347
4116
  if (this.o.resumeId) {
3348
- const loaded = await ctx.request(methods2.agent.session.load, {
3349
- sessionId: this.o.resumeId,
3350
- cwd,
3351
- mcpServers: []
3352
- });
3353
- this.active = ctx.attachSession({
3354
- sessionId: this.o.resumeId,
3355
- modes: loaded.modes
3356
- });
4117
+ const how = pickReconnect(sessionOpenFromInitialize(initRes));
4118
+ if (how === "load" || how === "resume") {
4119
+ const method = how === "load" ? methods2.agent.session.load : methods2.agent.session.resume;
4120
+ const restored = await ctx.request(method, {
4121
+ sessionId: this.o.resumeId,
4122
+ cwd,
4123
+ mcpServers: []
4124
+ });
4125
+ this.active = ctx.attachSession({
4126
+ sessionId: this.o.resumeId,
4127
+ modes: restored.modes,
4128
+ configOptions: restored.configOptions
4129
+ });
4130
+ if (Array.isArray(restored.configOptions)) this.configOptions = restored.configOptions;
4131
+ } else {
4132
+ this.active = await ctx.buildSession(cwd).start();
4133
+ const agent = this.o.providerName ?? this.o.binName;
4134
+ this.q.push({ type: "notice", level: "warn", message: tr("notice.acpSessionNotRestored", { agent }) });
4135
+ }
3357
4136
  } else {
3358
4137
  this.active = await ctx.buildSession(cwd).start();
3359
4138
  }
@@ -3441,9 +4220,9 @@ var init_session3 = __esm({
3441
4220
  }
3442
4221
  return (this.active?.modes?.availableModes ?? []).map((m) => ({ id: m.id, name: m.name, configId: null }));
3443
4222
  }
3444
- resolveAcpModeId(mode) {
4223
+ resolveAcpModeId(mode2) {
3445
4224
  if (this.o.modeMap) {
3446
- const want = this.o.modeMap[mode];
4225
+ const want = this.o.modeMap[mode2];
3447
4226
  if (!want) return null;
3448
4227
  const hit = this.modeChoices().find(
3449
4228
  (m) => m.id === want || m.id.endsWith(`#${want}`) || m.name.toLowerCase() === want
@@ -3452,24 +4231,24 @@ var init_session3 = __esm({
3452
4231
  }
3453
4232
  if (this.o.flavor === "cursor") {
3454
4233
  const map = { default: "agent", plan: "plan", ask: "ask" };
3455
- const want = map[mode] ?? "agent";
4234
+ const want = map[mode2] ?? "agent";
3456
4235
  const avail = this.active?.modes?.availableModes ?? [];
3457
4236
  const hit = avail.find((m) => m.id === want || m.name.toLowerCase() === want);
3458
4237
  return hit?.id ?? want;
3459
4238
  }
3460
- return mode === "plan" ? "plan" : "default";
4239
+ return mode2 === "plan" ? "plan" : "default";
3461
4240
  }
3462
- async applySessionMode(mode) {
4241
+ async applySessionMode(mode2) {
3463
4242
  if (!this.ctx || !this.active) return;
3464
4243
  const hasAllowAll = this.currentConfig().some((o) => o.id === "allow_all");
3465
- if (this.o.modeMap?.[mode] === "allow-all") {
4244
+ if (this.o.modeMap?.[mode2] === "allow-all") {
3466
4245
  const agentId = this.modeChoices().find((m) => m.id === "agent" || m.id.endsWith("#agent"));
3467
4246
  if (agentId?.configId) await this.setOption(agentId.configId, agentId.id);
3468
4247
  if (hasAllowAll) await this.setOption("allow_all", "on");
3469
4248
  return;
3470
4249
  }
3471
4250
  if (hasAllowAll && this.o.modeMap) await this.setOption("allow_all", "off");
3472
- const modeId = this.resolveAcpModeId(mode);
4251
+ const modeId = this.resolveAcpModeId(mode2);
3473
4252
  if (!modeId) return;
3474
4253
  try {
3475
4254
  const viaConfig = this.modeChoices().find((m) => m.id === modeId)?.configId;
@@ -3482,7 +4261,7 @@ var init_session3 = __esm({
3482
4261
  modeId
3483
4262
  });
3484
4263
  } catch (e) {
3485
- log("info", `ACP setMode(${mode}): ${String(e)}`);
4264
+ log("info", `ACP setMode(${mode2}): ${String(e)}`);
3486
4265
  }
3487
4266
  }
3488
4267
  /**
@@ -3562,19 +4341,32 @@ var init_session3 = __esm({
3562
4341
  effort: this.o.effort
3563
4342
  });
3564
4343
  const text = input.text.trim();
3565
- if (!text) {
4344
+ const attachments = input.images ?? [];
4345
+ if (!text && !attachments.length) {
4346
+ this.running = false;
4347
+ this.q.push({ type: "turn_ended", turnId: this.turnId, status: "completed" });
4348
+ return;
4349
+ }
4350
+ const built = await buildAcpPrompt(text, attachments, this.promptCaps);
4351
+ const agent = this.o.providerName ?? this.o.binName;
4352
+ if (built.imagesByPath.length && !this.promptCaps.image) {
4353
+ const switched = this.o.imagesFollowStartModel && this.initCaps.image && this.o.model !== this.startModel;
4354
+ this.q.push({ type: "notice", level: "warn", message: tr(switched ? "notice.acpImageByPathSwitched" : "notice.acpImageByPath", { agent, paths: built.imagesByPath.join(", ") }) });
4355
+ }
4356
+ for (const w of built.warnings) this.q.push({ type: "notice", level: "warn", message: tr("notice.attachmentIssue", { detail: w }) });
4357
+ if (!built.blocks.length) {
3566
4358
  this.running = false;
3567
4359
  this.q.push({ type: "turn_ended", turnId: this.turnId, status: "completed" });
3568
4360
  return;
3569
4361
  }
3570
- void this.runTurn(text);
4362
+ void this.runTurn(built.blocks);
3571
4363
  }
3572
4364
  // Send this turn's prompt; reading updates is the job of the persistent pumpUpdates() (following Paseo: session/update is handled at session level)
3573
- async runTurn(text) {
4365
+ async runTurn(prompt) {
3574
4366
  this.activeTurnId = this.turnId;
3575
4367
  this.wakePump?.();
3576
4368
  this.wakePump = null;
3577
- this.active.prompt(text).catch(() => {
4369
+ this.active.prompt(prompt).catch(() => {
3578
4370
  });
3579
4371
  }
3580
4372
  endTurn(ev) {
@@ -3628,12 +4420,15 @@ var init_session3 = __esm({
3628
4420
  async setModel(model) {
3629
4421
  this.o.model = model;
3630
4422
  await this.ready();
4423
+ if (this.o.imagesFollowStartModel) {
4424
+ this.promptCaps = model === this.startModel ? this.initCaps : { ...this.initCaps, image: false };
4425
+ }
3631
4426
  await this.applySessionModel(model);
3632
4427
  }
3633
- async setMode(mode) {
3634
- this.o.mode = mode;
4428
+ async setMode(mode2) {
4429
+ this.o.mode = mode2;
3635
4430
  await this.ready();
3636
- await this.applySessionMode(mode);
4431
+ await this.applySessionMode(mode2);
3637
4432
  }
3638
4433
  async setEffort(effort) {
3639
4434
  this.o.effort = effort;
@@ -3706,34 +4501,38 @@ function createAcpProvider(cfg2) {
3706
4501
  return {
3707
4502
  id: cfg2.id,
3708
4503
  name: cfg2.name,
3709
- capabilities: caps,
3710
- async detect() {
3711
- const r = await resolveProviderBin(cfg2);
4504
+ // images is what the Agent's own initialize says (probe at warm-up / providers.list, or any session); until then unknown
4505
+ get capabilities() {
4506
+ const learned2 = knownPromptCaps(cfg2.id);
4507
+ return learned2 ? { ...caps, images: learned2.image, imagesKnown: true } : { ...caps, images: false, imagesKnown: false };
4508
+ },
4509
+ async detect(launch) {
4510
+ const r = launch ? await resolveLaunch(launch) : await resolveProviderBin(cfg2);
3712
4511
  if (!r.ok) {
3713
4512
  return { available: false, reason: r.reason, installHint: cfg2.installHint };
3714
4513
  }
3715
4514
  let version;
4515
+ const versionArgs = cfg2.id === "devin" ? ["version"] : ["cursor", "opencode"].includes(cfg2.id) ? ["--version"] : null;
3716
4516
  try {
3717
- if (cfg2.id === "cursor") {
3718
- version = (await runVersion3(r, ["--version"])).split(/\r?\n/)[0]?.trim();
3719
- } else if (cfg2.id === "devin") {
3720
- version = (await runVersion3(r, ["version"])).split(/\r?\n/)[0]?.trim();
3721
- } else if (cfg2.id === "opencode") {
3722
- version = (await runVersion3(r, ["--version"])).split(/\r?\n/)[0]?.trim();
4517
+ if (versionArgs) {
4518
+ version = (await runVersion3(r, [...launch?.prefixArgs ?? [], ...versionArgs])).split(/\r?\n/)[0]?.trim();
3723
4519
  }
3724
4520
  } catch {
3725
4521
  }
3726
4522
  return { available: true, bin: r.display, version };
3727
4523
  },
3728
- async listModels() {
3729
- const r = await resolveProviderBin(cfg2);
3730
- if (!r.ok) return [{ id: "default", name: "\u9ED8\u8BA4", isDefault: true }];
3731
- if (cfg2.fetchModels) return cfg2.fetchModels(r);
3732
- return [{ id: "default", name: "\u9ED8\u8BA4", isDefault: true }];
4524
+ async listModels(launch) {
4525
+ const resolved = launch ? await resolveLaunch(launch) : await resolveProviderBin(cfg2);
4526
+ if (!resolved.ok) return [{ id: "default", name: PLACEHOLDER_MODEL_NAME, isDefault: true }];
4527
+ const r = launch?.prefixArgs?.length && resolved.via !== "cmd-shim" ? { ...resolved, prefixArgs: [...resolved.prefixArgs, ...launch.prefixArgs] } : resolved;
4528
+ if (cfg2.fetchModels) return cfg2.fetchModels(r, launch?.args ?? []);
4529
+ return [{ id: "default", name: PLACEHOLDER_MODEL_NAME, isDefault: true }];
3733
4530
  },
3734
4531
  async createSession(o) {
3735
4532
  return new AcpSession({
3736
4533
  threadId: o.threadId,
4534
+ providerId: cfg2.id,
4535
+ providerName: cfg2.name,
3737
4536
  cwd: o.cwd,
3738
4537
  model: o.model,
3739
4538
  mode: o.mode,
@@ -3741,7 +4540,8 @@ function createAcpProvider(cfg2) {
3741
4540
  resumeId: o.resumeId,
3742
4541
  binName: cfg2.binName,
3743
4542
  binCandidates: cfg2.binCandidates ?? [cfg2.binName],
3744
- acpArgs: cfg2.acpArgs,
4543
+ acpArgs: cfg2.argsFor ? await cfg2.argsFor(o.model) : cfg2.acpArgs,
4544
+ imagesFollowStartModel: cfg2.imagesFollowStartModel,
3745
4545
  appVersion: cfg2.appVersion ?? "0.0.1",
3746
4546
  flavor: cfg2.flavor ?? "generic",
3747
4547
  modeMap: cfg2.modeMap,
@@ -3757,7 +4557,10 @@ var init_provider3 = __esm({
3757
4557
  "daemon/src/providers/acp/provider.ts"() {
3758
4558
  init_bin();
3759
4559
  init_bin();
4560
+ init_launch();
4561
+ init_types();
3760
4562
  init_session3();
4563
+ init_promptCaps();
3761
4564
  acpCapabilities = {
3762
4565
  resume: true,
3763
4566
  interrupt: true,
@@ -3767,7 +4570,8 @@ var init_provider3 = __esm({
3767
4570
  questions: false,
3768
4571
  cost: false,
3769
4572
  effort: [],
3770
- modes: ["plan", "default", "acceptEdits", "bypassPermissions"]
4573
+ modes: ["plan", "default", "acceptEdits", "bypassPermissions"],
4574
+ deltasOnly: true
3771
4575
  };
3772
4576
  cursorAcpCapabilities = {
3773
4577
  ...acpCapabilities,
@@ -3777,8 +4581,53 @@ var init_provider3 = __esm({
3777
4581
  }
3778
4582
  });
3779
4583
 
3780
- // daemon/src/providers/acp/probe.ts
4584
+ // daemon/src/providers/acp/dsh.ts
4585
+ import { createHash as createHash2 } from "node:crypto";
4586
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
3781
4587
  import { tmpdir } from "node:os";
4588
+ import { join as join10 } from "node:path";
4589
+ function dshRoute(model) {
4590
+ if (!model) return null;
4591
+ try {
4592
+ const v = JSON.parse(model);
4593
+ if (Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string" && v[0] && v[1]) {
4594
+ return { provider: v[0], model: v[1] };
4595
+ }
4596
+ } catch {
4597
+ }
4598
+ return null;
4599
+ }
4600
+ function dshRoutePatch(route) {
4601
+ return [
4602
+ "# Agent Console: the route dsh's ACP starts on, so initialize advertises image input for it",
4603
+ "- id: acp",
4604
+ " config:",
4605
+ ` provider: ${JSON.stringify(route.provider)}`,
4606
+ ` model: ${JSON.stringify(route.model)}`,
4607
+ ""
4608
+ ].join("\n");
4609
+ }
4610
+ async function dshAcpArgs(model, dir = join10(tmpdir(), "agent-console-dsh")) {
4611
+ const route = dshRoute(model);
4612
+ if (!route) return [...DSH_ACP_ARGS];
4613
+ const body = dshRoutePatch(route);
4614
+ const file = join10(dir, `acp-route-${createHash2("sha256").update(body).digest("hex").slice(0, 16)}.yml`);
4615
+ await mkdir2(dir, { recursive: true });
4616
+ await writeFile3(file, body, { mode: 384 });
4617
+ return [...DSH_ACP_ARGS, "--patch", file];
4618
+ }
4619
+ function dshDefaultModel(models2, current) {
4620
+ return models2.find((m) => /magpie/i.test(m.id))?.id ?? current ?? models2[0]?.id ?? null;
4621
+ }
4622
+ var DSH_ACP_ARGS;
4623
+ var init_dsh = __esm({
4624
+ "daemon/src/providers/acp/dsh.ts"() {
4625
+ DSH_ACP_ARGS = ["--profile", "acp"];
4626
+ }
4627
+ });
4628
+
4629
+ // daemon/src/providers/acp/probe.ts
4630
+ import { tmpdir as tmpdir2 } from "node:os";
3782
4631
  function flatten(opts) {
3783
4632
  const out = [];
3784
4633
  for (const o of opts ?? []) {
@@ -3804,12 +4653,15 @@ function parseAcpSession(res) {
3804
4653
  const currentThinking = typeof thoughtOpt?.currentValue === "string" ? thoughtOpt.currentValue : null;
3805
4654
  return { models: models2, currentModel, thinking, currentThinking };
3806
4655
  }
3807
- function probeAcp(r, acpArgs, timeoutMs = 3e4) {
3808
- return probeAcpRaw(r, acpArgs, timeoutMs).then((x) => x.parsed);
4656
+ function probeAcp(r, acpArgs, timeoutMs = 3e4, providerId) {
4657
+ return probeAcpRaw(r, acpArgs, timeoutMs).then((x) => {
4658
+ rememberPromptCaps(providerId, x.promptCaps);
4659
+ return x.parsed;
4660
+ });
3809
4661
  }
3810
4662
  function probeAcpRaw(r, acpArgs, timeoutMs = 3e4, extMethod) {
3811
4663
  return new Promise((resolvePromise, reject) => {
3812
- const child = spawnResolved(r, acpArgs, { cwd: tmpdir() });
4664
+ const child = spawnResolved(r, acpArgs, { cwd: tmpdir2() });
3813
4665
  let buf = "";
3814
4666
  let nextId = 0;
3815
4667
  const pending = /* @__PURE__ */ new Map();
@@ -3825,9 +4677,9 @@ function probeAcpRaw(r, acpArgs, timeoutMs = 3e4, extMethod) {
3825
4677
  };
3826
4678
  const timer = setTimeout(() => finish(new Error("ACP \u63A2\u6D4B\u8D85\u65F6")), timeoutMs);
3827
4679
  const send2 = (method, params) => new Promise((res, rej) => {
3828
- const id = ++nextId;
3829
- pending.set(id, { res, rej });
3830
- child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
4680
+ const id2 = ++nextId;
4681
+ pending.set(id2, { res, rej });
4682
+ child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", id: id2, method, params }) + "\n");
3831
4683
  });
3832
4684
  child.on("error", (e) => finish(e));
3833
4685
  child.on("close", () => finish(new Error("ACP \u8FDB\u7A0B\u63D0\u524D\u9000\u51FA")));
@@ -3858,11 +4710,11 @@ function probeAcpRaw(r, acpArgs, timeoutMs = 3e4, extMethod) {
3858
4710
  });
3859
4711
  void (async () => {
3860
4712
  try {
3861
- await send2("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "Agent Console", version: "probe" } });
3862
- const res = await send2("session/new", { cwd: tmpdir(), mcpServers: [] });
4713
+ const init = await send2("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "Agent Console", version: "probe" } });
4714
+ const res = await send2("session/new", { cwd: tmpdir2(), mcpServers: [] });
3863
4715
  let ext = null;
3864
4716
  if (extMethod) ext = await send2(extMethod, {}).catch(() => null);
3865
- finish(null, { parsed: parseAcpSession(res ?? {}), ext });
4717
+ finish(null, { parsed: parseAcpSession(res ?? {}), ext, promptCaps: promptCapsFromInitialize(init) });
3866
4718
  } catch (e) {
3867
4719
  finish(e instanceof Error ? e : new Error(String(e)));
3868
4720
  }
@@ -3882,13 +4734,13 @@ function cursorCatalogToModels(catalog, currentModel) {
3882
4734
  const out = [];
3883
4735
  for (const m of catalog.models ?? []) {
3884
4736
  if (typeof m.value !== "string" || !m.value) continue;
3885
- const id = m.value.replace(/\[.*\]$/u, "");
4737
+ const id2 = m.value.replace(/\[.*\]$/u, "");
3886
4738
  const thought = (m.configOptions ?? []).find((o) => o.category === "thought_level");
3887
4739
  const levels = flatten(thought?.options);
3888
4740
  out.push({
3889
- id,
3890
- name: typeof m.name === "string" && m.name ? m.name : id,
3891
- isDefault: id === currentBase,
4741
+ id: id2,
4742
+ name: typeof m.name === "string" && m.name ? m.name : id2,
4743
+ isDefault: id2 === currentBase,
3892
4744
  ...thought && levels.length ? {
3893
4745
  thinkingOptions: levels.map((l) => ({ id: l.id, label: EFFORT_LABEL[l.id] ?? l.name })),
3894
4746
  defaultThinking: typeof thought.currentValue === "string" ? thought.currentValue : void 0,
@@ -3898,10 +4750,11 @@ function cursorCatalogToModels(catalog, currentModel) {
3898
4750
  }
3899
4751
  return out;
3900
4752
  }
3901
- function cursorProbeModels(acpArgs) {
3902
- return async (r) => {
4753
+ function cursorProbeModels(acpArgs, providerId) {
4754
+ return async (r, launchArgs = []) => {
3903
4755
  try {
3904
- const { parsed, ext } = await probeAcpRaw(r, acpArgs, 4e4, "cursor/list_available_models");
4756
+ const { parsed, ext, promptCaps } = await probeAcpRaw(r, [...acpArgs, ...launchArgs], 4e4, "cursor/list_available_models");
4757
+ rememberPromptCaps(providerId, promptCaps);
3905
4758
  const fromCatalog = ext ? cursorCatalogToModels(ext, parsed.currentModel) : [];
3906
4759
  return fromCatalog.length ? fromCatalog : probeToModels(parsed);
3907
4760
  } catch (e) {
@@ -3910,10 +4763,27 @@ function cursorProbeModels(acpArgs) {
3910
4763
  }
3911
4764
  };
3912
4765
  }
3913
- function acpProbeModels(acpArgs) {
3914
- return async (r) => {
4766
+ function dshProbeModels(providerId) {
4767
+ return async (r, launchArgs = []) => {
4768
+ try {
4769
+ const first = await probeAcpRaw(r, [...DSH_ACP_ARGS, ...launchArgs], 3e4);
4770
+ let caps = first.promptCaps;
4771
+ const start = dshDefaultModel(first.parsed.models, first.parsed.currentModel);
4772
+ if (start && start !== first.parsed.currentModel && dshRoute(start)) {
4773
+ caps = await probeAcpRaw(r, [...await dshAcpArgs(start), ...launchArgs], 3e4).then((x) => x.promptCaps, () => caps);
4774
+ }
4775
+ rememberPromptCaps(providerId, caps);
4776
+ return probeToModels(first.parsed);
4777
+ } catch (e) {
4778
+ log("warn", "ACP \u63A2\u6D4B\u6A21\u578B\u5931\u8D25", { err: String(e) });
4779
+ return [];
4780
+ }
4781
+ };
4782
+ }
4783
+ function acpProbeModels(acpArgs, providerId) {
4784
+ return async (r, launchArgs = []) => {
3915
4785
  try {
3916
- return probeToModels(await probeAcp(r, acpArgs));
4786
+ return probeToModels(await probeAcp(r, [...acpArgs, ...launchArgs], 3e4, providerId));
3917
4787
  } catch (e) {
3918
4788
  log("warn", "ACP \u63A2\u6D4B\u6A21\u578B\u5931\u8D25", { err: String(e) });
3919
4789
  return [];
@@ -3925,6 +4795,8 @@ var init_probe = __esm({
3925
4795
  "daemon/src/providers/acp/probe.ts"() {
3926
4796
  init_spawn();
3927
4797
  init_log();
4798
+ init_promptCaps();
4799
+ init_dsh();
3928
4800
  EFFORT_LABEL = {
3929
4801
  none: "None",
3930
4802
  minimal: "Minimal",
@@ -3956,13 +4828,13 @@ function acpConfigForEntry(entry, appVersion) {
3956
4828
  flavor: "cursor",
3957
4829
  capabilities: cursorAcpCapabilities,
3958
4830
  // Each model's own thinking levels come from cursor/list_available_models (as Paseo does)
3959
- fetchModels: cursorProbeModels(["acp"])
4831
+ fetchModels: cursorProbeModels(["acp"], entry.id)
3960
4832
  };
3961
4833
  case "devin":
3962
4834
  return {
3963
4835
  ...base,
3964
4836
  acpArgs: ["acp"],
3965
- fetchModels: acpProbeModels(["acp"]),
4837
+ fetchModels: acpProbeModels(["acp"], entry.id),
3966
4838
  modeMap: { plan: "plan", default: "ask", acceptEdits: "accept-edits", bypassPermissions: "bypass" },
3967
4839
  modeLabels: { plan: "Plan", default: "Ask", acceptEdits: "Code", bypassPermissions: "Bypass Permissions" }
3968
4840
  };
@@ -3970,7 +4842,7 @@ function acpConfigForEntry(entry, appVersion) {
3970
4842
  return {
3971
4843
  ...base,
3972
4844
  acpArgs: ["acp"],
3973
- fetchModels: acpProbeModels(["acp"]),
4845
+ fetchModels: acpProbeModels(["acp"], entry.id),
3974
4846
  modeMap: { plan: "plan", default: "build" },
3975
4847
  modeLabels: { plan: "Plan", default: "Build" }
3976
4848
  };
@@ -3978,14 +4850,20 @@ function acpConfigForEntry(entry, appVersion) {
3978
4850
  return {
3979
4851
  ...base,
3980
4852
  acpArgs: ["--acp"],
3981
- fetchModels: acpProbeModels(["--acp"]),
4853
+ fetchModels: acpProbeModels(["--acp"], entry.id),
3982
4854
  modeMap: { plan: "plan", default: "agent", bypassPermissions: "allow-all" },
3983
4855
  modeLabels: { plan: "Plan", default: "Agent", bypassPermissions: "Allow All" }
3984
4856
  };
3985
4857
  case "pi":
3986
- return { ...base, acpArgs: [], fetchModels: acpProbeModels([]), modeMap: { default: "" } };
4858
+ return { ...base, acpArgs: [], fetchModels: acpProbeModels([], entry.id), modeMap: { default: "" } };
3987
4859
  case "dsh":
3988
- return { ...base, acpArgs: ["--profile", "acp"], fetchModels: acpProbeModels(["--profile", "acp"]) };
4860
+ return {
4861
+ ...base,
4862
+ acpArgs: DSH_ACP_ARGS,
4863
+ argsFor: (model) => dshAcpArgs(model),
4864
+ imagesFollowStartModel: true,
4865
+ fetchModels: dshProbeModels(entry.id)
4866
+ };
3989
4867
  default:
3990
4868
  return null;
3991
4869
  }
@@ -3995,6 +4873,7 @@ var init_specs = __esm({
3995
4873
  "daemon/src/providers/acp/specs.ts"() {
3996
4874
  init_provider3();
3997
4875
  init_probe();
4876
+ init_dsh();
3998
4877
  ACP_IDS = /* @__PURE__ */ new Set(["cursor", "devin", "opencode", "dsh", "pi", "copilot"]);
3999
4878
  }
4000
4879
  });
@@ -4003,8 +4882,8 @@ var init_specs = __esm({
4003
4882
  function registerProvider(p) {
4004
4883
  providers.set(p.id, p);
4005
4884
  }
4006
- function getProvider(id) {
4007
- return providers.get(id);
4885
+ function getProvider(id2) {
4886
+ return providers.get(id2);
4008
4887
  }
4009
4888
  function listProviders() {
4010
4889
  return [...providers.values()];
@@ -4037,10 +4916,10 @@ var init_registry = __esm({
4037
4916
  });
4038
4917
 
4039
4918
  // daemon/src/providers/enabled.ts
4040
- function isProviderEnabled(settings, id) {
4919
+ function isProviderEnabled(settings, id2) {
4041
4920
  const m = settings.providerEnabled;
4042
- if (m && typeof m === "object" && typeof m[id] === "boolean") return m[id];
4043
- const entry = catalogById(id);
4921
+ if (m && typeof m === "object" && typeof m[id2] === "boolean") return m[id2];
4922
+ const entry = catalogById(id2);
4044
4923
  if (!entry) return true;
4045
4924
  return entry.status === "verified";
4046
4925
  }
@@ -4141,21 +5020,21 @@ var init_pickFolder = __esm({
4141
5020
  });
4142
5021
 
4143
5022
  // daemon/src/providers/magpieGateway.ts
4144
- import { readFile as readFile9 } from "node:fs/promises";
4145
- import { homedir as homedir6 } from "node:os";
4146
- import { join as join9 } from "node:path";
5023
+ import { readFile as readFile11 } from "node:fs/promises";
5024
+ import { homedir as homedir7 } from "node:os";
5025
+ import { join as join11 } from "node:path";
4147
5026
  async function readJson(path3) {
4148
5027
  try {
4149
- return JSON.parse(await readFile9(path3, "utf8"));
5028
+ return JSON.parse(await readFile11(path3, "utf8"));
4150
5029
  } catch {
4151
5030
  return null;
4152
5031
  }
4153
5032
  }
4154
5033
  async function readApplied() {
4155
- return await readJson(join9(homedir6(), ".config", "magpie", "applied.json")) ?? {};
5034
+ return await readJson(join11(homedir7(), ".config", "magpie", "applied.json")) ?? {};
4156
5035
  }
4157
5036
  async function readClaudeEnv() {
4158
- const s = await readJson(join9(homedir6(), ".claude", "settings.json"));
5037
+ const s = await readJson(join11(homedir7(), ".claude", "settings.json"));
4159
5038
  return s?.env ?? {};
4160
5039
  }
4161
5040
  async function gatewayBase() {
@@ -4169,7 +5048,7 @@ async function fetchGatewayModelIds() {
4169
5048
  const res = await fetch(`${base}/v1/models`, { signal: AbortSignal.timeout(5e3) });
4170
5049
  if (!res.ok) return [];
4171
5050
  const j = await res.json();
4172
- return (j.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && !!id);
5051
+ return (j.data ?? []).map((m) => m.id).filter((id2) => typeof id2 === "string" && !!id2);
4173
5052
  } catch (e) {
4174
5053
  log("warn", "\u8BFB\u53D6 magpie \u7F51\u5173\u6A21\u578B\u5217\u8868\u5931\u8D25", { err: String(e) });
4175
5054
  return [];
@@ -4229,7 +5108,7 @@ async function withMagpie(providerId, own) {
4229
5108
  return {
4230
5109
  ...m,
4231
5110
  name,
4232
- thinkingOptions: levels.map((id) => ({ id, label: EFFORT_LABEL2[id] ?? id })),
5111
+ thinkingOptions: levels.map((id2) => ({ id: id2, label: EFFORT_LABEL2[id2] ?? id2 })),
4233
5112
  defaultThinking: m.isDefault ? def : m.defaultThinking ?? def
4234
5113
  };
4235
5114
  });
@@ -4262,46 +5141,53 @@ function withTimeout(p, ms, fb) {
4262
5141
  function fresh(e, ttl) {
4263
5142
  return !!e && Date.now() - e.at < ttl;
4264
5143
  }
4265
- async function cachedDetect(p) {
4266
- const hit = detects.get(p.id);
5144
+ function cacheKey(p, launch) {
5145
+ return launch ? `${p.id}\0${launchKey(launch)}` : p.id;
5146
+ }
5147
+ async function cachedDetect(p, launch) {
5148
+ const key = cacheKey(p, launch);
5149
+ const hit = detects.get(key);
4267
5150
  if (fresh(hit, DETECT_MS)) return hit.value;
4268
- let job = inflightDetect.get(p.id);
5151
+ let job = inflightDetect.get(key);
4269
5152
  if (!job) {
5153
+ const timedOut = { available: false, reason: tr("provider.timeout") };
4270
5154
  job = withTimeout(
4271
- p.detect().catch((e) => ({ available: false, reason: String(e?.message ?? e) })),
5155
+ p.detect(launch).catch((e) => ({ available: false, reason: String(e?.message ?? e) })),
4272
5156
  STEP_TIMEOUT_MS,
4273
- { available: false, reason: "\u68C0\u6D4B\u8D85\u65F6" }
5157
+ timedOut
4274
5158
  ).then((v) => {
4275
- if (v.reason !== "\u68C0\u6D4B\u8D85\u65F6") detects.set(p.id, { at: Date.now(), value: v });
5159
+ if (v !== timedOut) detects.set(key, { at: Date.now(), value: v });
4276
5160
  return v;
4277
- }).finally(() => inflightDetect.delete(p.id));
4278
- inflightDetect.set(p.id, job);
5161
+ }).finally(() => inflightDetect.delete(key));
5162
+ inflightDetect.set(key, job);
4279
5163
  }
4280
5164
  return job;
4281
5165
  }
4282
- async function cachedModels(p) {
4283
- const hit = models.get(p.id);
5166
+ async function cachedModels(p, launch) {
5167
+ const key = cacheKey(p, launch);
5168
+ const hit = models.get(key);
4284
5169
  if (fresh(hit, MODELS_MS)) return hit.value;
4285
- let job = inflightModels.get(p.id);
5170
+ let job = inflightModels.get(key);
4286
5171
  if (!job) {
4287
- job = withTimeout(p.listModels().catch(() => []), MODELS_TIMEOUT_MS, null).then(async (v) => {
4288
- if (v === null) return models.get(p.id)?.value ?? [];
5172
+ job = withTimeout(p.listModels(launch).catch(() => []), MODELS_TIMEOUT_MS, null).then(async (v) => {
5173
+ if (v === null) return models.get(key)?.value ?? [];
4289
5174
  const list = await withMagpie(p.id, dropPlaceholder(v)).catch(() => dropPlaceholder(v));
4290
- models.set(p.id, { at: Date.now(), value: list });
5175
+ models.set(key, { at: Date.now(), value: list });
4291
5176
  return list;
4292
- }).finally(() => inflightModels.delete(p.id));
4293
- inflightModels.set(p.id, job);
5177
+ }).finally(() => inflightModels.delete(key));
5178
+ inflightModels.set(key, job);
4294
5179
  }
4295
5180
  return job;
4296
5181
  }
4297
5182
  function dropPlaceholder(list) {
4298
- if (list.length === 1 && list[0].id === "default" && list[0].name === "\u9ED8\u8BA4") return [];
5183
+ if (list.length === 1 && list[0].id === "default" && (list[0].name === "\u9ED8\u8BA4" || list[0].name === PLACEHOLDER_MODEL_NAME)) return [];
4299
5184
  return list;
4300
5185
  }
4301
- function clearProviderCache(id) {
4302
- if (id) {
4303
- detects.delete(id);
4304
- models.delete(id);
5186
+ function clearProviderCache(id2) {
5187
+ if (id2) {
5188
+ for (const m of [detects, models]) {
5189
+ for (const k of [...m.keys()]) if (k === id2 || k.startsWith(id2 + "\0")) m.delete(k);
5190
+ }
4305
5191
  } else {
4306
5192
  detects.clear();
4307
5193
  models.clear();
@@ -4310,7 +5196,10 @@ function clearProviderCache(id) {
4310
5196
  var DETECT_MS, MODELS_MS, STEP_TIMEOUT_MS, MODELS_TIMEOUT_MS, detects, models, inflightDetect, inflightModels;
4311
5197
  var init_infoCache = __esm({
4312
5198
  "daemon/src/providers/infoCache.ts"() {
5199
+ init_types();
4313
5200
  init_magpieGateway();
5201
+ init_launch();
5202
+ init_i18n();
4314
5203
  DETECT_MS = 60 * 1e3;
4315
5204
  MODELS_MS = 10 * 60 * 1e3;
4316
5205
  STEP_TIMEOUT_MS = 15e3;
@@ -4322,18 +5211,29 @@ var init_infoCache = __esm({
4322
5211
  }
4323
5212
  });
4324
5213
 
5214
+ // daemon/src/providers/modes.ts
5215
+ function clampMode(modes, mode2) {
5216
+ if (mode2 && modes.includes(mode2)) return mode2;
5217
+ if (modes.includes("default")) return "default";
5218
+ return modes[0] ?? "default";
5219
+ }
5220
+ var init_modes = __esm({
5221
+ "daemon/src/providers/modes.ts"() {
5222
+ }
5223
+ });
5224
+
4325
5225
  // daemon/src/ws/methods.ts
4326
5226
  import { z } from "zod";
4327
5227
  import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
4328
- import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
4329
- import { homedir as homedir7 } from "node:os";
4330
- import { dirname as dirname3, join as join10, win32 as win32Path } from "node:path";
5228
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
5229
+ import { homedir as homedir8 } from "node:os";
5230
+ import { dirname as dirname3, join as join12, win32 as win32Path } from "node:path";
4331
5231
  import { execFile as execFile3 } from "node:child_process";
4332
5232
  function wrap(schema, fn) {
4333
5233
  return (params, ctx) => {
4334
5234
  const r = schema.safeParse(params ?? {});
4335
5235
  if (!r.success) {
4336
- throw new ApiError("bad_params", "\u53C2\u6570\u4E0D\u5BF9\uFF1A" + r.error.issues.map((i) => i.message).join("\uFF1B"));
5236
+ throw new ApiError("bad_params", tr("err.badParams", { detail: r.error.issues.map((i) => i.message).join("; ") }));
4337
5237
  }
4338
5238
  return fn(r.data, ctx);
4339
5239
  };
@@ -4351,10 +5251,18 @@ function registerMethods(gw, svc) {
4351
5251
  return { ok: true };
4352
5252
  }));
4353
5253
  gw.method("settings.get", wrap(zEmpty, () => svc.settings.get()));
4354
- gw.method("settings.set", wrap(
4355
- z.object({ patch: z.record(z.string(), z.unknown()) }),
4356
- (p) => svc.settings.set(p.patch)
4357
- ));
5254
+ gw.method("settings.set", wrap(z.object({ patch: z.record(z.string(), z.unknown()) }), async (p) => {
5255
+ const checked = parseSettingsPatch(p.patch);
5256
+ if (!checked.ok) throw new ApiError("bad_params", `Invalid setting: ${checked.error}`);
5257
+ const before = await svc.settings.get();
5258
+ const next = await svc.settings.set(checked.patch);
5259
+ for (const id2 of Object.keys(checked.patch.providerCommand ?? {})) {
5260
+ if (JSON.stringify(before.providerCommand?.[id2] ?? null) !== JSON.stringify(next.providerCommand?.[id2] ?? null)) {
5261
+ clearProviderCache(id2);
5262
+ }
5263
+ }
5264
+ return next;
5265
+ }));
4358
5266
  gw.method("sub.add", wrap(
4359
5267
  z.object({ ch: z.string().min(1).max(120), afterSeq: z.number().int().nonnegative().optional() }),
4360
5268
  async (p, ctx) => {
@@ -4383,9 +5291,10 @@ function registerMethods(gw, svc) {
4383
5291
  listProviders().map(async (p2) => {
4384
5292
  const entry = catalogById(p2.id);
4385
5293
  const enabled = isProviderEnabled(settings, p2.id);
5294
+ const launch = launchFromSettings(settings, p2.id);
4386
5295
  const skip = !enabled || entry?.status === "planned";
4387
- const det = skip ? { available: false, reason: !enabled ? "\u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528" : "\u5C1A\u672A\u63A5\u5165" } : await cachedDetect(p2);
4388
- const models2 = !skip && det.available ? await cachedModels(p2) : [];
5296
+ const det = skip ? { available: false, reason: !enabled ? tr("provider.disabled") : tr("provider.planned") } : await cachedDetect(p2, launch);
5297
+ const models2 = !skip && det.available ? await cachedModels(p2, launch) : [];
4389
5298
  return {
4390
5299
  id: p2.id,
4391
5300
  name: p2.name,
@@ -4395,7 +5304,7 @@ function registerMethods(gw, svc) {
4395
5304
  ready: entry?.status === "verified",
4396
5305
  bin: det.bin,
4397
5306
  version: det.version,
4398
- reason: !enabled ? "\u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528" : det.reason,
5307
+ reason: !enabled ? tr("provider.disabled") : det.reason,
4399
5308
  installHint: det.installHint,
4400
5309
  capabilities: p2.capabilities,
4401
5310
  models: models2
@@ -4429,7 +5338,7 @@ function registerMethods(gw, svc) {
4429
5338
  }));
4430
5339
  gw.method("projects.remove", wrap(z.object({ id: z.string() }), async (p) => {
4431
5340
  const proj = await svc.projects.get(p.id);
4432
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
5341
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
4433
5342
  for (const t of await svc.threads.list()) {
4434
5343
  if (t.projectId === p.id) {
4435
5344
  await svc.threads.delete(t.id);
@@ -4442,8 +5351,8 @@ function registerMethods(gw, svc) {
4442
5351
  }));
4443
5352
  gw.method("projects.open", wrap(z.object({ id: z.string() }), async (p) => {
4444
5353
  const proj = await svc.projects.get(p.id);
4445
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
4446
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\u5939");
5354
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5355
+ if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.dirMissing"));
4447
5356
  const cmd = process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open";
4448
5357
  const target = process.platform === "win32" ? win32Path.normalize(proj.path) : proj.path;
4449
5358
  execFile3(cmd, [target], { windowsHide: true }, () => {
@@ -4483,16 +5392,18 @@ function registerMethods(gw, svc) {
4483
5392
  }),
4484
5393
  async (p) => {
4485
5394
  const proj = await svc.projects.get(p.projectId);
4486
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
4487
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728");
5395
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5396
+ if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
4488
5397
  if (!p.providerSessionId && !isProviderEnabled(await svc.settings.get(), p.provider)) {
4489
- throw new ApiError("bad_params", "\u8BE5 Agent \u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528");
5398
+ throw new ApiError("bad_params", tr("provider.disabledNamed", { name: catalogById(p.provider)?.name ?? p.provider }));
4490
5399
  }
4491
5400
  const cat = catalogById(p.provider);
4492
5401
  if (!p.providerSessionId && cat && !providerCreatable(cat.status)) {
4493
- throw new ApiError("bad_params", `${cat.name} \u5C1A\u672A\u63A5\u5165\uFF0C\u6682\u4E0D\u80FD\u65B0\u5EFA\u5BF9\u8BDD`);
5402
+ throw new ApiError("bad_params", tr("provider.plannedNamed", { name: cat.name }));
4494
5403
  }
4495
- const meta = await svc.threads.create(p);
5404
+ const provider = getProvider(p.provider);
5405
+ const mode2 = clampMode(provider?.capabilities.modes ?? [], p.mode);
5406
+ const meta = await svc.threads.create({ ...p, mode: mode2 });
4496
5407
  gw.publish("sidebar", { type: "thread.upsert", thread: meta });
4497
5408
  return meta;
4498
5409
  }
@@ -4501,7 +5412,7 @@ function registerMethods(gw, svc) {
4501
5412
  z.object({ id: z.string(), afterSeq: z.number().int().nonnegative().optional() }),
4502
5413
  async (p) => {
4503
5414
  const meta = await svc.threads.get(p.id);
4504
- if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
5415
+ if (!meta) throw new ApiError("not_found", tr("err.threadMissing"));
4505
5416
  const events = await svc.threads.readEvents(p.id, p.afterSeq ?? 0, 5e3);
4506
5417
  const rt = svc.runtime?.(p.id) ?? null;
4507
5418
  return {
@@ -4522,7 +5433,7 @@ function registerMethods(gw, svc) {
4522
5433
  }),
4523
5434
  async (p) => {
4524
5435
  const meta = await svc.threads.get(p.id);
4525
- if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
5436
+ if (!meta) throw new ApiError("not_found", tr("err.threadMissing"));
4526
5437
  const patch = {};
4527
5438
  if (p.title !== void 0) {
4528
5439
  patch.title = p.title.trim() || meta.title;
@@ -4537,7 +5448,7 @@ function registerMethods(gw, svc) {
4537
5448
  ));
4538
5449
  gw.method("threads.delete", wrap(z.object({ id: z.string() }), async (p) => {
4539
5450
  const meta = await svc.threads.get(p.id);
4540
- if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
5451
+ if (!meta) throw new ApiError("not_found", tr("err.threadMissing"));
4541
5452
  if (svc.manager) await svc.manager.deleteThread(p.id);
4542
5453
  else await svc.threads.delete(p.id);
4543
5454
  gw.publish("sidebar", { type: "thread.remove", id: p.id });
@@ -4545,8 +5456,8 @@ function registerMethods(gw, svc) {
4545
5456
  }));
4546
5457
  const projectPath = async (projectId) => {
4547
5458
  const proj = await svc.projects.get(projectId);
4548
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
4549
- if (!existsSync8(proj.path)) throw new ApiError("bad_path", "\u9879\u76EE\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728");
5459
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
5460
+ if (!existsSync8(proj.path)) throw new ApiError("bad_path", tr("err.projectDirGone"));
4550
5461
  return proj.path;
4551
5462
  };
4552
5463
  gw.method("git.status", wrap(
@@ -4571,7 +5482,7 @@ function registerMethods(gw, svc) {
4571
5482
  ));
4572
5483
  gw.method("history.list", wrap(z.object({ projectId: z.string() }), async (p) => {
4573
5484
  const proj = await svc.projects.get(p.projectId);
4574
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
5485
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
4575
5486
  const imported = new Set(
4576
5487
  (await svc.threads.list()).filter((t) => t.projectId === p.projectId && t.providerSessionId).map((t) => t.providerSessionId)
4577
5488
  );
@@ -4590,7 +5501,7 @@ function registerMethods(gw, svc) {
4590
5501
  }),
4591
5502
  async (p) => {
4592
5503
  const proj = await svc.projects.get(p.projectId);
4593
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
5504
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
4594
5505
  const metas = [];
4595
5506
  for (const item of p.items) {
4596
5507
  const provider = getProvider(item.provider);
@@ -4635,7 +5546,7 @@ function registerMethods(gw, svc) {
4635
5546
  ));
4636
5547
  gw.method("terminal.attach", wrap(z.object({ id: z.string() }), async (p, ctx) => {
4637
5548
  const r = svc.terminals.attach(p.id);
4638
- if (r.error || r.index == null) throw new ApiError("not_found", r.error ?? "\u7EC8\u7AEF\u4E0D\u5B58\u5728");
5549
+ if (r.error || r.index == null) throw new ApiError("not_found", r.error ?? tr("err.terminalMissing"));
4639
5550
  svc.gateway.subscribe(ctx.conn, `terminal:${r.index}`);
4640
5551
  return r;
4641
5552
  }));
@@ -4645,7 +5556,7 @@ function registerMethods(gw, svc) {
4645
5556
  ));
4646
5557
  gw.method("terminal.kill", wrap(z.object({ id: z.string() }), (p) => svc.terminals.kill(p.id)));
4647
5558
  const needManager = () => {
4648
- if (!svc.manager) throw new ApiError("unavailable", "\u4F1A\u8BDD\u7BA1\u7406\u5668\u672A\u542F\u52A8");
5559
+ if (!svc.manager) throw new ApiError("unavailable", tr("err.noManager"));
4649
5560
  return svc.manager;
4650
5561
  };
4651
5562
  gw.method("thread.send", wrap(
@@ -4656,10 +5567,13 @@ function registerMethods(gw, svc) {
4656
5567
  behavior: z.enum(["queue", "steer"]).optional()
4657
5568
  }),
4658
5569
  async (p) => {
4659
- await needManager().send(p.id, { text: p.text, attachments: p.attachments }, p.behavior ?? "queue");
4660
- return { ok: true };
5570
+ const delivered = await needManager().send(p.id, { text: p.text, attachments: p.attachments }, p.behavior ?? "queue");
5571
+ return { ok: true, delivered: delivered ?? "started" };
4661
5572
  }
4662
5573
  ));
5574
+ gw.method("thread.queueRemove", wrap(z.object({ id: z.string(), queueId: z.string() }), async (p) => {
5575
+ return { ok: needManager().removeQueued(p.id, p.queueId) };
5576
+ }));
4663
5577
  gw.method("thread.interrupt", wrap(z.object({ id: z.string() }), async (p) => {
4664
5578
  await needManager().interrupt(p.id);
4665
5579
  return { ok: true };
@@ -4717,15 +5631,15 @@ function registerMethods(gw, svc) {
4717
5631
  }));
4718
5632
  gw.method("thread.exportMarkdown", wrap(z.object({ id: z.string() }), async (p) => {
4719
5633
  const meta = await svc.threads.get(p.id);
4720
- if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
5634
+ if (!meta) throw new ApiError("not_found", tr("err.threadMissing"));
4721
5635
  const events = await svc.threads.readEvents(p.id, 0, 2e4);
4722
5636
  const L = [];
4723
5637
  L.push(`# ${meta.title}`, "");
4724
- L.push(`> ${meta.provider} \xB7 ${meta.model ?? "\u9ED8\u8BA4"} \xB7 ${meta.mode} \xB7 ${new Date(meta.createdAt).toLocaleString("zh-CN")}`, "");
5638
+ L.push(`> ${meta.provider} \xB7 ${meta.model ?? tr("export.defaultModel")} \xB7 ${meta.mode} \xB7 ${new Date(meta.createdAt).toLocaleString(intlLocale())}`, "");
4725
5639
  for (const e of events) {
4726
5640
  switch (e.type) {
4727
5641
  case "user_message":
4728
- L.push(`**\u6211**${e.steered ? "\uFF08\u63D2\u8BDD\uFF09" : ""}\uFF1A
5642
+ L.push(`**${tr("export.me")}**${e.steered ? tr("export.steered") : ""}:
4729
5643
 
4730
5644
  ${e.text}`, "");
4731
5645
  break;
@@ -4733,7 +5647,7 @@ ${e.text}`, "");
4733
5647
  L.push(e.text, "");
4734
5648
  break;
4735
5649
  case "reasoning":
4736
- L.push(`<details><summary>\u601D\u8003</summary>
5650
+ L.push(`<details><summary>${tr("export.reasoning")}</summary>
4737
5651
 
4738
5652
  ${e.text}
4739
5653
  </details>`, "");
@@ -4747,10 +5661,14 @@ ${e.text}
4747
5661
  break;
4748
5662
  }
4749
5663
  case "plan":
4750
- L.push("### \u8BA1\u5212\n\n" + e.markdown, "");
5664
+ L.push(`### ${tr("export.plan")}
5665
+
5666
+ ` + e.markdown, "");
4751
5667
  break;
4752
5668
  case "todo":
4753
- L.push("### \u5F85\u529E\n\n" + e.items.map((i) => `- [${i.status === "done" || i.status === "completed" ? "x" : " "}] ${i.text}`).join("\n"), "");
5669
+ L.push(`### ${tr("export.todo")}
5670
+
5671
+ ` + e.items.map((i) => `- [${i.status === "done" || i.status === "completed" ? "x" : " "}] ${i.text}`).join("\n"), "");
4754
5672
  break;
4755
5673
  case "notice":
4756
5674
  L.push(`_${e.message}_`, "");
@@ -4765,14 +5683,19 @@ ${e.text}
4765
5683
  const markdown = L.join("\n");
4766
5684
  const safe = meta.title.replace(/[\\/:*?"<>|\s]+/g, "_").slice(0, 60) || "thread";
4767
5685
  const filename = `${safe}.md`;
4768
- const dir = join10(homedir7(), "Downloads");
4769
- await mkdir2(dir, { recursive: true });
4770
- let dest = join10(dir, filename);
5686
+ let dir = join12(homedir8(), "Downloads");
5687
+ try {
5688
+ await mkdir3(dir, { recursive: true });
5689
+ } catch {
5690
+ dir = join12(svc.cfg.dataDir, "exports");
5691
+ await mkdir3(dir, { recursive: true });
5692
+ }
5693
+ let dest = join12(dir, filename);
4771
5694
  if (existsSync8(dest)) {
4772
5695
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4773
- dest = join10(dir, `${safe}-${stamp2}.md`);
5696
+ dest = join12(dir, `${safe}-${stamp2}.md`);
4774
5697
  }
4775
- await writeFile3(dest, markdown, "utf8");
5698
+ await writeFile4(dest, markdown, "utf8");
4776
5699
  revealInFileManager(dest);
4777
5700
  return { markdown, filename, path: dest };
4778
5701
  }));
@@ -4797,12 +5720,16 @@ function revealInFileManager(filePath) {
4797
5720
  var threadModeSchema, ApiError;
4798
5721
  var init_methods = __esm({
4799
5722
  "daemon/src/ws/methods.ts"() {
5723
+ init_settings();
4800
5724
  init_registry();
4801
5725
  init_catalog();
4802
5726
  init_enabled();
4803
5727
  init_pickFolder();
4804
5728
  init_bin();
4805
5729
  init_infoCache();
5730
+ init_launch();
5731
+ init_modes();
5732
+ init_i18n();
4806
5733
  threadModeSchema = z.enum(["plan", "default", "acceptEdits", "bypassPermissions", "ask"]);
4807
5734
  ApiError = class extends Error {
4808
5735
  code;
@@ -4815,13 +5742,13 @@ var init_methods = __esm({
4815
5742
  });
4816
5743
 
4817
5744
  // daemon/src/util/atomic.ts
4818
- import { writeFile as writeFile4, rename, mkdir as mkdir3 } from "node:fs/promises";
5745
+ import { writeFile as writeFile5, rename, mkdir as mkdir4 } from "node:fs/promises";
4819
5746
  import { dirname as dirname4 } from "node:path";
4820
5747
  async function writeJsonAtomic(file, obj) {
4821
5748
  try {
4822
- await mkdir3(dirname4(file), { recursive: true });
5749
+ await mkdir4(dirname4(file), { recursive: true });
4823
5750
  const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
4824
- await writeFile4(tmp, JSON.stringify(obj, null, 2), "utf8");
5751
+ await writeFile5(tmp, JSON.stringify(obj, null, 2), "utf8");
4825
5752
  await rename(tmp, file);
4826
5753
  } catch (e) {
4827
5754
  if (e instanceof ApiError) throw e;
@@ -4834,13 +5761,118 @@ var init_atomic = __esm({
4834
5761
  }
4835
5762
  });
4836
5763
 
5764
+ // daemon/src/store/settings.ts
5765
+ import { readFile as readFile12 } from "node:fs/promises";
5766
+ import { join as join13 } from "node:path";
5767
+ import { z as z2 } from "zod";
5768
+ function parseSettingsPatch(patch) {
5769
+ const r = settingsPatchSchema.safeParse(patch);
5770
+ if (r.success) return { ok: true, patch: r.data };
5771
+ return {
5772
+ ok: false,
5773
+ error: r.error.issues.map((i) => `${i.path.join(".") || "(patch)"}: ${i.message}`).join("; ")
5774
+ };
5775
+ }
5776
+ function sanitizeStored(raw) {
5777
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
5778
+ const shape = settingsPatchSchema.shape;
5779
+ const out = {};
5780
+ for (const [k, v] of Object.entries(raw)) {
5781
+ const field = shape[k];
5782
+ if (field && k !== "idleCloseMinutes" && !field.safeParse(v).success) continue;
5783
+ if (k === "idleCloseMinutes") {
5784
+ const n = Number(v);
5785
+ if (!Number.isFinite(n)) continue;
5786
+ out[k] = Math.min(IDLE_CLOSE_MAX, Math.max(IDLE_CLOSE_MIN, Math.round(n)));
5787
+ continue;
5788
+ }
5789
+ out[k] = v;
5790
+ }
5791
+ return out;
5792
+ }
5793
+ var DEFAULT_SETTINGS, IDLE_CLOSE_MIN, IDLE_CLOSE_MAX, mode, id, settingsPatchSchema, SettingsStore;
5794
+ var init_settings = __esm({
5795
+ "daemon/src/store/settings.ts"() {
5796
+ init_atomic();
5797
+ DEFAULT_SETTINGS = {
5798
+ defaultProvider: "claude",
5799
+ defaultMode: "default",
5800
+ sendKey: "enter",
5801
+ runningSend: "queue",
5802
+ showThinking: true,
5803
+ theme: "auto",
5804
+ idleCloseMinutes: 30,
5805
+ collapsedProjects: [],
5806
+ panel: { open: false, tab: "changes", width: 440 },
5807
+ sidebarWidth: 248,
5808
+ lastProjectId: null,
5809
+ lastThreadId: null,
5810
+ recentDirs: []
5811
+ };
5812
+ IDLE_CLOSE_MIN = 5;
5813
+ IDLE_CLOSE_MAX = 240;
5814
+ mode = z2.enum(["plan", "default", "acceptEdits", "bypassPermissions", "ask"]);
5815
+ id = z2.string().min(1).max(200);
5816
+ settingsPatchSchema = z2.object({
5817
+ defaultProvider: z2.string().min(1).max(64),
5818
+ defaultMode: mode,
5819
+ sendKey: z2.enum(["enter", "ctrlEnter"]),
5820
+ runningSend: z2.enum(["queue", "steer"]),
5821
+ showThinking: z2.boolean(),
5822
+ // auto = follow the host: Magpie's light/dark inside Magpie, the OS outside it
5823
+ theme: z2.enum(["auto", "light", "dark"]),
5824
+ idleCloseMinutes: z2.number().int().min(IDLE_CLOSE_MIN).max(IDLE_CLOSE_MAX),
5825
+ collapsedProjects: z2.array(id).max(1e3),
5826
+ panel: z2.object({ open: z2.boolean(), tab: z2.string().max(40), width: z2.number().min(0).max(5e3) }).partial(),
5827
+ sidebarWidth: z2.number().min(0).max(5e3),
5828
+ lastProjectId: id.nullable(),
5829
+ lastThreadId: id.nullable(),
5830
+ recentDirs: z2.array(z2.string().max(4096)).max(50),
5831
+ providerEnabled: z2.record(z2.string().max(64), z2.boolean()),
5832
+ providerCommand: z2.record(
5833
+ z2.string().max(64),
5834
+ z2.object({ executable: z2.string().max(4096).optional(), args: z2.array(z2.string().max(4096)).max(64).optional() }).strict()
5835
+ ),
5836
+ v1Migrated: z2.boolean()
5837
+ }).partial().strict();
5838
+ SettingsStore = class {
5839
+ file;
5840
+ cache = null;
5841
+ writing = Promise.resolve();
5842
+ constructor(dataDir) {
5843
+ this.file = join13(dataDir, "settings.json");
5844
+ }
5845
+ async get() {
5846
+ if (this.cache) return this.cache;
5847
+ let raw = {};
5848
+ try {
5849
+ raw = JSON.parse(await readFile12(this.file, "utf8"));
5850
+ } catch {
5851
+ }
5852
+ raw = sanitizeStored(raw);
5853
+ this.cache = { ...DEFAULT_SETTINGS, ...raw, panel: { ...DEFAULT_SETTINGS.panel, ...raw.panel } };
5854
+ return this.cache;
5855
+ }
5856
+ async set(patch) {
5857
+ const cur = await this.get();
5858
+ const next = { ...cur, ...patch, panel: { ...cur.panel, ...patch.panel } };
5859
+ this.cache = next;
5860
+ const snap = next;
5861
+ this.writing = this.writing.then(() => writeJsonAtomic(this.file, snap));
5862
+ await this.writing;
5863
+ return next;
5864
+ }
5865
+ };
5866
+ }
5867
+ });
5868
+
4837
5869
  // daemon/src/util/jsonl.ts
4838
- import { appendFile, readFile as readFile12, mkdir as mkdir4, rename as rename2, writeFile as writeFile5 } from "node:fs/promises";
5870
+ import { appendFile, readFile as readFile14, mkdir as mkdir5, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
4839
5871
  import { dirname as dirname5 } from "node:path";
4840
5872
  async function readJsonl(file) {
4841
5873
  let text = "";
4842
5874
  try {
4843
- text = await readFile12(file, "utf8");
5875
+ text = await readFile14(file, "utf8");
4844
5876
  } catch {
4845
5877
  return [];
4846
5878
  }
@@ -4859,7 +5891,7 @@ function appendJsonl(file, obj) {
4859
5891
  const prev = writers.get(file) ?? Promise.resolve();
4860
5892
  const next = prev.then(async () => {
4861
5893
  try {
4862
- await mkdir4(dirname5(file), { recursive: true });
5894
+ await mkdir5(dirname5(file), { recursive: true });
4863
5895
  await appendFile(file, JSON.stringify(obj) + "\n", "utf8");
4864
5896
  } catch (e) {
4865
5897
  if (e instanceof ApiError) throw e;
@@ -4886,9 +5918,9 @@ var legacy_exports = {};
4886
5918
  __export(legacy_exports, {
4887
5919
  migrateLegacy: () => migrateLegacy
4888
5920
  });
4889
- import { cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile16 } from "node:fs/promises";
5921
+ import { cp as cp2, mkdir as mkdir8, readdir as readdir6, readFile as readFile18 } from "node:fs/promises";
4890
5922
  import { existsSync as existsSync12 } from "node:fs";
4891
- import { join as join17 } from "node:path";
5923
+ import { join as join19 } from "node:path";
4892
5924
  function stamp() {
4893
5925
  const d = /* @__PURE__ */ new Date();
4894
5926
  const p = (n) => String(n).padStart(2, "0");
@@ -4897,19 +5929,19 @@ function stamp() {
4897
5929
  async function migrateLegacy(dataDir, stores) {
4898
5930
  const settings = await stores.settings.get();
4899
5931
  if (settings.v1Migrated) return { migrated: 0, skipped: true };
4900
- const oldDir = join17(dataDir, "sessions");
4901
- const oldSettingsPath = join17(dataDir, "settings.json");
5932
+ const oldDir = join19(dataDir, "sessions");
5933
+ const oldSettingsPath = join19(dataDir, "settings.json");
4902
5934
  if (!existsSync12(oldDir) && !existsSync12(oldSettingsPath)) {
4903
5935
  await stores.settings.set({ v1Migrated: true });
4904
5936
  return { migrated: 0, skipped: true };
4905
5937
  }
4906
- const backup = join17(dataDir, "trash", `legacy-${stamp()}`);
4907
- await mkdir7(backup, { recursive: true });
4908
- if (existsSync12(oldDir)) await cp2(oldDir, join17(backup, "sessions"), { recursive: true }).catch(() => {
5938
+ const backup = join19(dataDir, "trash", `legacy-${stamp()}`);
5939
+ await mkdir8(backup, { recursive: true });
5940
+ if (existsSync12(oldDir)) await cp2(oldDir, join19(backup, "sessions"), { recursive: true }).catch(() => {
4909
5941
  });
4910
- if (existsSync12(oldSettingsPath)) await cp2(oldSettingsPath, join17(backup, "settings.json")).catch(() => {
5942
+ if (existsSync12(oldSettingsPath)) await cp2(oldSettingsPath, join19(backup, "settings.json")).catch(() => {
4911
5943
  });
4912
- const oldSettings = existsSync12(oldSettingsPath) ? JSON.parse(await readFile16(oldSettingsPath, "utf8").catch(() => "{}")) : {};
5944
+ const oldSettings = existsSync12(oldSettingsPath) ? JSON.parse(await readFile18(oldSettingsPath, "utf8").catch(() => "{}")) : {};
4913
5945
  const projectIdByPath = /* @__PURE__ */ new Map();
4914
5946
  const lc = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
4915
5947
  for (const p of oldSettings.projects ?? []) {
@@ -4923,13 +5955,13 @@ async function migrateLegacy(dataDir, stores) {
4923
5955
  }
4924
5956
  let sessionIds = [];
4925
5957
  try {
4926
- sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync12(join17(oldDir, d, "meta.json")));
5958
+ sessionIds = (await readdir6(oldDir)).filter((d) => validId(d) && existsSync12(join19(oldDir, d, "meta.json")));
4927
5959
  } catch {
4928
5960
  }
4929
5961
  const metas = [];
4930
- for (const id of sessionIds) {
5962
+ for (const id2 of sessionIds) {
4931
5963
  try {
4932
- const m = JSON.parse(await readFile16(join17(oldDir, id, "meta.json"), "utf8"));
5964
+ const m = JSON.parse(await readFile18(join19(oldDir, id2, "meta.json"), "utf8"));
4933
5965
  if (m?.cwd) metas.push(m);
4934
5966
  } catch {
4935
5967
  }
@@ -4956,13 +5988,13 @@ async function migrateLegacy(dataDir, stores) {
4956
5988
  const projectOf = (cwd) => {
4957
5989
  const l = lc(cwd);
4958
5990
  let best = null;
4959
- for (const [pp, id] of projectIdByPath) {
5991
+ for (const [pp, id2] of projectIdByPath) {
4960
5992
  if (belongsTo(l, pp) && (best == null || pp.length > best.length)) best = pp;
4961
5993
  }
4962
5994
  return best ? projectIdByPath.get(best) : null;
4963
5995
  };
4964
5996
  let migrated = 0;
4965
- const threadsDir = join17(dataDir, "threads");
5997
+ const threadsDir = join19(dataDir, "threads");
4966
5998
  for (const m of metas) {
4967
5999
  try {
4968
6000
  const projectId = projectOf(m.cwd);
@@ -4999,10 +6031,10 @@ async function migrateLegacy(dataDir, stores) {
4999
6031
  importedFrom: m.cliSessionId ? { provider, sessionId: m.cliSessionId } : void 0,
5000
6032
  seq: 0
5001
6033
  };
5002
- const tdir = join17(threadsDir, meta.id);
6034
+ const tdir = join19(threadsDir, meta.id);
5003
6035
  if (existsSync12(tdir)) meta.id = `t_${m.id}_v1`.slice(0, 80);
5004
- await mkdir7(join17(threadsDir, meta.id), { recursive: true });
5005
- const oldEvts = (await readFile16(join17(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
6036
+ await mkdir8(join19(threadsDir, meta.id), { recursive: true });
6037
+ const oldEvts = (await readFile18(join19(oldDir, m.id, "events.jsonl"), "utf8").catch(() => "")).split("\n").filter((l) => l.trim()).map((l) => {
5006
6038
  try {
5007
6039
  return JSON.parse(l);
5008
6040
  } catch {
@@ -5081,9 +6113,9 @@ async function migrateLegacy(dataDir, stores) {
5081
6113
  seq++;
5082
6114
  lines.push({ type: "tool_call", itemId: key, kind: "other", title: o.title, status: "failed", seq, ts: Date.now(), threadId: meta.id });
5083
6115
  }
5084
- for (const ev of lines) await appendJsonl(join17(threadsDir, meta.id, "timeline.jsonl"), ev);
6116
+ for (const ev of lines) await appendJsonl(join19(threadsDir, meta.id, "timeline.jsonl"), ev);
5085
6117
  meta.seq = seq;
5086
- await writeJsonAtomic(join17(threadsDir, meta.id, "meta.json"), meta);
6118
+ await writeJsonAtomic(join19(threadsDir, meta.id, "meta.json"), meta);
5087
6119
  migrated++;
5088
6120
  } catch (e) {
5089
6121
  log("warn", "\u8FC1\u79FB\uFF1A\u4F1A\u8BDD\u5931\u8D25", { id: m.id, err: String(e) });
@@ -5148,7 +6180,7 @@ var init_warm = __esm({
5148
6180
 
5149
6181
  // daemon/src/main.ts
5150
6182
  import { mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync as chmodSync2 } from "node:fs";
5151
- import { join as join18, dirname as dirname7 } from "node:path";
6183
+ import { join as join20, dirname as dirname7 } from "node:path";
5152
6184
  import { fileURLToPath as fileURLToPath3 } from "node:url";
5153
6185
 
5154
6186
  // daemon/src/config.ts
@@ -5265,6 +6297,7 @@ function tokenEqual(a, b) {
5265
6297
 
5266
6298
  // daemon/src/http.ts
5267
6299
  init_log();
6300
+ init_i18n();
5268
6301
  var MIME = {
5269
6302
  ".html": "text/html; charset=utf-8",
5270
6303
  ".js": "text/javascript; charset=utf-8",
@@ -5306,7 +6339,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5306
6339
  await route(req, res);
5307
6340
  } catch (e) {
5308
6341
  log("error", "HTTP \u5904\u7406\u5F02\u5E38", { url: req.url, err: String(e) });
5309
- if (!res.headersSent) sendJson(res, 500, { ok: false, e: { code: "internal", message: "\u670D\u52A1\u5185\u90E8\u9519\u8BEF" } });
6342
+ if (!res.headersSent) sendJson(res, 500, { ok: false, e: { code: "internal", message: tr("http.internal") } });
5310
6343
  else res.end();
5311
6344
  }
5312
6345
  });
@@ -5318,7 +6351,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5318
6351
  }
5319
6352
  const origin = classifyOrigin(req.headers.origin, cfg2.port);
5320
6353
  if (origin === "foreign") {
5321
- sendJson(res, 403, { ok: false, e: { code: "bad_origin", message: "\u6765\u6E90\u4E0D\u5141\u8BB8" } });
6354
+ sendJson(res, 403, { ok: false, e: { code: "bad_origin", message: tr("http.badOrigin") } });
5322
6355
  return;
5323
6356
  }
5324
6357
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${cfg2.port}`);
@@ -5327,7 +6360,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5327
6360
  const isAttachment = pathname.startsWith("/attachments/");
5328
6361
  if (req.method === "OPTIONS") {
5329
6362
  if (pathname !== "/api/upload" || origin === "none") {
5330
- sendJson(res, 403, { ok: false, e: { code: "bad_preflight", message: "\u9884\u68C0\u4E0D\u5141\u8BB8" } });
6363
+ sendJson(res, 403, { ok: false, e: { code: "bad_preflight", message: tr("http.badPreflight") } });
5331
6364
  return;
5332
6365
  }
5333
6366
  send(res, 204, "", {
@@ -5344,7 +6377,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5344
6377
  const queryToken = isAttachment ? url.searchParams.get("t") : null;
5345
6378
  const validToken = typeof headerToken === "string" && tokenEqual(headerToken, cfg2.token) || typeof queryToken === "string" && tokenEqual(queryToken, cfg2.token);
5346
6379
  if (!validToken) {
5347
- sendJson(res, 403, { ok: false, e: { code: "bad_token", message: "\u4EE4\u724C\u65E0\u6548" } });
6380
+ sendJson(res, 403, { ok: false, e: { code: "bad_token", message: tr("http.badToken") } });
5348
6381
  return;
5349
6382
  }
5350
6383
  if (origin === "opaque") {
@@ -5355,10 +6388,10 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5355
6388
  await routeApi(req, res, pathname, url);
5356
6389
  return;
5357
6390
  }
5358
- const id = safeDecode(pathname.slice("/attachments/".length));
5359
- const a = id === null ? null : await attachments.get(id);
6391
+ const id2 = safeDecode(pathname.slice("/attachments/".length));
6392
+ const a = id2 === null ? null : await attachments.get(id2);
5360
6393
  if (!a) {
5361
- sendJson(res, 404, { ok: false, e: { code: "not_found", message: "\u9644\u4EF6\u4E0D\u5B58\u5728" } });
6394
+ sendJson(res, 404, { ok: false, e: { code: "not_found", message: tr("http.noAttachment") } });
5362
6395
  return;
5363
6396
  }
5364
6397
  const data2 = await attachments.read(a.id);
@@ -5366,11 +6399,11 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5366
6399
  return;
5367
6400
  }
5368
6401
  if (origin === "opaque") {
5369
- sendJson(res, 403, { ok: false, e: { code: "bad_origin", message: "\u6765\u6E90\u4E0D\u5141\u8BB8" } });
6402
+ sendJson(res, 403, { ok: false, e: { code: "bad_origin", message: tr("http.badOrigin") } });
5370
6403
  return;
5371
6404
  }
5372
6405
  if (req.method !== "GET" && req.method !== "HEAD") {
5373
- sendJson(res, 405, { ok: false, e: { code: "method", message: "\u65B9\u6CD5\u4E0D\u5141\u8BB8" } });
6406
+ sendJson(res, 405, { ok: false, e: { code: "method", message: tr("http.method") } });
5374
6407
  return;
5375
6408
  }
5376
6409
  if (pathname === "/" || pathname === "/index.html") {
@@ -5378,23 +6411,23 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5378
6411
  const html = await readFile(join4(appDir, "index.html"), "utf8");
5379
6412
  send(res, 200, html, { ...STATIC_HEADERS, "content-type": "text/html; charset=utf-8" });
5380
6413
  } catch {
5381
- send(res, 503, "app \u8FD8\u6CA1\u6784\u5EFA\uFF08\u7F3A app/index.html\uFF09", { ...STATIC_HEADERS, "content-type": "text/plain; charset=utf-8" });
6414
+ send(res, 503, tr("http.noApp"), { ...STATIC_HEADERS, "content-type": "text/plain; charset=utf-8" });
5382
6415
  }
5383
6416
  return;
5384
6417
  }
5385
6418
  const decoded = safeDecode(pathname);
5386
6419
  if (decoded === null) {
5387
- sendJson(res, 400, { ok: false, e: { code: "bad_path", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5" } });
6420
+ sendJson(res, 400, { ok: false, e: { code: "bad_path", message: tr("http.badPath") } });
5388
6421
  return;
5389
6422
  }
5390
6423
  const rel = normalize(decoded).replace(/^([\\/])+/, "");
5391
6424
  const full = normalize(join4(appDir, rel));
5392
6425
  if (!full.startsWith(normalize(appDir)) || rel.includes("..")) {
5393
- sendJson(res, 403, { ok: false, e: { code: "bad_path", message: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" } });
6426
+ sendJson(res, 403, { ok: false, e: { code: "bad_path", message: tr("http.pathDenied") } });
5394
6427
  return;
5395
6428
  }
5396
6429
  if (!existsSync2(full) || !(await stat(full)).isFile()) {
5397
- sendJson(res, 404, { ok: false, e: { code: "not_found", message: "\u6587\u4EF6\u4E0D\u5B58\u5728" } });
6430
+ sendJson(res, 404, { ok: false, e: { code: "not_found", message: tr("http.noFile") } });
5398
6431
  return;
5399
6432
  }
5400
6433
  const data = await readFile(full);
@@ -5408,13 +6441,13 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5408
6441
  if (pathname === "/api/upload" && req.method === "POST") {
5409
6442
  const name = url.searchParams.get("name") ?? "";
5410
6443
  if (!attachments.allowedExt(name)) {
5411
- sendJson(res, 400, { ok: false, e: { code: "bad_type", message: "\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B" } });
6444
+ sendJson(res, 400, { ok: false, e: { code: "bad_type", message: tr("http.badType") } });
5412
6445
  return;
5413
6446
  }
5414
6447
  const declared = Number(req.headers["content-length"] ?? 0);
5415
6448
  if (declared > MAX_UPLOAD) {
5416
6449
  res.setHeader("connection", "close");
5417
- sendJson(res, 413, { ok: false, e: { code: "too_large", message: "\u6587\u4EF6\u592A\u5927\uFF08\u4E0A\u9650 50MB\uFF09" } });
6450
+ sendJson(res, 413, { ok: false, e: { code: "too_large", message: tr("http.tooLarge") } });
5418
6451
  req.resume();
5419
6452
  return;
5420
6453
  }
@@ -5425,7 +6458,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5425
6458
  size += b.length;
5426
6459
  if (size > MAX_UPLOAD) {
5427
6460
  res.setHeader("connection", "close");
5428
- sendJson(res, 413, { ok: false, e: { code: "too_large", message: "\u6587\u4EF6\u592A\u5927\uFF08\u4E0A\u9650 50MB\uFF09" } });
6461
+ sendJson(res, 413, { ok: false, e: { code: "too_large", message: tr("http.tooLarge") } });
5429
6462
  return;
5430
6463
  }
5431
6464
  chunks.push(b);
@@ -5434,7 +6467,7 @@ function createHttpServer(cfg2, appDir, attachments, hooks = {}) {
5434
6467
  sendJson(res, 200, { ok: true, id: a.id, name: a.name, mime: a.mime, size: a.size });
5435
6468
  return;
5436
6469
  }
5437
- sendJson(res, 404, { ok: false, e: { code: "not_found", message: "\u63A5\u53E3\u4E0D\u5B58\u5728" } });
6470
+ sendJson(res, 404, { ok: false, e: { code: "not_found", message: tr("http.noRoute") } });
5438
6471
  }
5439
6472
  if (hooks.onUpgrade) {
5440
6473
  server.on("upgrade", hooks.onUpgrade);
@@ -5476,105 +6509,58 @@ var AttachmentStore = class {
5476
6509
  async save(name, data) {
5477
6510
  const ext = this.safeExt(name);
5478
6511
  if (!ext) throw new Error("\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B");
5479
- const id = "a_" + Date.now().toString(36) + randomBytes2(3).toString("hex");
6512
+ const id2 = "a_" + Date.now().toString(36) + randomBytes2(3).toString("hex");
5480
6513
  await mkdir(this.dir, { recursive: true });
5481
- const file = join5(this.dir, id + ext);
6514
+ const file = join5(this.dir, id2 + ext);
5482
6515
  await writeFile(file, data);
5483
- this.files.set(id, id + ext);
6516
+ this.files.set(id2, id2 + ext);
5484
6517
  const mime = MIME2[ext] ?? TEXT_MIME;
5485
- return { id, name: name || "\u672A\u547D\u540D" + ext, mime, size: data.length, path: file };
6518
+ return { id: id2, name: name || "\u672A\u547D\u540D" + ext, mime, size: data.length, path: file };
5486
6519
  }
5487
- async get(id) {
5488
- if (!validId(id)) return null;
5489
- const known = this.files.get(id);
6520
+ async get(id2) {
6521
+ if (!validId(id2)) return null;
6522
+ const known = this.files.get(id2);
5490
6523
  if (known) {
5491
- const hit = await this.describe(id, known);
6524
+ const hit = await this.describe(id2, known);
5492
6525
  if (hit) return hit;
5493
- this.files.delete(id);
6526
+ this.files.delete(id2);
5494
6527
  }
5495
6528
  const { readdir: readdir7 } = await import("node:fs/promises");
5496
6529
  try {
5497
6530
  for (const f of await readdir7(this.dir)) {
5498
- if (f.startsWith(id + ".")) {
5499
- this.files.set(id, f);
5500
- return await this.describe(id, f);
6531
+ if (f.startsWith(id2 + ".")) {
6532
+ this.files.set(id2, f);
6533
+ return await this.describe(id2, f);
5501
6534
  }
5502
6535
  }
5503
6536
  } catch {
5504
6537
  }
5505
6538
  return null;
5506
6539
  }
5507
- async describe(id, f) {
6540
+ async describe(id2, f) {
5508
6541
  const p = join5(this.dir, f);
5509
6542
  const s = await stat2(p).catch(() => null);
5510
6543
  if (!s?.isFile()) return null;
5511
6544
  const ext = extname2(f).toLowerCase();
5512
- return { id, name: f.slice(id.length + 1) || f, mime: MIME2[ext] ?? TEXT_MIME, size: s.size, path: p };
6545
+ return { id: id2, name: f.slice(id2.length + 1) || f, mime: MIME2[ext] ?? TEXT_MIME, size: s.size, path: p };
5513
6546
  }
5514
- async read(id) {
5515
- const a = await this.get(id);
6547
+ async read(id2) {
6548
+ const a = await this.get(id2);
5516
6549
  if (!a) return null;
5517
6550
  return readFile2(a.path);
5518
6551
  }
5519
6552
  };
5520
6553
 
5521
- // daemon/src/store/settings.ts
5522
- init_atomic();
5523
- import { readFile as readFile10 } from "node:fs/promises";
5524
- import { join as join11 } from "node:path";
5525
- var DEFAULT_SETTINGS = {
5526
- defaultProvider: "claude",
5527
- defaultMode: "default",
5528
- defaultEffort: null,
5529
- sendKey: "enter",
5530
- runningSend: "queue",
5531
- showThinking: true,
5532
- density: "comfortable",
5533
- theme: "auto",
5534
- idleCloseMinutes: 30,
5535
- idleExitMinutes: 60,
5536
- collapsedProjects: [],
5537
- panel: { open: false, tab: "changes", width: 440 },
5538
- sidebarWidth: 248,
5539
- lastProjectId: null,
5540
- lastThreadId: null,
5541
- recentDirs: []
5542
- };
5543
- var SettingsStore = class {
5544
- file;
5545
- cache = null;
5546
- writing = Promise.resolve();
5547
- constructor(dataDir) {
5548
- this.file = join11(dataDir, "settings.json");
5549
- }
5550
- async get() {
5551
- if (this.cache) return this.cache;
5552
- let raw = {};
5553
- try {
5554
- raw = JSON.parse(await readFile10(this.file, "utf8"));
5555
- } catch {
5556
- }
5557
- this.cache = { ...DEFAULT_SETTINGS, ...raw, panel: { ...DEFAULT_SETTINGS.panel, ...raw.panel } };
5558
- return this.cache;
5559
- }
5560
- async set(patch) {
5561
- const cur = await this.get();
5562
- const next = { ...cur, ...patch, panel: { ...cur.panel, ...patch.panel } };
5563
- this.cache = next;
5564
- const snap = next;
5565
- this.writing = this.writing.then(() => writeJsonAtomic(this.file, snap));
5566
- await this.writing;
5567
- return next;
5568
- }
5569
- };
6554
+ // daemon/src/main.ts
6555
+ init_settings();
5570
6556
 
5571
6557
  // daemon/src/store/projects.ts
5572
6558
  init_atomic();
5573
6559
  init_paths();
5574
6560
  init_methods();
5575
- import { readFile as readFile11 } from "node:fs/promises";
6561
+ import { readFile as readFile13 } from "node:fs/promises";
5576
6562
  import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
5577
- import { join as join12 } from "node:path";
6563
+ import { join as join14 } from "node:path";
5578
6564
  import { randomBytes as randomBytes3 } from "node:crypto";
5579
6565
  function newId() {
5580
6566
  return "p_" + Date.now().toString(36) + randomBytes3(3).toString("hex");
@@ -5584,21 +6570,21 @@ var ProjectsStore = class {
5584
6570
  cache = null;
5585
6571
  writing = Promise.resolve();
5586
6572
  constructor(dataDir) {
5587
- this.file = join12(dataDir, "projects.json");
6573
+ this.file = join14(dataDir, "projects.json");
5588
6574
  }
5589
6575
  async list() {
5590
6576
  if (this.cache) return this.cache;
5591
6577
  try {
5592
- this.cache = JSON.parse(await readFile11(this.file, "utf8"));
6578
+ this.cache = JSON.parse(await readFile13(this.file, "utf8"));
5593
6579
  if (!Array.isArray(this.cache)) this.cache = [];
5594
6580
  } catch {
5595
6581
  this.cache = [];
5596
6582
  }
5597
6583
  return this.cache;
5598
6584
  }
5599
- async get(id) {
5600
- if (!validId(id)) return null;
5601
- return (await this.list()).find((p) => p.id === id) ?? null;
6585
+ async get(id2) {
6586
+ if (!validId(id2)) return null;
6587
+ return (await this.list()).find((p) => p.id === id2) ?? null;
5602
6588
  }
5603
6589
  async save(list) {
5604
6590
  this.cache = list;
@@ -5625,17 +6611,17 @@ var ProjectsStore = class {
5625
6611
  await this.save(list);
5626
6612
  return proj;
5627
6613
  }
5628
- async rename(id, name) {
6614
+ async rename(id2, name) {
5629
6615
  const list = await this.list();
5630
- const p = list.find((x) => x.id === id);
6616
+ const p = list.find((x) => x.id === id2);
5631
6617
  if (!p) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
5632
6618
  p.name = name.trim() || p.name;
5633
6619
  await this.save(list);
5634
6620
  return p;
5635
6621
  }
5636
- async remove(id) {
6622
+ async remove(id2) {
5637
6623
  const list = await this.list();
5638
- const idx = list.findIndex((x) => x.id === id);
6624
+ const idx = list.findIndex((x) => x.id === id2);
5639
6625
  if (idx < 0) return;
5640
6626
  list.splice(idx, 1);
5641
6627
  await this.save(list);
@@ -5648,8 +6634,8 @@ init_jsonl();
5648
6634
  init_paths();
5649
6635
  init_methods();
5650
6636
  init_i18n();
5651
- import { mkdir as mkdir5, rename as rename3, readdir as readdir4, readFile as readFile13 } from "node:fs/promises";
5652
- import { join as join13 } from "node:path";
6637
+ import { mkdir as mkdir6, rename as rename3, readdir as readdir4, readFile as readFile15 } from "node:fs/promises";
6638
+ import { join as join15 } from "node:path";
5653
6639
  import { randomBytes as randomBytes4 } from "node:crypto";
5654
6640
  function newId2() {
5655
6641
  return "t_" + Date.now().toString(36) + randomBytes4(3).toString("hex");
@@ -5668,17 +6654,17 @@ var ThreadsStore = class {
5668
6654
  return p;
5669
6655
  }
5670
6656
  constructor(dataDir) {
5671
- this.dir = join13(dataDir, "threads");
5672
- this.trashDir = join13(dataDir, "trash", "threads");
6657
+ this.dir = join15(dataDir, "threads");
6658
+ this.trashDir = join15(dataDir, "trash", "threads");
5673
6659
  }
5674
- tdir(id) {
5675
- return join13(this.dir, id);
6660
+ tdir(id2) {
6661
+ return join15(this.dir, id2);
5676
6662
  }
5677
- metaPath(id) {
5678
- return join13(this.tdir(id), "meta.json");
6663
+ metaPath(id2) {
6664
+ return join15(this.tdir(id2), "meta.json");
5679
6665
  }
5680
- timelinePath(id) {
5681
- return join13(this.tdir(id), "timeline.jsonl");
6666
+ timelinePath(id2) {
6667
+ return join15(this.tdir(id2), "timeline.jsonl");
5682
6668
  }
5683
6669
  async loadAll() {
5684
6670
  if (this.loaded) return;
@@ -5689,9 +6675,9 @@ var ThreadsStore = class {
5689
6675
  } catch {
5690
6676
  return;
5691
6677
  }
5692
- for (const id of ids) {
6678
+ for (const id2 of ids) {
5693
6679
  try {
5694
- const m = JSON.parse(await readFile13(this.metaPath(id), "utf8"));
6680
+ const m = JSON.parse(await readFile15(this.metaPath(id2), "utf8"));
5695
6681
  if (m && typeof m.id === "string") this.metas.set(m.id, m);
5696
6682
  } catch {
5697
6683
  }
@@ -5701,10 +6687,10 @@ var ThreadsStore = class {
5701
6687
  await this.loadAll();
5702
6688
  return [...this.metas.values()].sort((a, b) => b.updatedAt - a.updatedAt);
5703
6689
  }
5704
- async get(id) {
5705
- if (!validId(id)) return null;
6690
+ async get(id2) {
6691
+ if (!validId(id2)) return null;
5706
6692
  await this.loadAll();
5707
- return this.metas.get(id) ?? null;
6693
+ return this.metas.get(id2) ?? null;
5708
6694
  }
5709
6695
  async saveMeta(meta) {
5710
6696
  this.metas.set(meta.id, meta);
@@ -5733,16 +6719,16 @@ var ThreadsStore = class {
5733
6719
  seq: 0
5734
6720
  };
5735
6721
  try {
5736
- await mkdir5(this.tdir(meta.id), { recursive: true });
6722
+ await mkdir6(this.tdir(meta.id), { recursive: true });
5737
6723
  } catch (e) {
5738
6724
  throw new ApiError("io_error", `\u521B\u5EFA\u5BF9\u8BDD\u76EE\u5F55\u5931\u8D25\uFF1A${e?.message ?? e}`);
5739
6725
  }
5740
6726
  await this.saveMeta(meta);
5741
6727
  return meta;
5742
6728
  }
5743
- async patch(id, patch) {
6729
+ async patch(id2, patch) {
5744
6730
  return this.serial(async () => {
5745
- const meta = await this.get(id);
6731
+ const meta = await this.get(id2);
5746
6732
  if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
5747
6733
  if (patch.title !== void 0 && patch.titleAuto === void 0) patch = { ...patch, titleAuto: false };
5748
6734
  const next = { ...meta, ...patch, id: meta.id, updatedAt: Date.now() };
@@ -5752,37 +6738,37 @@ var ThreadsStore = class {
5752
6738
  }
5753
6739
  // Append an event: inside the serialized section take meta.seq+1 as seq, write the jsonl and update meta.
5754
6740
  // A passed-in seq is ignored (keeps it strictly increasing); ts/threadId are filled in when missing.
5755
- async appendEvent(id, ev) {
6741
+ async appendEvent(id2, ev) {
5756
6742
  return this.serial(async () => {
5757
- const meta = await this.get(id);
6743
+ const meta = await this.get(id2);
5758
6744
  if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
5759
6745
  const full = {
5760
6746
  ...ev,
5761
6747
  seq: meta.seq + 1,
5762
6748
  ts: ev.ts ?? Date.now(),
5763
- threadId: id
6749
+ threadId: id2
5764
6750
  };
5765
- await appendJsonl(this.timelinePath(id), full);
6751
+ await appendJsonl(this.timelinePath(id2), full);
5766
6752
  meta.seq = full.seq;
5767
6753
  meta.updatedAt = Date.now();
5768
- this.metas.set(id, meta);
5769
- await writeJsonAtomic(this.metaPath(id), meta);
6754
+ this.metas.set(id2, meta);
6755
+ await writeJsonAtomic(this.metaPath(id2), meta);
5770
6756
  return full;
5771
6757
  });
5772
6758
  }
5773
- async readEvents(id, afterSeq = 0, limit = 5e3) {
5774
- const rows = await readJsonl(this.timelinePath(id));
6759
+ async readEvents(id2, afterSeq = 0, limit = 5e3) {
6760
+ const rows = await readJsonl(this.timelinePath(id2));
5775
6761
  const out = rows.filter((e) => typeof e.seq === "number" && e.seq > afterSeq);
5776
6762
  return out.slice(-limit);
5777
6763
  }
5778
- async delete(id) {
5779
- const meta = await this.get(id);
6764
+ async delete(id2) {
6765
+ const meta = await this.get(id2);
5780
6766
  if (!meta) return;
5781
6767
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
5782
- await mkdir5(this.trashDir, { recursive: true });
6768
+ await mkdir6(this.trashDir, { recursive: true });
5783
6769
  for (let attempt = 0; ; attempt++) {
5784
6770
  try {
5785
- await rename3(this.tdir(id), join13(this.trashDir, `${id}-${stamp2}`));
6771
+ await rename3(this.tdir(id2), join15(this.trashDir, `${id2}-${stamp2}`));
5786
6772
  break;
5787
6773
  } catch (e) {
5788
6774
  const code = e?.code;
@@ -5793,7 +6779,7 @@ var ThreadsStore = class {
5793
6779
  throw new ApiError("internal", "\u79FB\u5165\u56DE\u6536\u76EE\u5F55\u5931\u8D25\uFF1A" + String(e));
5794
6780
  }
5795
6781
  }
5796
- this.metas.delete(id);
6782
+ this.metas.delete(id2);
5797
6783
  }
5798
6784
  };
5799
6785
 
@@ -6000,6 +6986,8 @@ init_atomic();
6000
6986
  init_registry();
6001
6987
 
6002
6988
  // daemon/src/agents/manager.ts
6989
+ init_launch();
6990
+ init_modes();
6003
6991
  init_attachmentRefs();
6004
6992
  init_registry();
6005
6993
  init_catalog();
@@ -6041,7 +7029,7 @@ var Coalescer = class {
6041
7029
  this.flush(itemId);
6042
7030
  }
6043
7031
  dispose() {
6044
- for (const id of [...this.acc.keys()]) this.flush(id);
7032
+ for (const id2 of [...this.acc.keys()]) this.flush(id2);
6045
7033
  }
6046
7034
  };
6047
7035
 
@@ -6051,6 +7039,7 @@ init_log();
6051
7039
  init_i18n();
6052
7040
  init_commands();
6053
7041
  var SLASH_CACHE_TTL_MS = 10 * 60 * 1e3;
7042
+ var MAX_WARM_IDLE = 3;
6054
7043
  var DELTA_TYPES = ["assistant_delta", "reasoning_delta", "tool_output_delta"];
6055
7044
  var deltaKey = (type, itemId) => `${type}\0${itemId}`;
6056
7045
  var AgentManager = class {
@@ -6129,6 +7118,16 @@ var AgentManager = class {
6129
7118
  }
6130
7119
  return false;
6131
7120
  }
7121
+ /** Drop a queued message that hasn't been sent yet (the × on the composer's queue list) */
7122
+ removeQueued(threadId, queueId) {
7123
+ const rt = this.runtimes.get(threadId);
7124
+ if (!rt) return false;
7125
+ const before = rt.queue.length;
7126
+ rt.queue = rt.queue.filter((q) => q.id !== queueId);
7127
+ if (rt.queue.length === before) return false;
7128
+ this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
7129
+ return true;
7130
+ }
6132
7131
  runtimeInfo(threadId) {
6133
7132
  const rt = this.runtimes.get(threadId);
6134
7133
  if (!rt) return null;
@@ -6159,7 +7158,9 @@ var AgentManager = class {
6159
7158
  endedTurnIds: /* @__PURE__ */ new Set(),
6160
7159
  starting: false,
6161
7160
  commands: [],
6162
- sessionCreating: null
7161
+ sessionCreating: null,
7162
+ pendingText: /* @__PURE__ */ new Map(),
7163
+ deltasOnly: false
6163
7164
  };
6164
7165
  this.runtimes.set(threadId, r);
6165
7166
  }
@@ -6208,17 +7209,18 @@ var AgentManager = class {
6208
7209
  steered: true,
6209
7210
  attachments: this.attachmentLabels(attachmentRefs)
6210
7211
  });
6211
- return;
7212
+ return "steered";
6212
7213
  } catch (e) {
6213
7214
  log("info", `thread ${threadId} \u63D2\u8BDD\u5931\u8D25\uFF0C\u6539\u4E3A\u6392\u961F\uFF1A${String(e)}`);
6214
7215
  }
6215
7216
  }
6216
- const q = { id: "q" + Date.now().toString(36), text: input.text, attachments: attachmentRefs };
7217
+ const q = { id: "q" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), text: input.text, attachments: attachmentRefs };
6217
7218
  rt.queue.push(q);
6218
7219
  this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
6219
- return;
7220
+ return "queued";
6220
7221
  }
6221
7222
  await this.startTurn(threadId, input.text, attachmentRefs);
7223
+ return "started";
6222
7224
  }
6223
7225
  async startTurn(threadId, text, attachmentRefs) {
6224
7226
  const rt = this.rt(threadId);
@@ -6313,12 +7315,21 @@ var AgentManager = class {
6313
7315
  return;
6314
7316
  }
6315
7317
  if (ev.type === "assistant_delta" || ev.type === "reasoning_delta" || ev.type === "tool_output_delta") {
7318
+ if (rt.deltasOnly && ev.type !== "tool_output_delta") {
7319
+ const key = `${ev.type === "assistant_delta" ? "assistant" : "reasoning"}\0${ev.itemId}`;
7320
+ const prior = rt.pendingText.get(key);
7321
+ rt.pendingText.set(key, { type: ev.type === "assistant_delta" ? "assistant" : "reasoning", itemId: ev.itemId, text: (prior?.text ?? "") + ev.text });
7322
+ }
6316
7323
  rt.coalescer.push(deltaKey(ev.type, ev.itemId), ev.text);
6317
7324
  return;
6318
7325
  }
6319
7326
  const evItemId = ev.itemId;
6320
7327
  if (typeof evItemId === "string") this.flushDeltas(rt, evItemId);
7328
+ if (rt.pendingText.size && ev.type !== "usage" && ev.type !== "assistant_message" && ev.type !== "reasoning") {
7329
+ await this.persistPendingText(threadId, rt);
7330
+ }
6321
7331
  if (ev.type === "assistant_message" || ev.type === "reasoning") {
7332
+ rt.pendingText.delete(`${ev.type === "assistant_message" ? "assistant" : "reasoning"}\0${ev.itemId}`);
6322
7333
  await this.appendRaw(threadId, ev);
6323
7334
  return;
6324
7335
  }
@@ -6375,6 +7386,8 @@ var AgentManager = class {
6375
7386
  if (ev.status === "failed" && rt.session && !this.alive(rt.session)) rt.session = null;
6376
7387
  await this.setStatus(threadId, "idle");
6377
7388
  const next = rt.queue.shift();
7389
+ if (!next) void this.recycleIdle().catch(() => {
7390
+ });
6378
7391
  if (next) {
6379
7392
  this.gw.publish(`thread:${threadId}`, { type: "queue", queue: rt.queue });
6380
7393
  await this.startTurn(threadId, next.text, next.attachments).catch((e) => {
@@ -6385,6 +7398,15 @@ var AgentManager = class {
6385
7398
  }
6386
7399
  await this.appendRaw(threadId, ev);
6387
7400
  }
7401
+ async persistPendingText(threadId, rt) {
7402
+ const pending = [...rt.pendingText.values()];
7403
+ rt.pendingText.clear();
7404
+ for (const p of pending) {
7405
+ if (!p.text) continue;
7406
+ this.flushDeltas(rt, p.itemId);
7407
+ await this.appendRaw(threadId, { type: p.type === "assistant" ? "assistant_message" : "reasoning", itemId: p.itemId, text: p.text });
7408
+ }
7409
+ }
6388
7410
  alive(_s) {
6389
7411
  return true;
6390
7412
  }
@@ -6392,10 +7414,10 @@ var AgentManager = class {
6392
7414
  async respond(threadId, requestId, answer) {
6393
7415
  const rt = this.rt(threadId);
6394
7416
  const pending = rt.pending.get(requestId);
6395
- if (!pending) throw new ApiError("not_found", "\u6388\u6743/\u63D0\u95EE\u4E0D\u5B58\u5728\u6216\u5DF2\u56DE\u5E94");
7417
+ if (!pending) throw new ApiError("not_found", tr("err.requestGone"));
6396
7418
  if (pending.kind === "permission" && answer.optionId) {
6397
7419
  const valid = pending.options.some((o) => o.id === answer.optionId);
6398
- if (!valid) throw new ApiError("bad_params", `optionId \u975E\u6CD5\uFF1A${answer.optionId}`);
7420
+ if (!valid) throw new ApiError("bad_params", tr("err.badOption", { id: String(answer.optionId) }));
6399
7421
  }
6400
7422
  rt.pending.delete(requestId);
6401
7423
  if (pending.kind === "permission") {
@@ -6423,8 +7445,8 @@ var AgentManager = class {
6423
7445
  async interrupt(threadId) {
6424
7446
  const rt = this.rt(threadId);
6425
7447
  if (!rt.session || rt.status !== "running") return;
6426
- for (const [id, p] of rt.pending) {
6427
- await this.appendRaw(threadId, { type: "permission_resolved", requestId: id, cancelled: true });
7448
+ for (const [id2, p] of rt.pending) {
7449
+ await this.appendRaw(threadId, { type: "permission_resolved", requestId: id2, cancelled: true });
6428
7450
  p.resolve({ cancelled: true });
6429
7451
  }
6430
7452
  rt.pending.clear();
@@ -6462,10 +7484,10 @@ var AgentManager = class {
6462
7484
  if (rt.session) await rt.session.setModel(model);
6463
7485
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
6464
7486
  }
6465
- async setMode(threadId, mode) {
6466
- await this.threads.patch(threadId, { mode });
7487
+ async setMode(threadId, mode2) {
7488
+ await this.threads.patch(threadId, { mode: mode2 });
6467
7489
  const rt = this.rt(threadId);
6468
- if (rt.session) await rt.session.setMode(mode);
7490
+ if (rt.session) await rt.session.setMode(mode2);
6469
7491
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
6470
7492
  }
6471
7493
  async setEffort(threadId, effort) {
@@ -6484,7 +7506,7 @@ var AgentManager = class {
6484
7506
  }
6485
7507
  async setCursorOpts(threadId, patch) {
6486
7508
  const meta = await this.mustThread(threadId);
6487
- if (meta.provider !== "cursor") throw new ApiError("bad_params", "\u4EC5 Cursor \u5BF9\u8BDD\u652F\u6301\u8BE5\u9009\u9879");
7509
+ if (meta.provider !== "cursor") throw new ApiError("bad_params", tr("err.cursorOnly"));
6488
7510
  const next = await this.threads.patch(threadId, patch);
6489
7511
  const rt = this.rt(threadId);
6490
7512
  const sess = rt.session;
@@ -6497,14 +7519,14 @@ var AgentManager = class {
6497
7519
  async setProvider(threadId, providerId) {
6498
7520
  await this.mustThread(threadId);
6499
7521
  const meta = await this.threads.get(threadId);
6500
- if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
7522
+ if (!meta) throw new ApiError("not_found", tr("err.threadMissing"));
6501
7523
  const rt = this.rt(threadId);
6502
7524
  if (rt.status === "running" || rt.status === "initializing" || rt.starting) {
6503
- throw new ApiError("busy", "\u5BF9\u8BDD\u8FD0\u884C\u4E2D\uFF0C\u4E0D\u80FD\u66F4\u6362 Agent");
7525
+ throw new ApiError("busy", tr("err.busySwitch"));
6504
7526
  }
6505
7527
  const events = await this.threads.readEvents(threadId, 0, 5e3);
6506
7528
  if (events.some((e) => e.type === "user_message")) {
6507
- throw new ApiError("bad_params", "\u5DF2\u6709\u6D88\u606F\uFF0C\u4E0D\u80FD\u66F4\u6362 Agent");
7529
+ throw new ApiError("bad_params", tr("err.hasMessages"));
6508
7530
  }
6509
7531
  const p = await this.assertProviderUsable(providerId);
6510
7532
  if (rt.session) {
@@ -6519,7 +7541,7 @@ var AgentManager = class {
6519
7541
  await this.threads.patch(threadId, {
6520
7542
  provider: providerId,
6521
7543
  model: null,
6522
- mode: p.capabilities.modes.includes(meta.mode) ? meta.mode : p.capabilities.modes[0] ?? "default",
7544
+ mode: clampMode(p.capabilities.modes, meta.mode),
6523
7545
  effort: null,
6524
7546
  providerSessionId: null,
6525
7547
  status: "idle"
@@ -6622,7 +7644,7 @@ var AgentManager = class {
6622
7644
  if (rt.session) return rt.session;
6623
7645
  const provider = await this.assertProviderUsable(meta.provider, { allowDisabled: !!meta.providerSessionId });
6624
7646
  const proj = await this.projects.get(meta.projectId);
6625
- if (!proj) throw new ApiError("not_found", "\u9879\u76EE\u4E0D\u5B58\u5728");
7647
+ if (!proj) throw new ApiError("not_found", tr("err.projectMissing"));
6626
7648
  let model = meta.model;
6627
7649
  if (meta.provider === "dsh" && model) {
6628
7650
  const available = await cachedModels(provider).catch(() => []);
@@ -6631,16 +7653,20 @@ var AgentManager = class {
6631
7653
  await this.threads.patch(threadId, { model });
6632
7654
  this.gw.publish("sidebar", { type: "thread.upsert", thread: await this.threads.get(threadId) });
6633
7655
  }
7656
+ } else if (meta.provider === "dsh" && !model) {
7657
+ const available = await cachedModels(provider).catch(() => []);
7658
+ const magpie = available.filter((m) => /magpie/i.test(m.id));
7659
+ model = (magpie.find((m) => m.isDefault) ?? magpie[0] ?? available.find((m) => m.isDefault))?.id ?? null;
6634
7660
  }
6635
7661
  if (opts.updateStatus) await this.setStatus(threadId, "initializing");
6636
7662
  const settings = await this.settings.get();
6637
- const custom = settings.providerCommand?.[meta.provider];
6638
- const launch = custom?.executable?.trim() ? { cmd: custom.executable.trim(), prefixArgs: custom.args ?? [] } : void 0;
7663
+ const launch = launchFromSettings(settings, meta.provider);
7664
+ rt.deltasOnly = !!provider.capabilities.deltasOnly;
6639
7665
  rt.session = await provider.createSession({
6640
7666
  threadId,
6641
7667
  cwd: proj.path,
6642
7668
  model,
6643
- mode: meta.mode,
7669
+ mode: clampMode(provider.capabilities.modes, meta.mode),
6644
7670
  effort: meta.effort,
6645
7671
  resumeId: meta.providerSessionId,
6646
7672
  cursorFast: meta.cursorFast,
@@ -6658,7 +7684,7 @@ var AgentManager = class {
6658
7684
  // ---- internals ----
6659
7685
  async mustThread(threadId) {
6660
7686
  const meta = await this.threads.get(threadId);
6661
- if (!meta) throw new ApiError("not_found", "\u5BF9\u8BDD\u4E0D\u5B58\u5728");
7687
+ if (!meta) throw new ApiError("not_found", tr("err.threadMissing"));
6662
7688
  return meta;
6663
7689
  }
6664
7690
  providerOf(meta) {
@@ -6666,17 +7692,17 @@ var AgentManager = class {
6666
7692
  }
6667
7693
  async assertProviderUsable(providerId, opts = {}) {
6668
7694
  const p = getProvider(providerId);
6669
- if (!p) throw new ApiError("bad_params", `\u672A\u77E5 Agent\uFF1A${providerId}`);
7695
+ if (!p) throw new ApiError("bad_params", tr("provider.unknown", { id: providerId }));
6670
7696
  const s = await this.settings.get();
6671
7697
  if (!opts.allowDisabled && !isProviderEnabled(s, providerId)) {
6672
- throw new ApiError("bad_params", `${p.name} \u5DF2\u5728\u8BBE\u7F6E\u4E2D\u7981\u7528`);
7698
+ throw new ApiError("bad_params", tr("provider.disabledNamed", { name: p.name }));
6673
7699
  }
6674
7700
  const entry = catalogById(providerId);
6675
7701
  if (entry && !providerCreatable(entry.status)) {
6676
- throw new ApiError("bad_params", `${p.name} \u5C1A\u672A\u63A5\u5165\uFF0C\u6682\u4E0D\u80FD\u65B0\u5EFA\u5BF9\u8BDD`);
7702
+ throw new ApiError("bad_params", tr("provider.plannedNamed", { name: p.name }));
6677
7703
  }
6678
- const det = await cachedDetect(p);
6679
- if (!det.available) throw new ApiError("bad_params", det.reason ?? `${p.name} \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528`);
7704
+ const det = await cachedDetect(p, launchFromSettings(s, providerId));
7705
+ if (!det.available) throw new ApiError("bad_params", det.reason ?? tr("provider.unavailable", { name: p.name }));
6680
7706
  return p;
6681
7707
  }
6682
7708
  async setStatus(threadId, status) {
@@ -6698,23 +7724,41 @@ var AgentManager = class {
6698
7724
  const full = { ...ev, seq: -1, ts: Date.now(), threadId };
6699
7725
  this.gw.publish(`thread:${threadId}`, { type: "event", event: full });
6700
7726
  }
7727
+ /** B17: idle Agent processes are closed after idleCloseMinutes, and at most MAX_WARM_IDLE of them stay warm
7728
+ * (least recently used go first). A running turn, one starting, or one waiting on an approval is never touched. */
7729
+ recycling = false;
6701
7730
  async recycleIdle() {
6702
- const s = await this.settings.get();
6703
- const limit = (s.idleCloseMinutes ?? 30) * 6e4;
6704
- for (const rt of this.runtimes.values()) {
6705
- if (rt.status === "idle" && rt.pending.size === 0 && Date.now() - rt.lastActivity > limit) {
6706
- log("info", `\u7A7A\u95F2\u56DE\u6536 thread ${rt.threadId}`);
6707
- await this.close(rt.threadId);
7731
+ if (this.recycling) return;
7732
+ this.recycling = true;
7733
+ try {
7734
+ const s = await this.settings.get();
7735
+ const minutes = Number(s.idleCloseMinutes);
7736
+ const limit = (Number.isFinite(minutes) && minutes > 0 ? minutes : 30) * 6e4;
7737
+ const idle = [...this.runtimes.values()].filter((rt) => rt.session && rt.status === "idle" && !rt.starting && !rt.sessionCreating && rt.pending.size === 0 && rt.queue.length === 0).sort((a, b) => b.lastActivity - a.lastActivity);
7738
+ const now = Date.now();
7739
+ for (const [i, rt] of idle.entries()) {
7740
+ const expired = now - rt.lastActivity > limit;
7741
+ if (!expired && i < MAX_WARM_IDLE) continue;
7742
+ log("info", `idle cleanup: closing thread ${rt.threadId} (${expired ? "idle timeout" : "over the warm limit"})`);
7743
+ try {
7744
+ await this.close(rt.threadId);
7745
+ } catch (e) {
7746
+ log("info", `idle cleanup: thread ${rt.threadId}: ${String(e?.message ?? e)}`);
7747
+ this.runtimes.delete(rt.threadId);
7748
+ }
6708
7749
  }
7750
+ } finally {
7751
+ this.recycling = false;
6709
7752
  }
6710
7753
  }
6711
7754
  };
6712
7755
 
6713
7756
  // daemon/src/git/service.ts
7757
+ init_i18n();
6714
7758
  import { execFile as execFile4 } from "node:child_process";
6715
7759
  import { existsSync as existsSync10 } from "node:fs";
6716
- import { cp, mkdir as mkdir6, readFile as readFile14, rename as rename4, rm, stat as stat7 } from "node:fs/promises";
6717
- import { join as join14, normalize as normalize2, sep as sep2 } from "node:path";
7760
+ import { cp, mkdir as mkdir7, readFile as readFile16, rename as rename4, rm, stat as stat9 } from "node:fs/promises";
7761
+ import { join as join16, normalize as normalize2, sep as sep2 } from "node:path";
6718
7762
  var TIMEOUT2 = 15e3;
6719
7763
  var DIFF_LIMIT = 1024 * 1024;
6720
7764
  function runGit(cwd, args) {
@@ -6722,13 +7766,13 @@ function runGit(cwd, args) {
6722
7766
  execFile4("git", ["-c", "core.quotepath=false", ...args], { cwd, timeout: TIMEOUT2, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (e, stdout, stderr) => {
6723
7767
  const code = e && typeof e.code === "number" ? e.code : e ? 1 : 0;
6724
7768
  const missing = e?.code === "ENOENT";
6725
- res({ code, out: String(stdout ?? ""), err: missing ? "\u627E\u4E0D\u5230 git\uFF08\u8BF7\u5148\u5B89\u88C5 git \u5E76\u52A0\u5165 PATH\uFF09" : String(stderr || (e?.message ?? "")) });
7769
+ res({ code, out: String(stdout ?? ""), err: missing ? tr("git.missing") : String(stderr || (e?.message ?? "")) });
6726
7770
  });
6727
7771
  });
6728
7772
  }
6729
7773
  function insidePath(projectPath, p) {
6730
7774
  const rel = normalize2(p).replace(/\\/g, "/");
6731
- const abs = normalize2(join14(projectPath, rel));
7775
+ const abs = normalize2(join16(projectPath, rel));
6732
7776
  const proj = normalize2(projectPath);
6733
7777
  if (abs !== proj && !abs.startsWith(proj + sep2)) return null;
6734
7778
  return rel.replace(/\\/g, "/");
@@ -6774,16 +7818,16 @@ var GitService = class {
6774
7818
  // Unified diff text; {tooLarge:true} when the diff is > 1MB
6775
7819
  async diff(projectPath, p) {
6776
7820
  const rel = insidePath(projectPath, p);
6777
- if (!rel) return { error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
7821
+ if (!rel) return { error: tr("fs.pathDenied") };
6778
7822
  const ls = await runGit(projectPath, ["ls-files", "--error-unmatch", "--", rel]);
6779
7823
  if (ls.code !== 0) {
6780
- const full = join14(projectPath, rel);
6781
- if (!existsSync10(full)) return { error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
6782
- const s = await stat7(full);
6783
- if (!s.isFile()) return { error: "\u4E0D\u662F\u6587\u4EF6" };
7824
+ const full = join16(projectPath, rel);
7825
+ if (!existsSync10(full)) return { error: tr("fs.fileMissing") };
7826
+ const s = await stat9(full);
7827
+ if (!s.isFile()) return { error: tr("fs.notFile") };
6784
7828
  if (s.size > DIFF_LIMIT) return { tooLarge: true };
6785
- const text = await readFile14(full, "utf8").catch(() => null);
6786
- if (text === null) return { error: "\u4E8C\u8FDB\u5236\u6216\u65E0\u6CD5\u8BFB\u53D6" };
7829
+ const text = await readFile16(full, "utf8").catch(() => null);
7830
+ if (text === null) return { error: tr("git.binary") };
6787
7831
  const lines = text.split("\n").map((l) => "+" + l).join("\n");
6788
7832
  return { untracked: true, diff: `--- /dev/null
6789
7833
  +++ b/${rel}
@@ -6802,8 +7846,8 @@ ${lines}` };
6802
7846
  // Tracked → git restore; untracked → move to trash/discarded/<ts>/<relpath>
6803
7847
  async discard(projectPath, p, dataDir) {
6804
7848
  const rel = insidePath(projectPath, p);
6805
- if (!rel) return { ok: false, error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
6806
- if (rel === "." || rel === "" || rel === "./") return { ok: false, error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
7849
+ if (!rel) return { ok: false, error: tr("fs.pathDenied") };
7850
+ if (rel === "." || rel === "" || rel === "./") return { ok: false, error: tr("fs.pathDenied") };
6807
7851
  const ls = await runGit(projectPath, ["ls-files", "--error-unmatch", "--", rel]);
6808
7852
  if (ls.code === 0) {
6809
7853
  let r = await runGit(projectPath, ["restore", "--staged", "--worktree", "--", rel]);
@@ -6811,11 +7855,11 @@ ${lines}` };
6811
7855
  if (r.code !== 0) return { ok: false, error: r.err.trim() || "\u653E\u5F03\u5931\u8D25" };
6812
7856
  return { ok: true };
6813
7857
  }
6814
- const full = join14(projectPath, rel);
6815
- if (!existsSync10(full)) return { ok: false, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
7858
+ const full = join16(projectPath, rel);
7859
+ if (!existsSync10(full)) return { ok: false, error: tr("fs.fileMissing") };
6816
7860
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
6817
- const dest = join14(dataDir, "trash", "discarded", stamp2, rel);
6818
- await mkdir6(join14(dest, ".."), { recursive: true });
7861
+ const dest = join16(dataDir, "trash", "discarded", stamp2, rel);
7862
+ await mkdir7(join16(dest, ".."), { recursive: true });
6819
7863
  try {
6820
7864
  await moveAcrossDevices(full, dest);
6821
7865
  } catch (e) {
@@ -6826,8 +7870,9 @@ ${lines}` };
6826
7870
  };
6827
7871
 
6828
7872
  // daemon/src/files/service.ts
6829
- import { readdir as readdir5, readFile as readFile15, stat as stat8 } from "node:fs/promises";
6830
- import { join as join15, normalize as normalize3, sep as sep3, extname as extname3 } from "node:path";
7873
+ init_i18n();
7874
+ import { readdir as readdir5, readFile as readFile17, stat as stat10 } from "node:fs/promises";
7875
+ import { join as join17, normalize as normalize3, sep as sep3, extname as extname3 } from "node:path";
6831
7876
  import ignore from "ignore";
6832
7877
  function decodeText(buf) {
6833
7878
  try {
@@ -6890,7 +7935,7 @@ var LANG = {
6890
7935
  };
6891
7936
  function insidePath2(projectPath, p) {
6892
7937
  const rel = normalize3(p || ".").replace(/\\/g, "/");
6893
- const abs = normalize3(join15(projectPath, rel));
7938
+ const abs = normalize3(join17(projectPath, rel));
6894
7939
  const proj = normalize3(projectPath);
6895
7940
  if (abs !== proj && !abs.startsWith(proj + sep3)) return null;
6896
7941
  return rel;
@@ -6900,15 +7945,15 @@ var FileService = class {
6900
7945
  // Invalidate by .gitignore mtime: an edited .gitignore takes effect the next time a directory is expanded
6901
7946
  igCache = /* @__PURE__ */ new Map();
6902
7947
  async igFor(projectPath) {
6903
- const gi = join15(projectPath, ".gitignore");
6904
- const st = await stat8(gi).catch(() => null);
7948
+ const gi = join17(projectPath, ".gitignore");
7949
+ const st = await stat10(gi).catch(() => null);
6905
7950
  const mtime = st?.isFile() ? st.mtimeMs : -1;
6906
7951
  const hit = this.igCache.get(projectPath);
6907
7952
  if (hit && hit.mtime === mtime) return hit.ig;
6908
7953
  let ig = null;
6909
7954
  if (mtime >= 0) {
6910
7955
  try {
6911
- ig = ignore().add(await readFile15(gi, "utf8"));
7956
+ ig = ignore().add(await readFile17(gi, "utf8"));
6912
7957
  } catch {
6913
7958
  ig = null;
6914
7959
  }
@@ -6918,13 +7963,13 @@ var FileService = class {
6918
7963
  }
6919
7964
  async tree(projectPath, dir) {
6920
7965
  const rel = insidePath2(projectPath, dir);
6921
- if (rel === null) return { error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
6922
- const abs = join15(projectPath, rel);
6923
- const s = await stat8(abs).catch(() => null);
6924
- if (!s?.isDirectory()) return { error: "\u76EE\u5F55\u4E0D\u5B58\u5728" };
7966
+ if (rel === null) return { error: tr("fs.pathDenied") };
7967
+ const abs = join17(projectPath, rel);
7968
+ const s = await stat10(abs).catch(() => null);
7969
+ if (!s?.isDirectory()) return { error: tr("fs.dirMissing") };
6925
7970
  const ig = await this.igFor(projectPath);
6926
7971
  const dirents = await readdir5(abs, { withFileTypes: true }).catch(() => null);
6927
- if (!dirents) return { error: "\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55" };
7972
+ if (!dirents) return { error: tr("fs.dirUnreadable") };
6928
7973
  const out = [];
6929
7974
  for (const d of dirents) {
6930
7975
  if (out.length >= MAX_PER_LEVEL) break;
@@ -6933,7 +7978,7 @@ var FileService = class {
6933
7978
  const childRel = rel === "." ? name : `${rel}/${name}`;
6934
7979
  let isDir2 = d.isDirectory();
6935
7980
  if (d.isSymbolicLink()) {
6936
- const st = await stat8(join15(projectPath, childRel)).catch(() => null);
7981
+ const st = await stat10(join17(projectPath, childRel)).catch(() => null);
6937
7982
  if (!st) continue;
6938
7983
  isDir2 = st.isDirectory();
6939
7984
  } else if (!d.isFile() && !isDir2) {
@@ -6949,14 +7994,14 @@ var FileService = class {
6949
7994
  }
6950
7995
  async read(projectPath, p) {
6951
7996
  const rel = insidePath2(projectPath, p);
6952
- if (rel === null) return { error: "\u8DEF\u5F84\u4E0D\u5141\u8BB8" };
6953
- const abs = join15(projectPath, rel);
6954
- const s = await stat8(abs).catch(() => null);
6955
- if (!s) return { error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
6956
- if (!s.isFile()) return { error: "\u4E0D\u662F\u6587\u4EF6" };
7997
+ if (rel === null) return { error: tr("fs.pathDenied") };
7998
+ const abs = join17(projectPath, rel);
7999
+ const s = await stat10(abs).catch(() => null);
8000
+ if (!s) return { error: tr("fs.fileMissing") };
8001
+ if (!s.isFile()) return { error: tr("fs.notFile") };
6957
8002
  if (s.size > MAX_READ) return { tooLarge: true };
6958
- const buf = await readFile15(abs).catch(() => null);
6959
- if (!buf) return { error: "\u65E0\u6CD5\u8BFB\u53D6" };
8003
+ const buf = await readFile17(abs).catch(() => null);
8004
+ if (!buf) return { error: tr("fs.unreadable") };
6960
8005
  const head = buf.subarray(0, 8192);
6961
8006
  if (head.includes(0)) return { binary: true };
6962
8007
  const { text, encoding } = decodeText(buf);
@@ -6967,11 +8012,12 @@ var FileService = class {
6967
8012
  // daemon/src/terminal/service.ts
6968
8013
  init_pathscan();
6969
8014
  init_log();
8015
+ init_i18n();
6970
8016
  import { spawn as spawn3, execFileSync as execFileSync2 } from "node:child_process";
6971
8017
  import { accessSync as accessSync2, chmodSync, constants as fsc, existsSync as existsSync11, statSync as statSync6 } from "node:fs";
6972
8018
  import { createRequire as moduleRequire } from "node:module";
6973
8019
  import { release } from "node:os";
6974
- import { dirname as dirname6, join as join16 } from "node:path";
8020
+ import { dirname as dirname6, join as join18 } from "node:path";
6975
8021
  import { StringDecoder } from "node:string_decoder";
6976
8022
  var MAX_TERMS_PER_PROJECT = 8;
6977
8023
  var BUF_MAX = 2 * 1024 * 1024;
@@ -7017,7 +8063,7 @@ function fixSpawnHelper() {
7017
8063
  const req = moduleRequire(import.meta.url);
7018
8064
  const root = dirname6(req.resolve("node-pty/package.json"));
7019
8065
  for (const sub of [`prebuilds/darwin-${process.arch}`, "build/Release"]) {
7020
- const f = join16(root, sub, "spawn-helper");
8066
+ const f = join18(root, sub, "spawn-helper");
7021
8067
  if (existsSync11(f) && !isExecFile(f)) {
7022
8068
  chmodSync(f, 493);
7023
8069
  log("info", "\u5DF2\u4FEE\u590D node-pty spawn-helper \u7684\u53EF\u6267\u884C\u4F4D", { file: f });
@@ -7060,14 +8106,14 @@ var TerminalService = class {
7060
8106
  }
7061
8107
  async create(projectId, cwd, cols, rows) {
7062
8108
  const alive = [...this.terms.values()].filter((t) => t.projectId === projectId && t.alive);
7063
- if (alive.length >= MAX_TERMS_PER_PROJECT) return { error: `\u6BCF\u4E2A\u9879\u76EE\u6700\u591A ${MAX_TERMS_PER_PROJECT} \u4E2A\u7EC8\u7AEF` };
8109
+ if (alive.length >= MAX_TERMS_PER_PROJECT) return { error: tr("term.max", { n: MAX_TERMS_PER_PROJECT }) };
7064
8110
  const shell = pickShell();
7065
8111
  const term = {
7066
8112
  id: rid5(),
7067
8113
  index: nextIndex++,
7068
8114
  projectId,
7069
8115
  cwd,
7070
- title: `\u7EC8\u7AEF ${alive.length + 1}`,
8116
+ title: tr("term.title", { n: alive.length + 1 }),
7071
8117
  alive: true,
7072
8118
  simple: false,
7073
8119
  pty: null,
@@ -7115,25 +8161,25 @@ var TerminalService = class {
7115
8161
  this.byIndex.set(term.index, term);
7116
8162
  return { id: term.id, index: term.index, simple: term.simple };
7117
8163
  }
7118
- attach(id) {
7119
- const t = this.terms.get(id);
7120
- if (!t) return { error: "\u7EC8\u7AEF\u4E0D\u5B58\u5728" };
8164
+ attach(id2) {
8165
+ const t = this.terms.get(id2);
8166
+ if (!t) return { error: tr("err.terminalMissing") };
7121
8167
  const lines = t.buf.split("\n");
7122
8168
  const snapshot = lines.length > SNAP_LINES ? lines.slice(-SNAP_LINES).join("\n") : t.buf;
7123
8169
  const windowsPty = process.platform === "win32" && t.pty ? conptyInfo() : void 0;
7124
8170
  return { index: t.index, snapshot, simple: t.simple, ...windowsPty ? { windowsPty } : {} };
7125
8171
  }
7126
- indexOf(id) {
7127
- return this.terms.get(id)?.index ?? null;
8172
+ indexOf(id2) {
8173
+ return this.terms.get(id2)?.index ?? null;
7128
8174
  }
7129
- resize(id, cols, rows) {
7130
- const t = this.terms.get(id);
8175
+ resize(id2, cols, rows) {
8176
+ const t = this.terms.get(id2);
7131
8177
  if (!t || !t.alive) return { ok: false };
7132
8178
  t.pty?.resize(Math.max(20, cols), Math.max(5, rows));
7133
8179
  return { ok: true };
7134
8180
  }
7135
- kill(id) {
7136
- const t = this.terms.get(id);
8181
+ kill(id2) {
8182
+ const t = this.terms.get(id2);
7137
8183
  if (!t) return { ok: false };
7138
8184
  this.killTerm(t);
7139
8185
  return { ok: true };
@@ -7273,6 +8319,7 @@ if (nodeMajor < 20) {
7273
8319
  process.env.PATH = augmentPath(process.env, dirname7(process.execPath));
7274
8320
  var cfg = parseConfig(process.argv.slice(2));
7275
8321
  process.on("uncaughtException", (e) => {
8322
+ if (e?.code === "EPIPE") return;
7276
8323
  log("error", "uncaughtException", { err: String(e?.stack ?? e) });
7277
8324
  });
7278
8325
  process.on("unhandledRejection", (e) => {
@@ -7281,10 +8328,10 @@ process.on("unhandledRejection", (e) => {
7281
8328
  async function main() {
7282
8329
  initLog(cfg.dataDir);
7283
8330
  for (const d of ["threads", "attachments", "trash", "logs"]) {
7284
- mkdirSync2(join18(cfg.dataDir, d), { recursive: true });
8331
+ mkdirSync2(join20(cfg.dataDir, d), { recursive: true });
7285
8332
  }
7286
- const appDir = join18(dirname7(fileURLToPath3(import.meta.url)), "..", "..", "app");
7287
- const attachments = new AttachmentStore(join18(cfg.dataDir, "attachments"));
8333
+ const appDir = join20(dirname7(fileURLToPath3(import.meta.url)), "..", "..", "app");
8334
+ const attachments = new AttachmentStore(join20(cfg.dataDir, "attachments"));
7288
8335
  const { bindAttachmentStore: bindAttachmentStore2 } = await Promise.resolve().then(() => (init_attachmentRefs(), attachmentRefs_exports));
7289
8336
  bindAttachmentStore2(attachments);
7290
8337
  const settings = new SettingsStore(cfg.dataDir);
@@ -7311,7 +8358,7 @@ async function main() {
7311
8358
  git: new GitService(),
7312
8359
  files: new FileService(),
7313
8360
  terminals,
7314
- runtime: (id) => manager.runtimeInfo(id)
8361
+ runtime: (id2) => manager.runtimeInfo(id2)
7315
8362
  });
7316
8363
  const server = createHttpServer(cfg, appDir, attachments, {
7317
8364
  onUpgrade: (req, socket, head) => gateway.handleUpgrade(req, socket, head)
@@ -7343,7 +8390,7 @@ async function main() {
7343
8390
  const { warmProviderCache: warmProviderCache2 } = await Promise.resolve().then(() => (init_warm(), warm_exports));
7344
8391
  await warmProviderCache2(await settings.get());
7345
8392
  })().catch((e) => log("warn", "Provider \u9884\u70ED\u5931\u8D25", { err: String(e) }));
7346
- const daemonJsonPath = join18(cfg.dataDir, "daemon.json");
8393
+ const daemonJsonPath = join20(cfg.dataDir, "daemon.json");
7347
8394
  await writeJsonAtomic(daemonJsonPath, {
7348
8395
  pid: process.pid,
7349
8396
  port: cfg.port,
@@ -7354,7 +8401,9 @@ async function main() {
7354
8401
  });
7355
8402
  chmodSync2(daemonJsonPath, 384);
7356
8403
  log("info", `daemon \u5DF2\u542F\u52A8 \u7AEF\u53E3 ${cfg.port} \u6570\u636E\u76EE\u5F55 ${cfg.dataDir} \u7248\u672C ${cfg.version} pid ${process.pid}`);
7357
- if (!cfg.parentPid) console.log(`[agent-console] \u6253\u5F00 http://127.0.0.1:${cfg.port}/#t=${cfg.token}`);
8404
+ if (!cfg.parentPid) {
8405
+ console.log(process.stdout.isTTY ? `[agent-console] open http://127.0.0.1:${cfg.port}/#t=${cfg.token}` : `[agent-console] listening on http://127.0.0.1:${cfg.port}/ (token not printed: stdout is not a terminal)`);
8406
+ }
7358
8407
  let idleSince = Date.now();
7359
8408
  setInterval(() => {
7360
8409
  const busy = gateway.connectionCount > 0 || manager.anyBusy() || terminals.count > 0;
@@ -7391,9 +8440,16 @@ async function main() {
7391
8440
  }
7392
8441
  }, 2e3);
7393
8442
  parentWatch.unref();
7394
- process.stdin.on("end", () => shutdown(0));
7395
- process.stdin.on("close", () => shutdown(0));
7396
- process.stdin.on("error", () => shutdown(0));
8443
+ let lifelineGone = false;
8444
+ const lifelineClosed = () => {
8445
+ if (lifelineGone) return;
8446
+ lifelineGone = true;
8447
+ log("info", "plugin host closed the lifeline (stdin); daemon exiting with it");
8448
+ shutdown(0);
8449
+ };
8450
+ process.stdin.on("end", lifelineClosed);
8451
+ process.stdin.on("close", lifelineClosed);
8452
+ process.stdin.on("error", lifelineClosed);
7397
8453
  process.stdin.resume();
7398
8454
  }
7399
8455
  }