tau-coding-agent 0.1.6 → 0.2.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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -1,4 +1,3 @@
1
- import { complete } from "@earendil-works/pi-ai/compat";
2
1
  import type { Api, Model, UserMessage } from "@earendil-works/pi-ai";
3
2
  import {
4
3
  BorderedLoader,
@@ -7,12 +6,10 @@ import {
7
6
  getAgentDir,
8
7
  getLanguageFromPath,
9
8
  getMarkdownTheme,
10
- migrateSessionEntries,
11
9
  parseSessionEntries,
12
10
  type ExtensionAPI,
13
11
  type ExtensionCommandContext,
14
12
  type SessionEntry,
15
- type SessionHeader,
16
13
  type Theme,
17
14
  } from "@earendil-works/pi-coding-agent";
18
15
  import {
@@ -32,10 +29,10 @@ import path from "node:path";
32
29
 
33
30
  // --- Constants ---
34
31
 
35
- const INSIGHTS_META_SCHEMA_VERSION = 1;
36
- const SESSION_FINGERPRINT_VERSION = 1;
32
+ const INSIGHTS_META_SCHEMA_VERSION = 2;
33
+ const SESSION_FINGERPRINT_VERSION = 2;
37
34
  const INSIGHTS_FACET_SCHEMA_VERSION = 1;
38
- const FACET_PROMPT_VERSION = 1;
35
+ const FACET_PROMPT_VERSION = 2;
39
36
  const SYNTHESIS_PROMPT_VERSION = 1;
40
37
 
41
38
  const MIN_MEANINGFUL_USER_MESSAGES = 2;
@@ -46,6 +43,8 @@ const REDUCED_TRANSCRIPT_HEAD_CHARS = 12_000;
46
43
  const REDUCED_TRANSCRIPT_TAIL_CHARS = 12_000;
47
44
  const REDUCED_TRANSCRIPT_MAX_CHARS = FULL_TRANSCRIPT_MAX_CHARS;
48
45
 
46
+ const MIN_REPORT_WIDTH = 42; // Existing 40-column box plus its outer margin.
47
+
49
48
  const AGENT_ROOT = getAgentDir();
50
49
  const CACHE_ROOT = path.join(AGENT_ROOT, "insights");
51
50
  const META_CACHE_DIR = path.join(CACHE_ROOT, "session-meta");
@@ -140,9 +139,7 @@ type InsightScope = "current" | "project" | "all";
140
139
 
141
140
  type ConfiguredModelSelection = {
142
141
  model: Model<Api>;
143
- apiKey?: string;
144
- headers?: Record<string, string>;
145
- env?: Record<string, string>;
142
+ modelRegistry: ExtensionCommandContext["modelRegistry"];
146
143
  };
147
144
 
148
145
  type ReadonlySessionManager = Pick<
@@ -311,6 +308,7 @@ class AbortError extends Error {
311
308
  class InsightsReportComponent implements Component {
312
309
  private readonly markdown: Markdown;
313
310
  private scrollOffset = 0;
311
+ private renderWidth?: number;
314
312
  private cachedWidth?: number;
315
313
  private cachedLines?: string[];
316
314
  private cachedBodyWidth?: number;
@@ -344,8 +342,11 @@ class InsightsReportComponent implements Component {
344
342
  return;
345
343
  }
346
344
 
345
+ const width = this.renderWidth ?? this.tui.terminal.columns;
346
+ if (width < MIN_REPORT_WIDTH) return;
347
+
347
348
  const bodyHeight = this.getBodyHeight();
348
- const boxWidth = this.getBoxWidth(this.tui.terminal.columns);
349
+ const boxWidth = this.getBoxWidth(width);
349
350
  const bodyLines = this.getBodyLines(this.getContentWidth(boxWidth));
350
351
  const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
351
352
 
@@ -370,6 +371,19 @@ class InsightsReportComponent implements Component {
370
371
  }
371
372
 
372
373
  render(width: number): string[] {
374
+ this.renderWidth = width;
375
+ if (width < MIN_REPORT_WIDTH) {
376
+ if (width < 1) return [];
377
+ return [
378
+ truncateToWidth(
379
+ this.theme.fg("muted", `Resize to ${MIN_REPORT_WIDTH}+ columns to view insights.`),
380
+ width,
381
+ "",
382
+ ),
383
+ truncateToWidth(this.theme.fg("dim", "Enter/Esc close"), width, ""),
384
+ ];
385
+ }
386
+
373
387
  if (this.cachedWidth === width && this.cachedLines) {
374
388
  return this.cachedLines;
375
389
  }
@@ -437,11 +451,11 @@ class InsightsReportComponent implements Component {
437
451
  }
438
452
 
439
453
  private getBoxWidth(width: number): number {
440
- return Math.max(40, Math.min(width - 2, 140));
454
+ return Math.min(width - 2, 140);
441
455
  }
442
456
 
443
457
  private getContentWidth(boxWidth: number): number {
444
- return Math.max(10, boxWidth - 4);
458
+ return boxWidth - 4;
445
459
  }
446
460
 
447
461
  private borderLine(left: string, right: string, width: number): string {
@@ -453,7 +467,7 @@ class InsightsReportComponent implements Component {
453
467
  }
454
468
 
455
469
  private boxLine(content: string, width: number): string {
456
- const padded = ` ${content}`;
470
+ const padded = ` ${truncateToWidth(content, this.getContentWidth(width))}`;
457
471
  const visible = visibleWidth(padded);
458
472
  const rightPad = Math.max(0, width - 2 - visible);
459
473
  return `${this.theme.fg("borderMuted", "│")}${padded}${" ".repeat(rightPad)}${this.theme.fg("borderMuted", "│")}`;
@@ -779,12 +793,7 @@ async function getConfiguredModelSelection(
779
793
 
780
794
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
781
795
  if (!auth.ok) return null;
782
- return {
783
- model: ctx.model,
784
- apiKey: auth.apiKey,
785
- headers: auth.headers,
786
- env: auth.env,
787
- };
796
+ return { model: ctx.model, modelRegistry: ctx.modelRegistry };
788
797
  }
789
798
 
790
799
  async function listTargets(
@@ -804,32 +813,16 @@ async function listTargets(
804
813
  onProgress({ stage: "resolve", current, total });
805
814
  };
806
815
 
807
- const sessionFiles =
816
+ const cwd = path.resolve(ctx.cwd);
817
+ const sessions =
808
818
  scope === "project"
809
- ? await listProjectSessionFiles(ctx.cwd, ctx.sessionManager.getSessionDir(), progress)
810
- : await listAllSessionFiles(progress);
819
+ ? await SessionManager.list(cwd, ctx.sessionManager.getSessionDir(), progress, signal)
820
+ : await SessionManager.listAll(progress, signal);
811
821
 
812
822
  throwIfAborted(signal);
813
- return sessionFiles.map((sessionFile) => ({ path: sessionFile }));
814
- }
815
-
816
- async function listProjectSessionFiles(
817
- cwd: string,
818
- sessionDir: string,
819
- onProgress: (current: number, total: number) => void,
820
- ): Promise<string[]> {
821
- const currentCwd = path.resolve(cwd);
822
- const sessions = await SessionManager.list(currentCwd, sessionDir, onProgress);
823
823
  return sessions
824
- .filter((session) => path.resolve(session.cwd || currentCwd) === currentCwd)
825
- .map((session) => session.path);
826
- }
827
-
828
- async function listAllSessionFiles(
829
- onProgress: (current: number, total: number) => void,
830
- ): Promise<string[]> {
831
- const sessions = await SessionManager.listAll(onProgress);
832
- return sessions.map((session) => session.path);
824
+ .filter((session) => scope === "all" || path.resolve(session.cwd || cwd) === cwd)
825
+ .map((session) => ({ path: session.path }));
833
826
  }
834
827
 
835
828
  async function loadOrExtractMeta(
@@ -841,7 +834,7 @@ async function loadOrExtractMeta(
841
834
  cacheHit: boolean;
842
835
  runtime?: SessionRuntime;
843
836
  }> {
844
- const cacheInfo = await getCacheInfo(target.path);
837
+ const cacheInfo = await getCacheInfo(target);
845
838
 
846
839
  if (cacheInfo.cacheKey && cacheInfo.fingerprint) {
847
840
  const cached = await readMetaCache(cacheInfo.cacheKey, cacheInfo.fingerprint);
@@ -919,48 +912,12 @@ async function readOnlyOpenSession(sessionFile: string): Promise<ReadonlySession
919
912
  throw new Error(`Session file is empty or invalid: ${sessionFile}`);
920
913
  }
921
914
 
922
- migrateSessionEntries(fileEntries);
923
- const header = fileEntries.find((entry): entry is SessionHeader => entry.type === "session");
915
+ const header = fileEntries.find((entry) => entry.type === "session");
924
916
  if (!header) {
925
917
  throw new Error(`Session file has no header: ${sessionFile}`);
926
918
  }
927
919
 
928
- const entries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session");
929
- const byId = new Map(entries.map((entry) => [entry.id, entry]));
930
- const leafId = entries.at(-1)?.id ?? null;
931
- const sessionDir = path.dirname(sessionFile);
932
-
933
- return {
934
- getEntries() {
935
- return entries.slice();
936
- },
937
- getBranch(fromId?: string) {
938
- const startId = fromId ?? leafId;
939
- if (!startId) return [];
940
- const pathEntries: SessionEntry[] = [];
941
- let current = byId.get(startId);
942
- while (current) {
943
- pathEntries.unshift(current);
944
- current = current.parentId ? byId.get(current.parentId) : undefined;
945
- }
946
- return pathEntries;
947
- },
948
- getLeafId() {
949
- return leafId;
950
- },
951
- getHeader() {
952
- return header;
953
- },
954
- getSessionDir() {
955
- return sessionDir;
956
- },
957
- getSessionId() {
958
- return header.id;
959
- },
960
- getSessionFile() {
961
- return sessionFile;
962
- },
963
- } satisfies ReadonlySessionManager;
920
+ return SessionManager.inMemory(header.cwd, undefined, fileEntries);
964
921
  }
965
922
 
966
923
  function extractSessionArtifacts(
@@ -993,11 +950,23 @@ function extractSessionArtifacts(
993
950
  for (const entry of branchEntries) {
994
951
  collectModifiedFilesFromEntry(entry, modifiedFiles);
995
952
 
996
- if (entry.type !== "message") continue;
997
- const message = entry.message as unknown as Record<string, unknown>;
998
- const role = typeof message.role === "string" ? message.role : "";
953
+ const message =
954
+ entry.type === "message" ? (entry.message as unknown as Record<string, unknown>) : undefined;
955
+ const usage =
956
+ entry.type === "usage"
957
+ ? entry.usage
958
+ : message?.role === "assistant"
959
+ ? message.usage
960
+ : undefined;
961
+ if (isRecord(usage)) {
962
+ totalInputTokens +=
963
+ numberValue(usage.input) + numberValue(usage.cacheRead) + numberValue(usage.cacheWrite);
964
+ totalOutputTokens += numberValue(usage.output);
965
+ totalCost += numberValue(isRecord(usage.cost) ? usage.cost.total : undefined);
966
+ }
967
+ if (!message) continue;
999
968
 
1000
- if (role === "user") {
969
+ if (message.role === "user") {
1001
970
  if (isMeaningfulUserMessage(message)) {
1002
971
  meaningfulUserMessages++;
1003
972
  if (!firstUserPrompt) {
@@ -1007,14 +976,8 @@ function extractSessionArtifacts(
1007
976
  continue;
1008
977
  }
1009
978
 
1010
- if (role === "assistant") {
979
+ if (message.role === "assistant") {
1011
980
  assistantMessages++;
1012
- const usage = isRecord(message.usage) ? message.usage : undefined;
1013
- totalInputTokens +=
1014
- numberValue(usage?.input) + numberValue(usage?.cacheRead) + numberValue(usage?.cacheWrite);
1015
- totalOutputTokens += numberValue(usage?.output);
1016
- totalCost += numberValue(isRecord(usage?.cost) ? usage?.cost.total : undefined);
1017
-
1018
981
  const toolCalls = extractToolCalls(message.content);
1019
982
  for (const toolCall of toolCalls) {
1020
983
  incrementRecord(toolCounts, toolCall.name);
@@ -1023,7 +986,7 @@ function extractSessionArtifacts(
1023
986
  continue;
1024
987
  }
1025
988
 
1026
- if (role === "toolResult") {
989
+ if (message.role === "toolResult") {
1027
990
  toolResultMessages++;
1028
991
  if (message.isError === true && typeof message.toolName === "string") {
1029
992
  incrementRecord(toolsWithErrors, message.toolName);
@@ -1235,76 +1198,51 @@ function prepareTranscriptForFacet(transcript: NormalizedTranscript): PreparedTr
1235
1198
  };
1236
1199
  }
1237
1200
 
1238
- const selected = new Set<number>();
1239
- let headChars = 0;
1240
- for (let index = 0; index < transcript.blocks.length; index++) {
1241
- if (headChars >= REDUCED_TRANSCRIPT_HEAD_CHARS) break;
1242
- selected.add(index);
1243
- headChars += transcript.blocks[index]!.text.length + 2;
1244
- }
1201
+ const prefix = [
1202
+ truncateChars(transcript.headerText, 1_000),
1203
+ `[Transcript reduced due to length. Full size: ${formatCount(transcript.charLength)} chars.]`,
1204
+ ].join("\n\n");
1205
+ const gap = (count: number): string => `[... ${count} blocks omitted ...]`;
1206
+ // Charge each selected block for a possible preceding gap and both separators.
1207
+ // Reserve one more gap for the end, so assembly never needs a head-only final cut.
1208
+ const gapAllowance = gap(transcript.blocks.length).length + 4;
1209
+ const maxBodyLength = REDUCED_TRANSCRIPT_MAX_CHARS - prefix.length - 2 - gapAllowance;
1210
+ const selected = new Map<number, string>();
1211
+ const selectBlock = (index: number, budget: number): number => {
1212
+ if (selected.has(index)) return budget;
1213
+ const text = reduceTranscriptBlock(transcript.blocks[index]!.text, budget - gapAllowance);
1214
+ if (!text) return 0;
1215
+ selected.set(index, text);
1216
+ return budget - text.length - gapAllowance;
1217
+ };
1245
1218
 
1246
- let tailChars = 0;
1247
- for (let index = transcript.blocks.length - 1; index >= 0; index--) {
1248
- if (tailChars >= REDUCED_TRANSCRIPT_TAIL_CHARS) break;
1249
- if (selected.has(index)) continue;
1250
- selected.add(index);
1251
- tailChars += transcript.blocks[index]!.text.length + 2;
1219
+ // Independent allocations ensure an oversized opening cannot consume the ending.
1220
+ let headBudget = REDUCED_TRANSCRIPT_HEAD_CHARS;
1221
+ for (let index = 0; index < transcript.blocks.length && headBudget > 0; index++) {
1222
+ headBudget = selectBlock(index, headBudget);
1252
1223
  }
1253
-
1254
- for (let index = 0; index < transcript.blocks.length; index++) {
1224
+ let tailBudget = REDUCED_TRANSCRIPT_TAIL_CHARS;
1225
+ for (let index = transcript.blocks.length - 1; index >= 0 && tailBudget > 0; index--) {
1226
+ tailBudget = selectBlock(index, tailBudget);
1227
+ }
1228
+ let summaryBudget = maxBodyLength - REDUCED_TRANSCRIPT_HEAD_CHARS - REDUCED_TRANSCRIPT_TAIL_CHARS;
1229
+ for (let index = 0; index < transcript.blocks.length && summaryBudget > 0; index++) {
1255
1230
  if (transcript.blocks[index]!.isSummary) {
1256
- selected.add(index);
1231
+ summaryBudget = selectBlock(index, summaryBudget);
1257
1232
  }
1258
1233
  }
1259
1234
 
1260
- const ordered = [...selected].sort((a, b) => a - b);
1261
- const prefix = [
1262
- transcript.headerText,
1263
- `[Transcript reduced due to length. Full size: ${formatCount(transcript.charLength)} chars.]`,
1264
- ]
1265
- .filter(Boolean)
1266
- .join("\n\n")
1267
- .trim();
1268
-
1269
1235
  const reducedBlocks: string[] = [];
1270
1236
  let previous = -1;
1271
- let bodyLength = 0;
1272
- const maxBodyLength = Math.max(0, REDUCED_TRANSCRIPT_MAX_CHARS - prefix.length - 2);
1273
-
1274
- for (const index of ordered) {
1275
- if (previous >= 0 && index > previous + 1) {
1276
- const gapText = `[... ${index - previous - 1} blocks omitted ...]`;
1277
- const appendedGap = appendReducedBlock(reducedBlocks, gapText, maxBodyLength, bodyLength);
1278
- bodyLength = appendedGap.bodyLength;
1279
- if (!appendedGap.didAppend) break;
1280
- }
1281
-
1282
- const appendedBlock = appendReducedBlock(
1283
- reducedBlocks,
1284
- transcript.blocks[index]!.text,
1285
- maxBodyLength,
1286
- bodyLength,
1287
- );
1288
- bodyLength = appendedBlock.bodyLength;
1289
- if (!appendedBlock.didAppend) break;
1237
+ for (const [index, text] of [...selected].sort(([a], [b]) => a - b)) {
1238
+ if (index > previous + 1) reducedBlocks.push(gap(index - previous - 1));
1239
+ reducedBlocks.push(text);
1290
1240
  previous = index;
1291
1241
  }
1292
-
1293
- if (previous >= 0 && previous < transcript.blocks.length - 1) {
1294
- const appendedTailGap = appendReducedBlock(
1295
- reducedBlocks,
1296
- `[... ${transcript.blocks.length - previous - 1} blocks omitted ...]`,
1297
- maxBodyLength,
1298
- bodyLength,
1299
- );
1300
- bodyLength = appendedTailGap.bodyLength;
1242
+ if (previous < transcript.blocks.length - 1) {
1243
+ reducedBlocks.push(gap(transcript.blocks.length - previous - 1));
1301
1244
  }
1302
-
1303
- const reducedText = [prefix, reducedBlocks.join("\n\n")]
1304
- .filter(Boolean)
1305
- .join("\n\n")
1306
- .slice(0, REDUCED_TRANSCRIPT_MAX_CHARS)
1307
- .trim();
1245
+ const reducedText = [prefix, ...reducedBlocks].join("\n\n");
1308
1246
 
1309
1247
  return {
1310
1248
  text: reducedText,
@@ -1313,32 +1251,17 @@ function prepareTranscriptForFacet(transcript: NormalizedTranscript): PreparedTr
1313
1251
  };
1314
1252
  }
1315
1253
 
1316
- function appendReducedBlock(
1317
- blocks: string[],
1318
- block: string,
1319
- maxBodyLength: number,
1320
- currentBodyLength: number,
1321
- ): { bodyLength: number; didAppend: boolean } {
1322
- if (maxBodyLength <= currentBodyLength) {
1323
- return { bodyLength: currentBodyLength, didAppend: false };
1324
- }
1254
+ function reduceTranscriptBlock(text: string, maxChars: number): string | undefined {
1255
+ if (text.length <= maxChars) return text;
1325
1256
 
1326
- const separatorLength = blocks.length > 0 ? 2 : 0;
1327
- const remaining = maxBodyLength - currentBodyLength - separatorLength;
1328
- if (remaining <= 0) {
1329
- return { bodyLength: currentBodyLength, didAppend: false };
1330
- }
1257
+ const label = text.slice(0, text.indexOf("\n") + 1);
1258
+ const omission = "\n[... message text omitted ...]\n";
1259
+ const remaining = maxChars - label.length - omission.length;
1260
+ if (remaining < 2) return undefined;
1331
1261
 
1332
- const nextBlock = truncateChars(block, remaining);
1333
- if (!nextBlock) {
1334
- return { bodyLength: currentBodyLength, didAppend: false };
1335
- }
1336
-
1337
- blocks.push(nextBlock);
1338
- return {
1339
- bodyLength: currentBodyLength + separatorLength + nextBlock.length,
1340
- didAppend: true,
1341
- };
1262
+ const head = Math.ceil(remaining / 2);
1263
+ const tail = Math.floor(remaining / 2);
1264
+ return `${label}${text.slice(label.length, label.length + head)}${omission}${text.slice(-tail)}`;
1342
1265
  }
1343
1266
 
1344
1267
  async function extractFacet(
@@ -1368,18 +1291,13 @@ async function extractFacet(
1368
1291
  timestamp: Date.now(),
1369
1292
  };
1370
1293
 
1371
- const response = await complete(
1294
+ const response = await selection.modelRegistry.complete(
1372
1295
  selection.model,
1373
1296
  {
1374
1297
  systemPrompt: FACET_SYSTEM_PROMPT,
1375
1298
  messages: [userMessage],
1376
1299
  },
1377
- {
1378
- apiKey: selection.apiKey,
1379
- headers: selection.headers,
1380
- env: selection.env,
1381
- signal,
1382
- },
1300
+ { signal },
1383
1301
  );
1384
1302
 
1385
1303
  if (response.stopReason === "aborted") {
@@ -1541,18 +1459,13 @@ async function synthesizeReport(
1541
1459
  timestamp: Date.now(),
1542
1460
  };
1543
1461
 
1544
- const response = await complete(
1462
+ const response = await selection.modelRegistry.complete(
1545
1463
  selection.model,
1546
1464
  {
1547
1465
  systemPrompt: SYNTHESIS_SYSTEM_PROMPT,
1548
1466
  messages: [userMessage],
1549
1467
  },
1550
- {
1551
- apiKey: selection.apiKey,
1552
- headers: selection.headers,
1553
- env: selection.env,
1554
- signal,
1555
- },
1468
+ { signal },
1556
1469
  );
1557
1470
 
1558
1471
  if (response.stopReason === "aborted") {
@@ -1728,15 +1641,20 @@ function setBorderedLoaderMessage(loader: BorderedLoader, message: string): void
1728
1641
  }
1729
1642
  }
1730
1643
 
1731
- async function getCacheInfo(sessionFile: string | undefined): Promise<SessionCacheInfo> {
1644
+ async function getCacheInfo(target: SessionTarget): Promise<SessionCacheInfo> {
1645
+ const sessionFile = target.path;
1732
1646
  if (!sessionFile) return {};
1733
1647
 
1734
- const cacheKey = hashText(sessionFile);
1648
+ // A live selection can differ from the last entry restored from the same file.
1649
+ // Keep each selected view separate, including the empty (null-leaf) branch.
1650
+ const view = target.manager ? { leafId: target.manager.getLeafId() } : "saved";
1651
+ const cacheKey = hashText(JSON.stringify({ sessionFile, view }));
1735
1652
  try {
1736
1653
  const stats = await fs.stat(sessionFile);
1737
1654
  const fingerprint = hashText(
1738
1655
  JSON.stringify({
1739
1656
  sessionFile,
1657
+ view,
1740
1658
  mtimeMs: stats.mtimeMs,
1741
1659
  size: stats.size,
1742
1660
  schemaVersion: SESSION_FINGERPRINT_VERSION,