wave-code 0.17.0 → 0.17.2
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/acp/agent.d.ts +1 -0
- package/dist/acp/agent.d.ts.map +1 -1
- package/dist/acp/agent.js +116 -1
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +5 -2
- package/dist/components/InputBox.d.ts +2 -0
- package/dist/components/InputBox.d.ts.map +1 -1
- package/dist/components/InputBox.js +29 -8
- package/dist/components/LoadingIndicator.d.ts +2 -1
- package/dist/components/LoadingIndicator.d.ts.map +1 -1
- package/dist/components/LoadingIndicator.js +2 -2
- package/dist/components/QueuedMessageList.d.ts.map +1 -1
- package/dist/components/QueuedMessageList.js +10 -10
- package/dist/components/StatusLine.d.ts +2 -0
- package/dist/components/StatusLine.d.ts.map +1 -1
- package/dist/components/StatusLine.js +6 -6
- package/dist/components/TaskList.d.ts +3 -0
- package/dist/components/TaskList.d.ts.map +1 -1
- package/dist/components/TaskList.js +171 -28
- package/dist/components/TaskNotificationMessage.d.ts.map +1 -1
- package/dist/components/TaskNotificationMessage.js +1 -0
- package/dist/components/WorkflowManager.d.ts +6 -0
- package/dist/components/WorkflowManager.d.ts.map +1 -0
- package/dist/components/WorkflowManager.js +102 -0
- package/dist/constants/commands.d.ts.map +1 -1
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +8 -1
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +68 -4
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +22 -13
- package/dist/managers/inputHandlers.d.ts.map +1 -1
- package/dist/managers/inputHandlers.js +6 -3
- package/dist/managers/inputReducer.d.ts +10 -0
- package/dist/managers/inputReducer.d.ts.map +1 -1
- package/dist/managers/inputReducer.js +41 -4
- package/dist/print-cli.d.ts.map +1 -1
- package/dist/print-cli.js +3 -31
- package/dist/reducers/workflowManagerReducer.d.ts +41 -0
- package/dist/reducers/workflowManagerReducer.d.ts.map +1 -0
- package/dist/reducers/workflowManagerReducer.js +100 -0
- package/package.json +2 -2
- package/src/acp/agent.ts +137 -1
- package/src/components/ChatInterface.tsx +10 -1
- package/src/components/InputBox.tsx +43 -3
- package/src/components/LoadingIndicator.tsx +4 -1
- package/src/components/QueuedMessageList.tsx +7 -3
- package/src/components/StatusLine.tsx +28 -16
- package/src/components/TaskList.tsx +213 -19
- package/src/components/TaskNotificationMessage.tsx +1 -0
- package/src/components/WorkflowManager.tsx +320 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +83 -4
- package/src/hooks/useInputManager.ts +25 -13
- package/src/managers/inputHandlers.ts +5 -4
- package/src/managers/inputReducer.ts +54 -6
- package/src/print-cli.ts +3 -37
- package/src/reducers/workflowManagerReducer.ts +143 -0
|
@@ -1,37 +1,226 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useChat } from "../contexts/useChat.js";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
3
|
+
import { Box, Text, useStdout } from "ink";
|
|
4
4
|
import { useTasks } from "../hooks/useTasks.js";
|
|
5
|
+
import type { Task, TaskStatus } from "wave-agent-sdk";
|
|
6
|
+
|
|
7
|
+
const RECENTLY_COMPLETED_TTL_MS = 30_000;
|
|
8
|
+
const AUTO_HIDE_DELAY_MS = 5_000;
|
|
9
|
+
|
|
10
|
+
export const getStatusIcon = (status: TaskStatus): React.ReactNode => {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "pending":
|
|
13
|
+
return <Text color="gray">□</Text>;
|
|
14
|
+
case "in_progress":
|
|
15
|
+
return <Text color="yellow">■</Text>;
|
|
16
|
+
case "completed":
|
|
17
|
+
return <Text color="green">✓</Text>;
|
|
18
|
+
case "deleted":
|
|
19
|
+
return <Text color="red">✕</Text>;
|
|
20
|
+
default:
|
|
21
|
+
return <Text color="gray">?</Text>;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function byIdAsc(a: Task, b: Task): number {
|
|
26
|
+
const aNum = parseInt(a.id, 10);
|
|
27
|
+
const bNum = parseInt(b.id, 10);
|
|
28
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
29
|
+
return aNum - bNum;
|
|
30
|
+
}
|
|
31
|
+
return a.id.localeCompare(b.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sortTasksByPriority(
|
|
35
|
+
tasks: Task[],
|
|
36
|
+
completionTimestamps: Map<string, number>,
|
|
37
|
+
now: number,
|
|
38
|
+
needsTruncation: boolean,
|
|
39
|
+
): Task[] {
|
|
40
|
+
if (!needsTruncation) {
|
|
41
|
+
// No truncation needed — sort by ID for stable ordering
|
|
42
|
+
return [...tasks].sort(byIdAsc);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// When truncation is needed, prioritize:
|
|
46
|
+
// recently completed > in_progress > pending (unblocked first) > older completed > deleted
|
|
47
|
+
const recentCompleted: Task[] = [];
|
|
48
|
+
const olderCompleted: Task[] = [];
|
|
49
|
+
for (const task of tasks.filter((t) => t.status === "completed")) {
|
|
50
|
+
const ts = completionTimestamps.get(task.id);
|
|
51
|
+
if (ts && now - ts < RECENTLY_COMPLETED_TTL_MS) {
|
|
52
|
+
recentCompleted.push(task);
|
|
53
|
+
} else {
|
|
54
|
+
olderCompleted.push(task);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
recentCompleted.sort(byIdAsc);
|
|
58
|
+
olderCompleted.sort(byIdAsc);
|
|
59
|
+
|
|
60
|
+
const inProgress = tasks
|
|
61
|
+
.filter((t) => t.status === "in_progress")
|
|
62
|
+
.sort(byIdAsc);
|
|
63
|
+
|
|
64
|
+
const unresolvedTaskIds = new Set(
|
|
65
|
+
tasks.filter((t) => t.status !== "completed").map((t) => t.id),
|
|
66
|
+
);
|
|
67
|
+
const pending = tasks
|
|
68
|
+
.filter((t) => t.status === "pending")
|
|
69
|
+
.sort((a, b) => {
|
|
70
|
+
const aBlocked = a.blockedBy.some((id) => unresolvedTaskIds.has(id));
|
|
71
|
+
const bBlocked = b.blockedBy.some((id) => unresolvedTaskIds.has(id));
|
|
72
|
+
if (aBlocked !== bBlocked) return aBlocked ? 1 : -1;
|
|
73
|
+
return byIdAsc(a, b);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const deleted = tasks.filter((t) => t.status === "deleted").sort(byIdAsc);
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
...recentCompleted,
|
|
80
|
+
...inProgress,
|
|
81
|
+
...pending,
|
|
82
|
+
...olderCompleted,
|
|
83
|
+
...deleted,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getDisplayLimit(rows: number | undefined): number {
|
|
88
|
+
const available = rows ?? 24;
|
|
89
|
+
return available <= 10 ? 0 : Math.min(10, Math.max(3, available - 14));
|
|
90
|
+
}
|
|
5
91
|
|
|
6
92
|
export const TaskList: React.FC = () => {
|
|
7
93
|
const tasks = useTasks();
|
|
8
94
|
const { isTaskListVisible } = useChat();
|
|
95
|
+
const { stdout } = useStdout();
|
|
9
96
|
|
|
10
|
-
|
|
11
|
-
|
|
97
|
+
const completionTimestampsRef = React.useRef<Map<string, number>>(new Map());
|
|
98
|
+
const previousCompletedIdsRef = React.useRef<Set<string> | null>(null);
|
|
99
|
+
const autoHideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
|
|
100
|
+
null,
|
|
101
|
+
);
|
|
102
|
+
const [autoHidden, setAutoHidden] = React.useState(() => {
|
|
103
|
+
// If all tasks are already completed on mount (e.g. session restore),
|
|
104
|
+
// start hidden immediately instead of flashing for 5 seconds.
|
|
105
|
+
const active = tasks.filter((t) => t.status !== "deleted");
|
|
106
|
+
return active.length > 0 && active.every((t) => t.status === "completed");
|
|
107
|
+
});
|
|
108
|
+
const [, forceUpdate] = React.useState(0);
|
|
109
|
+
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const activeTasks = tasks.filter((t) => t.status !== "deleted");
|
|
112
|
+
|
|
113
|
+
// Track completion timestamps: only set when a task newly transitions to completed
|
|
114
|
+
if (previousCompletedIdsRef.current === null) {
|
|
115
|
+
previousCompletedIdsRef.current = new Set(
|
|
116
|
+
tasks.filter((t) => t.status === "completed").map((t) => t.id),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const currentCompletedIds = new Set(
|
|
120
|
+
tasks.filter((t) => t.status === "completed").map((t) => t.id),
|
|
121
|
+
);
|
|
122
|
+
for (const id of currentCompletedIds) {
|
|
123
|
+
if (!previousCompletedIdsRef.current.has(id)) {
|
|
124
|
+
completionTimestampsRef.current.set(id, now);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const id of completionTimestampsRef.current.keys()) {
|
|
128
|
+
if (!currentCompletedIds.has(id)) {
|
|
129
|
+
completionTimestampsRef.current.delete(id);
|
|
130
|
+
}
|
|
12
131
|
}
|
|
132
|
+
previousCompletedIdsRef.current = currentCompletedIds;
|
|
133
|
+
|
|
134
|
+
// Schedule re-render when the next recent completion expires
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (completionTimestampsRef.current.size === 0) return;
|
|
137
|
+
const currentNow = Date.now();
|
|
138
|
+
let earliestExpiry = Infinity;
|
|
139
|
+
for (const ts of completionTimestampsRef.current.values()) {
|
|
140
|
+
const expiry = ts + RECENTLY_COMPLETED_TTL_MS;
|
|
141
|
+
if (expiry > currentNow && expiry < earliestExpiry) {
|
|
142
|
+
earliestExpiry = expiry;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (earliestExpiry === Infinity) return;
|
|
146
|
+
const timer = setTimeout(
|
|
147
|
+
() => forceUpdate((n) => n + 1),
|
|
148
|
+
earliestExpiry - currentNow,
|
|
149
|
+
);
|
|
150
|
+
return () => clearTimeout(timer);
|
|
151
|
+
}, [tasks]);
|
|
13
152
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
153
|
+
// Auto-hide logic: when all active tasks are completed
|
|
154
|
+
const allCompleted =
|
|
155
|
+
activeTasks.length > 0 &&
|
|
156
|
+
activeTasks.every((t) => t.status === "completed");
|
|
157
|
+
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (allCompleted && !autoHidden) {
|
|
160
|
+
autoHideTimerRef.current = setTimeout(() => {
|
|
161
|
+
setAutoHidden(true);
|
|
162
|
+
}, AUTO_HIDE_DELAY_MS);
|
|
163
|
+
}
|
|
164
|
+
if (!allCompleted && autoHidden) {
|
|
165
|
+
setAutoHidden(false);
|
|
26
166
|
}
|
|
27
|
-
|
|
167
|
+
return () => {
|
|
168
|
+
if (autoHideTimerRef.current) {
|
|
169
|
+
clearTimeout(autoHideTimerRef.current);
|
|
170
|
+
autoHideTimerRef.current = null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}, [allCompleted, autoHidden]);
|
|
174
|
+
|
|
175
|
+
if (tasks.length === 0 || !isTaskListVisible || autoHidden) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const displayLimit = getDisplayLimit(stdout?.rows);
|
|
180
|
+
const needsTruncation = tasks.length > displayLimit;
|
|
181
|
+
const sorted = sortTasksByPriority(
|
|
182
|
+
tasks,
|
|
183
|
+
completionTimestampsRef.current,
|
|
184
|
+
now,
|
|
185
|
+
needsTruncation,
|
|
186
|
+
);
|
|
187
|
+
const displayed = displayLimit > 0 ? sorted.slice(0, displayLimit) : [];
|
|
188
|
+
const hidden = displayLimit > 0 ? sorted.slice(displayLimit) : sorted;
|
|
189
|
+
|
|
190
|
+
const doneCount = tasks.filter((t) => t.status === "completed").length;
|
|
191
|
+
const inProgressCount = tasks.filter(
|
|
192
|
+
(t) => t.status === "in_progress",
|
|
193
|
+
).length;
|
|
194
|
+
const openCount = tasks.filter((t) => t.status === "pending").length;
|
|
195
|
+
|
|
196
|
+
// Summary parts for hidden tasks
|
|
197
|
+
const hiddenInProgress = hidden.filter(
|
|
198
|
+
(t) => t.status === "in_progress",
|
|
199
|
+
).length;
|
|
200
|
+
const hiddenPending = hidden.filter((t) => t.status === "pending").length;
|
|
201
|
+
const hiddenCompleted = hidden.filter((t) => t.status === "completed").length;
|
|
202
|
+
|
|
203
|
+
const summaryParts: string[] = [];
|
|
204
|
+
if (hiddenInProgress > 0)
|
|
205
|
+
summaryParts.push(`${hiddenInProgress} in progress`);
|
|
206
|
+
if (hiddenPending > 0) summaryParts.push(`${hiddenPending} pending`);
|
|
207
|
+
if (hiddenCompleted > 0) summaryParts.push(`${hiddenCompleted} completed`);
|
|
28
208
|
|
|
29
209
|
return (
|
|
30
210
|
<Box flexDirection="column">
|
|
31
|
-
|
|
211
|
+
<Text dimColor>
|
|
212
|
+
{tasks.length} tasks ({doneCount} done, {inProgressCount} in progress,{" "}
|
|
213
|
+
{openCount} open)
|
|
214
|
+
</Text>
|
|
215
|
+
{displayed.map((task) => {
|
|
32
216
|
const isDimmed =
|
|
33
217
|
task.status === "completed" || task.status === "deleted";
|
|
34
|
-
const
|
|
218
|
+
const unresolvedTaskIds = new Set(
|
|
219
|
+
tasks.filter((t) => t.status !== "completed").map((t) => t.id),
|
|
220
|
+
);
|
|
221
|
+
const isBlocked =
|
|
222
|
+
task.blockedBy &&
|
|
223
|
+
task.blockedBy.some((id) => unresolvedTaskIds.has(id));
|
|
35
224
|
const blockingTaskIds = isBlocked
|
|
36
225
|
? task.blockedBy.map((id) => `#${id}`)
|
|
37
226
|
: [];
|
|
@@ -46,10 +235,15 @@ export const TaskList: React.FC = () => {
|
|
|
46
235
|
return (
|
|
47
236
|
<Box key={task.id} gap={1}>
|
|
48
237
|
{getStatusIcon(task.status)}
|
|
49
|
-
<Text dimColor={isDimmed}>
|
|
238
|
+
<Text dimColor={isDimmed} wrap="truncate-end">
|
|
239
|
+
{fullText}
|
|
240
|
+
</Text>
|
|
50
241
|
</Box>
|
|
51
242
|
);
|
|
52
243
|
})}
|
|
244
|
+
{displayLimit > 0 && hidden.length > 0 && (
|
|
245
|
+
<Text dimColor> … +{summaryParts.join(", ")}</Text>
|
|
246
|
+
)}
|
|
53
247
|
</Box>
|
|
54
248
|
);
|
|
55
249
|
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import React, { useEffect, useReducer } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useChat } from "../contexts/useChat.js";
|
|
4
|
+
import type { WorkflowRun } from "wave-agent-sdk";
|
|
5
|
+
import {
|
|
6
|
+
workflowManagerReducer,
|
|
7
|
+
type WorkflowManagerState,
|
|
8
|
+
} from "../reducers/workflowManagerReducer.js";
|
|
9
|
+
|
|
10
|
+
export interface WorkflowManagerProps {
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
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
|
+
export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
48
|
+
onCancel,
|
|
49
|
+
}) => {
|
|
50
|
+
const { workflowRuns, stopWorkflowRun } = useChat();
|
|
51
|
+
const MAX_VISIBLE_ITEMS = 3;
|
|
52
|
+
|
|
53
|
+
const [state, dispatch] = useReducer(workflowManagerReducer, {
|
|
54
|
+
runs: [],
|
|
55
|
+
selectedIndex: 0,
|
|
56
|
+
viewMode: "list",
|
|
57
|
+
detailRunId: null,
|
|
58
|
+
pendingEffect: null,
|
|
59
|
+
} as WorkflowManagerState);
|
|
60
|
+
|
|
61
|
+
// Handle pending effects
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!state.pendingEffect) return;
|
|
64
|
+
|
|
65
|
+
const effect = state.pendingEffect;
|
|
66
|
+
dispatch({ type: "CLEAR_PENDING_EFFECT" });
|
|
67
|
+
|
|
68
|
+
switch (effect.type) {
|
|
69
|
+
case "CANCEL":
|
|
70
|
+
onCancel();
|
|
71
|
+
break;
|
|
72
|
+
case "STOP_RUN":
|
|
73
|
+
stopWorkflowRun(effect.runId);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}, [state.pendingEffect, onCancel, stopWorkflowRun]);
|
|
77
|
+
|
|
78
|
+
const { runs, selectedIndex, viewMode, detailRunId } = state;
|
|
79
|
+
|
|
80
|
+
// Sync workflowRuns from context
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
dispatch({ type: "SET_RUNS", runs: workflowRuns });
|
|
83
|
+
}, [workflowRuns]);
|
|
84
|
+
|
|
85
|
+
useInput((input, key) => {
|
|
86
|
+
dispatch({ type: "HANDLE_KEY", input, key });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Detail view
|
|
90
|
+
if (viewMode === "detail" && detailRunId) {
|
|
91
|
+
const run = runs.find((r) => r.runId === detailRunId);
|
|
92
|
+
if (!run) {
|
|
93
|
+
dispatch({ type: "RESET_DETAIL" });
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const elapsed = run.endTime
|
|
98
|
+
? run.endTime - run.startTime
|
|
99
|
+
: Date.now() - run.startTime;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Box
|
|
103
|
+
flexDirection="column"
|
|
104
|
+
borderStyle="single"
|
|
105
|
+
borderColor="cyan"
|
|
106
|
+
borderBottom={false}
|
|
107
|
+
borderLeft={false}
|
|
108
|
+
borderRight={false}
|
|
109
|
+
paddingTop={1}
|
|
110
|
+
gap={1}
|
|
111
|
+
>
|
|
112
|
+
<Box>
|
|
113
|
+
<Text color="cyan" bold>
|
|
114
|
+
Workflow Details: {run.meta.name}
|
|
115
|
+
</Text>
|
|
116
|
+
</Box>
|
|
117
|
+
|
|
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
|
+
)}
|
|
187
|
+
|
|
188
|
+
<Box marginTop={1}>
|
|
189
|
+
<Text dimColor>
|
|
190
|
+
{run.status === "running" ? "k to stop · " : ""}Esc back
|
|
191
|
+
</Text>
|
|
192
|
+
</Box>
|
|
193
|
+
</Box>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Empty state
|
|
198
|
+
if (runs.length === 0) {
|
|
199
|
+
return (
|
|
200
|
+
<Box
|
|
201
|
+
flexDirection="column"
|
|
202
|
+
borderStyle="single"
|
|
203
|
+
borderColor="cyan"
|
|
204
|
+
borderBottom={false}
|
|
205
|
+
borderLeft={false}
|
|
206
|
+
borderRight={false}
|
|
207
|
+
paddingTop={1}
|
|
208
|
+
>
|
|
209
|
+
<Text color="cyan" bold>
|
|
210
|
+
Workflows
|
|
211
|
+
</Text>
|
|
212
|
+
<Text>No workflow runs found</Text>
|
|
213
|
+
<Text dimColor>Press Escape to close</Text>
|
|
214
|
+
</Box>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// List view
|
|
219
|
+
const startIndex = Math.max(
|
|
220
|
+
0,
|
|
221
|
+
Math.min(
|
|
222
|
+
selectedIndex - Math.floor(MAX_VISIBLE_ITEMS / 2),
|
|
223
|
+
Math.max(0, runs.length - MAX_VISIBLE_ITEMS),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
const visibleRuns = runs.slice(startIndex, startIndex + MAX_VISIBLE_ITEMS);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<Box
|
|
230
|
+
flexDirection="column"
|
|
231
|
+
borderStyle="single"
|
|
232
|
+
borderColor="cyan"
|
|
233
|
+
borderBottom={false}
|
|
234
|
+
borderLeft={false}
|
|
235
|
+
borderRight={false}
|
|
236
|
+
paddingTop={1}
|
|
237
|
+
gap={1}
|
|
238
|
+
>
|
|
239
|
+
<Box>
|
|
240
|
+
<Text color="cyan" bold>
|
|
241
|
+
Workflows
|
|
242
|
+
</Text>
|
|
243
|
+
</Box>
|
|
244
|
+
<Text dimColor>Select a run to view details</Text>
|
|
245
|
+
|
|
246
|
+
<Box flexDirection="column">
|
|
247
|
+
{visibleRuns.map((run, index) => {
|
|
248
|
+
const actualIndex = startIndex + index;
|
|
249
|
+
const isSelected = actualIndex === selectedIndex;
|
|
250
|
+
const elapsed = run.endTime
|
|
251
|
+
? run.endTime - run.startTime
|
|
252
|
+
: Date.now() - run.startTime;
|
|
253
|
+
const phaseText = run.phases.map((p) => p.title).join(" → ");
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<Box key={run.runId} flexDirection="column">
|
|
257
|
+
<Text
|
|
258
|
+
color={isSelected ? "black" : "white"}
|
|
259
|
+
backgroundColor={isSelected ? "cyan" : undefined}
|
|
260
|
+
>
|
|
261
|
+
{isSelected ? "▶ " : " "}
|
|
262
|
+
{actualIndex + 1}. {run.runId.slice(0, 8)} {run.meta.name}
|
|
263
|
+
<Text color={statusColor(run.status)}> ({run.status})</Text>
|
|
264
|
+
</Text>
|
|
265
|
+
<Text
|
|
266
|
+
color={isSelected ? "black" : "gray"}
|
|
267
|
+
backgroundColor={isSelected ? "cyan" : undefined}
|
|
268
|
+
>
|
|
269
|
+
{" "}
|
|
270
|
+
{run.totalAgents} agents · {formatTokens(run.totalTokens)}{" "}
|
|
271
|
+
tokens · {formatDuration(elapsed)}
|
|
272
|
+
</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
|
+
{isSelected && (
|
|
300
|
+
<Box marginLeft={4} flexDirection="column">
|
|
301
|
+
<Text color="gray" dimColor>
|
|
302
|
+
<Text color="blue">Script:</Text> {run.scriptPath}
|
|
303
|
+
</Text>
|
|
304
|
+
</Box>
|
|
305
|
+
)}
|
|
306
|
+
</Box>
|
|
307
|
+
);
|
|
308
|
+
})}
|
|
309
|
+
</Box>
|
|
310
|
+
|
|
311
|
+
<Box marginTop={1}>
|
|
312
|
+
<Text dimColor>
|
|
313
|
+
↑/↓ select · Enter detail ·{" "}
|
|
314
|
+
{runs[selectedIndex]?.status === "running" ? "k stop · " : ""}Esc
|
|
315
|
+
close
|
|
316
|
+
</Text>
|
|
317
|
+
</Box>
|
|
318
|
+
</Box>
|
|
319
|
+
);
|
|
320
|
+
};
|
|
@@ -62,4 +62,10 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
|
62
62
|
description: "Clear SSO authentication",
|
|
63
63
|
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
id: "workflows",
|
|
67
|
+
name: "workflows",
|
|
68
|
+
description: "View and manage workflow runs",
|
|
69
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
70
|
+
},
|
|
65
71
|
];
|