@co0ontty/wand 4.53.0 → 4.54.0-beta.gcb99d89

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.
@@ -6,6 +6,113 @@ function asRecord(value) {
6
6
  ? value
7
7
  : null;
8
8
  }
9
+ function grokText(value) {
10
+ if (typeof value === "string")
11
+ return value;
12
+ if (Array.isArray(value))
13
+ return value.map(grokText).filter(Boolean).join("\n");
14
+ const record = asRecord(value);
15
+ if (!record)
16
+ return "";
17
+ if (typeof record.text === "string" && record.text)
18
+ return record.text;
19
+ if (record.content !== undefined)
20
+ return grokText(record.content);
21
+ if (record.stdout !== undefined || record.stderr !== undefined) {
22
+ return [grokText(record.stdout), grokText(record.stderr)].filter(Boolean).join("\n");
23
+ }
24
+ try {
25
+ return JSON.stringify(record);
26
+ }
27
+ catch {
28
+ return "";
29
+ }
30
+ }
31
+ function grokToolName(event) {
32
+ const raw = [
33
+ typeof event.toolName === "string" ? event.toolName : "",
34
+ typeof event.kind === "string" ? event.kind : "",
35
+ typeof event.title === "string" ? event.title : "",
36
+ ].find((value) => value.trim());
37
+ if (!raw)
38
+ return null;
39
+ const mapped = {
40
+ run_terminal_command: "Bash",
41
+ bash: "Bash",
42
+ shell: "Bash",
43
+ read_file: "Read",
44
+ read: "Read",
45
+ search_replace: "Edit",
46
+ edit: "Edit",
47
+ write: "Write",
48
+ list_dir: "Glob",
49
+ glob: "Glob",
50
+ grep: "Grep",
51
+ web_search: "WebSearch",
52
+ webfetch: "WebFetch",
53
+ todo_write: "TodoWrite",
54
+ };
55
+ return mapped[raw.toLowerCase()] ?? raw;
56
+ }
57
+ function grokToolInput(event) {
58
+ return asRecord(event.rawInput) ?? asRecord(event.input) ?? {};
59
+ }
60
+ function grokToolOutput(event) {
61
+ const fromRaw = grokText(event.rawOutput);
62
+ if (fromRaw)
63
+ return fromRaw;
64
+ return grokText(event.content);
65
+ }
66
+ function upsertGrokTool(state, event, completed) {
67
+ const id = typeof event.toolCallId === "string" && event.toolCallId
68
+ ? event.toolCallId
69
+ : typeof event.id === "string" && event.id ? event.id : "tool";
70
+ const input = grokToolInput(event);
71
+ const name = grokToolName(event);
72
+ const title = typeof event.title === "string" ? event.title : undefined;
73
+ const existingUse = state.blocks.find((block) => block.type === "tool_use" && block.id === id);
74
+ if (existingUse?.type === "tool_use") {
75
+ if (name)
76
+ existingUse.name = name;
77
+ existingUse.description = title ?? existingUse.description;
78
+ existingUse.input = Object.keys(input).length ? input : existingUse.input;
79
+ }
80
+ else {
81
+ state.blocks.push({ type: "tool_use", id, name: name ?? "tool", description: title, input });
82
+ }
83
+ if (!completed)
84
+ return;
85
+ const content = grokToolOutput(event);
86
+ const failed = event.status === "failed" || event.status === "error";
87
+ const existingResult = state.blocks.find((block) => block.type === "tool_result" && block.tool_use_id === id);
88
+ if (existingResult?.type === "tool_result") {
89
+ existingResult.content = content || existingResult.content;
90
+ existingResult.is_error = failed;
91
+ return;
92
+ }
93
+ state.blocks.push({ type: "tool_result", tool_use_id: id, content, is_error: failed });
94
+ }
95
+ /** Headless `--output-format streaming-json` is type-tagged; ACP stdio uses session/update. */
96
+ function normalizeGrokEvent(event) {
97
+ if (typeof event.type === "string" && event.type)
98
+ return event;
99
+ if (event.method !== "session/update")
100
+ return event;
101
+ const params = asRecord(event.params);
102
+ const update = asRecord(params?.update) ?? asRecord(event.update);
103
+ if (!update)
104
+ return event;
105
+ const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "";
106
+ const content = asRecord(update.content);
107
+ const text = typeof content?.text === "string" ? content.text : grokText(update.content);
108
+ if (kind === "agent_message_chunk")
109
+ return { type: "text", data: text };
110
+ if (kind === "agent_thought_chunk" || kind === "thought")
111
+ return { type: "thought", data: text };
112
+ if (kind === "tool_call" || kind === "tool_call_update")
113
+ return { type: kind, ...update };
114
+ return event;
115
+ }
9
116
  export function buildGrokArgs(session, prompt) {
10
117
  const args = ["--no-auto-update", "-p", prompt, "--output-format", "streaming-json"];
11
118
  const model = session.selectedModel?.trim();
@@ -26,6 +133,7 @@ export function buildGrokArgs(session, prompt) {
26
133
  }
27
134
  /** Apply one official Grok Build `streaming-json` event. */
28
135
  export function applyGrokEvent(state, event) {
136
+ event = normalizeGrokEvent(event);
29
137
  const type = typeof event.type === "string" ? event.type : "";
30
138
  if (type === "text" && typeof event.data === "string" && event.data) {
31
139
  const previous = state.blocks.at(-1);
@@ -44,6 +152,14 @@ export function applyGrokEvent(state, event) {
44
152
  state.blocks.push({ type: "thinking", thinking: event.data });
45
153
  return null;
46
154
  }
155
+ if (type === "tool_call") {
156
+ upsertGrokTool(state, event, event.status === "completed" || event.status === "failed" || event.status === "error");
157
+ return null;
158
+ }
159
+ if (type === "tool_call_update") {
160
+ upsertGrokTool(state, event, event.status !== "in_progress");
161
+ return null;
162
+ }
47
163
  if (type === "end") {
48
164
  if (typeof event.sessionId === "string" && event.sessionId)
49
165
  state.sessionId = event.sessionId;
@@ -34,7 +34,7 @@ export function buildOpenCodeArgs(session) {
34
34
  || session.mode === "full-access"
35
35
  || session.mode === "managed"
36
36
  || session.mode === "auto-edit") {
37
- args.push("--dangerously-skip-permissions");
37
+ args.push("--auto");
38
38
  }
39
39
  if (session.claudeSessionId)
40
40
  args.push("--session", session.claudeSessionId);
@@ -33,6 +33,62 @@ export function piToolName(name) {
33
33
  const mapped = { bash: "Bash", read: "Read", edit: "Edit", write: "Write", grep: "Grep", find: "Glob", ls: "Glob" };
34
34
  return mapped[name.toLowerCase()] ?? `Pi/${name}`;
35
35
  }
36
+ function applyPiAssistantMessage(state, message) {
37
+ if (typeof message.model === "string")
38
+ state.model = message.model;
39
+ const usage = record(message.usage);
40
+ const cost = record(usage?.cost);
41
+ if (usage) {
42
+ state.usage = {
43
+ inputTokens: typeof usage.input === "number" ? usage.input : 0,
44
+ outputTokens: typeof usage.output === "number" ? usage.output : 0,
45
+ cacheReadInputTokens: typeof usage.cacheRead === "number" ? usage.cacheRead : 0,
46
+ cacheCreationInputTokens: typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0,
47
+ totalCostUsd: typeof cost?.total === "number" ? cost.total : 0,
48
+ };
49
+ }
50
+ const texts = [];
51
+ const thinkings = [];
52
+ const parts = Array.isArray(message.content) ? message.content : [];
53
+ for (const part of parts) {
54
+ const item = record(part);
55
+ if (!item)
56
+ continue;
57
+ if (item.type === "text") {
58
+ const text = typeof item.text === "string" ? item.text : textContent(item);
59
+ if (text)
60
+ texts.push(text);
61
+ }
62
+ else if (item.type === "thinking") {
63
+ const thinking = typeof item.thinking === "string"
64
+ ? item.thinking
65
+ : typeof item.text === "string" ? item.text : "";
66
+ if (thinking)
67
+ thinkings.push(thinking);
68
+ }
69
+ }
70
+ const thinking = thinkings.join("");
71
+ if (thinking && !state.blocks.some((block) => block.type === "thinking")) {
72
+ state.blocks.push({ type: "thinking", thinking });
73
+ }
74
+ const text = texts.join("");
75
+ if (text) {
76
+ const lastText = [...state.blocks].reverse().find((block) => block.type === "text");
77
+ if (lastText?.type === "text") {
78
+ if (text.length >= lastText.text.length)
79
+ lastText.text = text;
80
+ }
81
+ else {
82
+ state.blocks.push({ type: "text", text });
83
+ }
84
+ if (text.length >= state.result.length)
85
+ state.result = text;
86
+ }
87
+ if (message.stopReason === "error") {
88
+ return typeof message.errorMessage === "string" ? message.errorMessage : "Pi CLI execution failed";
89
+ }
90
+ return null;
91
+ }
36
92
  export function applyPiEvent(state, event) {
37
93
  if (event.type === "session" && typeof event.id === "string")
38
94
  state.sessionId = event.id;
@@ -54,6 +110,22 @@ export function applyPiEvent(state, event) {
54
110
  else
55
111
  state.blocks.push({ type: "thinking", thinking: delta });
56
112
  }
113
+ else if (update?.type === "text_end" && typeof update.content === "string" && update.content) {
114
+ const last = state.blocks.at(-1);
115
+ if (last?.type === "text")
116
+ last.text = update.content;
117
+ else
118
+ state.blocks.push({ type: "text", text: update.content });
119
+ if (update.content.length >= state.result.length)
120
+ state.result = update.content;
121
+ }
122
+ else if (update?.type === "thinking_end" && typeof update.content === "string" && update.content) {
123
+ const last = state.blocks.at(-1);
124
+ if (last?.type === "thinking")
125
+ last.thinking = update.content;
126
+ else
127
+ state.blocks.push({ type: "thinking", thinking: update.content });
128
+ }
57
129
  }
58
130
  if (event.type === "tool_execution_start") {
59
131
  const id = typeof event.toolCallId === "string" ? event.toolCallId : crypto.randomUUID();
@@ -64,23 +136,19 @@ export function applyPiEvent(state, event) {
64
136
  const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
65
137
  state.blocks.push({ type: "tool_result", tool_use_id: id, content: textContent(event.result), is_error: event.isError === true });
66
138
  }
67
- if (event.type === "message_end") {
139
+ if (event.type === "message_end" || event.type === "turn_end") {
68
140
  const message = record(event.message);
69
- if (message?.role === "assistant") {
70
- if (typeof message.model === "string")
71
- state.model = message.model;
72
- const usage = record(message.usage);
73
- const cost = record(usage?.cost);
74
- if (usage)
75
- state.usage = {
76
- inputTokens: typeof usage.input === "number" ? usage.input : 0,
77
- outputTokens: typeof usage.output === "number" ? usage.output : 0,
78
- cacheReadInputTokens: typeof usage.cacheRead === "number" ? usage.cacheRead : 0,
79
- cacheCreationInputTokens: typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0,
80
- totalCostUsd: typeof cost?.total === "number" ? cost.total : 0,
81
- };
82
- if (message.stopReason === "error")
83
- return typeof message.errorMessage === "string" ? message.errorMessage : "Pi CLI execution failed";
141
+ if (message?.role === "assistant")
142
+ return applyPiAssistantMessage(state, message);
143
+ }
144
+ if (event.type === "agent_end" && Array.isArray(event.messages)) {
145
+ for (const raw of event.messages) {
146
+ const message = record(raw);
147
+ if (message?.role === "assistant") {
148
+ const error = applyPiAssistantMessage(state, message);
149
+ if (error)
150
+ return error;
151
+ }
84
152
  }
85
153
  }
86
154
  return null;
@@ -1,11 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { ClaudeCliProtocolReducer } from "./structured-claude-protocol.js";
3
3
  import { startStructuredCli } from "./structured-exec-pump.js";
4
+ import { thinkingEffortToQoderEffort } from "./structured-provider-common.js";
4
5
  export function buildQoderArgs(session, prompt) {
5
6
  const args = ["-p", prompt, "--output-format", "stream-json"];
6
7
  const model = session.selectedModel?.trim();
7
8
  if (model && model !== "default")
8
9
  args.push("--model", model);
10
+ const effort = thinkingEffortToQoderEffort(session.thinkingEffort);
11
+ if (effort)
12
+ args.push("--reasoning-effort", effort);
9
13
  if (session.autoApprovePermissions === true
10
14
  || session.mode === "full-access"
11
15
  || session.mode === "managed") {
@@ -32,6 +32,7 @@ export declare class TerminalDaemonClient implements TerminalHost, StructuredExe
32
32
  /** Refresh the structured inventory and catch live handles up after a reconnect. */
33
33
  private refreshStructuredAfterReconnect;
34
34
  private handleFromState;
35
+ private deliverStructuredEvent;
35
36
  disconnect(): void;
36
37
  /**
37
38
  * Socket teardown path. Without a reconnect, a daemon restart would leave
@@ -377,21 +377,45 @@ export class TerminalDaemonClient {
377
377
  }
378
378
  handleFromState(state, reuse) {
379
379
  this.structuredInventory.set(state.runId, state);
380
- if (state.status !== "running") {
381
- this.structuredHandles.delete(state.runId);
382
- return reuse ?? new RemoteStructuredProcess(state.runId, state.incarnationId, state.pid, this);
383
- }
384
380
  let process = this.structuredHandles.get(state.runId) ?? null;
385
381
  if (!process || process.incarnationId !== state.incarnationId) {
386
- process = new RemoteStructuredProcess(state.runId, state.incarnationId, state.pid, this);
382
+ process = reuse && reuse.incarnationId === state.incarnationId
383
+ ? reuse
384
+ : new RemoteStructuredProcess(state.runId, state.incarnationId, state.pid, this);
385
+ }
386
+ if (state.status === "running")
387
387
  this.structuredHandles.set(state.runId, process);
388
- const pending = this.pendingStructuredEvents.get(state.runId) ?? [];
389
- this.pendingStructuredEvents.delete(state.runId);
390
- for (const event of pending)
391
- this.routeStructuredEvent(event);
388
+ else
389
+ this.structuredHandles.delete(state.runId);
390
+ // Events can arrive (and the child can even exit) before this RPC returns.
391
+ // Always drain the backlog onto THIS handle. If the run already finished,
392
+ // also replay daemon logs and synthesize exit so a fast Codex/Pi turn is
393
+ // not lost just because listeners were not attached yet.
394
+ const pending = this.pendingStructuredEvents.get(state.runId) ?? [];
395
+ this.pendingStructuredEvents.delete(state.runId);
396
+ for (const event of pending)
397
+ this.deliverStructuredEvent(process, event);
398
+ if (state.status !== "running") {
399
+ process.replayFromLogs(state);
400
+ process.acceptExit({ exitCode: state.exitCode, signal: state.signal });
392
401
  }
393
402
  return process;
394
403
  }
404
+ deliverStructuredEvent(handle, event) {
405
+ if (event.incarnationId !== handle.incarnationId)
406
+ return;
407
+ if (event.event === "sdata" && typeof event.data === "string" && typeof event.seq === "number") {
408
+ handle.acceptStream({
409
+ stream: event.stream === "stderr" ? "stderr" : "stdout",
410
+ data: event.data,
411
+ seq: event.seq,
412
+ });
413
+ return;
414
+ }
415
+ if (event.event === "sexit") {
416
+ handle.acceptExit({ exitCode: event.exitCode ?? null, signal: event.signal ?? null });
417
+ }
418
+ }
395
419
  disconnect() {
396
420
  this.disposed = true;
397
421
  if (this.reconnectTimer) {
package/dist/types.d.ts CHANGED
@@ -648,11 +648,15 @@ export interface TaskWindowLayout {
648
648
  windows: WorkWindowLayout[];
649
649
  activeWindowId: string | null;
650
650
  }
651
+ export type WorkspaceKind = "project" | "global";
652
+ /** Hidden workspace that owns standalone tasks without a user project. */
653
+ export declare const GLOBAL_WORKSPACE_ID = "wand-global";
651
654
  /** 项目 / 工作空间。锚定一个目录(如 wand 仓库),内含多个并行「任务」。 */
652
655
  export interface Workspace {
653
656
  id: string;
654
657
  name: string;
655
658
  cwd: string;
659
+ kind?: WorkspaceKind;
656
660
  defaultProvider?: WorkspaceDefaultProvider;
657
661
  /**
658
662
  * 工作空间级的布局占位(保留字段)。标签 / 分屏布局实际挂在 Task 上
@@ -674,6 +678,8 @@ export interface WorkspaceTask {
674
678
  workspaceId: string;
675
679
  name: string;
676
680
  worktree: WorkspaceTaskWorktree | null;
681
+ /** Optional directory override; empty/undefined falls back to the workspace cwd. */
682
+ cwd?: string;
677
683
  /** 该任务的工作窗口 Tabs;每个窗口内部可含一棵分屏树。 */
678
684
  layout: TaskWindowLayout | null;
679
685
  status: WorkspaceTaskStatus;
package/dist/types.js CHANGED
@@ -1 +1,2 @@
1
- export {};
1
+ /** Hidden workspace that owns standalone tasks without a user project. */
2
+ export const GLOBAL_WORKSPACE_ID = "wand-global";