@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +6 -1
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +32 -6
  22. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  23. package/dist/types/lsp/types.d.ts +10 -0
  24. package/dist/types/main.d.ts +3 -2
  25. package/dist/types/memory-backend/index.d.ts +2 -1
  26. package/dist/types/memory-backend/resolve.d.ts +1 -1
  27. package/dist/types/memory-backend/types.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  29. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/index.d.ts +5 -4
  32. package/dist/types/modes/interactive-mode.d.ts +1 -1
  33. package/dist/types/modes/setup-version.d.ts +11 -0
  34. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  35. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  36. package/dist/types/modes/types.d.ts +1 -1
  37. package/dist/types/sdk.d.ts +1 -1
  38. package/dist/types/task/executor.d.ts +7 -0
  39. package/dist/types/telemetry-export.d.ts +1 -1
  40. package/dist/types/tools/eval-render.d.ts +1 -8
  41. package/dist/types/tools/fetch.d.ts +15 -7
  42. package/dist/types/tools/render-utils.d.ts +8 -0
  43. package/dist/types/tools/renderers.d.ts +16 -2
  44. package/dist/types/tools/search.d.ts +1 -1
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/web/scrapers/github.d.ts +22 -0
  47. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  48. package/dist/types/web/search/types.d.ts +1 -1
  49. package/package.json +9 -9
  50. package/scripts/dev-launch +42 -0
  51. package/scripts/dev-launch-preload.ts +19 -0
  52. package/src/cli/args.ts +2 -2
  53. package/src/cli/gallery-cli.ts +223 -0
  54. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  55. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  56. package/src/cli/gallery-fixtures/edit.ts +194 -0
  57. package/src/cli/gallery-fixtures/fs.ts +153 -0
  58. package/src/cli/gallery-fixtures/index.ts +40 -0
  59. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  60. package/src/cli/gallery-fixtures/memory.ts +81 -0
  61. package/src/cli/gallery-fixtures/misc.ts +221 -0
  62. package/src/cli/gallery-fixtures/search.ts +213 -0
  63. package/src/cli/gallery-fixtures/shell.ts +167 -0
  64. package/src/cli/gallery-fixtures/types.ts +41 -0
  65. package/src/cli/gallery-fixtures/web.ts +158 -0
  66. package/src/cli/gallery-screenshot.ts +279 -0
  67. package/src/cli-commands.ts +1 -0
  68. package/src/commands/gallery.ts +52 -0
  69. package/src/commands/launch.ts +1 -1
  70. package/src/config/keybindings.ts +15 -6
  71. package/src/config/model-equivalence.ts +35 -12
  72. package/src/config/model-id-affixes.ts +39 -22
  73. package/src/config/model-registry.ts +16 -16
  74. package/src/config/settings-schema.ts +18 -5
  75. package/src/config/settings.ts +11 -0
  76. package/src/dap/client.ts +14 -16
  77. package/src/edit/renderer.ts +36 -48
  78. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  79. package/src/eval/agent-bridge.ts +34 -7
  80. package/src/extensibility/extensions/runner.ts +1 -0
  81. package/src/extensibility/plugins/doctor.ts +0 -1
  82. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  83. package/src/goals/tools/goal-tool.ts +2 -2
  84. package/src/internal-urls/docs-index.generated.ts +5 -5
  85. package/src/lsp/client.ts +104 -55
  86. package/src/lsp/types.ts +10 -0
  87. package/src/main.ts +44 -49
  88. package/src/memory-backend/index.ts +13 -1
  89. package/src/memory-backend/resolve.ts +3 -5
  90. package/src/memory-backend/types.ts +1 -1
  91. package/src/modes/components/custom-editor.ts +10 -1
  92. package/src/modes/components/status-line.ts +3 -5
  93. package/src/modes/components/tool-execution.ts +61 -16
  94. package/src/modes/controllers/command-controller.ts +13 -2
  95. package/src/modes/controllers/input-controller.ts +11 -3
  96. package/src/modes/controllers/selector-controller.ts +2 -2
  97. package/src/modes/index.ts +5 -4
  98. package/src/modes/interactive-mode.ts +17 -3
  99. package/src/modes/setup-version.ts +11 -0
  100. package/src/modes/setup-wizard/index.ts +3 -2
  101. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/context-usage.ts +10 -6
  104. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  105. package/src/sdk.ts +21 -23
  106. package/src/session/agent-session.ts +7 -7
  107. package/src/slash-commands/builtin-registry.ts +1 -1
  108. package/src/slash-commands/helpers/usage-report.ts +2 -0
  109. package/src/task/executor.ts +20 -2
  110. package/src/task/render.ts +1 -2
  111. package/src/telemetry-export.ts +25 -7
  112. package/src/tools/eval-backends.ts +6 -17
  113. package/src/tools/eval-render.ts +21 -18
  114. package/src/tools/eval.ts +5 -4
  115. package/src/tools/fetch.ts +94 -84
  116. package/src/tools/render-utils.ts +17 -3
  117. package/src/tools/renderers.ts +16 -1
  118. package/src/tools/report-tool-issue.ts +1 -1
  119. package/src/tools/search.ts +173 -81
  120. package/src/tools/todo.ts +20 -7
  121. package/src/tools/write.ts +22 -1
  122. package/src/web/scrapers/github.ts +255 -3
  123. package/src/web/scrapers/youtube.ts +3 -2
  124. package/src/web/search/providers/perplexity.ts +199 -51
  125. package/src/web/search/render.ts +39 -54
  126. package/src/web/search/types.ts +5 -1
  127. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  128. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -31,7 +31,7 @@ import {
31
31
  renderJsonTreeLines,
32
32
  } from "../../tools/json-tree";
33
33
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
34
- import { toolRenderers } from "../../tools/renderers";
34
+ import { type ToolRenderer, toolRenderers } from "../../tools/renderers";
35
35
  import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
36
36
  import { isFramedBlockComponent, renderStatusLine } from "../../tui";
37
37
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
@@ -51,6 +51,12 @@ function addBoxChild(box: Box, component: unknown): boolean {
51
51
  return isFramedBlockComponent(child);
52
52
  }
53
53
 
54
+ function setBoxPaddingForFramedBlock(box: Box, hasFramedBlock: boolean): void {
55
+ const padding = hasFramedBlock ? 0 : 1;
56
+ box.setPaddingX(padding);
57
+ box.setPaddingY(padding);
58
+ }
59
+
54
60
  /**
55
61
  * Drop trailing removal/hunk-header lines that appear in a streaming diff
56
62
  * before the matching `+added` lines have arrived. Without this, a partial
@@ -524,6 +530,39 @@ export class ToolExecutionComponent extends Container {
524
530
  return (this.#result.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
525
531
  }
526
532
 
533
+ /**
534
+ * While a tool's preview is still streaming, a block whose preview is
535
+ * append-only (rows only grow at the bottom, never re-layout) lets the
536
+ * renderer commit the scrolled-off head of an over-tall preview to native
537
+ * scrollback instead of dropping it — the same anti-yank path a streaming
538
+ * assistant reply uses (see {@link TranscriptContainer} +
539
+ * `NativeScrollbackLiveRegion`). Covers both phases: a pre-result call preview
540
+ * (a `write` whose content streams in) and a partial-result preview that
541
+ * streams output below fixed input (an `eval`/`bash` whose stdout grows under
542
+ * its code cell). Gated on {@link isTranscriptBlockFinalized} so the boundary
543
+ * closes the instant the block reaches a terminal state — a final result that
544
+ * may collapse to a compact view, a backgrounded async tool, or a seal — and
545
+ * the renderer decides whether its current preview shape qualifies via
546
+ * `isStreamingPreviewAppendOnly` (typically: only the expanded full view,
547
+ * which is top-anchored; the collapsed tail window re-layouts but is bounded
548
+ * so it never overflows anyway).
549
+ */
550
+ isTranscriptBlockAppendOnly(): boolean {
551
+ // A finalized block's preview can collapse/re-layout; only a live,
552
+ // still-streaming block is a candidate.
553
+ if (this.isTranscriptBlockFinalized()) return false;
554
+ const predicate =
555
+ (this.#tool as { isStreamingPreviewAppendOnly?: ToolRenderer["isStreamingPreviewAppendOnly"] } | undefined)
556
+ ?.isStreamingPreviewAppendOnly ?? toolRenderers[this.#toolName]?.isStreamingPreviewAppendOnly;
557
+ if (!predicate) return false;
558
+ try {
559
+ return predicate(this.#getCallArgsForRender(), this.#renderState, this.#result);
560
+ } catch (err) {
561
+ logger.warn("Tool append-only predicate failed", { tool: this.#toolName, error: String(err) });
562
+ return false;
563
+ }
564
+ }
565
+
527
566
  /**
528
567
  * Mark the tool terminal even though no result arrived (the turn aborted or
529
568
  * abandoned it) and stop animating, so it can freeze and stops pinning the
@@ -595,22 +634,28 @@ export class ToolExecutionComponent extends Container {
595
634
  // call preview once result lines exist.
596
635
  this.#renderState.renderContext = this.#buildRenderContext();
597
636
 
598
- // Render call component
637
+ // Render call component. The fallback label only stands in for a
638
+ // missing `renderCall`; when the call is intentionally suppressed
639
+ // (mergeCallAndResult once a result exists) we render nothing here so
640
+ // the result component isn't preceded by a redundant tool-name line.
599
641
  const shouldRenderCall = !this.#result || !mergeCallAndResult;
600
- if (shouldRenderCall && tool.renderCall) {
601
- try {
602
- const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
603
- if (callComponent) {
604
- contentBoxHasFramedBlock = addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
642
+ if (shouldRenderCall) {
643
+ if (tool.renderCall) {
644
+ try {
645
+ const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
646
+ if (callComponent) {
647
+ contentBoxHasFramedBlock =
648
+ addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
649
+ }
650
+ } catch (err) {
651
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
652
+ // Fall back to default on error
653
+ addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
605
654
  }
606
- } catch (err) {
607
- logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
608
- // Fall back to default on error
655
+ } else {
656
+ // No custom renderCall, show tool name
609
657
  addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
610
658
  }
611
- } else {
612
- // No custom renderCall, show tool name
613
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
614
659
  }
615
660
 
616
661
  // Render result component if we have a result
@@ -650,7 +695,7 @@ export class ToolExecutionComponent extends Container {
650
695
  addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
651
696
  }
652
697
  }
653
- this.#contentBox.setPaddingX(contentBoxHasFramedBlock ? 0 : 1);
698
+ setBoxPaddingForFramedBlock(this.#contentBox, contentBoxHasFramedBlock);
654
699
  } else if (this.#toolName in toolRenderers) {
655
700
  // Built-in tools with renderers
656
701
  const renderer = toolRenderers[this.#toolName];
@@ -693,7 +738,7 @@ export class ToolExecutionComponent extends Container {
693
738
  );
694
739
  if (resultComponent) {
695
740
  const fileBoxHasFramedBlock = addBoxChild(fileBox, resultComponent);
696
- fileBox.setPaddingX(fileBoxHasFramedBlock ? 0 : 1);
741
+ setBoxPaddingForFramedBlock(fileBox, fileBoxHasFramedBlock);
697
742
  }
698
743
  } catch (err) {
699
744
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
@@ -776,7 +821,7 @@ export class ToolExecutionComponent extends Container {
776
821
  }
777
822
  }
778
823
  }
779
- this.#contentBox.setPaddingX(contentBoxHasFramedBlock ? 0 : 1);
824
+ setBoxPaddingForFramedBlock(this.#contentBox, contentBoxHasFramedBlock);
780
825
  }
781
826
  } else {
782
827
  // Other built-in tools: use Text directly with caching
@@ -13,7 +13,6 @@ import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi
13
13
  import { formatDuration, Snowflake } from "@oh-my-pi/pi-utils";
14
14
  import { $ } from "bun";
15
15
  import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
16
- import { loadCustomShare } from "../../export/custom-share";
17
16
  import type { CompactOptions } from "../../extensibility/extensions/types";
18
17
  import {
19
18
  diffMentalModelContent,
@@ -131,6 +130,7 @@ export class CommandController {
131
130
  }
132
131
 
133
132
  try {
133
+ const { loadCustomShare } = await import("../../export/custom-share");
134
134
  const customShare = await loadCustomShare();
135
135
  if (customShare) {
136
136
  const loader = new BorderedLoader(this.ctx.ui, theme, "Sharing...");
@@ -465,7 +465,7 @@ export class CommandController {
465
465
  const argumentText = text.slice(7).trim();
466
466
  const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
467
467
  const agentDir = this.ctx.settings.getAgentDir();
468
- const backend = resolveMemoryBackend(this.ctx.settings);
468
+ const backend = await resolveMemoryBackend(this.ctx.settings);
469
469
 
470
470
  if (action === "view") {
471
471
  const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
@@ -1272,6 +1272,8 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe
1272
1272
  if (email) return email;
1273
1273
  const accountId = (report.metadata?.accountId as string | undefined) ?? limit.scope.accountId;
1274
1274
  if (accountId) return accountId;
1275
+ const projectId = (report.metadata?.projectId as string | undefined) ?? limit.scope.projectId;
1276
+ if (projectId) return projectId;
1275
1277
  return `account ${index + 1}`;
1276
1278
  }
1277
1279
 
@@ -1280,6 +1282,8 @@ function formatUnlimitedReportLabel(report: UsageReport, index: number): string
1280
1282
  if (email) return email;
1281
1283
  const accountId = report.metadata?.accountId as string | undefined;
1282
1284
  if (accountId) return accountId;
1285
+ const projectId = report.metadata?.projectId as string | undefined;
1286
+ if (projectId) return projectId;
1283
1287
  return `account ${index + 1}`;
1284
1288
  }
1285
1289
 
@@ -1365,6 +1369,13 @@ function formatAggregateAmount(limits: UsageLimit[]): string {
1365
1369
  return `${formatNumber(remainingPct)}% free`;
1366
1370
  }
1367
1371
 
1372
+ // Count unique accounts from limit scopes — not limits.length.
1373
+ const uniqueAccountIds = new Set(
1374
+ limits.map(limit => limit.scope.accountId).filter((id): id is string => typeof id === "string" && id.length > 0),
1375
+ );
1376
+ if (uniqueAccountIds.size > 0) return `${uniqueAccountIds.size} ${uniqueAccountIds.size === 1 ? "acct" : "accts"}`;
1377
+ // No account IDs available — keep the pre-existing fallback so providers
1378
+ // that don't populate scope.accountId still show a summary.
1368
1379
  return `${limits.length} accts`;
1369
1380
  }
1370
1381
 
@@ -144,6 +144,8 @@ export class InputController {
144
144
  this.ctx.editor.setActionKeys("app.clear", this.ctx.keybindings.getKeys("app.clear"));
145
145
  this.ctx.editor.onClear = () => this.handleCtrlC();
146
146
  this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
147
+ this.ctx.editor.setActionKeys("app.display.reset", this.ctx.keybindings.getKeys("app.display.reset"));
148
+ this.ctx.editor.onDisplayReset = () => this.ctx.ui.resetDisplay();
147
149
  this.ctx.editor.onExit = () => this.handleCtrlD();
148
150
  this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
149
151
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
@@ -188,11 +190,9 @@ export class InputController {
188
190
  this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
189
191
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
190
192
  this.ctx.editor.onDequeue = () => this.handleDequeue();
191
-
192
193
  this.ctx.editor.clearCustomKeyHandlers();
193
194
  // Wire up extension shortcuts
194
195
  this.registerExtensionShortcuts();
195
-
196
196
  const planModeKeys = this.ctx.keybindings.getKeys("app.plan.toggle");
197
197
  for (const key of planModeKeys) {
198
198
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
@@ -846,7 +846,15 @@ export class InputController {
846
846
  child.setExpanded(expanded);
847
847
  }
848
848
  }
849
- this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
849
+ // Toggling expansion mutates every block, but on ED3-risk terminals the
850
+ // transcript freezes a snapshot of each block once it scrolls past the live
851
+ // region (committed native scrollback is immutable there). A plain repaint
852
+ // replays those stale snapshots, so the toggle appears to do nothing above
853
+ // the live block. resetDisplay() invalidates the snapshots and forces a
854
+ // full clear + replay — the keyboard-accessible resize-reset equivalent —
855
+ // which is the only path that re-emits the whole transcript at its new
856
+ // heights.
857
+ this.ctx.ui.resetDisplay();
850
858
  }
851
859
 
852
860
  toggleThinkingBlockVisibility(): void {
@@ -7,7 +7,6 @@ import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-m
7
7
  import { getRoleInfo } from "../../config/model-registry";
8
8
  import { formatModelSelectorValue } from "../../config/model-resolver";
9
9
  import { settings } from "../../config/settings";
10
- import { DebugSelectorComponent } from "../../debug";
11
10
  import { disableProvider, enableProvider } from "../../discovery";
12
11
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
13
12
  import {
@@ -1080,7 +1079,8 @@ export class SelectorController {
1080
1079
  });
1081
1080
  }
1082
1081
 
1083
- showDebugSelector(): void {
1082
+ async showDebugSelector(): Promise<void> {
1083
+ const { DebugSelectorComponent } = await import("../../debug");
1084
1084
  this.showSelector(done => {
1085
1085
  const selector = new DebugSelectorComponent(this.ctx, done);
1086
1086
  return { component: selector, focus: selector };
@@ -2,11 +2,13 @@ import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
2
2
  import { postmortem } from "@oh-my-pi/pi-utils";
3
3
 
4
4
  /**
5
- * Run modes for the coding agent.
5
+ * Interactive mode and embeddable RPC client exports for the coding agent.
6
+ *
7
+ * Branch-specific runners live in their concrete modules so importing this
8
+ * barrel does not pull print, RPC server, or ACP server mode into the normal
9
+ * TUI graph.
6
10
  */
7
- export { runAcpMode } from "./acp";
8
11
  export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
9
- export { type PrintModeOptions, runPrintMode } from "./print-mode";
10
12
  export {
11
13
  defineRpcClientTool,
12
14
  type ModelInfo,
@@ -17,7 +19,6 @@ export {
17
19
  type RpcClientToolResult,
18
20
  type RpcEventListener,
19
21
  } from "./rpc/rpc-client";
20
- export { runRpcMode } from "./rpc/rpc-mode";
21
22
  export type {
22
23
  RpcCommand,
23
24
  RpcHostToolCallRequest,
@@ -250,6 +250,20 @@ export interface InteractiveModeOptions {
250
250
  initialMessages?: string[];
251
251
  }
252
252
 
253
+ /**
254
+ * Plan-review preview block. Once rendered it is static (a one-shot Markdown of
255
+ * the plan file), so even while it sits as the live bottom block beneath the
256
+ * approval selector its scrolled-off head is safe to commit to native
257
+ * scrollback. Reporting append-only lets an over-tall plan + selector commit the
258
+ * plan's head instead of clipping it — without this a plain {@link Container} is
259
+ * deferred and a long plan is cut off the top on ED3-risk terminals.
260
+ */
261
+ class PlanReviewBlock extends Container {
262
+ isTranscriptBlockAppendOnly(): boolean {
263
+ return true;
264
+ }
265
+ }
266
+
253
267
  export class InteractiveMode implements InteractiveModeContext {
254
268
  session: AgentSession;
255
269
  sessionManager: SessionManager;
@@ -1680,7 +1694,7 @@ export class InteractiveMode implements InteractiveModeContext {
1680
1694
  #renderPlanPreview(planContent: string, options?: { append?: boolean }): void {
1681
1695
  const existingContainer = this.#planReviewContainer;
1682
1696
  const replaceExisting = options?.append !== true && existingContainer !== undefined;
1683
- const planReviewContainer = replaceExisting ? existingContainer : new Container();
1697
+ const planReviewContainer = replaceExisting ? existingContainer : new PlanReviewBlock();
1684
1698
  planReviewContainer.clear();
1685
1699
  planReviewContainer.addChild(new Spacer(1));
1686
1700
  planReviewContainer.addChild(new DynamicBorder());
@@ -2844,8 +2858,8 @@ export class InteractiveMode implements InteractiveModeContext {
2844
2858
  }
2845
2859
  }
2846
2860
 
2847
- showDebugSelector(): void {
2848
- this.#selectorController.showDebugSelector();
2861
+ async showDebugSelector(): Promise<void> {
2862
+ await this.#selectorController.showDebugSelector();
2849
2863
  }
2850
2864
 
2851
2865
  showSessionObserver(): void {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Setup version the wizard advances a fresh install to. Bump it whenever a new
3
+ * setup scene lands (or an existing scene raises its `minVersion`).
4
+ *
5
+ * Kept in its own dependency-free module so the cold-launch gate in `main.ts`
6
+ * can answer "is the stored setup version stale?" without statically importing
7
+ * the full wizard — every scene (sign-in/OAuth, web search, theme previews) plus
8
+ * the overlay component and their TUI deps. MUST equal `max(scene.minVersion)`
9
+ * across `ALL_SCENES`; the `setup-wizard` barrel and test suite guard it.
10
+ */
11
+ export const CURRENT_SETUP_VERSION = 1;
@@ -1,4 +1,5 @@
1
1
  import type { Settings } from "../../config/settings";
2
+ import { CURRENT_SETUP_VERSION } from "../setup-version";
2
3
  import type { InteractiveModeContext } from "../types";
3
4
  import { glyphSetupScene } from "./scenes/glyph";
4
5
  import { providersSetupScene } from "./scenes/providers";
@@ -8,14 +9,14 @@ import { SetupWizardComponent } from "./wizard-overlay";
8
9
 
9
10
  export type { SetupScene, SetupSceneController, SetupSceneHost, SetupSceneResult } from "./scenes/types";
10
11
 
12
+ export { CURRENT_SETUP_VERSION };
13
+
11
14
  export const ALL_SCENES = [
12
15
  providersSetupScene,
13
16
  glyphSetupScene,
14
17
  themeSetupScene,
15
18
  ] as const satisfies readonly SetupScene[];
16
19
 
17
- export const CURRENT_SETUP_VERSION = ALL_SCENES.reduce((max, scene) => Math.max(max, scene.minVersion), 0);
18
-
19
20
  export interface SetupSceneSelectionOptions {
20
21
  resuming?: boolean;
21
22
  isTTY?: boolean;
@@ -19,7 +19,8 @@ type Availability = "checking" | boolean;
19
19
  /**
20
20
  * "Web search" panel: picks the provider the web_search tool should prefer and
21
21
  * reports whether the highlighted provider is ready to use given current
22
- * credentials (env keys or OAuth sign-ins from the Sign in tab).
22
+ * credentials (env keys or OAuth sign-ins from the Sign in tab) or an
23
+ * unauthenticated fallback.
23
24
  */
24
25
  export class WebSearchTab implements SetupTab {
25
26
  readonly id = "web-search";
@@ -91,7 +92,7 @@ export class WebSearchTab implements SetupTab {
91
92
  let ready = false;
92
93
  try {
93
94
  const provider = await getSearchProvider(id);
94
- ready = await provider.isAvailable(this.host.ctx.session.modelRegistry.authStorage);
95
+ ready = await provider.isExplicitlyAvailable(this.host.ctx.session.modelRegistry.authStorage);
95
96
  } catch {
96
97
  ready = false;
97
98
  }
@@ -269,7 +269,7 @@ export interface InteractiveModeContext {
269
269
  handleSessionDeleteCommand(): Promise<void>;
270
270
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
271
271
  showHookConfirm(title: string, message: string): Promise<boolean>;
272
- showDebugSelector(): void;
272
+ showDebugSelector(): Promise<void>;
273
273
  showSessionObserver(): void;
274
274
  resetObserverRegistry(): void;
275
275
 
@@ -37,6 +37,9 @@ export interface ContextBreakdown {
37
37
  freeTokens: number;
38
38
  }
39
39
 
40
+ const EMPTY_STRING_PARTS: readonly string[] = [];
41
+ const EMPTY_TOOLS: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">> = [];
42
+
40
43
  export function estimateSkillsTokens(skills: readonly Skill[]): number {
41
44
  const fragments: string[] = [];
42
45
  for (const skill of skills) {
@@ -75,15 +78,16 @@ export function estimateToolSchemaTokens(
75
78
  * messages walked incrementally as new entries append.
76
79
  */
77
80
  export function computeNonMessageTokens(session: AgentSession): number {
78
- const parts = computeNonMessageBreakdown(session);
79
- return parts.systemPromptTokens + parts.systemContextTokens + parts.toolsTokens + parts.skillsTokens;
81
+ const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
82
+ const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
83
+ return countTokens(systemPromptParts) + estimateToolSchemaTokens(tools);
80
84
  }
81
85
 
82
86
  /**
83
- * Shared helper for the four non-message token totals. Single source of truth
84
- * for both `computeNonMessageTokens` (status-line incremental cache) and
85
- * `computeContextBreakdown` (/context panel). The split avoids drift between
86
- * the two surfaces — they MUST report the same numbers.
87
+ * Shared helper for the four non-message token totals used by
88
+ * `computeContextBreakdown` (/context panel). Keep this category split stable:
89
+ * the status-line fast path intentionally uses the equivalent collapsed total
90
+ * in `computeNonMessageTokens`.
87
91
  */
88
92
  function computeNonMessageBreakdown(session: AgentSession): {
89
93
  skillsTokens: number;
@@ -37,6 +37,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
37
37
  `| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
38
38
  `| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
39
39
  `| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
40
+ `| \`${appKey(bindings, "app.display.reset")}\` | Reset terminal display |`,
40
41
  `| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
41
42
  `| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle role models (slow/default/smol) |`,
42
43
  `| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle role models (backward) |`,
package/src/sdk.ts CHANGED
@@ -36,7 +36,6 @@ import {
36
36
  } from "@oh-my-pi/pi-utils";
37
37
  import chalk from "chalk";
38
38
  import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
39
- import { createAutoresearchExtension } from "./autoresearch";
40
39
  import { loadCapability } from "./capability";
41
40
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
42
41
  import { bucketRules } from "./capability/rule-buckets";
@@ -57,7 +56,6 @@ import { resolveConfigValue } from "./config/resolve-config-value";
57
56
  import { initializeWithSettings } from "./discovery";
58
57
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
59
58
  import { defaultEvalSessionId } from "./eval/session-id";
60
- import { TtsrManager } from "./export/ttsr";
61
59
  import {
62
60
  type CustomCommandsLoadResult,
63
61
  type LoadedCustomCommand,
@@ -90,7 +88,7 @@ import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls
90
88
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
91
89
  import { discoverAndLoadMCPTools, MCPManager, type MCPToolsLoadResult } from "./mcp";
92
90
  import { resolveMemoryBackend } from "./memory-backend";
93
- import { getMnemopiSessionState, type MnemopiSessionState } from "./mnemopi/state";
91
+ import type { MnemopiSessionState } from "./mnemopi/state";
94
92
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
95
93
  import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
96
94
  import {
@@ -1150,6 +1148,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1150
1148
 
1151
1149
  // Discover rules and bucket them in one pass to avoid repeated scans over large rule sets.
1152
1150
  const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time("discoverTtsrRules", async () => {
1151
+ const { TtsrManager } = await import("./export/ttsr");
1153
1152
  const ttsrSettings = settings.getGroup("ttsr");
1154
1153
  const ttsrManager = new TtsrManager(ttsrSettings);
1155
1154
  const rulesResult =
@@ -1295,7 +1294,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1295
1294
  session ? session.trackEvalExecution(execution, abortController) : execution,
1296
1295
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1297
1296
  getHindsightSessionState: () => session?.getHindsightSessionState(),
1298
- getMnemopiSessionState: () => getMnemopiSessionState(session),
1297
+ getMnemopiSessionState: () => session?.getMnemopiSessionState(),
1299
1298
  getAgentId: () => resolvedAgentId,
1300
1299
  getToolByName: name => session?.getToolByName(name),
1301
1300
  agentRegistry,
@@ -1472,7 +1471,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1472
1471
  }
1473
1472
 
1474
1473
  const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
1475
- inlineExtensions.push(createAutoresearchExtension);
1474
+ inlineExtensions.push((await import("./autoresearch")).createAutoresearchExtension);
1476
1475
  if (customTools.length > 0) {
1477
1476
  inlineExtensions.push(createCustomToolsExtension(customTools));
1478
1477
  }
@@ -1607,9 +1606,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1607
1606
  // `ExtensionToolWrapper` installed below is the only place the per-tool approval gate runs.
1608
1607
  // A conditional runner means the approval system silently disappears for users with no
1609
1608
  // extensions, contradicting non-yolo `tools.approvalMode` settings without feedback.
1610
- // (Today `createAutoresearchExtension` is unconditionally pushed below, so this scenario
1611
- // is unreachable; the unconditional construction makes that invariant explicit instead of
1612
- // implicit, so a future change to make autoresearch optional cannot silently re-open the hole.)
1609
+ // (The builtin autoresearch extension is unconditionally loaded above, so this scenario
1610
+ // is unreachable; unconditional runner construction keeps that invariant explicit and
1611
+ // prevents future optional extensions from silently re-opening the hole.)
1613
1612
  const extensionRunner: ExtensionRunner = new ExtensionRunner(
1614
1613
  extensionsResult.extensions,
1615
1614
  extensionsResult.runtime,
@@ -1723,7 +1722,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1723
1722
 
1724
1723
  const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1725
1724
  const eagerTasks = settings.get("task.eager");
1726
- const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1725
+ const intentField = $flag("PI_INTENT_TRACING", settings.get("tools.intentTracing")) ? INTENT_FIELD : undefined;
1727
1726
  const rebuildSystemPrompt = async (
1728
1727
  toolNames: string[],
1729
1728
  tools: Map<string, AgentTool>,
@@ -1749,7 +1748,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1749
1748
  const promptTools = buildSystemPromptToolMetadata(tools, {
1750
1749
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
1751
1750
  });
1752
- const memoryBackend = resolveMemoryBackend(settings);
1751
+ const memoryBackend = await resolveMemoryBackend(settings);
1753
1752
  const memoryInstructions = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
1754
1753
 
1755
1754
  // Build combined append prompt: memory instructions + MCP server instructions
@@ -2267,19 +2266,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2267
2266
  }
2268
2267
  }
2269
2268
 
2270
- logger.time("startMemoryStartupTask", () =>
2271
- Promise.resolve(
2272
- resolveMemoryBackend(settings).start({
2273
- session,
2274
- settings,
2275
- modelRegistry,
2276
- agentDir,
2277
- taskDepth,
2278
- parentHindsightSessionState: options.parentHindsightSessionState,
2279
- parentMnemopiSessionState: options.parentMnemopiSessionState,
2280
- }),
2281
- ),
2282
- );
2269
+ logger.time("startMemoryStartupTask", async () => {
2270
+ const memoryBackend = await resolveMemoryBackend(settings);
2271
+ await memoryBackend.start({
2272
+ session,
2273
+ settings,
2274
+ modelRegistry,
2275
+ agentDir,
2276
+ taskDepth,
2277
+ parentHindsightSessionState: options.parentHindsightSessionState,
2278
+ parentMnemopiSessionState: options.parentMnemopiSessionState,
2279
+ });
2280
+ });
2283
2281
 
2284
2282
  // Wire MCP manager callbacks to session for reactive tool updates.
2285
2283
  // Skip when reusing a parent's manager — the parent owns the callbacks.
@@ -128,7 +128,6 @@ import {
128
128
  } from "../eval/py/executor";
129
129
  import { defaultEvalSessionId } from "../eval/session-id";
130
130
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
131
- import { exportSessionToHtml } from "../export/html";
132
131
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
133
132
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
134
133
  import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
@@ -2967,14 +2966,14 @@ export class AgentSession {
2967
2966
  }
2968
2967
 
2969
2968
  #rekeyHindsightMemoryForCurrentSessionId(): void {
2970
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
2969
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2971
2970
  const sid = this.agent.sessionId;
2972
2971
  if (!sid) return;
2973
2972
  this.getHindsightSessionState()?.setSessionId(sid);
2974
2973
  }
2975
2974
 
2976
2975
  #rekeyMnemopiMemoryForCurrentSessionId(): void {
2977
- if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2976
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2978
2977
  const sid = this.agent.sessionId;
2979
2978
  if (!sid) return;
2980
2979
  this.getMnemopiSessionState()?.setSessionId(sid);
@@ -2982,14 +2981,14 @@ export class AgentSession {
2982
2981
 
2983
2982
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
2984
2983
  #resetHindsightConversationTrackingIfHindsight(): void {
2985
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
2984
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2986
2985
  const state = this.getHindsightSessionState();
2987
2986
  if (!state || state.aliasOf) return;
2988
2987
  state.resetConversationTracking();
2989
2988
  }
2990
2989
 
2991
2990
  #resetMnemopiConversationTrackingIfMnemopi(): void {
2992
- if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2991
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2993
2992
  const state = this.getMnemopiSessionState();
2994
2993
  if (!state || state.aliasOf) return;
2995
2994
  state.resetConversationTracking();
@@ -3670,7 +3669,7 @@ export class AgentSession {
3670
3669
  }
3671
3670
 
3672
3671
  async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
3673
- const backend = resolveMemoryBackend(this.settings);
3672
+ const backend = await resolveMemoryBackend(this.settings);
3674
3673
  if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
3675
3674
 
3676
3675
  try {
@@ -6096,7 +6095,7 @@ export class AgentSession {
6096
6095
  messagesToSummarize: AgentMessage[];
6097
6096
  turnPrefixMessages: AgentMessage[];
6098
6097
  }): Promise<string | undefined> {
6099
- const backend = resolveMemoryBackend(this.settings);
6098
+ const backend = await resolveMemoryBackend(this.settings);
6100
6099
  if (!backend.preCompactionContext) return undefined;
6101
6100
  const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
6102
6101
  try {
@@ -9588,6 +9587,7 @@ export class AgentSession {
9588
9587
  */
9589
9588
  async exportToHtml(outputPath?: string): Promise<string> {
9590
9589
  const themeName = getCurrentThemeName();
9590
+ const { exportSessionToHtml } = await import("../export/html");
9591
9591
  return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
9592
9592
  }
9593
9593
 
@@ -934,7 +934,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
934
934
  allowArgs: true,
935
935
  handle: async (command, runtime) => {
936
936
  const verb = (command.args.trim().split(/\s+/)[0] ?? "").toLowerCase() || "view";
937
- const backend = resolveMemoryBackend(runtime.settings);
937
+ const backend = await resolveMemoryBackend(runtime.settings);
938
938
  switch (verb) {
939
939
  case "view": {
940
940
  const payload = await backend.buildDeveloperInstructions(
@@ -26,6 +26,8 @@ function formatUsageReportAccount(report: UsageReport, limit: UsageLimit, index:
26
26
  if (typeof email === "string" && email) return email;
27
27
  const accountId = report.metadata?.accountId ?? limit.scope.accountId;
28
28
  if (typeof accountId === "string" && accountId) return accountId;
29
+ const projectId = report.metadata?.projectId ?? limit.scope.projectId;
30
+ if (typeof projectId === "string" && projectId) return projectId;
29
31
  return `account ${index + 1}`;
30
32
  }
31
33