negotium 0.2.16 → 0.2.17

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/dist/main.js CHANGED
@@ -1010,6 +1010,70 @@ var init_codex_tree_kill = __esm(() => {
1010
1010
  killOwnedCodexTreesForShutdown = manager.killOwnedTreesForShutdown;
1011
1011
  });
1012
1012
 
1013
+ // ../../packages/core/src/agents/compaction-support.ts
1014
+ function isCjkCodePoint(code) {
1015
+ return code >= 12288 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 4352 && code <= 4607 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 || code >= 131072 && code <= 262143;
1016
+ }
1017
+ function estimateTextTokens(text) {
1018
+ let cjkChars = 0;
1019
+ let cjkUnits = 0;
1020
+ for (let index = 0;index < text.length; index++) {
1021
+ const code = text.codePointAt(index) ?? 0;
1022
+ const wide = code > 65535;
1023
+ if (wide)
1024
+ index++;
1025
+ if (isCjkCodePoint(code)) {
1026
+ cjkChars++;
1027
+ cjkUnits += wide ? 2 : 1;
1028
+ }
1029
+ }
1030
+ const latinUnits = text.length - cjkUnits;
1031
+ return Math.ceil(cjkChars / CHARS_PER_CJK_TOKEN + latinUnits / CHARS_PER_TEXT_TOKEN);
1032
+ }
1033
+ function estimateConversationTokens(messages) {
1034
+ return messages.reduce((sum, message) => sum + 4 + estimateTextTokens(message.content), 0);
1035
+ }
1036
+ var ACTIVE_TASK_TEMPLATE = `You are compressing a long agent conversation so the main agent can continue
1037
+ without losing context. Produce a single concise summary using EXACTLY these
1038
+ section headers, in this order:
1039
+
1040
+ ## Active Task
1041
+ One sentence: what is the agent currently working on?
1042
+
1043
+ ## Goal
1044
+ One or two sentences: the user's overall objective in this session.
1045
+
1046
+ ## Constraints
1047
+ Bulleted list of durable requirements, user preferences, technical limits, or
1048
+ process rules that should continue to govern the work. Skip if none.
1049
+
1050
+ ## Key Decisions
1051
+ Bulleted list of decisions already made that should not be reopened unless the
1052
+ user asks. Include the rationale when it is short and important. Skip if none.
1053
+
1054
+ ## Pending
1055
+ Bulleted list of unresolved items, decisions to make, or work explicitly
1056
+ deferred. Use "(blocked: <reason>)" when applicable.
1057
+
1058
+ ## Next Steps
1059
+ Bulleted list of the concrete next actions the main agent should take after
1060
+ compaction, in likely execution order. Skip if none.
1061
+
1062
+ ## Files
1063
+ Bulleted list of \`absolute/paths\` touched or referenced (read, written,
1064
+ inspected). Skip if none.
1065
+
1066
+ ## Recent context
1067
+ 3-5 bullets capturing the most recent tool calls + their salient outputs.
1068
+ Prefer specifics (paths, line numbers, exit codes, key values) over generic
1069
+ recaps. Skip details that have no bearing on the next step.
1070
+
1071
+ RULES:
1072
+ - Output ONLY the eight sections above, with no preamble or postscript.
1073
+ - Do NOT echo the user's words verbatim - paraphrase tightly.
1074
+ - Do NOT invent file paths or facts not present in the transcript.
1075
+ - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
1076
+
1013
1077
  // ../../packages/core/src/runtime/user-turn-envelope.ts
1014
1078
  function legacyUserTurnEnvelope(prompt, attachments) {
1015
1079
  return attachments?.length ? { prompt, attachments } : { prompt };
@@ -1572,7 +1636,7 @@ var exports_version = {};
1572
1636
  __export(exports_version, {
1573
1637
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
1574
1638
  });
1575
- var NEGOTIUM_VERSION = "0.2.16";
1639
+ var NEGOTIUM_VERSION = "0.2.17";
1576
1640
 
1577
1641
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
1578
1642
  import { spawn } from "child_process";
@@ -6427,6 +6491,7 @@ function maestroProvider(opts) {
6427
6491
  ...opts,
6428
6492
  enableToolSearch: !opts.toolPolicy && opts.enableToolSearch !== false,
6429
6493
  toolResultTruncation: buildMaestroToolResultTruncation(opts),
6494
+ ephemeralSystemPrompt: opts.ephemeralSystemPrompt,
6430
6495
  apiKeyOverrides: resolveMaestroApiKeyOverrides(userId),
6431
6496
  agent: "maestro",
6432
6497
  disallowedTools: buildMaestroDisallowedTools(callerDisallowedTools, opts.toolPolicy),
@@ -11321,70 +11386,6 @@ var init_personal_general = __esm(async () => {
11321
11386
  ]);
11322
11387
  });
11323
11388
 
11324
- // ../../packages/core/src/agents/compaction-support.ts
11325
- function isCjkCodePoint(code) {
11326
- return code >= 12288 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 4352 && code <= 4607 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 || code >= 131072 && code <= 262143;
11327
- }
11328
- function estimateTextTokens(text) {
11329
- let cjkChars = 0;
11330
- let cjkUnits = 0;
11331
- for (let index = 0;index < text.length; index++) {
11332
- const code = text.codePointAt(index) ?? 0;
11333
- const wide = code > 65535;
11334
- if (wide)
11335
- index++;
11336
- if (isCjkCodePoint(code)) {
11337
- cjkChars++;
11338
- cjkUnits += wide ? 2 : 1;
11339
- }
11340
- }
11341
- const latinUnits = text.length - cjkUnits;
11342
- return Math.ceil(cjkChars / CHARS_PER_CJK_TOKEN + latinUnits / CHARS_PER_TEXT_TOKEN);
11343
- }
11344
- function estimateConversationTokens(messages) {
11345
- return messages.reduce((sum, message) => sum + 4 + estimateTextTokens(message.content), 0);
11346
- }
11347
- var ACTIVE_TASK_TEMPLATE = `You are compressing a long agent conversation so the main agent can continue
11348
- without losing context. Produce a single concise summary using EXACTLY these
11349
- section headers, in this order:
11350
-
11351
- ## Active Task
11352
- One sentence: what is the agent currently working on?
11353
-
11354
- ## Goal
11355
- One or two sentences: the user's overall objective in this session.
11356
-
11357
- ## Constraints
11358
- Bulleted list of durable requirements, user preferences, technical limits, or
11359
- process rules that should continue to govern the work. Skip if none.
11360
-
11361
- ## Key Decisions
11362
- Bulleted list of decisions already made that should not be reopened unless the
11363
- user asks. Include the rationale when it is short and important. Skip if none.
11364
-
11365
- ## Pending
11366
- Bulleted list of unresolved items, decisions to make, or work explicitly
11367
- deferred. Use "(blocked: <reason>)" when applicable.
11368
-
11369
- ## Next Steps
11370
- Bulleted list of the concrete next actions the main agent should take after
11371
- compaction, in likely execution order. Skip if none.
11372
-
11373
- ## Files
11374
- Bulleted list of \`absolute/paths\` touched or referenced (read, written,
11375
- inspected). Skip if none.
11376
-
11377
- ## Recent context
11378
- 3-5 bullets capturing the most recent tool calls + their salient outputs.
11379
- Prefer specifics (paths, line numbers, exit codes, key values) over generic
11380
- recaps. Skip details that have no bearing on the next step.
11381
-
11382
- RULES:
11383
- - Output ONLY the eight sections above, with no preamble or postscript.
11384
- - Do NOT echo the user's words verbatim - paraphrase tightly.
11385
- - Do NOT invent file paths or facts not present in the transcript.
11386
- - Keep the entire summary under 1500 words.`, CHARS_PER_TEXT_TOKEN = 3.5, CHARS_PER_CJK_TOKEN = 0.9;
11387
-
11388
11389
  // ../../packages/core/src/prompts/builders.ts
11389
11390
  import { readFileSync as readFileSync13 } from "fs";
11390
11391
  import { resolve as resolve13 } from "path";
@@ -17209,6 +17210,9 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17209
17210
  let pendingSawDelta = false;
17210
17211
  let accumulatedText = "";
17211
17212
  let pendingText = "";
17213
+ let assistantContextTokens = 0;
17214
+ let toolContextTokens = 0;
17215
+ let lastContextProgressAt = 0;
17212
17216
  let lastVisibleMessageId = null;
17213
17217
  const visibleMessageIds = [];
17214
17218
  let lastTaskPanelText = null;
@@ -17227,6 +17231,20 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17227
17231
  model
17228
17232
  });
17229
17233
  };
17234
+ const broadcastContextProgress = (force = false) => {
17235
+ if (silent)
17236
+ return;
17237
+ const now = Date.now();
17238
+ if (!force && lastContextProgressAt > 0 && now - lastContextProgressAt < 250)
17239
+ return;
17240
+ lastContextProgressAt = now;
17241
+ hub.broadcastAiStatus(topicId, {
17242
+ kind: "context_progress",
17243
+ queryId,
17244
+ assistantTokens: assistantContextTokens,
17245
+ toolTokens: toolContextTokens
17246
+ });
17247
+ };
17230
17248
  const nextSyntheticToolUseId = () => `tool-${queryId}-${++syntheticToolCounter}`;
17231
17249
  const bindToolUseId = (providerToolUseId, fallback) => {
17232
17250
  const providerId = normalizeToolUseId(providerToolUseId);
@@ -17336,6 +17354,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17336
17354
  const incoming = event.content;
17337
17355
  accumulatedText += incoming;
17338
17356
  pendingText += incoming;
17357
+ assistantContextTokens = estimateTextTokens(accumulatedText);
17358
+ broadcastContextProgress();
17339
17359
  break;
17340
17360
  }
17341
17361
  case "text":
@@ -17343,10 +17363,15 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17343
17363
  const incoming = event.content;
17344
17364
  accumulatedText += incoming;
17345
17365
  pendingText += incoming;
17366
+ assistantContextTokens = estimateTextTokens(accumulatedText);
17367
+ broadcastContextProgress();
17346
17368
  }
17347
17369
  break;
17348
17370
  case "tool_use":
17349
17371
  emitPendingAssistantMessage();
17372
+ toolContextTokens += estimateTextTokens(`${event.name}
17373
+ ${JSON.stringify(event.input ?? {})}`);
17374
+ broadcastContextProgress(true);
17350
17375
  if (isVisualsShowHtmlTool(event.name)) {
17351
17376
  const input = event.input;
17352
17377
  if (typeof input.html !== "string" || input.html.trim().length === 0) {
@@ -17500,6 +17525,8 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
17500
17525
  }
17501
17526
  break;
17502
17527
  case "tool_result":
17528
+ toolContextTokens += estimateTextTokens(event.content);
17529
+ broadcastContextProgress(true);
17503
17530
  if (isVisualToolResultHandled(event.toolUseId))
17504
17531
  break;
17505
17532
  if (!silent) {
@@ -17900,6 +17927,7 @@ __export(exports_turn_runner, {
17900
17927
  channelTranscriptSpeaker: () => channelTranscriptSpeaker,
17901
17928
  canonicalModelId: () => canonicalModelId,
17902
17929
  buildVideoHtml: () => buildVideoHtml,
17930
+ buildTurnReminderQuery: () => buildTurnReminderQuery,
17903
17931
  buildMermaidHtml: () => buildMermaidHtml,
17904
17932
  buildMentionOnlyChannelPrompt: () => buildMentionOnlyChannelPrompt,
17905
17933
  buildImageHtml: () => buildImageHtml,
@@ -17925,6 +17953,18 @@ function withDefaultPlaywright(configuredMcp, isManager) {
17925
17953
  enabled.add("background-bash");
17926
17954
  return [...enabled];
17927
17955
  }
17956
+ function buildTurnReminderQuery(agent, prompt, reminders) {
17957
+ const reminderPrompt = reminders.join(`
17958
+
17959
+ `);
17960
+ if (!reminderPrompt)
17961
+ return { prompt };
17962
+ if (agent === "maestro")
17963
+ return { prompt, ephemeralSystemPrompt: reminderPrompt };
17964
+ return { prompt: `${prompt}
17965
+
17966
+ ${reminderPrompt}` };
17967
+ }
17928
17968
  function appendSystemMessage(topicId, text2) {
17929
17969
  const message = {
17930
17970
  id: randomUUID16(),
@@ -18872,15 +18912,11 @@ function startAiTurn(params) {
18872
18912
  if (consumePlaywrightUnavailable(userId, topic.title)) {
18873
18913
  turnReminders.push("<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>");
18874
18914
  }
18875
- const effectivePrompt = turnReminders.length > 0 ? `${agentPrompt}
18876
-
18877
- ${turnReminders.join(`
18878
-
18879
- `)}` : agentPrompt;
18915
+ const reminderQuery = buildTurnReminderQuery(agentKind, agentPrompt, turnReminders);
18880
18916
  try {
18881
18917
  yield* runAgent({
18882
18918
  agent: agentKind,
18883
- prompt: effectivePrompt,
18919
+ ...reminderQuery,
18884
18920
  attachments: promptAttachments,
18885
18921
  cwd: workspaceCwd,
18886
18922
  systemPrompt,
@@ -22461,6 +22497,7 @@ __export(exports_src, {
22461
22497
  extractFileEvents: () => extractFileEvents,
22462
22498
  executeVaultCommand: () => executeVaultCommand,
22463
22499
  executeExternalUserTurn: () => executeExternalUserTurn,
22500
+ estimateTextTokens: () => estimateTextTokens,
22464
22501
  errorResult: () => errorResult,
22465
22502
  errMsg: () => errMsg,
22466
22503
  ensurePersonalGeneral: () => ensurePersonalGeneral,
@@ -30914,6 +30951,7 @@ var init_commands = __esm(() => {
30914
30951
  { name: "new", usage: "/new", description: "reset the current session" },
30915
30952
  { name: "compact", usage: "/compact", description: "summarize and shrink context" },
30916
30953
  { name: "status", usage: "/status", description: "show model and token usage" },
30954
+ { name: "context", usage: "/context", description: "show live context breakdown" },
30917
30955
  { name: "model", usage: "/model", description: "choose the model" },
30918
30956
  { name: "effort", usage: "/effort", description: "choose reasoning effort" },
30919
30957
  { name: "topics", usage: "/topics", description: "open topic picker" },
@@ -30942,297 +30980,6 @@ var init_commands = __esm(() => {
30942
30980
  ];
30943
30981
  });
30944
30982
 
30945
- // ../../adapters/terminal/src/path-suggest.ts
30946
- import { execFile as execFile3 } from "child_process";
30947
- import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
30948
- import { homedir as homedir10 } from "os";
30949
- import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
30950
- import { promisify as promisify3 } from "util";
30951
- function indexRecursivePaths(files) {
30952
- const paths = [];
30953
- const directories = new Set;
30954
- for (const relPath of files) {
30955
- const segments = relPath.split("/");
30956
- if (segments.some((segment) => segment.startsWith(".")))
30957
- continue;
30958
- for (let index = 0;index < segments.length - 1; index += 1) {
30959
- const directory = segments.slice(0, index + 1).join("/");
30960
- if (directories.has(directory))
30961
- continue;
30962
- directories.add(directory);
30963
- paths.push({
30964
- relPath: directory,
30965
- isDir: true,
30966
- lowerName: (segments[index] ?? "").toLowerCase()
30967
- });
30968
- }
30969
- paths.push({ relPath, isDir: false, lowerName: (segments.at(-1) ?? "").toLowerCase() });
30970
- }
30971
- return paths;
30972
- }
30973
- async function listFilesWithRipgrep(dir) {
30974
- const args = ["--files", "--max-depth", String(RECURSIVE_MAX_DEPTH)];
30975
- for (const excluded of RECURSIVE_EXCLUDES)
30976
- args.push("-g", `!${excluded}`);
30977
- args.push(".");
30978
- let out = "";
30979
- try {
30980
- const result = await execFileAsync3("rg", args, {
30981
- cwd: dir,
30982
- encoding: "utf8",
30983
- timeout: RECURSIVE_TIMEOUT_MS,
30984
- maxBuffer: 32 * 1024 * 1024
30985
- });
30986
- out = result.stdout;
30987
- } catch (err2) {
30988
- const stdout = err2?.stdout;
30989
- out = typeof stdout === "string" ? stdout : "";
30990
- }
30991
- return out.split(`
30992
- `).filter(Boolean).map((path) => path.replaceAll(sep3, "/").replace(/^\.\//, ""));
30993
- }
30994
- function cacheRecursivePaths(dir, files) {
30995
- const now = Date.now();
30996
- for (const [key, entry] of recursiveCache) {
30997
- if (now - entry.loadedAt >= RECURSIVE_CACHE_TTL_MS)
30998
- recursiveCache.delete(key);
30999
- }
31000
- recursiveCache.delete(dir);
31001
- while (recursiveCache.size >= MAX_RECURSIVE_CACHE_ENTRIES) {
31002
- const oldest = recursiveCache.keys().next().value;
31003
- if (oldest === undefined)
31004
- break;
31005
- recursiveCache.delete(oldest);
31006
- }
31007
- recursiveCache.set(dir, {
31008
- loadedAt: now,
31009
- paths: indexRecursivePaths(files),
31010
- matches: new Map
31011
- });
31012
- }
31013
- function loadFilesRecursive(dir, loader) {
31014
- const cached = recursiveCache.get(dir);
31015
- if (cached && Date.now() - cached.loadedAt < RECURSIVE_CACHE_TTL_MS) {
31016
- recursiveCache.delete(dir);
31017
- recursiveCache.set(dir, cached);
31018
- return Promise.resolve(false);
31019
- }
31020
- const active2 = recursiveSearches.get(dir);
31021
- if (active2)
31022
- return active2;
31023
- const search = (async () => {
31024
- try {
31025
- cacheRecursivePaths(dir, await loader(dir));
31026
- } catch {
31027
- cacheRecursivePaths(dir, []);
31028
- }
31029
- return true;
31030
- })().finally(() => recursiveSearches.delete(dir));
31031
- recursiveSearches.set(dir, search);
31032
- return search;
31033
- }
31034
- function activeAtToken(lineText, col) {
31035
- const upto = lineText.slice(0, col);
31036
- const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
31037
- if (!match)
31038
- return null;
31039
- const frag = match[1] ?? "";
31040
- return { start: col - frag.length - 1, frag };
31041
- }
31042
- function resolveFragment(frag) {
31043
- const home = homedir10();
31044
- let path;
31045
- if (frag === "" || frag === "~")
31046
- path = home;
31047
- else if (frag === "~/")
31048
- path = home;
31049
- else if (frag.startsWith("~/"))
31050
- path = join40(home, frag.slice(2));
31051
- else if (frag.startsWith("/"))
31052
- path = frag;
31053
- else
31054
- path = join40(home, frag);
31055
- if (frag.endsWith("/") || frag === "" || frag === "~") {
31056
- return { dir: path, prefix: "" };
31057
- }
31058
- return { dir: dirname19(path), prefix: basename11(path) };
31059
- }
31060
- function toToken(fullPath, isDir) {
31061
- const home = homedir10();
31062
- let shown = fullPath;
31063
- if (fullPath === home)
31064
- shown = "~";
31065
- else if (fullPath.startsWith(`${home}/`))
31066
- shown = `~/${fullPath.slice(home.length + 1)}`;
31067
- return `@${shown}${isDir ? "/" : ""}`;
31068
- }
31069
- function rankAndSlice(dir, candidates) {
31070
- candidates.sort((a, b) => {
31071
- if (a.isPrefixMatch !== b.isPrefixMatch)
31072
- return a.isPrefixMatch ? -1 : 1;
31073
- if (a.isDir !== b.isDir)
31074
- return a.isDir ? -1 : 1;
31075
- const depthA = a.relPath.split("/").length;
31076
- const depthB = b.relPath.split("/").length;
31077
- if (depthA !== depthB)
31078
- return depthA - depthB;
31079
- return a.relPath.localeCompare(b.relPath);
31080
- });
31081
- return candidates.slice(0, MAX_SUGGESTIONS).map(({ relPath, isDir }) => ({
31082
- label: `${relPath}${isDir ? "/" : ""}`,
31083
- value: toToken(join40(dir, relPath), isDir),
31084
- isDir
31085
- }));
31086
- }
31087
- function recursiveCandidates(dir, lowerPrefix, seen) {
31088
- const entry = recursiveCache.get(dir);
31089
- if (!entry)
31090
- return [];
31091
- let matches = entry.matches.get(lowerPrefix);
31092
- if (!matches) {
31093
- matches = entry.paths.filter((path) => path.lowerName.includes(lowerPrefix)).map((path) => ({
31094
- relPath: path.relPath,
31095
- isDir: path.isDir,
31096
- isPrefixMatch: path.lowerName.startsWith(lowerPrefix)
31097
- }));
31098
- if (entry.matches.size >= MAX_MATCH_CACHE_ENTRIES) {
31099
- const oldest = entry.matches.keys().next().value;
31100
- if (oldest !== undefined)
31101
- entry.matches.delete(oldest);
31102
- }
31103
- entry.matches.set(lowerPrefix, matches);
31104
- } else {
31105
- entry.matches.delete(lowerPrefix);
31106
- entry.matches.set(lowerPrefix, matches);
31107
- }
31108
- return matches.filter((candidate) => {
31109
- if (seen.has(candidate.relPath))
31110
- return false;
31111
- seen.add(candidate.relPath);
31112
- return true;
31113
- });
31114
- }
31115
- function isRecursivePathQuery(lineText, col) {
31116
- const token = activeAtToken(lineText, col);
31117
- if (!token)
31118
- return false;
31119
- const { prefix } = resolveFragment(token.frag);
31120
- return !prefix.startsWith(".") && prefix.length >= RECURSIVE_MIN_PREFIX_LEN;
31121
- }
31122
- async function warmPathSuggestions(lineText, col, loader = listFilesWithRipgrep) {
31123
- if (!isRecursivePathQuery(lineText, col))
31124
- return false;
31125
- const token = activeAtToken(lineText, col);
31126
- if (!token)
31127
- return false;
31128
- const { dir } = resolveFragment(token.frag);
31129
- try {
31130
- if (!statSync19(dir).isDirectory())
31131
- return false;
31132
- } catch {
31133
- return false;
31134
- }
31135
- return loadFilesRecursive(dir, loader);
31136
- }
31137
- function pathSuggestions(lineText, col) {
31138
- const token = activeAtToken(lineText, col);
31139
- if (!token)
31140
- return null;
31141
- const { dir, prefix } = resolveFragment(token.frag);
31142
- let entries;
31143
- try {
31144
- entries = readdirSync14(dir, { withFileTypes: true });
31145
- } catch {
31146
- return { start: token.start, end: col, items: [], truncated: 0, searching: false };
31147
- }
31148
- const lowerPrefix = prefix.toLowerCase();
31149
- const showHidden = prefix.startsWith(".");
31150
- const candidates = entries.filter((entry) => {
31151
- const name = entry.name;
31152
- if (!showHidden && name.startsWith("."))
31153
- return false;
31154
- if (lowerPrefix === "")
31155
- return true;
31156
- const lowerName = name.toLowerCase();
31157
- return showHidden ? lowerName.startsWith(lowerPrefix) : lowerName.includes(lowerPrefix);
31158
- }).map((entry) => {
31159
- let isDir = entry.isDirectory();
31160
- if (!isDir && entry.isSymbolicLink()) {
31161
- try {
31162
- isDir = statSync19(join40(dir, entry.name)).isDirectory();
31163
- } catch {}
31164
- }
31165
- return {
31166
- relPath: entry.name,
31167
- isDir,
31168
- isPrefixMatch: entry.name.toLowerCase().startsWith(lowerPrefix)
31169
- };
31170
- });
31171
- if (!showHidden && lowerPrefix.length >= RECURSIVE_MIN_PREFIX_LEN) {
31172
- const seen = new Set(candidates.map((c) => c.relPath));
31173
- candidates.push(...recursiveCandidates(dir, lowerPrefix, seen));
31174
- }
31175
- const items = rankAndSlice(dir, candidates);
31176
- return {
31177
- start: token.start,
31178
- end: col,
31179
- items,
31180
- truncated: Math.max(0, candidates.length - items.length),
31181
- searching: recursiveSearches.has(dir)
31182
- };
31183
- }
31184
- function fragmentToAbsolutePath(frag) {
31185
- const home = homedir10();
31186
- if (frag === "~" || frag === "~/")
31187
- return home;
31188
- if (frag.startsWith("~/"))
31189
- return join40(home, frag.slice(2));
31190
- if (frag.startsWith("/"))
31191
- return frag;
31192
- return join40(home, frag);
31193
- }
31194
- function fragmentResolves(frag) {
31195
- try {
31196
- return existsSync33(fragmentToAbsolutePath(frag));
31197
- } catch {
31198
- return false;
31199
- }
31200
- }
31201
- function stripResolvedPathTriggers(text2) {
31202
- return text2.replaceAll(AT_PATH_TOKEN, (whole, lead, frag) => {
31203
- if (fragmentResolves(frag))
31204
- return `${lead}${frag}`;
31205
- const pathPart = frag.replace(TRAILING_PUNCT, "");
31206
- if (pathPart && pathPart !== frag && fragmentResolves(pathPart)) {
31207
- return `${lead}${pathPart}${frag.slice(pathPart.length)}`;
31208
- }
31209
- return whole;
31210
- });
31211
- }
31212
- function completePathToken(lineText, col, suggestion, options = {}) {
31213
- const token = activeAtToken(lineText, col);
31214
- if (!token)
31215
- return null;
31216
- let tokenEnd = col;
31217
- while (tokenEnd < lineText.length && !/\s/.test(lineText[tokenEnd] ?? " "))
31218
- tokenEnd += 1;
31219
- const trailingText = lineText.slice(col, tokenEnd);
31220
- const pathTail = trailingText.replace(TRAILING_PUNCT, "");
31221
- const end = col + pathTail.length;
31222
- const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
31223
- const line = lineText.slice(0, token.start) + replacement + lineText.slice(end);
31224
- return { line, col: token.start + replacement.length };
31225
- }
31226
- var MAX_SUGGESTIONS = 8, RECURSIVE_MIN_PREFIX_LEN = 4, RECURSIVE_MAX_DEPTH = 10, RECURSIVE_TIMEOUT_MS = 2000, RECURSIVE_CACHE_TTL_MS = 30000, MAX_RECURSIVE_CACHE_ENTRIES = 3, MAX_MATCH_CACHE_ENTRIES = 24, RECURSIVE_EXCLUDES, recursiveCache, recursiveSearches, execFileAsync3, AT_PATH_TOKEN, TRAILING_PUNCT;
31227
- var init_path_suggest = __esm(() => {
31228
- RECURSIVE_EXCLUDES = ["node_modules", ".git", ".cache", "dist", "build", "Library", ".Trash"];
31229
- recursiveCache = new Map;
31230
- recursiveSearches = new Map;
31231
- execFileAsync3 = promisify3(execFile3);
31232
- AT_PATH_TOKEN = /(^|[\s([{<])@([^\s]+)/g;
31233
- TRAILING_PUNCT = /[.,;:!?)\]}>"'`]+$/;
31234
- });
31235
-
31236
30983
  // ../../adapters/terminal/src/subagent-graph.ts
31237
30984
  import {
31238
30985
  layoutTerminalGraph,
@@ -31867,6 +31614,19 @@ function applyAiStatus(state, topicId, raw, createdAt) {
31867
31614
  tools: []
31868
31615
  });
31869
31616
  }
31617
+ if (kind === "context_progress") {
31618
+ if (isStaleTerminalStatus(current3, status))
31619
+ return state;
31620
+ return setActivity(state, topicId, {
31621
+ ...liveCurrent,
31622
+ running: true,
31623
+ queryId: typeof status.queryId === "string" ? status.queryId : current3.queryId,
31624
+ contextProgress: {
31625
+ assistantTokens: Math.max(0, Number(status.assistantTokens) || 0),
31626
+ toolTokens: Math.max(0, Number(status.toolTokens) || 0)
31627
+ }
31628
+ });
31629
+ }
31870
31630
  if (kind === "ai_done") {
31871
31631
  if (isStaleTerminalStatus(current3, status))
31872
31632
  return state;
@@ -31980,6 +31740,356 @@ var init_state2 = __esm(async () => {
31980
31740
  await init_src();
31981
31741
  });
31982
31742
 
31743
+ // ../../adapters/terminal/src/context-usage.ts
31744
+ function latestUsageMessage(messages) {
31745
+ for (let index = messages.length - 1;index >= 0; index--) {
31746
+ const usage = messages[index]?.authorId === "ai" ? messages[index]?.usage : undefined;
31747
+ if (usage?.context !== undefined && usage.contextWindow)
31748
+ return { index, usage };
31749
+ }
31750
+ return;
31751
+ }
31752
+ function estimateMessages(messages, predicate) {
31753
+ return messages.reduce((sum, message) => predicate(message) ? sum + 4 + estimateTextTokens(message.text) : sum, 0);
31754
+ }
31755
+ function activeContextBreakdown(state) {
31756
+ const topic = activeTopic(state);
31757
+ if (!topic)
31758
+ return;
31759
+ const messages = activeMessages(state);
31760
+ const latest = latestUsageMessage(messages);
31761
+ const stored = state.topicUsage[topic.id]?.currentSession;
31762
+ const confirmed = latest?.usage.context ?? stored?.contextTokens;
31763
+ const contextWindow = latest?.usage.contextWindow ?? stored?.contextWindow;
31764
+ if (confirmed === undefined || !contextWindow)
31765
+ return;
31766
+ const activity = state.activity[topic.id];
31767
+ if (!activity?.running) {
31768
+ return {
31769
+ context: confirmed,
31770
+ contextWindow,
31771
+ confirmed,
31772
+ user: 0,
31773
+ assistant: 0,
31774
+ tools: 0,
31775
+ free: Math.max(0, contextWindow - confirmed),
31776
+ estimated: false
31777
+ };
31778
+ }
31779
+ const afterBaseline = latest ? messages.slice(latest.index + 1) : stored?.timestamp ? messages.filter((message) => message.createdAt > stored.timestamp) : messages;
31780
+ const user = estimateMessages(afterBaseline, (message) => message.authorId !== "ai" && message.kind !== "system");
31781
+ const assistant = activity.contextProgress?.assistantTokens ?? estimateMessages(afterBaseline, (message) => message.authorId === "ai" && message.kind !== "tool");
31782
+ const tools = activity.contextProgress?.toolTokens ?? estimateMessages(afterBaseline, (message) => message.kind === "tool");
31783
+ const context = confirmed + user + assistant + tools;
31784
+ return {
31785
+ context,
31786
+ contextWindow,
31787
+ confirmed,
31788
+ user,
31789
+ assistant,
31790
+ tools,
31791
+ free: Math.max(0, contextWindow - context),
31792
+ estimated: true
31793
+ };
31794
+ }
31795
+ var init_context_usage = __esm(async () => {
31796
+ await __promiseAll([
31797
+ init_src(),
31798
+ init_state2()
31799
+ ]);
31800
+ });
31801
+
31802
+ // ../../adapters/terminal/src/path-suggest.ts
31803
+ import { execFile as execFile3 } from "child_process";
31804
+ import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync19 } from "fs";
31805
+ import { homedir as homedir10 } from "os";
31806
+ import { basename as basename11, dirname as dirname19, join as join40, sep as sep3 } from "path";
31807
+ import { promisify as promisify3 } from "util";
31808
+ function indexRecursivePaths(files) {
31809
+ const paths = [];
31810
+ const directories = new Set;
31811
+ for (const relPath of files) {
31812
+ const segments = relPath.split("/");
31813
+ if (segments.some((segment) => segment.startsWith(".")))
31814
+ continue;
31815
+ for (let index = 0;index < segments.length - 1; index += 1) {
31816
+ const directory = segments.slice(0, index + 1).join("/");
31817
+ if (directories.has(directory))
31818
+ continue;
31819
+ directories.add(directory);
31820
+ paths.push({
31821
+ relPath: directory,
31822
+ isDir: true,
31823
+ lowerName: (segments[index] ?? "").toLowerCase()
31824
+ });
31825
+ }
31826
+ paths.push({ relPath, isDir: false, lowerName: (segments.at(-1) ?? "").toLowerCase() });
31827
+ }
31828
+ return paths;
31829
+ }
31830
+ async function listFilesWithRipgrep(dir) {
31831
+ const args = ["--files", "--max-depth", String(RECURSIVE_MAX_DEPTH)];
31832
+ for (const excluded of RECURSIVE_EXCLUDES)
31833
+ args.push("-g", `!${excluded}`);
31834
+ args.push(".");
31835
+ let out = "";
31836
+ try {
31837
+ const result = await execFileAsync3("rg", args, {
31838
+ cwd: dir,
31839
+ encoding: "utf8",
31840
+ timeout: RECURSIVE_TIMEOUT_MS,
31841
+ maxBuffer: 32 * 1024 * 1024
31842
+ });
31843
+ out = result.stdout;
31844
+ } catch (err2) {
31845
+ const stdout = err2?.stdout;
31846
+ out = typeof stdout === "string" ? stdout : "";
31847
+ }
31848
+ return out.split(`
31849
+ `).filter(Boolean).map((path) => path.replaceAll(sep3, "/").replace(/^\.\//, ""));
31850
+ }
31851
+ function cacheRecursivePaths(dir, files) {
31852
+ const now = Date.now();
31853
+ for (const [key, entry] of recursiveCache) {
31854
+ if (now - entry.loadedAt >= RECURSIVE_CACHE_TTL_MS)
31855
+ recursiveCache.delete(key);
31856
+ }
31857
+ recursiveCache.delete(dir);
31858
+ while (recursiveCache.size >= MAX_RECURSIVE_CACHE_ENTRIES) {
31859
+ const oldest = recursiveCache.keys().next().value;
31860
+ if (oldest === undefined)
31861
+ break;
31862
+ recursiveCache.delete(oldest);
31863
+ }
31864
+ recursiveCache.set(dir, {
31865
+ loadedAt: now,
31866
+ paths: indexRecursivePaths(files),
31867
+ matches: new Map
31868
+ });
31869
+ }
31870
+ function loadFilesRecursive(dir, loader) {
31871
+ const cached = recursiveCache.get(dir);
31872
+ if (cached && Date.now() - cached.loadedAt < RECURSIVE_CACHE_TTL_MS) {
31873
+ recursiveCache.delete(dir);
31874
+ recursiveCache.set(dir, cached);
31875
+ return Promise.resolve(false);
31876
+ }
31877
+ const active2 = recursiveSearches.get(dir);
31878
+ if (active2)
31879
+ return active2;
31880
+ const search = (async () => {
31881
+ try {
31882
+ cacheRecursivePaths(dir, await loader(dir));
31883
+ } catch {
31884
+ cacheRecursivePaths(dir, []);
31885
+ }
31886
+ return true;
31887
+ })().finally(() => recursiveSearches.delete(dir));
31888
+ recursiveSearches.set(dir, search);
31889
+ return search;
31890
+ }
31891
+ function activeAtToken(lineText, col) {
31892
+ const upto = lineText.slice(0, col);
31893
+ const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
31894
+ if (!match)
31895
+ return null;
31896
+ const frag = match[1] ?? "";
31897
+ return { start: col - frag.length - 1, frag };
31898
+ }
31899
+ function resolveFragment(frag) {
31900
+ const home = homedir10();
31901
+ let path;
31902
+ if (frag === "" || frag === "~")
31903
+ path = home;
31904
+ else if (frag === "~/")
31905
+ path = home;
31906
+ else if (frag.startsWith("~/"))
31907
+ path = join40(home, frag.slice(2));
31908
+ else if (frag.startsWith("/"))
31909
+ path = frag;
31910
+ else
31911
+ path = join40(home, frag);
31912
+ if (frag.endsWith("/") || frag === "" || frag === "~") {
31913
+ return { dir: path, prefix: "" };
31914
+ }
31915
+ return { dir: dirname19(path), prefix: basename11(path) };
31916
+ }
31917
+ function toToken(fullPath, isDir) {
31918
+ const home = homedir10();
31919
+ let shown = fullPath;
31920
+ if (fullPath === home)
31921
+ shown = "~";
31922
+ else if (fullPath.startsWith(`${home}/`))
31923
+ shown = `~/${fullPath.slice(home.length + 1)}`;
31924
+ return `@${shown}${isDir ? "/" : ""}`;
31925
+ }
31926
+ function rankAndSlice(dir, candidates) {
31927
+ candidates.sort((a, b) => {
31928
+ if (a.isPrefixMatch !== b.isPrefixMatch)
31929
+ return a.isPrefixMatch ? -1 : 1;
31930
+ if (a.isDir !== b.isDir)
31931
+ return a.isDir ? -1 : 1;
31932
+ const depthA = a.relPath.split("/").length;
31933
+ const depthB = b.relPath.split("/").length;
31934
+ if (depthA !== depthB)
31935
+ return depthA - depthB;
31936
+ return a.relPath.localeCompare(b.relPath);
31937
+ });
31938
+ return candidates.slice(0, MAX_SUGGESTIONS).map(({ relPath, isDir }) => ({
31939
+ label: `${relPath}${isDir ? "/" : ""}`,
31940
+ value: toToken(join40(dir, relPath), isDir),
31941
+ isDir
31942
+ }));
31943
+ }
31944
+ function recursiveCandidates(dir, lowerPrefix, seen) {
31945
+ const entry = recursiveCache.get(dir);
31946
+ if (!entry)
31947
+ return [];
31948
+ let matches = entry.matches.get(lowerPrefix);
31949
+ if (!matches) {
31950
+ matches = entry.paths.filter((path) => path.lowerName.includes(lowerPrefix)).map((path) => ({
31951
+ relPath: path.relPath,
31952
+ isDir: path.isDir,
31953
+ isPrefixMatch: path.lowerName.startsWith(lowerPrefix)
31954
+ }));
31955
+ if (entry.matches.size >= MAX_MATCH_CACHE_ENTRIES) {
31956
+ const oldest = entry.matches.keys().next().value;
31957
+ if (oldest !== undefined)
31958
+ entry.matches.delete(oldest);
31959
+ }
31960
+ entry.matches.set(lowerPrefix, matches);
31961
+ } else {
31962
+ entry.matches.delete(lowerPrefix);
31963
+ entry.matches.set(lowerPrefix, matches);
31964
+ }
31965
+ return matches.filter((candidate) => {
31966
+ if (seen.has(candidate.relPath))
31967
+ return false;
31968
+ seen.add(candidate.relPath);
31969
+ return true;
31970
+ });
31971
+ }
31972
+ function isRecursivePathQuery(lineText, col) {
31973
+ const token = activeAtToken(lineText, col);
31974
+ if (!token)
31975
+ return false;
31976
+ const { prefix } = resolveFragment(token.frag);
31977
+ return !prefix.startsWith(".") && prefix.length >= RECURSIVE_MIN_PREFIX_LEN;
31978
+ }
31979
+ async function warmPathSuggestions(lineText, col, loader = listFilesWithRipgrep) {
31980
+ if (!isRecursivePathQuery(lineText, col))
31981
+ return false;
31982
+ const token = activeAtToken(lineText, col);
31983
+ if (!token)
31984
+ return false;
31985
+ const { dir } = resolveFragment(token.frag);
31986
+ try {
31987
+ if (!statSync19(dir).isDirectory())
31988
+ return false;
31989
+ } catch {
31990
+ return false;
31991
+ }
31992
+ return loadFilesRecursive(dir, loader);
31993
+ }
31994
+ function pathSuggestions(lineText, col) {
31995
+ const token = activeAtToken(lineText, col);
31996
+ if (!token)
31997
+ return null;
31998
+ const { dir, prefix } = resolveFragment(token.frag);
31999
+ let entries;
32000
+ try {
32001
+ entries = readdirSync14(dir, { withFileTypes: true });
32002
+ } catch {
32003
+ return { start: token.start, end: col, items: [], truncated: 0, searching: false };
32004
+ }
32005
+ const lowerPrefix = prefix.toLowerCase();
32006
+ const showHidden = prefix.startsWith(".");
32007
+ const candidates = entries.filter((entry) => {
32008
+ const name = entry.name;
32009
+ if (!showHidden && name.startsWith("."))
32010
+ return false;
32011
+ if (lowerPrefix === "")
32012
+ return true;
32013
+ const lowerName = name.toLowerCase();
32014
+ return showHidden ? lowerName.startsWith(lowerPrefix) : lowerName.includes(lowerPrefix);
32015
+ }).map((entry) => {
32016
+ let isDir = entry.isDirectory();
32017
+ if (!isDir && entry.isSymbolicLink()) {
32018
+ try {
32019
+ isDir = statSync19(join40(dir, entry.name)).isDirectory();
32020
+ } catch {}
32021
+ }
32022
+ return {
32023
+ relPath: entry.name,
32024
+ isDir,
32025
+ isPrefixMatch: entry.name.toLowerCase().startsWith(lowerPrefix)
32026
+ };
32027
+ });
32028
+ if (!showHidden && lowerPrefix.length >= RECURSIVE_MIN_PREFIX_LEN) {
32029
+ const seen = new Set(candidates.map((c) => c.relPath));
32030
+ candidates.push(...recursiveCandidates(dir, lowerPrefix, seen));
32031
+ }
32032
+ const items = rankAndSlice(dir, candidates);
32033
+ return {
32034
+ start: token.start,
32035
+ end: col,
32036
+ items,
32037
+ truncated: Math.max(0, candidates.length - items.length),
32038
+ searching: recursiveSearches.has(dir)
32039
+ };
32040
+ }
32041
+ function fragmentToAbsolutePath(frag) {
32042
+ const home = homedir10();
32043
+ if (frag === "~" || frag === "~/")
32044
+ return home;
32045
+ if (frag.startsWith("~/"))
32046
+ return join40(home, frag.slice(2));
32047
+ if (frag.startsWith("/"))
32048
+ return frag;
32049
+ return join40(home, frag);
32050
+ }
32051
+ function fragmentResolves(frag) {
32052
+ try {
32053
+ return existsSync33(fragmentToAbsolutePath(frag));
32054
+ } catch {
32055
+ return false;
32056
+ }
32057
+ }
32058
+ function stripResolvedPathTriggers(text2) {
32059
+ return text2.replaceAll(AT_PATH_TOKEN, (whole, lead, frag) => {
32060
+ if (fragmentResolves(frag))
32061
+ return `${lead}${frag}`;
32062
+ const pathPart = frag.replace(TRAILING_PUNCT, "");
32063
+ if (pathPart && pathPart !== frag && fragmentResolves(pathPart)) {
32064
+ return `${lead}${pathPart}${frag.slice(pathPart.length)}`;
32065
+ }
32066
+ return whole;
32067
+ });
32068
+ }
32069
+ function completePathToken(lineText, col, suggestion, options = {}) {
32070
+ const token = activeAtToken(lineText, col);
32071
+ if (!token)
32072
+ return null;
32073
+ let tokenEnd = col;
32074
+ while (tokenEnd < lineText.length && !/\s/.test(lineText[tokenEnd] ?? " "))
32075
+ tokenEnd += 1;
32076
+ const trailingText = lineText.slice(col, tokenEnd);
32077
+ const pathTail = trailingText.replace(TRAILING_PUNCT, "");
32078
+ const end = col + pathTail.length;
32079
+ const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
32080
+ const line = lineText.slice(0, token.start) + replacement + lineText.slice(end);
32081
+ return { line, col: token.start + replacement.length };
32082
+ }
32083
+ var MAX_SUGGESTIONS = 8, RECURSIVE_MIN_PREFIX_LEN = 4, RECURSIVE_MAX_DEPTH = 10, RECURSIVE_TIMEOUT_MS = 2000, RECURSIVE_CACHE_TTL_MS = 30000, MAX_RECURSIVE_CACHE_ENTRIES = 3, MAX_MATCH_CACHE_ENTRIES = 24, RECURSIVE_EXCLUDES, recursiveCache, recursiveSearches, execFileAsync3, AT_PATH_TOKEN, TRAILING_PUNCT;
32084
+ var init_path_suggest = __esm(() => {
32085
+ RECURSIVE_EXCLUDES = ["node_modules", ".git", ".cache", "dist", "build", "Library", ".Trash"];
32086
+ recursiveCache = new Map;
32087
+ recursiveSearches = new Map;
32088
+ execFileAsync3 = promisify3(execFile3);
32089
+ AT_PATH_TOKEN = /(^|[\s([{<])@([^\s]+)/g;
32090
+ TRAILING_PUNCT = /[.,;:!?)\]}>"'`]+$/;
32091
+ });
32092
+
31983
32093
  // ../../adapters/terminal/src/terminal-width.ts
31984
32094
  function stripAnsi(value) {
31985
32095
  return value.replace(new RegExp("\\u001b\\]8;[^\\u0007\\u001b]*(?:\\u0007|\\u001b\\\\)", "g"), "").replace(new RegExp("\\u001b\\[[0-?]*[ -/]*[@-~]", "g"), "");
@@ -32811,9 +32921,9 @@ function helpLines() {
32811
32921
  line(" Esc/Ctrl-C stop active turn \xB7 Ctrl-C twice when idle to quit"),
32812
32922
  line(""),
32813
32923
  line(" Commands", { fg: theme.cyan, bold: true }),
32814
- line(" /new /model /effort /topics /public /private"),
32815
- line(" /fork /spawn /del /copy"),
32816
- line(" /abort /help /quit", { fg: theme.muted })
32924
+ line(" /new /model /effort /status /context"),
32925
+ line(" /topics /public /private /fork /spawn"),
32926
+ line(" /del /copy /abort /help /quit", { fg: theme.muted })
32817
32927
  ];
32818
32928
  }
32819
32929
  function tokenCount(value) {
@@ -32828,19 +32938,17 @@ function tokenCount(value) {
32828
32938
  function latestActiveUsage(state) {
32829
32939
  return activeMessages(state).slice().reverse().find((message) => message.authorId === "ai" && message.usage)?.usage;
32830
32940
  }
32831
- function activeContextUsage(state) {
32832
- const usage = latestActiveUsage(state);
32833
- if (usage?.context !== undefined && usage.contextWindow) {
32834
- return { context: usage.context, contextWindow: usage.contextWindow };
32835
- }
32836
- const topic = activeTopic(state);
32837
- const current3 = topic ? state.topicUsage[topic.id]?.currentSession : undefined;
32838
- return current3 ? { context: current3.contextTokens, contextWindow: current3.contextWindow } : undefined;
32941
+ function contextUsageColor(ratio) {
32942
+ if (ratio !== undefined && ratio >= 90)
32943
+ return theme.red;
32944
+ if (ratio !== undefined && ratio >= 80)
32945
+ return theme.amber;
32946
+ return theme.text;
32839
32947
  }
32840
32948
  function statusLines(state) {
32841
32949
  const topic = activeTopic(state);
32842
32950
  const usage = latestActiveUsage(state);
32843
- const contextUsage = activeContextUsage(state);
32951
+ const contextUsage = activeContextBreakdown(state);
32844
32952
  const total = topic ? state.topicUsage[topic.id] : undefined;
32845
32953
  const hasTopicUsage = total !== undefined && total.queries > 0;
32846
32954
  const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
@@ -32852,8 +32960,8 @@ function statusLines(state) {
32852
32960
  line(` Model ${effectiveTopicModel(topic)}`),
32853
32961
  line(` Effort ${topic?.effectiveEffort ?? topic?.defaultEffort ?? "-"}`),
32854
32962
  line(""),
32855
- line(` Context ${tokenCount(contextUsage?.context)} / ${tokenCount(contextUsage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: ratio !== undefined && ratio >= 80 ? theme.amber : theme.text }),
32856
- line(" Measured on the latest model request", { fg: theme.muted, dim: true }),
32963
+ line(` Context ${contextUsage?.estimated ? "~" : ""}${tokenCount(contextUsage?.context)} / ${tokenCount(contextUsage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: contextUsageColor(ratio) }),
32964
+ line(contextUsage?.estimated ? " Live estimate; replaced by provider usage when the turn finishes" : " Measured on the latest model request", { fg: theme.muted, dim: true }),
32857
32965
  line(""),
32858
32966
  line(` Last turn input ${tokenCount(usage?.input)} \xB7 output ${tokenCount(usage?.output)}`),
32859
32967
  line(` Cache read ${tokenCount(usage?.cachedInput)}`),
@@ -32873,6 +32981,31 @@ function statusLines(state) {
32873
32981
  line(" Esc close", { fg: theme.muted })
32874
32982
  ];
32875
32983
  }
32984
+ function contextLines(state) {
32985
+ const usage = activeContextBreakdown(state);
32986
+ const ratio = usage ? Math.round(usage.context / usage.contextWindow * 100) : undefined;
32987
+ const value = (tokens) => tokenCount(tokens);
32988
+ return [
32989
+ line(" Context", { fg: theme.accent, bold: true }),
32990
+ line(""),
32991
+ line(` ${usage?.estimated ? "Estimated" : "Current"} ${usage?.estimated ? "~" : ""}${value(usage?.context)} / ${value(usage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: contextUsageColor(ratio) }),
32992
+ line(""),
32993
+ line(" Breakdown", { fg: theme.cyan, bold: true }),
32994
+ line(` Confirmed ${value(usage?.confirmed)}`),
32995
+ line(` New user ${value(usage?.user)}`),
32996
+ line(` Assistant ${value(usage?.assistant)}`),
32997
+ line(` Tools ${value(usage?.tools)}`),
32998
+ line(` Free ${value(usage?.free)}`),
32999
+ line(""),
33000
+ line(usage?.estimated ? " Live estimate; final provider usage replaces it" : " Provider-confirmed latest request; live categories appear during a turn", { fg: theme.muted, dim: true }),
33001
+ line(" System prompt and tool schemas are included in Confirmed", {
33002
+ fg: theme.muted,
33003
+ dim: true
33004
+ }),
33005
+ line(""),
33006
+ line(" Esc close", { fg: theme.muted })
33007
+ ];
33008
+ }
32876
33009
  function subagentTreePrefix(topic, topics, rootTopicId) {
32877
33010
  if (!topic.isSubagent || topic.id === rootTopicId)
32878
33011
  return "";
@@ -33241,6 +33374,8 @@ function conversationLines(state, width, height, animationFrame = 0, nowMs = ter
33241
33374
  return helpLines().slice(0, height);
33242
33375
  if (state.overlay === "status")
33243
33376
  return statusLines(state).slice(0, height);
33377
+ if (state.overlay === "context")
33378
+ return contextLines(state).slice(0, height);
33244
33379
  if (state.overlay === "topics")
33245
33380
  return topicOverlayLines(state, width, height, animationFrame).slice(0, height);
33246
33381
  if (state.overlay === "subagents")
@@ -33461,35 +33596,71 @@ function footerStatusText(state) {
33461
33596
  function footerUsageText(state) {
33462
33597
  const topic = activeTopic(state);
33463
33598
  if (!topic)
33464
- return [""];
33465
- const contextUsage = activeContextUsage(state);
33599
+ return [{ text: "", spans: [] }];
33600
+ const contextUsage = activeContextBreakdown(state);
33466
33601
  const total = state.topicUsage[topic.id];
33467
33602
  const ratio = contextUsage?.context !== undefined && contextUsage.contextWindow ? Math.round(contextUsage.context / contextUsage.contextWindow * 100) : undefined;
33468
- const context = contextUsage?.context !== undefined && contextUsage.contextWindow ? `${tokenCount(contextUsage.context)}/${tokenCount(contextUsage.contextWindow)} ${ratio}%` : "";
33603
+ const context = contextUsage?.context !== undefined && contextUsage.contextWindow ? `${contextUsage.estimated ? "~" : ""}${tokenCount(contextUsage.context)}/${tokenCount(contextUsage.contextWindow)} ${ratio}%` : "";
33469
33604
  const hasTopicUsage = total !== undefined && total.queries > 0;
33470
33605
  const cumulative = hasTopicUsage ? `\u03A3 ${tokenCount(total.inputTokens)} in/${tokenCount(total.outputTokens)} out` : "";
33471
33606
  const cache = hasTopicUsage && total.cacheReadInputTokens ? `cache ${tokenCount(total.cacheReadInputTokens)}` : "";
33472
33607
  const cost = hasTopicUsage ? `est $${total.estimatedCostUsd.toFixed(2)}` : "";
33473
- const parts = [context, cumulative, cache, cost].filter(Boolean);
33608
+ const variant = (values2) => {
33609
+ const present = values2.filter((value) => value.text);
33610
+ const spans = present.flatMap((value, index) => [
33611
+ ...index > 0 ? [{ text: " \xB7 ", fg: theme.subtle }] : [],
33612
+ { text: value.text, fg: value.fg }
33613
+ ]);
33614
+ if (spans.length > 0)
33615
+ spans.push({ text: " ", fg: theme.muted });
33616
+ return {
33617
+ text: `${present.map((value) => value.text).join(" \xB7 ")}${present.length ? " " : ""}`,
33618
+ spans
33619
+ };
33620
+ };
33621
+ const values = {
33622
+ context: { text: context, fg: contextUsageColor(ratio) },
33623
+ cumulative: { text: cumulative, fg: theme.muted },
33624
+ cache: { text: cache, fg: theme.muted },
33625
+ cost: { text: cost, fg: theme.muted }
33626
+ };
33474
33627
  return [
33475
- `${parts.join(" \xB7 ")} `,
33476
- `${[context, cumulative, cache].filter(Boolean).join(" \xB7 ")} `,
33477
- `${[context, cumulative].filter(Boolean).join(" \xB7 ")} `,
33478
- context ? `${context} ` : "",
33479
- ""
33628
+ variant([values.context, values.cumulative, values.cache, values.cost]),
33629
+ variant([values.context, values.cumulative, values.cache]),
33630
+ variant([values.context, values.cumulative]),
33631
+ variant([values.context]),
33632
+ variant([])
33480
33633
  ];
33481
33634
  }
33482
33635
  function footerStatusLine(state, width) {
33483
33636
  const leftCandidates = footerStatusText(state);
33484
33637
  const rightCandidates = footerUsageText(state);
33485
33638
  for (const right of rightCandidates) {
33486
- for (const left of leftCandidates) {
33487
- if (displayWidth(left) + (right ? 1 + displayWidth(right) : 0) <= width) {
33488
- return joinSides(left, right, width);
33639
+ for (const left2 of leftCandidates) {
33640
+ const rightWidth = displayWidth(right.text);
33641
+ if (displayWidth(left2) + (rightWidth ? 1 + rightWidth : 0) <= width) {
33642
+ const gap = width - displayWidth(left2) - rightWidth;
33643
+ return [
33644
+ { text: left2, fg: theme.accent, bold: true },
33645
+ { text: " ".repeat(gap), bg: theme.surfaceRaised },
33646
+ ...right.spans
33647
+ ];
33489
33648
  }
33490
33649
  }
33491
33650
  }
33492
- return joinSides(leftCandidates[leftCandidates.length - 1] ?? "", "", width);
33651
+ const left = sliceWidth(leftCandidates[leftCandidates.length - 1] ?? "", width);
33652
+ return [
33653
+ { text: left, fg: theme.accent, bold: true },
33654
+ { text: " ".repeat(Math.max(0, width - displayWidth(left))), bg: theme.surfaceRaised }
33655
+ ];
33656
+ }
33657
+ function paintFooterSpans(spans) {
33658
+ return spans.map((span) => paint(span.text, {
33659
+ fg: span.fg ?? theme.text,
33660
+ bg: span.bg ?? theme.surfaceRaised,
33661
+ bold: span.bold,
33662
+ dim: span.dim
33663
+ })).join("");
33493
33664
  }
33494
33665
  function noticeStyle(state) {
33495
33666
  return NOTICE_STYLE[state.noticeLevel ?? "warn"];
@@ -33540,11 +33711,7 @@ function footerLines(state, width) {
33540
33711
  ];
33541
33712
  }
33542
33713
  return [
33543
- paint(footerStatusLine(state, width), {
33544
- fg: theme.accent,
33545
- bg: theme.surfaceRaised,
33546
- bold: true
33547
- }),
33714
+ paintFooterSpans(footerStatusLine(state, width)),
33548
33715
  paint(joinSides("", pickFitting(footerHintText(state), width), width), {
33549
33716
  fg: state.notice ? noticeStyle(state).fg : theme.muted,
33550
33717
  bg: theme.canvas
@@ -33679,6 +33846,7 @@ var init_render = __esm(async () => {
33679
33846
  init_terminal_width();
33680
33847
  await __promiseAll([
33681
33848
  init_src(),
33849
+ init_context_usage(),
33682
33850
  init_state2()
33683
33851
  ]);
33684
33852
  theme = {
@@ -34821,6 +34989,11 @@ async function runTerminalCommand(commandLine, context) {
34821
34989
  context.queueRender();
34822
34990
  return;
34823
34991
  }
34992
+ if (command === "context") {
34993
+ context.state = { ...context.state, overlay: "context" };
34994
+ context.queueRender();
34995
+ return;
34996
+ }
34824
34997
  if (command === "model") {
34825
34998
  if (args.length > 0) {
34826
34999
  context.state = { ...context.state, notice: "Usage: /model", noticeLevel: "warn" };
@@ -45650,4 +45823,4 @@ switch (command) {
45650
45823
  }
45651
45824
  }
45652
45825
 
45653
- //# debugId=5D6FE2D4C44E041364756E2164756E21
45826
+ //# debugId=C4FD6B649361AE3464756E2164756E21