@co0ontty/wand 4.63.0 → 4.64.0-beta.g3402a64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/build-info.d.ts +18 -0
  2. package/dist/build-info.js +13 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/config.js +6 -11
  5. package/dist/env-utils.d.ts +5 -0
  6. package/dist/env-utils.js +4 -0
  7. package/dist/git-quick-commit.js +7 -11
  8. package/dist/ios-ota.js +8 -29
  9. package/dist/layout-tree.d.ts +6 -0
  10. package/dist/layout-tree.js +9 -0
  11. package/dist/missions.js +2 -2
  12. package/dist/models.js +2 -5
  13. package/dist/path-repair.js +2 -1
  14. package/dist/process-manager.js +10 -20
  15. package/dist/provider-cli-updater.js +3 -6
  16. package/dist/pty-shell-launch.js +3 -5
  17. package/dist/server-app-connect.js +11 -46
  18. package/dist/server-github-routes.js +12 -11
  19. package/dist/server-local-preview-routes.js +6 -1
  20. package/dist/server-request.d.ts +4 -2
  21. package/dist/server-request.js +25 -4
  22. package/dist/server-session-routes.js +14 -78
  23. package/dist/server-settings-routes.d.ts +2 -6
  24. package/dist/server-settings-routes.js +3 -8
  25. package/dist/server-task-routes.d.ts +9 -6
  26. package/dist/server-task-routes.js +50 -43
  27. package/dist/server-update-routes.d.ts +2 -5
  28. package/dist/server-update-routes.js +42 -9
  29. package/dist/server-workspace-routes.js +4 -12
  30. package/dist/server.js +33 -99
  31. package/dist/session-ai-context.js +6 -38
  32. package/dist/session-provider.d.ts +16 -0
  33. package/dist/session-provider.js +49 -0
  34. package/dist/session-registry.d.ts +10 -0
  35. package/dist/session-registry.js +41 -14
  36. package/dist/session-topic.js +3 -11
  37. package/dist/shell-quote.d.ts +9 -0
  38. package/dist/shell-quote.js +13 -0
  39. package/dist/signal-utils.d.ts +10 -0
  40. package/dist/signal-utils.js +38 -0
  41. package/dist/storage.js +17 -55
  42. package/dist/structured-client-protocol.js +4 -8
  43. package/dist/structured-codex-protocol.js +1 -6
  44. package/dist/structured-content.d.ts +2 -0
  45. package/dist/structured-content.js +6 -0
  46. package/dist/structured-exec-host.d.ts +0 -2
  47. package/dist/structured-exec-host.js +2 -11
  48. package/dist/structured-exec-pump.js +2 -12
  49. package/dist/structured-grok-adapter.js +1 -5
  50. package/dist/structured-opencode-adapter.js +1 -5
  51. package/dist/structured-pi-adapter.js +9 -11
  52. package/dist/structured-provider-common.d.ts +11 -3
  53. package/dist/structured-provider-common.js +16 -23
  54. package/dist/structured-qoder-adapter.js +3 -8
  55. package/dist/structured-session-manager.js +6 -17
  56. package/dist/task-title.js +4 -12
  57. package/dist/task-types.d.ts +23 -1
  58. package/dist/task-types.js +26 -0
  59. package/dist/terminal-daemon-protocol.d.ts +0 -6
  60. package/dist/terminal-daemon-server.js +1 -9
  61. package/dist/text-utils.d.ts +7 -0
  62. package/dist/text-utils.js +15 -0
  63. package/dist/tui/attach.js +17 -93
  64. package/dist/tui/commands.d.ts +2 -0
  65. package/dist/tui/commands.js +3 -2
  66. package/dist/tui/index.js +28 -115
  67. package/dist/tui/runtime-utils.d.ts +7 -0
  68. package/dist/tui/runtime-utils.js +33 -0
  69. package/dist/tui/service-actions.d.ts +31 -0
  70. package/dist/tui/service-actions.js +86 -0
  71. package/dist/tui/snapshot.js +1 -8
  72. package/dist/update-helper.js +6 -35
  73. package/dist/web-ui/content/scripts.js +397 -497
  74. package/dist/web-ui/content/styles.css +1 -1
  75. package/dist/web-ui/content/tailwind.css +7210 -0
  76. package/dist/web-ui/embedded-assets.js +2 -2
  77. package/dist/web-ui/index.js +3 -3
  78. package/dist/web-ui/provider-identity.d.ts +4 -0
  79. package/dist/web-ui/provider-identity.js +9 -0
  80. package/dist/web-ui/styles.js +24 -7
  81. package/dist/workspace-binding.d.ts +2 -0
  82. package/dist/workspace-binding.js +4 -3
  83. package/package.json +9 -12
package/dist/storage.js CHANGED
@@ -4,6 +4,10 @@ import path from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { GLOBAL_WORKSPACE_ID } from "./types.js";
6
6
  import { normalizeSessionDirectory } from "./session-directory-tree.js";
7
+ import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider } from "./session-provider.js";
8
+ import { normalizeWandTaskAgentMode } from "./task-types.js";
9
+ import { firstLayoutTabId } from "./layout-tree.js";
10
+ import { isThinkingEffort } from "./structured-provider-common.js";
7
11
  import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
8
12
  function safeJsonParse(raw) {
9
13
  if (!raw)
@@ -26,13 +30,15 @@ export function parseWandTaskAgent(raw) {
26
30
  const provider = value.provider;
27
31
  const model = value.model;
28
32
  const thinkingEffort = value.thinkingEffort;
29
- if (provider !== "claude" && provider !== "codex" && provider !== "opencode" && provider !== "grok" && provider !== "qoder" && provider !== "pi")
33
+ if (!isSessionProvider(provider))
30
34
  return null;
31
35
  if (typeof model !== "string" || !model.trim() || model.trim().length > 128)
32
36
  return null;
33
37
  if (thinkingEffort !== "off" && thinkingEffort !== "standard" && thinkingEffort !== "deep" && thinkingEffort !== "max")
34
38
  return null;
35
- return { provider, model: model.trim(), thinkingEffort };
39
+ // mode 是后加列:历史行没有该字段时按标准模式读取,不因此整条配置退化成 null。
40
+ const mode = normalizeWandTaskAgentMode(provider, value.mode);
41
+ return { provider, model: model.trim(), thinkingEffort, mode };
36
42
  }
37
43
  /** `wand_milestones` 行 → 领域对象;名字必须非空,脏行直接跳过。 */
38
44
  function mapWandMilestoneRow(row) {
@@ -120,13 +126,6 @@ function parseApprovalStats(value) {
120
126
  total: value.total,
121
127
  };
122
128
  }
123
- function isThinkingEffort(value) {
124
- return value === "off"
125
- || value === "standard"
126
- || value === "deep"
127
- || value === "max"
128
- || (typeof value === "string" && /^codex:[a-z0-9][a-z0-9_-]{0,31}$/.test(value));
129
- }
130
129
  function serializeSessionOptions(snapshot) {
131
130
  const options = {
132
131
  schemaVersion: SESSION_OPTIONS_SCHEMA_VERSION,
@@ -224,37 +223,6 @@ function parseQueuedMessageSkills(raw, queueLength) {
224
223
  : [];
225
224
  });
226
225
  }
227
- function inferSessionProvider(row) {
228
- if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode" || row.provider === "grok" || row.provider === "qoder" || row.provider === "pi") {
229
- return row.provider;
230
- }
231
- if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
232
- return "claude";
233
- }
234
- if (row.runner === "codex-cli-exec") {
235
- return "codex";
236
- }
237
- if (row.runner === "opencode-cli-run") {
238
- return "opencode";
239
- }
240
- if (row.runner === "grok-cli-headless")
241
- return "grok";
242
- if (row.runner === "qoder-cli-print")
243
- return "qoder";
244
- if (row.runner === "pi-cli-json")
245
- return "pi";
246
- if (/^codex\b/i.test(row.command.trim()))
247
- return "codex";
248
- if (/^opencode\b/i.test(row.command.trim()))
249
- return "opencode";
250
- if (/^grok\b/i.test(row.command.trim()))
251
- return "grok";
252
- if (/^qodercli\b/i.test(row.command.trim()))
253
- return "qoder";
254
- if (/^pi\b/i.test(row.command.trim()))
255
- return "pi";
256
- return /^claude\b/i.test(row.command.trim()) ? "claude" : undefined;
257
- }
258
226
  function parseWorktreeInfo(raw) {
259
227
  const parsed = safeJsonParse(raw);
260
228
  if (parsed && typeof parsed.branch === "string" && typeof parsed.path === "string") {
@@ -265,11 +233,8 @@ function parseWorktreeInfo(raw) {
265
233
  function parseWorktreeMergeInfo(raw) {
266
234
  return safeJsonParse(raw);
267
235
  }
268
- function serializeWorktreeMergeInfo(info) {
269
- return info ? JSON.stringify(info) : null;
270
- }
271
- function serializeWorktreeInfo(info) {
272
- return info ? JSON.stringify(info) : null;
236
+ function serializeJsonOrNull(value) {
237
+ return value ? JSON.stringify(value) : null;
273
238
  }
274
239
  function normalizeWorktreeMergeStatus(raw) {
275
240
  if (raw === "ready" || raw === "checking" || raw === "merging" || raw === "merged" || raw === "failed") {
@@ -311,11 +276,6 @@ function mapWorkspaceTaskWorktree(raw) {
311
276
  return null;
312
277
  return parsed;
313
278
  }
314
- function firstLayoutTabId(node) {
315
- if (node.type === "pane")
316
- return node.tabs[node.active]?.id ?? node.tabs[0]?.id;
317
- return firstLayoutTabId(node.children[0]) ?? firstLayoutTabId(node.children[1]);
318
- }
319
279
  /** 读取旧版单棵分屏树时就地包成一个工作窗口,避免升级后丢失布局。 */
320
280
  function mapWorkspaceTaskLayout(raw) {
321
281
  const parsed = safeJsonParse(raw);
@@ -427,9 +387,9 @@ function sessionPersistValues(snapshot) {
427
387
  snapshot.resumedFromSessionId ?? null,
428
388
  snapshot.autoRecovered ? 1 : 0,
429
389
  snapshot.worktreeEnabled ? 1 : 0,
430
- serializeWorktreeInfo(snapshot.worktree),
390
+ serializeJsonOrNull(snapshot.worktree),
431
391
  snapshot.worktreeMergeStatus ?? null,
432
- serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
392
+ serializeJsonOrNull(snapshot.worktreeMergeInfo),
433
393
  snapshot.title ?? null,
434
394
  snapshot.description ?? null,
435
395
  serializeSessionOptions(snapshot),
@@ -460,9 +420,9 @@ function sessionRuntimeMetadataValues(snapshot) {
460
420
  snapshot.resumedFromSessionId ?? null,
461
421
  snapshot.autoRecovered ? 1 : 0,
462
422
  snapshot.worktreeEnabled ? 1 : 0,
463
- serializeWorktreeInfo(snapshot.worktree),
423
+ serializeJsonOrNull(snapshot.worktree),
464
424
  snapshot.worktreeMergeStatus ?? null,
465
- serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
425
+ serializeJsonOrNull(snapshot.worktreeMergeInfo),
466
426
  snapshot.title ?? null,
467
427
  snapshot.description ?? null,
468
428
  serializeSessionOptions(snapshot),
@@ -470,7 +430,9 @@ function sessionRuntimeMetadataValues(snapshot) {
470
430
  ];
471
431
  }
472
432
  function mapSessionCore(row) {
473
- const provider = inferSessionProvider(row);
433
+ const provider = isSessionProvider(row.provider)
434
+ ? row.provider
435
+ : (inferProviderFromRunner(row.runner) ?? inferProviderFromCommand(row.command));
474
436
  const sessionOptions = parseSessionOptions(row.session_options);
475
437
  const queuedMessages = parseQueuedMessages(row.queued_messages);
476
438
  return {
@@ -1,9 +1,5 @@
1
+ import { asRecord } from "./structured-content.js";
1
2
  export const WAND_PROTOCOL_VERSION = 2;
2
- function record(value) {
3
- return value && typeof value === "object" && !Array.isArray(value)
4
- ? value
5
- : null;
6
- }
7
3
  function text(value) {
8
4
  return typeof value === "string" && value ? value : undefined;
9
5
  }
@@ -24,11 +20,11 @@ function questionsFromInput(input) {
24
20
  const rawQuestions = arrayValue(input.questions) ?? [];
25
21
  const questions = [];
26
22
  for (const rawQuestion of rawQuestions) {
27
- const question = record(rawQuestion);
23
+ const question = asRecord(rawQuestion);
28
24
  if (!question)
29
25
  continue;
30
26
  const options = (arrayValue(question.options) ?? []).flatMap((rawOption, index) => {
31
- const option = record(rawOption);
27
+ const option = asRecord(rawOption);
32
28
  if (!option)
33
29
  return [];
34
30
  return [{
@@ -72,7 +68,7 @@ function tasksFromSegment(messages, start, end) {
72
68
  if (latestTodoWrite) {
73
69
  const todos = arrayValue(latestTodoWrite.input.todos) ?? arrayValue(latestTodoWrite.input.plan) ?? [];
74
70
  const items = todos.flatMap((rawTodo, index) => {
75
- const todo = record(rawTodo);
71
+ const todo = asRecord(rawTodo);
76
72
  if (!todo)
77
73
  return [];
78
74
  return [{
@@ -2,12 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { getErrorMessage } from "./error-utils.js";
5
- import { normalizeStructuredToolResultContent } from "./structured-content.js";
6
- function asRecord(value) {
7
- return value && typeof value === "object" && !Array.isArray(value)
8
- ? value
9
- : null;
10
- }
5
+ import { asRecord, normalizeStructuredToolResultContent } from "./structured-content.js";
11
6
  function getString(value) {
12
7
  return typeof value === "string" ? value : "";
13
8
  }
@@ -1,3 +1,5 @@
1
+ /** 把 unknown 收敛成普通对象;数组 / null / 原始值都返回 null。 */
2
+ export declare function asRecord(value: unknown): Record<string, unknown> | null;
1
3
  /** Preserve both Responses content parts and arbitrary structured tool output. */
2
4
  export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
3
5
  type: string;
@@ -1,3 +1,9 @@
1
+ /** 把 unknown 收敛成普通对象;数组 / null / 原始值都返回 null。 */
2
+ export function asRecord(value) {
3
+ return value && typeof value === "object" && !Array.isArray(value)
4
+ ? value
5
+ : null;
6
+ }
1
7
  /** Preserve both Responses content parts and arbitrary structured tool output. */
2
8
  export function normalizeStructuredToolResultContent(content) {
3
9
  if (typeof content === "string")
@@ -83,5 +83,3 @@ export declare class InProcessStructuredExecHost implements StructuredExecHost {
83
83
  listRuns(): Promise<StructuredRunState[]>;
84
84
  forgetRun(runId: string): void;
85
85
  }
86
- /** Map a NodeJS.Signals name to the numeric value used by waitpid-style APIs. */
87
- export declare function osSignalNumber(signal: NodeJS.Signals): number;
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { StringDecoder } from "node:string_decoder";
4
+ import { signalNumberFromName } from "./signal-utils.js";
4
5
  /**
5
6
  * Ownership seam for structured CLI runs, mirroring TerminalHost for PTYs.
6
7
  * Persistent adapters (terminald) outlive web restarts; the in-process adapter
@@ -74,7 +75,7 @@ class InProcessStructuredExecProcess {
74
75
  this.child.on("close", (code, signal) => {
75
76
  settleExit({
76
77
  exitCode: code,
77
- signal: signal === null || signal === undefined ? null : osSignalNumber(signal),
78
+ signal: signal === null || signal === undefined ? null : signalNumberFromName(signal),
78
79
  });
79
80
  });
80
81
  this.child.on("error", () => settleExit({ exitCode: null, signal: null }));
@@ -140,13 +141,3 @@ export class InProcessStructuredExecHost {
140
141
  wrapped.interrupt();
141
142
  }
142
143
  }
143
- /** Map a NodeJS.Signals name to the numeric value used by waitpid-style APIs. */
144
- export function osSignalNumber(signal) {
145
- const table = {
146
- SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6,
147
- SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12,
148
- SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18,
149
- SIGSTOP: 19, SIGTSTP: 20,
150
- };
151
- return table[signal] ?? 0;
152
- }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createUtf8TextDecoder, } from "./structured-exec-host.js";
3
+ import { signalNameFromNumber } from "./signal-utils.js";
3
4
  export function startStructuredCli(options) {
4
5
  const spawnedAt = new Date().toISOString();
5
6
  const observer = options.observer;
@@ -117,17 +118,6 @@ function wireRemoteHandle(handle, onStdout, onStderr, finish) {
117
118
  onStderr(event.data);
118
119
  });
119
120
  handle.onExit((event) => {
120
- finish(event.exitCode, numericToSignalName(event.signal));
121
+ finish(event.exitCode, signalNameFromNumber(event.signal));
121
122
  });
122
123
  }
123
- function numericToSignalName(signal) {
124
- if (signal === null || signal === 0)
125
- return null;
126
- const names = {
127
- 1: "SIGHUP", 2: "SIGINT", 3: "SIGQUIT", 4: "SIGILL", 5: "SIGTRAP", 6: "SIGABRT",
128
- 7: "SIGBUS", 8: "SIGFPE", 9: "SIGKILL", 10: "SIGUSR1", 11: "SIGSEGV", 12: "SIGUSR2",
129
- 13: "SIGPIPE", 14: "SIGALRM", 15: "SIGTERM", 17: "SIGCHLD", 18: "SIGCONT",
130
- 19: "SIGSTOP", 20: "SIGTSTP",
131
- };
132
- return names[signal] ?? "SIGTERM";
133
- }
@@ -1,11 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { startStructuredCli } from "./structured-exec-pump.js";
3
+ import { asRecord } from "./structured-content.js";
3
4
  import { thinkingEffortToGrokEffort } from "./structured-provider-common.js";
4
- function asRecord(value) {
5
- return value && typeof value === "object" && !Array.isArray(value)
6
- ? value
7
- : null;
8
- }
9
5
  function grokText(value) {
10
6
  if (typeof value === "string")
11
7
  return value;
@@ -1,12 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { startStructuredCli } from "./structured-exec-pump.js";
4
+ import { asRecord } from "./structured-content.js";
4
5
  import { thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
5
- function asRecord(value) {
6
- return value && typeof value === "object" && !Array.isArray(value)
7
- ? value
8
- : null;
9
- }
10
6
  function extractStructuredText(value) {
11
7
  if (typeof value === "string")
12
8
  return value;
@@ -1,15 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { startStructuredCli } from "./structured-exec-pump.js";
3
+ import { asRecord } from "./structured-content.js";
3
4
  import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
4
- function record(value) {
5
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
6
- }
7
5
  function textContent(value) {
8
6
  if (typeof value === "string")
9
7
  return value;
10
8
  if (Array.isArray(value))
11
9
  return value.map((item) => textContent(item)).filter(Boolean).join("\n");
12
- const item = record(value);
10
+ const item = asRecord(value);
13
11
  if (!item)
14
12
  return "";
15
13
  if (item.type === "text" && typeof item.text === "string")
@@ -36,8 +34,8 @@ export function piToolName(name) {
36
34
  function applyPiAssistantMessage(state, message) {
37
35
  if (typeof message.model === "string")
38
36
  state.model = message.model;
39
- const usage = record(message.usage);
40
- const cost = record(usage?.cost);
37
+ const usage = asRecord(message.usage);
38
+ const cost = asRecord(usage?.cost);
41
39
  if (usage) {
42
40
  state.usage = {
43
41
  inputTokens: typeof usage.input === "number" ? usage.input : 0,
@@ -51,7 +49,7 @@ function applyPiAssistantMessage(state, message) {
51
49
  const thinkings = [];
52
50
  const parts = Array.isArray(message.content) ? message.content : [];
53
51
  for (const part of parts) {
54
- const item = record(part);
52
+ const item = asRecord(part);
55
53
  if (!item)
56
54
  continue;
57
55
  if (item.type === "text") {
@@ -93,7 +91,7 @@ export function applyPiEvent(state, event) {
93
91
  if (event.type === "session" && typeof event.id === "string")
94
92
  state.sessionId = event.id;
95
93
  if (event.type === "message_update") {
96
- const update = record(event.assistantMessageEvent);
94
+ const update = asRecord(event.assistantMessageEvent);
97
95
  const delta = typeof update?.delta === "string" ? update.delta : "";
98
96
  if (update?.type === "text_delta" && delta) {
99
97
  const last = state.blocks.at(-1);
@@ -130,20 +128,20 @@ export function applyPiEvent(state, event) {
130
128
  if (event.type === "tool_execution_start") {
131
129
  const id = typeof event.toolCallId === "string" ? event.toolCallId : crypto.randomUUID();
132
130
  const name = typeof event.toolName === "string" ? event.toolName : "tool";
133
- state.blocks.push({ type: "tool_use", id, name: piToolName(name), input: record(event.args) ?? {} });
131
+ state.blocks.push({ type: "tool_use", id, name: piToolName(name), input: asRecord(event.args) ?? {} });
134
132
  }
135
133
  if (event.type === "tool_execution_end") {
136
134
  const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
137
135
  state.blocks.push({ type: "tool_result", tool_use_id: id, content: textContent(event.result), is_error: event.isError === true });
138
136
  }
139
137
  if (event.type === "message_end" || event.type === "turn_end") {
140
- const message = record(event.message);
138
+ const message = asRecord(event.message);
141
139
  if (message?.role === "assistant")
142
140
  return applyPiAssistantMessage(state, message);
143
141
  }
144
142
  if (event.type === "agent_end" && Array.isArray(event.messages)) {
145
143
  for (const raw of event.messages) {
146
- const message = record(raw);
144
+ const message = asRecord(raw);
147
145
  if (message?.role === "assistant") {
148
146
  const error = applyPiAssistantMessage(state, message);
149
147
  if (error)
@@ -1,4 +1,6 @@
1
1
  import type { SessionProvider, SessionRunner, SessionSnapshot, StructuredSessionState, WandConfig } from "./types.js";
2
+ /** 判断任意值是否为合法的 thinking effort 字面量(含 `codex:<level>` 形式)。 */
3
+ export declare function isThinkingEffort(value: unknown): value is NonNullable<SessionSnapshot["thinkingEffort"]>;
2
4
  export declare function isStructuredRunnerForProvider(provider: SessionProvider, runner: unknown): runner is SessionRunner;
3
5
  export declare function defaultStructuredRunner(provider: SessionProvider, configuredClaudeRunner?: WandConfig["structuredRunner"]): SessionRunner;
4
6
  export declare function resolveStructuredRunner(provider: SessionProvider, requestedRunner: unknown, configuredClaudeRunner?: WandConfig["structuredRunner"]): SessionRunner;
@@ -8,7 +10,13 @@ export declare function thinkingEffortToSdkBudget(effort: SessionSnapshot["think
8
10
  export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot["thinkingEffort"]): "low" | "medium" | "max" | null;
9
11
  export declare function thinkingEffortToClaudeSlashEffort(effort: SessionSnapshot["thinkingEffort"]): string;
10
12
  export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
11
- export declare function thinkingEffortToOpenCodeVariant(effort: SessionSnapshot["thinkingEffort"]): string | null;
12
- export declare function thinkingEffortToGrokEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
13
- export declare function thinkingEffortToQoderEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
13
+ /**
14
+ * OpenCode / Grok / Qoder 三家共用同一套 effort 级别(off/standard/deep/max →
15
+ * null/low/high/max),`codex:<level>` 透传 level。
16
+ */
17
+ declare function thinkingEffortToLowHighMax(effort: SessionSnapshot["thinkingEffort"]): string | null;
18
+ export declare const thinkingEffortToOpenCodeVariant: typeof thinkingEffortToLowHighMax;
19
+ export declare const thinkingEffortToGrokEffort: typeof thinkingEffortToLowHighMax;
20
+ export declare const thinkingEffortToQoderEffort: typeof thinkingEffortToLowHighMax;
14
21
  export declare function thinkingEffortToPiLevel(effort: SessionSnapshot["thinkingEffort"]): string | null;
22
+ export {};
@@ -1,3 +1,11 @@
1
+ /** 判断任意值是否为合法的 thinking effort 字面量(含 `codex:<level>` 形式)。 */
2
+ export function isThinkingEffort(value) {
3
+ return value === "off"
4
+ || value === "standard"
5
+ || value === "deep"
6
+ || value === "max"
7
+ || (typeof value === "string" && /^codex:[a-z0-9][a-z0-9_-]{0,31}$/.test(value));
8
+ }
1
9
  export function isStructuredRunnerForProvider(provider, runner) {
2
10
  if (provider === "claude")
3
11
  return runner === "claude-sdk" || runner === "claude-cli-print";
@@ -76,29 +84,11 @@ export function thinkingEffortToCodexReasoningEffort(effort) {
76
84
  return "xhigh";
77
85
  return null;
78
86
  }
79
- export function thinkingEffortToOpenCodeVariant(effort) {
80
- if (!effort || effort === "off")
81
- return null;
82
- if (effort === "standard")
83
- return "low";
84
- if (effort === "deep")
85
- return "high";
86
- if (effort === "max")
87
- return "max";
88
- return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
89
- }
90
- export function thinkingEffortToGrokEffort(effort) {
91
- if (!effort || effort === "off")
92
- return null;
93
- if (effort === "standard")
94
- return "low";
95
- if (effort === "deep")
96
- return "high";
97
- if (effort === "max")
98
- return "max";
99
- return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
100
- }
101
- export function thinkingEffortToQoderEffort(effort) {
87
+ /**
88
+ * OpenCode / Grok / Qoder 三家共用同一套 effort 级别(off/standard/deep/max →
89
+ * null/low/high/max),`codex:<level>` 透传 level。
90
+ */
91
+ function thinkingEffortToLowHighMax(effort) {
102
92
  if (!effort || effort === "off")
103
93
  return null;
104
94
  if (effort === "standard")
@@ -109,6 +99,9 @@ export function thinkingEffortToQoderEffort(effort) {
109
99
  return "max";
110
100
  return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
111
101
  }
102
+ export const thinkingEffortToOpenCodeVariant = thinkingEffortToLowHighMax;
103
+ export const thinkingEffortToGrokEffort = thinkingEffortToLowHighMax;
104
+ export const thinkingEffortToQoderEffort = thinkingEffortToLowHighMax;
112
105
  export function thinkingEffortToPiLevel(effort) {
113
106
  if (!effort || effort === "off")
114
107
  return "off";
@@ -10,14 +10,9 @@ export function buildQoderArgs(session, prompt) {
10
10
  const effort = thinkingEffortToQoderEffort(session.thinkingEffort);
11
11
  if (effort)
12
12
  args.push("--reasoning-effort", effort);
13
- if (session.autoApprovePermissions === true
14
- || session.mode === "full-access"
15
- || session.mode === "managed") {
16
- args.push("--permission-mode", "bypass_permissions");
17
- }
18
- else if (session.mode === "auto-edit") {
19
- args.push("--permission-mode", "accept_edits");
20
- }
13
+ // 与 PTY runner 保持一致:Qoder 默认 yolo(bypass_permissions)启动。结构化 runner
14
+ // 没有权限桥,--print 下未批准的调用只会被拒绝,所以除显式 auto-edit 外一律 bypass。
15
+ args.push("--permission-mode", session.mode === "auto-edit" ? "accept_edits" : "bypass_permissions");
21
16
  if (session.claudeSessionId)
22
17
  args.push("-r", session.claudeSessionId);
23
18
  return args;
@@ -4,9 +4,11 @@ import { prepareSessionWorktree } from "./git-worktree.js";
4
4
  import { truncateMessagesForTransport } from "./message-truncator.js";
5
5
  import { buildChildEnv } from "./env-utils.js";
6
6
  import { getErrorMessage } from "./error-utils.js";
7
+ import { signalNameFromNumber } from "./signal-utils.js";
7
8
  import { resolveSdkClaudeBinary } from "./claude-sdk-runner.js";
8
9
  import { provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, } from "./session-topic.js";
9
10
  import { resolveSessionCwd } from "./session-cwd.js";
11
+ import { isSessionProvider } from "./session-provider.js";
10
12
  import { resolveSystemAiContext } from "./session-ai-context.js";
11
13
  import { CodexRunner } from "./structured-codex-adapter.js";
12
14
  import { CodexProtocolReducer } from "./structured-codex-protocol.js";
@@ -133,17 +135,6 @@ function recoveredCommandLabel(runner) {
133
135
  default: return "claude -p";
134
136
  }
135
137
  }
136
- function numericToSignal(signal) {
137
- if (signal === null || signal === 0)
138
- return null;
139
- const names = {
140
- 1: "SIGHUP", 2: "SIGINT", 3: "SIGQUIT", 4: "SIGILL", 5: "SIGTRAP", 6: "SIGABRT",
141
- 7: "SIGBUS", 8: "SIGFPE", 9: "SIGKILL", 10: "SIGUSR1", 11: "SIGSEGV", 12: "SIGUSR2",
142
- 13: "SIGPIPE", 14: "SIGALRM", 15: "SIGTERM", 17: "SIGCHLD", 18: "SIGCONT",
143
- 19: "SIGSTOP", 20: "SIGTSTP",
144
- };
145
- return names[signal] ?? "SIGTERM";
146
- }
147
138
  const STREAM_EMIT_DEBOUNCE_MS = 16;
148
139
  /** Min interval between full saveSession() calls for an in-progress streaming turn.
149
140
  * saveSession serializes the entire messages array, so doing it on every NDJSON
@@ -375,9 +366,7 @@ export class StructuredSessionManager {
375
366
  continue;
376
367
  const restoredStatus = snapshot.status === "running" ? "idle" : snapshot.status;
377
368
  const storedProvider = snapshot.provider ?? snapshot.structuredState?.provider;
378
- const provider = storedProvider === "codex" || storedProvider === "opencode" || storedProvider === "grok" || storedProvider === "qoder" || storedProvider === "pi"
379
- ? storedProvider
380
- : "claude";
369
+ const provider = isSessionProvider(storedProvider) ? storedProvider : "claude";
381
370
  const storedRunner = snapshot.runner ?? snapshot.structuredState?.runner;
382
371
  // Legacy/corrupt snapshots are normalized on restore so send dispatch can
383
372
  // rely on the provider/runner invariant without making startup fail.
@@ -666,7 +655,7 @@ export class StructuredSessionManager {
666
655
  feedFullLog();
667
656
  this.finalizeRecoveredRun(sessionId, requestId, processor, {
668
657
  exitCode: initialState.exitCode,
669
- signal: initialState.signal === null ? null : numericToSignal(initialState.signal),
658
+ signal: initialState.signal === null ? null : signalNameFromNumber(initialState.signal),
670
659
  stderr: initialState.stderrLog,
671
660
  stdoutTruncated: initialState.stdoutTruncated,
672
661
  });
@@ -707,7 +696,7 @@ export class StructuredSessionManager {
707
696
  carry = "";
708
697
  this.finalizeRecoveredRun(sessionId, requestId, processor, {
709
698
  exitCode: event.exitCode,
710
- signal: event.signal === null ? null : numericToSignal(event.signal),
699
+ signal: event.signal === null ? null : signalNameFromNumber(event.signal),
711
700
  stderr: processor.stderr,
712
701
  stdoutTruncated: initialState.stdoutTruncated,
713
702
  });
@@ -947,7 +936,7 @@ export class StructuredSessionManager {
947
936
  const id = randomUUID();
948
937
  const startedAt = new Date().toISOString();
949
938
  const requestedProvider = options.provider ?? "claude";
950
- if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode" && requestedProvider !== "grok" && requestedProvider !== "qoder" && requestedProvider !== "pi") {
939
+ if (!isSessionProvider(requestedProvider)) {
951
940
  throw new Error(`不支持的结构化 provider: ${String(requestedProvider)}`);
952
941
  }
953
942
  const provider = requestedProvider;
@@ -1,16 +1,8 @@
1
1
  import { callConfiguredAiText } from "./git-quick-commit.js";
2
+ import { clipAtWordBoundary } from "./text-utils.js";
2
3
  /** 任务标题留空时由描述自动生成;面板上显示得下,也不至于截断成半句话。 */
3
4
  export const TASK_TITLE_MAX_LENGTH = 40;
4
5
  const DESCRIPTION_HINT_MAX = 2_000;
5
- function clip(value, maxLength = TASK_TITLE_MAX_LENGTH) {
6
- if (value.length <= maxLength)
7
- return value;
8
- const sliced = value.slice(0, maxLength);
9
- const lastSpace = sliced.lastIndexOf(" ");
10
- if (lastSpace >= Math.floor(maxLength * 0.55))
11
- return sliced.slice(0, lastSpace);
12
- return sliced;
13
- }
14
6
  function firstMeaningfulLine(description) {
15
7
  const lines = description
16
8
  .split(/\r?\n/)
@@ -28,11 +20,11 @@ function firstMeaningfulLine(description) {
28
20
  * 后台自动生成成功后再覆盖它;失败或超时就保留这个兜底。
29
21
  */
30
22
  export function provisionalTaskTitleFromDescription(description) {
31
- return clip(firstMeaningfulLine(description));
23
+ return clipAtWordBoundary(firstMeaningfulLine(description), TASK_TITLE_MAX_LENGTH);
32
24
  }
33
25
  /** 模型输出可能带引号、代码块或「标题:」前缀,逐层剥掉再按单行裁剪。 */
34
26
  export function parseGeneratedTaskTitle(raw) {
35
- return clip(stripGeneratedTitle(raw));
27
+ return clipAtWordBoundary(stripGeneratedTitle(raw), TASK_TITLE_MAX_LENGTH);
36
28
  }
37
29
  function stripGeneratedTitle(raw) {
38
30
  const withoutFences = raw.trim().replace(/^```(?:json|text)?\s*/i, "").replace(/\s*```$/i, "").trim();
@@ -92,5 +84,5 @@ export async function generateWandTaskTitle(description, cwd, language = "", ai
92
84
  // 先判可信度再裁剪:截断过的长句看起来就像标题,会绕过长度检查。
93
85
  if (!isPlausibleTaskTitle(stripped))
94
86
  throw new Error("模型返回的任务标题不可用。");
95
- return clip(stripped);
87
+ return clipAtWordBoundary(stripped, TASK_TITLE_MAX_LENGTH);
96
88
  }
@@ -6,12 +6,34 @@ export type WandTaskPriority = "none" | "low" | "medium" | "high" | "urgent";
6
6
  export type WandTaskAgentProvider = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi";
7
7
  export type WandTaskAgentModel = string;
8
8
  export type WandTaskAgentEffort = "off" | "standard" | "deep" | "max";
9
- /** 任务上的默认派发配置:先选工具 / 模型 / 思考深度,再一键交给 Agent。 */
9
+ /**
10
+ * 派发时的执行模式,只暴露三种:
11
+ * - managed:托管,自动确认全部权限并使用完全自主提示词;
12
+ * - full-access:全限,自动确认权限但仍保留交互语义;
13
+ * - default:标准,逐步确认操作。
14
+ */
15
+ export type WandTaskAgentMode = "managed" | "full-access" | "default";
16
+ /** 合法执行模式清单;顺序即下拉顺序。 */
17
+ export declare const WAND_TASK_AGENT_MODES: readonly WandTaskAgentMode[];
18
+ /** 历史行没有 mode 字段时的兜底:标准模式,与旧 `mode: "agent"` 的权限姿态一致。 */
19
+ export declare const DEFAULT_WAND_TASK_AGENT_MODE: WandTaskAgentMode;
20
+ export declare function isWandTaskAgentMode(value: unknown): value is WandTaskAgentMode;
21
+ /**
22
+ * 各 provider 实际支持的工作模式。Codex 只有 full-access 一个有效值
23
+ * (与新建会话的 `supportedModes` / `/api/sessions/:id/mode` 一致);
24
+ * 否则 `default` 会让 codex 以 `--sandbox read-only` 跑,派发的任务根本改不了代码。
25
+ */
26
+ export declare function supportedWandTaskAgentModes(provider: WandTaskAgentProvider): readonly WandTaskAgentMode[];
27
+ /** 把任意(含旧数据 / 其它客户端缺省的)模式夹到该 provider 真正支持的值。 */
28
+ export declare function normalizeWandTaskAgentMode(provider: WandTaskAgentProvider, mode: unknown): WandTaskAgentMode;
29
+ /** 任务上的默认派发配置:先选工具 / 模型 / 思考深度 / 工作模式,再一键交给 Agent。 */
10
30
  export interface WandTaskAgent {
11
31
  provider: WandTaskAgentProvider;
12
32
  /** 具体模型 ID;"default" 表示跟随服务端为该 provider 选择的默认模型。 */
13
33
  model: WandTaskAgentModel;
14
34
  thinkingEffort: WandTaskAgentEffort;
35
+ /** 执行模式;缺省时按标准模式处理。 */
36
+ mode: WandTaskAgentMode;
15
37
  }
16
38
  /** 任务标题来源;标题是可选字段,留空时由服务端按描述自动生成。 */
17
39
  export type WandTaskTitleSource = "user" | "auto";
@@ -2,5 +2,31 @@
2
2
  export function isClosedWandTaskStatus(status) {
3
3
  return status === "done" || status === "archived";
4
4
  }
5
+ /** 合法执行模式清单;顺序即下拉顺序。 */
6
+ export const WAND_TASK_AGENT_MODES = ["managed", "full-access", "default"];
7
+ /** 历史行没有 mode 字段时的兜底:标准模式,与旧 `mode: "agent"` 的权限姿态一致。 */
8
+ export const DEFAULT_WAND_TASK_AGENT_MODE = "default";
9
+ export function isWandTaskAgentMode(value) {
10
+ return value === "managed" || value === "full-access" || value === "default";
11
+ }
12
+ /**
13
+ * 各 provider 实际支持的工作模式。Codex 只有 full-access 一个有效值
14
+ * (与新建会话的 `supportedModes` / `/api/sessions/:id/mode` 一致);
15
+ * 否则 `default` 会让 codex 以 `--sandbox read-only` 跑,派发的任务根本改不了代码。
16
+ */
17
+ export function supportedWandTaskAgentModes(provider) {
18
+ if (provider === "codex")
19
+ return ["full-access"];
20
+ return WAND_TASK_AGENT_MODES;
21
+ }
22
+ /** 把任意(含旧数据 / 其它客户端缺省的)模式夹到该 provider 真正支持的值。 */
23
+ export function normalizeWandTaskAgentMode(provider, mode) {
24
+ const supported = supportedWandTaskAgentModes(provider);
25
+ if (isWandTaskAgentMode(mode) && supported.includes(mode))
26
+ return mode;
27
+ return supported.includes(DEFAULT_WAND_TASK_AGENT_MODE)
28
+ ? DEFAULT_WAND_TASK_AGENT_MODE
29
+ : supported[0];
30
+ }
5
31
  /** 里程碑名上限;所有任务面板的新增入口共用同一约束。 */
6
32
  export const WAND_MILESTONE_NAME_MAX_LENGTH = 60;