@arvorco/relentless 0.1.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.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Runner
|
|
3
|
+
*
|
|
4
|
+
* Wraps the execution runner with a beautiful terminal interface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
8
|
+
import { render, useApp } from "ink";
|
|
9
|
+
import { App } from "./App.js";
|
|
10
|
+
import type { TUIState, Story, AgentState } from "./types.js";
|
|
11
|
+
import type { AgentName } from "../agents/types.js";
|
|
12
|
+
import type { PRD } from "../prd/types.js";
|
|
13
|
+
import type { RelentlessConfig } from "../config/schema.js";
|
|
14
|
+
import { getAgent, getInstalledAgents } from "../agents/registry.js";
|
|
15
|
+
import { loadPRD, getNextStory, isComplete, countStories } from "../prd/index.js";
|
|
16
|
+
import { routeStory } from "../execution/router.js";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
export interface TUIRunnerOptions {
|
|
20
|
+
agent: AgentName | "auto";
|
|
21
|
+
maxIterations: number;
|
|
22
|
+
workingDirectory: string;
|
|
23
|
+
prdPath: string;
|
|
24
|
+
promptPath: string;
|
|
25
|
+
feature: string;
|
|
26
|
+
config: RelentlessConfig;
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TUIRunnerProps extends TUIRunnerOptions {
|
|
31
|
+
onComplete: (success: boolean) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function TUIRunnerComponent({
|
|
35
|
+
agent: preferredAgent,
|
|
36
|
+
maxIterations,
|
|
37
|
+
workingDirectory,
|
|
38
|
+
prdPath,
|
|
39
|
+
promptPath,
|
|
40
|
+
feature,
|
|
41
|
+
config,
|
|
42
|
+
dryRun = false,
|
|
43
|
+
onComplete,
|
|
44
|
+
}: TUIRunnerProps): React.ReactElement {
|
|
45
|
+
const { exit } = useApp();
|
|
46
|
+
const [state, setState] = useState<TUIState>({
|
|
47
|
+
feature,
|
|
48
|
+
project: "",
|
|
49
|
+
branchName: "",
|
|
50
|
+
stories: [],
|
|
51
|
+
iteration: 0,
|
|
52
|
+
maxIterations,
|
|
53
|
+
currentStory: null,
|
|
54
|
+
currentAgent: null,
|
|
55
|
+
agents: [],
|
|
56
|
+
outputLines: [],
|
|
57
|
+
elapsedSeconds: 0,
|
|
58
|
+
isRunning: false,
|
|
59
|
+
isComplete: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Timer for elapsed time
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!state.isRunning) return;
|
|
65
|
+
|
|
66
|
+
const interval = setInterval(() => {
|
|
67
|
+
setState((prev) => ({
|
|
68
|
+
...prev,
|
|
69
|
+
elapsedSeconds: prev.elapsedSeconds + 1,
|
|
70
|
+
}));
|
|
71
|
+
}, 1000);
|
|
72
|
+
|
|
73
|
+
return () => clearInterval(interval);
|
|
74
|
+
}, [state.isRunning]);
|
|
75
|
+
|
|
76
|
+
// Add output line
|
|
77
|
+
const addOutput = useCallback((line: string) => {
|
|
78
|
+
setState((prev) => ({
|
|
79
|
+
...prev,
|
|
80
|
+
outputLines: [...prev.outputLines.slice(-100), line], // Keep last 100 lines
|
|
81
|
+
}));
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
// Main execution effect
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
let cancelled = false;
|
|
87
|
+
|
|
88
|
+
async function run() {
|
|
89
|
+
try {
|
|
90
|
+
// Load PRD
|
|
91
|
+
const prd = await loadPRD(prdPath);
|
|
92
|
+
|
|
93
|
+
// Get installed agents
|
|
94
|
+
const installed = await getInstalledAgents();
|
|
95
|
+
const agentStates: AgentState[] = installed.map((a) => ({
|
|
96
|
+
name: a.name,
|
|
97
|
+
displayName: a.displayName,
|
|
98
|
+
active: false,
|
|
99
|
+
rateLimited: false,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// Initial state
|
|
103
|
+
setState((prev) => ({
|
|
104
|
+
...prev,
|
|
105
|
+
project: prd.project,
|
|
106
|
+
branchName: prd.branchName,
|
|
107
|
+
stories: prd.userStories.map((s) => ({
|
|
108
|
+
id: s.id,
|
|
109
|
+
title: s.title,
|
|
110
|
+
passes: s.passes,
|
|
111
|
+
})),
|
|
112
|
+
agents: agentStates,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
addOutput(`Starting Relentless for feature: ${feature}`);
|
|
116
|
+
addOutput(`Project: ${prd.project}`);
|
|
117
|
+
addOutput(`Max iterations: ${maxIterations}`);
|
|
118
|
+
|
|
119
|
+
if (dryRun) {
|
|
120
|
+
addOutput("DRY RUN - not executing agents");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Track rate-limited agents
|
|
124
|
+
const limitedAgents = new Map<AgentName, { resetTime?: Date; detectedAt: Date }>();
|
|
125
|
+
|
|
126
|
+
// Main loop
|
|
127
|
+
for (let i = 1; i <= maxIterations && !cancelled; i++) {
|
|
128
|
+
setState((prev) => ({ ...prev, iteration: i }));
|
|
129
|
+
|
|
130
|
+
// Reload PRD
|
|
131
|
+
const currentPRD = await loadPRD(prdPath);
|
|
132
|
+
|
|
133
|
+
// Check completion
|
|
134
|
+
if (isComplete(currentPRD)) {
|
|
135
|
+
addOutput("All stories complete!");
|
|
136
|
+
setState((prev) => ({ ...prev, isComplete: true, isRunning: false }));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Get next story
|
|
141
|
+
const story = getNextStory(currentPRD);
|
|
142
|
+
if (!story) {
|
|
143
|
+
addOutput("No more stories to work on!");
|
|
144
|
+
setState((prev) => ({ ...prev, isComplete: true, isRunning: false }));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Update current story
|
|
149
|
+
setState((prev) => ({
|
|
150
|
+
...prev,
|
|
151
|
+
currentStory: { id: story.id, title: story.title, passes: story.passes },
|
|
152
|
+
elapsedSeconds: 0,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
addOutput(`\n--- Iteration ${i}/${maxIterations} ---`);
|
|
156
|
+
addOutput(`Story: ${story.id} - ${story.title}`);
|
|
157
|
+
|
|
158
|
+
// Select agent
|
|
159
|
+
let agentName: AgentName;
|
|
160
|
+
if (preferredAgent === "auto") {
|
|
161
|
+
agentName = routeStory(story, config.routing);
|
|
162
|
+
} else {
|
|
163
|
+
agentName = preferredAgent;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if agent is rate-limited
|
|
167
|
+
const limitState = limitedAgents.get(agentName);
|
|
168
|
+
if (limitState) {
|
|
169
|
+
const hasReset = limitState.resetTime
|
|
170
|
+
? new Date() >= limitState.resetTime
|
|
171
|
+
: new Date().getTime() - limitState.detectedAt.getTime() > 3600000;
|
|
172
|
+
|
|
173
|
+
if (hasReset) {
|
|
174
|
+
limitedAgents.delete(agentName);
|
|
175
|
+
} else {
|
|
176
|
+
// Try fallback
|
|
177
|
+
for (const fallbackName of config.fallback.priority) {
|
|
178
|
+
if (!limitedAgents.has(fallbackName)) {
|
|
179
|
+
const installed = agentStates.find((a) => a.name === fallbackName);
|
|
180
|
+
if (installed) {
|
|
181
|
+
agentName = fallbackName;
|
|
182
|
+
addOutput(`Switched to fallback agent: ${fallbackName}`);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const agent = getAgent(agentName);
|
|
191
|
+
|
|
192
|
+
// Update agent state
|
|
193
|
+
setState((prev) => ({
|
|
194
|
+
...prev,
|
|
195
|
+
currentAgent: {
|
|
196
|
+
name: agent.name,
|
|
197
|
+
displayName: agent.displayName,
|
|
198
|
+
active: true,
|
|
199
|
+
rateLimited: false,
|
|
200
|
+
},
|
|
201
|
+
agents: prev.agents.map((a) => ({
|
|
202
|
+
...a,
|
|
203
|
+
active: a.name === agent.name,
|
|
204
|
+
})),
|
|
205
|
+
isRunning: true,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
addOutput(`Agent: ${agent.displayName}`);
|
|
209
|
+
|
|
210
|
+
if (dryRun) {
|
|
211
|
+
addOutput("[Dry run - skipping execution]");
|
|
212
|
+
await sleep(1000);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Load prompt
|
|
217
|
+
const prompt = await Bun.file(promptPath).text();
|
|
218
|
+
|
|
219
|
+
// Invoke agent with streaming if available
|
|
220
|
+
if (agent.invokeStream) {
|
|
221
|
+
const stream = agent.invokeStream(prompt, {
|
|
222
|
+
workingDirectory,
|
|
223
|
+
dangerouslyAllowAll: config.agents[agent.name]?.dangerouslyAllowAll ?? true,
|
|
224
|
+
model: config.agents[agent.name]?.model,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let result;
|
|
228
|
+
for await (const chunk of stream) {
|
|
229
|
+
if (cancelled) break;
|
|
230
|
+
// Split chunk into lines and add each
|
|
231
|
+
const lines = chunk.split("\n");
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
if (line.trim()) {
|
|
234
|
+
addOutput(line);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
result = chunk; // Will be overwritten by return value
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Get the final result
|
|
241
|
+
const finalResult = await stream.next();
|
|
242
|
+
if (finalResult.done && finalResult.value) {
|
|
243
|
+
const agentResult = finalResult.value;
|
|
244
|
+
|
|
245
|
+
// Check rate limit
|
|
246
|
+
const rateLimit = agent.detectRateLimit(agentResult.output);
|
|
247
|
+
if (rateLimit.limited) {
|
|
248
|
+
addOutput(`⚠️ ${agent.displayName} rate limited!`);
|
|
249
|
+
limitedAgents.set(agent.name, {
|
|
250
|
+
resetTime: rateLimit.resetTime,
|
|
251
|
+
detectedAt: new Date(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
setState((prev) => ({
|
|
255
|
+
...prev,
|
|
256
|
+
agents: prev.agents.map((a) =>
|
|
257
|
+
a.name === agent.name
|
|
258
|
+
? { ...a, rateLimited: true, resetTime: rateLimit.resetTime }
|
|
259
|
+
: a
|
|
260
|
+
),
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
i--; // Retry iteration
|
|
264
|
+
await sleep(config.fallback.retryDelay);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (agentResult.isComplete) {
|
|
269
|
+
addOutput("🎉 Agent signaled COMPLETE!");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
addOutput(`Duration: ${(agentResult.duration / 1000).toFixed(1)}s`);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// Non-streaming fallback
|
|
276
|
+
addOutput("Running agent (non-streaming)...");
|
|
277
|
+
const result = await agent.invoke(prompt, {
|
|
278
|
+
workingDirectory,
|
|
279
|
+
dangerouslyAllowAll: config.agents[agent.name]?.dangerouslyAllowAll ?? true,
|
|
280
|
+
model: config.agents[agent.name]?.model,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Add output preview
|
|
284
|
+
const lines = result.output.split("\n").slice(0, 10);
|
|
285
|
+
for (const line of lines) {
|
|
286
|
+
if (line.trim()) {
|
|
287
|
+
addOutput(line);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (result.output.split("\n").length > 10) {
|
|
291
|
+
addOutput(`... (${result.output.split("\n").length - 10} more lines)`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check rate limit
|
|
295
|
+
const rateLimit = agent.detectRateLimit(result.output);
|
|
296
|
+
if (rateLimit.limited) {
|
|
297
|
+
addOutput(`⚠️ ${agent.displayName} rate limited!`);
|
|
298
|
+
limitedAgents.set(agent.name, {
|
|
299
|
+
resetTime: rateLimit.resetTime,
|
|
300
|
+
detectedAt: new Date(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
setState((prev) => ({
|
|
304
|
+
...prev,
|
|
305
|
+
agents: prev.agents.map((a) =>
|
|
306
|
+
a.name === agent.name
|
|
307
|
+
? { ...a, rateLimited: true, resetTime: rateLimit.resetTime }
|
|
308
|
+
: a
|
|
309
|
+
),
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
i--; // Retry iteration
|
|
313
|
+
await sleep(config.fallback.retryDelay);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (result.isComplete) {
|
|
318
|
+
addOutput("🎉 Agent signaled COMPLETE!");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
addOutput(`Duration: ${(result.duration / 1000).toFixed(1)}s`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Update stories from PRD
|
|
325
|
+
const updatedPRD = await loadPRD(prdPath);
|
|
326
|
+
setState((prev) => ({
|
|
327
|
+
...prev,
|
|
328
|
+
stories: updatedPRD.userStories.map((s) => ({
|
|
329
|
+
id: s.id,
|
|
330
|
+
title: s.title,
|
|
331
|
+
passes: s.passes,
|
|
332
|
+
})),
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
const counts = countStories(updatedPRD);
|
|
336
|
+
addOutput(`Progress: ${counts.completed}/${counts.total} complete`);
|
|
337
|
+
|
|
338
|
+
// Delay between iterations
|
|
339
|
+
await sleep(config.execution.iterationDelay);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Final state
|
|
343
|
+
const finalPRD = await loadPRD(prdPath);
|
|
344
|
+
const success = isComplete(finalPRD);
|
|
345
|
+
|
|
346
|
+
setState((prev) => ({
|
|
347
|
+
...prev,
|
|
348
|
+
isRunning: false,
|
|
349
|
+
isComplete: success,
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
if (!success) {
|
|
353
|
+
addOutput(`\n⚠️ Reached max iterations (${maxIterations}) without completing all stories.`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
onComplete(success);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
addOutput(`Error: ${error}`);
|
|
359
|
+
setState((prev) => ({
|
|
360
|
+
...prev,
|
|
361
|
+
isRunning: false,
|
|
362
|
+
error: String(error),
|
|
363
|
+
}));
|
|
364
|
+
onComplete(false);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
run();
|
|
369
|
+
|
|
370
|
+
return () => {
|
|
371
|
+
cancelled = true;
|
|
372
|
+
};
|
|
373
|
+
}, []);
|
|
374
|
+
|
|
375
|
+
return <App state={state} />;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function sleep(ms: number): Promise<void> {
|
|
379
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Run the TUI
|
|
384
|
+
*/
|
|
385
|
+
export async function runTUI(options: TUIRunnerOptions): Promise<boolean> {
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
const { unmount } = render(
|
|
388
|
+
<TUIRunnerComponent
|
|
389
|
+
{...options}
|
|
390
|
+
onComplete={(success) => {
|
|
391
|
+
// Wait a bit to show final state
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
unmount();
|
|
394
|
+
resolve(success);
|
|
395
|
+
}, 2000);
|
|
396
|
+
}}
|
|
397
|
+
/>
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentOutput Component
|
|
3
|
+
*
|
|
4
|
+
* Scrolling window showing agent output
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { colors, borders } from "../theme.js";
|
|
10
|
+
|
|
11
|
+
interface AgentOutputProps {
|
|
12
|
+
lines: string[];
|
|
13
|
+
maxLines?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AgentOutput({
|
|
17
|
+
lines,
|
|
18
|
+
maxLines = 8,
|
|
19
|
+
}: AgentOutputProps): React.ReactElement {
|
|
20
|
+
// Take last N lines for display
|
|
21
|
+
const displayLines = lines.slice(-maxLines);
|
|
22
|
+
|
|
23
|
+
// Pad to maintain consistent height
|
|
24
|
+
const paddedLines = [...displayLines];
|
|
25
|
+
while (paddedLines.length < maxLines) {
|
|
26
|
+
paddedLines.push("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection="column" borderStyle="single" borderColor={colors.dim}>
|
|
31
|
+
<Box paddingX={1} borderBottom borderColor={colors.dim}>
|
|
32
|
+
<Text color={colors.dim} bold>
|
|
33
|
+
Agent Output
|
|
34
|
+
</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
<Box flexDirection="column" paddingX={1} paddingY={0}>
|
|
37
|
+
{paddedLines.map((line, i) => (
|
|
38
|
+
<Text key={i} color={colors.dim} wrap="truncate">
|
|
39
|
+
{line || " "}
|
|
40
|
+
</Text>
|
|
41
|
+
))}
|
|
42
|
+
</Box>
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentStatus Component
|
|
3
|
+
*
|
|
4
|
+
* Footer showing agent availability and rate limit status
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { colors } from "../theme.js";
|
|
10
|
+
import type { AgentState } from "../types.js";
|
|
11
|
+
|
|
12
|
+
interface AgentStatusProps {
|
|
13
|
+
agents: AgentState[];
|
|
14
|
+
iteration: number;
|
|
15
|
+
maxIterations: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatResetTime(date: Date): string {
|
|
19
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AgentStatus({
|
|
23
|
+
agents,
|
|
24
|
+
iteration,
|
|
25
|
+
maxIterations,
|
|
26
|
+
}: AgentStatusProps): React.ReactElement {
|
|
27
|
+
// Find next reset time
|
|
28
|
+
const nextReset = agents
|
|
29
|
+
.filter((a) => a.rateLimited && a.resetTime)
|
|
30
|
+
.sort((a, b) => (a.resetTime?.getTime() ?? 0) - (b.resetTime?.getTime() ?? 0))[0];
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Box
|
|
34
|
+
borderStyle="single"
|
|
35
|
+
borderColor={colors.dim}
|
|
36
|
+
paddingX={1}
|
|
37
|
+
flexDirection="row"
|
|
38
|
+
justifyContent="space-between"
|
|
39
|
+
>
|
|
40
|
+
<Box>
|
|
41
|
+
<Text color={colors.dim}>Agents: </Text>
|
|
42
|
+
{agents.map((agent, i) => (
|
|
43
|
+
<React.Fragment key={agent.name}>
|
|
44
|
+
<Text color={colors.dim}>{agent.name} </Text>
|
|
45
|
+
<Text color={agent.active ? colors.success : agent.rateLimited ? colors.warning : colors.dim}>
|
|
46
|
+
{agent.active ? "●" : agent.rateLimited ? "○" : "○"}
|
|
47
|
+
</Text>
|
|
48
|
+
{i < agents.length - 1 && <Text color={colors.dim}> </Text>}
|
|
49
|
+
</React.Fragment>
|
|
50
|
+
))}
|
|
51
|
+
</Box>
|
|
52
|
+
<Box>
|
|
53
|
+
<Text color={colors.dim}>
|
|
54
|
+
Iteration: {iteration}/{maxIterations}
|
|
55
|
+
</Text>
|
|
56
|
+
{nextReset?.resetTime && (
|
|
57
|
+
<Text color={colors.dim}>
|
|
58
|
+
{" "}Next reset: {nextReset.name} @ {formatResetTime(nextReset.resetTime)}
|
|
59
|
+
</Text>
|
|
60
|
+
)}
|
|
61
|
+
</Box>
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CurrentStory Component
|
|
3
|
+
*
|
|
4
|
+
* Shows the current story being worked on with elapsed time
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import Spinner from "ink-spinner";
|
|
10
|
+
import { colors, symbols } from "../theme.js";
|
|
11
|
+
import type { Story } from "../types.js";
|
|
12
|
+
|
|
13
|
+
interface CurrentStoryProps {
|
|
14
|
+
story: Story | null;
|
|
15
|
+
elapsedSeconds: number;
|
|
16
|
+
isRunning: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatTime(seconds: number): string {
|
|
20
|
+
const mins = Math.floor(seconds / 60);
|
|
21
|
+
const secs = seconds % 60;
|
|
22
|
+
if (mins > 0) {
|
|
23
|
+
return `${mins}m ${secs}s`;
|
|
24
|
+
}
|
|
25
|
+
return `${secs}s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CurrentStory({
|
|
29
|
+
story,
|
|
30
|
+
elapsedSeconds,
|
|
31
|
+
isRunning,
|
|
32
|
+
}: CurrentStoryProps): React.ReactElement {
|
|
33
|
+
if (!story) {
|
|
34
|
+
return (
|
|
35
|
+
<Box paddingY={1}>
|
|
36
|
+
<Text color={colors.dim}>No story in progress</Text>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Box flexDirection="column" paddingY={1}>
|
|
43
|
+
<Box>
|
|
44
|
+
<Text color={colors.dim}>Current Story: </Text>
|
|
45
|
+
<Text color={colors.warning} bold>
|
|
46
|
+
{story.id}
|
|
47
|
+
</Text>
|
|
48
|
+
<Text color={colors.dim}> - </Text>
|
|
49
|
+
<Text>{story.title}</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
<Box>
|
|
52
|
+
{isRunning ? (
|
|
53
|
+
<>
|
|
54
|
+
<Text color={colors.success}>
|
|
55
|
+
<Spinner type="dots" />
|
|
56
|
+
</Text>
|
|
57
|
+
<Text color={colors.success}> Working...</Text>
|
|
58
|
+
</>
|
|
59
|
+
) : (
|
|
60
|
+
<Text color={colors.dim}>{symbols.pending} Waiting</Text>
|
|
61
|
+
)}
|
|
62
|
+
<Text color={colors.dim}> [elapsed: {formatTime(elapsedSeconds)}]</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header Component
|
|
3
|
+
*
|
|
4
|
+
* Displays title and current agent status
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { symbols, colors } from "../theme.js";
|
|
10
|
+
import type { AgentState } from "../types.js";
|
|
11
|
+
|
|
12
|
+
interface HeaderProps {
|
|
13
|
+
agent: AgentState | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Header({ agent }: HeaderProps): React.ReactElement {
|
|
17
|
+
return (
|
|
18
|
+
<Box
|
|
19
|
+
borderStyle="single"
|
|
20
|
+
borderColor={colors.primary}
|
|
21
|
+
paddingX={1}
|
|
22
|
+
flexDirection="row"
|
|
23
|
+
justifyContent="space-between"
|
|
24
|
+
>
|
|
25
|
+
<Box>
|
|
26
|
+
<Text color={colors.primary} bold>
|
|
27
|
+
{symbols.lightning} RELENTLESS
|
|
28
|
+
</Text>
|
|
29
|
+
<Text color={colors.dim}> Universal AI Agent Orchestrator</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
<Box>
|
|
32
|
+
<Text color={colors.dim}>Agent: </Text>
|
|
33
|
+
{agent ? (
|
|
34
|
+
<>
|
|
35
|
+
<Text color={agent.rateLimited ? colors.warning : colors.success} bold>
|
|
36
|
+
{agent.displayName}
|
|
37
|
+
</Text>
|
|
38
|
+
<Text color={colors.dim}>
|
|
39
|
+
{" "}
|
|
40
|
+
({agent.rateLimited ? "limited" : "active"})
|
|
41
|
+
</Text>
|
|
42
|
+
</>
|
|
43
|
+
) : (
|
|
44
|
+
<Text color={colors.dim}>none</Text>
|
|
45
|
+
)}
|
|
46
|
+
</Box>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar Component
|
|
3
|
+
*
|
|
4
|
+
* Visual progress bar for story completion
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { colors } from "../theme.js";
|
|
10
|
+
|
|
11
|
+
interface ProgressBarProps {
|
|
12
|
+
completed: number;
|
|
13
|
+
total: number;
|
|
14
|
+
width?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ProgressBar({
|
|
18
|
+
completed,
|
|
19
|
+
total,
|
|
20
|
+
width = 40,
|
|
21
|
+
}: ProgressBarProps): React.ReactElement {
|
|
22
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
23
|
+
const filledWidth = total > 0 ? Math.round((completed / total) * width) : 0;
|
|
24
|
+
const emptyWidth = width - filledWidth;
|
|
25
|
+
|
|
26
|
+
const filled = "█".repeat(filledWidth);
|
|
27
|
+
const empty = "░".repeat(emptyWidth);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box>
|
|
31
|
+
<Text color={colors.success}>{filled}</Text>
|
|
32
|
+
<Text color={colors.dim}>{empty}</Text>
|
|
33
|
+
<Text color={colors.dim}>
|
|
34
|
+
{" "}
|
|
35
|
+
{completed}/{total} ({percentage}%)
|
|
36
|
+
</Text>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|