@danypops/pi-jittor 0.2.0 → 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.
@@ -1,5 +1,3 @@
1
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
3
1
  import {
4
2
  BENCHMARK_TUI_MAX_CANDIDATES,
5
3
  BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE,
@@ -15,6 +13,8 @@ import {
15
13
  type RankedModel,
16
14
  type UtilityComponentName,
17
15
  } from "@danypops/jittor";
16
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
17
+ import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
18
18
  import { sessionSecretField } from "./session-identity.ts";
19
19
 
20
20
  export interface BenchmarkPanelClient {
@@ -31,13 +31,18 @@ type BenchmarkPanelAction = "refresh" | "close";
31
31
  const COMPONENT_LABELS: Record<UtilityComponentName, string> = { quality: "Q", cost: "$", latency: "L", context: "C", reliability: "R" };
32
32
 
33
33
  function componentText(item: RankedModel): string {
34
- return item.components.map((component) => `${COMPONENT_LABELS[component.name]} ${component.score === null ? "?" : component.score.toFixed(3)}`).join(" · ");
34
+ return item.components
35
+ .map((component) => `${COMPONENT_LABELS[component.name]} ${component.score === null ? "?" : component.score.toFixed(3)}`)
36
+ .join(" · ");
35
37
  }
36
38
 
37
39
  function candidateLines(item: RankedModel, index: number, currentIdentity: string): string[] {
38
40
  const current = item.identity.startsWith(`${currentIdentity}:`);
39
41
  const localSamples = item.components.find((component) => component.name === "reliability")?.evidenceCount ?? 0;
40
- const provenance = item.provenance.slice(0, BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE).map((source) => `${source.sourceId}@${source.revision} ${source.freshness}`).join(" · ");
42
+ const provenance = item.provenance
43
+ .slice(0, BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE)
44
+ .map((source) => `${source.sourceId}@${source.revision} ${source.freshness}`)
45
+ .join(" · ");
41
46
  return [
42
47
  ` ${index + 1}. ${item.identity}${index === 0 ? " recommended" : ""}${current ? " current" : ""}`,
43
48
  ` utility ${item.utility === null ? "?" : item.utility.toFixed(3)} · confidence ${(item.confidence * 100).toFixed(0)}% · ${componentText(item)}`,
@@ -50,13 +55,18 @@ export function renderBenchmarkView(result: ModelRankingResult, currentIdentity:
50
55
  const shown = result.ranked.slice(0, BENCHMARK_TUI_MAX_CANDIDATES);
51
56
  const currentIndex = result.ranked.findIndex((item) => item.identity.startsWith(`${currentIdentity}:`));
52
57
  const recommended = result.ranked[0];
53
- const reason = recommended && currentIndex > 0
54
- ? `Recommendation differs from current: ${recommended.identity} ranks #1; current ranks #${currentIndex + 1}.`
55
- : recommended && currentIndex === 0 ? "Current model is the top recommendation." : "Current model is outside the ranked candidates.";
58
+ const reason =
59
+ recommended && currentIndex > 0
60
+ ? `Recommendation differs from current: ${recommended.identity} ranks #1; current ranks #${currentIndex + 1}.`
61
+ : recommended && currentIndex === 0
62
+ ? "Current model is the top recommendation."
63
+ : "Current model is outside the ranked candidates.";
56
64
  const lines = [
57
65
  theme.fg("borderMuted", "─".repeat(safeWidth)),
58
66
  theme.bold("Jittor Benchmark Recommendations"),
59
- result.scopeAuthority === "exact-session" ? "Scope: exact session" : "Scope: available models · ADVISORY (exact session scope unavailable)",
67
+ result.scopeAuthority === "exact-session"
68
+ ? "Scope: exact session"
69
+ : "Scope: available models · ADVISORY (exact session scope unavailable)",
60
70
  `Domain: ${result.domain} · Type: ${result.type} · evidence ${result.completeness}`,
61
71
  reason,
62
72
  ...shown.flatMap((item, index) => candidateLines(item, index, currentIdentity)),
@@ -78,7 +88,7 @@ export async function showBenchmarkPanel(
78
88
  ): Promise<void> {
79
89
  for (;;) {
80
90
  const session_id = ctx.sessionManager.getSessionId();
81
- const result = await client.call("models.rank", {
91
+ const result = (await client.call("models.rank", {
82
92
  candidates,
83
93
  session_id,
84
94
  ...sessionSecretField(session_id),
@@ -94,14 +104,19 @@ export async function showBenchmarkPanel(
94
104
  reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
95
105
  },
96
106
  sourceIds: ["openrouter-models", "lmarena-hf", "artificial-analysis-direct", "openrouter-design-arena"],
97
- }) as ModelRankingResult;
107
+ })) as ModelRankingResult;
98
108
  if (ctx.mode !== "tui") {
99
- ctx.ui.notify(renderBenchmarkView(result, currentIdentity, 100, { fg: (_color, text) => text, bold: (text) => text }).join("\n"), "info");
109
+ ctx.ui.notify(
110
+ renderBenchmarkView(result, currentIdentity, 100, { fg: (_color, text) => text, bold: (text) => text }).join("\n"),
111
+ "info",
112
+ );
100
113
  return;
101
114
  }
102
115
  const action = await ctx.ui.custom<BenchmarkPanelAction>((_tui, theme, _keybindings, done) => ({
103
116
  invalidate() {},
104
- render(width: number): string[] { return renderBenchmarkView(result, currentIdentity, width, theme); },
117
+ render(width: number): string[] {
118
+ return renderBenchmarkView(result, currentIdentity, width, theme);
119
+ },
105
120
  handleInput(data: string): void {
106
121
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done("close");
107
122
  else if (data === "r") done("refresh");
@@ -1,17 +1,17 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
1
  import {
3
2
  CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
4
3
  CODEX_RECOVERY_BASE_DELAY_MS,
5
4
  CODEX_RECOVERY_JITTER_RATIO,
6
5
  CODEX_RECOVERY_MAX_ATTEMPTS,
7
6
  CODEX_RECOVERY_MAX_DELAY_MS,
8
- MILLISECONDS_PER_MINUTE,
9
- MILLISECONDS_PER_SECOND,
10
- CodexRecoveryPolicy,
11
- classifyCodexFailure,
12
7
  type CodexFailureKind,
13
8
  type CodexFailureMetadata,
9
+ CodexRecoveryPolicy,
10
+ classifyCodexFailure,
11
+ MILLISECONDS_PER_MINUTE,
12
+ MILLISECONDS_PER_SECOND,
14
13
  } from "@danypops/jittor";
14
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
15
  import type { CodexRecoveryControl } from "../settings.ts";
16
16
  import { headerValue } from "./http-headers.ts";
17
17
 
@@ -25,8 +25,14 @@ export interface CodexRecoveryRuntime {
25
25
  export const SYSTEM_RECOVERY_RUNTIME: CodexRecoveryRuntime = {
26
26
  now: Date.now,
27
27
  random: Math.random,
28
- setTimeout(callback, delayMs) { return setTimeout(() => { void callback(); }, delayMs); },
29
- clearTimeout(handle) { clearTimeout(handle as ReturnType<typeof setTimeout>); },
28
+ setTimeout(callback, delayMs) {
29
+ return setTimeout(() => {
30
+ void callback();
31
+ }, delayMs);
32
+ },
33
+ clearTimeout(handle) {
34
+ clearTimeout(handle as ReturnType<typeof setTimeout>);
35
+ },
30
36
  };
31
37
 
32
38
  /**
@@ -47,13 +53,16 @@ export class CodexRecoveryCapability {
47
53
  private readonly control: CodexRecoveryControl,
48
54
  private readonly runtime: CodexRecoveryRuntime,
49
55
  ) {
50
- this.policy = new CodexRecoveryPolicy({
51
- baseDelayMs: CODEX_RECOVERY_BASE_DELAY_MS,
52
- maxDelayMs: CODEX_RECOVERY_MAX_DELAY_MS,
53
- maxAttempts: CODEX_RECOVERY_MAX_ATTEMPTS,
54
- attemptWindowMs: CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
55
- jitterRatio: CODEX_RECOVERY_JITTER_RATIO,
56
- }, runtime.random);
56
+ this.policy = new CodexRecoveryPolicy(
57
+ {
58
+ baseDelayMs: CODEX_RECOVERY_BASE_DELAY_MS,
59
+ maxDelayMs: CODEX_RECOVERY_MAX_DELAY_MS,
60
+ maxAttempts: CODEX_RECOVERY_MAX_ATTEMPTS,
61
+ attemptWindowMs: CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
62
+ jitterRatio: CODEX_RECOVERY_JITTER_RATIO,
63
+ },
64
+ runtime.random,
65
+ );
57
66
  }
58
67
 
59
68
  /** Clears the tracked response at the start of every new turn, before any Codex response for it has arrived. */
@@ -90,9 +99,13 @@ export class CodexRecoveryCapability {
90
99
  const attempt = this.cooldown?.attempt ?? (state.pending ? state.attempts + 1 : state.attempts);
91
100
  const phase = this.cooldown
92
101
  ? `cooldown ${Math.ceil(Math.max(0, this.cooldown.until - now) / MILLISECONDS_PER_SECOND)}s`
93
- : state.pending ? "pending"
94
- : state.attempts >= CODEX_RECOVERY_MAX_ATTEMPTS ? "exhausted"
95
- : state.attempts > 0 ? "waiting" : "idle";
102
+ : state.pending
103
+ ? "pending"
104
+ : state.attempts >= CODEX_RECOVERY_MAX_ATTEMPTS
105
+ ? "exhausted"
106
+ : state.attempts > 0
107
+ ? "waiting"
108
+ : "idle";
96
109
  const failureKind = this.cooldown?.failureKind ?? state.lastFailureKind;
97
110
  return [
98
111
  `Codex recovery: ${enabled ? "on" : "off"}`,
@@ -119,12 +132,15 @@ export class CodexRecoveryCapability {
119
132
  if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
120
133
  const attempt = this.policy.recordAttempt(this.runtime.now());
121
134
  if (!attempt) return;
122
- this.pi.sendMessage({
123
- customType: "jittor-codex-recovery",
124
- content: `Retry the previous Codex request after a transient ${attempt.failureKind} failure. Automatic recovery attempt ${attempt.attempt} of ${CODEX_RECOVERY_MAX_ATTEMPTS}.`,
125
- display: false,
126
- details: { attempt: attempt.attempt, failureKind: attempt.failureKind },
127
- }, { triggerTurn: true, deliverAs: "followUp" });
135
+ this.pi.sendMessage(
136
+ {
137
+ customType: "jittor-codex-recovery",
138
+ content: `Retry the previous Codex request after a transient ${attempt.failureKind} failure. Automatic recovery attempt ${attempt.attempt} of ${CODEX_RECOVERY_MAX_ATTEMPTS}.`,
139
+ display: false,
140
+ details: { attempt: attempt.attempt, failureKind: attempt.failureKind },
141
+ },
142
+ { triggerTurn: true, deliverAs: "followUp" },
143
+ );
128
144
  }, plan.delayMs);
129
145
  }
130
146
  }
@@ -1,8 +1,4 @@
1
- import {
2
- CONTEXT_HUB_CONTRIBUTION_DEDUP_LIMIT,
3
- validateContextContribution,
4
- type ContextSegment,
5
- } from "@danypops/jittor";
1
+ import { CONTEXT_HUB_CONTRIBUTION_DEDUP_LIMIT, type ContextSegment, validateContextContribution } from "@danypops/jittor";
6
2
 
7
3
  /**
8
4
  * Merges Jittor's own directly-computed segments (tool ledger, real usage) with whatever
@@ -1,4 +1,4 @@
1
- import { classifyTaskFromTools, modelRunMetrics, type MetricObservation, type ModelRunObservation } from "@danypops/jittor";
1
+ import { classifyTaskFromTools, type MetricObservation, type ModelRunObservation, modelRunMetrics } from "@danypops/jittor";
2
2
 
3
3
  export interface ActiveLocalModelRun {
4
4
  runId: string;
@@ -59,18 +59,22 @@ export class LocalRunTelemetry {
59
59
  this.active = undefined;
60
60
  if (!active || typeof message !== "object" || message === null || Array.isArray(message)) return [];
61
61
  const value = message as Record<string, unknown>;
62
- if (value["role"] !== "assistant" || typeof value["provider"] !== "string" || typeof value["model"] !== "string") return [];
63
- const usage = typeof value["usage"] === "object" && value["usage"] !== null ? value["usage"] as Record<string, unknown> : {};
64
- const amount = (name: string): number => typeof usage[name] === "number" && Number.isFinite(usage[name]) ? usage[name] as number : 0;
65
- const cost = typeof usage["cost"] === "object" && usage["cost"] !== null && typeof (usage["cost"] as Record<string, unknown>)["total"] === "number"
66
- ? (usage["cost"] as Record<string, number>)["total"] ?? 0 : 0;
67
- const stopReason = ["stop", "length", "toolUse", "error", "aborted"].includes(String(value["stopReason"]))
68
- ? value["stopReason"] as ModelRunObservation["stopReason"] : "unknown";
62
+ if (value.role !== "assistant" || typeof value.provider !== "string" || typeof value.model !== "string") return [];
63
+ const usage = typeof value.usage === "object" && value.usage !== null ? (value.usage as Record<string, unknown>) : {};
64
+ const amount = (name: string): number =>
65
+ typeof usage[name] === "number" && Number.isFinite(usage[name]) ? (usage[name] as number) : 0;
66
+ const cost =
67
+ typeof usage.cost === "object" && usage.cost !== null && typeof (usage.cost as Record<string, unknown>).total === "number"
68
+ ? ((usage.cost as Record<string, number>).total ?? 0)
69
+ : 0;
70
+ const stopReason = ["stop", "length", "toolUse", "error", "aborted"].includes(String(value.stopReason))
71
+ ? (value.stopReason as ModelRunObservation["stopReason"])
72
+ : "unknown";
69
73
  const completedAt = Math.max(Date.now(), active.firstTokenAt ?? active.startedAt, active.startedAt);
70
74
  this.lastCompleted = {
71
75
  runId: active.runId,
72
- provider: value["provider"],
73
- model: value["model"],
76
+ provider: value.provider,
77
+ model: value.model,
74
78
  thinking: thinkingLevel,
75
79
  ...classifyTaskFromTools(active.toolNames),
76
80
  startedAt: active.startedAt,
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  classifyGoogleVertexFailure,
3
+ type GoogleVertexFailureMetadata,
3
4
  googleVertexFailureMetrics,
4
5
  hasAnthropicRateLimitHeaders,
6
+ type MetricObservation,
5
7
  parseAnthropicRateLimitHeaders,
6
8
  parseCodexRateLimitHeaders,
7
- type GoogleVertexFailureMetadata,
8
- type MetricObservation,
9
9
  } from "@danypops/jittor";
10
10
  import { headerValue } from "./http-headers.ts";
11
11
 
@@ -67,22 +67,36 @@ export class ProviderResponseTelemetry {
67
67
  }
68
68
  // Well-evidenced regardless of headers: GCP's own quota system fronts this transport, so the
69
69
  // same failure classification as google-vertex applies below.
70
- this.lastAnthropicVertexResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
70
+ this.lastAnthropicVertexResponse = {
71
+ status,
72
+ ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}),
73
+ };
71
74
  }
72
75
  if (provider === "google-vertex") {
73
- this.lastGoogleVertexResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
76
+ this.lastGoogleVertexResponse = {
77
+ status,
78
+ ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}),
79
+ };
74
80
  }
75
81
  if (Object.keys(headers).some((name) => name.toLowerCase().startsWith("x-codex-"))) {
76
82
  try {
77
83
  const updates = parseCodexRateLimitHeaders(new Headers(headers), Date.now());
78
- await recordMetrics(client, updates.flatMap((update) => update.metrics));
84
+ await recordMetrics(
85
+ client,
86
+ updates.flatMap((update) => update.metrics),
87
+ );
79
88
  } catch {
80
89
  notifySchemaDrift("Codex telemetry schema drift");
81
90
  }
82
91
  }
83
92
  }
84
93
 
85
- async handleMessageEnd(client: ProviderTelemetryClient, provider: string | undefined, stopReason: string | undefined, errorMessage: string | undefined): Promise<void> {
94
+ async handleMessageEnd(
95
+ client: ProviderTelemetryClient,
96
+ provider: string | undefined,
97
+ stopReason: string | undefined,
98
+ errorMessage: string | undefined,
99
+ ): Promise<void> {
86
100
  if (provider === "google-vertex") {
87
101
  if (stopReason === "error") {
88
102
  const failure = classifyGoogleVertexFailure(errorMessage, this.lastGoogleVertexResponse);
@@ -1,5 +1,11 @@
1
+ import {
2
+ CONTEXT_DEFAULT_RESERVE_TOKENS,
3
+ CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
4
+ CONTEXT_TREE_MAX_NODES,
5
+ type ContextSegment,
6
+ type ContextSegmentItem,
7
+ } from "@danypops/jittor";
1
8
  import type { BuildSystemPromptOptions } from "@earendil-works/pi-coding-agent";
2
- import { CONTEXT_DEFAULT_RESERVE_TOKENS, CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, CONTEXT_TREE_MAX_NODES, type ContextSegment, type ContextSegmentItem } from "@danypops/jittor";
3
9
 
4
10
  /**
5
11
  * Ported from pi-papyrus's context-budget.ts: the Pi-generic half (session message-history tree
@@ -29,21 +35,21 @@ export interface SessionTreeNodeLike {
29
35
  function messageContentCharacters(message: unknown): number {
30
36
  if (typeof message !== "object" || message === null) return 0;
31
37
  const record = message as Record<string, unknown>;
32
- if (record["role"] === "bashExecution") {
38
+ if (record.role === "bashExecution") {
33
39
  // Pi's own context builder excludes "!!"-prefixed bash output from context; match that.
34
- if (record["excludeFromContext"] === true) return 0;
35
- return String(record["command"] ?? "").length + String(record["output"] ?? "").length;
40
+ if (record.excludeFromContext === true) return 0;
41
+ return String(record.command ?? "").length + String(record.output ?? "").length;
36
42
  }
37
- const content = record["content"];
43
+ const content = record.content;
38
44
  if (typeof content === "string") return content.length;
39
45
  if (!Array.isArray(content)) return 0;
40
46
  let characters = 0;
41
47
  for (const block of content) {
42
48
  if (typeof block !== "object" || block === null) continue;
43
49
  const b = block as Record<string, unknown>;
44
- if (b["type"] === "text") characters += String(b["text"] ?? "").length;
45
- else if (b["type"] === "thinking") characters += String(b["thinking"] ?? "").length;
46
- else if (b["type"] === "toolCall") characters += JSON.stringify(b["arguments"] ?? {}).length;
50
+ if (b.type === "text") characters += String(b.text ?? "").length;
51
+ else if (b.type === "thinking") characters += String(b.thinking ?? "").length;
52
+ else if (b.type === "toolCall") characters += JSON.stringify(b.arguments ?? {}).length;
47
53
  // "image" blocks are deliberately not counted here -- image tokens follow a different,
48
54
  // non-character-based cost model this char/4 estimate cannot represent; this is a real,
49
55
  // documented undercount for image-heavy sessions, not a silent approximation.
@@ -54,13 +60,20 @@ function messageContentCharacters(message: unknown): number {
54
60
  function messageSnippet(message: unknown, maxLength = 48): string {
55
61
  if (typeof message !== "object" || message === null) return "";
56
62
  const record = message as Record<string, unknown>;
57
- if (record["role"] === "bashExecution") return String(record["command"] ?? "");
58
- const content = record["content"];
59
- const text = typeof content === "string"
60
- ? content
61
- : Array.isArray(content)
62
- ? content.map((block) => (typeof block === "object" && block !== null && (block as Record<string, unknown>)["type"] === "text" ? String((block as Record<string, unknown>)["text"] ?? "") : "")).join(" ")
63
- : "";
63
+ if (record.role === "bashExecution") return String(record.command ?? "");
64
+ const content = record.content;
65
+ const text =
66
+ typeof content === "string"
67
+ ? content
68
+ : Array.isArray(content)
69
+ ? content
70
+ .map((block) =>
71
+ typeof block === "object" && block !== null && (block as Record<string, unknown>).type === "text"
72
+ ? String((block as Record<string, unknown>).text ?? "")
73
+ : "",
74
+ )
75
+ .join(" ")
76
+ : "";
64
77
  const collapsed = text.replace(/\s+/g, " ").trim();
65
78
  return collapsed.length > maxLength ? `${collapsed.slice(0, maxLength - 1)}…` : collapsed;
66
79
  }
@@ -68,7 +81,7 @@ function messageSnippet(message: unknown, maxLength = 48): string {
68
81
  function entryLabel(entry: SessionEntryLike): string {
69
82
  if (entry.type === "compaction") return "compaction summary";
70
83
  if (entry.type === "branch_summary") return "branch summary";
71
- const role = typeof entry.message === "object" && entry.message !== null ? (entry.message as Record<string, unknown>)["role"] : undefined;
84
+ const role = typeof entry.message === "object" && entry.message !== null ? (entry.message as Record<string, unknown>).role : undefined;
72
85
  const prefix = typeof role === "string" ? role : entry.type;
73
86
  const snippet = messageSnippet(entry.message);
74
87
  return snippet ? `${prefix}: ${snippet}` : prefix;
@@ -113,7 +126,11 @@ interface WalkFrame {
113
126
  * by a reverse-order (children-before-parent) construction pass -- an ordinary long-running
114
127
  * session is one long linear chain, so recursion depth would equal entry count.
115
128
  */
116
- export function buildMessageHistoryTree(roots: ReadonlyArray<SessionTreeNodeLike>, activeEntryIds: ReadonlySet<string>, branchEntryIds?: ReadonlySet<string>): MessageHistoryTree {
129
+ export function buildMessageHistoryTree(
130
+ roots: ReadonlyArray<SessionTreeNodeLike>,
131
+ activeEntryIds: ReadonlySet<string>,
132
+ branchEntryIds?: ReadonlySet<string>,
133
+ ): MessageHistoryTree {
117
134
  const visited = new Set<string>();
118
135
  let truncated = false;
119
136
  let activeTokens = 0;
@@ -122,8 +139,14 @@ export function buildMessageHistoryTree(roots: ReadonlyArray<SessionTreeNodeLike
122
139
  const stack: WalkFrame[] = [...roots].reverse().map((root) => ({ node: root, parentIndex: null }));
123
140
  while (stack.length > 0) {
124
141
  const frame = stack.pop()!;
125
- if (order.length >= CONTEXT_TREE_MAX_NODES) { truncated = true; break; }
126
- if (visited.has(frame.node.entry.id)) { truncated = true; continue; } // cycle guard
142
+ if (order.length >= CONTEXT_TREE_MAX_NODES) {
143
+ truncated = true;
144
+ break;
145
+ }
146
+ if (visited.has(frame.node.entry.id)) {
147
+ truncated = true;
148
+ continue;
149
+ } // cycle guard
127
150
  visited.add(frame.node.entry.id);
128
151
  const index = order.length;
129
152
  order.push(frame);
@@ -137,11 +160,12 @@ export function buildMessageHistoryTree(roots: ReadonlyArray<SessionTreeNodeLike
137
160
  for (let index = order.length - 1; index >= 0; index--) {
138
161
  const frame = order[index]!;
139
162
  const entry = frame.node.entry;
140
- const characters = entry.type === "message"
141
- ? messageContentCharacters(entry.message)
142
- : entry.type === "compaction" || entry.type === "branch_summary"
143
- ? (entry.summary ?? "").length
144
- : 0;
163
+ const characters =
164
+ entry.type === "message"
165
+ ? messageContentCharacters(entry.message)
166
+ : entry.type === "compaction" || entry.type === "branch_summary"
167
+ ? (entry.summary ?? "").length
168
+ : 0;
145
169
  const tokens = Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
146
170
  const isActive = activeEntryIds.has(entry.id);
147
171
  if (isActive) activeTokens += tokens;
@@ -210,7 +234,10 @@ export function buildBasePromptItems(options: BuildSystemPromptOptions, totalCha
210
234
  }
211
235
 
212
236
  const visibleSkills = (options.skills ?? []).filter((skill) => !skill.disableModelInvocation);
213
- const skillsCharacters = visibleSkills.reduce((sum, skill) => sum + skill.name.length + skill.description.length + skill.filePath.length + 20, 0);
237
+ const skillsCharacters = visibleSkills.reduce(
238
+ (sum, skill) => sum + skill.name.length + skill.description.length + skill.filePath.length + 20,
239
+ 0,
240
+ );
214
241
  if (skillsCharacters > 0) {
215
242
  items.push({ label: `Skills catalog (${visibleSkills.length} skills)`, estimatedTokens: toCeilTokens(skillsCharacters) });
216
243
  }
@@ -218,7 +245,10 @@ export function buildBasePromptItems(options: BuildSystemPromptOptions, totalCha
218
245
  const contextFiles = options.contextFiles ?? [];
219
246
  const contextFilesCharacters = contextFiles.reduce((sum, file) => sum + file.path.length + file.content.length + 40, 0);
220
247
  if (contextFilesCharacters > 0) {
221
- items.push({ label: `Project context files (${contextFiles.length}, e.g. AGENTS.md)`, estimatedTokens: toCeilTokens(contextFilesCharacters) });
248
+ items.push({
249
+ label: `Project context files (${contextFiles.length}, e.g. AGENTS.md)`,
250
+ estimatedTokens: toCeilTokens(contextFilesCharacters),
251
+ });
222
252
  }
223
253
 
224
254
  const knownCharacters = toolSnippetsCharacters + skillsCharacters + contextFilesCharacters;
@@ -300,9 +330,10 @@ export function composeContextBreakdown(input: ComposeContextBreakdownInput): Co
300
330
  const overshootTokens = input.totalTokens === null ? 0 : Math.max(0, knownTokens - input.totalTokens);
301
331
  const other: ContextSegment = {
302
332
  key: "other",
303
- label: overshootTokens > 0
304
- ? `Unaccounted (message envelope, cache-control markers, and other wire-protocol overhead) -- estimate overshoot: other segments' estimates already exceed the real total by ~${overshootTokens} tokens, so this is a floor, not a real zero`
305
- : "Unaccounted (message envelope, cache-control markers, and other wire-protocol overhead)",
333
+ label:
334
+ overshootTokens > 0
335
+ ? `Unaccounted (message envelope, cache-control markers, and other wire-protocol overhead) -- estimate overshoot: other segments' estimates already exceed the real total by ~${overshootTokens} tokens, so this is a floor, not a real zero`
336
+ : "Unaccounted (message envelope, cache-control markers, and other wire-protocol overhead)",
306
337
  estimatedTokens: input.totalTokens === null ? 0 : Math.max(0, input.totalTokens - knownTokens),
307
338
  confidence: "correlated",
308
339
  };
@@ -1,6 +1,6 @@
1
+ import type { ContextSegment } from "@danypops/jittor";
1
2
  import { buildContextRows, type ContextSegment as MalevichContextSegment } from "malevich-tui-components";
2
3
  import type { ContextBreakdown } from "./context-breakdown.ts";
3
- import type { ContextSegment } from "@danypops/jittor";
4
4
 
5
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). */
6
6
  const MAX_ITEMS_PER_SEGMENT_LINE = 5;
@@ -15,8 +15,16 @@ function percentOf(part: number, whole: number): string {
15
15
 
16
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
17
  function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
18
- const items = [...(segment.items ?? [])].sort((left, right) => right.estimatedTokens - left.estimatedTokens).slice(0, MAX_ITEMS_PER_SEGMENT_LINE);
19
- return { key: segment.key, label: `${segment.label} [${segment.confidence}]`, estimatedTokens: segment.estimatedTokens, items, unknown: segment.unknown };
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
+ };
20
28
  }
21
29
 
22
30
  /**
@@ -29,13 +37,16 @@ function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
29
37
  export function buildContextReport(breakdown: ContextBreakdown): string {
30
38
  const lines: string[] = [];
31
39
  if (breakdown.totalTokens !== null && breakdown.effectiveBudget !== null) {
32
- lines.push(`Real usage: ${formatTokens(breakdown.totalTokens)} / ${formatTokens(breakdown.effectiveBudget)} tokens (${percentOf(breakdown.totalTokens, breakdown.effectiveBudget)} of usable budget)`);
40
+ lines.push(
41
+ `Real usage: ${formatTokens(breakdown.totalTokens)} / ${formatTokens(breakdown.effectiveBudget)} tokens (${percentOf(breakdown.totalTokens, breakdown.effectiveBudget)} of usable budget)`,
42
+ );
33
43
  } else if (breakdown.totalTokens !== null) {
34
44
  lines.push(`Real usage: ${formatTokens(breakdown.totalTokens)} tokens (model context window unknown)`);
35
45
  } else {
36
46
  lines.push("Real usage: not yet reported -- sizes below are estimates only");
37
47
  }
38
- if (breakdown.overshootTokens > 0) lines.push(`Estimates exceed real total by ~${breakdown.overshootTokens} tok -- sizes below are approximate, not exact`);
48
+ if (breakdown.overshootTokens > 0)
49
+ lines.push(`Estimates exceed real total by ~${breakdown.overshootTokens} tok -- sizes below are approximate, not exact`);
39
50
 
40
51
  const sorted = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
41
52
  const rows = buildContextRows(sorted, breakdown.totalTokens ?? undefined);
@@ -1,7 +1,15 @@
1
- import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
- import { matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
- import { buildContextRows, renderContextRowLines, renderContextUsageBar, type ContextBarTheme, type ContextRow, type ContextRowsTheme, type ContextSegment as MalevichContextSegment } from "malevich-tui-components";
4
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";
5
13
  import type { ContextBreakdown } from "./context-breakdown.ts";
6
14
  import { buildContextReport } from "./context-report.ts";
7
15
 
@@ -32,7 +40,13 @@ function percentOf(part: number, whole: number): string {
32
40
 
33
41
  /** Folds each segment's confidence tier into its label so it survives Malevich's confidence-unaware row builder. */
34
42
  function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
35
- return { key: segment.key, label: `${segment.label} [${segment.confidence}]`, estimatedTokens: segment.estimatedTokens, items: segment.items, unknown: segment.unknown };
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
+ };
36
50
  }
37
51
 
38
52
  class ContextViewport {
@@ -62,7 +76,13 @@ class ContextViewport {
62
76
 
63
77
  const { totalTokens, effectiveBudget } = this.breakdown;
64
78
  if (totalTokens !== null && effectiveBudget !== null) {
65
- lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`, contentWidth, ""));
79
+ lines.push(
80
+ truncateToWidth(
81
+ `${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`,
82
+ contentWidth,
83
+ "",
84
+ ),
85
+ );
66
86
  } else if (totalTokens !== null) {
67
87
  lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} tokens (model context window unknown)`, contentWidth, ""));
68
88
  } else {
@@ -73,7 +93,16 @@ class ContextViewport {
73
93
  const barTheme: ContextBarTheme = { colorFor, empty: (s) => theme.fg("dim", s) };
74
94
  lines.push(renderContextUsageBar(barTheme, this.segments, contentWidth, effectiveBudget ?? undefined, totalTokens ?? undefined));
75
95
  if (this.breakdown.overshootTokens > 0) {
76
- lines.push(truncateToWidth(theme.fg("warning", `Estimates exceed real total by ~${this.breakdown.overshootTokens} tok — sizes below are approximate, not exact`), contentWidth, ""));
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
+ );
77
106
  }
78
107
  lines.push("");
79
108
 
@@ -90,7 +119,10 @@ class ContextViewport {
90
119
  }
91
120
 
92
121
  handleInput(data: string): void {
93
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
122
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
123
+ this.close();
124
+ return;
125
+ }
94
126
  if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
95
127
  else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.rows.length - VISIBLE_ROWS), this.offsetY + 1);
96
128
  else return;