@oh-my-pi/pi-tui 18.11.1 → 18.11.2
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/CHANGELOG.md +27 -110
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/prompt/composer.d.ts +20 -0
- package/dist/types/prompt/custom-editor.d.ts +2 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/terminal.d.ts +1 -1
- package/dist/types/tui.d.ts +9 -0
- package/package.json +9 -9
- package/src/apps/git/git-tui.ts +27 -10
- package/src/apps/git/sidebar.ts +5 -1
- package/src/autocomplete.ts +7 -0
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/assistant-message.ts +3 -3
- package/src/chat/table-chart.ts +24 -57
- package/src/chrome/transcript-container.ts +10 -0
- package/src/components/settings-list.ts +2 -4
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/btw-history-panel.ts +30 -23
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/settings-selector.ts +3 -0
- package/src/prompt/composer.ts +158 -11
- package/src/prompt/custom-editor.ts +19 -1
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/status-line/component.ts +18 -10
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/terminal.ts +97 -24
- package/src/tools/eval.ts +40 -7
- package/src/tui.ts +28 -7
package/src/charts/chart-svg.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Draws a {@link ChartSpec} as a standalone SVG document colored only with
|
|
3
3
|
* figure tokens — `var(--fg)`, `var(--muted)`, `var(--border)`,
|
|
4
4
|
* `var(--accent)`, `var(--surface)`, `var(--c1)`…`var(--c6)` — on a
|
|
5
|
-
* transparent background — the token vocabulary of assistant ```svg figures
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* transparent background — the token vocabulary of assistant ```svg figures:
|
|
6
|
+
* the host resolves them against the live theme (`prepareSvg`) for the
|
|
7
|
+
* rasterized terminal image, and a TSP terminal draws the chart as an ```svg
|
|
8
|
+
* figure in the palette it shows. The root carries {@link chartAlt} as its
|
|
9
|
+
* `aria-label`.
|
|
8
10
|
*
|
|
9
11
|
* Styled after Apple's charts and Tern's UI: a UI sans (Geist where Tern
|
|
10
12
|
* provides it), muted labels, hairline gridlines that recede behind the
|
|
@@ -236,15 +238,8 @@ interface Tier {
|
|
|
236
238
|
readonly high: number;
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
/**
|
|
240
|
-
export
|
|
241
|
-
readonly svg: string;
|
|
242
|
-
readonly width: number;
|
|
243
|
-
readonly height: number;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** The SVG document for `spec`. */
|
|
247
|
-
export function renderChartSvg(spec: ChartSpec): ChartSvg {
|
|
241
|
+
/** The SVG document for `spec`, labeled for screen readers with {@link chartAlt}. */
|
|
242
|
+
export function renderChartSvg(spec: ChartSpec): string {
|
|
248
243
|
const layer = drawChart(spec);
|
|
249
244
|
const header = drawHeader(spec, layer);
|
|
250
245
|
const head = matrixHead(spec);
|
|
@@ -268,8 +263,8 @@ export function renderChartSvg(spec: ChartSpec): ChartSvg {
|
|
|
268
263
|
height += 20;
|
|
269
264
|
}
|
|
270
265
|
height = Math.ceil(height + PAD);
|
|
271
|
-
|
|
272
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" font-family="${FONT}" font-size="${LABEL_SIZE}">`,
|
|
266
|
+
return [
|
|
267
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" role="img" aria-label="${escapeXml(chartAlt(spec))}" font-family="${FONT}" font-size="${LABEL_SIZE}">`,
|
|
273
268
|
layer.body.includes(`url(#${INK_MASK})`) ? INK_DEFS : "",
|
|
274
269
|
`<g transform="translate(0 ${PAD})">${header.body}</g>`,
|
|
275
270
|
head.body && `<g transform="translate(0 ${num(PAD + header.height)})">${head.body}</g>`,
|
|
@@ -278,11 +273,10 @@ export function renderChartSvg(spec: ChartSpec): ChartSvg {
|
|
|
278
273
|
caption,
|
|
279
274
|
"</svg>",
|
|
280
275
|
].join("");
|
|
281
|
-
return { svg, width: WIDTH, height };
|
|
282
276
|
}
|
|
283
277
|
|
|
284
|
-
/** One-line description of the chart, for screen readers
|
|
285
|
-
|
|
278
|
+
/** One-line description of the chart, for screen readers. */
|
|
279
|
+
function chartAlt(spec: ChartSpec): string {
|
|
286
280
|
const joint =
|
|
287
281
|
spec.kind === "stacked" ? " + " : spec.kind === "slope" ? " → " : spec.kind === "butterfly" ? " vs " : ", ";
|
|
288
282
|
const names = spec.series.map(entry => entry.name).join(joint);
|
|
@@ -4432,6 +4426,13 @@ function token(name: string): string {
|
|
|
4432
4426
|
return `var(--${name})`;
|
|
4433
4427
|
}
|
|
4434
4428
|
|
|
4429
|
+
/** `raw` safe as XML text or a double-quoted attribute value. */
|
|
4430
|
+
function escapeXml(raw: string): string {
|
|
4431
|
+
return raw.replace(/[&<>"]/g, char =>
|
|
4432
|
+
char === "&" ? "&" : char === "<" ? "<" : char === ">" ? ">" : """,
|
|
4433
|
+
);
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4435
4436
|
function num(value: number): string {
|
|
4436
4437
|
return Number(value.toFixed(1)).toString();
|
|
4437
4438
|
}
|
|
@@ -4469,10 +4470,6 @@ function text(x: number, y: number, content: string | readonly TextRun[], style:
|
|
|
4469
4470
|
style.weight ? `font-weight="${style.weight}"` : "",
|
|
4470
4471
|
style.italic ? `font-style="italic"` : "",
|
|
4471
4472
|
].filter(Boolean);
|
|
4472
|
-
const escape = (raw: string) =>
|
|
4473
|
-
raw.replace(/[&<>"]/g, char =>
|
|
4474
|
-
char === "&" ? "&" : char === "<" ? "<" : char === ">" ? ">" : """,
|
|
4475
|
-
);
|
|
4476
4473
|
const body = runs
|
|
4477
4474
|
.filter(run => run.content)
|
|
4478
4475
|
.map(run => {
|
|
@@ -4480,7 +4477,7 @@ function text(x: number, y: number, content: string | readonly TextRun[], style:
|
|
|
4480
4477
|
run.fill ? ` fill="${token(run.fill)}"` : "",
|
|
4481
4478
|
run.weight ? ` font-weight="${run.weight}"` : "",
|
|
4482
4479
|
].join("");
|
|
4483
|
-
return own ? `<tspan${own}>${
|
|
4480
|
+
return own ? `<tspan${own}>${escapeXml(run.content)}</tspan>` : escapeXml(run.content);
|
|
4484
4481
|
})
|
|
4485
4482
|
.join("");
|
|
4486
4483
|
return `<text ${attrs.join(" ")}>${body}</text>`;
|
|
@@ -39,7 +39,7 @@ import { formatTurnUsage, type TurnUsageSummary } from "../overlays/usage-row";
|
|
|
39
39
|
import { FigureMarkdown } from "./figure-markdown";
|
|
40
40
|
import { svgFigureRendering } from "./svg-figure";
|
|
41
41
|
import { hasSvgFence } from "./svg-source";
|
|
42
|
-
import {
|
|
42
|
+
import { chartFence, hasChartTable, lookupTableChart, splitTableCharts } from "./table-chart";
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Max wrapped rows of a turn-ending provider error rendered inline in the
|
|
@@ -919,7 +919,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
919
919
|
children.push(markdown(`t${index}`, `t${index}`, linked(source, streaming), streaming));
|
|
920
920
|
continue;
|
|
921
921
|
}
|
|
922
|
-
// A chart follows its table as an
|
|
922
|
+
// A chart follows its table as an ```svg figure the terminal colors; the prose splits around it.
|
|
923
923
|
const segments = splitTableCharts(source, streaming);
|
|
924
924
|
segments.forEach((segment, part) => {
|
|
925
925
|
const slot = part === 0 ? `t${index}` : `t${index}.${part}`;
|
|
@@ -930,7 +930,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
930
930
|
}
|
|
931
931
|
const chart = lookupTableChart(segment.table);
|
|
932
932
|
if (chart instanceof Promise) this.#awaitChart(chart);
|
|
933
|
-
else if (chart) children.push(
|
|
933
|
+
else if (chart) children.push(markdown(slot, slot, chartFence(chart), false));
|
|
934
934
|
});
|
|
935
935
|
} else if (content.type === "thinking") {
|
|
936
936
|
const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
|
package/src/chat/table-chart.ts
CHANGED
|
@@ -5,24 +5,19 @@
|
|
|
5
5
|
* and draws that chart — locally under `always`, through the host's
|
|
6
6
|
* model-backed {@link TableChartPlanner} under `smart` for the tables
|
|
7
7
|
* {@link shouldJudge} picks — and caches it per table source. {@link TableChartFigure} shows it
|
|
8
|
-
* as a rasterized
|
|
9
|
-
* terminal the SVG
|
|
10
|
-
*
|
|
8
|
+
* as a terminal image rasterized in omp's theme; {@link chartFence} hands a
|
|
9
|
+
* TSP terminal the SVG as an ```svg fence, which it draws in the palette it
|
|
10
|
+
* shows. Either way a chart follows theme switches like the prose around it.
|
|
11
11
|
*/
|
|
12
12
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import type { Token, Tokens } from "@oh-my-pi/pi-utils/marked";
|
|
14
14
|
import { type ChartPlan, buildChart, planChart, shouldJudge, worthCharting } from "../charts/chart-plan";
|
|
15
|
-
import {
|
|
15
|
+
import { renderChartSvg } from "../charts/chart-svg";
|
|
16
16
|
import { analyzeTable, type TableAnalysis } from "../charts/table-data";
|
|
17
17
|
import type { ImageBudget } from "../components/image";
|
|
18
18
|
import { lexDocument } from "../components/markdown";
|
|
19
|
-
import { keyed } from "../native/describe";
|
|
20
|
-
import { nativeImageNode } from "../native/blobs";
|
|
21
|
-
import type { NativeNode } from "../native/node";
|
|
22
|
-
import { getThemeEpoch } from "../theme";
|
|
23
19
|
import type { Component } from "../tui";
|
|
24
|
-
import { SvgFigure
|
|
25
|
-
import { prepareSvg } from "./svg-source";
|
|
20
|
+
import { SvgFigure } from "./svg-figure";
|
|
26
21
|
|
|
27
22
|
/**
|
|
28
23
|
* `smart`: a model refines the local guess where meaning decides (kind,
|
|
@@ -47,18 +42,11 @@ export interface TableChartRequest {
|
|
|
47
42
|
*/
|
|
48
43
|
export type TableChartPlanner = (request: TableChartRequest) => Promise<ChartPlan | null>;
|
|
49
44
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/** Size in SVG user units. */
|
|
56
|
-
readonly width: number;
|
|
57
|
-
readonly height: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The chart under a table: drawn, none, or a smart pick on its way (the promise settles once the cache holds it). */
|
|
61
|
-
export type ChartLookup = TableChart | null | Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* The chart under a table: its SVG, colored with figure tokens (`var(--accent)`, …)
|
|
47
|
+
* still to resolve; none; or a smart pick on its way (the promise settles once the cache holds it).
|
|
48
|
+
*/
|
|
49
|
+
export type ChartLookup = string | null | Promise<void>;
|
|
62
50
|
|
|
63
51
|
/** One run of a Markdown block: prose (ending at a charted table), or the chart drawn after that table. */
|
|
64
52
|
export type ChartSegment =
|
|
@@ -67,8 +55,6 @@ export type ChartSegment =
|
|
|
67
55
|
|
|
68
56
|
/** Tables cached per source; a long session keeps its recent charts without growing without bound. */
|
|
69
57
|
const CACHE_LIMIT = 256;
|
|
70
|
-
/** SVG user units per terminal column (16 per row on 1:2 cells), sizing native images. */
|
|
71
|
-
const UNITS_PER_COLUMN = 8;
|
|
72
58
|
/** A GFM delimiter row (`|---|:--:|`), the cheap test before lexing a block. */
|
|
73
59
|
const DELIMITER_ROW = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*\|/m;
|
|
74
60
|
|
|
@@ -78,20 +64,14 @@ interface ChartEntry {
|
|
|
78
64
|
readonly guess: ChartPlan | undefined;
|
|
79
65
|
/** The host's planner picks this table's chart. */
|
|
80
66
|
readonly smart: boolean;
|
|
81
|
-
/** `undefined` until drawn (or judged chartless). */
|
|
82
|
-
chart?:
|
|
67
|
+
/** The chart's SVG; `undefined` until drawn (or judged chartless). */
|
|
68
|
+
chart?: string | null;
|
|
83
69
|
pending?: Promise<void>;
|
|
84
70
|
}
|
|
85
71
|
|
|
86
72
|
let chartMode: TableChartMode = "off";
|
|
87
73
|
let chartPlanner: TableChartPlanner | undefined;
|
|
88
74
|
const charts = new Map<string, ChartEntry>();
|
|
89
|
-
/** The theme-resolved image node owning a chart's native bytes. */
|
|
90
|
-
const kNativeImage = Symbol("tableChart.nativeImage");
|
|
91
|
-
|
|
92
|
-
interface NativeTagged extends TableChart {
|
|
93
|
-
[kNativeImage]?: { epoch: number; image: NativeNode };
|
|
94
|
-
}
|
|
95
75
|
|
|
96
76
|
/** Set how tables become charts (the host's `tui.autoGraph`) and its `smart` planner; the host rebuilds the transcript after. */
|
|
97
77
|
export function setTableCharts(mode: TableChartMode, planner?: TableChartPlanner): void {
|
|
@@ -188,34 +168,21 @@ function chartEntry(table: Tokens.Table): ChartEntry {
|
|
|
188
168
|
* Draw `plan`. A guess must also pass {@link worthCharting}; a model's pick
|
|
189
169
|
* already judged the chart worth drawing.
|
|
190
170
|
*/
|
|
191
|
-
function drawChart(analysis: TableAnalysis, plan: ChartPlan, guessed = false):
|
|
171
|
+
function drawChart(analysis: TableAnalysis, plan: ChartPlan, guessed = false): string | null {
|
|
192
172
|
const spec = buildChart(analysis, plan);
|
|
193
173
|
if (!spec || (guessed && !worthCharting(spec))) return null;
|
|
194
|
-
return
|
|
174
|
+
return renderChartSvg(spec);
|
|
195
175
|
}
|
|
196
176
|
|
|
197
177
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
178
|
+
* Chart `svg` as a Markdown ```svg fence, for a TSP terminal's `md` node. Its
|
|
179
|
+
* figure tokens stay unresolved: the terminal draws the fence as a figure in
|
|
180
|
+
* the palette it shows the prose in, so the chart cannot fall out of step with
|
|
181
|
+
* the page when omp's own idea of the appearance does, and a theme switch
|
|
182
|
+
* recolors it without a resend.
|
|
200
183
|
*/
|
|
201
|
-
export function
|
|
202
|
-
|
|
203
|
-
const tagged: NativeTagged = chart;
|
|
204
|
-
let entry = tagged[kNativeImage];
|
|
205
|
-
if (entry?.epoch !== epoch) {
|
|
206
|
-
const svg = prepareSvg(chart.svg, svgFigurePalette());
|
|
207
|
-
entry = {
|
|
208
|
-
epoch,
|
|
209
|
-
image: nativeImageNode(new TextEncoder().encode(svg), "image/svg+xml", {
|
|
210
|
-
alt: chart.alt,
|
|
211
|
-
w: chart.width,
|
|
212
|
-
h: chart.height,
|
|
213
|
-
max: { w: `${Math.round(chart.width / UNITS_PER_COLUMN)}ch` },
|
|
214
|
-
}),
|
|
215
|
-
};
|
|
216
|
-
tagged[kNativeImage] = entry;
|
|
217
|
-
}
|
|
218
|
-
return keyed(entry.image, key);
|
|
184
|
+
export function chartFence(svg: string): string {
|
|
185
|
+
return `\`\`\`svg\n${svg}\n\`\`\``;
|
|
219
186
|
}
|
|
220
187
|
|
|
221
188
|
export interface TableChartFigureOptions {
|
|
@@ -235,8 +202,8 @@ export class TableChartFigure implements Component {
|
|
|
235
202
|
readonly #options: TableChartFigureOptions;
|
|
236
203
|
readonly #figure: SvgFigure;
|
|
237
204
|
#table: Tokens.Table | undefined;
|
|
238
|
-
/** `undefined` while not looked up or waiting for a pick. */
|
|
239
|
-
#chart:
|
|
205
|
+
/** The chart's SVG; `undefined` while not looked up or waiting for a pick. */
|
|
206
|
+
#chart: string | null | undefined;
|
|
240
207
|
#waiting = false;
|
|
241
208
|
#disposed = false;
|
|
242
209
|
|
|
@@ -289,6 +256,6 @@ export class TableChartFigure implements Component {
|
|
|
289
256
|
return;
|
|
290
257
|
}
|
|
291
258
|
this.#chart = found;
|
|
292
|
-
if (found) this.#figure.update(found
|
|
259
|
+
if (found) this.#figure.update(found, true);
|
|
293
260
|
}
|
|
294
261
|
}
|
|
@@ -412,6 +412,16 @@ export class TranscriptContainer extends Container {
|
|
|
412
412
|
return this.#entries.map(entry => entry.emitted);
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/** Whether any block is still streaming or otherwise unfinalized. */
|
|
416
|
+
hasActiveBlock(): boolean {
|
|
417
|
+
this.#syncEntries();
|
|
418
|
+
this.#settleFinalized();
|
|
419
|
+
for (let index = this.#frontier; index < this.#entries.length; index++) {
|
|
420
|
+
if (this.#entries[index]!.state === "active") return true;
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
415
425
|
/** Whether visible active capacity and live-block memory permit another admission. */
|
|
416
426
|
canAdmit(rows: number): boolean {
|
|
417
427
|
const active = this.#entries.filter(entry => entry.state === "active").length;
|
|
@@ -4,6 +4,7 @@ import { fuzzyFilter } from "../fuzzy";
|
|
|
4
4
|
import { getKeybindings } from "../keybindings";
|
|
5
5
|
import { extractPrintableText } from "../keys";
|
|
6
6
|
import type { MouseRoutable, SgrMouseEvent } from "../mouse";
|
|
7
|
+
import { slugify } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import type { TspPrefsControl, TspPrefsRow, TspPrefsSection, TspProps, TspSpan } from "@oh-my-pi/pi-wire";
|
|
8
9
|
import { col, node, span } from "../native/describe";
|
|
9
10
|
import { sameItems, sameProps } from "../native/memo";
|
|
@@ -78,10 +79,7 @@ export interface PrefsSectionRef {
|
|
|
78
79
|
|
|
79
80
|
/** The section id for a section title (`"Status Line"` → `"status-line"`). */
|
|
80
81
|
export function prefsSectionId(title: string): string {
|
|
81
|
-
return title
|
|
82
|
-
.toLowerCase()
|
|
83
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
84
|
-
.replace(/^-|-$/g, "");
|
|
82
|
+
return slugify(title);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
/** A setting as a native settings row, given its typed control; `undefined` for headings. */
|
|
@@ -4,6 +4,10 @@ import type { ObservableSession } from "./session-observer-registry";
|
|
|
4
4
|
|
|
5
5
|
export type AgentMetrics = AgentMetricsSummary;
|
|
6
6
|
|
|
7
|
+
export interface AgentCostSnapshot {
|
|
8
|
+
cost: number | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export interface AggregateMetrics extends AgentMetrics {
|
|
8
12
|
reportedAgents: number;
|
|
9
13
|
/** Rows whose duration is an observer-measured active runtime. */
|
|
@@ -203,15 +207,28 @@ export function hubFallbackStatsSession<TRecord extends AgentRecordLike>(
|
|
|
203
207
|
return session && typeof session.getSessionStats === "function" ? session : undefined;
|
|
204
208
|
}
|
|
205
209
|
|
|
206
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Shared row policy for the Agent Hub and the status-line cost total.
|
|
212
|
+
* Non-cost metrics come from live progress, then persisted history, then the retained
|
|
213
|
+
* snapshot (parked rows only), then the live session. Cost always overlays the cumulative
|
|
214
|
+
* direct-session bill: live, then `history.directCost` (written at park), then retained.
|
|
215
|
+
* Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never wins.
|
|
216
|
+
*/
|
|
217
|
+
function projectRowMetrics<TMetrics extends AgentCostSnapshot>(
|
|
218
|
+
ref: AgentRecordLike,
|
|
219
|
+
observed: ObservableSession | undefined,
|
|
220
|
+
live: TMetrics | undefined,
|
|
221
|
+
retained: TMetrics | undefined,
|
|
222
|
+
hasLiveSession: boolean,
|
|
223
|
+
): TMetrics | AgentMetrics | undefined {
|
|
224
|
+
const cost = live?.cost ?? ref.history?.directCost ?? retained?.cost;
|
|
225
|
+
const metrics = observed?.progress
|
|
226
|
+
? progressMetrics(observed)
|
|
227
|
+
: (ref.history?.metrics ?? (!hasLiveSession ? retained : undefined) ?? live);
|
|
207
228
|
return metrics && cost !== undefined ? { ...metrics, cost } : metrics;
|
|
208
229
|
}
|
|
209
230
|
|
|
210
|
-
/**
|
|
211
|
-
* Preserve progress/history for non-cost metrics, overlaying cumulative direct-session cost.
|
|
212
|
-
* Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never overrides live
|
|
213
|
-
* progress; only the cumulative direct-cost sources do.
|
|
214
|
-
*/
|
|
231
|
+
/** Full Agent Hub row metrics; see {@link projectRowMetrics} for source precedence. */
|
|
215
232
|
export function hubRowMetrics<TRecord extends AgentRecordLike>(
|
|
216
233
|
ref: TRecord,
|
|
217
234
|
observed: ObservableSession | undefined,
|
|
@@ -220,12 +237,46 @@ export function hubRowMetrics<TRecord extends AgentRecordLike>(
|
|
|
220
237
|
const session = hubFallbackStatsSession(ref, observed);
|
|
221
238
|
const liveEntry = session ? sessionMetrics.get(session) : undefined;
|
|
222
239
|
const retainedEntry = sessionMetrics.get(ref);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
return projectRowMetrics(ref, observed, liveEntry?.metrics, retainedEntry?.metrics, session !== undefined);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Cost-only projection for frequent status updates; full roster metrics keep their own cache. */
|
|
244
|
+
export function aggregateCost<TRecord extends AgentRecordLike>(args: {
|
|
245
|
+
rows: readonly TRecord[];
|
|
246
|
+
observedById: ReadonlyMap<string, ObservableSession>;
|
|
247
|
+
sessionCosts: WeakMap<object, AgentCostSnapshot>;
|
|
248
|
+
}): number {
|
|
249
|
+
let total = 0;
|
|
250
|
+
const countedSessions = new Set<NonNullable<AgentRecordLike["session"]>>();
|
|
251
|
+
for (const ref of args.rows) {
|
|
252
|
+
const observed = args.observedById.get(ref.id);
|
|
253
|
+
const session = hubFallbackStatsSession(ref, observed);
|
|
254
|
+
let cached = session ? args.sessionCosts.get(session) : undefined;
|
|
255
|
+
if (session) {
|
|
256
|
+
const directCost = readSessionDirectCost(session);
|
|
257
|
+
if (directCost !== undefined) {
|
|
258
|
+
cached = { cost: directCost };
|
|
259
|
+
args.sessionCosts.set(session, cached);
|
|
260
|
+
} else if (!cached || !fallbackReadCurrent(cached, session)) {
|
|
261
|
+
const stamp = fallbackReadStamp(session);
|
|
262
|
+
cached = { cost: readSessionMetrics(session, undefined)?.cost };
|
|
263
|
+
if (stamp) fallbackReadStamps.set(cached, stamp);
|
|
264
|
+
args.sessionCosts.set(session, cached);
|
|
265
|
+
}
|
|
266
|
+
if (cached?.cost !== undefined) args.sessionCosts.set(ref, cached);
|
|
267
|
+
}
|
|
268
|
+
const cost = projectRowMetrics(
|
|
269
|
+
ref,
|
|
270
|
+
observed,
|
|
271
|
+
cached?.cost !== undefined ? cached : undefined,
|
|
272
|
+
args.sessionCosts.get(ref),
|
|
273
|
+
session !== undefined,
|
|
274
|
+
)?.cost;
|
|
275
|
+
if (cost === undefined || (session && countedSessions.has(session))) continue;
|
|
276
|
+
if (session) countedSessions.add(session);
|
|
277
|
+
total += finiteMetric(cost);
|
|
278
|
+
}
|
|
279
|
+
return total;
|
|
229
280
|
}
|
|
230
281
|
|
|
231
282
|
export function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
|
|
@@ -369,8 +369,9 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
369
369
|
}
|
|
370
370
|
handleInput(data: string): void {
|
|
371
371
|
if (this.#composer) {
|
|
372
|
-
// The input owns every key, including panel shortcuts and
|
|
373
|
-
|
|
372
|
+
// The input owns every key it handles, including panel shortcuts and
|
|
373
|
+
// pasted text; the line keys it ignores still scroll the answer above.
|
|
374
|
+
if (!this.#composer.input.handleInput(data)) this.#scrollDetail(data);
|
|
374
375
|
this.#options.requestRender();
|
|
375
376
|
return;
|
|
376
377
|
}
|
|
@@ -417,31 +418,37 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
417
418
|
else if (matchesKey(data, "home")) this.#select(0);
|
|
418
419
|
else if (matchesKey(data, "end")) this.#select(this.#records.length - 1);
|
|
419
420
|
else return;
|
|
420
|
-
} else {
|
|
421
|
-
|
|
422
|
-
? "line-up"
|
|
423
|
-
: matchesSelectDown(data)
|
|
424
|
-
? "line-down"
|
|
425
|
-
: matchesSelectPageUp(data)
|
|
426
|
-
? "page-up"
|
|
427
|
-
: matchesSelectPageDown(data)
|
|
428
|
-
? "page-down"
|
|
429
|
-
: matchesKey(data, "home")
|
|
430
|
-
? "start"
|
|
431
|
-
: matchesKey(data, "end")
|
|
432
|
-
? "end"
|
|
433
|
-
: undefined;
|
|
434
|
-
if (by === "line-up") this.#detail.scroll(-1);
|
|
435
|
-
else if (by === "line-down") this.#detail.scroll(1);
|
|
436
|
-
else if (by === "page-up") this.#detail.page(-1);
|
|
437
|
-
else if (by === "page-down") this.#detail.page(1);
|
|
438
|
-
else if (!this.#detail.handleScrollKey(data)) return;
|
|
439
|
-
if (by) this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
|
|
440
|
-
this.#followLatest = matchesKey(data, "end");
|
|
421
|
+
} else if (!this.#scrollDetail(data)) {
|
|
422
|
+
return;
|
|
441
423
|
}
|
|
442
424
|
this.#options.requestRender();
|
|
443
425
|
}
|
|
444
426
|
|
|
427
|
+
/** Apply an answer-pane scroll key; a scroll stops following the newest row unless it is End. */
|
|
428
|
+
#scrollDetail(data: string): boolean {
|
|
429
|
+
const by: TspScrollBy | undefined = matchesSelectUp(data)
|
|
430
|
+
? "line-up"
|
|
431
|
+
: matchesSelectDown(data)
|
|
432
|
+
? "line-down"
|
|
433
|
+
: matchesSelectPageUp(data)
|
|
434
|
+
? "page-up"
|
|
435
|
+
: matchesSelectPageDown(data)
|
|
436
|
+
? "page-down"
|
|
437
|
+
: matchesKey(data, "home")
|
|
438
|
+
? "start"
|
|
439
|
+
: matchesKey(data, "end")
|
|
440
|
+
? "end"
|
|
441
|
+
: undefined;
|
|
442
|
+
if (by === "line-up") this.#detail.scroll(-1);
|
|
443
|
+
else if (by === "line-down") this.#detail.scroll(1);
|
|
444
|
+
else if (by === "page-up") this.#detail.page(-1);
|
|
445
|
+
else if (by === "page-down") this.#detail.page(1);
|
|
446
|
+
else if (!this.#detail.handleScrollKey(data)) return false;
|
|
447
|
+
if (by) this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
|
|
448
|
+
this.#followLatest = matchesKey(data, "end");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
445
452
|
/** A large glass sheet (the list beside the reader), palette-sized while there is nothing to list. */
|
|
446
453
|
get nativeOverlay(): NonNullable<Component["nativeOverlay"]> {
|
|
447
454
|
return this.#records.length > 0 ? SHEET : EMPTY_SHEET;
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { TspPickerGroup, TspPickerItem, TspProps, TspSpan, TspTone } from "@oh-my-pi/pi-wire";
|
|
9
9
|
import type { Component } from "../../tui";
|
|
10
|
-
import { formatKeyHint } from "../../app-keybindings";
|
|
11
10
|
import { node, span, stableKey } from "../../native/describe";
|
|
12
11
|
import type { NativeNode } from "../../native/node";
|
|
13
12
|
import { pickerFuzzyHits, pickerQuery } from "../../native/picker";
|
|
@@ -499,11 +498,10 @@ export class ExtensionList implements Component {
|
|
|
499
498
|
]
|
|
500
499
|
: undefined,
|
|
501
500
|
facts: { kind: this.#getKindLabel(ext.kind) },
|
|
502
|
-
disabled
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
: undefined,
|
|
501
|
+
// Only a disabled provider makes the row inert. An extension switched
|
|
502
|
+
// off by its own toggle must stay activatable, or the click/dblclick
|
|
503
|
+
// that turned it off could never turn it back on.
|
|
504
|
+
disabled: masterDisabled ? "Provider disabled" : undefined,
|
|
507
505
|
};
|
|
508
506
|
}
|
|
509
507
|
|
|
@@ -1101,6 +1101,9 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1101
1101
|
const open = list.openSubmenu;
|
|
1102
1102
|
if (open?.id === id && open.component instanceof MultiSelectSubmenu) {
|
|
1103
1103
|
open.component.setValues(value);
|
|
1104
|
+
// The native page applies a chip toggle without closing its
|
|
1105
|
+
// editor, so the row and its tab badge only update here.
|
|
1106
|
+
this.#refreshItems();
|
|
1104
1107
|
return;
|
|
1105
1108
|
}
|
|
1106
1109
|
const next = [...value];
|