@capagents/forgeqe 2.0.3 → 2.0.4
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 +1 -1
- package/src/tui/agentIcons.ts +104 -26
- package/src/tui/inspector.tsx +90 -49
- package/src/tui/theme.ts +17 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capagents/forgeqe",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "ForgeQE: a browser agent CLI that runs goals and test cases in local Chrome or Chromium, or BrowserStack and TestMu AI/LambdaTest when configured, and turns passing runs into tests in your repo",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/tui/agentIcons.ts
CHANGED
|
@@ -1,39 +1,75 @@
|
|
|
1
1
|
import { AGENT_HUES, theme } from "./theme.ts"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Each agent's face in the shell: a short name and an icon, so it is clear which agent or subagent is working. The
|
|
5
|
-
* is
|
|
6
|
-
* Plain data, tested without a terminal.
|
|
4
|
+
* Each agent's face in the shell: a short name and an icon, so it is clear which agent or subagent is working. The
|
|
5
|
+
* icon is a picture of the agent's job after one set of hexagon tiles (soft tones, a deeper one where shapes overlap),
|
|
6
|
+
* in a colour no other agent has; the name is in that colour too. Plain data, tested without a terminal.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export type Hue = keyof typeof AGENT_HUES
|
|
10
|
-
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Eight pixels wide and four high: `#` the agent's main tone, `+` its deeper tone (where shapes overlap, or the part
|
|
11
|
+
* that stands out), `.` empty. A quadrant block holds two by two pixels, so a picture is four characters on two rows.
|
|
14
12
|
*/
|
|
15
|
-
export
|
|
13
|
+
export const SHAPES = {
|
|
14
|
+
/** A plain hexagon: an agent not listed. */
|
|
15
|
+
hexagon: ["..####..", ".######.", ".######.", "..####.."],
|
|
16
|
+
/** Hexagons stacked into one: ForgeQE's agent, which starts the others. */
|
|
17
|
+
hub: ["...##...", ".##++##.", "##++++##", ".##..##."],
|
|
18
|
+
/** A browser window with its title bar. */
|
|
19
|
+
window: ["++++++++", "#......#", "#......#", "########"],
|
|
20
|
+
/** A magnifier. */
|
|
21
|
+
magnifier: [".###....", "#+++#...", ".###.#..", ".....##."],
|
|
22
|
+
/** A bar chart. */
|
|
23
|
+
chart: ["......##", "...##.##", "##.##.##", "++++++++"],
|
|
24
|
+
/** A funnel: a request narrowed to a specification. */
|
|
25
|
+
funnel: ["########", ".######.", "..++++..", "...++..."],
|
|
26
|
+
/** A list with its bullets. */
|
|
27
|
+
list: ["+.######", "........", "+.####..", "........"],
|
|
28
|
+
/** A flag on its pole. */
|
|
29
|
+
flag: ["##+++++.", "##++++..", "##......", "##......"],
|
|
30
|
+
/** A pencil, its tip at the bottom left. */
|
|
31
|
+
pencil: [".....+++", "...###+.", ".###....", "+#......"],
|
|
32
|
+
/** An open book on its spine. */
|
|
33
|
+
book: ["##....##", "###..###", "###++###", "...++..."],
|
|
34
|
+
/** A cross. */
|
|
35
|
+
cross: ["...##...", ".##++##.", ".##++##.", "...##..."],
|
|
36
|
+
/** A plug with its prongs and cable. */
|
|
37
|
+
plug: ["..#..#..", ".++++++.", ".++++++.", "...##..."],
|
|
38
|
+
/** A check mark. */
|
|
39
|
+
check: ["......##", "##...##.", ".##+##..", "..+++..."],
|
|
40
|
+
/** A comment bubble. */
|
|
41
|
+
bubble: ["########", "#.+.+.+#", "########", ".##....."],
|
|
42
|
+
/** A gavel on its block. */
|
|
43
|
+
gavel: [".++++...", ".++++#..", ".....##.", "###...##"],
|
|
44
|
+
/** A bug. */
|
|
45
|
+
bug: ["+..##..+", ".######.", "+##++##+", ".##++##."],
|
|
46
|
+
} as const
|
|
47
|
+
|
|
48
|
+
export type Shape = keyof typeof SHAPES
|
|
49
|
+
export type Hue = keyof typeof AGENT_HUES
|
|
50
|
+
|
|
51
|
+
export type AgentFace = { name: string; shape: Shape; hue: Hue }
|
|
16
52
|
|
|
17
53
|
/**
|
|
18
54
|
* Keyed by the agent's name in src/agents.ts (its AgentSpec name, or "engine"). Each has its own hue; the hue's
|
|
19
55
|
* neighbourhood says what the agent does: purples plan and lead, blues run and look, greens write code, warm hues judge.
|
|
20
56
|
*/
|
|
21
57
|
export const FACES: Record<string, AgentFace> = {
|
|
22
|
-
forgeqe: { name: "ForgeQE's agent",
|
|
23
|
-
"forgeqe-refine": { name: "Refine",
|
|
24
|
-
"forgeqe-api-plan": { name: "API planner",
|
|
25
|
-
"forgeqe-explore-plan": { name: "Explorer",
|
|
26
|
-
engine: { name: "Browser agent",
|
|
27
|
-
"forgeqe-find": { name: "Test finder",
|
|
28
|
-
"forgeqe-bi": { name: "Insights",
|
|
29
|
-
"forgeqe-test": { name: "Scribe",
|
|
30
|
-
"forgeqe-read": { name: "Scribe, reading",
|
|
31
|
-
"forgeqe-heal": { name: "Healer",
|
|
32
|
-
"forgeqe-api": { name: "API test writer",
|
|
33
|
-
"forgeqe-review": { name: "Reviewer",
|
|
34
|
-
"forgeqe-pr-review": { name: "PR reviewer",
|
|
35
|
-
"forgeqe-judge": { name: "Judge",
|
|
36
|
-
"forgeqe-defect-rca": { name: "Root cause",
|
|
58
|
+
forgeqe: { name: "ForgeQE's agent", shape: "hub", hue: "purple" },
|
|
59
|
+
"forgeqe-refine": { name: "Refine", shape: "funnel", hue: "indigo" },
|
|
60
|
+
"forgeqe-api-plan": { name: "API planner", shape: "list", hue: "orchid" },
|
|
61
|
+
"forgeqe-explore-plan": { name: "Explorer", shape: "flag", hue: "magenta" },
|
|
62
|
+
engine: { name: "Browser agent", shape: "window", hue: "blue" },
|
|
63
|
+
"forgeqe-find": { name: "Test finder", shape: "magnifier", hue: "cyan" },
|
|
64
|
+
"forgeqe-bi": { name: "Insights", shape: "chart", hue: "yellow" },
|
|
65
|
+
"forgeqe-test": { name: "Scribe", shape: "pencil", hue: "green" },
|
|
66
|
+
"forgeqe-read": { name: "Scribe, reading", shape: "book", hue: "leaf" },
|
|
67
|
+
"forgeqe-heal": { name: "Healer", shape: "cross", hue: "lime" },
|
|
68
|
+
"forgeqe-api": { name: "API test writer", shape: "plug", hue: "teal" },
|
|
69
|
+
"forgeqe-review": { name: "Reviewer", shape: "check", hue: "pink" },
|
|
70
|
+
"forgeqe-pr-review": { name: "PR reviewer", shape: "bubble", hue: "rose" },
|
|
71
|
+
"forgeqe-judge": { name: "Judge", shape: "gavel", hue: "coral" },
|
|
72
|
+
"forgeqe-defect-rca": { name: "Root cause", shape: "bug", hue: "orange" },
|
|
37
73
|
}
|
|
38
74
|
|
|
39
75
|
export const BRAIN = "forgeqe"
|
|
@@ -41,9 +77,51 @@ export const BROWSER = "engine"
|
|
|
41
77
|
export const SCRIBE = "forgeqe-test"
|
|
42
78
|
|
|
43
79
|
/** An agent's face; one not listed is named by its id, without the "forgeqe-" prefix, with a plain grey hexagon. */
|
|
44
|
-
export function faceOf(agent: string): AgentFace & { main: string } {
|
|
80
|
+
export function faceOf(agent: string): AgentFace & { main: string; deep: string } {
|
|
45
81
|
const face = FACES[agent]
|
|
46
|
-
if (face) return { ...face,
|
|
82
|
+
if (face) return { ...face, ...AGENT_HUES[face.hue] }
|
|
47
83
|
const name = agent.replace(/^forgeqe-/, "").replace(/-/g, " ")
|
|
48
|
-
return { name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
84
|
+
return { name: name.charAt(0).toUpperCase() + name.slice(1), shape: "hexagon", hue: "blue", main: theme.faint, deep: theme.border }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Columns an icon takes: two pixels to a quadrant block. */
|
|
88
|
+
export const ICON_WIDTH = SHAPES.hexagon[0].length / 2
|
|
89
|
+
|
|
90
|
+
/** The quadrant block for each set of its four pixels: top left 1, top right 2, bottom left 4, bottom right 8. */
|
|
91
|
+
const QUADRANT = " ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█"
|
|
92
|
+
|
|
93
|
+
/** One character of an icon and its colour. */
|
|
94
|
+
export type IconCell = { ch: string; fg: string }
|
|
95
|
+
|
|
96
|
+
/** "#7fe0d6" toward white by `amount` (0 to 1): the highlight that sweeps across a working agent's icon. */
|
|
97
|
+
export function lighten(hex: string, amount: number): string {
|
|
98
|
+
const n = parseInt(hex.slice(1), 16)
|
|
99
|
+
const channel = (shift: number) => {
|
|
100
|
+
const v = (n >> shift) & 0xff
|
|
101
|
+
return Math.round(v + (255 - v) * amount)
|
|
102
|
+
}
|
|
103
|
+
return `#${[16, 8, 0].map((s) => channel(s).toString(16).padStart(2, "0")).join("")}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Frames in one sweep of the highlight: across the icon, then a pause as long. */
|
|
107
|
+
export const SWEEP_FRAMES = ICON_WIDTH * 2
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The agent's icon as two rows of quadrant blocks, each in the tone most of its pixels have (a tie keeps the main
|
|
111
|
+
* tone). `frame`: while the agent works, the frame of the highlight that sweeps across it, a column at a time, then
|
|
112
|
+
* rests; undefined, the icon is still.
|
|
113
|
+
*/
|
|
114
|
+
export function iconCells(agent: string, frame?: number): IconCell[][] {
|
|
115
|
+
const face = faceOf(agent)
|
|
116
|
+
const px = SHAPES[face.shape]
|
|
117
|
+
const lit = frame === undefined ? -1 : frame % SWEEP_FRAMES
|
|
118
|
+
return [0, 1].map((r) =>
|
|
119
|
+
Array.from({ length: ICON_WIDTH }, (_, c) => {
|
|
120
|
+
const pixels = [px[r * 2]![c * 2], px[r * 2]![c * 2 + 1], px[r * 2 + 1]![c * 2], px[r * 2 + 1]![c * 2 + 1]]
|
|
121
|
+
const bits = pixels.reduce((n, p, i) => (p === "#" || p === "+" ? n | (1 << i) : n), 0)
|
|
122
|
+
const deep = pixels.filter((p) => p === "+").length > pixels.filter((p) => p === "#").length
|
|
123
|
+
const tone = deep ? face.deep : face.main
|
|
124
|
+
return { ch: QUADRANT[bits]!, fg: c === lit ? lighten(tone, 0.6) : tone }
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
49
127
|
}
|
package/src/tui/inspector.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
-
import type
|
|
2
|
+
import { useEffect, useState, type ReactNode, type RefObject } from "react"
|
|
3
3
|
|
|
4
4
|
import type { Usage } from "../types.ts"
|
|
5
|
-
import { faceOf } from "./agentIcons.ts"
|
|
5
|
+
import { faceOf, iconCells, ICON_WIDTH } from "./agentIcons.ts"
|
|
6
6
|
import { fitButtons, Spinner, type Button } from "./components.tsx"
|
|
7
7
|
import { crewMs, filesByUse, type CrewMember, type FileUse, type Inspector, type Progress } from "./session.ts"
|
|
8
8
|
import { clip, plainText, tailCut } from "./text.ts"
|
|
@@ -67,10 +67,33 @@ export function Panel(props: { title: string; color: string; width?: number; hei
|
|
|
67
67
|
)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
/** Milliseconds a frame of a working agent's icon shows: the highlight crosses it and rests in about a second. */
|
|
71
|
+
const SWEEP_MS = 140
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* An agent's icon (src/tui/agentIcons.ts): its picture in quadrant blocks, four columns and two rows, in its colours.
|
|
75
|
+
* `live`: the agent is working, and a highlight sweeps across the icon in place of a spinner.
|
|
76
|
+
*/
|
|
77
|
+
export function AgentIcon({ agent, live = false }: { agent: string; live?: boolean }) {
|
|
78
|
+
const [frame, setFrame] = useState(0)
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!live) return
|
|
81
|
+
const timer = setInterval(() => setFrame((f) => f + 1), SWEEP_MS)
|
|
82
|
+
return () => clearInterval(timer)
|
|
83
|
+
}, [live])
|
|
84
|
+
return (
|
|
85
|
+
<box style={{ flexDirection: "column", width: ICON_WIDTH, height: 2, flexShrink: 0 }}>
|
|
86
|
+
{iconCells(agent, live ? frame : undefined).map((row, r) => (
|
|
87
|
+
<text key={r}>
|
|
88
|
+
{row.map((cell, c) => (
|
|
89
|
+
<span key={c} fg={cell.fg}>
|
|
90
|
+
{cell.ch}
|
|
91
|
+
</span>
|
|
92
|
+
))}
|
|
93
|
+
</text>
|
|
94
|
+
))}
|
|
95
|
+
</box>
|
|
96
|
+
)
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
/** How far the agent's own plan has got, while it has one of two steps or more. */
|
|
@@ -94,8 +117,10 @@ export const STATUS_ROWS = 2
|
|
|
94
117
|
export function StatusBar(props: {
|
|
95
118
|
running: boolean
|
|
96
119
|
who: string
|
|
97
|
-
/** The agent working (its name in src/agents.ts): while the request runs,
|
|
120
|
+
/** The agent working (its name in src/agents.ts): its icon leads both rows while the request runs, and moves. */
|
|
98
121
|
agent?: string
|
|
122
|
+
/** The colour `who` is written in: the working agent's own; its icon stands in for the spinner. */
|
|
123
|
+
whoColor?: string
|
|
99
124
|
activity: string
|
|
100
125
|
elapsed: string
|
|
101
126
|
status: string
|
|
@@ -106,7 +131,17 @@ export function StatusBar(props: {
|
|
|
106
131
|
/** Buttons for the view (Focus panels, Close), before Stop; the first to give way when the line is short. */
|
|
107
132
|
actions?: Button[]
|
|
108
133
|
}) {
|
|
109
|
-
const
|
|
134
|
+
const icon = props.running && props.agent ? props.agent : null
|
|
135
|
+
if (icon) {
|
|
136
|
+
return (
|
|
137
|
+
<box style={{ flexDirection: "row", height: STATUS_ROWS, flexShrink: 0, paddingLeft: 1 }}>
|
|
138
|
+
<AgentIcon agent={icon} live />
|
|
139
|
+
<box style={{ width: props.width - ICON_WIDTH - 1, flexShrink: 0 }}>
|
|
140
|
+
<StatusBar {...props} agent={undefined} whoColor={faceOf(icon).main} width={props.width - ICON_WIDTH - 1} />
|
|
141
|
+
</box>
|
|
142
|
+
</box>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
110
145
|
const done = props.status === "done"
|
|
111
146
|
const failed = props.status === "fail"
|
|
112
147
|
const color = props.running ? theme.warn : done ? theme.ok : failed ? theme.err : theme.muted
|
|
@@ -115,15 +150,15 @@ export function StatusBar(props: {
|
|
|
115
150
|
const cells = 12
|
|
116
151
|
const barText = bar ? ` ${bar.done}/${bar.total} ` : ""
|
|
117
152
|
const stop = "■ Stop · esc"
|
|
118
|
-
const fixed = state.length + 4 + (
|
|
153
|
+
const fixed = state.length + 4 + (props.running ? stop.length + 4 + props.elapsed.length + 2 + (bar ? cells + barText.length : 0) : 0)
|
|
119
154
|
const actions = fitButtons(props.actions || [], Math.max(0, props.width - fixed - 2))
|
|
120
155
|
const [full, empty] = bar ? progressBar(bar.done, bar.total, cells) : ["", ""]
|
|
121
156
|
return (
|
|
122
157
|
<box style={{ flexDirection: "column", height: STATUS_ROWS, flexShrink: 0, paddingLeft: 1, paddingRight: 1 }}>
|
|
123
158
|
<box style={{ flexDirection: "row", justifyContent: "space-between", height: 1 }}>
|
|
124
159
|
<box style={{ flexDirection: "row" }}>
|
|
125
|
-
{props.running ? <Spinner kind="dots" color={theme.warn} /> : <text fg={color}>{"●"}</text>}
|
|
126
|
-
<text fg={
|
|
160
|
+
{props.whoColor ? null : props.running ? <Spinner kind="dots" color={theme.warn} /> : <text fg={color}>{"●"}</text>}
|
|
161
|
+
<text fg={props.running ? props.whoColor || theme.text : color}>{`${props.whoColor ? "" : " "}${clip(state, Math.max(8, props.width - fixed + state.length))}`}</text>
|
|
127
162
|
</box>
|
|
128
163
|
<box style={{ flexDirection: "row" }}>
|
|
129
164
|
{actions.map((b) => (
|
|
@@ -170,19 +205,18 @@ const CREW_MARK: Record<CrewMember["state"], [string, string]> = {
|
|
|
170
205
|
|
|
171
206
|
/** Rows the crew strip takes, the blank row under it included: none until a second agent has joined the request. */
|
|
172
207
|
export function crewRows(s: Inspector): number {
|
|
173
|
-
return s.crew.length > 1 ?
|
|
208
|
+
return s.crew.length > 1 ? 3 : 0
|
|
174
209
|
}
|
|
175
210
|
|
|
176
|
-
/** What a member's
|
|
211
|
+
/** What a member's second row says: "waiting", or how long it has worked. */
|
|
177
212
|
function crewState(m: CrewMember, now: number): string {
|
|
178
213
|
return m.state === "waiting" ? "waiting" : formatDuration(crewMs(m, now))
|
|
179
214
|
}
|
|
180
215
|
|
|
181
|
-
/**
|
|
182
|
-
const
|
|
216
|
+
/** Columns a tile's text takes, and the tile with its icon. */
|
|
217
|
+
const CREW_TEXT = 18
|
|
218
|
+
const crewTextWidth = (m: CrewMember, now: number) => Math.min(CREW_TEXT, Math.max(faceOf(m.agent).name.length, crewState(m, now).length + 2))
|
|
183
219
|
const CREW_GAP = " › "
|
|
184
|
-
/** "◫ Browser agent ✓ 39s": an agent in the strip, its icon, name, state mark, and state. */
|
|
185
|
-
const crewWidth = (m: CrewMember, now: number) => 2 + Math.min(CREW_NAME, faceOf(m.agent).name.length) + 3 + crewState(m, now).length
|
|
186
220
|
|
|
187
221
|
/**
|
|
188
222
|
* The chat view's line under the conversation while a request runs (the panels closed): the working agent's icon, its
|
|
@@ -192,11 +226,11 @@ const crewWidth = (m: CrewMember, now: number) => 2 + Math.min(CREW_NAME, faceOf
|
|
|
192
226
|
export function AgentWorking(props: { s: Inspector; activity: string; stats: string; width: number }) {
|
|
193
227
|
const { s } = props
|
|
194
228
|
const face = faceOf(s.agent)
|
|
195
|
-
const room = Math.max(10, props.width -
|
|
196
|
-
const name = clip(face.name, Math.max(8, room -
|
|
229
|
+
const room = Math.max(10, props.width - ICON_WIDTH - 1)
|
|
230
|
+
const name = clip(face.name, Math.max(8, room - 4))
|
|
197
231
|
const stats = `(${props.stats})`
|
|
198
232
|
const chain = s.crew.length > 1 ? s.crew : []
|
|
199
|
-
const size = (m: CrewMember) =>
|
|
233
|
+
const size = (m: CrewMember) => 2 + faceOf(m.agent).name.length
|
|
200
234
|
let first = chain.length
|
|
201
235
|
let used = 0
|
|
202
236
|
while (first > 0) {
|
|
@@ -206,40 +240,40 @@ export function AgentWorking(props: { s: Inspector; activity: string; stats: str
|
|
|
206
240
|
first--
|
|
207
241
|
}
|
|
208
242
|
return (
|
|
209
|
-
<box style={{ flexDirection: "
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
<
|
|
214
|
-
|
|
243
|
+
<box style={{ flexDirection: "row", marginTop: 1, flexShrink: 0 }}>
|
|
244
|
+
<AgentIcon agent={s.agent} live />
|
|
245
|
+
<box style={{ flexDirection: "column", paddingLeft: 1, flexGrow: 1, flexShrink: 1 }}>
|
|
246
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
247
|
+
<text>
|
|
248
|
+
<span fg={face.main}>{name}</span>
|
|
249
|
+
<span fg={theme.memory}>{` ${clip(props.activity || "Working…", Math.max(8, room - name.length - 4))}`}</span>
|
|
250
|
+
</text>
|
|
251
|
+
</box>
|
|
252
|
+
<text style={{ height: 1 }}>
|
|
253
|
+
{first > 0 ? <span fg={theme.faint}>{`+${first} `}</span> : null}
|
|
254
|
+
{chain.slice(first).flatMap((m, i) => {
|
|
255
|
+
const [mark, color] = CREW_MARK[m.state]
|
|
256
|
+
return [
|
|
257
|
+
...(i ? [<span key={`${m.agent}-gap`} fg={theme.border}>{CREW_GAP}</span>] : []),
|
|
258
|
+
<span key={`${m.agent}-mark`} fg={color}>{`${mark} `}</span>,
|
|
259
|
+
<span key={m.agent} fg={faceOf(m.agent).main}>{faceOf(m.agent).name}</span>,
|
|
260
|
+
]
|
|
261
|
+
})}
|
|
262
|
+
<span fg={theme.faint}>{`${chain.length ? " " : ""}${clip(stats, Math.max(8, room - used))}`}</span>
|
|
215
263
|
</text>
|
|
216
264
|
</box>
|
|
217
|
-
<text style={{ height: 1, paddingLeft: 2 }}>
|
|
218
|
-
{first > 0 ? <span fg={theme.faint}>{`+${first} `}</span> : null}
|
|
219
|
-
{chain.slice(first).flatMap((m, i) => {
|
|
220
|
-
const [mark, color] = CREW_MARK[m.state]
|
|
221
|
-
const f = faceOf(m.agent)
|
|
222
|
-
return [
|
|
223
|
-
...(i ? [<span key={`${m.agent}-gap`} fg={theme.border}>{CREW_GAP}</span>] : []),
|
|
224
|
-
<span key={`${m.agent}-mark`} fg={color}>{`${mark} `}</span>,
|
|
225
|
-
<span key={m.agent} fg={f.main}>{`${f.icon} ${f.name}`}</span>,
|
|
226
|
-
]
|
|
227
|
-
})}
|
|
228
|
-
<span fg={theme.faint}>{`${chain.length ? " " : ""}${clip(stats, Math.max(8, room - used))}`}</span>
|
|
229
|
-
</text>
|
|
230
265
|
</box>
|
|
231
266
|
)
|
|
232
267
|
}
|
|
233
268
|
|
|
234
269
|
/**
|
|
235
|
-
* Every agent that took part in the request, in the order they started,
|
|
236
|
-
*
|
|
237
|
-
* earlier.
|
|
270
|
+
* Every agent that took part in the request, in the order they started, as tiles: the icon, its name, and whether it
|
|
271
|
+
* is working (its icon moving, with its time), waiting for a subagent, or done. The latest that fit; "+N" for the earlier.
|
|
238
272
|
*/
|
|
239
273
|
export function CrewStrip(props: { s: Inspector; width: number; running: boolean; now?: number }) {
|
|
240
274
|
const now = props.now ?? Date.now()
|
|
241
275
|
const crew = props.s.crew
|
|
242
|
-
const tileWidth = (m: CrewMember) =>
|
|
276
|
+
const tileWidth = (m: CrewMember) => ICON_WIDTH + 1 + crewTextWidth(m, now)
|
|
243
277
|
let first = crew.length
|
|
244
278
|
let used = 0
|
|
245
279
|
while (first > 0) {
|
|
@@ -250,19 +284,26 @@ export function CrewStrip(props: { s: Inspector; width: number; running: boolean
|
|
|
250
284
|
first--
|
|
251
285
|
}
|
|
252
286
|
return (
|
|
253
|
-
<box style={{ flexDirection: "row", height:
|
|
287
|
+
<box style={{ flexDirection: "row", height: 2, flexShrink: 0, marginBottom: 1, paddingLeft: 1 }}>
|
|
254
288
|
{first > 0 ? <text fg={theme.faint}>{`+${first} `}</text> : null}
|
|
255
289
|
{crew.slice(first).map((m, i) => {
|
|
290
|
+
const face = faceOf(m.agent)
|
|
256
291
|
const [mark, color] = CREW_MARK[m.state]
|
|
257
292
|
const live = m.state === "working" && props.running
|
|
293
|
+
const text = crewTextWidth(m, now)
|
|
258
294
|
return (
|
|
259
295
|
<box key={m.agent} style={{ flexDirection: "row", flexShrink: 0 }}>
|
|
260
296
|
{i ? <text fg={theme.border}>{CREW_GAP}</text> : null}
|
|
261
|
-
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
297
|
+
<AgentIcon agent={m.agent} live={live} />
|
|
298
|
+
<box style={{ flexDirection: "column", width: text + 1, paddingLeft: 1, flexShrink: 0 }}>
|
|
299
|
+
{/* Each agent's name in its own colour, so its tile, the status bar, and the icon read as one agent. */}
|
|
300
|
+
<text fg={face.main}>{clip(face.name, text)}</text>
|
|
301
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
302
|
+
{/* The working agent's icon moves, so its time needs no spinner beside it. */}
|
|
303
|
+
{live ? null : <text fg={color} style={{ width: 2, flexShrink: 0 }}>{mark}</text>}
|
|
304
|
+
<text fg={m.state === "working" ? theme.warn : theme.faint}>{crewState(m, now)}</text>
|
|
305
|
+
</box>
|
|
306
|
+
</box>
|
|
266
307
|
</box>
|
|
267
308
|
)
|
|
268
309
|
})}
|
package/src/tui/theme.ts
CHANGED
|
@@ -20,29 +20,29 @@ export const theme = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* The agents' colours (src/tui/agentIcons.ts), one per agent, for its
|
|
24
|
-
* that do the same kind of work.
|
|
23
|
+
* The agents' colours (src/tui/agentIcons.ts), one per agent: its main tone, for its name and icon, and the deeper one
|
|
24
|
+
* where its icon's shapes overlap. Neighbouring hues go to agents that do the same kind of work.
|
|
25
25
|
*/
|
|
26
26
|
export const AGENT_HUES = {
|
|
27
27
|
// Plan and lead.
|
|
28
|
-
purple: "#a78bfa",
|
|
29
|
-
indigo: "#8fa2ff",
|
|
30
|
-
orchid: "#e0a3f5",
|
|
31
|
-
magenta: "#f59fe0",
|
|
28
|
+
purple: { main: "#a78bfa", deep: "#5b2bc4" },
|
|
29
|
+
indigo: { main: "#8fa2ff", deep: "#3d4fd6" },
|
|
30
|
+
orchid: { main: "#e0a3f5", deep: "#a443d6" },
|
|
31
|
+
magenta: { main: "#f59fe0", deep: "#c2389e" },
|
|
32
32
|
// Run and look.
|
|
33
|
-
blue: "#a9d0f8",
|
|
34
|
-
cyan: "#8ee3f5",
|
|
35
|
-
yellow: "#f0e48c",
|
|
33
|
+
blue: { main: "#a9d0f8", deep: "#5ea6f0" },
|
|
34
|
+
cyan: { main: "#8ee3f5", deep: "#1fa3c4" },
|
|
35
|
+
yellow: { main: "#f0e48c", deep: "#b8a32a" },
|
|
36
36
|
// Write code.
|
|
37
|
-
green: "#6fd3b2",
|
|
38
|
-
leaf: "#9be89b",
|
|
39
|
-
lime: "#cde88a",
|
|
40
|
-
teal: "#7fe0d6",
|
|
37
|
+
green: { main: "#6fd3b2", deep: "#3a9a7e" },
|
|
38
|
+
leaf: { main: "#9be89b", deep: "#3fa34d" },
|
|
39
|
+
lime: { main: "#cde88a", deep: "#86a92c" },
|
|
40
|
+
teal: { main: "#7fe0d6", deep: "#1f9c90" },
|
|
41
41
|
// Judge.
|
|
42
|
-
pink: "#f29bc9",
|
|
43
|
-
rose: "#ff9eaa",
|
|
44
|
-
coral: "#ffb199",
|
|
45
|
-
orange: "#ffc58a",
|
|
42
|
+
pink: { main: "#f29bc9", deep: "#e0457b" },
|
|
43
|
+
rose: { main: "#ff9eaa", deep: "#d93a52" },
|
|
44
|
+
coral: { main: "#ffb199", deep: "#e0603a" },
|
|
45
|
+
orange: { main: "#ffc58a", deep: "#d6701f" },
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** 950 → "950", 47_100 → "47.1K", 2_300_000 → "2.3M". */
|