@danypops/pi-jittor 0.1.1 → 0.2.1
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/extension/src/benchmark-tui.ts +27 -12
- package/extension/src/capabilities/codex-recovery.ts +39 -23
- package/extension/src/capabilities/context-hub.ts +1 -5
- package/extension/src/capabilities/local-run-telemetry.ts +14 -10
- package/extension/src/capabilities/provider-response-telemetry.ts +20 -6
- package/extension/src/context-breakdown.ts +347 -0
- package/extension/src/context-report.ts +39 -25
- package/extension/src/context-view.ts +140 -0
- package/extension/src/footer.ts +56 -26
- package/extension/src/index.ts +261 -97
- package/extension/src/service-client.ts +1 -1
- package/extension/src/settings-tui.ts +35 -20
- package/extension/src/settings.ts +13 -10
- package/extension/src/tui.ts +148 -63
- package/extension/src/usage.ts +124 -42
- package/package.json +4 -4
|
@@ -1,46 +1,60 @@
|
|
|
1
|
-
import type { ContextSegment
|
|
1
|
+
import type { ContextSegment } from "@danypops/jittor";
|
|
2
|
+
import { buildContextRows, type ContextSegment as MalevichContextSegment } from "malevich-tui-components";
|
|
3
|
+
import type { ContextBreakdown } from "./context-breakdown.ts";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
tokens: number | null;
|
|
5
|
-
contextWindow: number;
|
|
6
|
-
percent: number | null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Bounds how many items render per segment -- a report is a scan-at-a-glance summary, not a full dump. */
|
|
5
|
+
/** Bounds how many items render per segment in the plain-text fallback -- a notify-mode report is a scan-at-a-glance summary, not a full dump (the interactive TUI view has no such cap, since it scrolls). */
|
|
10
6
|
const MAX_ITEMS_PER_SEGMENT_LINE = 5;
|
|
11
7
|
|
|
12
8
|
function formatTokens(tokens: number): string {
|
|
13
9
|
return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
|
|
14
10
|
}
|
|
15
11
|
|
|
16
|
-
function
|
|
17
|
-
return
|
|
12
|
+
function percentOf(part: number, whole: number): string {
|
|
13
|
+
return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Malevich's row builder is confidence-unaware (it's a generic segment/item shape); folding the tier into the label is how it survives into the rendered row text, e.g. "Active Rules [exact-cooperative]". */
|
|
17
|
+
function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
|
|
18
|
+
const items = [...(segment.items ?? [])]
|
|
19
|
+
.sort((left, right) => right.estimatedTokens - left.estimatedTokens)
|
|
20
|
+
.slice(0, MAX_ITEMS_PER_SEGMENT_LINE);
|
|
21
|
+
return {
|
|
22
|
+
key: segment.key,
|
|
23
|
+
label: `${segment.label} [${segment.confidence}]`,
|
|
24
|
+
estimatedTokens: segment.estimatedTokens,
|
|
25
|
+
items,
|
|
26
|
+
unknown: segment.unknown,
|
|
27
|
+
};
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
|
-
* Plain-text Context Hub report: real
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
31
|
+
* Plain-text Context Hub report (non-TUI fallback): real usage against the model's effective
|
|
32
|
+
* (reserve-adjusted) budget first -- matching Papyrus's own real-vs-estimate honesty accounting
|
|
33
|
+
* -- an explicit overshoot warning when the known segments' estimates exceed the real total, then
|
|
34
|
+
* every segment heaviest-first. Malevich's buildContextRows renders segments in the order given,
|
|
35
|
+
* so sorting by weight is this function's own policy, not Malevich's.
|
|
26
36
|
*/
|
|
27
|
-
export function buildContextReport(
|
|
37
|
+
export function buildContextReport(breakdown: ContextBreakdown): string {
|
|
28
38
|
const lines: string[] = [];
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
if (breakdown.totalTokens !== null && breakdown.effectiveBudget !== null) {
|
|
40
|
+
lines.push(
|
|
41
|
+
`Real usage: ${formatTokens(breakdown.totalTokens)} / ${formatTokens(breakdown.effectiveBudget)} tokens (${percentOf(breakdown.totalTokens, breakdown.effectiveBudget)} of usable budget)`,
|
|
42
|
+
);
|
|
43
|
+
} else if (breakdown.totalTokens !== null) {
|
|
44
|
+
lines.push(`Real usage: ${formatTokens(breakdown.totalTokens)} tokens (model context window unknown)`);
|
|
32
45
|
} else {
|
|
33
46
|
lines.push("Real usage: not yet reported -- sizes below are estimates only");
|
|
34
47
|
}
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
if (breakdown.overshootTokens > 0)
|
|
49
|
+
lines.push(`Estimates exceed real total by ~${breakdown.overshootTokens} tok -- sizes below are approximate, not exact`);
|
|
50
|
+
|
|
51
|
+
const sorted = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
|
|
52
|
+
const rows = buildContextRows(sorted, breakdown.totalTokens ?? undefined);
|
|
53
|
+
if (rows.length === 0) {
|
|
37
54
|
lines.push("", "(no segments observed yet)");
|
|
38
55
|
return lines.join("\n");
|
|
39
56
|
}
|
|
40
57
|
lines.push("");
|
|
41
|
-
for (const
|
|
42
|
-
lines.push(`${segment.label} — ${formatTokens(segment.estimatedTokens)} tok [${segment.confidence}]`);
|
|
43
|
-
for (const item of topItems(segment.items)) lines.push(` ${formatTokens(item.estimatedTokens)} tok ${item.label}`);
|
|
44
|
-
}
|
|
58
|
+
for (const row of rows) lines.push(row.isHeader ? row.text : `${" ".repeat(row.depth)}${row.text}`);
|
|
45
59
|
return lines.join("\n");
|
|
46
60
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ContextSegment } from "@danypops/jittor";
|
|
2
|
+
import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { matchesKey, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import {
|
|
5
|
+
buildContextRows,
|
|
6
|
+
type ContextBarTheme,
|
|
7
|
+
type ContextRow,
|
|
8
|
+
type ContextRowsTheme,
|
|
9
|
+
type ContextSegment as MalevichContextSegment,
|
|
10
|
+
renderContextRowLines,
|
|
11
|
+
renderContextUsageBar,
|
|
12
|
+
} from "malevich-tui-components";
|
|
13
|
+
import type { ContextBreakdown } from "./context-breakdown.ts";
|
|
14
|
+
import { buildContextReport } from "./context-report.ts";
|
|
15
|
+
|
|
16
|
+
const VISIBLE_ROWS = 24;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A dynamically-contributed segment set (any string key from any extension, not a fixed enum)
|
|
20
|
+
* can't use a hardcoded per-key color map the way Papyrus's own ContextViewport did for its
|
|
21
|
+
* fixed seven segments -- this cycles a small categorical palette keyed by a stable hash of the
|
|
22
|
+
* segment key, so the same key always renders the same color within one process without needing
|
|
23
|
+
* every possible contributor's key to be known in advance.
|
|
24
|
+
*/
|
|
25
|
+
const PALETTE: ThemeColor[] = ["accent", "success", "syntaxFunction", "warning", "syntaxKeyword", "syntaxType", "muted"];
|
|
26
|
+
|
|
27
|
+
function paletteColor(key: string): ThemeColor {
|
|
28
|
+
let hash = 0;
|
|
29
|
+
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
30
|
+
return PALETTE[hash % PALETTE.length]!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatTokenCount(tokens: number): string {
|
|
34
|
+
return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function percentOf(part: number, whole: number): string {
|
|
38
|
+
return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Folds each segment's confidence tier into its label so it survives Malevich's confidence-unaware row builder. */
|
|
42
|
+
function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
|
|
43
|
+
return {
|
|
44
|
+
key: segment.key,
|
|
45
|
+
label: `${segment.label} [${segment.confidence}]`,
|
|
46
|
+
estimatedTokens: segment.estimatedTokens,
|
|
47
|
+
items: segment.items,
|
|
48
|
+
unknown: segment.unknown,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ContextViewport {
|
|
53
|
+
private offsetY = 0;
|
|
54
|
+
private readonly rows: ContextRow[];
|
|
55
|
+
private readonly segments: readonly MalevichContextSegment[];
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
private readonly tui: TUI,
|
|
59
|
+
private readonly theme: Theme,
|
|
60
|
+
private readonly breakdown: ContextBreakdown,
|
|
61
|
+
private readonly close: () => void,
|
|
62
|
+
) {
|
|
63
|
+
// Heaviest-first: Malevich renders segments in the order given, so sorting by weight for a
|
|
64
|
+
// merged multi-producer view is this viewport's own policy, matching context-report.ts.
|
|
65
|
+
this.segments = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
|
|
66
|
+
this.rows = buildContextRows(this.segments, breakdown.totalTokens ?? undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
invalidate(): void {}
|
|
70
|
+
|
|
71
|
+
render(width: number): string[] {
|
|
72
|
+
const theme = this.theme;
|
|
73
|
+
const contentWidth = Math.max(1, width);
|
|
74
|
+
const border = theme.fg("borderMuted", "─".repeat(contentWidth));
|
|
75
|
+
const lines: string[] = [border, truncateToWidth(theme.fg("accent", theme.bold("Context")), contentWidth, "")];
|
|
76
|
+
|
|
77
|
+
const { totalTokens, effectiveBudget } = this.breakdown;
|
|
78
|
+
if (totalTokens !== null && effectiveBudget !== null) {
|
|
79
|
+
lines.push(
|
|
80
|
+
truncateToWidth(
|
|
81
|
+
`${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`,
|
|
82
|
+
contentWidth,
|
|
83
|
+
"",
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
} else if (totalTokens !== null) {
|
|
87
|
+
lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} tokens (model context window unknown)`, contentWidth, ""));
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(theme.fg("dim", "No real usage reported yet — sizes below are estimates only"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const colorFor = (key: string) => (s: string) => theme.fg(paletteColor(key), s);
|
|
93
|
+
const barTheme: ContextBarTheme = { colorFor, empty: (s) => theme.fg("dim", s) };
|
|
94
|
+
lines.push(renderContextUsageBar(barTheme, this.segments, contentWidth, effectiveBudget ?? undefined, totalTokens ?? undefined));
|
|
95
|
+
if (this.breakdown.overshootTokens > 0) {
|
|
96
|
+
lines.push(
|
|
97
|
+
truncateToWidth(
|
|
98
|
+
theme.fg(
|
|
99
|
+
"warning",
|
|
100
|
+
`Estimates exceed real total by ~${this.breakdown.overshootTokens} tok — sizes below are approximate, not exact`,
|
|
101
|
+
),
|
|
102
|
+
contentWidth,
|
|
103
|
+
"",
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
lines.push("");
|
|
108
|
+
|
|
109
|
+
const rowsTheme: ContextRowsTheme = { colorFor, header: (s) => theme.bold(s) };
|
|
110
|
+
const visible = this.rows.slice(this.offsetY, this.offsetY + VISIBLE_ROWS);
|
|
111
|
+
lines.push(...renderContextRowLines(visible, contentWidth, rowsTheme));
|
|
112
|
+
if (this.rows.length === 0) lines.push(theme.fg("dim", " (nothing observed yet)"));
|
|
113
|
+
else lines.push(theme.fg("muted", ` ${Math.min(this.offsetY + VISIBLE_ROWS, this.rows.length)}/${this.rows.length}`));
|
|
114
|
+
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push(theme.fg("dim", "↑↓ scroll · esc close"));
|
|
117
|
+
lines.push(border);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
handleInput(data: string): void {
|
|
122
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
123
|
+
this.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
|
|
127
|
+
else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.rows.length - VISIBLE_ROWS), this.offsetY + 1);
|
|
128
|
+
else return;
|
|
129
|
+
this.tui.requestRender();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Interactive scrollable Context Hub view in TUI mode; the same plain-text report as /context's non-interactive path otherwise. */
|
|
134
|
+
export async function showContextView(ctx: ExtensionCommandContext, breakdown: ContextBreakdown): Promise<void> {
|
|
135
|
+
if (ctx.mode !== "tui") {
|
|
136
|
+
ctx.ui.notify(buildContextReport(breakdown), "info");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new ContextViewport(tui, theme, breakdown, done));
|
|
140
|
+
}
|
package/extension/src/footer.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { ProgressBar } from "malevich-tui-components";
|
|
5
2
|
import {
|
|
6
3
|
FOOTER_BAR_MAX_WIDTH,
|
|
7
4
|
FOOTER_BAR_MIN_WIDTH,
|
|
5
|
+
FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
|
|
8
6
|
FOOTER_CONTEXT_ACCENT_FRACTION,
|
|
9
7
|
FOOTER_CONTEXT_ERROR_FRACTION,
|
|
10
|
-
FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
|
|
11
8
|
FOOTER_CONTEXT_WARNING_FRACTION,
|
|
12
9
|
FOOTER_WIDE_TERMINAL_WIDTH,
|
|
13
10
|
MILLISECONDS_PER_DAY,
|
|
@@ -15,6 +12,9 @@ import {
|
|
|
15
12
|
MILLISECONDS_PER_MINUTE,
|
|
16
13
|
TELEMETRY_STALE_AFTER_MS,
|
|
17
14
|
} from "@danypops/jittor";
|
|
15
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
17
|
+
import { ProgressBar } from "malevich-tui-components";
|
|
18
18
|
|
|
19
19
|
type FooterColor = "accent" | "dim" | "warning" | "error";
|
|
20
20
|
|
|
@@ -48,19 +48,21 @@ interface FooterContext {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** A bounded quota is explicitly remaining; unbounded values never receive a fabricated bar. */
|
|
51
|
-
export type ProviderBudget =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
export type ProviderBudget =
|
|
52
|
+
| {
|
|
53
|
+
kind: "bounded";
|
|
54
|
+
label: string;
|
|
55
|
+
remainingFraction: number;
|
|
56
|
+
observedAt?: number;
|
|
57
|
+
resetsAt?: number;
|
|
58
|
+
resetText?: string;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
kind: "unbounded";
|
|
62
|
+
label: string;
|
|
63
|
+
valueText: string;
|
|
64
|
+
observedAt?: number;
|
|
65
|
+
};
|
|
64
66
|
|
|
65
67
|
export interface CompactionProgress {
|
|
66
68
|
startedAt: number;
|
|
@@ -92,17 +94,25 @@ function footerCwd(cwd: string, home: string | undefined): string {
|
|
|
92
94
|
const resolvedCwd = resolve(cwd);
|
|
93
95
|
const resolvedHome = resolve(home);
|
|
94
96
|
const relativeToHome = relative(resolvedHome, resolvedCwd);
|
|
95
|
-
const inside =
|
|
97
|
+
const inside =
|
|
98
|
+
relativeToHome === "" || (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
|
|
96
99
|
if (!inside) return cwd;
|
|
97
100
|
return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
function sanitize(value: string): string {
|
|
101
|
-
return value
|
|
104
|
+
return value
|
|
105
|
+
.replace(/[\r\n\t]/g, " ")
|
|
106
|
+
.replace(/ +/g, " ")
|
|
107
|
+
.trim();
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
function usageTotals(context: FooterContext): UsageTotals {
|
|
105
|
-
let input = 0,
|
|
111
|
+
let input = 0,
|
|
112
|
+
output = 0,
|
|
113
|
+
cacheRead = 0,
|
|
114
|
+
cacheWrite = 0,
|
|
115
|
+
cost = 0;
|
|
106
116
|
for (const entry of context.sessionManager.getEntries()) {
|
|
107
117
|
if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
|
|
108
118
|
const usage = entry.message.usage;
|
|
@@ -113,7 +123,7 @@ function usageTotals(context: FooterContext): UsageTotals {
|
|
|
113
123
|
cost += usage?.cost?.total ?? 0;
|
|
114
124
|
}
|
|
115
125
|
const prompt = input + cacheRead + cacheWrite;
|
|
116
|
-
return { input, output, cacheRead, cacheWrite, cost, ...(prompt > 0 ? { cacheHit: cacheRead / prompt * 100 } : {}) };
|
|
126
|
+
return { input, output, cacheRead, cacheWrite, cost, ...(prompt > 0 ? { cacheHit: (cacheRead / prompt) * 100 } : {}) };
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
function barWidth(width: number): number {
|
|
@@ -201,7 +211,13 @@ function resetLabel(resetsAt: number | undefined, now: number): string | undefin
|
|
|
201
211
|
* the segment is omitted entirely rather than showing a placeholder that could never resolve.
|
|
202
212
|
* `null` means not known yet but might resolve, which still earns the `?` placeholder.
|
|
203
213
|
*/
|
|
204
|
-
function budgetSegment(
|
|
214
|
+
function budgetSegment(
|
|
215
|
+
budget: ProviderBudget | null | undefined,
|
|
216
|
+
theme: FooterTheme,
|
|
217
|
+
width: number,
|
|
218
|
+
compact: boolean,
|
|
219
|
+
now: number,
|
|
220
|
+
): string | undefined {
|
|
205
221
|
if (budget === undefined) return undefined;
|
|
206
222
|
const w = barWidth(width);
|
|
207
223
|
if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
|
|
@@ -210,8 +226,8 @@ function budgetSegment(budget: ProviderBudget | null | undefined, theme: FooterT
|
|
|
210
226
|
if (budget.kind === "unbounded") return `${budget.label} ${budget.valueText}${staleText}`;
|
|
211
227
|
const remaining = Math.min(1, Math.max(0, budget.remainingFraction));
|
|
212
228
|
const bar = theme.fg(fillColor(1 - remaining), progressBar(remaining, w));
|
|
213
|
-
const value = `${
|
|
214
|
-
const reset = compact ? undefined : resetLabel(budget.resetsAt, now) ?? budget.resetText;
|
|
229
|
+
const value = `${compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1)}% left`;
|
|
230
|
+
const reset = compact ? undefined : (resetLabel(budget.resetsAt, now) ?? budget.resetText);
|
|
215
231
|
return `${budget.label} ${bar} ${value}${reset ? ` · ${reset}` : ""}${staleText}`;
|
|
216
232
|
}
|
|
217
233
|
|
|
@@ -238,7 +254,12 @@ function repositorySegment(context: FooterContext, footerData: FooterData, theme
|
|
|
238
254
|
return theme.fg("dim", cwd);
|
|
239
255
|
}
|
|
240
256
|
|
|
241
|
-
function modelSegments(
|
|
257
|
+
function modelSegments(
|
|
258
|
+
context: FooterContext,
|
|
259
|
+
footerData: FooterData,
|
|
260
|
+
theme: FooterTheme,
|
|
261
|
+
thinkingLevel: string,
|
|
262
|
+
): { full: string; compact: string } {
|
|
242
263
|
const model = context.model;
|
|
243
264
|
const modelName = theme.bold(model?.id ?? "no-model");
|
|
244
265
|
const provider = model && footerData.getAvailableProviderCount() > 1 ? `(${model.provider}) ` : "";
|
|
@@ -312,7 +333,16 @@ export function installIntegratedFooter(ctx: ExtensionContext, state: Integrated
|
|
|
312
333
|
return {
|
|
313
334
|
invalidate() {},
|
|
314
335
|
render(width: number): string[] {
|
|
315
|
-
return renderFooterLines(
|
|
336
|
+
return renderFooterLines(
|
|
337
|
+
ctx as unknown as FooterContext,
|
|
338
|
+
footerData,
|
|
339
|
+
theme,
|
|
340
|
+
state.providerBudget,
|
|
341
|
+
getThinkingLevel(),
|
|
342
|
+
width,
|
|
343
|
+
Date.now(),
|
|
344
|
+
state.compaction,
|
|
345
|
+
);
|
|
316
346
|
},
|
|
317
347
|
dispose() {
|
|
318
348
|
unsubscribe?.();
|