@danypops/pi-jittor 0.6.0 → 0.8.0

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.
@@ -52,7 +52,12 @@ import {
52
52
  import { ContextGrowthCapability } from "./observability/context-growth.ts";
53
53
  import { ContextHubCapability } from "./observability/context-hub.ts";
54
54
  import { showContextView } from "./observability/context-view.ts";
55
- import { type CompactionProgress, type IntegratedFooterState, installIntegratedFooter } from "./observability/footer.ts";
55
+ import {
56
+ type CompactionProgress,
57
+ type IntegratedFooterState,
58
+ installIntegratedFooter,
59
+ type RouterFooterInfo,
60
+ } from "./observability/footer.ts";
56
61
  import { LocalRunTelemetry } from "./observability/model-run.ts";
57
62
  import { captureProviderContextSnapshot } from "./observability/provider-context-snapshot.ts";
58
63
  import { ProviderResponseTelemetry } from "./observability/provider-response.ts";
@@ -159,21 +164,6 @@ async function refreshFooter(client: JittorExtensionClient, state: IntegratedFoo
159
164
  state.requestRender?.();
160
165
  }
161
166
 
162
- function delay(milliseconds: number, signal?: AbortSignal): Promise<void> {
163
- if (milliseconds <= 0) return Promise.resolve();
164
- return new Promise((resolve, reject) => {
165
- const timer = setTimeout(resolve, milliseconds);
166
- signal?.addEventListener(
167
- "abort",
168
- () => {
169
- clearTimeout(timer);
170
- reject(new Error("Jittor throttle cancelled"));
171
- },
172
- { once: true },
173
- );
174
- });
175
- }
176
-
177
167
  function routeModelAvailable(ctx: ExtensionContext, route: Route): boolean {
178
168
  return ctx.modelRegistry.getAvailable().some((model) => model.provider === route.provider && model.id === route.model);
179
169
  }
@@ -313,38 +303,6 @@ async function syncCurrentRoute(
313
303
  });
314
304
  }
315
305
 
316
- function halt(ctx: ExtensionContext, reason: string): false {
317
- ctx.ui.notify(`${reason}. ${RECOVERY_GUIDANCE}.`, "warning");
318
- ctx.abort();
319
- return false;
320
- }
321
-
322
- async function applyDecision(
323
- pi: ExtensionAPI,
324
- client: JittorExtensionClient,
325
- ctx: ExtensionContext,
326
- decision: PolicyDecision,
327
- allowResync = true,
328
- ): Promise<boolean> {
329
- if (decision.action === "halt") return halt(ctx, `Jittor blocked this provider request: ${decision.reason}`);
330
- if (decision.action === "throttle") await delay(decision.delayMs ?? 0, ctx.signal);
331
- if (!decision.route || (await applyRoute(pi, ctx, decision.route))) return true;
332
- if (allowResync) {
333
- await syncAvailableRoutes(pi, client, ctx);
334
- return applyDecision(
335
- pi,
336
- client,
337
- ctx,
338
- (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision,
339
- false,
340
- );
341
- }
342
- return halt(
343
- ctx,
344
- `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`,
345
- );
346
- }
347
-
348
306
  let assistantUsageRunSequence = 0;
349
307
 
350
308
  /**
@@ -498,54 +456,61 @@ export function registerJittorExtension(
498
456
  const contextFingerprinter = new HmacContextFingerprinter(contextFingerprintKey);
499
457
  let contextCaptureSequence = 0;
500
458
  pi.on("before_provider_request", (event, ctx) => {
501
- try {
502
- const sessionId = ctx.sessionManager.getSessionId();
503
- let history:
504
- | {
505
- roots: SessionTreeNodeLike[];
506
- activeEntryIds: Set<string>;
507
- branchEntryIds: Set<string>;
508
- }
509
- | undefined;
459
+ // Pi awaits this hook immediately before opening the provider request. Snapshotting a large
460
+ // payload here used to add its full CPU cost to TTFT even though persistence was detached.
461
+ // Defer the entire observational path to a later macrotask; the provider payload is immutable
462
+ // after serialization in Pi's request path, and a missed snapshot is preferable to delaying it.
463
+ const timer = setTimeout(() => {
510
464
  try {
511
- history = {
512
- roots: ctx.sessionManager.getTree() as SessionTreeNodeLike[],
513
- activeEntryIds: new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id)),
514
- branchEntryIds: new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id)),
465
+ const sessionId = ctx.sessionManager.getSessionId();
466
+ let history:
467
+ | {
468
+ roots: SessionTreeNodeLike[];
469
+ activeEntryIds: Set<string>;
470
+ branchEntryIds: Set<string>;
471
+ }
472
+ | undefined;
473
+ try {
474
+ history = {
475
+ roots: ctx.sessionManager.getTree() as SessionTreeNodeLike[],
476
+ activeEntryIds: new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id)),
477
+ branchEntryIds: new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id)),
478
+ };
479
+ } catch {
480
+ // Older/custom SessionManager implementations may not expose tree projections.
481
+ }
482
+ const captureInput = {
483
+ payload: event.payload,
484
+ captureId: `${++contextCaptureSequence}`,
485
+ sessionId,
486
+ provider: ctx.model?.provider ?? "unknown",
487
+ model: ctx.model?.id ?? "unknown",
488
+ capturedAt: Date.now(),
489
+ fingerprinter: contextFingerprinter,
515
490
  };
491
+ let snapshot: ContextSnapshot;
492
+ try {
493
+ snapshot = captureProviderContextSnapshot({ ...captureInput, ...(history ? { history } : {}) });
494
+ } catch {
495
+ // A custom SessionManager tree shape must not suppress the real request-payload snapshot.
496
+ snapshot = captureProviderContextSnapshot(captureInput);
497
+ }
498
+ // Observation must never alter or abort the provider request. Both local writes are detached;
499
+ // they receive only bounded token sizes and keyed fingerprints.
500
+ const requestTokens = snapshot.segments
501
+ .filter((segment) => segment.requestPosition !== null)
502
+ .reduce((sum, segment) => sum + segment.tokens, 0);
503
+ const compactionMetrics = compactionTelemetry.observeContextSnapshot(requestTokens, "structural-estimate", snapshot.capturedAt, {
504
+ provider: snapshot.provider,
505
+ model: snapshot.model,
506
+ });
507
+ void client.call("context.snapshot", snapshot).catch(() => undefined);
508
+ if (compactionMetrics.length > 0) void recordMetrics(client, compactionMetrics).catch(() => undefined);
516
509
  } catch {
517
- // Older/custom SessionManager implementations may not expose tree projections.
518
- }
519
- const captureInput = {
520
- payload: event.payload,
521
- captureId: `${++contextCaptureSequence}`,
522
- sessionId,
523
- provider: ctx.model?.provider ?? "unknown",
524
- model: ctx.model?.id ?? "unknown",
525
- capturedAt: Date.now(),
526
- fingerprinter: contextFingerprinter,
527
- };
528
- let snapshot: ContextSnapshot;
529
- try {
530
- snapshot = captureProviderContextSnapshot({ ...captureInput, ...(history ? { history } : {}) });
531
- } catch {
532
- // A custom SessionManager tree shape must not suppress the real request-payload snapshot.
533
- snapshot = captureProviderContextSnapshot(captureInput);
510
+ // Snapshot collection is strictly failure-isolated from provider delivery.
534
511
  }
535
- // Observation must never alter or abort the provider request. Both local writes are detached;
536
- // they receive only bounded token sizes and keyed fingerprints.
537
- const requestTokens = snapshot.segments
538
- .filter((segment) => segment.requestPosition !== null)
539
- .reduce((sum, segment) => sum + segment.tokens, 0);
540
- const compactionMetrics = compactionTelemetry.observeContextSnapshot(requestTokens, "structural-estimate", snapshot.capturedAt, {
541
- provider: snapshot.provider,
542
- model: snapshot.model,
543
- });
544
- void client.call("context.snapshot", snapshot).catch(() => undefined);
545
- if (compactionMetrics.length > 0) void recordMetrics(client, compactionMetrics).catch(() => undefined);
546
- } catch {
547
- // Snapshot collection is strictly failure-isolated from provider delivery.
548
- }
512
+ }, 0);
513
+ timer.unref?.();
549
514
  });
550
515
  const stopContextHub = pi.events?.on?.(CONTEXT_HUB_CONTRIBUTION_CHANNEL, (payload) => contextHub.observe(payload));
551
516
  // Cached from the most recent before_agent_start observation: Pi's own base system prompt is
@@ -623,7 +588,13 @@ export function registerJittorExtension(
623
588
  else footerState.requestRender?.();
624
589
  };
625
590
  const showFooter = (ctx: ExtensionContext): void => {
626
- if (enforcement.isFooterEnabled()) installIntegratedFooter(ctx, footerState, () => pi.getThinkingLevel());
591
+ if (enforcement.isFooterEnabled())
592
+ installIntegratedFooter(
593
+ ctx,
594
+ footerState,
595
+ () => pi.getThinkingLevel(),
596
+ (): RouterFooterInfo => ({ autoMode: autoMode.getAutoMode() }),
597
+ );
627
598
  else ctx.ui.setFooter(undefined);
628
599
  };
629
600
  const disable = async (ctx: ExtensionContext): Promise<void> => {
@@ -991,25 +962,58 @@ export function registerJittorExtension(
991
962
  ]).catch(() => undefined);
992
963
  });
993
964
 
994
- pi.on("agent_settled", async (_event, ctx) => {
965
+ let ambientRouting: Promise<void> | null = null;
966
+ const scheduleAmbientRouting = (ctx: ExtensionContext): void => {
967
+ if (ambientRouting || !ctx.isIdle()) return;
968
+ ambientRouting = (async () => {
969
+ try {
970
+ await syncCurrentRoute(pi, client, ctx);
971
+ await syncAvailableRoutes(pi, client, ctx);
972
+ if (!ctx.isIdle()) return;
973
+ let budgetActed = false;
974
+ if (enforcement.isEnabled()) {
975
+ const decision = (await client.call("router.decide", {
976
+ session_id: ctx.sessionManager.getSessionId(),
977
+ })) as PolicyDecision;
978
+ if (!ctx.isIdle()) return;
979
+ budgetActed = decision.action !== "continue";
980
+ if (decision.action === "halt") {
981
+ ctx.ui.notify(`Jittor recommends pausing future requests: ${decision.reason}.`, "warning");
982
+ } else if (decision.route && !(await applyRoute(pi, ctx, decision.route)) && ctx.isIdle()) {
983
+ // Catalogs can change while an ambient decision is in flight. Refresh and try one
984
+ // newly-computed route; failure simply leaves the current model in place.
985
+ await syncAvailableRoutes(pi, client, ctx);
986
+ const refreshed = (await client.call("router.decide", {
987
+ session_id: ctx.sessionManager.getSessionId(),
988
+ })) as PolicyDecision;
989
+ if (ctx.isIdle() && refreshed.route) await applyRoute(pi, ctx, refreshed.route);
990
+ }
991
+ }
992
+ if (!budgetActed && ctx.isIdle()) await runAutoModeTurn(ctx);
993
+ } catch {
994
+ // Routing is opportunistic: daemon latency or failure must never enter Pi's request path.
995
+ } finally {
996
+ ambientRouting = null;
997
+ }
998
+ })();
999
+ };
1000
+
1001
+ pi.on("agent_settled", (_event, ctx) => {
995
1002
  if (footerState.compaction) {
996
1003
  finishCompactionUi();
997
1004
  if (compactionTelemetry.hasOpenCompaction())
998
- await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "agent-settled-without-completion")]).catch(() => undefined);
1005
+ void recordMetrics(client, [compactionTelemetry.abort(Date.now(), "agent-settled-without-completion")]).catch(() => undefined);
999
1006
  }
1000
1007
  scheduleCodexRecovery(ctx);
1008
+ scheduleAmbientRouting(ctx);
1001
1009
  if (!enforcement.isFooterEnabled()) return;
1002
- try {
1003
- await syncCurrentRoute(pi, client, ctx);
1004
- await syncAvailableRoutes(pi, client, ctx);
1005
- await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
1006
- } catch {
1010
+ void refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => {
1007
1011
  footerState.providerBudget = null;
1008
1012
  footerState.requestRender?.();
1009
- }
1013
+ });
1010
1014
  });
1011
1015
 
1012
- pi.on("input", async (event, ctx) => {
1016
+ pi.on("input", (event) => {
1013
1017
  if (event.source !== "extension") {
1014
1018
  cancelRecovery(true);
1015
1019
  // Auto mode's own effort classifier reads this at the next turn_start -- captured
@@ -1017,57 +1021,34 @@ export function registerJittorExtension(
1017
1021
  // separate, independently-toggled feature from budget enforcement.
1018
1022
  pendingUserText = event.text;
1019
1023
  }
1020
- if (event.source === "extension" || !enforcement.isEnabled()) return { action: "continue" as const };
1021
- try {
1022
- const next = (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision;
1023
- if (next.action === "halt") {
1024
- ctx.ui.notify(`Jittor blocked input: ${next.reason}. ${RECOVERY_GUIDANCE}.`, "warning");
1025
- return { action: "handled" as const };
1026
- }
1027
- return { action: "continue" as const };
1028
- } catch {
1029
- ctx.ui.notify(`Jittor could not verify budget telemetry, so fail-closed enforcement blocked input. ${RECOVERY_GUIDANCE}.`, "error");
1030
- return { action: "handled" as const };
1031
- }
1024
+ // Routing is advisory and ambient. The submitted message always enters Pi immediately;
1025
+ // daemon decisions computed while idle can optimize a later turn.
1026
+ return { action: "continue" as const };
1032
1027
  });
1033
1028
 
1034
- pi.on("model_select", async (event, ctx) => {
1035
- await syncCurrentRoute(pi, client, ctx, event.model)
1029
+ pi.on("model_select", (event, ctx) => {
1030
+ void syncCurrentRoute(pi, client, ctx, event.model)
1036
1031
  .then(() => syncAvailableRoutes(pi, client, ctx))
1032
+ .then(() =>
1033
+ enforcement.isFooterEnabled()
1034
+ ? refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined)
1035
+ : undefined,
1036
+ )
1037
1037
  .catch(() => undefined);
1038
- if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
1039
1038
  });
1040
1039
 
1041
- pi.on("thinking_level_select", async (event, ctx) => {
1042
- await syncCurrentRoute(pi, client, ctx, ctx.model, event.level).catch(() => undefined);
1040
+ pi.on("thinking_level_select", (event, ctx) => {
1041
+ void syncCurrentRoute(pi, client, ctx, ctx.model, event.level).catch(() => undefined);
1043
1042
  });
1044
1043
 
1045
- pi.on("turn_start", async (event, ctx) => {
1044
+ pi.on("turn_start", (event, ctx) => {
1046
1045
  currentSessionId = ctx.sessionManager.getSessionId();
1047
1046
  compactionTelemetry.observeTurn();
1048
1047
  codexRecoveryCapability.resetTurn();
1049
1048
  providerResponseTelemetry.resetTurn();
1050
1049
  localRunTelemetry.beginTurn(event.timestamp);
1051
- // Budget/enforcement routing is safety-critical and always wins on conflict: Auto mode is
1052
- // only evaluated when budget pressure required no action this turn (action === "continue"),
1053
- // so the two decision sources never fight over the model mid-turn.
1054
- let budgetActedThisTurn = false;
1055
- if (enforcement.isEnabled()) {
1056
- try {
1057
- await syncCurrentRoute(pi, client, ctx);
1058
- await syncAvailableRoutes(pi, client, ctx);
1059
- const budgetDecision = (await client.call("router.decide", {
1060
- session_id: ctx.sessionManager.getSessionId(),
1061
- })) as PolicyDecision;
1062
- budgetActedThisTurn = budgetDecision.action !== "continue";
1063
- await applyDecision(pi, client, ctx, budgetDecision);
1064
- await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
1065
- } catch {
1066
- halt(ctx, "Jittor could not verify or apply a safe route");
1067
- return;
1068
- }
1069
- }
1070
- if (!budgetActedThisTurn) await runAutoModeTurn(ctx).catch(() => undefined);
1050
+ // Deliberately no daemon or routing work here: Pi awaits turn_start before provider delivery.
1051
+ // The previous agent_settled event computes and applies recommendations for later turns.
1071
1052
  });
1072
1053
 
1073
1054
  pi.on("message_update", async (event) => {
@@ -1084,35 +1065,35 @@ export function registerJittorExtension(
1084
1065
  if (currentTurnToolNames.length < EFFORT_CLASSIFICATION_MAX_TOOL_NAMES) currentTurnToolNames.push(event.toolName);
1085
1066
  });
1086
1067
 
1087
- pi.on("after_provider_response", async (event, ctx) => {
1068
+ pi.on("after_provider_response", (event, ctx) => {
1088
1069
  localRunTelemetry.onProviderResponse();
1089
1070
  if (ctx.model?.provider === "openai-codex") codexRecoveryCapability.notifyResponse(event.status, event.headers);
1090
1071
  const notifySchemaDrift = (message: string) => {
1091
1072
  if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected ${message}. ${RECOVERY_GUIDANCE}.`, "error");
1092
1073
  };
1093
- await providerResponseTelemetry.handleProviderResponse(client, ctx.model?.provider, event.status, event.headers, notifySchemaDrift);
1094
- if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
1074
+ // Pi fires this before consuming the provider stream. Never put telemetry or footer RPCs
1075
+ // between the HTTP response and the first streamed token.
1076
+ void providerResponseTelemetry
1077
+ .handleProviderResponse(client, ctx.model?.provider, event.status, event.headers, notifySchemaDrift)
1078
+ .catch(() => undefined);
1095
1079
  });
1096
1080
 
1097
- pi.on("turn_end", async (event, ctx) => {
1081
+ pi.on("turn_end", (event, ctx) => {
1098
1082
  const tokens = ctx.getContextUsage()?.tokens;
1099
1083
  if (typeof tokens === "number" && Number.isFinite(tokens)) contextGrowth.observe(++contextGrowthTurn, tokens);
1100
1084
  const metrics = localRunTelemetry.completeTurn(event.message, pi.getThinkingLevel());
1101
- await recordMetrics(client, metrics).catch(() => undefined);
1085
+ void recordMetrics(client, metrics).catch(() => undefined);
1102
1086
  priorTurnToolNames = currentTurnToolNames;
1103
1087
  currentTurnToolNames = [];
1104
1088
  });
1105
1089
 
1106
- pi.on("message_end", async (event, ctx) => {
1090
+ pi.on("message_end", (event, ctx) => {
1107
1091
  if (event.message.role === "assistant") {
1108
1092
  if (event.message.provider === "openai-codex")
1109
1093
  codexRecoveryCapability.notifyMessageEnd(event.message.stopReason, event.message.errorMessage);
1110
- await providerResponseTelemetry.handleMessageEnd(
1111
- client,
1112
- event.message.provider,
1113
- event.message.stopReason,
1114
- event.message.errorMessage,
1115
- );
1094
+ void providerResponseTelemetry
1095
+ .handleMessageEnd(client, event.message.provider, event.message.stopReason, event.message.errorMessage)
1096
+ .catch(() => undefined);
1116
1097
  }
1117
1098
  const metrics = assistantUsageMetrics(
1118
1099
  event.message,
@@ -1132,9 +1113,10 @@ export function registerJittorExtension(
1132
1113
  cacheRead: amount("cache-read-tokens"),
1133
1114
  cacheWrite: amount("cache-write-tokens"),
1134
1115
  });
1135
- await recordMetrics(client, metrics).catch(() => undefined);
1116
+ void recordMetrics(client, metrics).catch(() => undefined);
1136
1117
  }
1137
- if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
1118
+ // Footer refresh is consolidated in agent_settled; finalized-message accounting must not
1119
+ // hold back streamed lifecycle delivery or the next queued prompt.
1138
1120
  });
1139
1121
 
1140
1122
  pi.on("session_shutdown", async (_event, ctx) => {
@@ -69,6 +69,11 @@ export type ProviderBudget =
69
69
  valueText: string;
70
70
  };
71
71
 
72
+ /** Compact, always-visible state of Jittor's effort-based Auto mode -- the newer, effort-driven router, distinct from the existing budget-pressure route already reflected by the model/budget segments. */
73
+ export interface RouterFooterInfo {
74
+ autoMode: "off" | "suggest" | "auto-switch";
75
+ }
76
+
72
77
  export interface CompactionProgress {
73
78
  startedAt: number;
74
79
  initialFraction: number;
@@ -238,6 +243,19 @@ function budgetSegment(
238
243
  return `${budget.label} ${bar} ${value}${reset ? ` · ${reset}` : ""}${staleText}`;
239
244
  }
240
245
 
246
+ /**
247
+ * "off" is dim (nothing happening); "suggest" is plain/unstyled -- it evaluates and may prompt,
248
+ * but only ever acts on explicit confirmation; "auto-switch" alone gets the eye-catching accent
249
+ * color, since it is the only state that can change the active model without asking first --
250
+ * the same distinction that already gates entering it behind an explicit settings confirmation.
251
+ */
252
+ function routerSegment(info: RouterFooterInfo | undefined, theme: FooterTheme): string | undefined {
253
+ if (!info) return undefined;
254
+ if (info.autoMode === "off") return `auto ${theme.fg("dim", info.autoMode)}`;
255
+ if (info.autoMode === "auto-switch") return `auto ${theme.fg("accent", info.autoMode)}`;
256
+ return `auto ${info.autoMode}`;
257
+ }
258
+
241
259
  function usageSegment(context: FooterContext): string {
242
260
  const totals = usageTotals(context);
243
261
  const parts: string[] = [];
@@ -295,6 +313,7 @@ export function renderFooterLines(
295
313
  width: number,
296
314
  now = Date.now(),
297
315
  compaction?: CompactionProgress,
316
+ router?: RouterFooterInfo,
298
317
  ): string[] {
299
318
  const safeWidth = Math.max(1, width);
300
319
  const repository = repositorySegment(context, footerData, theme);
@@ -306,6 +325,7 @@ export function renderFooterLines(
306
325
  const minimalContext = minimalContextSegment(context, theme, safeWidth, now, compaction);
307
326
  const fullBudget = budgetSegment(providerBudget, theme, safeWidth, false, now);
308
327
  const compactBudget = budgetSegment(providerBudget, theme, safeWidth, true, now);
328
+ const routerInfo = routerSegment(router, theme);
309
329
  const statuses = [...footerData.getExtensionStatuses().entries()]
310
330
  .filter(([key]) => key !== "jittor")
311
331
  .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
@@ -313,8 +333,11 @@ export function renderFooterLines(
313
333
  .join(" ");
314
334
 
315
335
  const candidates = [
336
+ joinSegments([repository, model.full, usage, fullContext, fullBudget, routerInfo, statuses]),
337
+ joinSegments([repository, model.full, usage, fullContext, fullBudget, routerInfo]),
316
338
  joinSegments([repository, model.full, usage, fullContext, fullBudget, statuses]),
317
339
  joinSegments([repository, model.full, usage, fullContext, fullBudget]),
340
+ joinSegments([model.full, usage, compactContext, compactBudget, routerInfo, statuses]),
318
341
  joinSegments([model.full, usage, compactContext, compactBudget, statuses]),
319
342
  joinSegments([model.full, usage, compactContext, compactBudget]),
320
343
  joinSegments([model.full, compactUsage, compactContext, compactBudget]),
@@ -332,7 +355,14 @@ export interface IntegratedFooterState {
332
355
  requestRender?: () => void;
333
356
  }
334
357
 
335
- export function installIntegratedFooter(ctx: ExtensionContext, state: IntegratedFooterState, getThinkingLevel: () => string): void {
358
+ export function installIntegratedFooter(
359
+ ctx: ExtensionContext,
360
+ state: IntegratedFooterState,
361
+ getThinkingLevel: () => string,
362
+ // Called fresh every render, the same as getThinkingLevel -- Auto mode's setting can change at
363
+ // any time via /jittor settings, so the footer must never cache a stale snapshot of it.
364
+ getRouterInfo: () => RouterFooterInfo | undefined = () => undefined,
365
+ ): void {
336
366
  ctx.ui.setStatus("jittor", undefined);
337
367
  ctx.ui.setFooter((tui, theme, footerData) => {
338
368
  state.requestRender = () => tui.requestRender();
@@ -349,6 +379,7 @@ export function installIntegratedFooter(ctx: ExtensionContext, state: Integrated
349
379
  width,
350
380
  Date.now(),
351
381
  state.compaction,
382
+ getRouterInfo(),
352
383
  );
353
384
  },
354
385
  dispose() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-jittor",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Pi extension for Jittor token and context observability with optimization controls backed by the @danypops/jittor daemon",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@danypops/pi-extension-harness": "^0.6.2",
29
- "@danypops/pi-process-harness": "^0.1.2",
29
+ "@danypops/pi-process-harness": "^0.3.2",
30
30
  "@danypops/pi-tui-harness": "^0.0.2",
31
31
  "@earendil-works/pi-ai": "*",
32
32
  "bun-types": "latest"