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.
- package/dist/bundle/wave.mjs +1275 -895
- package/package.json +4 -2
- package/src/commands/update.ts +62 -22
- package/src/components/BackgroundTaskManager.tsx +76 -54
- package/src/components/ConfirmationDetails.tsx +17 -3
- package/src/components/InputBox.tsx +2 -0
- package/src/components/MessageBlockItem.tsx +0 -6
- package/src/components/WorkflowManager.tsx +38 -138
- package/src/components/WorkflowRunDetail.tsx +193 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +359 -127
- package/src/daemon/commands.ts +13 -4
- package/src/daemon-cli.ts +6 -1
- package/src/hooks/useInputManager.ts +4 -0
- package/src/hooks/useRemoteControl.ts +324 -0
- package/src/index.ts +35 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +1 -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 +97 -0
- package/src/remote/notificationCoalescer.ts +174 -0
- package/src/remote/relayClient.ts +668 -0
- package/src/remote/remoteControlCliHost.ts +391 -0
- package/src/remote/remoteControlCommands.ts +197 -0
- package/src/remote/remoteControlConfig.ts +334 -0
- package/src/remote/remoteControlService.ts +799 -0
- package/src/remote/remoteControlSink.ts +77 -0
- package/src/remote/remoteNotifications.ts +194 -0
- package/src/stdio/agentBridge.ts +608 -74
- package/src/stdio/daemonServer.ts +30 -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-cli.ts +11 -2
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@vscode/ripgrep": "^1.18.0",
|
|
32
|
+
"qrcode": "^1.5.4",
|
|
32
33
|
"sharp": "^0.35.4"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
36
|
+
"@types/qrcode": "^1.5.6",
|
|
35
37
|
"@types/react": "^19.1.8",
|
|
36
38
|
"@types/semver": "^7.7.1",
|
|
37
39
|
"@types/yargs": "^17.0.0",
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"wrap-ansi": "^10.0.0",
|
|
58
60
|
"yargs": "^17.7.2",
|
|
59
61
|
"zod": "^3.23.8",
|
|
60
|
-
"wave-agent-sdk": "1.
|
|
62
|
+
"wave-agent-sdk": "1.4.0"
|
|
61
63
|
},
|
|
62
64
|
"engines": {
|
|
63
65
|
"node": ">=22"
|
package/src/commands/update.ts
CHANGED
|
@@ -1,32 +1,70 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "child_process";
|
|
2
|
-
import https from "https";
|
|
3
2
|
import chalk from "chalk";
|
|
4
3
|
import { isUpdateAvailable } from "../utils/version.js";
|
|
5
4
|
import { readNearestPackageJson } from "../utils/readPackageJson.js";
|
|
6
5
|
|
|
7
6
|
const currentVersion = readNearestPackageJson().version;
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
.on("error", (err) => {
|
|
27
|
-
reject(err);
|
|
28
|
-
});
|
|
8
|
+
/** npm registry mirror for China users (the CLI downloads its runtime deps from the same one). */
|
|
9
|
+
const REGISTRY_MIRROR = "https://registry.npmmirror.com";
|
|
10
|
+
/** Official registry — the fallback when the mirror is unreachable. */
|
|
11
|
+
const REGISTRY_OFFICIAL = "https://registry.npmjs.org";
|
|
12
|
+
/** Cap each registry request so a hanging one cannot stall the fallback. */
|
|
13
|
+
const REGISTRY_TIMEOUT_MS = 5000;
|
|
14
|
+
|
|
15
|
+
interface LatestVersion {
|
|
16
|
+
version: string;
|
|
17
|
+
/** The registry that answered — the install reuses it (see getLatestVersion). */
|
|
18
|
+
registry: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fetchLatestVersion(registry: string): Promise<string> {
|
|
22
|
+
const response = await fetch(`${registry}/wave-code/latest`, {
|
|
23
|
+
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
|
|
29
24
|
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`HTTP ${response.status} fetching ${registry}/wave-code/latest`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const json = (await response.json()) as { version?: string };
|
|
31
|
+
if (!json.version) {
|
|
32
|
+
throw new Error(`No version in the response from ${registry}`);
|
|
33
|
+
}
|
|
34
|
+
return json.version;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The mirror answers first so the check is fast in CN; the official registry is
|
|
39
|
+
* a silent fallback so an unreachable mirror never blocks the check.
|
|
40
|
+
*
|
|
41
|
+
* The install reuses whichever registry answered: a machine that can only reach
|
|
42
|
+
* the official registry would fail the install for the very reason the check
|
|
43
|
+
* fell back.
|
|
44
|
+
*/
|
|
45
|
+
async function getLatestVersion(): Promise<LatestVersion> {
|
|
46
|
+
try {
|
|
47
|
+
return {
|
|
48
|
+
version: await fetchLatestVersion(REGISTRY_MIRROR),
|
|
49
|
+
registry: REGISTRY_MIRROR,
|
|
50
|
+
};
|
|
51
|
+
} catch (mirrorError) {
|
|
52
|
+
try {
|
|
53
|
+
return {
|
|
54
|
+
version: await fetchLatestVersion(REGISTRY_OFFICIAL),
|
|
55
|
+
registry: REGISTRY_OFFICIAL,
|
|
56
|
+
};
|
|
57
|
+
} catch (officialError) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Could not reach ${REGISTRY_MIRROR} (${errorText(mirrorError)}) ` +
|
|
60
|
+
`or ${REGISTRY_OFFICIAL} (${errorText(officialError)})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function errorText(error: unknown): string {
|
|
67
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
function detectPackageManager(): "npm" | "pnpm" | "yarn" {
|
|
@@ -53,7 +91,7 @@ export async function updateCommand() {
|
|
|
53
91
|
console.log(chalk.dim(`Current version: ${currentVersion}`));
|
|
54
92
|
|
|
55
93
|
try {
|
|
56
|
-
const latestVersion = await getLatestVersion();
|
|
94
|
+
const { version: latestVersion, registry } = await getLatestVersion();
|
|
57
95
|
console.log(chalk.dim(`Latest version: ${latestVersion}`));
|
|
58
96
|
|
|
59
97
|
if (!isUpdateAvailable(currentVersion, latestVersion)) {
|
|
@@ -79,6 +117,8 @@ export async function updateCommand() {
|
|
|
79
117
|
updateCmd = "npm";
|
|
80
118
|
args = ["install", "-g", "wave-code@latest"];
|
|
81
119
|
}
|
|
120
|
+
// Appended once so every package manager (including a future branch) gets it.
|
|
121
|
+
args.push(`--registry=${registry}`);
|
|
82
122
|
|
|
83
123
|
console.log(chalk.blue(`Updating WAVE Code using ${packageManager}...`));
|
|
84
124
|
console.log(chalk.dim(`Running: ${updateCmd} ${args.join(" ")}`));
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
backgroundTaskManagerReducer,
|
|
7
7
|
type BackgroundTaskManagerState,
|
|
8
8
|
} from "../reducers/backgroundTaskManagerReducer.js";
|
|
9
|
+
import WorkflowRunDetail, {
|
|
10
|
+
formatDuration,
|
|
11
|
+
formatTime,
|
|
12
|
+
} from "./WorkflowRunDetail.js";
|
|
9
13
|
|
|
10
14
|
export interface BackgroundTaskManagerProps {
|
|
11
15
|
onCancel: () => void;
|
|
@@ -14,8 +18,12 @@ export interface BackgroundTaskManagerProps {
|
|
|
14
18
|
export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
15
19
|
onCancel,
|
|
16
20
|
}) => {
|
|
17
|
-
const {
|
|
18
|
-
|
|
21
|
+
const {
|
|
22
|
+
backgroundTasks,
|
|
23
|
+
workflowRuns,
|
|
24
|
+
getBackgroundTaskOutput,
|
|
25
|
+
stopBackgroundTask,
|
|
26
|
+
} = useChat();
|
|
19
27
|
const MAX_VISIBLE_ITEMS = 3;
|
|
20
28
|
|
|
21
29
|
const [state, dispatch] = useReducer(backgroundTaskManagerReducer, {
|
|
@@ -71,18 +79,6 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
|
71
79
|
}
|
|
72
80
|
}, [viewMode, detailTaskId, getBackgroundTaskOutput]);
|
|
73
81
|
|
|
74
|
-
const formatDuration = (ms: number): string => {
|
|
75
|
-
if (ms < 1000) return `${ms}ms`;
|
|
76
|
-
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
77
|
-
const minutes = Math.floor(ms / 60000);
|
|
78
|
-
const seconds = Math.round((ms % 60000) / 1000);
|
|
79
|
-
return `${minutes}m ${seconds}s`;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const formatTime = (timestamp: number): string => {
|
|
83
|
-
return new Date(timestamp).toLocaleTimeString();
|
|
84
|
-
};
|
|
85
|
-
|
|
86
82
|
// Calculate visible window
|
|
87
83
|
const startIndex = Math.max(
|
|
88
84
|
0,
|
|
@@ -104,6 +100,15 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
|
104
100
|
return null;
|
|
105
101
|
}
|
|
106
102
|
|
|
103
|
+
// A workflow background task's id IS its run id, so a workflow task can show
|
|
104
|
+
// real progress instead of the (always empty) text output: the same run
|
|
105
|
+
// fields `/workflows` shows, via the same component. Absent run (e.g. the
|
|
106
|
+
// session was cleared) falls back to the plain task detail below.
|
|
107
|
+
const selectedRun =
|
|
108
|
+
task.type === "workflow"
|
|
109
|
+
? (workflowRuns.find((r) => r.runId === task.id) ?? null)
|
|
110
|
+
: null;
|
|
111
|
+
|
|
107
112
|
return (
|
|
108
113
|
<Box
|
|
109
114
|
flexDirection="column"
|
|
@@ -134,53 +139,70 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
|
134
139
|
</Text>
|
|
135
140
|
</Box>
|
|
136
141
|
)}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<Box>
|
|
144
|
-
<Text>
|
|
145
|
-
<Text color="blue">Started:</Text> {formatTime(task.startTime)}
|
|
146
|
-
{task.runtime !== undefined && (
|
|
142
|
+
{/* With the run block below, the task's own status and start time
|
|
143
|
+
would repeat it — the run is the authority on how far the
|
|
144
|
+
workflow got. The header keeps only what identifies the task. */}
|
|
145
|
+
{!selectedRun && (
|
|
146
|
+
<>
|
|
147
|
+
<Box>
|
|
147
148
|
<Text>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
<Text color="blue">Status:</Text> {task.status}
|
|
150
|
+
{task.exitCode !== undefined &&
|
|
151
|
+
` (exit code: ${task.exitCode})`}
|
|
152
|
+
</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
<Box>
|
|
155
|
+
<Text>
|
|
156
|
+
<Text color="blue">Started:</Text>{" "}
|
|
157
|
+
{formatTime(task.startTime)}
|
|
158
|
+
{task.runtime !== undefined && (
|
|
159
|
+
<Text>
|
|
160
|
+
{" "}
|
|
161
|
+
| <Text color="blue">Runtime:</Text>{" "}
|
|
162
|
+
{formatDuration(task.runtime)}
|
|
163
|
+
</Text>
|
|
164
|
+
)}
|
|
151
165
|
</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
{task.outputPath && (
|
|
168
|
+
<Box>
|
|
169
|
+
<Text>
|
|
170
|
+
<Text color="blue">Log File:</Text> {task.outputPath}
|
|
171
|
+
</Text>
|
|
172
|
+
</Box>
|
|
152
173
|
)}
|
|
153
|
-
|
|
154
|
-
</Box>
|
|
155
|
-
{task.outputPath && (
|
|
156
|
-
<Box>
|
|
157
|
-
<Text>
|
|
158
|
-
<Text color="blue">Log File:</Text> {task.outputPath}
|
|
159
|
-
</Text>
|
|
160
|
-
</Box>
|
|
174
|
+
</>
|
|
161
175
|
)}
|
|
162
176
|
</Box>
|
|
163
177
|
|
|
164
|
-
{
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
{selectedRun ? (
|
|
179
|
+
<WorkflowRunDetail run={selectedRun} />
|
|
180
|
+
) : (
|
|
181
|
+
<>
|
|
182
|
+
{detailOutput.stdout && (
|
|
183
|
+
<Box flexDirection="column" marginTop={1}>
|
|
184
|
+
<Text color="green" bold>
|
|
185
|
+
OUTPUT (last 10 lines):
|
|
186
|
+
</Text>
|
|
187
|
+
<Box borderStyle="single" borderColor="green" padding={1}>
|
|
188
|
+
<Text>{getLastLines(detailOutput.stdout, 10)}</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
</Box>
|
|
191
|
+
)}
|
|
174
192
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
{detailOutput.stderr && (
|
|
194
|
+
<Box flexDirection="column" marginTop={1}>
|
|
195
|
+
<Text color="red" bold>
|
|
196
|
+
ERRORS:
|
|
197
|
+
</Text>
|
|
198
|
+
<Box borderStyle="single" borderColor="red" padding={1}>
|
|
199
|
+
<Text color="red">
|
|
200
|
+
{getLastLines(detailOutput.stderr, 10)}
|
|
201
|
+
</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
</Box>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
184
206
|
)}
|
|
185
207
|
|
|
186
208
|
<Box marginTop={1}>
|
|
@@ -37,10 +37,24 @@ const getActionDescription = (
|
|
|
37
37
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
38
38
|
return "Answer questions to clarify intent";
|
|
39
39
|
case ARTIFACT_TOOL_NAME:
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
switch (toolInput.action) {
|
|
41
|
+
case "list":
|
|
42
|
+
return `List artifacts (scope: ${toolInput.scope || "mine"})`;
|
|
43
|
+
case "upload_asset":
|
|
44
|
+
return `Upload asset: ${toolInput.file_path || "unknown file"}`;
|
|
45
|
+
case "list_assets":
|
|
46
|
+
return `List assets of artifact: ${toolInput.url || "unknown artifact"}`;
|
|
47
|
+
case "read_asset":
|
|
48
|
+
return `Read asset: ${toolInput.asset_id || "unknown asset"}`;
|
|
49
|
+
case "delete_asset":
|
|
50
|
+
return `Delete asset: ${toolInput.asset_id || "unknown asset"}`;
|
|
51
|
+
case "copy_from":
|
|
52
|
+
return `Copy assets into artifact: ${toolInput.url || "unknown artifact"}`;
|
|
53
|
+
case "read":
|
|
54
|
+
return `Read artifact: ${toolInput.url || "unknown artifact"}`;
|
|
55
|
+
default:
|
|
56
|
+
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
42
57
|
}
|
|
43
|
-
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
44
58
|
default:
|
|
45
59
|
return "Execute operation";
|
|
46
60
|
}
|
|
@@ -98,6 +98,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
98
98
|
compact,
|
|
99
99
|
addDir,
|
|
100
100
|
renameSession,
|
|
101
|
+
remoteControlCommand,
|
|
101
102
|
currentModel,
|
|
102
103
|
configuredModels,
|
|
103
104
|
setModel,
|
|
@@ -201,6 +202,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
201
202
|
onReloadPlugins: reloadPlugins,
|
|
202
203
|
onAddDir: addDir,
|
|
203
204
|
onRenameSession: renameSession,
|
|
205
|
+
onRemoteControlCommand: remoteControlCommand,
|
|
204
206
|
onHasSlashCommand: hasSlashCommand,
|
|
205
207
|
onAbortMessage: abortMessage,
|
|
206
208
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import type { Message, MessageBlock } from "wave-agent-sdk";
|
|
4
|
-
import { MessageSource } from "wave-agent-sdk";
|
|
5
4
|
import { ToolDisplay } from "./ToolDisplay.js";
|
|
6
5
|
import { CompactDisplay } from "./CompactDisplay.js";
|
|
7
6
|
import { ReasoningDisplay } from "./ReasoningDisplay.js";
|
|
@@ -25,11 +24,6 @@ export const MessageBlockItem = ({
|
|
|
25
24
|
<Box flexDirection="column" paddingTop={paddingTop}>
|
|
26
25
|
{block.type === "text" && block.content.trim() && (
|
|
27
26
|
<Box>
|
|
28
|
-
{block.source === MessageSource.HOOK && (
|
|
29
|
-
<Text color="magenta" bold>
|
|
30
|
-
~{" "}
|
|
31
|
-
</Text>
|
|
32
|
-
)}
|
|
33
27
|
{message.role === "user" || isExpanded ? (
|
|
34
28
|
<Text
|
|
35
29
|
backgroundColor={message.role === "user" ? "gray" : undefined}
|
|
@@ -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
|
-
<
|
|
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
|
-
|
|
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.
|
|
263
|
-
<Text color={
|
|
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
|
-
{
|
|
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>
|