@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.10.10] - 2026-06-09
6
+
7
+ ### Added
8
+
9
+ - Added a read-only `view` op to the `todo` tool that echoes the current list without mutating state, so the agent can recover exact task text instead of guessing it from memory.
10
+
11
+ ### Changed
12
+
13
+ - Rewrote the bash tool's coreutils guidance (tool prompt and system prompt) around an explicit litmus: pipelines that compute a new fact (`wc -l`, `sort | uniq -c`, `comm`, `diff`) are legitimate bash, while commands that merely move, page, or trim bytes a dedicated tool can fetch remain banned — output trimming destroys data the `artifact://` capture would have saved.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed the model selector dropping an immediate Enter when cached models were available but the selector's offline refresh was still pending.
18
+ - Fixed dynamic `import(...)` inside functions passed to the browser tool's `tab.evaluate`/`page.evaluate` failing with `__omp_import__ is not defined`. The eval/browser JS runtime rewrites dynamic-import callees to the worker-injected `__omp_import__` helper, but puppeteer serializes evaluate callbacks with `Function.prototype.toString()` and re-runs them inside the page, where the helper does not exist. The rewriter now substitutes a guarded shim that falls back to native dynamic import when the helper is absent, so serialized code works in the page realm while in-worker imports keep resolving against the session cwd.
19
+ - Transcript block freezing is now unconditional instead of gated on ED3-risk terminal detection: every finalized block replays its frozen snapshot once it crosses out of the live region, on all terminals including Windows, because the rewritten renderer's committed scrollback is immutable everywhere. Still-mutating blocks (pending tools, streaming messages, async thinking renderers) anchor the live region and keep repainting until they finalize, which structurally fixes stale/duplicated output from late async expansions ([#1823](https://github.com/can1357/oh-my-pi/issues/1823)).
20
+ - Fixed the edit tool's post-edit diff preview occasionally echoing a context line twice with out-of-order numbering. Block-boundary context injection classified space-prefixed diff rows as old-file-only, so an unchanged line sitting in a net-offset region (old N / new N+k) was missing from the new file's visibility window; `findBlockContextLines` then re-surfaced it under its post-edit number and the row was spliced in after the adjacent change run. New-file boundary lines are now translated back to pre-edit numbers (the compact-preview renumbering contract) and merged into a single old-numbered insertion pass — also fixing closers below a net-offset edit being dropped or renumbered incorrectly.
21
+ - Fixed the Anthropic web-search provider claiming the Claude Code identity on API-key requests: the CC billing header + system instruction were injected whenever the model wasn't Haiku 3.5, regardless of auth mode. Injection is now OAuth-gated like the streaming path, and OAuth search requests patch the billing header's `cch` attestation (via `wrapFetchForCch`) instead of shipping the `cch=00000` placeholder.
22
+ - Fixed long streamed content appearing cut off mid-run: scrolled-off rows were erased from the viewport without ever being appended to terminal history. The transcript's commit boundary (`deriveLiveCommitState`) was all-or-nothing per block — one perpetually rewriting row (a task tool's ticking progress tree, per-agent cost/tool counters, spinner stats) suspended scrollback commits for the entire block, so once the block outgrew the viewport its static head (e.g. a task's prompt/context markdown) was neither committed nor on screen until the tool sealed, and was lost outright if the session ended mid-run. A stable-prefix ratchet now promotes leading rows that stayed visibly identical for a full 30-frame window as commit-safe, so the settled head reaches native scrollback while only the genuinely volatile tail stays deferred; a rewrite above the promoted run retreats the boundary and the engine audit recommits (duplication, never loss).
23
+ - Fixed local tiny-title worker stdout/stderr leaking raw native model output such as `</title>` and cache/status lines into the interactive TUI scrollback ([#2206](https://github.com/can1357/oh-my-pi/issues/2206)).
24
+ - Fixed task-agent discovery advertising Claude Code custom agents from `.claude/agents/*.md` as OMP subagents; direct task-agent discovery now only loads OMP-native `.omp` agent roots, while Claude marketplace plugin agents keep their existing provider path ([#2209](https://github.com/can1357/oh-my-pi/issues/2209)).
25
+
26
+ ### Removed
27
+
28
+ - Removed the `clearOnShrink` setting and its `PI_CLEAR_ON_SHRINK` environment variable: the rewritten renderer always clears shrunken rows exactly, so the flicker/perf tradeoff the setting controlled no longer exists. Existing config entries are ignored.
29
+ - Removed the prompt-submit native-scrollback reconciliation checkpoint and the eager streaming render mode from the interactive controllers — the renderer's append-only contract made both obsolete.
30
+
5
31
  ## [15.10.9] - 2026-06-09
6
32
 
7
33
  ### Fixed
@@ -262,6 +262,11 @@ export interface CanonicalModelQueryOptions {
262
262
  availableOnly?: boolean;
263
263
  candidates?: readonly Model<Api>[];
264
264
  }
265
+ /** A canonical record (with query-filtered variants) plus the variant model selected for it. */
266
+ export interface CanonicalModelSelection {
267
+ record: CanonicalModelRecord;
268
+ model: Model<Api>;
269
+ }
265
270
  /**
266
271
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
267
272
  */
@@ -306,6 +311,14 @@ export declare class ModelRegistry {
306
311
  */
307
312
  getAll(): Model<Api>[];
308
313
  getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[];
314
+ /**
315
+ * One-pass equivalent of `getCanonicalModels` + `resolveCanonicalModel` per
316
+ * record. The per-query state (candidate-selector set, availability memo,
317
+ * provider rank, candidate order) is built once, so the whole catalog
318
+ * resolves in O(records + candidates) instead of O(records × candidates).
319
+ * This is the path the model selector hydrates from synchronously on open.
320
+ */
321
+ getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[];
309
322
  getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
310
323
  resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
311
324
  getCanonicalId(model: Model<Api>): string | undefined;
@@ -716,15 +716,6 @@ export declare const SETTINGS_SCHEMA: {
716
716
  readonly description: "Show terminal cursor for IME support";
717
717
  };
718
718
  };
719
- readonly clearOnShrink: {
720
- readonly type: "boolean";
721
- readonly default: false;
722
- readonly ui: {
723
- readonly tab: "appearance";
724
- readonly label: "Clear on Shrink";
725
- readonly description: "Clear empty rows when content shrinks (may cause flicker)";
726
- };
727
- };
728
719
  readonly defaultThinkingLevel: {
729
720
  readonly type: "enum";
730
721
  readonly values: readonly [...import("@oh-my-pi/pi-ai").Effort[], "auto"];
@@ -18,7 +18,6 @@ export interface TerminalStateInfo {
18
18
  hyperlinks: boolean;
19
19
  deccara: boolean;
20
20
  screenToScrollback: boolean;
21
- eagerEraseScrollbackRisk: boolean;
22
21
  synchronizedOutput: boolean;
23
22
  multiplexer: string | null;
24
23
  env: {
@@ -1,25 +1,18 @@
1
1
  import { Container, type NativeScrollbackLiveRegion } from "@oh-my-pi/pi-tui";
2
2
  /**
3
- * Transcript container that freezes the rendered output of every block except
4
- * the bottom-most (live) one on terminals where committed native scrollback is
5
- * immutable.
3
+ * Transcript container that always renders every block's current content and
4
+ * reports the live-region seam (`NativeScrollbackLiveRegion`) that gates the
5
+ * engine's append-only scrollback commits.
6
6
  *
7
- * On ED3-risk terminals with an unobservable viewport (ghostty/kitty/iTerm2/…)
8
- * the renderer cannot clear saved lines (`\x1b[3J` may yank a reader) or query
9
- * whether the user has scrolled, so any block that re-lays-out *after* it has
10
- * scrolled past the viewport leaves a stale duplicate above the live region
11
- * (a finalized assistant message re-wrapping, a tool preview collapsing to its
12
- * compact result, a late async tool completion). The renderer's only safe move
13
- * for such an offscreen edit is to not repaint which is correct only if the
14
- * committed region never changes underneath it.
15
- *
16
- * This container provides that guarantee: a block's render is snapshotted while
17
- * it is the live (bottom-most) block, and once a newer block is appended it
18
- * replays the snapshot instead of recomputing. Mutations after a block leaves
19
- * live are intentionally deferred until the next checkpoint {@link thaw} (prompt
20
- * submit → native-scrollback rebuild), where the whole transcript is replayed
21
- * and any drift reconciles safely. On terminals that can rebuild history this
22
- * freezing is unnecessary, so it renders every block live for full fidelity.
7
+ * The engine never rewrites committed history: rows above the seam that have
8
+ * entered the tape keep whatever bytes they were committed with ("let the
9
+ * history be"), while the visible window always repaints from each block's
10
+ * latest render a late tool result, a post-finalize error pin, or an expand
11
+ * toggle is always reflected on screen. Blocks that are still mutating (an
12
+ * unfinalized tool, a streaming assistant message) stay below the seam so
13
+ * their rows do not enter history while they can still change; a streaming
14
+ * block whose render grows append-only deepens the seam through its settled
15
+ * head so a long reply's scrolled-off rows still reach scrollback mid-stream.
23
16
  */
24
17
  export declare class TranscriptContainer extends Container implements NativeScrollbackLiveRegion {
25
18
  #private;
@@ -27,13 +20,6 @@ export declare class TranscriptContainer extends Container implements NativeScro
27
20
  clear(): void;
28
21
  getNativeScrollbackLiveRegionStart(): number | undefined;
29
22
  getNativeScrollbackCommitSafeEnd(): number | undefined;
30
- /**
31
- * Retire all frozen snapshots so the next render reflects each block's current
32
- * state. Call at reconciliation checkpoints (prompt submit) where the whole
33
- * transcript is replayed into native scrollback and any drift a frozen block
34
- * accumulated is reconciled.
35
- */
36
- thaw(): void;
37
23
  render(width: number): string[];
38
24
  }
39
25
  /**
@@ -7,8 +7,7 @@ export interface DiscoveryResult {
7
7
  /**
8
8
  * Discover agents from filesystem and merge with bundled agents.
9
9
  *
10
- * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
11
- *
10
+ * Precedence (highest wins): project .omp, user .omp, Claude plugin agents, then bundled
12
11
  * @param cwd - Current working directory for project agent discovery
13
12
  */
14
13
  export declare function discoverAgents(cwd: string, home?: string): Promise<DiscoveryResult>;
@@ -32,7 +32,7 @@ export declare const TINY_WORKER_ARG = "--tiny-worker";
32
32
  */
33
33
  export declare function tinyWorkerEnvOverlay(env: Record<string, string | undefined>, deviceSetting: string | undefined, dtypeSetting: string | undefined): Record<string, string>;
34
34
  interface SpawnedSubprocess {
35
- proc: Subprocess<"ignore", "inherit", "inherit">;
35
+ proc: Subprocess<"ignore", "ignore", "ignore">;
36
36
  inbound: Set<(message: TinyTitleWorkerOutbound) => void>;
37
37
  errors: Set<(error: Error) => void>;
38
38
  /**
@@ -40,6 +40,7 @@ declare const todoSchema: z.ZodObject<{
40
40
  note: "note";
41
41
  rm: "rm";
42
42
  start: "start";
43
+ view: "view";
43
44
  }>;
44
45
  list: z.ZodOptional<z.ZodArray<z.ZodObject<{
45
46
  phase: z.ZodString;
@@ -113,6 +114,7 @@ export declare class TodoTool implements AgentTool<typeof todoSchema, TodoToolDe
113
114
  note: "note";
114
115
  rm: "rm";
115
116
  start: "start";
117
+ view: "view";
116
118
  }>;
117
119
  list: z.ZodOptional<z.ZodArray<z.ZodObject<{
118
120
  phase: z.ZodString;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.10.9",
4
+ "version": "15.10.10",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,14 +47,14 @@
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.10.9",
51
- "@oh-my-pi/omp-stats": "15.10.9",
52
- "@oh-my-pi/pi-agent-core": "15.10.9",
53
- "@oh-my-pi/pi-ai": "15.10.9",
54
- "@oh-my-pi/pi-mnemopi": "15.10.9",
55
- "@oh-my-pi/pi-natives": "15.10.9",
56
- "@oh-my-pi/pi-tui": "15.10.9",
57
- "@oh-my-pi/pi-utils": "15.10.9",
50
+ "@oh-my-pi/hashline": "15.10.10",
51
+ "@oh-my-pi/omp-stats": "15.10.10",
52
+ "@oh-my-pi/pi-agent-core": "15.10.10",
53
+ "@oh-my-pi/pi-ai": "15.10.10",
54
+ "@oh-my-pi/pi-mnemopi": "15.10.10",
55
+ "@oh-my-pi/pi-natives": "15.10.10",
56
+ "@oh-my-pi/pi-tui": "15.10.10",
57
+ "@oh-my-pi/pi-utils": "15.10.10",
58
58
  "@opentelemetry/api": "^1.9.1",
59
59
  "@opentelemetry/context-async-hooks": "^2.7.1",
60
60
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -64,22 +64,16 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
64
64
  }
65
65
 
66
66
  const filteredCanonical = modelRegistry
67
- .getCanonicalModels({ availableOnly: true, candidates: filteredModels })
68
- .map(record => {
69
- const selected = modelRegistry.resolveCanonicalModel(record.id, {
70
- availableOnly: true,
71
- candidates: filteredModels,
72
- });
73
- if (!selected) return undefined;
74
- return {
67
+ .getCanonicalModelSelections({ availableOnly: true, candidates: filteredModels })
68
+ .map(
69
+ ({ record, model: selected }): CanonicalRow => ({
75
70
  canonical: record.id,
76
71
  selected: `${selected.provider}/${selected.id}`,
77
72
  variants: String(record.variants.length),
78
73
  context: formatNumber(selected.contextWindow),
79
74
  maxOut: formatNumber(selected.maxTokens),
80
- } satisfies CanonicalRow;
81
- })
82
- .filter((row): row is CanonicalRow => row !== undefined)
75
+ }),
76
+ )
83
77
  .sort((left, right) => left.canonical.localeCompare(right.canonical));
84
78
 
85
79
  if (filteredModels.length === 0 && filteredCanonical.length === 0) {
@@ -428,6 +428,12 @@ export interface CanonicalModelQueryOptions {
428
428
  candidates?: readonly Model<Api>[];
429
429
  }
430
430
 
431
+ /** A canonical record (with query-filtered variants) plus the variant model selected for it. */
432
+ export interface CanonicalModelSelection {
433
+ record: CanonicalModelRecord;
434
+ model: Model<Api>;
435
+ }
436
+
431
437
  /** Result of loading custom models from models.json */
432
438
  interface CustomModelsResult {
433
439
  models?: CustomModelOverlay[];
@@ -2217,48 +2223,81 @@ export class ModelRegistry {
2217
2223
  return this.#models;
2218
2224
  }
2219
2225
 
2220
- #isModelAvailable(model: Model<Api>): boolean {
2226
+ /**
2227
+ * Availability predicate with per-provider memoization. Auth lookups
2228
+ * (`authStorage.hasAuth`) and the disabled-provider set are resolved once
2229
+ * per provider instead of once per model, which matters when filtering the
2230
+ * full bundled catalog (thousands of models, ~50 providers).
2231
+ */
2232
+ #createAvailabilityCheck(): (model: Model<Api>) => boolean {
2221
2233
  const disabledProviders = getDisabledProviderIdsFromSettings();
2222
- return (
2223
- !disabledProviders.has(model.provider) &&
2224
- (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
2225
- );
2234
+ const byProvider = new Map<string, boolean>();
2235
+ return model => {
2236
+ let available = byProvider.get(model.provider);
2237
+ if (available === undefined) {
2238
+ available =
2239
+ !disabledProviders.has(model.provider) &&
2240
+ (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider));
2241
+ byProvider.set(model.provider, available);
2242
+ }
2243
+ return available;
2244
+ };
2245
+ }
2246
+
2247
+ /**
2248
+ * Build the shared per-query filter state for canonical model queries.
2249
+ * Hoisted out of the per-record loop: building the candidate-selector set
2250
+ * and availability memo once per query instead of once per record is what
2251
+ * keeps `getCanonicalModelSelections` linear instead of O(records × candidates).
2252
+ */
2253
+ #canonicalQueryFilters(options: CanonicalModelQueryOptions | undefined): {
2254
+ candidateKeys: Set<string> | undefined;
2255
+ isAvailable: ((model: Model<Api>) => boolean) | undefined;
2256
+ } {
2257
+ return {
2258
+ candidateKeys: options?.candidates
2259
+ ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
2260
+ : undefined,
2261
+ isAvailable: options?.availableOnly ? this.#createAvailabilityCheck() : undefined,
2262
+ };
2226
2263
  }
2227
2264
 
2228
2265
  #filterCanonicalVariants(
2229
2266
  record: CanonicalModelRecord,
2230
- options: CanonicalModelQueryOptions | undefined,
2267
+ candidateKeys: ReadonlySet<string> | undefined,
2268
+ isAvailable: ((model: Model<Api>) => boolean) | undefined,
2231
2269
  ): CanonicalModelVariant[] {
2232
- const candidateKeys = options?.candidates
2233
- ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
2234
- : undefined;
2235
2270
  return record.variants.filter(variant => {
2236
2271
  if (candidateKeys && !candidateKeys.has(variant.selector)) {
2237
2272
  return false;
2238
2273
  }
2239
- if (options?.availableOnly && !this.#isModelAvailable(variant.model)) {
2274
+ if (isAvailable && !isAvailable(variant.model)) {
2240
2275
  return false;
2241
2276
  }
2242
2277
  return true;
2243
2278
  });
2244
2279
  }
2245
2280
 
2281
+ #buildModelOrder(candidates: readonly Model<Api>[]): Map<string, number> {
2282
+ const modelOrder = new Map<string, number>();
2283
+ for (let index = 0; index < candidates.length; index += 1) {
2284
+ modelOrder.set(formatCanonicalVariantSelector(candidates[index]!), index);
2285
+ }
2286
+ return modelOrder;
2287
+ }
2288
+
2246
2289
  #providerRank(): Map<string, number> {
2247
2290
  return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
2248
2291
  }
2249
2292
 
2250
2293
  #resolveCanonicalVariant(
2251
2294
  variants: readonly CanonicalModelVariant[],
2252
- allCandidates: readonly Model<Api>[],
2295
+ modelOrder: ReadonlyMap<string, number>,
2296
+ providerRank: ReadonlyMap<string, number>,
2253
2297
  ): CanonicalModelVariant | undefined {
2254
2298
  if (variants.length === 0) {
2255
2299
  return undefined;
2256
2300
  }
2257
- const providerRank = this.#providerRank();
2258
- const modelOrder = new Map<string, number>();
2259
- for (let index = 0; index < allCandidates.length; index += 1) {
2260
- modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
2261
- }
2262
2301
  const sourceRank: Record<CanonicalModelVariant["source"], number> = {
2263
2302
  override: 1,
2264
2303
  bundled: 1,
@@ -2289,9 +2328,10 @@ export class ModelRegistry {
2289
2328
  }
2290
2329
 
2291
2330
  getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
2331
+ const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
2292
2332
  const records: CanonicalModelRecord[] = [];
2293
2333
  for (const record of this.#canonicalIndex.records) {
2294
- const variants = this.#filterCanonicalVariants(record, options);
2334
+ const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
2295
2335
  if (variants.length === 0) {
2296
2336
  continue;
2297
2337
  }
@@ -2304,12 +2344,43 @@ export class ModelRegistry {
2304
2344
  return records;
2305
2345
  }
2306
2346
 
2347
+ /**
2348
+ * One-pass equivalent of `getCanonicalModels` + `resolveCanonicalModel` per
2349
+ * record. The per-query state (candidate-selector set, availability memo,
2350
+ * provider rank, candidate order) is built once, so the whole catalog
2351
+ * resolves in O(records + candidates) instead of O(records × candidates).
2352
+ * This is the path the model selector hydrates from synchronously on open.
2353
+ */
2354
+ getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
2355
+ const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
2356
+ const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
2357
+ const modelOrder = this.#buildModelOrder(candidates);
2358
+ const providerRank = this.#providerRank();
2359
+ const selections: CanonicalModelSelection[] = [];
2360
+ for (const record of this.#canonicalIndex.records) {
2361
+ const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
2362
+ if (variants.length === 0) {
2363
+ continue;
2364
+ }
2365
+ const resolved = this.#resolveCanonicalVariant(variants, modelOrder, providerRank);
2366
+ if (!resolved) {
2367
+ continue;
2368
+ }
2369
+ selections.push({
2370
+ record: { id: record.id, name: record.name, variants },
2371
+ model: resolved.model,
2372
+ });
2373
+ }
2374
+ return selections;
2375
+ }
2376
+
2307
2377
  getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
2308
2378
  const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
2309
2379
  if (!record) {
2310
2380
  return [];
2311
2381
  }
2312
- return this.#filterCanonicalVariants(record, options);
2382
+ const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
2383
+ return this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
2313
2384
  }
2314
2385
 
2315
2386
  resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
@@ -2318,7 +2389,7 @@ export class ModelRegistry {
2318
2389
  return undefined;
2319
2390
  }
2320
2391
  const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
2321
- return this.#resolveCanonicalVariant(variants, candidates)?.model;
2392
+ return this.#resolveCanonicalVariant(variants, this.#buildModelOrder(candidates), this.#providerRank())?.model;
2322
2393
  }
2323
2394
 
2324
2395
  getCanonicalId(model: Model<Api>): string | undefined {
@@ -2330,7 +2401,7 @@ export class ModelRegistry {
2330
2401
  * This is a fast check that doesn't refresh OAuth tokens.
2331
2402
  */
2332
2403
  getAvailable(): Model<Api>[] {
2333
- return this.#models.filter(model => this.#isModelAvailable(model));
2404
+ return this.#models.filter(this.#createAvailabilityCheck());
2334
2405
  }
2335
2406
 
2336
2407
  /**
@@ -686,16 +686,6 @@ export const SETTINGS_SCHEMA = {
686
686
  ui: { tab: "appearance", label: "Show Hardware Cursor", description: "Show terminal cursor for IME support" },
687
687
  },
688
688
 
689
- clearOnShrink: {
690
- type: "boolean",
691
- default: false,
692
- ui: {
693
- tab: "appearance",
694
- label: "Clear on Shrink",
695
- description: "Clear empty rows when content shrinks (may cause flicker)",
696
- },
697
- },
698
-
699
689
  // ────────────────────────────────────────────────────────────────────────
700
690
  // Model
701
691
  // ────────────────────────────────────────────────────────────────────────
@@ -36,7 +36,6 @@ export interface TerminalStateInfo {
36
36
  hyperlinks: boolean;
37
37
  deccara: boolean;
38
38
  screenToScrollback: boolean;
39
- eagerEraseScrollbackRisk: boolean;
40
39
  synchronizedOutput: boolean;
41
40
  multiplexer: string | null;
42
41
  env: { TERM?: string; TERM_PROGRAM?: string; TERM_PROGRAM_VERSION?: string; COLORTERM?: string };
@@ -82,7 +81,6 @@ export function collectTerminalState(runtime: TerminalRuntimeState): TerminalSta
82
81
  hyperlinks: TERMINAL.hyperlinks,
83
82
  deccara: TERMINAL.deccara,
84
83
  screenToScrollback: TERMINAL.supportsScreenToScrollback,
85
- eagerEraseScrollbackRisk: TERMINAL.eagerEraseScrollbackRisk,
86
84
  synchronizedOutput: runtime.synchronizedOutput,
87
85
  multiplexer: detectMultiplexer(env),
88
86
  env: {
@@ -115,7 +113,6 @@ export function formatTerminalState(info: TerminalStateInfo): string {
115
113
  "",
116
114
  "Scrollback",
117
115
  ` Screen->history clear: ${info.screenToScrollback ? "CSI 22 J" : "CSI 2 J (redraw)"}`,
118
- ` Eager-erase risk: ${yesNo(info.eagerEraseScrollbackRisk)} (ED3 may yank scrolled readers)`,
119
116
  "",
120
117
  "Detection signals",
121
118
  ` TERM: ${info.env.TERM ?? "(unset)"}`,
package/src/edit/diff.ts CHANGED
@@ -55,13 +55,10 @@ function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, conten
55
55
  return `${prefix}${lineNum}|${content}`;
56
56
  }
57
57
 
58
- type DiffSource = "old" | "new";
59
-
60
58
  interface ParsedNumberedDiffRow {
61
59
  prefix: "+" | "-" | " ";
62
60
  lineNumber: number;
63
61
  content: string;
64
- source: DiffSource;
65
62
  }
66
63
 
67
64
  function parseNumberedDiffRow(row: string): ParsedNumberedDiffRow | undefined {
@@ -70,12 +67,7 @@ function parseNumberedDiffRow(row: string): ParsedNumberedDiffRow | undefined {
70
67
  const prefix = match[1] as "+" | "-" | " ";
71
68
  const lineNumber = Number.parseInt(match[2], 10);
72
69
  if (!Number.isFinite(lineNumber)) return undefined;
73
- return {
74
- prefix,
75
- lineNumber,
76
- content: match[3] ?? "",
77
- source: prefix === "+" ? "new" : "old",
78
- };
70
+ return { prefix, lineNumber, content: match[3] ?? "" };
79
71
  }
80
72
 
81
73
  function isDiffChangeRow(row: string | undefined): boolean {
@@ -92,7 +84,6 @@ function adjustedContextInsertIndex(rows: readonly string[], index: number): num
92
84
 
93
85
  function insertBracketContextRows(
94
86
  rows: string[],
95
- source: DiffSource,
96
87
  contextLines: ReadonlyMap<number, string>,
97
88
  seenRows: Set<string>,
98
89
  ): void {
@@ -106,7 +97,7 @@ function insertBracketContextRows(
106
97
  let nextSourceLine: number | undefined;
107
98
  for (let i = 0; i < rows.length; i++) {
108
99
  const parsed = parseNumberedDiffRow(rows[i]);
109
- if (!parsed || parsed.source !== source) continue;
100
+ if (!parsed || parsed.prefix === "+") continue;
110
101
  if (parsed.lineNumber < lineNumber) {
111
102
  previousSourceLine = parsed.lineNumber;
112
103
  continue;
@@ -127,6 +118,16 @@ function insertBracketContextRows(
127
118
  }
128
119
  }
129
120
 
121
+ /**
122
+ * Insert off-window block-boundary rows (enclosing header, matching closing
123
+ * bracket, …) into a numbered diff. Context rows carry pre-edit line numbers —
124
+ * the renumbering contract of `buildCompactDiffPreview` — so boundary lines
125
+ * discovered in the new file are translated back to their pre-edit numbers
126
+ * and merged with the old-file pass before a single insertion sweep. Without
127
+ * the translation, a context line sitting in a net-offset region would be
128
+ * re-inserted under its post-edit number: duplicated, out of order, and
129
+ * renumbered incorrectly by the preview.
130
+ */
130
131
  function addMatchingBracketContextRows(
131
132
  rows: string[],
132
133
  oldLines: readonly string[],
@@ -136,16 +137,48 @@ function addMatchingBracketContextRows(
136
137
  const oldVisible: number[] = [];
137
138
  const newVisible: number[] = [];
138
139
  const seenRows = new Set(rows);
140
+ // Change positions in new-file coordinates, used to translate an unchanged
141
+ // new-file line number back to its pre-edit equivalent.
142
+ const changes: { newPos: number; delta: 1 | -1 }[] = [];
143
+ let offset = 0;
139
144
 
140
145
  for (const row of rows) {
141
146
  const parsed = parseNumberedDiffRow(row);
142
147
  if (!parsed) continue;
143
- if (parsed.source === "old") oldVisible.push(parsed.lineNumber);
144
- else newVisible.push(parsed.lineNumber);
148
+ switch (parsed.prefix) {
149
+ case "-":
150
+ oldVisible.push(parsed.lineNumber);
151
+ changes.push({ newPos: parsed.lineNumber + offset, delta: -1 });
152
+ offset--;
153
+ break;
154
+ case "+":
155
+ newVisible.push(parsed.lineNumber);
156
+ changes.push({ newPos: parsed.lineNumber, delta: 1 });
157
+ offset++;
158
+ break;
159
+ default:
160
+ // Context rows are visible in BOTH files: pre-edit number as
161
+ // written, post-edit number shifted by the net change so far.
162
+ oldVisible.push(parsed.lineNumber);
163
+ newVisible.push(parsed.lineNumber + offset);
164
+ break;
165
+ }
145
166
  }
146
167
 
147
- insertBracketContextRows(rows, "old", findBlockContextLines(oldLines, oldVisible, source), seenRows);
148
- insertBracketContextRows(rows, "new", findBlockContextLines(newLines, newVisible, source), seenRows);
168
+ const toOldLineNumber = (newLineNumber: number): number => {
169
+ let shift = 0;
170
+ for (const change of changes) {
171
+ if (change.newPos <= newLineNumber) shift += change.delta;
172
+ }
173
+ return newLineNumber - shift;
174
+ };
175
+
176
+ const contextRows = findBlockContextLines(oldLines, oldVisible, source);
177
+ for (const [lineNumber, text] of findBlockContextLines(newLines, newVisible, source)) {
178
+ const oldLineNumber = toOldLineNumber(lineNumber);
179
+ if (!contextRows.has(oldLineNumber)) contextRows.set(oldLineNumber, text);
180
+ }
181
+ insertBracketContextRows(rows, contextRows, seenRows);
149
182
  }
150
183
 
151
184
  /**
@@ -82,6 +82,14 @@ function parseProgram(code: string): { program: { body: ReadonlyArray<BabelProgr
82
82
  }
83
83
  }
84
84
 
85
+ // Callee substituted for dynamic `import(...)` calls. Functions handed to puppeteer
86
+ // (`tab.evaluate`, `page.evaluate`, `waitForFunction`, `$$eval`, ...) are serialized with
87
+ // `Function.prototype.toString()` and re-evaluated inside the browser page, where the
88
+ // worker-injected `__omp_import__` global does not exist. The swap therefore guards on the
89
+ // helper's presence and falls back to native dynamic import, so serialized code keeps
90
+ // working in foreign realms while in-worker code still resolves against the session cwd.
91
+ const DYNAMIC_IMPORT_CALLEE = '(typeof __omp_import__ === "function" ? __omp_import__ : (s, o) => import(s, o))';
92
+
85
93
  function buildOmpImportCall(sourceLiteral: string, optionsLiteral: string | undefined): string {
86
94
  // Route every static import through the worker-injected `__omp_import__` helper so the
87
95
  // specifier resolves against the session cwd (and `with`-attribute imports keep working).
@@ -180,7 +188,7 @@ export function rewriteImports(code: string): string {
180
188
  const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
181
189
  const callee = call.callee;
182
190
  if (callee?.type !== "Import" || typeof callee.start !== "number" || typeof callee.end !== "number") return;
183
- edits.push({ start: callee.start, end: callee.end, text: "__omp_import__" });
191
+ edits.push({ start: callee.start, end: callee.end, text: DYNAMIC_IMPORT_CALLEE });
184
192
  });
185
193
 
186
194
  if (edits.length === 0) return code;