@oh-my-pi/pi-coding-agent 15.11.6 → 15.11.7

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 (56) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/cli.js +114 -71
  3. package/dist/types/cli/bench-cli.d.ts +78 -0
  4. package/dist/types/commands/bench.d.ts +29 -0
  5. package/dist/types/config/model-resolver.d.ts +3 -2
  6. package/dist/types/config/settings-schema.d.ts +72 -0
  7. package/dist/types/edit/renderer.d.ts +1 -0
  8. package/dist/types/modes/components/oauth-selector.d.ts +10 -1
  9. package/dist/types/modes/components/settings-selector.d.ts +8 -1
  10. package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
  11. package/dist/types/modes/components/tool-execution.d.ts +13 -9
  12. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
  13. package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
  14. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
  15. package/dist/types/session/snapcompact-inline.d.ts +2 -0
  16. package/dist/types/tools/bash.d.ts +2 -0
  17. package/dist/types/tools/eval-render.d.ts +1 -0
  18. package/dist/types/tools/renderers.d.ts +13 -0
  19. package/dist/types/tools/ssh.d.ts +1 -0
  20. package/package.json +11 -11
  21. package/src/cli/bench-cli.ts +437 -0
  22. package/src/cli-commands.ts +1 -0
  23. package/src/commands/bench.ts +42 -0
  24. package/src/config/model-registry.ts +52 -5
  25. package/src/config/model-resolver.ts +36 -5
  26. package/src/config/settings-schema.ts +92 -0
  27. package/src/edit/renderer.ts +5 -0
  28. package/src/hindsight/client.ts +26 -1
  29. package/src/hindsight/state.ts +6 -2
  30. package/src/internal-urls/docs-index.generated.ts +1 -1
  31. package/src/mcp/transports/stdio.ts +81 -7
  32. package/src/modes/components/oauth-selector.ts +67 -7
  33. package/src/modes/components/settings-selector.ts +27 -0
  34. package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
  35. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  36. package/src/modes/components/tool-execution.ts +18 -10
  37. package/src/modes/controllers/input-controller.ts +8 -6
  38. package/src/modes/controllers/selector-controller.ts +4 -2
  39. package/src/modes/interactive-mode.ts +24 -0
  40. package/src/modes/setup-wizard/index.ts +1 -0
  41. package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
  42. package/src/modes/setup-wizard/scenes/providers.ts +36 -2
  43. package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
  44. package/src/modes/setup-wizard/scenes/theme.ts +28 -1
  45. package/src/modes/setup-wizard/scenes/types.ts +10 -1
  46. package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
  47. package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
  48. package/src/modes/utils/context-usage.ts +1 -1
  49. package/src/prompts/bench.md +7 -0
  50. package/src/sdk.ts +1 -0
  51. package/src/session/agent-session.ts +5 -0
  52. package/src/session/snapcompact-inline.ts +11 -19
  53. package/src/tools/bash.ts +3 -0
  54. package/src/tools/eval-render.ts +4 -0
  55. package/src/tools/renderers.ts +13 -0
  56. package/src/tools/ssh.ts +3 -0
@@ -1,7 +1,7 @@
1
1
  import type { AuthStorage } from "@oh-my-pi/pi-ai";
2
2
  import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
3
3
  import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
4
- import { Input, matchesKey, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
4
+ import { Input, matchesKey, type SgrMouseEvent, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
5
5
  import { getAgentDbPath } from "@oh-my-pi/pi-utils";
6
6
  import { OAuthSelectorComponent } from "../../components/oauth-selector";
7
7
  import { theme } from "../../theme/theme";
@@ -35,6 +35,8 @@ export class SignInTab implements SetupTab {
35
35
  #loginAbort: AbortController | undefined;
36
36
  #loggingInProvider: string | undefined;
37
37
  #disposed = false;
38
+ /** Render line where the provider selector begins. */
39
+ #selectorRowStart = 2;
38
40
 
39
41
  constructor(private readonly host: SetupSceneHost) {
40
42
  this.#authStorage = host.ctx.session.modelRegistry.authStorage;
@@ -68,12 +70,19 @@ export class SignInTab implements SetupTab {
68
70
  this.#selector.handleInput(data);
69
71
  }
70
72
 
73
+ /** Forward mouse to the provider selector; pointer is inert during an active login or code prompt. */
74
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
75
+ if (this.#loggingInProvider || this.#prompt) return;
76
+ this.#selector.routeMouse(event, line - this.#selectorRowStart, col);
77
+ }
78
+
71
79
  render(width: number): readonly string[] {
72
80
  const lines: string[] = [];
73
81
  if (this.#loggingInProvider) {
74
82
  lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`));
75
83
  } else {
76
84
  lines.push(theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), "");
85
+ this.#selectorRowStart = lines.length;
77
86
  lines.push(...this.#selector.render(width));
78
87
  }
79
88
 
@@ -1,4 +1,11 @@
1
- import { padding, type SelectItem, SelectList, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import {
2
+ padding,
3
+ type SelectItem,
4
+ SelectList,
5
+ type SgrMouseEvent,
6
+ truncateToWidth,
7
+ visibleWidth,
8
+ } from "@oh-my-pi/pi-tui";
2
9
  import {
3
10
  enableAutoTheme,
4
11
  getAvailableThemes,
@@ -89,6 +96,8 @@ class ThemeSceneController implements SetupSceneController {
89
96
  #message: string | undefined;
90
97
  #previewRequest = 0;
91
98
  #disposed = false;
99
+ /** Render line where the select list began, or -1 while it is not shown. */
100
+ #listRowStart = -1;
92
101
  readonly #originalTheme = getCurrentThemeName();
93
102
  readonly #originalSymbolPreset: SymbolPreset;
94
103
  readonly #originalColorBlindMode: boolean;
@@ -117,6 +126,22 @@ class ThemeSceneController implements SetupSceneController {
117
126
  this.#selectList.handleInput(data);
118
127
  }
119
128
 
129
+ /** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
130
+ routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
131
+ if (event.wheel !== null) {
132
+ this.#selectList.handleWheel(event.wheel);
133
+ return;
134
+ }
135
+ const index = this.#listRowStart >= 0 ? this.#selectList.hitTest(line - this.#listRowStart) : undefined;
136
+ if (event.motion) {
137
+ this.#selectList.setHoverIndex(index ?? null);
138
+ return;
139
+ }
140
+ if (event.leftClick && index !== undefined) {
141
+ this.#selectList.clickItem(index);
142
+ }
143
+ }
144
+
120
145
  render(width: number): readonly string[] {
121
146
  const lines = [
122
147
  theme.fg("muted", "Theme changes preview live. Nothing is saved until you press Enter."),
@@ -128,8 +153,10 @@ class ThemeSceneController implements SetupSceneController {
128
153
  "",
129
154
  ];
130
155
  if (this.#loadingAllThemes) {
156
+ this.#listRowStart = -1;
131
157
  lines.push(theme.fg("dim", "Loading themes…"));
132
158
  } else {
159
+ this.#listRowStart = lines.length;
133
160
  lines.push(...this.#selectList.render(width));
134
161
  }
135
162
  if (this.#message) {
@@ -1,4 +1,4 @@
1
- import type { Component } from "@oh-my-pi/pi-tui";
1
+ import type { Component, SgrMouseEvent } from "@oh-my-pi/pi-tui";
2
2
  import type { InteractiveModeContext } from "../../types";
3
3
 
4
4
  export type SetupSceneResult = "done" | "skipped";
@@ -17,6 +17,13 @@ export interface SetupSceneController extends Component {
17
17
  onMount?(): void | Promise<void>;
18
18
  onUnmount?(): void;
19
19
  dispose?(): void;
20
+ /**
21
+ * Route an SGR mouse report (tracking is on while the wizard holds the
22
+ * alternate screen). `line`/`col` are 0-based within this controller's
23
+ * last rendered output. When absent, the wizard falls back to synthesizing
24
+ * arrow keys from wheel notches.
25
+ */
26
+ routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
20
27
  }
21
28
 
22
29
  /**
@@ -36,6 +43,8 @@ export interface SetupTab {
36
43
  invalidate(): void;
37
44
  /** Called when the tab becomes active (including initial mount). */
38
45
  onActivate?(): void;
46
+ /** Mouse routing at tab-local coordinates; see {@link SetupSceneController.routeMouse}. */
47
+ routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
39
48
  dispose(): void;
40
49
  }
41
50
 
@@ -1,4 +1,4 @@
1
- import { type SelectItem, SelectList, truncateToWidth } from "@oh-my-pi/pi-tui";
1
+ import { type SelectItem, SelectList, type SgrMouseEvent, truncateToWidth } from "@oh-my-pi/pi-tui";
2
2
  import { SETTINGS_SCHEMA } from "../../../config/settings-schema";
3
3
  import { getSearchProvider, setPreferredSearchProvider } from "../../../web/search/provider";
4
4
  import { isSearchProviderPreference, type SearchProviderId } from "../../../web/search/types";
@@ -31,6 +31,8 @@ export class WebSearchTab implements SetupTab {
31
31
  #availability = new Map<SearchProviderId, Availability>();
32
32
  #status: string[] = [];
33
33
  #disposed = false;
34
+ /** Render line where the select list begins. */
35
+ #listRowStart = 0;
34
36
 
35
37
  constructor(private readonly host: SetupSceneHost) {
36
38
  this.#list = new SelectList(WEB_SEARCH_ITEMS, MAX_VISIBLE, getSelectListTheme());
@@ -55,6 +57,22 @@ export class WebSearchTab implements SetupTab {
55
57
  this.#list.handleInput(data);
56
58
  }
57
59
 
60
+ /** Wheel moves the highlight; hover lights the row under the pointer; click confirms it. */
61
+ routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
62
+ if (event.wheel !== null) {
63
+ this.#list.handleWheel(event.wheel);
64
+ return;
65
+ }
66
+ const index = this.#list.hitTest(line - this.#listRowStart);
67
+ if (event.motion) {
68
+ this.#list.setHoverIndex(index ?? null);
69
+ return;
70
+ }
71
+ if (event.leftClick && index !== undefined) {
72
+ this.#list.clickItem(index);
73
+ }
74
+ }
75
+
58
76
  invalidate(): void {
59
77
  this.#list.invalidate();
60
78
  }
@@ -64,11 +82,9 @@ export class WebSearchTab implements SetupTab {
64
82
  }
65
83
 
66
84
  render(width: number): readonly string[] {
67
- const lines = [
68
- theme.fg("muted", "Choose the provider the web_search tool should prefer."),
69
- "",
70
- ...this.#list.render(width),
71
- ];
85
+ const lines = [theme.fg("muted", "Choose the provider the web_search tool should prefer."), ""];
86
+ this.#listRowStart = lines.length;
87
+ lines.push(...this.#list.render(width));
72
88
  const selected = this.#list.getSelectedItem();
73
89
  if (selected) {
74
90
  lines.push("", ...this.#readinessLines(selected.value).map(line => truncateToWidth(line, width)));
@@ -1,4 +1,4 @@
1
- import { type Component, matchesKey, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { type Component, matchesKey, padding, parseSgrMouse, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import { APP_NAME } from "@oh-my-pi/pi-utils";
3
3
  import { gradientLogo, PI_LOGO } from "../components/welcome";
4
4
  import { theme } from "../theme/theme";
@@ -61,6 +61,8 @@ export class SetupWizardComponent implements Component {
61
61
  #timer: NodeJS.Timeout | undefined;
62
62
  #done = Promise.withResolvers<void>();
63
63
  #disposed = false;
64
+ /** Screen row where the active scene's body began in the last rendered frame. */
65
+ #bodyRowStart = 0;
64
66
 
65
67
  constructor(
66
68
  readonly ctx: InteractiveModeContext,
@@ -87,6 +89,10 @@ export class SetupWizardComponent implements Component {
87
89
 
88
90
  handleInput(data: string): void {
89
91
  if (this.#phase === "done") return;
92
+ if (data.startsWith("\x1b[<")) {
93
+ this.#handleMouse(data);
94
+ return;
95
+ }
90
96
  if (matchesKey(data, "ctrl+c")) {
91
97
  this.#beginOutro();
92
98
  return;
@@ -116,6 +122,36 @@ export class SetupWizardComponent implements Component {
116
122
  this.#activeScene?.handleInput?.(data);
117
123
  }
118
124
 
125
+ /**
126
+ * Mouse handling for the fullscreen wizard (SGR tracking is on while the
127
+ * overlay holds the alternate screen). The frame paints from screen row 0,
128
+ * so report coordinates index directly into the last rendered lines: scene
129
+ * body rows start at #bodyRowStart, indented by SCENE_MARGIN_X. Scenes
130
+ * that implement routeMouse get hit-tested events (wheel, hover, click);
131
+ * for the rest a wheel notch falls back to an arrow key. A left click
132
+ * advances the splash/outro like Enter. Raw reports never reach scene
133
+ * keyboard input.
134
+ */
135
+ #handleMouse(data: string): void {
136
+ const event = parseSgrMouse(data);
137
+ if (!event) return;
138
+ if (this.#phase === "splash" || this.#phase === "outro") {
139
+ if (!event.leftClick) return;
140
+ if (this.#phase === "splash") this.#beginScene();
141
+ else this.#complete();
142
+ return;
143
+ }
144
+ const scene = this.#activeScene;
145
+ if (!scene) return;
146
+ if (scene.routeMouse) {
147
+ scene.routeMouse(event, event.row - this.#bodyRowStart, event.col - SCENE_MARGIN_X);
148
+ return;
149
+ }
150
+ if (event.wheel !== null) {
151
+ scene.handleInput?.(event.wheel === -1 ? "\x1b[A" : "\x1b[B");
152
+ }
153
+ }
154
+
119
155
  render(width: number): readonly string[] {
120
156
  const safeWidth = Math.max(1, width);
121
157
  const height = Math.max(1, this.ctx.ui.terminal.rows);
@@ -163,6 +199,7 @@ export class SetupWizardComponent implements Component {
163
199
  header.push(indentLine(theme.fg("muted", subtitle), width, SCENE_MARGIN_X));
164
200
  }
165
201
  header.push("");
202
+ this.#bodyRowStart = header.length;
166
203
 
167
204
  const footer = [
168
205
  "",
@@ -183,7 +183,7 @@ export function computeContextBreakdown(
183
183
  const renderToolResults = session.settings.get("snapcompact.toolResults");
184
184
  if (renderSystemPrompt !== "none" || renderToolResults) {
185
185
  snapcompactSavings = estimateInlineSavings({
186
- options: { renderSystemPrompt, renderToolResults },
186
+ options: { renderSystemPrompt, renderToolResults, shape: session.settings.get("snapcompact.shape") },
187
187
  model,
188
188
  systemPrompt: session.systemPrompt ?? [],
189
189
  messages: session.messages ?? [],
@@ -0,0 +1,7 @@
1
+ Write a continuous, plain-prose technical explanation of how a relational database executes a SQL query: lexing and parsing, semantic analysis, logical plan construction, cost-based optimization, physical operator selection, and row-by-row execution through the iterator model.
2
+
3
+ Form:
4
+ - Plain paragraphs only: no headings, no lists, no code fences, no preamble.
5
+ - Do not wrap up early or summarize; keep writing until you are cut off.
6
+
7
+ Output only the explanation.
package/src/sdk.ts CHANGED
@@ -2168,6 +2168,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2168
2168
  ? new SnapcompactInlineTransformer({
2169
2169
  renderSystemPrompt: snapcompactSystemPromptMode,
2170
2170
  renderToolResults: settings.get("snapcompact.toolResults"),
2171
+ shape: settings.get("snapcompact.shape"),
2171
2172
  })
2172
2173
  : undefined;
2173
2174
  const transformProviderContext =
@@ -6388,6 +6388,10 @@ export class AgentSession {
6388
6388
  const snapcompactResult = await snapcompact.compact(preparation, {
6389
6389
  convertToLlm,
6390
6390
  model: this.model,
6391
+ shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
6392
+ // Providers with hard image caps (OpenRouter: 8) silently drop
6393
+ // frames past the cap — keep the archive within budget.
6394
+ maxFrames: snapcompact.providerFrameBudget(this.model.provider),
6391
6395
  });
6392
6396
  summary = snapcompactResult.summary;
6393
6397
  shortSummary = snapcompactResult.shortSummary;
@@ -7921,6 +7925,7 @@ export class AgentSession {
7921
7925
  const snapcompactResult = await snapcompact.compact(preparation, {
7922
7926
  convertToLlm,
7923
7927
  model: this.model,
7928
+ maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
7924
7929
  });
7925
7930
  summary = snapcompactResult.summary;
7926
7931
  shortSummary = snapcompactResult.shortSummary;
@@ -28,22 +28,15 @@ export type SnapcompactSystemPromptMode = "none" | "agents-md" | "all";
28
28
  export interface SnapcompactInlineOptions {
29
29
  renderSystemPrompt: SnapcompactSystemPromptMode;
30
30
  renderToolResults: boolean;
31
+ /** Frame variant override; `"auto"`/omitted picks the provider's eval winner. */
32
+ shape?: snapcompact.ShapeVariantName | "auto";
31
33
  }
32
34
 
33
- /**
34
- * Image-count budget per provider. Snapcompact frames are 1568px (<2000px) so
35
- * dimension/size limits never bind; only COUNT does. Strictest mainstream is
36
- * Groq (~5), so unknown providers get the safe floor.
37
- */
38
- const INLINE_IMAGE_BUDGET_BY_PROVIDER: Record<string, number> = {
39
- anthropic: 90,
40
- "amazon-bedrock": 90,
41
- openai: 200,
42
- google: 200,
43
- "google-vertex": 200,
44
- "google-gemini-cli": 200,
45
- };
46
- const DEFAULT_INLINE_IMAGE_BUDGET = 5;
35
+ // Per-provider image-count budgets live in @oh-my-pi/snapcompact
36
+ // (`providerImageBudget`): snapcompact frames are 1568px (<2000px) so
37
+ // dimension/size limits never bind; only COUNT does. Once the budget is
38
+ // spent (e.g. OpenRouter's hard 8-image cap, already consumed by archive
39
+ // frames), tool results ship verbatim as text.
47
40
  const MAX_SYSTEM_PROMPT_FRAMES = 6;
48
41
  /** Tool results under this many tokens are never rasterized — the swap can't
49
42
  * save enough to justify trading crisp text for an image. */
@@ -273,7 +266,7 @@ export function estimateInlineSavings(input: {
273
266
  return { visionCapable: false, savedTokens: 0 };
274
267
  }
275
268
 
276
- const shape = snapcompact.resolveShape(model.api);
269
+ const shape = snapcompact.resolveShape(model, options.shape);
277
270
  let existingImages = 0;
278
271
  for (const message of input.messages) {
279
272
  if (!Array.isArray(message.content)) continue;
@@ -281,7 +274,7 @@ export function estimateInlineSavings(input: {
281
274
  if (block.type === "image") existingImages++;
282
275
  }
283
276
  }
284
- const budget = (INLINE_IMAGE_BUDGET_BY_PROVIDER[model.provider] ?? DEFAULT_INLINE_IMAGE_BUDGET) - existingImages;
277
+ const budget = snapcompact.providerImageBudget(model.provider) - existingImages;
285
278
 
286
279
  const candidates: InlineToolResultCandidate[] = [];
287
280
  if (options.renderToolResults) {
@@ -407,9 +400,8 @@ export class SnapcompactInlineTransformer {
407
400
  // rendering would lose the content entirely.
408
401
  if (!model.input.includes("image")) return context;
409
402
 
410
- const shape = snapcompact.resolveShape(model.api);
411
- const budget =
412
- (INLINE_IMAGE_BUDGET_BY_PROVIDER[model.provider] ?? DEFAULT_INLINE_IMAGE_BUDGET) - countContextImages(context);
403
+ const shape = snapcompact.resolveShape(model, this.options.shape);
404
+ const budget = snapcompact.providerImageBudget(model.provider) - countContextImages(context);
413
405
  if (budget <= 0) return context;
414
406
 
415
407
  const messages = [...context.messages];
package/src/tools/bash.ts CHANGED
@@ -1385,6 +1385,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1385
1385
  },
1386
1386
  mergeCallAndResult: true,
1387
1387
  inline: true,
1388
+ // Pending preview caps the command to a viewport-sized tail window that
1389
+ // shifts while args stream; keep it out of native scrollback mid-run.
1390
+ provisionalPendingPreview: true,
1388
1391
  };
1389
1392
  }
1390
1393
 
@@ -754,4 +754,8 @@ export const evalToolRenderer = {
754
754
 
755
755
  mergeCallAndResult: true,
756
756
  inline: true,
757
+ // Pending preview shows tail-window code cells; the result render
758
+ // interleaves each cell's output under its code, re-laying-out every row
759
+ // below the first cell. Keep the preview out of native scrollback mid-run.
760
+ provisionalPendingPreview: true,
757
761
  };
@@ -43,6 +43,19 @@ export type ToolRenderer = {
43
43
  mergeCallAndResult?: boolean;
44
44
  /** Render without background box, inline in the response flow */
45
45
  inline?: boolean;
46
+ /**
47
+ * Collapsed pending preview is provisional — a tail-window or otherwise
48
+ * re-anchored view the result render replaces wholesale (an edit's
49
+ * streamed-diff tail, bash/ssh command caps, eval cells whose outputs
50
+ * interleave under each cell). Its rows must never commit to native
51
+ * scrollback mid-run; see
52
+ * `ToolExecutionComponent.isTranscriptBlockCommitStable`. Absent = the
53
+ * pending preview streams top-anchored append-shaped rows the result
54
+ * render preserves (task context/assignment, write content), which stay
55
+ * commit-eligible so a call taller than the viewport scrolls into history
56
+ * instead of reading as cut off.
57
+ */
58
+ provisionalPendingPreview?: boolean;
46
59
  };
47
60
 
48
61
  export const toolRenderers: Record<string, ToolRenderer> = {
package/src/tools/ssh.ts CHANGED
@@ -346,4 +346,7 @@ export const sshToolRenderer = {
346
346
  });
347
347
  },
348
348
  mergeCallAndResult: true,
349
+ // Pending preview caps the command to a viewport-sized tail window that
350
+ // shifts while args stream; keep it out of native scrollback mid-run.
351
+ provisionalPendingPreview: true,
349
352
  };