@capagents/forgeqe 1.0.3 → 1.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/App.tsx +7 -9
- package/src/tui/inspector.tsx +17 -55
package/package.json
CHANGED
package/src/tui/App.tsx
CHANGED
|
@@ -83,6 +83,9 @@ export const SPLIT_MIN = 120
|
|
|
83
83
|
export function App({ options }: { options: TuiOptions }) {
|
|
84
84
|
const renderer = useRenderer()
|
|
85
85
|
const { width, height } = useTerminalDimensions()
|
|
86
|
+
/** The terminal's width when a request ends (callbacks outlive the render they were made in). */
|
|
87
|
+
const widthRef = useRef(width)
|
|
88
|
+
widthRef.current = width
|
|
86
89
|
const scrollRef = useRef<ScrollBoxRenderable>(null)
|
|
87
90
|
const activityScroll = useRef<ScrollBoxRenderable>(null)
|
|
88
91
|
const submitRef = useRef<(value: string) => void>(() => {})
|
|
@@ -775,7 +778,8 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
775
778
|
} finally {
|
|
776
779
|
pushProduced()
|
|
777
780
|
// What this request used and cost in total: ForgeQE's agent, browser runs, Scribe, and the reviewer.
|
|
778
|
-
|
|
781
|
+
// The split view's session and tokens panels show this; only the one-pane view needs it in the chat.
|
|
782
|
+
const spent = widthRef.current < SPLIT_MIN ? requestLine(meterStart) : ""
|
|
779
783
|
if (spent) push("sys", spent, "chat")
|
|
780
784
|
finish(finalStatus)
|
|
781
785
|
}
|
|
@@ -1071,17 +1075,11 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1071
1075
|
}
|
|
1072
1076
|
const llm = cfgNow?.llm
|
|
1073
1077
|
const sessionInfo: SessionInfo = {
|
|
1074
|
-
profile,
|
|
1075
|
-
model: model || (cfgNow ? modelLabel(cfgNow) : ""),
|
|
1076
|
-
auth: !llm ? "–" : llm.auth === "entra" ? "Entra ID, no key" : llm.apiKeyEnv ? `API key in ${llm.apiKeyEnv}` : llm.provider === "mock" ? "offline demo" : llm.provider,
|
|
1078
|
+
model: cfgNow ? `${llm?.provider === "mock" ? "offline demo" : llm?.deployment || llm?.model || "none"}${profile ? ` (${profile})` : ""}` : "…",
|
|
1077
1079
|
cwd: shortCwd(),
|
|
1078
|
-
tests: testsRef.current.on ? "written to this folder" : requests.current ? `not written (${testsRef.current.why || "off"})` : "set on the first request",
|
|
1079
|
-
browser: `${headed ? "visible" : "headless"} · up to ${maxSteps} steps${options.url ? ` · ${hostOf(options.url)}` : ""}`,
|
|
1080
1080
|
...(cfgNow?.domain ? { domain: `${cfgNow.domain.industry}${cfgNow.domain.regulations.length ? ` (${cfgNow.domain.regulations.join(", ")})` : ""}` } : {}),
|
|
1081
|
-
...(llm?.priceWarning && llm.price ? { price: `${llm.price.input}/${llm.price.output} ${llm.price.currency} per M in/out from llms.json, not the list price` } : {}),
|
|
1082
1081
|
started: sessionStarted.current,
|
|
1083
1082
|
requests: requests.current,
|
|
1084
|
-
total: spent.roles.length ? { usage: spent.total, cost: spent.cost } : undefined,
|
|
1085
1083
|
}
|
|
1086
1084
|
const contextPanel: ContextInfo = {
|
|
1087
1085
|
tokens: insp.context,
|
|
@@ -1119,7 +1117,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1119
1117
|
<box style={{ flexDirection: "row", height: bottom, flexShrink: 0 }}>
|
|
1120
1118
|
<FilesPanel s={insp} width={third} height={bottom} />
|
|
1121
1119
|
<ContextPanel c={contextPanel} width={third} height={bottom} />
|
|
1122
|
-
<TokensPanel
|
|
1120
|
+
<TokensPanel total={spent.total} width={paneWidth - 2 * third} height={bottom} />
|
|
1123
1121
|
</box>
|
|
1124
1122
|
</box>
|
|
1125
1123
|
)
|
package/src/tui/inspector.tsx
CHANGED
|
@@ -2,7 +2,6 @@ import type { ScrollBoxRenderable } from "@opentui/core"
|
|
|
2
2
|
import type { ReactNode, RefObject } from "react"
|
|
3
3
|
|
|
4
4
|
import type { Usage } from "../types.ts"
|
|
5
|
-
import { formatCost, type Cost, type Role } from "../usage.ts"
|
|
6
5
|
import { Spinner } from "./components.tsx"
|
|
7
6
|
import { filesByUse, type FileUse, type Inspector, type Progress } from "./session.ts"
|
|
8
7
|
import { clip, tailCut } from "./text.ts"
|
|
@@ -22,13 +21,6 @@ export const SECTION = {
|
|
|
22
21
|
tokens: theme.ok,
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
const ROLE_COLOR: Record<Role, string> = {
|
|
26
|
-
"ForgeQE's agent": theme.accent,
|
|
27
|
-
"browser agent": theme.thought,
|
|
28
|
-
Scribe: theme.ok,
|
|
29
|
-
reviewer: theme.warn,
|
|
30
|
-
judge: theme.memory,
|
|
31
|
-
}
|
|
32
24
|
|
|
33
25
|
/** Rows a panel's border takes: top and bottom. */
|
|
34
26
|
export const PANEL_CHROME = 2
|
|
@@ -57,47 +49,36 @@ export function Panel(props: { title: string; color: string; width?: number; hei
|
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
export type SessionInfo = {
|
|
60
|
-
|
|
52
|
+
/** "gpt-5.4 (azure-gpt54)": the model, and the llms.json profile it comes from. */
|
|
61
53
|
model: string
|
|
62
|
-
auth: string
|
|
63
54
|
cwd: string
|
|
64
|
-
tests: string
|
|
65
|
-
browser: string
|
|
66
55
|
domain?: string
|
|
67
|
-
/** The profile's price is set in llms.json and differs from the model's list price. */
|
|
68
|
-
price?: string
|
|
69
56
|
started?: number
|
|
70
57
|
requests: number
|
|
71
|
-
total?: { usage: Usage; cost?: Cost }
|
|
72
58
|
}
|
|
73
59
|
|
|
74
60
|
/** Rows SessionPanel takes, border included. */
|
|
75
61
|
export function sessionRows(info: SessionInfo): number {
|
|
76
|
-
return
|
|
62
|
+
return 3 + (info.domain ? 1 : 0) + PANEL_CHROME
|
|
77
63
|
}
|
|
78
64
|
|
|
79
|
-
/** Top of the left pane: what
|
|
65
|
+
/** Top of the left pane: what the session works with, nothing more; tokens are in their own panel. */
|
|
80
66
|
export function SessionPanel({ info, width }: { info: SessionInfo; width: number }) {
|
|
81
|
-
const room = Math.max(10, width -
|
|
67
|
+
const room = Math.max(10, width - 15)
|
|
82
68
|
const row = (label: string, value: string, color: string = theme.text) => (
|
|
83
69
|
<text key={label}>
|
|
84
|
-
<span fg={theme.muted}>{label.padEnd(
|
|
70
|
+
<span fg={theme.muted}>{label.padEnd(10)}</span>
|
|
85
71
|
<span fg={color}>{clip(value, room)}</span>
|
|
86
72
|
</text>
|
|
87
73
|
)
|
|
88
74
|
const started = info.started ? new Date(info.started) : null
|
|
89
75
|
const since = started ? ` · since ${String(started.getHours()).padStart(2, "0")}:${String(started.getMinutes()).padStart(2, "0")}` : ""
|
|
90
|
-
const used = info.total ? `${formatTokens(info.total.usage.input + info.total.usage.output)} tokens${info.total.cost ? ` · ${formatCost(info.total.cost)}` : ""}` : "nothing yet"
|
|
91
76
|
return (
|
|
92
77
|
<Panel title={`Session${since}`} color={SECTION.session} width={width}>
|
|
93
|
-
{row("Model",
|
|
94
|
-
{row("Sign-in", info.auth, info.auth.startsWith("Entra") ? theme.ok : theme.text)}
|
|
78
|
+
{row("Model", info.model)}
|
|
95
79
|
{row("Folder", tailCut(info.cwd, room))}
|
|
96
|
-
{row("Tests", info.tests, info.tests.startsWith("written") ? theme.ok : theme.muted)}
|
|
97
|
-
{row("Browser", info.browser)}
|
|
98
80
|
{info.domain ? row("Domain", info.domain, theme.memory) : null}
|
|
99
|
-
{
|
|
100
|
-
{row("Used", `${info.requests} request${info.requests === 1 ? "" : "s"} · ${used}`, theme.muted)}
|
|
81
|
+
{row("Requests", String(info.requests), theme.muted)}
|
|
101
82
|
</Panel>
|
|
102
83
|
)
|
|
103
84
|
}
|
|
@@ -309,40 +290,21 @@ export function ContextPanel({ c, width, height }: { c: ContextInfo; width: numb
|
|
|
309
290
|
)
|
|
310
291
|
}
|
|
311
292
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
live?: number
|
|
317
|
-
width: number
|
|
318
|
-
height: number
|
|
319
|
-
}) {
|
|
320
|
-
const { roles, total, width } = props
|
|
321
|
-
// The label takes what the token count (7) and, when priced, the cost (two spaces and up to 7) leave.
|
|
322
|
-
const priced = roles.some((r) => r.cost) || Boolean(total.cost)
|
|
323
|
-
const labelWidth = Math.max(6, width - 4 - 7 - (priced ? 9 : 0))
|
|
324
|
-
const line = (label: string, labelColor: string, u: Usage, c: Cost | undefined, valueColor: string = theme.text) => (
|
|
293
|
+
/** What the session used, in tokens: the total and how it splits. Costs are left to reports and `forgeqe models`. */
|
|
294
|
+
export function TokensPanel(props: { total: Usage; width: number; height: number }) {
|
|
295
|
+
const { total, width } = props
|
|
296
|
+
const line = (label: string, n: number, color: string = theme.text) => (
|
|
325
297
|
<text key={label}>
|
|
326
|
-
<span fg={
|
|
327
|
-
<span fg={
|
|
328
|
-
{c ? <span fg={theme.muted}>{` ${formatCost(c)}`}</span> : null}
|
|
298
|
+
<span fg={theme.muted}>{label.padEnd(10)}</span>
|
|
299
|
+
<span fg={color}>{formatTokens(n).padStart(Math.max(7, Math.min(10, width - 14)))}</span>
|
|
329
300
|
</text>
|
|
330
301
|
)
|
|
331
302
|
return (
|
|
332
303
|
<Panel title="Tokens" color={SECTION.tokens} width={width} height={props.height}>
|
|
333
|
-
{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
</text>
|
|
338
|
-
) : null}
|
|
339
|
-
{roles.length ? roles.map((r) => line(r.role, ROLE_COLOR[r.role] || theme.text, r.usage, r.cost)) : props.live ? null : <text fg={theme.faint}>{"None used yet"}</text>}
|
|
340
|
-
{roles.length > 1 ? line("Total", theme.text, total.usage, total.cost, theme.ok) : null}
|
|
341
|
-
{roles.length ? (
|
|
342
|
-
<text fg={theme.faint}>
|
|
343
|
-
{clip(`in ${formatTokens(total.usage.input)} · out ${formatTokens(total.usage.output)}${total.usage.cached ? ` · ${formatTokens(total.usage.cached)} cached` : ""}`, Math.max(10, width - 4))}
|
|
344
|
-
</text>
|
|
345
|
-
) : null}
|
|
304
|
+
{line("Total", total.input + total.output, theme.ok)}
|
|
305
|
+
{line("Input", total.input)}
|
|
306
|
+
{line("Output", total.output)}
|
|
307
|
+
{line("Cached", total.cached || 0, theme.muted)}
|
|
346
308
|
</Panel>
|
|
347
309
|
)
|
|
348
310
|
}
|