wave-code 1.3.0 → 1.5.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.
- package/dist/bundle/wave.mjs +1382 -1131
- package/package.json +9 -7
- package/src/cli.tsx +14 -22
- package/src/commands/update.ts +62 -22
- package/src/components/AgentsManager.tsx +3 -3
- package/src/components/App.tsx +5 -0
- package/src/components/BackgroundTaskManager.tsx +76 -54
- package/src/components/BtwDisplay.tsx +71 -15
- package/src/components/CompactDisplay.tsx +1 -6
- package/src/components/ConfirmationDetails.tsx +34 -13
- package/src/components/ConfirmationSelector.tsx +31 -36
- package/src/components/DiffDisplay.tsx +1 -1
- package/src/components/HooksManager.tsx +3 -3
- package/src/components/InputBox.tsx +37 -11
- package/src/components/LoginCommand.tsx +16 -35
- package/src/components/Markdown.tsx +7 -3
- package/src/components/MarketplaceAddForm.tsx +15 -19
- package/src/components/MessageBlockItem.tsx +0 -6
- package/src/components/RemoteControlView.tsx +175 -0
- package/src/components/ScrolledContent.tsx +15 -13
- package/src/components/SkillsManager.tsx +3 -3
- package/src/components/TaskList.tsx +3 -3
- package/src/components/ToolDisplay.tsx +3 -18
- package/src/components/WorkflowManager.tsx +150 -136
- package/src/components/WorkflowRunDetail.tsx +193 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +308 -154
- package/src/daemon/commands.ts +31 -22
- package/src/daemon-cli.ts +6 -1
- package/src/hooks/useInputManager.ts +25 -39
- package/src/hooks/useLineScroll.ts +51 -5
- package/src/hooks/useRemoteControl.ts +346 -0
- package/src/index.ts +35 -0
- package/src/managers/inputHandlers.ts +5 -0
- package/src/managers/inputReducer.ts +39 -0
- package/src/print-cli.ts +5 -0
- package/src/reducers/workflowManagerReducer.ts +21 -2
- package/src/remote/frameBudget.ts +45 -0
- package/src/remote/hostIdentity.ts +128 -0
- package/src/remote/messageWindow.ts +46 -0
- package/src/remote/notificationCoalescer.ts +192 -0
- package/src/remote/relayClient.ts +690 -0
- package/src/remote/remoteControlCliHost.ts +420 -0
- package/src/remote/remoteControlCommands.ts +197 -0
- package/src/remote/remoteControlConfig.ts +353 -0
- package/src/remote/remoteControlService.ts +822 -0
- package/src/remote/remoteControlSink.ts +78 -0
- package/src/remote/remoteNotifications.ts +200 -0
- package/src/stdio/agentBridge.ts +874 -106
- package/src/stdio/daemonServer.ts +30 -0
- package/src/stdio/hostSlashCommands.ts +209 -0
- package/src/stdio/jsonRpcConnection.ts +11 -2
- package/src/stdio/protocol.ts +14 -115
- package/src/stdio/stdioServer.ts +33 -2
- package/src/stdio/uiPayload.ts +65 -0
- package/src/stdio-cli.ts +11 -2
- package/src/types.ts +7 -0
- package/src/utils/gitCredentialHelper.ts +106 -0
- package/src/utils/logger.ts +5 -2
- package/src/utils/rewindCheckpoints.ts +2 -4
- package/src/utils/workspaceDiff.ts +620 -0
- package/src/utils/worktree.ts +97 -0
- package/src/utils/bracketedPaste.ts +0 -170
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useReducer } from "react";
|
|
2
|
-
import { Box, Text, useInput,
|
|
2
|
+
import { Box, Text, useInput, useWindowSize } from "ink";
|
|
3
3
|
import type { SkillMetadata } from "wave-agent-sdk";
|
|
4
4
|
import {
|
|
5
5
|
skillsManagerReducer,
|
|
@@ -55,7 +55,7 @@ export const SkillsManager: React.FC<SkillsManagerProps> = ({
|
|
|
55
55
|
skills,
|
|
56
56
|
}) => {
|
|
57
57
|
const [state, dispatch] = useReducer(skillsManagerReducer, initialState);
|
|
58
|
-
const {
|
|
58
|
+
const { rows } = useWindowSize();
|
|
59
59
|
|
|
60
60
|
// Handle pending effects
|
|
61
61
|
useEffect(() => {
|
|
@@ -114,7 +114,7 @@ export const SkillsManager: React.FC<SkillsManagerProps> = ({
|
|
|
114
114
|
// Window slice: center the selected item within the visible area, clamping
|
|
115
115
|
// to the terminal's available rows (reusable pattern from
|
|
116
116
|
// AgentsManager/BackgroundTaskManager).
|
|
117
|
-
const availableRows =
|
|
117
|
+
const availableRows = rows || 24;
|
|
118
118
|
const maxVisible = Math.max(3, Math.min(15, availableRows - 12));
|
|
119
119
|
const selectedFlatIndex = entries.findIndex(
|
|
120
120
|
(e) => e.selectableIndex === state.selectedIndex,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useChat } from "../contexts/useChat.js";
|
|
3
|
-
import { Box, Text,
|
|
3
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
4
4
|
import { useTasks } from "../hooks/useTasks.js";
|
|
5
5
|
import type { Task, TaskStatus } from "wave-agent-sdk";
|
|
6
6
|
|
|
@@ -92,7 +92,7 @@ function getDisplayLimit(rows: number | undefined): number {
|
|
|
92
92
|
export const TaskList: React.FC = () => {
|
|
93
93
|
const tasks = useTasks();
|
|
94
94
|
const { isTaskListVisible, isBtwActive } = useChat();
|
|
95
|
-
const {
|
|
95
|
+
const { rows } = useWindowSize();
|
|
96
96
|
|
|
97
97
|
const completionTimestampsRef = React.useRef<Map<string, number>>(new Map());
|
|
98
98
|
const previousCompletedIdsRef = React.useRef<Set<string> | null>(null);
|
|
@@ -195,7 +195,7 @@ export const TaskList: React.FC = () => {
|
|
|
195
195
|
return null;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
const displayLimit = getDisplayLimit(
|
|
198
|
+
const displayLimit = getDisplayLimit(rows);
|
|
199
199
|
const needsTruncation = tasks.length > displayLimit;
|
|
200
200
|
const sorted = sortTasksByPriority(
|
|
201
201
|
tasks,
|
|
@@ -91,12 +91,7 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
|
|
|
91
91
|
|
|
92
92
|
{/* Display shortResult in collapsed state */}
|
|
93
93
|
{!isExpanded && shortResult && !error && (
|
|
94
|
-
<Box
|
|
95
|
-
paddingLeft={2}
|
|
96
|
-
borderLeft
|
|
97
|
-
borderColor="gray"
|
|
98
|
-
flexDirection="column"
|
|
99
|
-
>
|
|
94
|
+
<Box paddingLeft={2} flexDirection="column">
|
|
100
95
|
{shortResult.split("\n").map((line, index) => (
|
|
101
96
|
<Text key={index} color="gray" wrap="truncate-end">
|
|
102
97
|
{line}
|
|
@@ -107,12 +102,7 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
|
|
|
107
102
|
|
|
108
103
|
{/* Display complete parameters in expanded state */}
|
|
109
104
|
{isExpanded && parameters && (
|
|
110
|
-
<Box
|
|
111
|
-
paddingLeft={2}
|
|
112
|
-
borderLeft
|
|
113
|
-
borderColor="gray"
|
|
114
|
-
flexDirection="column"
|
|
115
|
-
>
|
|
105
|
+
<Box paddingLeft={2} flexDirection="column">
|
|
116
106
|
<Text color="cyan" bold>
|
|
117
107
|
Parameters:
|
|
118
108
|
</Text>
|
|
@@ -123,12 +113,7 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
|
|
|
123
113
|
{/* Display complete result in expanded state */}
|
|
124
114
|
{isExpanded && result && (
|
|
125
115
|
<Box flexDirection="column">
|
|
126
|
-
<Box
|
|
127
|
-
paddingLeft={2}
|
|
128
|
-
borderLeft
|
|
129
|
-
borderColor="green"
|
|
130
|
-
flexDirection="column"
|
|
131
|
-
>
|
|
116
|
+
<Box paddingLeft={2} flexDirection="column">
|
|
132
117
|
<Text color="cyan" bold>
|
|
133
118
|
Result:
|
|
134
119
|
</Text>
|
|
@@ -1,53 +1,34 @@
|
|
|
1
|
-
import React, { useEffect, useReducer } from "react";
|
|
1
|
+
import React, { useEffect, useReducer, useState } 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
};
|
|
18
|
+
/** Inline save dialog state, opened from a run's detail view (spec workflow「把
|
|
19
|
+
* 脚本存进库并按名字复用」场景 2). */
|
|
20
|
+
interface SaveDialogState {
|
|
21
|
+
name: string;
|
|
22
|
+
scope: "project" | "user";
|
|
23
|
+
/** Set on a conflict so the next Enter overwrites instead of failing again. */
|
|
24
|
+
overwrite: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
46
27
|
|
|
47
28
|
export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
48
29
|
onCancel,
|
|
49
30
|
}) => {
|
|
50
|
-
const { workflowRuns, stopWorkflowRun } = useChat();
|
|
31
|
+
const { workflowRuns, stopWorkflowRun, saveWorkflowScript } = useChat();
|
|
51
32
|
const MAX_VISIBLE_ITEMS = 3;
|
|
52
33
|
|
|
53
34
|
const [state, dispatch] = useReducer(workflowManagerReducer, {
|
|
@@ -57,6 +38,8 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
57
38
|
detailRunId: null,
|
|
58
39
|
pendingEffect: null,
|
|
59
40
|
} as WorkflowManagerState);
|
|
41
|
+
const [saveDialog, setSaveDialog] = useState<SaveDialogState | null>(null);
|
|
42
|
+
const [saveNotice, setSaveNotice] = useState<string | null>(null);
|
|
60
43
|
|
|
61
44
|
// Handle pending effects
|
|
62
45
|
useEffect(() => {
|
|
@@ -82,7 +65,93 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
82
65
|
dispatch({ type: "SET_RUNS", runs: workflowRuns });
|
|
83
66
|
}, [workflowRuns]);
|
|
84
67
|
|
|
68
|
+
const submitSave = async () => {
|
|
69
|
+
if (!saveDialog || !state.detailRunId) return;
|
|
70
|
+
try {
|
|
71
|
+
const saved = await saveWorkflowScript(state.detailRunId, {
|
|
72
|
+
name: saveDialog.name,
|
|
73
|
+
scope: saveDialog.scope,
|
|
74
|
+
overwrite: saveDialog.overwrite,
|
|
75
|
+
});
|
|
76
|
+
setSaveDialog(null);
|
|
77
|
+
setSaveNotice(`Saved "${saved.name}" to ${saved.path}`);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
setSaveDialog((current) =>
|
|
81
|
+
current
|
|
82
|
+
? {
|
|
83
|
+
...current,
|
|
84
|
+
error: message,
|
|
85
|
+
// A name conflict is the one failure the user can resolve right
|
|
86
|
+
// here: the next Enter overwrites (spec 场景 4).
|
|
87
|
+
overwrite: message.includes("already exists")
|
|
88
|
+
? true
|
|
89
|
+
: current.overwrite,
|
|
90
|
+
}
|
|
91
|
+
: current,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
85
96
|
useInput((input, key) => {
|
|
97
|
+
// The save dialog owns the keyboard while it is open, so typing a name
|
|
98
|
+
// never triggers the list/detail shortcuts underneath.
|
|
99
|
+
if (saveDialog) {
|
|
100
|
+
if (key.escape) {
|
|
101
|
+
setSaveDialog(null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (key.tab) {
|
|
105
|
+
setSaveDialog((current) =>
|
|
106
|
+
current
|
|
107
|
+
? {
|
|
108
|
+
...current,
|
|
109
|
+
scope: current.scope === "project" ? "user" : "project",
|
|
110
|
+
error: undefined,
|
|
111
|
+
}
|
|
112
|
+
: current,
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.return) {
|
|
117
|
+
void submitSave();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.backspace || key.delete) {
|
|
121
|
+
setSaveDialog((current) =>
|
|
122
|
+
current
|
|
123
|
+
? {
|
|
124
|
+
...current,
|
|
125
|
+
name: current.name.slice(0, -1),
|
|
126
|
+
error: undefined,
|
|
127
|
+
}
|
|
128
|
+
: current,
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (input && !key.ctrl && !key.meta) {
|
|
133
|
+
setSaveDialog((current) =>
|
|
134
|
+
current
|
|
135
|
+
? { ...current, name: current.name + input, error: undefined }
|
|
136
|
+
: current,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (state.viewMode === "detail" && state.detailRunId && input === "s") {
|
|
143
|
+
const run = state.runs.find((r) => r.runId === state.detailRunId);
|
|
144
|
+
if (run) {
|
|
145
|
+
setSaveNotice(null);
|
|
146
|
+
setSaveDialog({
|
|
147
|
+
name: run.meta.name,
|
|
148
|
+
scope: "project",
|
|
149
|
+
overwrite: false,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
86
155
|
dispatch({ type: "HANDLE_KEY", input, key });
|
|
87
156
|
});
|
|
88
157
|
|
|
@@ -94,10 +163,6 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
94
163
|
return null;
|
|
95
164
|
}
|
|
96
165
|
|
|
97
|
-
const elapsed = run.endTime
|
|
98
|
-
? run.endTime - run.startTime
|
|
99
|
-
: Date.now() - run.startTime;
|
|
100
|
-
|
|
101
166
|
return (
|
|
102
167
|
<Box
|
|
103
168
|
flexDirection="column"
|
|
@@ -115,79 +180,27 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
115
180
|
</Text>
|
|
116
181
|
</Box>
|
|
117
182
|
|
|
118
|
-
<
|
|
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>
|
|
183
|
+
<WorkflowRunDetail run={run} />
|
|
169
184
|
|
|
170
|
-
{
|
|
171
|
-
<Box flexDirection="column"
|
|
172
|
-
<Text color="cyan"
|
|
173
|
-
|
|
185
|
+
{saveDialog && (
|
|
186
|
+
<Box flexDirection="column">
|
|
187
|
+
<Text color="cyan">Save dynamic workflow</Text>
|
|
188
|
+
<Text>
|
|
189
|
+
Name: {saveDialog.name}
|
|
190
|
+
<Text color="cyan">▏</Text>
|
|
174
191
|
</Text>
|
|
175
|
-
{
|
|
176
|
-
|
|
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
|
-
))}
|
|
192
|
+
<Text dimColor>Scope: {saveDialog.scope} · Tab to switch</Text>
|
|
193
|
+
{saveDialog.error && <Text color="red">{saveDialog.error}</Text>}
|
|
185
194
|
</Box>
|
|
186
195
|
)}
|
|
187
196
|
|
|
197
|
+
{saveNotice && <Text color="green">{saveNotice}</Text>}
|
|
198
|
+
|
|
188
199
|
<Box marginTop={1}>
|
|
189
200
|
<Text dimColor>
|
|
190
|
-
{
|
|
201
|
+
{saveDialog
|
|
202
|
+
? "Enter save · Tab scope · Esc cancel"
|
|
203
|
+
: `${run.status === "running" ? "k to stop · " : ""}s save script · Esc back`}
|
|
191
204
|
</Text>
|
|
192
205
|
</Box>
|
|
193
206
|
</Box>
|
|
@@ -225,6 +238,17 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
225
238
|
);
|
|
226
239
|
const visibleRuns = runs.slice(startIndex, startIndex + MAX_VISIBLE_ITEMS);
|
|
227
240
|
|
|
241
|
+
// Header counts of the states a user acts on (a failed run is plain to see in
|
|
242
|
+
// its own row, so it needs no tally).
|
|
243
|
+
const runningCount = runs.filter((r) => r.status === "running").length;
|
|
244
|
+
const completedCount = runs.filter((r) => r.status === "completed").length;
|
|
245
|
+
const subtitle = [
|
|
246
|
+
runningCount > 0 ? `${runningCount} running` : "",
|
|
247
|
+
completedCount > 0 ? `${completedCount} completed` : "",
|
|
248
|
+
]
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.join(" · ");
|
|
251
|
+
|
|
228
252
|
return (
|
|
229
253
|
<Box
|
|
230
254
|
flexDirection="column"
|
|
@@ -240,6 +264,7 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
240
264
|
<Text color="cyan" bold>
|
|
241
265
|
Workflows
|
|
242
266
|
</Text>
|
|
267
|
+
{subtitle && <Text dimColor> {subtitle}</Text>}
|
|
243
268
|
</Box>
|
|
244
269
|
<Text dimColor>Select a run to view details</Text>
|
|
245
270
|
|
|
@@ -250,7 +275,15 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
250
275
|
const elapsed = run.endTime
|
|
251
276
|
? run.endTime - run.startTime
|
|
252
277
|
: Date.now() - run.startTime;
|
|
253
|
-
|
|
278
|
+
// Duration always reads; the two counters are dropped at zero so an
|
|
279
|
+
// unstarted run does not claim "0 agents · 0 tokens".
|
|
280
|
+
const meta = [
|
|
281
|
+
formatDuration(elapsed),
|
|
282
|
+
run.totalAgents > 0 ? `${run.totalAgents} agents` : "",
|
|
283
|
+
run.totalTokens > 0
|
|
284
|
+
? `${formatTokens(run.totalTokens)} tokens`
|
|
285
|
+
: "",
|
|
286
|
+
].filter(Boolean);
|
|
254
287
|
|
|
255
288
|
return (
|
|
256
289
|
<Box key={run.runId} flexDirection="column">
|
|
@@ -259,43 +292,24 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
|
259
292
|
backgroundColor={isSelected ? "cyan" : undefined}
|
|
260
293
|
>
|
|
261
294
|
{isSelected ? "▶ " : " "}
|
|
262
|
-
{actualIndex + 1}. {run.
|
|
263
|
-
<Text color={
|
|
295
|
+
{actualIndex + 1}. {run.meta.name || run.meta.description}
|
|
296
|
+
<Text color={workflowStatusColor(run.status)}>
|
|
297
|
+
{" "}
|
|
298
|
+
({run.status})
|
|
299
|
+
</Text>
|
|
300
|
+
{/* Oversized marker: the row is where a run is picked, so the
|
|
301
|
+
"too big to leave running" signal belongs on it (spec
|
|
302
|
+
「预算与规模硬上限」场景 7). The counts are on the meta line
|
|
303
|
+
right below, and the stop key is in the footer. */}
|
|
304
|
+
{run.sizeWarning && <Text color="yellow"> ⚠ Large</Text>}
|
|
264
305
|
</Text>
|
|
265
306
|
<Text
|
|
266
307
|
color={isSelected ? "black" : "gray"}
|
|
267
308
|
backgroundColor={isSelected ? "cyan" : undefined}
|
|
268
309
|
>
|
|
269
310
|
{" "}
|
|
270
|
-
{
|
|
271
|
-
tokens · {formatDuration(elapsed)}
|
|
311
|
+
{meta.join(" · ")}
|
|
272
312
|
</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
313
|
{isSelected && (
|
|
300
314
|
<Box marginLeft={4} flexDirection="column">
|
|
301
315
|
<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;
|
|
@@ -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",
|