@arvorco/relentless 0.6.1 → 0.8.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/.claude/commands/relentless.learn.md +52 -0
- package/.claude/skills/checklist/SKILL.md +9 -2
- package/.claude/skills/constitution/SKILL.md +11 -0
- package/.claude/skills/constitution/templates/constitution.md +0 -3
- package/.claude/skills/learn/SKILL.md +361 -0
- package/.claude/skills/learn/scripts/common.sh +46 -0
- package/.claude/skills/learn/scripts/extract-costs.sh +48 -0
- package/.claude/skills/learn/scripts/extract-errors.sh +47 -0
- package/.claude/skills/learn/scripts/extract-failures.sh +47 -0
- package/.claude/skills/learn/scripts/extract-learnings.sh +66 -0
- package/.claude/skills/learn/scripts/extract-patterns.sh +72 -0
- package/.claude/skills/learn/scripts/extract-session-context.sh +77 -0
- package/.claude/skills/learn/scripts/generate-stats.sh +129 -0
- package/.claude/skills/learn/templates/learning-proposal.md +105 -0
- package/.claude/skills/learn/templates/stats.md +69 -0
- package/.claude/skills/plan/SKILL.md +10 -3
- package/.claude/skills/specify/SKILL.md +10 -4
- package/.claude/skills/tasks/SKILL.md +10 -3
- package/.claude/skills/validators/SKILL.md +174 -0
- package/.claude/skills/validators/scripts/common.sh +182 -0
- package/.claude/skills/validators/scripts/validate-checklist.sh +190 -0
- package/.claude/skills/validators/scripts/validate-constitution.sh +111 -0
- package/.claude/skills/validators/scripts/validate-plan.sh +163 -0
- package/.claude/skills/validators/scripts/validate-prompt.sh +125 -0
- package/.claude/skills/validators/scripts/validate-spec.sh +214 -0
- package/.claude/skills/validators/scripts/validate-tasks.sh +179 -0
- package/.claude/skills/validators/scripts/validate.sh +295 -0
- package/CHANGELOG.md +112 -1
- package/package.json +1 -1
- package/relentless/constitution.md +0 -3
- package/src/agents/amp.ts +3 -6
- package/src/agents/claude.ts +4 -7
- package/src/agents/codex.ts +3 -6
- package/src/agents/droid.ts +3 -6
- package/src/agents/exec.ts +56 -10
- package/src/agents/gemini.ts +3 -6
- package/src/agents/opencode.ts +3 -6
- package/src/agents/types.ts +2 -0
- package/src/config/schema.ts +2 -2
- package/src/execution/runner.ts +4 -0
- package/src/init/scaffolder.ts +2 -0
- package/src/tui/App.tsx +132 -16
- package/src/tui/TUIRunner.tsx +68 -4
- package/src/tui/components/CostBadge.tsx +59 -0
- package/src/tui/components/MessageItem.tsx +113 -0
- package/src/tui/components/MessageQueuePanel.tsx +126 -0
- package/src/tui/components/OutputPanel.tsx +270 -0
- package/src/tui/components/QueueInput.tsx +28 -11
- package/src/tui/components/RateLimitIndicator.tsx +97 -0
- package/src/tui/components/StatusBar.tsx +188 -0
- package/src/tui/components/TaskItem.tsx +131 -0
- package/src/tui/components/TaskPanel.tsx +189 -0
- package/src/tui/components/TokenCounter.tsx +48 -0
- package/src/tui/hooks/useAnimation.ts +220 -0
- package/src/tui/hooks/useCostTracking.ts +199 -0
- package/src/tui/hooks/useResponsiveLayout.ts +94 -0
- package/src/tui/hooks/useTUI.ts +57 -1
- package/src/tui/index.tsx +24 -0
- package/src/tui/layouts/LayoutSwitcher.tsx +95 -0
- package/src/tui/layouts/ThreeColumnLayout.tsx +97 -0
- package/src/tui/layouts/VerticalLayout.tsx +69 -0
- package/src/tui/layouts/index.ts +9 -0
- package/src/tui/theme.ts +152 -21
- package/src/tui/types.ts +95 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageQueuePanel Component
|
|
3
|
+
*
|
|
4
|
+
* mIRC-style message queue panel for the right column
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { colors, borders, symbols } from "../theme.js";
|
|
10
|
+
import { MessageItem } from "./MessageItem.js";
|
|
11
|
+
import type { MessageItem as MessageItemType } from "../types.js";
|
|
12
|
+
import type { QueueItem } from "../../queue/types.js";
|
|
13
|
+
|
|
14
|
+
interface MessageQueuePanelProps {
|
|
15
|
+
/** Messages to display (mIRC-style) */
|
|
16
|
+
messages: MessageItemType[];
|
|
17
|
+
/** Queue items (pending commands) */
|
|
18
|
+
queueItems: QueueItem[];
|
|
19
|
+
/** Maximum messages to show */
|
|
20
|
+
maxMessages?: number;
|
|
21
|
+
/** Maximum queue items to show */
|
|
22
|
+
maxQueueItems?: number;
|
|
23
|
+
/** Panel title */
|
|
24
|
+
title?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format queue item for display
|
|
29
|
+
*/
|
|
30
|
+
function formatQueueItem(item: QueueItem, index: number): string {
|
|
31
|
+
return `${index + 1}. ${item.content}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function MessageQueuePanel({
|
|
35
|
+
messages,
|
|
36
|
+
queueItems,
|
|
37
|
+
maxMessages = 10,
|
|
38
|
+
maxQueueItems = 3,
|
|
39
|
+
title = "Messages",
|
|
40
|
+
}: MessageQueuePanelProps): React.ReactElement {
|
|
41
|
+
// Get visible messages (most recent)
|
|
42
|
+
const visibleMessages = messages.slice(-maxMessages);
|
|
43
|
+
const hasMoreMessages = messages.length > maxMessages;
|
|
44
|
+
|
|
45
|
+
// Get visible queue items
|
|
46
|
+
const visibleQueueItems = queueItems.slice(0, maxQueueItems);
|
|
47
|
+
const hasMoreQueueItems = queueItems.length > maxQueueItems;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Box flexDirection="column" paddingX={1} width="100%">
|
|
51
|
+
{/* Messages section */}
|
|
52
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
53
|
+
<Box>
|
|
54
|
+
<Text color={colors.accent} bold>
|
|
55
|
+
{title}
|
|
56
|
+
</Text>
|
|
57
|
+
{messages.length > 0 && (
|
|
58
|
+
<Text color={colors.dim}> ({messages.length})</Text>
|
|
59
|
+
)}
|
|
60
|
+
</Box>
|
|
61
|
+
|
|
62
|
+
{/* Scroll indicator */}
|
|
63
|
+
{hasMoreMessages && (
|
|
64
|
+
<Text color={colors.dim}>
|
|
65
|
+
{symbols.priority.high} {messages.length - maxMessages} more
|
|
66
|
+
</Text>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{/* Messages list */}
|
|
70
|
+
<Box flexDirection="column">
|
|
71
|
+
{visibleMessages.length > 0 ? (
|
|
72
|
+
visibleMessages.map((msg) => (
|
|
73
|
+
<MessageItem key={msg.id} message={msg} maxWidth={25} />
|
|
74
|
+
))
|
|
75
|
+
) : (
|
|
76
|
+
<Text color={colors.dim} dimColor>
|
|
77
|
+
No messages yet
|
|
78
|
+
</Text>
|
|
79
|
+
)}
|
|
80
|
+
</Box>
|
|
81
|
+
</Box>
|
|
82
|
+
|
|
83
|
+
{/* Queue section */}
|
|
84
|
+
<Box flexDirection="column">
|
|
85
|
+
<Box>
|
|
86
|
+
<Text color={colors.dim} bold>
|
|
87
|
+
{borders.horizontal}
|
|
88
|
+
{borders.horizontal} Queue {borders.horizontal}
|
|
89
|
+
</Text>
|
|
90
|
+
{queueItems.length > 0 && (
|
|
91
|
+
<Text color={colors.warning}> ({queueItems.length})</Text>
|
|
92
|
+
)}
|
|
93
|
+
</Box>
|
|
94
|
+
|
|
95
|
+
{/* Queue items */}
|
|
96
|
+
<Box flexDirection="column">
|
|
97
|
+
{visibleQueueItems.length > 0 ? (
|
|
98
|
+
visibleQueueItems.map((item, i) => (
|
|
99
|
+
<Text key={item.addedAt} color={colors.dim} wrap="truncate">
|
|
100
|
+
{formatQueueItem(item, i)}
|
|
101
|
+
</Text>
|
|
102
|
+
))
|
|
103
|
+
) : (
|
|
104
|
+
<Text color={colors.dim} dimColor>
|
|
105
|
+
Empty
|
|
106
|
+
</Text>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{/* More items indicator */}
|
|
110
|
+
{hasMoreQueueItems && (
|
|
111
|
+
<Text color={colors.dim}>
|
|
112
|
+
+{queueItems.length - maxQueueItems} more
|
|
113
|
+
</Text>
|
|
114
|
+
)}
|
|
115
|
+
</Box>
|
|
116
|
+
|
|
117
|
+
{/* Help text */}
|
|
118
|
+
<Box marginTop={1}>
|
|
119
|
+
<Text color={colors.dim} dimColor>
|
|
120
|
+
Items sent to agent each iteration
|
|
121
|
+
</Text>
|
|
122
|
+
</Box>
|
|
123
|
+
</Box>
|
|
124
|
+
</Box>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutputPanel Component
|
|
3
|
+
*
|
|
4
|
+
* Enhanced central output panel with context header,
|
|
5
|
+
* code block detection, and fullscreen support
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { Box, Text } from "ink";
|
|
10
|
+
import { colors, borders } from "../theme.js";
|
|
11
|
+
import type { Story, OutputMode } from "../types.js";
|
|
12
|
+
import type { AgentName } from "../../agents/types.js";
|
|
13
|
+
|
|
14
|
+
interface OutputPanelProps {
|
|
15
|
+
/** Output lines to display */
|
|
16
|
+
lines: string[];
|
|
17
|
+
/** Maximum lines to show */
|
|
18
|
+
maxLines?: number;
|
|
19
|
+
/** Current story being worked on */
|
|
20
|
+
currentStory?: Story | null;
|
|
21
|
+
/** Current agent name */
|
|
22
|
+
currentAgent?: string;
|
|
23
|
+
/** Current model being used */
|
|
24
|
+
currentModel?: string;
|
|
25
|
+
/** Routing information */
|
|
26
|
+
routing?: {
|
|
27
|
+
mode: "free" | "cheap" | "good" | "genius";
|
|
28
|
+
complexity: "simple" | "medium" | "complex" | "expert";
|
|
29
|
+
harness: AgentName;
|
|
30
|
+
model: string;
|
|
31
|
+
};
|
|
32
|
+
/** Display mode (normal or fullscreen) */
|
|
33
|
+
displayMode?: OutputMode;
|
|
34
|
+
/** Panel title */
|
|
35
|
+
title?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ParsedLine {
|
|
39
|
+
text: string;
|
|
40
|
+
type: "normal" | "code" | "code-start" | "code-end" | "header" | "success" | "error" | "warning";
|
|
41
|
+
language?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse output lines to detect code blocks and special formatting
|
|
46
|
+
*/
|
|
47
|
+
function parseLines(lines: string[]): ParsedLine[] {
|
|
48
|
+
const parsed: ParsedLine[] = [];
|
|
49
|
+
let inCodeBlock = false;
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
// Check for code block markers
|
|
53
|
+
if (line.startsWith("```")) {
|
|
54
|
+
if (inCodeBlock) {
|
|
55
|
+
parsed.push({ text: line, type: "code-end" });
|
|
56
|
+
inCodeBlock = false;
|
|
57
|
+
} else {
|
|
58
|
+
const language = line.substring(3).trim();
|
|
59
|
+
parsed.push({ text: line, type: "code-start", language });
|
|
60
|
+
inCodeBlock = true;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Inside code block
|
|
66
|
+
if (inCodeBlock) {
|
|
67
|
+
parsed.push({ text: line, type: "code" });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for special line types
|
|
72
|
+
if (line.startsWith("---") || line.startsWith("===")) {
|
|
73
|
+
parsed.push({ text: line, type: "header" });
|
|
74
|
+
} else if (
|
|
75
|
+
line.includes("success") ||
|
|
76
|
+
line.includes("complete") ||
|
|
77
|
+
line.includes("\u2713") ||
|
|
78
|
+
line.includes("\uD83C\uDF89")
|
|
79
|
+
) {
|
|
80
|
+
parsed.push({ text: line, type: "success" });
|
|
81
|
+
} else if (
|
|
82
|
+
line.includes("error") ||
|
|
83
|
+
line.includes("Error") ||
|
|
84
|
+
line.includes("failed") ||
|
|
85
|
+
line.includes("\u274C")
|
|
86
|
+
) {
|
|
87
|
+
parsed.push({ text: line, type: "error" });
|
|
88
|
+
} else if (
|
|
89
|
+
line.includes("warning") ||
|
|
90
|
+
line.includes("Warning") ||
|
|
91
|
+
line.includes("\u26A0")
|
|
92
|
+
) {
|
|
93
|
+
parsed.push({ text: line, type: "warning" });
|
|
94
|
+
} else {
|
|
95
|
+
parsed.push({ text: line, type: "normal" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get color for a parsed line type
|
|
104
|
+
*/
|
|
105
|
+
function getLineColor(type: ParsedLine["type"]): string {
|
|
106
|
+
switch (type) {
|
|
107
|
+
case "code":
|
|
108
|
+
case "code-start":
|
|
109
|
+
case "code-end":
|
|
110
|
+
return colors.accent;
|
|
111
|
+
case "header":
|
|
112
|
+
return colors.primary;
|
|
113
|
+
case "success":
|
|
114
|
+
return colors.success;
|
|
115
|
+
case "error":
|
|
116
|
+
return colors.error;
|
|
117
|
+
case "warning":
|
|
118
|
+
return colors.warning;
|
|
119
|
+
default:
|
|
120
|
+
return colors.dim;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Format mode badge text
|
|
126
|
+
*/
|
|
127
|
+
function getModeBadge(mode: string): { text: string; color: string } {
|
|
128
|
+
switch (mode) {
|
|
129
|
+
case "free":
|
|
130
|
+
return { text: "FREE", color: colors.success };
|
|
131
|
+
case "cheap":
|
|
132
|
+
return { text: "CHEAP", color: colors.primary };
|
|
133
|
+
case "good":
|
|
134
|
+
return { text: "GOOD", color: colors.warning };
|
|
135
|
+
case "genius":
|
|
136
|
+
return { text: "GENIUS", color: colors.accent };
|
|
137
|
+
default:
|
|
138
|
+
return { text: mode.toUpperCase(), color: colors.dim };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Format complexity badge
|
|
144
|
+
*/
|
|
145
|
+
function getComplexityBadge(complexity: string): { text: string; color: string } {
|
|
146
|
+
switch (complexity) {
|
|
147
|
+
case "simple":
|
|
148
|
+
return { text: "S", color: colors.success };
|
|
149
|
+
case "medium":
|
|
150
|
+
return { text: "M", color: colors.warning };
|
|
151
|
+
case "complex":
|
|
152
|
+
return { text: "C", color: colors.error };
|
|
153
|
+
case "expert":
|
|
154
|
+
return { text: "E", color: colors.accent };
|
|
155
|
+
default:
|
|
156
|
+
return { text: complexity[0]?.toUpperCase() ?? "?", color: colors.dim };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function OutputPanel({
|
|
161
|
+
lines,
|
|
162
|
+
maxLines = 12,
|
|
163
|
+
currentStory,
|
|
164
|
+
currentAgent,
|
|
165
|
+
currentModel,
|
|
166
|
+
routing,
|
|
167
|
+
displayMode = "normal",
|
|
168
|
+
title = "Output",
|
|
169
|
+
}: OutputPanelProps): React.ReactElement {
|
|
170
|
+
// Parse lines for syntax highlighting
|
|
171
|
+
const parsedLines = useMemo(() => parseLines(lines), [lines]);
|
|
172
|
+
|
|
173
|
+
// Get visible lines based on max
|
|
174
|
+
const clampedMaxLines = Math.max(0, maxLines);
|
|
175
|
+
const displayLines =
|
|
176
|
+
clampedMaxLines > 0 ? parsedLines.slice(-clampedMaxLines) : [];
|
|
177
|
+
|
|
178
|
+
// Build context header
|
|
179
|
+
const hasContext = currentStory || currentAgent || routing;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Box flexDirection="column" paddingX={1} width="100%">
|
|
183
|
+
{/* Context header */}
|
|
184
|
+
{hasContext && (
|
|
185
|
+
<Box flexDirection="row" marginBottom={1}>
|
|
186
|
+
{/* Story ID */}
|
|
187
|
+
{currentStory && (
|
|
188
|
+
<>
|
|
189
|
+
<Text color={colors.warning} bold>
|
|
190
|
+
{currentStory.id}
|
|
191
|
+
</Text>
|
|
192
|
+
<Text color={colors.dim}> {borders.vertical} </Text>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Agent/Model */}
|
|
197
|
+
{(currentAgent || currentModel) && (
|
|
198
|
+
<>
|
|
199
|
+
<Text color={colors.primary}>
|
|
200
|
+
{currentAgent ?? ""}
|
|
201
|
+
{currentModel ? `/${currentModel}` : ""}
|
|
202
|
+
</Text>
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Routing info */}
|
|
207
|
+
{routing && (
|
|
208
|
+
<>
|
|
209
|
+
<Text color={colors.dim}> {borders.vertical} </Text>
|
|
210
|
+
<Text color={getModeBadge(routing.mode).color}>
|
|
211
|
+
{getModeBadge(routing.mode).text}
|
|
212
|
+
</Text>
|
|
213
|
+
<Text color={colors.dim}>/</Text>
|
|
214
|
+
<Text color={getComplexityBadge(routing.complexity).color}>
|
|
215
|
+
{getComplexityBadge(routing.complexity).text}
|
|
216
|
+
</Text>
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Fullscreen indicator */}
|
|
221
|
+
{displayMode === "fullscreen" && (
|
|
222
|
+
<>
|
|
223
|
+
<Text color={colors.dim}> {borders.vertical} </Text>
|
|
224
|
+
<Text color={colors.accent}>[F] Fullscreen</Text>
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
</Box>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Title bar */}
|
|
231
|
+
<Box>
|
|
232
|
+
<Text color={colors.dim} bold>
|
|
233
|
+
{borders.horizontal}
|
|
234
|
+
{borders.horizontal} {title} {borders.horizontal}
|
|
235
|
+
{borders.horizontal}
|
|
236
|
+
</Text>
|
|
237
|
+
</Box>
|
|
238
|
+
|
|
239
|
+
{/* Output content */}
|
|
240
|
+
<Box flexDirection="column">
|
|
241
|
+
{displayLines.length > 0 ? (
|
|
242
|
+
displayLines.map((line, i) => (
|
|
243
|
+
<Text
|
|
244
|
+
key={i}
|
|
245
|
+
color={getLineColor(line.type)}
|
|
246
|
+
dimColor={line.type === "normal"}
|
|
247
|
+
wrap="truncate"
|
|
248
|
+
>
|
|
249
|
+
{line.text}
|
|
250
|
+
</Text>
|
|
251
|
+
))
|
|
252
|
+
) : (
|
|
253
|
+
<Text color={colors.dim} dimColor>
|
|
254
|
+
Waiting for agent output...
|
|
255
|
+
</Text>
|
|
256
|
+
)}
|
|
257
|
+
</Box>
|
|
258
|
+
|
|
259
|
+
{/* Auto-scroll indicator */}
|
|
260
|
+
{lines.length > clampedMaxLines && (
|
|
261
|
+
<Box marginTop={1}>
|
|
262
|
+
<Text color={colors.dim}>
|
|
263
|
+
{borders.vertical} {lines.length - clampedMaxLines} lines above{" "}
|
|
264
|
+
{borders.vertical}
|
|
265
|
+
</Text>
|
|
266
|
+
</Box>
|
|
267
|
+
)}
|
|
268
|
+
</Box>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
@@ -138,23 +138,40 @@ interface QueueInputProps {
|
|
|
138
138
|
/**
|
|
139
139
|
* Queue Input Component
|
|
140
140
|
*
|
|
141
|
-
* Displays
|
|
142
|
-
*
|
|
141
|
+
* Displays a full-width input field at the bottom of the TUI for adding items to the queue.
|
|
142
|
+
* Styled like Claude Code's input area with horizontal borders.
|
|
143
143
|
*/
|
|
144
144
|
export function QueueInput({ active, value }: QueueInputProps): React.ReactElement | null {
|
|
145
145
|
if (!active) {
|
|
146
|
-
|
|
146
|
+
// Show hint when not active
|
|
147
|
+
return (
|
|
148
|
+
<Box flexDirection="column" width="100%">
|
|
149
|
+
<Box width="100%">
|
|
150
|
+
<Text color={colors.dim}>{"─".repeat(80)}</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
<Box paddingX={1}>
|
|
153
|
+
<Text color={colors.dim}>Press 'q' to add to queue, 'd' to delete, 'D' to clear</Text>
|
|
154
|
+
</Box>
|
|
155
|
+
<Box width="100%">
|
|
156
|
+
<Text color={colors.dim}>{"─".repeat(80)}</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
</Box>
|
|
159
|
+
);
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
const formatted = formatQueueInput(value);
|
|
150
|
-
|
|
151
162
|
return (
|
|
152
|
-
<Box
|
|
153
|
-
<
|
|
154
|
-
{
|
|
155
|
-
</
|
|
156
|
-
<
|
|
157
|
-
|
|
163
|
+
<Box flexDirection="column" width="100%">
|
|
164
|
+
<Box width="100%">
|
|
165
|
+
<Text color={colors.accent}>{"─".repeat(80)}</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
<Box paddingX={1} width="100%">
|
|
168
|
+
<Text color={colors.accent} bold>❯ </Text>
|
|
169
|
+
<Text>{value}</Text>
|
|
170
|
+
<Text color={colors.accent}>█</Text>
|
|
171
|
+
</Box>
|
|
172
|
+
<Box width="100%">
|
|
173
|
+
<Text color={colors.accent}>{"─".repeat(80)}</Text>
|
|
174
|
+
</Box>
|
|
158
175
|
</Box>
|
|
159
176
|
);
|
|
160
177
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RateLimitIndicator Component
|
|
3
|
+
*
|
|
4
|
+
* Rate limit countdown display for agents
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { colors, symbols } from "../theme.js";
|
|
10
|
+
import { useCountdown } from "../hooks/useAnimation.js";
|
|
11
|
+
import type { AgentState } from "../types.js";
|
|
12
|
+
|
|
13
|
+
interface RateLimitIndicatorProps {
|
|
14
|
+
/** All agent states */
|
|
15
|
+
agents: AgentState[];
|
|
16
|
+
/** Whether to show compact view */
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
/** Maximum agents to show */
|
|
19
|
+
maxAgents?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AgentCountdownProps {
|
|
23
|
+
agent: AgentState;
|
|
24
|
+
compact: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function AgentCountdown({ agent, compact }: AgentCountdownProps): React.ReactElement | null {
|
|
28
|
+
const countdown = useCountdown(agent.resetTime, agent.rateLimited);
|
|
29
|
+
|
|
30
|
+
if (!agent.rateLimited || !countdown) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (compact) {
|
|
35
|
+
return (
|
|
36
|
+
<Text color={colors.status.rateLimited}>
|
|
37
|
+
{agent.name.substring(0, 3)}: {countdown.display}
|
|
38
|
+
</Text>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box>
|
|
44
|
+
<Text color={colors.status.rateLimited}>
|
|
45
|
+
{symbols.clock} {agent.displayName}: {countdown.display}
|
|
46
|
+
</Text>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function RateLimitIndicator({
|
|
52
|
+
agents,
|
|
53
|
+
compact = false,
|
|
54
|
+
maxAgents = 3,
|
|
55
|
+
}: RateLimitIndicatorProps): React.ReactElement | null {
|
|
56
|
+
// Filter to rate-limited agents only
|
|
57
|
+
const limitedAgents = agents.filter((a) => a.rateLimited);
|
|
58
|
+
|
|
59
|
+
if (limitedAgents.length === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const visibleAgents = limitedAgents.slice(0, maxAgents);
|
|
64
|
+
const hasMore = limitedAgents.length > maxAgents;
|
|
65
|
+
|
|
66
|
+
if (compact) {
|
|
67
|
+
return (
|
|
68
|
+
<Box>
|
|
69
|
+
{visibleAgents.map((agent, i) => (
|
|
70
|
+
<React.Fragment key={agent.name}>
|
|
71
|
+
{i > 0 && <Text color={colors.dim}> </Text>}
|
|
72
|
+
<AgentCountdown agent={agent} compact />
|
|
73
|
+
</React.Fragment>
|
|
74
|
+
))}
|
|
75
|
+
{hasMore && (
|
|
76
|
+
<Text color={colors.dim}> +{limitedAgents.length - maxAgents}</Text>
|
|
77
|
+
)}
|
|
78
|
+
</Box>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Box flexDirection="column">
|
|
84
|
+
<Text color={colors.status.rateLimited} bold>
|
|
85
|
+
Rate Limited:
|
|
86
|
+
</Text>
|
|
87
|
+
{visibleAgents.map((agent) => (
|
|
88
|
+
<AgentCountdown key={agent.name} agent={agent} compact={false} />
|
|
89
|
+
))}
|
|
90
|
+
{hasMore && (
|
|
91
|
+
<Text color={colors.dim}>
|
|
92
|
+
+{limitedAgents.length - maxAgents} more
|
|
93
|
+
</Text>
|
|
94
|
+
)}
|
|
95
|
+
</Box>
|
|
96
|
+
);
|
|
97
|
+
}
|