@capagents/forgeqe 1.1.6 → 1.1.8
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/HOW_TO_USE.md +18 -8
- package/docs/user-guide.html +19 -10
- package/package.json +1 -1
- package/prompts/brain.md +4 -2
- package/src/brain/index.ts +4 -0
- package/src/capabilities/registry.ts +6 -2
- package/src/cases/api.ts +75 -22
- package/src/code/runtime.ts +3 -0
- package/src/mcp/reporttools.ts +205 -14
- package/src/mcp/server.ts +22 -7
- package/src/mcp/tools.ts +16 -5
- package/src/report/evidence.ts +313 -0
- package/src/report/html.ts +7 -3
- package/src/report/template.html +46 -45
- package/src/tui/App.tsx +22 -9
- package/src/tui/inspector.tsx +76 -25
- package/src/tui/transcript.tsx +19 -1
package/src/tui/App.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import { initWorkspace } from "../init.ts"
|
|
|
30
30
|
import { ShortcutsPanel } from "./shortcuts.tsx"
|
|
31
31
|
import { ago, getStarted, projectLine, setupSentence, type Fact, type GetStarted, recentWork, startActions, type RecentItem, type StartAction } from "./landing.ts"
|
|
32
32
|
import { TIPS, formatDuration, formatTokens, packageVersion, shortCwd, theme } from "./theme.ts"
|
|
33
|
-
import { emptyProduced, Line, Produced, producedCard, settleCall, TestsSetting, TranscriptLine, trimLines } from "./transcript.tsx"
|
|
33
|
+
import { emptyProduced, Line, Produced, producedCard, settleCall, sinceRequest, TestsSetting, TranscriptLine, trimLines } from "./transcript.tsx"
|
|
34
34
|
import { Dialog, dialogSpec } from "./dialogs.ts"
|
|
35
35
|
import { activityFromLog, clip, modelLabel } from "./text.ts"
|
|
36
36
|
import { featuresText } from "../capabilities/render.ts"
|
|
@@ -444,7 +444,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
444
444
|
reloadAfterAuth()
|
|
445
445
|
return
|
|
446
446
|
}
|
|
447
|
-
const controller = begin("Signing in…")
|
|
447
|
+
const controller = begin("Signing in…", system ? `Sign in to ${system}` : "Sign in")
|
|
448
448
|
controller.signal.addEventListener("abort", () => settleEntry(null), { once: true })
|
|
449
449
|
let finalStatus = "fail"
|
|
450
450
|
try {
|
|
@@ -972,7 +972,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
972
972
|
if (!aborted()) setActivity(`Agent: ${clip(e.text, 60)}…`)
|
|
973
973
|
return
|
|
974
974
|
case "call":
|
|
975
|
-
pushTool(e.text, e.ok, e.id)
|
|
975
|
+
pushTool(e.text, e.ok, e.id, !e.quiet)
|
|
976
976
|
if (!aborted()) setActivity(nowWorking())
|
|
977
977
|
return
|
|
978
978
|
case "agent": {
|
|
@@ -1037,6 +1037,9 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1037
1037
|
return
|
|
1038
1038
|
case "report":
|
|
1039
1039
|
producedRef.current.reports.push({ title: e.title, html: e.html })
|
|
1040
|
+
// What the report wrote is a file the request made: it is in Files with the rest.
|
|
1041
|
+
for (const f of [e.markdown, e.html]) useFile(inspRef.current, { path: path.relative(process.cwd(), f) || f, action: "write" })
|
|
1042
|
+
bump()
|
|
1040
1043
|
return
|
|
1041
1044
|
}
|
|
1042
1045
|
}
|
|
@@ -1078,10 +1081,12 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1078
1081
|
ask,
|
|
1079
1082
|
})
|
|
1080
1083
|
|
|
1081
|
-
|
|
1084
|
+
/** `request`: what was asked, which heads this request's lines in Activity so they are not read as the last one's. */
|
|
1085
|
+
const begin = (activityText: string, request: string) => {
|
|
1082
1086
|
const controller = new AbortController()
|
|
1083
1087
|
abortRef.current = controller
|
|
1084
1088
|
started.current = Date.now()
|
|
1089
|
+
pushLine({ kind: "request", text: request.replace(/\s+/g, " ").trim(), pane: "activity", quiet: true, at: started.current })
|
|
1085
1090
|
usedRef.current = new Map()
|
|
1086
1091
|
producedRef.current = emptyProduced()
|
|
1087
1092
|
newRequest(inspRef.current)
|
|
@@ -1095,8 +1100,16 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1095
1100
|
}
|
|
1096
1101
|
|
|
1097
1102
|
const finish = (finalStatus: string, followUps: FollowUp[] = []) => {
|
|
1098
|
-
// A call the request ended inside of (stopped, or its result never came) is not left spinning in Activity
|
|
1099
|
-
|
|
1103
|
+
// A call the request ended inside of (stopped, or its result never came) is not left spinning in Activity; then the
|
|
1104
|
+
// request's closing line says how it ended, how long it took, and how many activities it had.
|
|
1105
|
+
const took = started.current != null ? formatDuration(Date.now() - started.current) : ""
|
|
1106
|
+
const ended = (["done", "fail", "stopped", "idle"] as const).find((x) => x === finalStatus) || "fail"
|
|
1107
|
+
const endId = idRef.current++
|
|
1108
|
+
setLines((prev) => {
|
|
1109
|
+
const settled = prev.some((l) => l.kind === "running") ? prev.map((l) => (l.kind === "running" ? { ...l, kind: "tool" as const, text: `${l.text} · ${finalStatus === "stopped" ? "stopped" : "no result"}` } : l)) : prev
|
|
1110
|
+
const n = sinceRequest(settled)
|
|
1111
|
+
return trimLines([...settled, { id: endId, kind: "requestEnd", text: `${took ? `in ${took} · ` : ""}${n} ${n === 1 ? "activity" : "activities"}`, pane: "activity", quiet: true, ended }])
|
|
1112
|
+
})
|
|
1100
1113
|
setNext(nextSteps(producedRef.current, { testsOn: testsRef.current.on, followUps }))
|
|
1101
1114
|
progressDone(inspRef.current, finalStatus === "stopped")
|
|
1102
1115
|
bump()
|
|
@@ -1120,7 +1133,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1120
1133
|
directNoted.current = true
|
|
1121
1134
|
push("sys", `ForgeQE's agent is off (${why}), so the text runs as a browser goal.${why.includes("agent runtime") ? ` ${INSTALL_HINT}` : " Press Tab for a live profile."} /features lists what ForgeQE can do.`)
|
|
1122
1135
|
}
|
|
1123
|
-
const controller = begin(cfg.llm.provider === "mock" ? "Running the offline demo…" : "Starting…")
|
|
1136
|
+
const controller = begin(cfg.llm.provider === "mock" ? "Running the offline demo…" : "Starting…", goal)
|
|
1124
1137
|
testsRef.current = { on: false, why: why.includes("agent runtime") ? "the agent runtime Scribe needs is not installed" : "ForgeQE's agent is off in the offline profile" }
|
|
1125
1138
|
let finalStatus = "fail"
|
|
1126
1139
|
try {
|
|
@@ -1135,7 +1148,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1135
1148
|
}
|
|
1136
1149
|
|
|
1137
1150
|
const askAgent = async (text: string, cfg: AppConfig) => {
|
|
1138
|
-
const controller = begin("Thinking…")
|
|
1151
|
+
const controller = begin("Thinking…", text)
|
|
1139
1152
|
const meterStart = meterSnapshot()
|
|
1140
1153
|
setThinking(true)
|
|
1141
1154
|
const setup = sessionSetup(cfg, options.repo)
|
|
@@ -1784,7 +1797,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1784
1797
|
<box style={{ flexDirection: "column", width: paneWidth, height: rowsAvailable, flexShrink: 0 }}>
|
|
1785
1798
|
<StatusBar running={running} who={insp.who} activity={activity} elapsed={elapsed} status={status} width={paneWidth} onStop={stop} progress={suiteProgress || planProgress(insp)} actions={paneButtons} />
|
|
1786
1799
|
<box style={{ height: 1, flexShrink: 0 }} />
|
|
1787
|
-
{plan ? <PlanPanel s={insp} width={paneWidth} height={plan} selected={sel} onFocus={focusOn("plan")} /> : null}
|
|
1800
|
+
{plan ? <PlanPanel s={insp} width={paneWidth} height={plan} selected={sel} onFocus={focusOn("plan")} animate={running} /> : null}
|
|
1788
1801
|
<ActivityPanel
|
|
1789
1802
|
lines={activityLines}
|
|
1790
1803
|
scrollRef={activityScroll}
|
package/src/tui/inspector.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { fitButtons, Spinner, type Button } from "./components.tsx"
|
|
|
6
6
|
import { filesByUse, type FileUse, type Inspector, type Progress } from "./session.ts"
|
|
7
7
|
import { clip, tailCut } from "./text.ts"
|
|
8
8
|
import { formatTokens, theme } from "./theme.ts"
|
|
9
|
-
import type
|
|
9
|
+
import { isMarker, type Line } from "./transcript.tsx"
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The split view's panels. Each is a rounded box with its title in the border, in its section's colour from the
|
|
@@ -171,18 +171,19 @@ const PROGRESS_MARK: Record<Progress["status"], [string, string]> = {
|
|
|
171
171
|
skipped: ["■", theme.faint],
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
/** `live`: a task in progress or a step running, drawn with a spinner while the request runs. */
|
|
175
|
+
export type PlanLine = { mark: string; color: string; text: string; textColor: string; live?: boolean }
|
|
175
176
|
|
|
176
177
|
export function planLines(s: Inspector): PlanLine[] {
|
|
177
178
|
const lines: PlanLine[] = []
|
|
178
179
|
for (const p of s.plan) {
|
|
179
180
|
const [mark, color] = PLAN_MARK[p.status] || PLAN_MARK.pending!
|
|
180
|
-
lines.push({ mark, color, text: p.text, textColor: p.status === "completed" || p.status === "cancelled" ? theme.muted : theme.text })
|
|
181
|
+
lines.push({ mark, color, text: p.text, textColor: p.status === "completed" || p.status === "cancelled" ? theme.muted : theme.text, ...(p.status === "in_progress" ? { live: true } : {}) })
|
|
181
182
|
}
|
|
182
183
|
if (s.progress.items.length && s.plan.length) lines.push({ mark: "─", color: theme.border, text: s.progress.title, textColor: theme.faint })
|
|
183
184
|
for (const i of s.progress.items) {
|
|
184
185
|
const [mark, color] = PROGRESS_MARK[i.status]
|
|
185
|
-
lines.push({ mark, color, text: i.detail ? `${i.label} — ${i.detail}` : i.label, textColor: i.status === "fail" ? theme.err : i.status === "running" ? theme.text : theme.muted })
|
|
186
|
+
lines.push({ mark, color, text: i.detail ? `${i.label} — ${i.detail}` : i.label, textColor: i.status === "fail" ? theme.err : i.status === "running" ? theme.text : theme.muted, ...(i.status === "running" ? { live: true } : {}) })
|
|
186
187
|
}
|
|
187
188
|
return lines
|
|
188
189
|
}
|
|
@@ -201,7 +202,7 @@ export function windowRows(total: number, rows: number, selected?: number): [num
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
/** The agent's to-do list, then the browser steps or suite cases; the latest that fit, or around the selected one. */
|
|
204
|
-
export function PlanPanel(props: { s: Inspector; width: number; height: number; selected?: number; onFocus?: () => void }) {
|
|
205
|
+
export function PlanPanel(props: { s: Inspector; width: number; height: number; selected?: number; onFocus?: () => void; animate?: boolean }) {
|
|
205
206
|
const { s, width, height } = props
|
|
206
207
|
const lines = planLines(s)
|
|
207
208
|
const done = s.plan.filter((p) => p.status === "completed").length
|
|
@@ -213,10 +214,9 @@ export function PlanPanel(props: { s: Inspector; width: number; height: number;
|
|
|
213
214
|
const active = first + i === props.selected
|
|
214
215
|
return (
|
|
215
216
|
<box key={first + i} style={{ flexDirection: "row", height: 1, flexShrink: 0, backgroundColor: active ? theme.button : undefined }}>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
</text>
|
|
217
|
+
{/* What is being worked on now spins, so it reads as live; a task left open after the request does not. */}
|
|
218
|
+
<box style={{ width: 2, flexShrink: 0 }}>{l.live && props.animate ? <Spinner kind="dots" color={l.color} /> : <text fg={l.color}>{l.mark}</text>}</box>
|
|
219
|
+
<text fg={active ? theme.text : l.textColor}>{clip(l.text, Math.max(10, width - 8))}</text>
|
|
220
220
|
</box>
|
|
221
221
|
)
|
|
222
222
|
})}
|
|
@@ -227,7 +227,8 @@ export function PlanPanel(props: { s: Inspector; width: number; height: number;
|
|
|
227
227
|
/** Each kind of activity line: its mark, the mark's colour, and the text's colour. */
|
|
228
228
|
const ACTIVITY_STYLE: Record<string, [string, string, string]> = {
|
|
229
229
|
running: ["◐", theme.warn, theme.text],
|
|
230
|
-
|
|
230
|
+
// A finished call: ✓, so it never reads as the ○ of a task still to do.
|
|
231
|
+
tool: ["✓", theme.ok, theme.muted],
|
|
231
232
|
file: ["✎", theme.accent, theme.text],
|
|
232
233
|
code: ["▸", theme.memory, theme.memory],
|
|
233
234
|
timing: ["+", theme.warn, theme.warn],
|
|
@@ -255,6 +256,42 @@ function ToolCall({ parts: [name, args], width }: { parts: [string, string]; wid
|
|
|
255
256
|
)
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
const ENDED: Record<NonNullable<Line["ended"]>, [string, string, string]> = {
|
|
260
|
+
done: ["✓", "Done", theme.ok],
|
|
261
|
+
fail: ["✗", "Failed", theme.err],
|
|
262
|
+
stopped: ["■", "Stopped", theme.warn],
|
|
263
|
+
idle: ["?", "Waiting for your answer", theme.warn],
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** "── 14:02 can you generate a defect report… ─────": where a request's activities start. */
|
|
267
|
+
function RequestStart({ line, width, first }: { line: Line; width: number; first: boolean }) {
|
|
268
|
+
const d = new Date(line.at || Date.now())
|
|
269
|
+
const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`
|
|
270
|
+
// The rule runs the whole row: the mark column and the text's.
|
|
271
|
+
const room = Math.max(12, (width || 60) + 3)
|
|
272
|
+
const text = clip(line.text, Math.max(8, room - time.length - 8))
|
|
273
|
+
const fill = Math.max(2, room - 3 - time.length - 2 - text.length - 1)
|
|
274
|
+
return (
|
|
275
|
+
<text style={{ height: 1, flexShrink: 0, marginTop: first ? 0 : 1 }}>
|
|
276
|
+
<span fg={theme.border}>{"── "}</span>
|
|
277
|
+
<span fg={theme.accent}>{time}</span>
|
|
278
|
+
<span fg={theme.text}>{` ${text} `}</span>
|
|
279
|
+
<span fg={theme.border}>{"─".repeat(fill)}</span>
|
|
280
|
+
</text>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** "✓ Done in 1m 12s · 16 activities": how a request ended. */
|
|
285
|
+
function RequestEnd({ line, width }: { line: Line; width: number }) {
|
|
286
|
+
const [mark, label, color] = ENDED[line.ended || "done"]
|
|
287
|
+
return (
|
|
288
|
+
<text style={{ height: 1, flexShrink: 0 }}>
|
|
289
|
+
<span fg={color}>{`${mark} ${label}`}</span>
|
|
290
|
+
<span fg={theme.faint}>{` ${clip(line.text, Math.max(8, (width || 60) - label.length))}`}</span>
|
|
291
|
+
</text>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
258
295
|
/** Rows an activity line may take: a call is one row; narration and results up to three. */
|
|
259
296
|
const NOTE_ROWS = 3
|
|
260
297
|
|
|
@@ -262,15 +299,24 @@ const NOTE_ROWS = 3
|
|
|
262
299
|
* One activity line, its mark in a column of its own so wrapped text stays under the text. `width` is the text's
|
|
263
300
|
* columns: a call is cut to one row and anything else to three, so a burst of output never takes the panel over.
|
|
264
301
|
*/
|
|
265
|
-
export function ActivityRow({ line, width = 0 }: { line: Line; width?: number }) {
|
|
302
|
+
export function ActivityRow({ line, width = 0, first = false }: { line: Line; width?: number; first?: boolean }) {
|
|
303
|
+
if (line.kind === "request") return <RequestStart line={line} width={width} first={first} />
|
|
304
|
+
if (line.kind === "requestEnd") return <RequestEnd line={line} width={width} />
|
|
266
305
|
const [mark, markColor, textColor] = ACTIVITY_STYLE[line.kind] || ["·", theme.faint, theme.muted]
|
|
267
306
|
const oneRow = line.kind === "tool" || line.kind === "running" || line.kind === "file"
|
|
268
307
|
const fit = (t: string, rows: number) => (width > 0 ? clip(t.replace(/\s+/g, " "), width * rows) : t)
|
|
269
308
|
return (
|
|
270
309
|
<box style={{ flexDirection: "row", flexShrink: 0 }}>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
{line.kind === "running" ? (
|
|
311
|
+
// A call in progress spins until its result takes the row.
|
|
312
|
+
<box style={{ width: 2, flexShrink: 0 }}>
|
|
313
|
+
<Spinner kind="dots" color={markColor} />
|
|
314
|
+
</box>
|
|
315
|
+
) : (
|
|
316
|
+
<text fg={markColor} style={{ width: 2, flexShrink: 0 }}>
|
|
317
|
+
{mark}
|
|
318
|
+
</text>
|
|
319
|
+
)}
|
|
274
320
|
{(line.kind === "tool" || line.kind === "running") && toolArgs(line.text) ? (
|
|
275
321
|
// "Azure DevOps · pipelines build (action list, project QI-Claims)": the call readable, its arguments quiet.
|
|
276
322
|
<ToolCall parts={toolArgs(line.text)!} width={width} />
|
|
@@ -292,9 +338,10 @@ export const ACTIVITY_FILTERS = [
|
|
|
292
338
|
] as const
|
|
293
339
|
export type ActivityFilter = (typeof ACTIVITY_FILTERS)[number]["id"]
|
|
294
340
|
|
|
341
|
+
/** The lines a filter keeps; where each request starts and ends always stays, so a filtered line is never orphaned. */
|
|
295
342
|
export function filterActivity(lines: Line[], filter: ActivityFilter): Line[] {
|
|
296
343
|
const kinds = ACTIVITY_FILTERS.find((f) => f.id === filter)?.kinds
|
|
297
|
-
return kinds ? lines.filter((l) => (kinds as readonly string[]).includes(l.kind)) : lines
|
|
344
|
+
return kinds ? lines.filter((l) => isMarker(l) || (kinds as readonly string[]).includes(l.kind)) : lines
|
|
298
345
|
}
|
|
299
346
|
|
|
300
347
|
/**
|
|
@@ -335,7 +382,9 @@ export function ActivityPanel(props: {
|
|
|
335
382
|
const room = Math.max(10, props.width - 12)
|
|
336
383
|
const shown = filterActivity(props.lines, filter)
|
|
337
384
|
const label = ACTIVITY_FILTERS.find((f) => f.id === filter)!.label
|
|
338
|
-
|
|
385
|
+
// Counts are of activities: the lines marking where requests start and end are not.
|
|
386
|
+
const real = (ls: Line[]) => ls.filter((l) => !isMarker(l)).length
|
|
387
|
+
const count = filter === "all" ? (real(shown) ? ` · ${real(shown)}` : "") : ` · ${label} ${real(shown)} of ${real(props.lines)}`
|
|
339
388
|
return (
|
|
340
389
|
<Panel title={`Activity${count}`} color={SECTION.activity} width={props.width} height={props.height} focused={props.focused} keys="↑↓ pgup pgdn scroll · esc chat" onFocus={props.onFocus}>
|
|
341
390
|
{props.focused || filter !== "all" ? (
|
|
@@ -354,11 +403,13 @@ export function ActivityPanel(props: {
|
|
|
354
403
|
</box>
|
|
355
404
|
) : null}
|
|
356
405
|
{props.now?.now ? (
|
|
357
|
-
<
|
|
358
|
-
<
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
406
|
+
<box style={{ flexDirection: "row", height: 1, flexShrink: 0 }}>
|
|
407
|
+
<text fg={theme.muted}>{"Now "}</text>
|
|
408
|
+
<box style={{ width: 2, flexShrink: 0 }}>
|
|
409
|
+
<Spinner kind="dots" color={theme.warn} />
|
|
410
|
+
</box>
|
|
411
|
+
<text fg={theme.text}>{clip(props.now.now, room)}</text>
|
|
412
|
+
</box>
|
|
362
413
|
) : null}
|
|
363
414
|
{props.now?.next ? (
|
|
364
415
|
<text style={{ height: 1, flexShrink: 0 }}>
|
|
@@ -367,16 +418,16 @@ export function ActivityPanel(props: {
|
|
|
367
418
|
<span fg={theme.muted}>{clip(props.now.next, room)}</span>
|
|
368
419
|
</text>
|
|
369
420
|
) : null}
|
|
370
|
-
{shown
|
|
421
|
+
{real(shown) ? (
|
|
371
422
|
<scrollbox ref={props.scrollRef} stickyScroll stickyStart="bottom" style={{ flexGrow: 1 }} focused={false}>
|
|
372
|
-
{shown.map((line) => (
|
|
373
|
-
<ActivityRow key={line.id} line={line} width={Math.max(10, props.width - 8)} />
|
|
423
|
+
{shown.map((line, i) => (
|
|
424
|
+
<ActivityRow key={line.id} line={line} width={Math.max(10, props.width - 8)} first={i === 0} />
|
|
374
425
|
))}
|
|
375
426
|
</scrollbox>
|
|
376
427
|
) : (
|
|
377
428
|
<box style={{ flexGrow: 1, justifyContent: "center", alignItems: "center" }}>
|
|
378
429
|
<text fg={theme.faint}>
|
|
379
|
-
{clip(props.lines
|
|
430
|
+
{clip(real(props.lines) ? `Nothing under ${label} yet` : "Tool calls, browser steps, and the agents' reasoning show here", Math.max(10, props.width - 4))}
|
|
380
431
|
</text>
|
|
381
432
|
</box>
|
|
382
433
|
)}
|
package/src/tui/transcript.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { clip } from "./text.ts"
|
|
|
17
17
|
|
|
18
18
|
export type Line = {
|
|
19
19
|
id: number
|
|
20
|
-
kind: "sys" | "user" | "thought" | "timing" | "memory" | "tool" | "file" | "code" | "ok" | "err" | "summary" | "suitesummary" | "reply" | "card" | "running"
|
|
20
|
+
kind: "sys" | "user" | "thought" | "timing" | "memory" | "tool" | "file" | "code" | "ok" | "err" | "summary" | "suitesummary" | "reply" | "card" | "running" | "request" | "requestEnd"
|
|
21
21
|
text: string
|
|
22
22
|
summary?: RunSummary
|
|
23
23
|
suite?: SuiteResult
|
|
@@ -29,6 +29,24 @@ export type Line = {
|
|
|
29
29
|
quiet?: boolean
|
|
30
30
|
/** A tool call's id: its "running" line becomes the call's result line when the call finishes. */
|
|
31
31
|
call?: string
|
|
32
|
+
/** A request's first line in Activity ("request"): when it started. */
|
|
33
|
+
at?: number
|
|
34
|
+
/** A request's last line in Activity ("requestEnd"): how it ended. */
|
|
35
|
+
ended?: "done" | "fail" | "stopped" | "idle"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Activity lines that mark where a request starts and ends; they are not activities themselves. */
|
|
39
|
+
export const isMarker = (l: Line) => l.kind === "request" || l.kind === "requestEnd"
|
|
40
|
+
|
|
41
|
+
/** The activities since the last request started, markers left out: what the request did. */
|
|
42
|
+
export function sinceRequest(lines: Line[]): number {
|
|
43
|
+
let n = 0
|
|
44
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
45
|
+
const l = lines[i]!
|
|
46
|
+
if (l.kind === "request") break
|
|
47
|
+
if (l.pane === "activity" && !isMarker(l)) n++
|
|
48
|
+
}
|
|
49
|
+
return n
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
/** Lines kept: the oldest quiet ones go first, so a busy run never pushes the conversation out. */
|