@arvorco/relentless 0.1.17 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvorco/relentless",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Universal AI agent orchestrator - works with Claude Code, Amp, OpenCode, Codex, Droid, and Gemini",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/tui/App.tsx CHANGED
@@ -21,38 +21,22 @@ interface AppProps {
21
21
 
22
22
  export function App({ state }: AppProps): React.ReactElement {
23
23
  const { stdout } = useStdout();
24
- const [terminalRows, setTerminalRows] = React.useState(() => stdout.rows ?? 24);
25
-
26
- React.useEffect(() => {
27
- if (!stdout.isTTY) return;
28
-
29
- const handleResize = () => {
30
- setTerminalRows(stdout.rows ?? 24);
31
- };
32
-
33
- stdout.on("resize", handleResize);
34
- return () => {
35
- stdout.off("resize", handleResize);
36
- };
37
- }, [stdout]);
38
-
24
+ const terminalRows = stdout.rows ?? 24;
25
+
39
26
  const completedCount = state.stories.filter((s) => s.passes).length;
40
27
  const totalCount = state.stories.length;
41
-
42
- // Prevent Ink from overflowing terminal height.
43
- // Total height = agentOutputLines + storyGridRows + 18 (with compact layout below).
44
- const contentBudget = Math.max(0, terminalRows - 18);
45
-
46
- const minAgentLines = contentBudget > 0 ? Math.min(3, contentBudget) : 0;
47
- const maxAgentLines = Math.min(10, contentBudget);
48
- let agentOutputLines = Math.floor(contentBudget * 0.35);
49
- agentOutputLines = Math.max(minAgentLines, Math.min(maxAgentLines, agentOutputLines));
50
-
51
- let storyGridRows = contentBudget - agentOutputLines;
52
- if (totalCount > 0 && storyGridRows === 0 && contentBudget >= 2) {
53
- agentOutputLines -= 1;
54
- storyGridRows = 1;
55
- }
28
+
29
+ // Calculate available rows for stories based on terminal height
30
+ // Chrome: Header(2) + Feature/Progress(2) + CurrentStory(2) + AgentOutputHeader(1) + AgentStatusFooter(2) + Padding(2) = ~11 lines
31
+ // AgentOutput: 6 lines
32
+ // Remaining space for stories
33
+ const chromeHeight = 11;
34
+ const agentOutputLines = 6;
35
+ const availableForStories = Math.max(8, terminalRows - chromeHeight - agentOutputLines);
36
+
37
+ // Calculate story rows needed for 2-column layout
38
+ const storyRows = Math.ceil(totalCount / 2);
39
+ const maxStoryRows = Math.min(storyRows, availableForStories);
56
40
 
57
41
  return (
58
42
  <Box flexDirection="column" width="100%">
@@ -87,7 +71,7 @@ export function App({ state }: AppProps): React.ReactElement {
87
71
  <StoryGrid
88
72
  stories={state.stories}
89
73
  currentStoryId={state.currentStory?.id}
90
- maxRows={storyGridRows}
74
+ maxRows={maxStoryRows}
91
75
  />
92
76
 
93
77
  {/* Agent status footer */}
@@ -113,6 +113,10 @@ function TUIRunnerComponent({
113
113
  id: s.id,
114
114
  title: s.title,
115
115
  passes: s.passes,
116
+ priority: s.priority,
117
+ criteriaCount: s.acceptanceCriteria.length,
118
+ research: s.research,
119
+ phase: s.phase,
116
120
  })),
117
121
  agents: agentStates,
118
122
  }));
@@ -153,7 +157,15 @@ function TUIRunnerComponent({
153
157
  // Update current story
154
158
  setState((prev) => ({
155
159
  ...prev,
156
- currentStory: { id: story.id, title: story.title, passes: story.passes },
160
+ currentStory: {
161
+ id: story.id,
162
+ title: story.title,
163
+ passes: story.passes,
164
+ priority: story.priority,
165
+ criteriaCount: story.acceptanceCriteria.length,
166
+ research: story.research,
167
+ phase: story.phase,
168
+ },
157
169
  elapsedSeconds: 0,
158
170
  }));
159
171
 
@@ -334,6 +346,10 @@ function TUIRunnerComponent({
334
346
  id: s.id,
335
347
  title: s.title,
336
348
  passes: s.passes,
349
+ priority: s.priority,
350
+ criteriaCount: s.acceptanceCriteria.length,
351
+ research: s.research,
352
+ phase: s.phase,
337
353
  })),
338
354
  }));
339
355
 
@@ -21,27 +21,25 @@ export function AgentOutput({
21
21
  const displayLines = clampedMaxLines > 0 ? lines.slice(-clampedMaxLines) : [];
22
22
 
23
23
  return (
24
- <Box flexDirection="column" borderStyle="single" borderColor={colors.dim}>
25
- <Box paddingX={1} borderBottom borderColor={colors.dim}>
24
+ <Box flexDirection="column" paddingY={1}>
25
+ <Box paddingX={1}>
26
26
  <Text color={colors.dim} bold>
27
- Agent Output
27
+ ── Agent Output ──
28
28
  </Text>
29
29
  </Box>
30
- {clampedMaxLines > 0 && (
31
- <Box flexDirection="column" paddingX={1} height={clampedMaxLines}>
32
- {displayLines.length > 0 ? (
33
- displayLines.map((line, i) => (
34
- <Text key={i} color={colors.dim} wrap="truncate">
35
- {line}
36
- </Text>
37
- ))
38
- ) : (
39
- <Text color={colors.dim} dimColor>
40
- Waiting for agent output...
30
+ <Box flexDirection="column" paddingX={1}>
31
+ {displayLines.length > 0 ? (
32
+ displayLines.slice(0, clampedMaxLines).map((line, i) => (
33
+ <Text key={i} color={colors.dim} wrap="truncate">
34
+ {line}
41
35
  </Text>
42
- )}
43
- </Box>
44
- )}
36
+ ))
37
+ ) : (
38
+ <Text color={colors.dim} dimColor>
39
+ Waiting for agent output...
40
+ </Text>
41
+ )}
42
+ </Box>
45
43
  </Box>
46
44
  );
47
45
  }
@@ -31,9 +31,8 @@ export function AgentStatus({
31
31
 
32
32
  return (
33
33
  <Box
34
- borderStyle="single"
35
- borderColor={colors.dim}
36
34
  paddingX={1}
35
+ paddingY={1}
37
36
  flexDirection="row"
38
37
  justifyContent="space-between"
39
38
  >
@@ -16,9 +16,8 @@ interface HeaderProps {
16
16
  export function Header({ agent }: HeaderProps): React.ReactElement {
17
17
  return (
18
18
  <Box
19
- borderStyle="single"
20
- borderColor={colors.primary}
21
19
  paddingX={1}
20
+ paddingY={1}
22
21
  flexDirection="row"
23
22
  justifyContent="space-between"
24
23
  >
@@ -37,43 +37,47 @@ export function StoryGrid({
37
37
  rows.push(row);
38
38
  }
39
39
 
40
- // Constrain rows to prevent TUI from overflowing terminal height.
41
- // When constrained, keep the current story visible by windowing around its row.
42
- const totalRows = rows.length;
43
- const clampedMaxRows = maxRows === undefined ? undefined : Math.max(0, maxRows);
44
-
40
+ // Window around current story to show context
45
41
  let visibleRows = rows;
46
42
  let startRow = 0;
43
+ let endRow = rows.length;
47
44
 
48
- if (clampedMaxRows !== undefined && totalRows > clampedMaxRows) {
45
+ if (maxRows && rows.length > maxRows) {
46
+ // Find the row containing the current story
49
47
  const currentRowIdx = currentStoryId
50
- ? rows.findIndex((r) => r.some((s) => s.id === currentStoryId))
48
+ ? rows.findIndex((row) => row.some((story) => story.id === currentStoryId))
51
49
  : -1;
52
50
 
53
- const windowSize = clampedMaxRows;
54
- const half = Math.floor(windowSize / 2);
55
-
56
- startRow = currentRowIdx >= 0 ? Math.max(0, currentRowIdx - half) : 0;
57
- if (startRow + windowSize > totalRows) {
58
- startRow = Math.max(0, totalRows - windowSize);
51
+ if (currentRowIdx >= 0) {
52
+ // Center the window around the current story
53
+ const half = Math.floor(maxRows / 2);
54
+ startRow = Math.max(0, currentRowIdx - half);
55
+
56
+ // Adjust if window goes past the end
57
+ if (startRow + maxRows > rows.length) {
58
+ startRow = Math.max(0, rows.length - maxRows);
59
+ }
60
+
61
+ endRow = Math.min(rows.length, startRow + maxRows);
62
+ visibleRows = rows.slice(startRow, endRow);
63
+ } else {
64
+ // No current story, just show first N rows
65
+ visibleRows = rows.slice(0, maxRows);
66
+ endRow = Math.min(rows.length, maxRows);
59
67
  }
60
-
61
- visibleRows = rows.slice(startRow, startRow + windowSize);
62
68
  }
63
69
 
64
70
  return (
65
- <Box flexDirection="column" borderStyle="single" borderColor={colors.dim}>
66
- <Box paddingX={1} borderBottom borderColor={colors.dim}>
71
+ <Box flexDirection="column" paddingY={1}>
72
+ <Box paddingX={1}>
67
73
  <Text color={colors.dim} bold>
68
- {visibleRows.length < totalRows
69
- ? `Stories (${startRow + 1}-${startRow + visibleRows.length} of ${totalRows} rows)`
70
- : "Stories"}
74
+ {visibleRows.length < rows.length
75
+ ? `── Stories (rows ${startRow + 1}-${endRow} of ${rows.length}, ${stories.length} total) ──`
76
+ : `── Stories (${stories.length}) ──`}
71
77
  </Text>
72
78
  </Box>
73
- <Box flexDirection="column" paddingX={1} paddingY={0}>
74
- {visibleRows.map((row, visibleRowIdx) => {
75
- const rowIdx = startRow + visibleRowIdx;
76
- return (
79
+ <Box flexDirection="column" paddingX={1}>
80
+ {visibleRows.map((row, rowIdx) => (
77
81
  <Box key={rowIdx} flexDirection="row">
78
82
  {row.map((story, colIdx) => {
79
83
  const isCurrent = story.id === currentStoryId;
@@ -88,29 +92,60 @@ export function StoryGrid({
88
92
  ? colors.success
89
93
  : colors.dim;
90
94
 
95
+ const priorityColor =
96
+ story.priority <= 2 ? colors.error : story.priority <= 5 ? colors.warning : colors.dim;
97
+
91
98
  return (
92
99
  <Box key={colIdx} width="50%">
100
+ {/* Status symbol */}
93
101
  <Text color={symbolColor}>{symbol} </Text>
102
+
103
+ {/* Story ID */}
94
104
  <Text
95
105
  color={story.passes ? colors.success : isCurrent ? colors.warning : undefined}
96
106
  dimColor={story.passes}
97
107
  >
98
- {story.id.padEnd(8)}
108
+ {story.id.padEnd(9)}
99
109
  </Text>
110
+
111
+ {/* Priority badge */}
112
+ <Text color={priorityColor} bold={story.priority <= 3}>
113
+ P{story.priority}{" "}
114
+ </Text>
115
+
116
+ {/* Title (longer) */}
100
117
  <Text
101
- color={colors.dim}
118
+ color={story.passes ? colors.dim : undefined}
102
119
  dimColor={story.passes}
103
120
  strikethrough={story.passes}
104
121
  wrap="truncate"
105
122
  >
106
- {story.title.substring(0, 25)}
123
+ {story.title.substring(0, 40)}
124
+ </Text>
125
+
126
+ {/* Acceptance criteria count */}
127
+ <Text color={colors.dim} dimColor>
128
+ {" "}
129
+ ({story.criteriaCount}c)
107
130
  </Text>
131
+
132
+ {/* Research indicator */}
133
+ {story.research && (
134
+ <Text color={colors.dim}> 🔍</Text>
135
+ )}
136
+
137
+ {/* Phase badge */}
138
+ {story.phase && (
139
+ <Text color={colors.dim} dimColor>
140
+ {" "}
141
+ [{story.phase}]
142
+ </Text>
143
+ )}
108
144
  </Box>
109
145
  );
110
146
  })}
111
147
  </Box>
112
- );
113
- })}
148
+ ))}
114
149
  </Box>
115
150
  </Box>
116
151
  );
package/src/tui/types.ts CHANGED
@@ -10,6 +10,10 @@ export interface Story {
10
10
  id: string;
11
11
  title: string;
12
12
  passes: boolean;
13
+ priority: number;
14
+ criteriaCount: number;
15
+ research?: boolean;
16
+ phase?: string;
13
17
  }
14
18
 
15
19
  export interface AgentState {