wave-code 1.2.0 → 1.4.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.
Files changed (64) hide show
  1. package/dist/bundle/wave.mjs +1738 -893
  2. package/package.json +6 -3
  3. package/src/cli.tsx +9 -0
  4. package/src/commands/plugin/install.ts +6 -7
  5. package/src/commands/plugin/uninstall.ts +11 -5
  6. package/src/commands/update.ts +62 -22
  7. package/src/components/BackgroundTaskManager.tsx +76 -54
  8. package/src/components/ChatInterface.tsx +1 -0
  9. package/src/components/ConfirmationDetails.tsx +51 -16
  10. package/src/components/ConfirmationSelector.tsx +39 -0
  11. package/src/components/InputBox.tsx +41 -2
  12. package/src/components/Markdown.tsx +21 -8
  13. package/src/components/MarketplaceDetail.tsx +10 -27
  14. package/src/components/MarketplaceView.tsx +17 -3
  15. package/src/components/MessageBlockItem.tsx +0 -6
  16. package/src/components/Notifications.tsx +7 -0
  17. package/src/components/PlanView.tsx +22 -17
  18. package/src/components/PluginManagerShell.tsx +3 -3
  19. package/src/components/PluginManagerTypes.ts +2 -1
  20. package/src/components/ResumeCommand.tsx +84 -0
  21. package/src/components/ScrolledContent.tsx +71 -0
  22. package/src/components/SessionSelector.tsx +184 -6
  23. package/src/components/SessionSelectorFlow.tsx +246 -0
  24. package/src/components/WorkflowManager.tsx +38 -138
  25. package/src/components/WorkflowRunDetail.tsx +193 -0
  26. package/src/constants/commands.ts +24 -0
  27. package/src/contexts/useChat.tsx +517 -155
  28. package/src/daemon/commands.ts +582 -50
  29. package/src/daemon-cli.ts +12 -1
  30. package/src/hooks/useInputManager.ts +20 -0
  31. package/src/hooks/usePluginManager.ts +104 -78
  32. package/src/hooks/useRemoteControl.ts +324 -0
  33. package/src/index.ts +97 -8
  34. package/src/managers/inputHandlers.ts +16 -9
  35. package/src/managers/inputReducer.ts +30 -9
  36. package/src/print-cli.ts +5 -0
  37. package/src/reducers/confirmationReducer.ts +20 -1
  38. package/src/reducers/selectorReducer.ts +3 -0
  39. package/src/reducers/workflowManagerReducer.ts +21 -2
  40. package/src/remote/frameBudget.ts +45 -0
  41. package/src/remote/hostIdentity.ts +128 -0
  42. package/src/remote/messageWindow.ts +97 -0
  43. package/src/remote/notificationCoalescer.ts +174 -0
  44. package/src/remote/relayClient.ts +668 -0
  45. package/src/remote/remoteControlCliHost.ts +391 -0
  46. package/src/remote/remoteControlCommands.ts +197 -0
  47. package/src/remote/remoteControlConfig.ts +334 -0
  48. package/src/remote/remoteControlService.ts +799 -0
  49. package/src/remote/remoteControlSink.ts +77 -0
  50. package/src/remote/remoteNotifications.ts +194 -0
  51. package/src/session-selector-cli.tsx +9 -255
  52. package/src/stdio/agentBridge.ts +1001 -165
  53. package/src/stdio/daemonServer.ts +30 -0
  54. package/src/stdio/jsonRpcConnection.ts +11 -2
  55. package/src/stdio/protocol.ts +14 -106
  56. package/src/stdio/stdioServer.ts +33 -2
  57. package/src/stdio-cli.ts +16 -1
  58. package/src/utils/gitCredentialHelper.ts +106 -0
  59. package/src/utils/logger.ts +5 -2
  60. package/src/utils/rewindCheckpoints.ts +2 -4
  61. package/src/utils/sessionOwnership.ts +62 -0
  62. package/src/utils/workspaceDiff.ts +620 -0
  63. package/src/utils/worktree.ts +97 -0
  64. package/src/components/PlanDisplay.tsx +0 -18
@@ -1,49 +1,20 @@
1
1
  import React, { useEffect, useReducer } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { useChat } from "../contexts/useChat.js";
4
- import type { WorkflowRun } from "wave-agent-sdk";
5
4
  import {
6
5
  workflowManagerReducer,
7
6
  type WorkflowManagerState,
8
7
  } from "../reducers/workflowManagerReducer.js";
8
+ import WorkflowRunDetail, {
9
+ formatDuration,
10
+ formatTokens,
11
+ workflowStatusColor,
12
+ } from "./WorkflowRunDetail.js";
9
13
 
10
14
  export interface WorkflowManagerProps {
11
15
  onCancel: () => void;
12
16
  }
13
17
 
14
- const statusColor = (
15
- status: WorkflowRun["status"],
16
- ): "green" | "blue" | "red" | "yellow" => {
17
- switch (status) {
18
- case "running":
19
- return "green";
20
- case "completed":
21
- return "blue";
22
- case "failed":
23
- case "aborted":
24
- return "red";
25
- case "paused":
26
- return "yellow";
27
- }
28
- };
29
-
30
- const formatDuration = (ms: number): string => {
31
- if (ms < 1000) return `${ms}ms`;
32
- if (ms < 60000) return `${Math.round(ms / 1000)}s`;
33
- const minutes = Math.floor(ms / 60000);
34
- const seconds = Math.round((ms % 60000) / 1000);
35
- return `${minutes}m ${seconds}s`;
36
- };
37
-
38
- const formatTime = (timestamp: number): string => {
39
- return new Date(timestamp).toLocaleTimeString();
40
- };
41
-
42
- const formatTokens = (tokens: number): string => {
43
- if (tokens < 1000) return `${tokens}`;
44
- return `${(tokens / 1000).toFixed(1)}k`;
45
- };
46
-
47
18
  export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
48
19
  onCancel,
49
20
  }) => {
@@ -94,10 +65,6 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
94
65
  return null;
95
66
  }
96
67
 
97
- const elapsed = run.endTime
98
- ? run.endTime - run.startTime
99
- : Date.now() - run.startTime;
100
-
101
68
  return (
102
69
  <Box
103
70
  flexDirection="column"
@@ -115,75 +82,7 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
115
82
  </Text>
116
83
  </Box>
117
84
 
118
- <Box flexDirection="column" gap={0}>
119
- <Box>
120
- <Text>
121
- <Text color="blue">Run ID:</Text> {run.runId}
122
- </Text>
123
- </Box>
124
- {run.meta.description && (
125
- <Box>
126
- <Text>
127
- <Text color="blue">Description:</Text> {run.meta.description}
128
- </Text>
129
- </Box>
130
- )}
131
- <Box>
132
- <Text>
133
- <Text color="blue">Status:</Text>{" "}
134
- <Text color={statusColor(run.status)}>{run.status}</Text>
135
- </Text>
136
- </Box>
137
- <Box>
138
- <Text>
139
- <Text color="blue">Started:</Text> {formatTime(run.startTime)}
140
- {run.endTime && (
141
- <Text>
142
- {" "}
143
- | <Text color="blue">Ended:</Text> {formatTime(run.endTime)}
144
- </Text>
145
- )}
146
- {" | "}
147
- <Text color="blue">Duration:</Text> {formatDuration(elapsed)}
148
- </Text>
149
- </Box>
150
- <Box>
151
- <Text>
152
- <Text color="blue">Agents:</Text> {run.totalAgents} {" | "}
153
- <Text color="blue">Tokens:</Text> {formatTokens(run.totalTokens)}
154
- </Text>
155
- </Box>
156
- <Box>
157
- <Text>
158
- <Text color="blue">Script:</Text> {run.scriptPath}
159
- </Text>
160
- </Box>
161
- {run.error && (
162
- <Box>
163
- <Text color="red">
164
- <Text color="blue">Error:</Text> {run.error}
165
- </Text>
166
- </Box>
167
- )}
168
- </Box>
169
-
170
- {run.phases.length > 0 && (
171
- <Box flexDirection="column" marginTop={1}>
172
- <Text color="cyan" bold>
173
- Phases:
174
- </Text>
175
- {run.phases.map((phase, i) => (
176
- <Box key={i} marginLeft={2}>
177
- <Text>
178
- <Text color="blue">{phase.title}</Text>
179
- {" — "}
180
- {phase.agentCount} agents | {formatTokens(phase.tokens)}{" "}
181
- tokens | {formatDuration(phase.elapsed)}
182
- </Text>
183
- </Box>
184
- ))}
185
- </Box>
186
- )}
85
+ <WorkflowRunDetail run={run} />
187
86
 
188
87
  <Box marginTop={1}>
189
88
  <Text dimColor>
@@ -225,6 +124,17 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
225
124
  );
226
125
  const visibleRuns = runs.slice(startIndex, startIndex + MAX_VISIBLE_ITEMS);
227
126
 
127
+ // Header counts of the states a user acts on (a failed run is plain to see in
128
+ // its own row, so it needs no tally).
129
+ const runningCount = runs.filter((r) => r.status === "running").length;
130
+ const completedCount = runs.filter((r) => r.status === "completed").length;
131
+ const subtitle = [
132
+ runningCount > 0 ? `${runningCount} running` : "",
133
+ completedCount > 0 ? `${completedCount} completed` : "",
134
+ ]
135
+ .filter(Boolean)
136
+ .join(" · ");
137
+
228
138
  return (
229
139
  <Box
230
140
  flexDirection="column"
@@ -240,6 +150,7 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
240
150
  <Text color="cyan" bold>
241
151
  Workflows
242
152
  </Text>
153
+ {subtitle && <Text dimColor> {subtitle}</Text>}
243
154
  </Box>
244
155
  <Text dimColor>Select a run to view details</Text>
245
156
 
@@ -250,7 +161,15 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
250
161
  const elapsed = run.endTime
251
162
  ? run.endTime - run.startTime
252
163
  : Date.now() - run.startTime;
253
- const phaseText = run.phases.map((p) => p.title).join(" → ");
164
+ // Duration always reads; the two counters are dropped at zero so an
165
+ // unstarted run does not claim "0 agents · 0 tokens".
166
+ const meta = [
167
+ formatDuration(elapsed),
168
+ run.totalAgents > 0 ? `${run.totalAgents} agents` : "",
169
+ run.totalTokens > 0
170
+ ? `${formatTokens(run.totalTokens)} tokens`
171
+ : "",
172
+ ].filter(Boolean);
254
173
 
255
174
  return (
256
175
  <Box key={run.runId} flexDirection="column">
@@ -259,43 +178,24 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
259
178
  backgroundColor={isSelected ? "cyan" : undefined}
260
179
  >
261
180
  {isSelected ? "▶ " : " "}
262
- {actualIndex + 1}. {run.runId.slice(0, 8)} {run.meta.name}
263
- <Text color={statusColor(run.status)}> ({run.status})</Text>
181
+ {actualIndex + 1}. {run.meta.name || run.meta.description}
182
+ <Text color={workflowStatusColor(run.status)}>
183
+ {" "}
184
+ ({run.status})
185
+ </Text>
186
+ {/* Oversized marker: the row is where a run is picked, so the
187
+ "too big to leave running" signal belongs on it (spec
188
+ 「预算与规模硬上限」场景 7). The counts are on the meta line
189
+ right below, and the stop key is in the footer. */}
190
+ {run.sizeWarning && <Text color="yellow"> ⚠ Large</Text>}
264
191
  </Text>
265
192
  <Text
266
193
  color={isSelected ? "black" : "gray"}
267
194
  backgroundColor={isSelected ? "cyan" : undefined}
268
195
  >
269
196
  {" "}
270
- {run.totalAgents} agents · {formatTokens(run.totalTokens)}{" "}
271
- tokens · {formatDuration(elapsed)}
197
+ {meta.join(" · ")}
272
198
  </Text>
273
- {phaseText && (
274
- <Text
275
- color={isSelected ? "black" : "gray"}
276
- backgroundColor={isSelected ? "cyan" : undefined}
277
- >
278
- {" "}
279
- {run.phases.map((p, pi) => (
280
- <React.Fragment key={pi}>
281
- {pi > 0 && " → "}
282
- <Text
283
- color={
284
- isSelected
285
- ? "black"
286
- : pi === run.phases.length - 1 &&
287
- run.status === "running"
288
- ? "green"
289
- : undefined
290
- }
291
- backgroundColor={isSelected ? "cyan" : undefined}
292
- >
293
- {p.title}
294
- </Text>
295
- </React.Fragment>
296
- ))}
297
- </Text>
298
- )}
299
199
  {isSelected && (
300
200
  <Box marginLeft={4} flexDirection="column">
301
201
  <Text color="gray" dimColor>
@@ -0,0 +1,193 @@
1
+ /**
2
+ * WorkflowRunDetail - one workflow run's own fields, rendered for the terminal.
3
+ *
4
+ * Shared by the two CLI panels that inspect a run: the `/workflows` detail view,
5
+ * and the `/tasks` detail view when the selected task is a workflow (a workflow
6
+ * background task's id IS its run id, so the two panels join on it). Both render
7
+ * this component, so they cannot disagree about what a run's progress looks like.
8
+ *
9
+ * The information set matches what the webview renders through
10
+ * `WorkflowRunProgress` — same fields, same order, same zero-value rules — but
11
+ * the carrier differs: Ink text lines instead of a DOM block, and the log lines
12
+ * are capped because an Ink panel does not scroll. That split is the spec's
13
+ * cross-renderer rule (docs/specs/multi-agent/workflow.md 边界情况).
14
+ */
15
+
16
+ import React from "react";
17
+ import { Box, Text } from "ink";
18
+ import { getLastLines } from "wave-agent-sdk";
19
+ import type { WorkflowRun } from "wave-agent-sdk";
20
+ import {
21
+ WORKFLOW_SIZE_WARNING_AGENTS,
22
+ WORKFLOW_SIZE_WARNING_TOKENS,
23
+ } from "wave-agent-sdk/constants";
24
+
25
+ export const formatDuration = (ms: number): string => {
26
+ if (ms < 1000) return `${ms}ms`;
27
+ if (ms < 60000) return `${Math.round(ms / 1000)}s`;
28
+ const minutes = Math.floor(ms / 60000);
29
+ const seconds = Math.round((ms % 60000) / 1000);
30
+ return `${minutes}m ${seconds}s`;
31
+ };
32
+
33
+ export const formatTime = (timestamp: number): string => {
34
+ return new Date(timestamp).toLocaleTimeString();
35
+ };
36
+
37
+ export const formatTokens = (tokens: number): string => {
38
+ if (tokens < 1000) return `${tokens}`;
39
+ // Millions matter here: a run crosses the 1.5M size threshold already showing
40
+ // "1500.0k", and the marker line names that threshold.
41
+ if (tokens < 1_000_000) return `${(tokens / 1000).toFixed(1)}k`;
42
+ return `${(tokens / 1_000_000).toFixed(1)}M`;
43
+ };
44
+
45
+ export const workflowStatusColor = (
46
+ status: WorkflowRun["status"],
47
+ ): "green" | "blue" | "red" | "yellow" => {
48
+ switch (status) {
49
+ case "running":
50
+ return "green";
51
+ case "completed":
52
+ return "blue";
53
+ case "failed":
54
+ case "aborted":
55
+ return "red";
56
+ case "paused":
57
+ return "yellow";
58
+ }
59
+ };
60
+
61
+ /**
62
+ * Log lines rendered. Same convention as this panel's OUTPUT block ("last 10
63
+ * lines"): a TUI panel cannot scroll, and a workflow that logs inside a loop can
64
+ * hold hundreds of retained lines.
65
+ */
66
+ export const MAX_RUN_LOG_LINES = 10;
67
+
68
+ const WorkflowRunDetail: React.FC<{ run: WorkflowRun }> = ({ run }) => {
69
+ const elapsed = run.endTime
70
+ ? run.endTime - run.startTime
71
+ : Date.now() - run.startTime;
72
+ const logs = run.logs ?? [];
73
+ const failures = run.failures ?? [];
74
+
75
+ return (
76
+ <>
77
+ <Box flexDirection="column" gap={0}>
78
+ <Box>
79
+ <Text>
80
+ <Text color="blue">Run ID:</Text> {run.runId}
81
+ </Text>
82
+ </Box>
83
+ {run.meta.description && (
84
+ <Box>
85
+ <Text>
86
+ <Text color="blue">Description:</Text> {run.meta.description}
87
+ </Text>
88
+ </Box>
89
+ )}
90
+ <Box>
91
+ <Text>
92
+ <Text color="blue">Status:</Text>{" "}
93
+ <Text color={workflowStatusColor(run.status)}>{run.status}</Text>
94
+ </Text>
95
+ </Box>
96
+ <Box>
97
+ <Text>
98
+ <Text color="blue">Started:</Text> {formatTime(run.startTime)}
99
+ {run.endTime && (
100
+ <Text>
101
+ {" "}
102
+ | <Text color="blue">Ended:</Text> {formatTime(run.endTime)}
103
+ </Text>
104
+ )}
105
+ {" | "}
106
+ <Text color="blue">Duration:</Text> {formatDuration(elapsed)}
107
+ </Text>
108
+ </Box>
109
+ <Box>
110
+ <Text>
111
+ <Text color="blue">Agents:</Text> {run.totalAgents} {" | "}
112
+ <Text color="blue">Tokens:</Text> {formatTokens(run.totalTokens)}
113
+ </Text>
114
+ </Box>
115
+ {/* Oversized marker (spec「预算与规模硬上限」场景 7): the run is bigger
116
+ than the thresholds, which is the human's cue to stop it — the model
117
+ is never told, so this line has to carry the scale and the
118
+ thresholds itself. Stopping stays with this panel's own affordance
119
+ (the footer's `k`), so the line only names it when the run is still
120
+ running and there is something to stop. */}
121
+ {run.sizeWarning && (
122
+ <Box>
123
+ <Text color="yellow">
124
+ ⚠ Large run: {run.totalAgents} agents /{" "}
125
+ {formatTokens(run.totalTokens)} tokens — over the{" "}
126
+ {WORKFLOW_SIZE_WARNING_AGENTS} agents /{" "}
127
+ {formatTokens(WORKFLOW_SIZE_WARNING_TOKENS)} tokens threshold.
128
+ {run.status === "running" && " Press k to stop it."}
129
+ </Text>
130
+ </Box>
131
+ )}
132
+ <Box>
133
+ <Text>
134
+ <Text color="blue">Script:</Text> {run.scriptPath}
135
+ </Text>
136
+ </Box>
137
+ {run.error && (
138
+ <Box>
139
+ <Text color="red">
140
+ <Text color="blue">Error:</Text> {run.error}
141
+ </Text>
142
+ </Box>
143
+ )}
144
+ </Box>
145
+
146
+ {failures.length > 0 && (
147
+ <Box flexDirection="column" marginTop={1}>
148
+ <Text color="red" bold>
149
+ Failures:
150
+ </Text>
151
+ {failures.map((failure, i) => (
152
+ <Box key={i} marginLeft={2}>
153
+ <Text color="red">{failure}</Text>
154
+ </Box>
155
+ ))}
156
+ </Box>
157
+ )}
158
+
159
+ {/* Only render a section that has content: an empty "Phases:" / "Logs:"
160
+ block reads as missing data rather than "nothing happened yet". */}
161
+ {run.phases.length > 0 && (
162
+ <Box flexDirection="column" marginTop={1}>
163
+ <Text color="cyan" bold>
164
+ Phases:
165
+ </Text>
166
+ {run.phases.map((phase, i) => (
167
+ <Box key={i} marginLeft={2}>
168
+ <Text>
169
+ <Text color="blue">{phase.title}</Text>
170
+ {" — "}
171
+ {phase.agentCount} agents | {formatTokens(phase.tokens)} tokens
172
+ | {formatDuration(phase.elapsed)}
173
+ </Text>
174
+ </Box>
175
+ ))}
176
+ </Box>
177
+ )}
178
+
179
+ {logs.length > 0 && (
180
+ <Box flexDirection="column" marginTop={1}>
181
+ <Text color="cyan" bold>
182
+ Logs (last {MAX_RUN_LOG_LINES} lines):
183
+ </Text>
184
+ <Box marginLeft={2}>
185
+ <Text>{getLastLines(logs.join("\n"), MAX_RUN_LOG_LINES)}</Text>
186
+ </Box>
187
+ </Box>
188
+ )}
189
+ </>
190
+ );
191
+ };
192
+
193
+ export default WorkflowRunDetail;
@@ -56,6 +56,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
56
56
  description: "View and manage plugins",
57
57
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
58
58
  },
59
+ {
60
+ id: "reload-plugins",
61
+ name: "reload-plugins",
62
+ description: "Reload plugins in place, without restarting the session",
63
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
64
+ },
59
65
  {
60
66
  id: "btw",
61
67
  name: "btw",
@@ -92,6 +98,24 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
92
98
  description: "Enable plan mode or view the current session plan",
93
99
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
94
100
  },
101
+ {
102
+ id: "resume",
103
+ name: "resume",
104
+ description: "Switch to another conversation without leaving the session",
105
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
106
+ },
107
+ {
108
+ id: "rename",
109
+ name: "rename",
110
+ description: "Set a custom title for the current conversation",
111
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
112
+ },
113
+ {
114
+ id: "remote-control",
115
+ name: "remote-control",
116
+ description: "Serve this terminal's session to a phone",
117
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
118
+ },
95
119
  {
96
120
  id: "clear",
97
121
  name: "clear",