wave-code 0.17.1 → 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.
Files changed (57) hide show
  1. package/dist/acp/agent.js +1 -1
  2. package/dist/components/ChatInterface.d.ts.map +1 -1
  3. package/dist/components/ChatInterface.js +5 -2
  4. package/dist/components/InputBox.d.ts +2 -0
  5. package/dist/components/InputBox.d.ts.map +1 -1
  6. package/dist/components/InputBox.js +29 -8
  7. package/dist/components/LoadingIndicator.d.ts +2 -1
  8. package/dist/components/LoadingIndicator.d.ts.map +1 -1
  9. package/dist/components/LoadingIndicator.js +2 -2
  10. package/dist/components/QueuedMessageList.d.ts.map +1 -1
  11. package/dist/components/QueuedMessageList.js +10 -10
  12. package/dist/components/StatusLine.d.ts +2 -0
  13. package/dist/components/StatusLine.d.ts.map +1 -1
  14. package/dist/components/StatusLine.js +6 -6
  15. package/dist/components/TaskList.d.ts +3 -0
  16. package/dist/components/TaskList.d.ts.map +1 -1
  17. package/dist/components/TaskList.js +171 -28
  18. package/dist/components/TaskNotificationMessage.d.ts.map +1 -1
  19. package/dist/components/TaskNotificationMessage.js +1 -0
  20. package/dist/components/WorkflowManager.d.ts +6 -0
  21. package/dist/components/WorkflowManager.d.ts.map +1 -0
  22. package/dist/components/WorkflowManager.js +102 -0
  23. package/dist/constants/commands.d.ts.map +1 -1
  24. package/dist/constants/commands.js +6 -0
  25. package/dist/contexts/useChat.d.ts +8 -1
  26. package/dist/contexts/useChat.d.ts.map +1 -1
  27. package/dist/contexts/useChat.js +68 -4
  28. package/dist/hooks/useInputManager.d.ts +2 -0
  29. package/dist/hooks/useInputManager.d.ts.map +1 -1
  30. package/dist/hooks/useInputManager.js +22 -13
  31. package/dist/managers/inputHandlers.d.ts.map +1 -1
  32. package/dist/managers/inputHandlers.js +6 -3
  33. package/dist/managers/inputReducer.d.ts +10 -0
  34. package/dist/managers/inputReducer.d.ts.map +1 -1
  35. package/dist/managers/inputReducer.js +41 -4
  36. package/dist/print-cli.d.ts.map +1 -1
  37. package/dist/print-cli.js +3 -31
  38. package/dist/reducers/workflowManagerReducer.d.ts +41 -0
  39. package/dist/reducers/workflowManagerReducer.d.ts.map +1 -0
  40. package/dist/reducers/workflowManagerReducer.js +100 -0
  41. package/package.json +2 -2
  42. package/src/acp/agent.ts +1 -1
  43. package/src/components/ChatInterface.tsx +10 -1
  44. package/src/components/InputBox.tsx +43 -3
  45. package/src/components/LoadingIndicator.tsx +4 -1
  46. package/src/components/QueuedMessageList.tsx +7 -3
  47. package/src/components/StatusLine.tsx +28 -16
  48. package/src/components/TaskList.tsx +213 -19
  49. package/src/components/TaskNotificationMessage.tsx +1 -0
  50. package/src/components/WorkflowManager.tsx +320 -0
  51. package/src/constants/commands.ts +6 -0
  52. package/src/contexts/useChat.tsx +83 -4
  53. package/src/hooks/useInputManager.ts +25 -13
  54. package/src/managers/inputHandlers.ts +5 -4
  55. package/src/managers/inputReducer.ts +54 -6
  56. package/src/print-cli.ts +3 -37
  57. 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
- if (tasks.length === 0 || !isTaskListVisible) {
11
- return null;
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
- const getStatusIcon = (status: string) => {
15
- switch (status) {
16
- case "pending":
17
- return <Text color="gray">□</Text>;
18
- case "in_progress":
19
- return <Text color="yellow">■</Text>;
20
- case "completed":
21
- return <Text color="green">✓</Text>;
22
- case "deleted":
23
- return <Text color="red">✕</Text>;
24
- default:
25
- return <Text color="gray">?</Text>;
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
- {tasks.map((task) => {
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 isBlocked = task.blockedBy && task.blockedBy.length > 0;
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}>{fullText}</Text>
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
  };
@@ -10,6 +10,7 @@ const statusColor: Record<TaskNotificationBlock["status"], string> = {
10
10
  completed: "green",
11
11
  failed: "red",
12
12
  killed: "yellow",
13
+ aborted: "gray",
13
14
  };
14
15
 
15
16
  export const TaskNotificationMessage = ({
@@ -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
  ];