@oh-my-pi/pi-coding-agent 15.11.4 → 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 (98) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/cli.js +520 -451
  3. package/dist/types/cli/bench-cli.d.ts +78 -0
  4. package/dist/types/cli/usage-cli.d.ts +10 -1
  5. package/dist/types/commands/bench.d.ts +29 -0
  6. package/dist/types/commands/usage.d.ts +9 -0
  7. package/dist/types/config/model-resolver.d.ts +3 -2
  8. package/dist/types/config/settings-schema.d.ts +125 -3
  9. package/dist/types/edit/renderer.d.ts +1 -0
  10. package/dist/types/modes/components/oauth-selector.d.ts +10 -1
  11. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  12. package/dist/types/modes/components/session-selector.d.ts +1 -1
  13. package/dist/types/modes/components/settings-selector.d.ts +8 -1
  14. package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +10 -0
  18. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  19. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
  20. package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
  21. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
  22. package/dist/types/modes/types.d.ts +2 -0
  23. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  24. package/dist/types/session/agent-session.d.ts +14 -1
  25. package/dist/types/session/auth-storage.d.ts +1 -1
  26. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  27. package/dist/types/session/snapcompact-inline.d.ts +107 -4
  28. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  29. package/dist/types/task/render.d.ts +1 -0
  30. package/dist/types/tools/bash.d.ts +2 -0
  31. package/dist/types/tools/eval-render.d.ts +1 -0
  32. package/dist/types/tools/renderers.d.ts +13 -0
  33. package/dist/types/tools/ssh.d.ts +1 -0
  34. package/dist/types/tools/todo.d.ts +0 -11
  35. package/package.json +11 -11
  36. package/src/cli/bench-cli.ts +437 -0
  37. package/src/cli/usage-cli.ts +187 -16
  38. package/src/cli-commands.ts +1 -0
  39. package/src/commands/bench.ts +42 -0
  40. package/src/commands/usage.ts +8 -0
  41. package/src/config/model-registry.ts +52 -5
  42. package/src/config/model-resolver.ts +36 -5
  43. package/src/config/settings-schema.ts +148 -3
  44. package/src/config/settings.ts +9 -0
  45. package/src/edit/renderer.ts +5 -0
  46. package/src/hindsight/client.ts +26 -1
  47. package/src/hindsight/state.ts +6 -2
  48. package/src/internal-urls/docs-index.generated.ts +2 -2
  49. package/src/mcp/transports/stdio.ts +81 -7
  50. package/src/modes/components/oauth-selector.ts +67 -7
  51. package/src/modes/components/reset-usage-selector.ts +161 -0
  52. package/src/modes/components/session-selector.ts +8 -2
  53. package/src/modes/components/settings-selector.ts +89 -47
  54. package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
  55. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  56. package/src/modes/components/tool-execution.ts +26 -0
  57. package/src/modes/components/transcript-container.ts +23 -1
  58. package/src/modes/controllers/command-controller.ts +24 -1
  59. package/src/modes/controllers/input-controller.ts +8 -6
  60. package/src/modes/controllers/selector-controller.ts +72 -2
  61. package/src/modes/interactive-mode.ts +83 -0
  62. package/src/modes/session-observer-registry.ts +61 -3
  63. package/src/modes/setup-wizard/index.ts +1 -0
  64. package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
  65. package/src/modes/setup-wizard/scenes/providers.ts +36 -2
  66. package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
  67. package/src/modes/setup-wizard/scenes/theme.ts +28 -1
  68. package/src/modes/setup-wizard/scenes/types.ts +10 -1
  69. package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
  70. package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
  71. package/src/modes/theme/theme.ts +2 -2
  72. package/src/modes/types.ts +2 -0
  73. package/src/modes/utils/context-usage.ts +75 -1
  74. package/src/prompts/bench.md +7 -0
  75. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  76. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  77. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  78. package/src/prompts/tools/browser.md +33 -43
  79. package/src/prompts/tools/eval.md +27 -50
  80. package/src/prompts/tools/irc.md +29 -31
  81. package/src/prompts/tools/read.md +31 -37
  82. package/src/prompts/tools/todo.md +1 -2
  83. package/src/sdk.ts +4 -2
  84. package/src/session/agent-session.ts +136 -6
  85. package/src/session/auth-storage.ts +3 -0
  86. package/src/session/codex-auto-reset.ts +190 -0
  87. package/src/session/snapcompact-inline.ts +404 -75
  88. package/src/slash-commands/builtin-registry.ts +145 -8
  89. package/src/slash-commands/helpers/context-report.ts +28 -1
  90. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  91. package/src/slash-commands/helpers/usage-report.ts +12 -0
  92. package/src/task/index.ts +30 -7
  93. package/src/task/render.ts +34 -19
  94. package/src/tools/bash.ts +3 -0
  95. package/src/tools/eval-render.ts +4 -0
  96. package/src/tools/renderers.ts +13 -0
  97. package/src/tools/ssh.ts +3 -0
  98. package/src/tools/todo.ts +8 -128
@@ -16,6 +16,7 @@ export const commands: CommandEntry[] = [
16
16
  { name: "auth-broker", load: () => import("./commands/auth-broker").then(m => m.default) },
17
17
  { name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) },
18
18
  { name: "agents", load: () => import("./commands/agents").then(m => m.default) },
19
+ { name: "bench", load: () => import("./commands/bench").then(m => m.default) },
19
20
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
20
21
  { name: "completions", load: () => import("./commands/completions").then(m => m.default) },
21
22
  { name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
@@ -0,0 +1,42 @@
1
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
2
+ import { runBenchCommand } from "../cli/bench-cli";
3
+
4
+ export default class Bench extends Command {
5
+ static description =
6
+ "Benchmark models with the same prompt: time-to-first-token and generation throughput (tokens/s)";
7
+
8
+ static args = {
9
+ models: Args.string({
10
+ description: "Model selectors (provider/model or fuzzy id, e.g. opus)",
11
+ required: true,
12
+ multiple: true,
13
+ }),
14
+ };
15
+
16
+ static flags = {
17
+ runs: Flags.integer({ description: "Requests per model (results are averaged)", default: 1 }),
18
+ "max-tokens": Flags.integer({ description: "Max output tokens per request", default: 512 }),
19
+ prompt: Flags.string({ description: "Custom prompt text (default: bundled bench prompt)" }),
20
+ json: Flags.boolean({ description: "Output JSON" }),
21
+ };
22
+
23
+ static examples = [
24
+ "# Compare two models\n omp bench anthropic/claude-opus-4-5 openai/gpt-5.2",
25
+ "# Fuzzy selectors work\n omp bench opus sonnet",
26
+ "# Average over 3 runs each\n omp bench opus gpt-5.2 --runs 3",
27
+ "# Machine-readable output\n omp bench opus --json",
28
+ ];
29
+
30
+ async run(): Promise<void> {
31
+ const { args, flags } = await this.parse(Bench);
32
+ await runBenchCommand({
33
+ models: args.models ?? [],
34
+ flags: {
35
+ runs: flags.runs,
36
+ maxTokens: flags["max-tokens"],
37
+ prompt: flags.prompt,
38
+ json: flags.json,
39
+ },
40
+ });
41
+ }
42
+ }
@@ -15,6 +15,11 @@ export default class Usage extends Command {
15
15
  description: "Redact account emails/ids (shortest unique prefix) for sharing screenshots",
16
16
  default: false,
17
17
  }),
18
+ history: Flags.boolean({
19
+ description: "Show recorded usage-limit history (hourly snapshots) instead of a live snapshot",
20
+ default: false,
21
+ }),
22
+ days: Flags.integer({ char: "d", description: "History window in days (with --history)", default: 7 }),
18
23
  };
19
24
 
20
25
  static examples = [
@@ -22,6 +27,7 @@ export default class Usage extends Command {
22
27
  "# Only Anthropic accounts\n omp usage --provider anthropic",
23
28
  "# Redact account identifiers for screenshots\n omp usage --redact",
24
29
  "# Machine-readable output\n omp usage --json",
30
+ "# Usage-limit trend over the last 30 days\n omp usage --history --days 30",
25
31
  ];
26
32
 
27
33
  async run(): Promise<void> {
@@ -30,6 +36,8 @@ export default class Usage extends Command {
30
36
  json: flags.json,
31
37
  provider: flags.provider,
32
38
  redact: flags.redact,
39
+ history: flags.history,
40
+ days: flags.days,
33
41
  });
34
42
  }
35
43
  }
@@ -20,6 +20,11 @@ import {
20
20
  UNK_CONTEXT_WINDOW,
21
21
  UNK_MAX_TOKENS,
22
22
  } from "@oh-my-pi/pi-catalog/provider-models";
23
+ import {
24
+ collapseBuiltModelVariants,
25
+ getVariantAliasSources,
26
+ resolveVariantAlias,
27
+ } from "@oh-my-pi/pi-catalog/variant-collapse";
23
28
 
24
29
  // Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
25
30
  // any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
@@ -542,7 +547,37 @@ function normalizeSuppressedSelector(selector: string): string {
542
547
  if (!trimmed) return trimmed;
543
548
  const parsed = parseModelString(trimmed);
544
549
  if (!parsed) return trimmed;
545
- return `${parsed.provider}/${parsed.id}`;
550
+ // Retired effort-tier variant ids normalize to their collapsed logical id
551
+ // so persisted suppressions keyed by raw member ids still bind.
552
+ const aliasId = resolveVariantAlias(parsed.provider, parsed.id);
553
+ return `${parsed.provider}/${aliasId ?? parsed.id}`;
554
+ }
555
+
556
+ /**
557
+ * Look up a model's override, falling back to entries keyed by retired
558
+ * effort-tier variant ids (models.yml authored before collapsing). A raw key
559
+ * only re-binds when no live model holds that id.
560
+ */
561
+ function resolveModelOverrideWithAliases(
562
+ overrides: Map<string, ModelOverride>,
563
+ model: Model<Api>,
564
+ hasLiveModel: (provider: string, id: string) => boolean,
565
+ ): ModelOverride | undefined {
566
+ const direct = overrides.get(model.id);
567
+ if (direct) return direct;
568
+ for (const rawId of getVariantAliasSources(model.provider, model.id)) {
569
+ if (hasLiveModel(model.provider, rawId)) continue;
570
+ const remapped = overrides.get(rawId);
571
+ if (remapped) {
572
+ logger.debug("model override re-keyed through variant alias", {
573
+ provider: model.provider,
574
+ from: rawId,
575
+ to: model.id,
576
+ });
577
+ return remapped;
578
+ }
579
+ }
580
+ return undefined;
546
581
  }
547
582
 
548
583
  function getDisabledProviderIdsFromSettings(): Set<string> {
@@ -799,7 +834,9 @@ export class ModelRegistry {
799
834
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
800
835
  // Merge runtime extension models so they survive refresh() cycles
801
836
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
802
- const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
837
+ // Custom/config providers bypass the model-manager merge point —
838
+ // collapse effort-tier variants here so X/X-thinking twins fold.
839
+ const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
803
840
  this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
804
841
  this.#rebuildCanonicalIndex();
805
842
  this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
@@ -1152,7 +1189,7 @@ export class ModelRegistry {
1152
1189
  const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1153
1190
  // Merge runtime extension models so they survive online discovery completion
1154
1191
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1155
- const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
1192
+ const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1156
1193
  this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1157
1194
  this.#rebuildCanonicalIndex();
1158
1195
  }
@@ -1398,8 +1435,13 @@ export class ModelRegistry {
1398
1435
  #applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
1399
1436
  const overrides = this.#modelOverrides.get(provider);
1400
1437
  if (!overrides || overrides.size === 0) return models;
1438
+ let liveIds: Set<string> | null = null;
1439
+ const hasLiveModel = (_provider: string, id: string) => {
1440
+ liveIds ??= new Set(models.map(m => m.id));
1441
+ return liveIds.has(id);
1442
+ };
1401
1443
  return models.map(model => {
1402
- const override = overrides.get(model.id);
1444
+ const override = resolveModelOverrideWithAliases(overrides, model, hasLiveModel);
1403
1445
  if (!override) return model;
1404
1446
  return applyModelOverride(model, override);
1405
1447
  });
@@ -1443,10 +1485,15 @@ export class ModelRegistry {
1443
1485
  }
1444
1486
  #applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
1445
1487
  if (overrides.size === 0) return models;
1488
+ let liveKeys: Set<string> | null = null;
1489
+ const hasLiveModel = (provider: string, id: string) => {
1490
+ liveKeys ??= new Set(models.map(m => `${m.provider}\u0000${m.id}`));
1491
+ return liveKeys.has(`${provider}\u0000${id}`);
1492
+ };
1446
1493
  return models.map(model => {
1447
1494
  const providerOverrides = overrides.get(model.provider);
1448
1495
  if (!providerOverrides) return model;
1449
- const override = providerOverrides.get(model.id);
1496
+ const override = resolveModelOverrideWithAliases(providerOverrides, model, hasLiveModel);
1450
1497
  if (!override) return model;
1451
1498
  return applyModelOverride(model, override);
1452
1499
  });
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Layering:
5
5
  * - `matchModel` is the single matching engine. Order: exact `provider/id`
6
- * reference (with OpenRouter routed/date fallbacks) → exact canonical id →
7
- * exact bare id → provider-scoped fuzzysubstring with alias-vs-dated pick.
6
+ * reference (with variant-alias and OpenRouter routed/date fallbacks) →
7
+ * exact canonical id → exact bare id retired variant alias
8
+ * provider-scoped fuzzy → substring with alias-vs-dated pick.
8
9
  * - `parseModelPatternWithContext`/`parseModelPattern` layer the selector
9
10
  * grammar on top: trailing `:level` thinking suffixes (`splitThinkingSuffix`)
10
11
  * and `@upstream` provider routing (`splitUpstreamRouting`).
@@ -19,9 +20,11 @@ import type { Api, Effort, KnownProvider, Model, ModelSpec } from "@oh-my-pi/pi-
19
20
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
20
21
  import { modelMatchesHost } from "@oh-my-pi/pi-catalog/hosts";
21
22
  import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
23
+ import { stripThinkingVariantToken } from "@oh-my-pi/pi-catalog/identity/family";
22
24
  import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
23
25
  import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
24
26
  import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models";
27
+ import { resolveBareVariantAlias, resolveVariantAlias } from "@oh-my-pi/pi-catalog/variant-collapse";
25
28
  import { fuzzyMatch } from "@oh-my-pi/pi-tui";
26
29
  import { logger } from "@oh-my-pi/pi-utils";
27
30
  import chalk from "chalk";
@@ -228,6 +231,18 @@ export function resolveProviderModelReference(
228
231
  return exact;
229
232
  }
230
233
 
234
+ // Retired effort-tier variant ids resolve to their collapsed logical
235
+ // model: hand-table aliases first, then the `X-thinking` → `X` grammar
236
+ // for auto-derived pairs. Exact lookup above always wins while raw is live.
237
+ const variantAliasId =
238
+ resolveVariantAlias(normalizedProvider, normalizedModelId) ?? stripThinkingVariantToken(normalizedModelId);
239
+ if (variantAliasId) {
240
+ const aliased = index.get(`${normalizedProvider}\u0000${variantAliasId.toLowerCase()}`);
241
+ if (aliased) {
242
+ return aliased;
243
+ }
244
+ }
245
+
231
246
  if (normalizedProvider !== "openrouter") {
232
247
  return undefined;
233
248
  }
@@ -407,11 +422,13 @@ function findExactCanonicalModelMatch(
407
422
 
408
423
  /**
409
424
  * The single model-matching engine. Tries, in order:
410
- * 1. exact `provider/id` reference (OpenRouter routed/date fallbacks included),
425
+ * 1. exact `provider/id` reference (variant-alias and OpenRouter routed/date
426
+ * fallbacks included),
411
427
  * 2. exact canonical id (coalesces provider variants),
412
428
  * 3. exact bare id (preference-ranked),
413
- * 4. provider-scoped fuzzy match,
414
- * 5. substring match with the alias-vs-dated pick.
429
+ * 4. retired effort-tier variant alias (collapsed catalog entries),
430
+ * 5. provider-scoped fuzzy match,
431
+ * 6. substring match with the alias-vs-dated pick.
415
432
  * Returns the matched model or undefined if no match found.
416
433
  */
417
434
  function matchModel(
@@ -440,6 +457,20 @@ function matchModel(
440
457
  if (exactMatches.length > 0) {
441
458
  return pickPreferredModel(exactMatches, context);
442
459
  }
460
+
461
+ // Retired effort-tier variant ids (bare, no provider prefix) resolve to
462
+ // their collapsed logical model; models from the providers whose table
463
+ // declared the alias win ties. Auto-derived `X-thinking` pairs resolve
464
+ // through the grammar fallback.
465
+ const bareAlias = resolveBareVariantAlias(modelPattern);
466
+ const bareAliasTargetId = bareAlias?.id ?? stripThinkingVariantToken(modelPattern);
467
+ if (bareAliasTargetId) {
468
+ const aliasMatches = availableModels.filter(m => m.id.toLowerCase() === bareAliasTargetId.toLowerCase());
469
+ if (aliasMatches.length > 0) {
470
+ const preferred = bareAlias ? aliasMatches.filter(m => bareAlias.providers.includes(m.provider)) : [];
471
+ return pickPreferredModel(preferred.length > 0 ? preferred : aliasMatches, context);
472
+ }
473
+ }
443
474
  // Check for provider/modelId format — fuzzy match within provider only.
444
475
  const slashIndex = modelPattern.indexOf("/");
445
476
  if (slashIndex !== -1) {
@@ -1,4 +1,5 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
+ import { SHAPE_VARIANT_NAMES } from "@oh-my-pi/snapcompact";
2
3
  import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
3
4
  import {
4
5
  TINY_MODEL_DEVICE_DEFAULT,
@@ -1572,14 +1573,28 @@ export const SETTINGS_SCHEMA = {
1572
1573
 
1573
1574
  // Experimental: snapcompact inline imaging (transient, per-request; never persisted)
1574
1575
  "snapcompact.systemPrompt": {
1575
- type: "boolean",
1576
- default: false,
1576
+ type: "enum",
1577
+ values: ["none", "agents-md", "all"] as const,
1578
+ default: "none",
1577
1579
  ui: {
1578
1580
  tab: "context",
1579
1581
  group: "Experimental",
1580
1582
  label: "Snapcompact System Prompt",
1581
1583
  description:
1582
- "Experimental: render the system prompt as dense PNG image(s) and attach to the first user message (vision models only). Saves tokens; loses system-prompt prompt caching.",
1584
+ "Experimental: render selected system prompt text as dense PNG image(s) and attach to the first user message (vision models only). Saves tokens; loses prompt caching for imaged text.",
1585
+ options: [
1586
+ { value: "none", label: "None", description: "Keep the system prompt as text." },
1587
+ {
1588
+ value: "agents-md",
1589
+ label: "AGENTS.md",
1590
+ description: "Only move loaded context-file instructions to images, when that saves tokens.",
1591
+ },
1592
+ {
1593
+ value: "all",
1594
+ label: "All",
1595
+ description: "Move the full system prompt to images, when that saves tokens.",
1596
+ },
1597
+ ],
1583
1598
  },
1584
1599
  },
1585
1600
 
@@ -1595,6 +1610,97 @@ export const SETTINGS_SCHEMA = {
1595
1610
  },
1596
1611
  },
1597
1612
 
1613
+ "snapcompact.shape": {
1614
+ type: "enum",
1615
+ values: ["auto", ...SHAPE_VARIANT_NAMES] as const,
1616
+ default: "auto",
1617
+ ui: {
1618
+ tab: "context",
1619
+ group: "Experimental",
1620
+ label: "Snapcompact Shape",
1621
+ description:
1622
+ "Frame shape snapcompact prints text with (compaction archive and inline imaging). Auto picks a shape tuned for the current model.",
1623
+ options: [
1624
+ {
1625
+ value: "auto",
1626
+ label: "Auto",
1627
+ description: "Picks a shape tuned for the current model, falling back to its provider family.",
1628
+ },
1629
+ {
1630
+ value: "8x8r-bw",
1631
+ label: "8x8 repeated, black",
1632
+ description:
1633
+ "unscii square cell, black ink, every line printed twice with the copy on a pale highlight band.",
1634
+ },
1635
+ {
1636
+ value: "8x8r-sent",
1637
+ label: "8x8 repeated, sentence hues",
1638
+ description: "Repeated grid with ink cycling six hues at sentence boundaries.",
1639
+ },
1640
+ {
1641
+ value: "8x8u-bw",
1642
+ label: "8x8, black",
1643
+ description: "Plain unscii square cell, single-printed lines, black ink.",
1644
+ },
1645
+ {
1646
+ value: "8x8u-sent",
1647
+ label: "8x8, sentence hues",
1648
+ description: "Plain unscii square cell with sentence-hue ink.",
1649
+ },
1650
+ {
1651
+ value: "6x6u-bw",
1652
+ label: "6x6 dense, black",
1653
+ description: "unscii squeezed to 6x6 — densest readable cell, fewest frames — in black ink.",
1654
+ },
1655
+ {
1656
+ value: "6x6u-sent",
1657
+ label: "6x6 dense, sentence hues",
1658
+ description: "Densest cell with sentence-hue ink.",
1659
+ },
1660
+ {
1661
+ value: "5x8-bw",
1662
+ label: "5x8 legacy, black",
1663
+ description: "Original X.org 5x8 glyphs on the 2576px frame, black ink.",
1664
+ },
1665
+ {
1666
+ value: "5x8-sent",
1667
+ label: "5x8 legacy, sentence hues",
1668
+ description: "The original snapcompact shape (pre-shape-table sessions rendered this).",
1669
+ },
1670
+ {
1671
+ value: "6x12-dim",
1672
+ label: "6x12, dimmed stopwords",
1673
+ description: "X.org 6x12 glyphs, black ink, function words dimmed gray.",
1674
+ },
1675
+ {
1676
+ value: "8x13-bw",
1677
+ label: "8x13, black",
1678
+ description: "X.org 8x13 glyphs, black ink.",
1679
+ },
1680
+ {
1681
+ value: "8on16-bw",
1682
+ label: "8x13 on 16px pitch, black",
1683
+ description: "8x13 glyphs on an 8x16 cell (extra leading), black ink.",
1684
+ },
1685
+ {
1686
+ value: "doc-8on16-bw",
1687
+ label: "Doc 8on16, black",
1688
+ description: "Two word-wrapped newspaper columns of 8x13 glyphs on a 16px pitch, black ink.",
1689
+ },
1690
+ {
1691
+ value: "doc-8on16-sent",
1692
+ label: "Doc 8on16, sentence hues",
1693
+ description: "Two-column doc layout with sentence-hue ink.",
1694
+ },
1695
+ {
1696
+ value: "doc-8on16-sent-dim",
1697
+ label: "Doc 8on16, sentence hues + dimmed stopwords",
1698
+ description: "Two-column doc layout, sentence-hue ink, function words dimmed gray.",
1699
+ },
1700
+ ],
1701
+ },
1702
+ },
1703
+
1598
1704
  // Branch summaries
1599
1705
  "branchSummary.enabled": {
1600
1706
  type: "boolean",
@@ -3617,6 +3723,39 @@ export const SETTINGS_SCHEMA = {
3617
3723
  ],
3618
3724
  },
3619
3725
  },
3726
+ // Codex saved rate-limit resets (auto-redeem)
3727
+ "codexResets.autoRedeem": {
3728
+ type: "boolean",
3729
+ default: false,
3730
+ ui: {
3731
+ tab: "providers",
3732
+ group: "Services",
3733
+ label: "Codex Auto-Redeem Saved Resets",
3734
+ description:
3735
+ "When a turn is blocked by the Codex weekly limit on the active account and no other account is available, automatically spend one saved rate-limit reset (ChatGPT 'save rate limit resets'). Conservative: never fires for 5-hour-only or Spark limits, near a natural reset, or twice for the same block. Requires retries enabled.",
3736
+ },
3737
+ },
3738
+ "codexResets.minBlockedMinutes": {
3739
+ type: "number",
3740
+ default: 60,
3741
+ ui: {
3742
+ tab: "providers",
3743
+ group: "Services",
3744
+ label: "Codex Auto-Redeem Min Block",
3745
+ description:
3746
+ "Only auto-redeem when the natural weekly reset is at least this many minutes away (don't spend a ~30-day credit to save a short wait).",
3747
+ },
3748
+ },
3749
+ "codexResets.keepCredits": {
3750
+ type: "number",
3751
+ default: 0,
3752
+ ui: {
3753
+ tab: "providers",
3754
+ group: "Services",
3755
+ label: "Codex Auto-Redeem Reserve",
3756
+ description: "Never auto-spend below this many saved resets (0 = the last credit may be spent automatically).",
3757
+ },
3758
+ },
3620
3759
  "provider.appendOnlyContext": {
3621
3760
  type: "enum",
3622
3761
  values: ["auto", "on", "off"] as const,
@@ -4005,6 +4144,11 @@ export interface ShellMinimizerSettings {
4005
4144
  sourceOutlineLevel: "default" | "aggressive";
4006
4145
  legacyFilters: boolean | undefined;
4007
4146
  }
4147
+ export interface CodexResetsSettings {
4148
+ autoRedeem: boolean;
4149
+ minBlockedMinutes: number;
4150
+ keepCredits: number;
4151
+ }
4008
4152
 
4009
4153
  /** Map group prefix -> typed settings interface */
4010
4154
  export interface GroupTypeMap {
@@ -4024,6 +4168,7 @@ export interface GroupTypeMap {
4024
4168
  modelTags: ModelTagsSettings;
4025
4169
  cycleOrder: string[];
4026
4170
  shellMinimizer: ShellMinimizerSettings;
4171
+ codexResets: CodexResetsSettings;
4027
4172
  }
4028
4173
 
4029
4174
  export type GroupPrefix = keyof GroupTypeMap;
@@ -798,6 +798,15 @@ export class Settings {
798
798
  raw["compaction.strategy"] = "shake";
799
799
  }
800
800
 
801
+ // snapcompact.systemPrompt: boolean -> scoped enum.
802
+ const snapcompactObj = raw.snapcompact as Record<string, unknown> | undefined;
803
+ if (snapcompactObj && typeof snapcompactObj.systemPrompt === "boolean") {
804
+ snapcompactObj.systemPrompt = snapcompactObj.systemPrompt ? "all" : "none";
805
+ }
806
+ if (typeof raw["snapcompact.systemPrompt"] === "boolean") {
807
+ raw["snapcompact.systemPrompt"] = raw["snapcompact.systemPrompt"] ? "all" : "none";
808
+ }
809
+
801
810
  // statusLine: rename "plan_mode" segment to "mode"
802
811
  const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
803
812
  if (statusLineObj) {
@@ -579,6 +579,11 @@ function wrapEditRendererLine(line: string, width: number): string[] {
579
579
 
580
580
  export const editToolRenderer = {
581
581
  mergeCallAndResult: true,
582
+ // Pending preview is a TAIL window of the streamed diff ("… N more lines
583
+ // above" + last rows); the result render re-anchors the block top-first, so
584
+ // committing the preview's settled head would strand a stale call-box
585
+ // fragment in native scrollback.
586
+ provisionalPendingPreview: true,
582
587
 
583
588
  renderCall(
584
589
  args: EditRenderArgs,
@@ -546,7 +546,7 @@ interface BuiltMemoryItem {
546
546
  function buildMemoryItem(item: MemoryItemInput): BuiltMemoryItem {
547
547
  const out: BuiltMemoryItem = { content: item.content };
548
548
  if (item.timestamp !== undefined) {
549
- out.timestamp = item.timestamp instanceof Date ? item.timestamp.toISOString() : item.timestamp;
549
+ out.timestamp = item.timestamp instanceof Date ? formatDateWithLocalOffset(item.timestamp) : item.timestamp;
550
550
  }
551
551
  if (item.context !== undefined) out.context = item.context;
552
552
  if (item.metadata !== undefined) out.metadata = item.metadata;
@@ -558,6 +558,31 @@ function buildMemoryItem(item: MemoryItemInput): BuiltMemoryItem {
558
558
  return out;
559
559
  }
560
560
 
561
+ function formatDateWithLocalOffset(date: Date): string {
562
+ const offsetMinutes = date.getTimezoneOffset();
563
+ const offsetSign = offsetMinutes <= 0 ? "+" : "-";
564
+ const absoluteOffset = Math.abs(offsetMinutes);
565
+ const offsetHours = Math.floor(absoluteOffset / 60);
566
+ const offsetRemainderMinutes = absoluteOffset % 60;
567
+ const milliseconds = date.getMilliseconds();
568
+ const millisecondsPart = milliseconds === 0 ? "" : `.${pad3(milliseconds)}`;
569
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}T${pad2(
570
+ date.getHours(),
571
+ )}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}${millisecondsPart}${offsetSign}${pad2(
572
+ offsetHours,
573
+ )}:${pad2(offsetRemainderMinutes)}`;
574
+ }
575
+
576
+ function pad2(value: number): string {
577
+ return value < 10 ? `0${value}` : String(value);
578
+ }
579
+
580
+ function pad3(value: number): string {
581
+ if (value < 10) return `00${value}`;
582
+ if (value < 100) return `0${value}`;
583
+ return String(value);
584
+ }
585
+
561
586
  function buildQueryString(query: Record<string, unknown>): string {
562
587
  const params = new URLSearchParams();
563
588
  for (const [key, value] of Object.entries(query)) {
@@ -26,6 +26,7 @@ const RETAIN_FLUSH_INTERVAL_MS = 5_000;
26
26
  interface PendingRetainItem {
27
27
  content: string;
28
28
  context?: string;
29
+ timestamp: Date;
29
30
  }
30
31
 
31
32
  interface RecallOutcome {
@@ -84,7 +85,7 @@ export class HindsightRetainQueue {
84
85
  if (this.#closed) {
85
86
  throw new Error("Hindsight retain queue is closed.");
86
87
  }
87
- this.#items.push({ content, context });
88
+ this.#items.push({ content, context, timestamp: new Date() });
88
89
 
89
90
  if (this.#items.length >= RETAIN_FLUSH_BATCH_SIZE) {
90
91
  void this.flush();
@@ -154,6 +155,7 @@ export class HindsightRetainQueue {
154
155
  context: item.context ?? state.config.retainContext,
155
156
  metadata: { session_id: sessionId },
156
157
  tags: state.retainTags,
158
+ timestamp: item.timestamp,
157
159
  }));
158
160
  await state.client.retainBatch(state.bankId, batch, { async: true });
159
161
  if (state.config.debug) {
@@ -281,6 +283,7 @@ export class HindsightSessionState {
281
283
  }
282
284
 
283
285
  async retainSession(messages: HindsightMessage[]): Promise<void> {
286
+ const retainedAt = new Date();
284
287
  const retainFullWindow = this.config.retainMode === "full-session";
285
288
  let target: HindsightMessage[];
286
289
  let documentId: string;
@@ -291,7 +294,7 @@ export class HindsightSessionState {
291
294
  } else {
292
295
  const windowTurns = this.config.retainEveryNTurns + this.config.retainOverlapTurns;
293
296
  target = sliceLastTurnsByUserBoundary(messages, windowTurns);
294
- documentId = `${this.sessionId}-${Date.now()}`;
297
+ documentId = `${this.sessionId}-${retainedAt.getTime()}`;
295
298
  }
296
299
 
297
300
  const { transcript } = prepareRetentionTranscript(target, true);
@@ -303,6 +306,7 @@ export class HindsightSessionState {
303
306
  context: this.config.retainContext,
304
307
  metadata: { session_id: this.sessionId },
305
308
  tags: this.retainTags,
309
+ timestamp: retainedAt,
306
310
  async: true,
307
311
  });
308
312
  }