wave-code 1.3.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 (42) hide show
  1. package/dist/bundle/wave.mjs +1275 -895
  2. package/package.json +4 -2
  3. package/src/commands/update.ts +62 -22
  4. package/src/components/BackgroundTaskManager.tsx +76 -54
  5. package/src/components/ConfirmationDetails.tsx +17 -3
  6. package/src/components/InputBox.tsx +2 -0
  7. package/src/components/MessageBlockItem.tsx +0 -6
  8. package/src/components/WorkflowManager.tsx +38 -138
  9. package/src/components/WorkflowRunDetail.tsx +193 -0
  10. package/src/constants/commands.ts +6 -0
  11. package/src/contexts/useChat.tsx +359 -127
  12. package/src/daemon/commands.ts +13 -4
  13. package/src/daemon-cli.ts +6 -1
  14. package/src/hooks/useInputManager.ts +4 -0
  15. package/src/hooks/useRemoteControl.ts +324 -0
  16. package/src/index.ts +35 -0
  17. package/src/managers/inputHandlers.ts +2 -0
  18. package/src/managers/inputReducer.ts +1 -0
  19. package/src/print-cli.ts +5 -0
  20. package/src/reducers/workflowManagerReducer.ts +21 -2
  21. package/src/remote/frameBudget.ts +45 -0
  22. package/src/remote/hostIdentity.ts +128 -0
  23. package/src/remote/messageWindow.ts +97 -0
  24. package/src/remote/notificationCoalescer.ts +174 -0
  25. package/src/remote/relayClient.ts +668 -0
  26. package/src/remote/remoteControlCliHost.ts +391 -0
  27. package/src/remote/remoteControlCommands.ts +197 -0
  28. package/src/remote/remoteControlConfig.ts +334 -0
  29. package/src/remote/remoteControlService.ts +799 -0
  30. package/src/remote/remoteControlSink.ts +77 -0
  31. package/src/remote/remoteNotifications.ts +194 -0
  32. package/src/stdio/agentBridge.ts +608 -74
  33. package/src/stdio/daemonServer.ts +30 -0
  34. package/src/stdio/jsonRpcConnection.ts +11 -2
  35. package/src/stdio/protocol.ts +14 -115
  36. package/src/stdio/stdioServer.ts +33 -2
  37. package/src/stdio-cli.ts +11 -2
  38. package/src/utils/gitCredentialHelper.ts +106 -0
  39. package/src/utils/logger.ts +5 -2
  40. package/src/utils/rewindCheckpoints.ts +2 -4
  41. package/src/utils/workspaceDiff.ts +620 -0
  42. package/src/utils/worktree.ts +97 -0
@@ -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;
@@ -110,6 +110,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
110
110
  description: "Set a custom title for the current conversation",
111
111
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
112
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
+ },
113
119
  {
114
120
  id: "clear",
115
121
  name: "clear",