@capagents/forgeqe 2.0.2 → 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 +38 -20
- package/src/tui/inspector.tsx +28 -16
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
|
@@ -2,14 +2,13 @@ import { AGENT_HUES, theme } from "./theme.ts"
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Plain data, tested without a terminal.
|
|
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.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
* Eight pixels wide and four high
|
|
12
|
-
*
|
|
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.
|
|
13
12
|
*/
|
|
14
13
|
export const SHAPES = {
|
|
15
14
|
/** A plain hexagon: an agent not listed. */
|
|
@@ -85,25 +84,44 @@ export function faceOf(agent: string): AgentFace & { main: string; deep: string
|
|
|
85
84
|
return { name: name.charAt(0).toUpperCase() + name.slice(1), shape: "hexagon", hue: "blue", main: theme.faint, deep: theme.border }
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
/**
|
|
89
|
-
export
|
|
87
|
+
/** Columns an icon takes: two pixels to a quadrant block. */
|
|
88
|
+
export const ICON_WIDTH = SHAPES.hexagon[0].length / 2
|
|
90
89
|
|
|
91
|
-
/** The
|
|
92
|
-
|
|
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[][] {
|
|
93
115
|
const face = faceOf(agent)
|
|
94
116
|
const px = SHAPES[face.shape]
|
|
95
|
-
const
|
|
117
|
+
const lit = frame === undefined ? -1 : frame % SWEEP_FRAMES
|
|
96
118
|
return [0, 1].map((r) =>
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return top === bottom ? { ch: "█", fg: top } : { ch: "▀", fg: top, bg: bottom }
|
|
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 }
|
|
104
125
|
}),
|
|
105
126
|
)
|
|
106
127
|
}
|
|
107
|
-
|
|
108
|
-
/** Columns an icon takes. */
|
|
109
|
-
export const ICON_WIDTH = SHAPES.hexagon[0].length
|
package/src/tui/inspector.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import { faceOf, iconCells, ICON_WIDTH } from "./agentIcons.ts"
|
|
@@ -67,14 +67,26 @@ export function Panel(props: { title: string; color: string; width?: number; hei
|
|
|
67
67
|
)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
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])
|
|
72
84
|
return (
|
|
73
85
|
<box style={{ flexDirection: "column", width: ICON_WIDTH, height: 2, flexShrink: 0 }}>
|
|
74
|
-
{iconCells(agent).map((row, r) => (
|
|
86
|
+
{iconCells(agent, live ? frame : undefined).map((row, r) => (
|
|
75
87
|
<text key={r}>
|
|
76
88
|
{row.map((cell, c) => (
|
|
77
|
-
<span key={c}
|
|
89
|
+
<span key={c} fg={cell.fg}>
|
|
78
90
|
{cell.ch}
|
|
79
91
|
</span>
|
|
80
92
|
))}
|
|
@@ -105,9 +117,9 @@ export const STATUS_ROWS = 2
|
|
|
105
117
|
export function StatusBar(props: {
|
|
106
118
|
running: boolean
|
|
107
119
|
who: string
|
|
108
|
-
/** The agent working (its name in src/agents.ts): its icon leads both rows 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. */
|
|
109
121
|
agent?: string
|
|
110
|
-
/** The colour `who` is written in: the working agent's own. */
|
|
122
|
+
/** The colour `who` is written in: the working agent's own; its icon stands in for the spinner. */
|
|
111
123
|
whoColor?: string
|
|
112
124
|
activity: string
|
|
113
125
|
elapsed: string
|
|
@@ -123,7 +135,7 @@ export function StatusBar(props: {
|
|
|
123
135
|
if (icon) {
|
|
124
136
|
return (
|
|
125
137
|
<box style={{ flexDirection: "row", height: STATUS_ROWS, flexShrink: 0, paddingLeft: 1 }}>
|
|
126
|
-
<AgentIcon agent={icon} />
|
|
138
|
+
<AgentIcon agent={icon} live />
|
|
127
139
|
<box style={{ width: props.width - ICON_WIDTH - 1, flexShrink: 0 }}>
|
|
128
140
|
<StatusBar {...props} agent={undefined} whoColor={faceOf(icon).main} width={props.width - ICON_WIDTH - 1} />
|
|
129
141
|
</box>
|
|
@@ -145,8 +157,8 @@ export function StatusBar(props: {
|
|
|
145
157
|
<box style={{ flexDirection: "column", height: STATUS_ROWS, flexShrink: 0, paddingLeft: 1, paddingRight: 1 }}>
|
|
146
158
|
<box style={{ flexDirection: "row", justifyContent: "space-between", height: 1 }}>
|
|
147
159
|
<box style={{ flexDirection: "row" }}>
|
|
148
|
-
{props.running ? <Spinner kind="dots" color={theme.warn} /> : <text fg={color}>{"●"}</text>}
|
|
149
|
-
<text fg={props.running ? props.whoColor || theme.text : color}>{
|
|
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>
|
|
150
162
|
</box>
|
|
151
163
|
<box style={{ flexDirection: "row" }}>
|
|
152
164
|
{actions.map((b) => (
|
|
@@ -229,12 +241,11 @@ export function AgentWorking(props: { s: Inspector; activity: string; stats: str
|
|
|
229
241
|
}
|
|
230
242
|
return (
|
|
231
243
|
<box style={{ flexDirection: "row", marginTop: 1, flexShrink: 0 }}>
|
|
232
|
-
<AgentIcon agent={s.agent} />
|
|
244
|
+
<AgentIcon agent={s.agent} live />
|
|
233
245
|
<box style={{ flexDirection: "column", paddingLeft: 1, flexGrow: 1, flexShrink: 1 }}>
|
|
234
246
|
<box style={{ flexDirection: "row", height: 1 }}>
|
|
235
|
-
<Spinner kind="dots" color={face.main} />
|
|
236
247
|
<text>
|
|
237
|
-
<span fg={face.main}>{
|
|
248
|
+
<span fg={face.main}>{name}</span>
|
|
238
249
|
<span fg={theme.memory}>{` ${clip(props.activity || "Working…", Math.max(8, room - name.length - 4))}`}</span>
|
|
239
250
|
</text>
|
|
240
251
|
</box>
|
|
@@ -257,7 +268,7 @@ export function AgentWorking(props: { s: Inspector; activity: string; stats: str
|
|
|
257
268
|
|
|
258
269
|
/**
|
|
259
270
|
* Every agent that took part in the request, in the order they started, as tiles: the icon, its name, and whether it
|
|
260
|
-
* is working (
|
|
271
|
+
* is working (its icon moving, with its time), waiting for a subagent, or done. The latest that fit; "+N" for the earlier.
|
|
261
272
|
*/
|
|
262
273
|
export function CrewStrip(props: { s: Inspector; width: number; running: boolean; now?: number }) {
|
|
263
274
|
const now = props.now ?? Date.now()
|
|
@@ -283,12 +294,13 @@ export function CrewStrip(props: { s: Inspector; width: number; running: boolean
|
|
|
283
294
|
return (
|
|
284
295
|
<box key={m.agent} style={{ flexDirection: "row", flexShrink: 0 }}>
|
|
285
296
|
{i ? <text fg={theme.border}>{CREW_GAP}</text> : null}
|
|
286
|
-
<AgentIcon agent={m.agent} />
|
|
297
|
+
<AgentIcon agent={m.agent} live={live} />
|
|
287
298
|
<box style={{ flexDirection: "column", width: text + 1, paddingLeft: 1, flexShrink: 0 }}>
|
|
288
299
|
{/* Each agent's name in its own colour, so its tile, the status bar, and the icon read as one agent. */}
|
|
289
300
|
<text fg={face.main}>{clip(face.name, text)}</text>
|
|
290
301
|
<box style={{ flexDirection: "row", height: 1 }}>
|
|
291
|
-
|
|
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>}
|
|
292
304
|
<text fg={m.state === "working" ? theme.warn : theme.faint}>{crewState(m, now)}</text>
|
|
293
305
|
</box>
|
|
294
306
|
</box>
|