@capagents/forgeqe 1.1.8 → 1.2.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/src/tui/theme.ts CHANGED
@@ -35,35 +35,34 @@ export function formatDuration(ms: number, precise = false): string {
35
35
  return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`
36
36
  }
37
37
 
38
- /** Four rows: ascenders, then a three-row x-height. Capitals use all four. */
39
- const GLYPHS: Record<string, [string, string, string, string]> = {
40
- w: [" ", "█ █", "█ █ █", "▀▀▀▀▀"],
41
- e: [" ", "█▀▀█", "█▀▀▀", "▀▀▀▀"],
42
- b: ["▄ ", "█▀▀█", "█ █", "▀▀▀▀"],
43
- p: [" ", "█▀▀█", "█ █", "█▀▀▀"],
44
- i: [" ", "▀", "█", "▀"],
45
- l: ["▄", "█", "█", "▀"],
46
- o: [" ", "█▀▀█", "█ █", "▀▀▀▀"],
47
- t: [" ▄ ", "▀█▀▀", " █ ", " ▀▀▀"],
48
- r: [" ", "█▀▀", "█ ", "▀ "],
49
- c: [" ", "█▀▀▀", "█ ", "▀▀▀▀"],
50
- h: ["▄ ", "█▀▀█", "█ █", "▀ ▀"],
51
- s: [" ", "█▀▀▀", "▀▀▀█", "▀▀▀▀"],
52
- a: [" ", "▀▀▀█", "█▀▀█", "▀▀▀▀"],
53
- f: [" ▄▀", "▀█▀", " █ ", " ▀ "],
54
- g: [" ", "█▀▀█", "█ █", "▀▀▀█"],
55
- Q: ["█▀▀█ ", "█ █ ", "█ █ ", "▀▀▀█▄"],
56
- E: ["█▀▀▀", "█▄▄ ", "█ ", "▀▀▀▀"],
57
- ".": [" ", " ", " ", "▀"],
38
+ /**
39
+ * The wordmark's capitals as pixel maps, five pixels tall: every letter has the same weight, height and baseline. A
40
+ * sixth row, left blank, is the Q's tail. Two pixel rows go in one text row as half blocks, so it is three rows high.
41
+ */
42
+ const LETTERS: Record<string, string[]> = {
43
+ F: ["####", "#...", "###.", "#...", "#..."],
44
+ O: ["####", "#..#", "#..#", "#..#", "####"],
45
+ R: ["####", "#..#", "###.", "#..#", "#..#"],
46
+ G: ["####", "#...", "#.##", "#..#", "####"],
47
+ E: ["####", "#...", "###.", "#...", "####"],
48
+ Q: ["####.", "#..#.", "#..#.", "#..#.", "####.", "...##"],
58
49
  }
59
50
 
51
+ /** The letters of `word` (capitals; others are left out) as three rows of half blocks, one space between letters. */
60
52
  export function logoRows(word: string): string[] {
61
- const rows = ["", "", "", ""]
62
- const letters = [...word]
63
- letters.forEach((ch, index) => {
64
- const glyph = GLYPHS[ch] ?? GLYPHS[ch.toLowerCase()]
65
- if (!glyph) return
66
- for (let r = 0; r < 4; r++) rows[r] += (index ? " " : "") + glyph[r]
53
+ const rows = ["", "", ""]
54
+ ;[...word.toUpperCase()].filter((ch) => LETTERS[ch]).forEach((ch, index) => {
55
+ const px = LETTERS[ch]!
56
+ const at = (r: number, c: number) => px[r]?.[c] === "#"
57
+ for (let r = 0; r < 3; r++) {
58
+ let cells = ""
59
+ for (let c = 0; c < px[0]!.length; c++) {
60
+ const top = at(r * 2, c)
61
+ const bottom = at(r * 2 + 1, c)
62
+ cells += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " "
63
+ }
64
+ rows[r] += (index ? " " : "") + cells
65
+ }
67
66
  })
68
67
  return rows
69
68
  }
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs"
2
2
  import os from "node:os"
3
3
  import path from "node:path"
4
4
  import { SyntaxStyle } from "@opentui/core"
5
- import type { ReactNode } from "react"
5
+ import { createContext, useContext, type ReactNode } from "react"
6
6
 
7
7
  import type { CodeResult, RunStatus } from "../types.ts"
8
8
  import { CARD_MARK, type Card } from "../card.ts"
@@ -11,7 +11,7 @@ import { suiteExitCode, type SuiteResult } from "../suite/run.ts"
11
11
  import { STATUS_MARK, shortPath, type RunSummary } from "../summary.ts"
12
12
  import { BAR } from "./components.tsx"
13
13
  import { formatDuration, formatTokens, theme } from "./theme.ts"
14
- import { clip } from "./text.ts"
14
+ import { clip, shortLink } from "./text.ts"
15
15
 
16
16
  /** The transcript: its lines, and the cards for a run, a suite, a tool result, and what a request produced. */
17
17
 
@@ -195,7 +195,7 @@ export function SummaryCard({ summary }: { summary: RunSummary }) {
195
195
  {summary.savedDir ? (
196
196
  <text>
197
197
  <span fg={theme.faint}>{"Saved "}</span>
198
- <Linked text={`${summary.savedDir}/ `} color={theme.muted} />
198
+ <Linked text={`${summary.savedDir}/ `} color={theme.muted} lead={UNDER + 10} />
199
199
  <span fg={theme.faint}>{summary.savedFiles.join(" · ")}</span>
200
200
  </text>
201
201
  ) : null}
@@ -218,14 +218,27 @@ export function linkTarget(token: string, cwd = process.cwd()): string | null {
218
218
  * Text whose web addresses and existing file paths can be opened from the terminal (OSC 8 links: cmd/ctrl+click in
219
219
  * VS Code and most terminals). A path that does not exist stays plain text, so no link leads nowhere.
220
220
  */
221
- export function Linked({ text, color }: { text: string; color: string }) {
222
- const parts: { text: string; href?: string }[] = []
221
+ /** The columns a line of the conversation has, for links to fit in; 0 when not known (no shortening). */
222
+ export const LineRoom = createContext(0)
223
+
224
+ /** Columns before a card's line of text: the "⎿" gutter under the card's title. */
225
+ const UNDER = 4
226
+
227
+ /**
228
+ * `lead`: the columns before this text on its row (a gutter, a label in another span). A link longer than what is left
229
+ * of the row is drawn shortened, its target whole, so it moves to the next row in one piece instead of splitting.
230
+ */
231
+ export function Linked({ text, color, lead = UNDER }: { text: string; color: string; lead?: number }) {
232
+ const room = useContext(LineRoom)
233
+ const parts: { text: string; href?: string; max?: number }[] = []
223
234
  let at = 0
224
235
  for (const m of text.matchAll(LINKABLE)) {
225
236
  const href = linkTarget(m[0])
226
237
  if (!href) continue
227
238
  if (m.index! > at) parts.push({ text: text.slice(at, m.index) })
228
- parts.push({ text: m[0], href })
239
+ // Beside its label when a useful part fits there ("Page .forgeqe/…/report.html"), else alone on the next row.
240
+ const beside = room - lead - m.index! - 1
241
+ parts.push({ text: m[0], href, ...(room ? { max: beside >= 30 ? beside : room - lead - 1 } : {}) })
229
242
  at = m.index! + m[0].length
230
243
  }
231
244
  if (at < text.length) parts.push({ text: text.slice(at) })
@@ -234,7 +247,7 @@ export function Linked({ text, color }: { text: string; color: string }) {
234
247
  {parts.map((p, i) =>
235
248
  p.href ? (
236
249
  <a key={i} href={p.href} fg={theme.accent}>
237
- {p.text}
250
+ {p.max ? shortLink(p.text, p.max) : p.text}
238
251
  </a>
239
252
  ) : (
240
253
  <span key={i} fg={color}>
@@ -251,7 +264,7 @@ export function Row({ label, value, color = theme.muted }: { label: string; valu
251
264
  return (
252
265
  <text>
253
266
  <span fg={theme.faint}>{label.padEnd(10)}</span>
254
- <Linked text={value} color={color} />
267
+ <Linked text={value} color={color} lead={UNDER + 10} />
255
268
  </text>
256
269
  )
257
270
  }
@@ -307,7 +320,7 @@ export const CARD_COLOR: Record<Card["status"], string> = { ok: theme.ok, fail:
307
320
 
308
321
  export function ResultCard({ card }: { card: Card }) {
309
322
  const color = CARD_COLOR[card.status]
310
- const lineColor = (l: string) => (/^\s*✓/.test(l) ? theme.ok : /^\s*[✗!]/.test(l) ? theme.err : /^\s*\?/.test(l) ? theme.warn : theme.muted)
323
+ const lineColor = (l: string) => (/^\s*✓/.test(l) ? theme.ok : /^\s*[✗!]/.test(l) ? theme.err : /^\s*[?~]/.test(l) ? theme.warn : /^\s*(Fix:|→)/.test(l) ? theme.text : theme.muted)
311
324
  // What a run used is in the footer and the reports; price talk stays out of the conversation.
312
325
  const body = card.body.filter((l) => !/^\s*Usage\s{2,}/.test(l)).filter((l, i, all) => l || (i > 0 && i < all.length - 1))
313
326
  return (
package/src/types.ts CHANGED
@@ -116,6 +116,8 @@ export type CodeResult = {
116
116
  knownDefect?: boolean
117
117
  /** The job did not finish: what it wrote was moved to the needs-attention folder (see src/code/attention.ts). */
118
118
  attention?: { dir: string; files: string[]; patch?: string; readme: string }
119
+ /** The job worked from an existing test: ran it as it was, extended it, or updated it. */
120
+ existing?: "reused" | "extended" | "updated"
119
121
  }
120
122
 
121
123
  /** What an agent did to a file, for the shell's file list. */
@@ -136,6 +138,12 @@ export type CodeEvent =
136
138
  | { type: "code"; kind: "usage"; usage: Usage; context?: number }
137
139
  /** The agent's to-do list, each time it changes. */
138
140
  | { type: "code"; kind: "plan"; items: PlanItem[] }
141
+ /** The reviewer's verdict on a test that passed: approved or not, why, and each issue with its file and fix. */
142
+ | { type: "code"; kind: "review"; attempt: number; max: number; approved: boolean; summary: string; issues: CodeReview["issues"] }
143
+ /** Scribe starts another attempt: why, and what it was asked to fix (the reviewer's issues, the test's error). */
144
+ | { type: "code"; kind: "attempt"; attempt: number; max: number; why: string; fixing: string[] }
145
+ /** What Scribe said it did in an attempt: its notes, and the files it changed. */
146
+ | { type: "code"; kind: "answer"; attempt: number; notes?: string; changed: string[] }
139
147
 
140
148
  export type AgentEvent =
141
149
  | { type: "log"; level: "info" | "warn" | "error"; text: string }