scream-code 0.16.2 → 0.16.3

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.
@@ -4,9 +4,9 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
- import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize$1, x as dirname$2, y as KnowledgeStore } from "./src-tDEINaMV.mjs";
7
+ import { C as join$1, D as resolve$2, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize$1, x as dirname$2, y as KnowledgeStore } from "./src-tDEINaMV.mjs";
8
8
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-CzJKBIoN.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-GajpxjyL.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
@@ -50779,7 +50779,7 @@ function canonicalizePath(path, cwd, pathClass = DEFAULT_PATH_CLASS) {
50779
50779
  const normalizedPath = normalizeUserPath(path, pathClass);
50780
50780
  if (pathClass === "win32" && isWin32DriveRelative(normalizedPath)) throw new PathSecurityError("PATH_INVALID", path, normalizedPath, `"${path}" is a drive-relative Windows path. Use an absolute path like C:\\path or a path relative to the working directory.`);
50781
50781
  if (!isAbsolute$1(normalizedPath) && !isAbsolute$1(cwd)) throw new PathSecurityError("PATH_INVALID", path, normalizedPath, `Cannot resolve "${path}" against non-absolute cwd "${cwd}".`);
50782
- return normalize$1(isAbsolute$1(normalizedPath) ? normalizedPath : resolve$1(cwd, normalizedPath));
50782
+ return normalize$1(isAbsolute$1(normalizedPath) ? normalizedPath : resolve$2(cwd, normalizedPath));
50783
50783
  }
50784
50784
  /**
50785
50785
  * True iff `candidate` is `base` itself or a descendant of it, compared
@@ -58452,7 +58452,7 @@ function dump$1(input, options) {
58452
58452
  return "";
58453
58453
  }
58454
58454
  var dumper = { dump: dump$1 };
58455
- var load = loader.load;
58455
+ var load$2 = loader.load;
58456
58456
  loader.loadAll;
58457
58457
  dumper.dump;
58458
58458
  //#endregion
@@ -60001,7 +60001,7 @@ function parseFrontmatter(text) {
60001
60001
  };
60002
60002
  try {
60003
60003
  return {
60004
- data: load(yamlText) ?? {},
60004
+ data: load$2(yamlText) ?? {},
60005
60005
  body
60006
60006
  };
60007
60007
  } catch (error) {
@@ -60025,7 +60025,7 @@ function parseSkillText(options) {
60025
60025
  const name = nonEmptyString$1(metadata.name);
60026
60026
  const description = nonEmptyString$1(metadata.description);
60027
60027
  if (isDirectorySkill && (name === void 0 || description === void 0)) throw new SkillParseError(`Missing required frontmatter field ${name === void 0 ? "\"name\"" : "\"description\""} in ${options.skillMdPath}`);
60028
- const skillPath = resolve$1(options.skillMdPath);
60028
+ const skillPath = resolve$2(options.skillMdPath);
60029
60029
  const content = parsed.body.trim();
60030
60030
  return {
60031
60031
  name: name ?? options.skillDirName,
@@ -60407,7 +60407,7 @@ async function defaultIsFile(p) {
60407
60407
  }
60408
60408
  }
60409
60409
  async function findProjectRoot$2(workDir) {
60410
- const start = resolve$1(workDir);
60410
+ const start = resolve$2(workDir);
60411
60411
  let current = start;
60412
60412
  while (true) {
60413
60413
  if (await exists(join$1(current, ".git"))) return current;
@@ -60428,7 +60428,7 @@ function resolveConfiguredDir(dir, projectRoot, userHomeDir) {
60428
60428
  if (dir === "~") return userHomeDir;
60429
60429
  if (dir.startsWith("~/")) return join$1(userHomeDir, dir.slice(2));
60430
60430
  if (isAbsolute$1(dir)) return dir;
60431
- return resolve$1(projectRoot, dir);
60431
+ return resolve$2(projectRoot, dir);
60432
60432
  }
60433
60433
  function isWithin$1(child, parent) {
60434
60434
  const rel = relative$1(parent, child);
@@ -78140,7 +78140,7 @@ const McpJsonFileSchema = z.object({ mcpServers: z.record(z.string(), McpServerC
78140
78140
  /** Maximum number of parent directories to walk when discovering mcp.json. */
78141
78141
  const MAX_PARENT_WALK = 20;
78142
78142
  function resolveMcpJsonPaths(input) {
78143
- const cwd = resolve$1(input.cwd);
78143
+ const cwd = resolve$2(input.cwd);
78144
78144
  return {
78145
78145
  user: join$1(resolveScreamHome(input.homeDir), "mcp.json"),
78146
78146
  project: join$1(cwd, ".scream-code", "mcp.json"),
@@ -99461,7 +99461,7 @@ function finalizeRawAgentProfileSource(content, profilePath, sources) {
99461
99461
  function parseAgentProfileYaml(content, profilePath) {
99462
99462
  let parsed;
99463
99463
  try {
99464
- parsed = load(content);
99464
+ parsed = load$2(content);
99465
99465
  } catch (error) {
99466
99466
  throw new Error(`Invalid agent profile YAML at ${profilePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
99467
99467
  }
@@ -123105,7 +123105,7 @@ async function exportSessionDirectory(input) {
123105
123105
  installSource: input.request.installSource,
123106
123106
  shellEnv: input.request.shellEnv
123107
123107
  });
123108
- const outputPath = input.request.outputPath !== void 0 ? resolve$1(input.request.outputPath) : resolve$1(`${input.summary.id}.zip`);
123108
+ const outputPath = input.request.outputPath !== void 0 ? resolve$2(input.request.outputPath) : resolve$2(`${input.summary.id}.zip`);
123109
123109
  return {
123110
123110
  zipPath: outputPath,
123111
123111
  entries: await writeExportZip({
@@ -123633,7 +123633,7 @@ async function readSessionIndex(homeDir, sessionsDir) {
123633
123633
  if (trimmed === "") continue;
123634
123634
  const entry = parseIndexLine(trimmed);
123635
123635
  if (entry === void 0) continue;
123636
- const sessionDir = resolve$1(entry.sessionDir);
123636
+ const sessionDir = resolve$2(entry.sessionDir);
123637
123637
  if (!isAbsolute$1(entry.sessionDir)) continue;
123638
123638
  if (!isAbsolute$1(entry.workDir)) continue;
123639
123639
  if (!isPathInside(sessionsDir, sessionDir)) continue;
@@ -123641,7 +123641,7 @@ async function readSessionIndex(homeDir, sessionsDir) {
123641
123641
  result.set(entry.sessionId, {
123642
123642
  sessionId: entry.sessionId,
123643
123643
  sessionDir,
123644
- workDir: resolve$1(entry.workDir)
123644
+ workDir: resolve$2(entry.workDir)
123645
123645
  });
123646
123646
  }
123647
123647
  return result;
@@ -123682,7 +123682,7 @@ function parseIndexLine(line) {
123682
123682
  }
123683
123683
  }
123684
123684
  function isPathInside(parent, child) {
123685
- const rel = relative$1(resolve$1(parent), resolve$1(child));
123685
+ const rel = relative$1(resolve$2(parent), resolve$2(child));
123686
123686
  return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
123687
123687
  }
123688
123688
  //#endregion
@@ -123697,7 +123697,7 @@ function slugifyWorkDirName(name) {
123697
123697
  const WORKDIR_KEY_PREFIX = "wd_";
123698
123698
  const HASH_LENGTH = 12;
123699
123699
  function normalizeWorkDir(workDir) {
123700
- const resolved = resolve$1(workDir);
123700
+ const resolved = resolve$2(workDir);
123701
123701
  let dir = resolved;
123702
123702
  while (dir !== dirname$2(dir)) {
123703
123703
  if (existsSync(join$1(dir, ".git")) || existsSync(join$1(dir, "package.json"))) return dir;
@@ -127996,6 +127996,20 @@ const BUILTIN_SLASH_COMMANDS = [
127996
127996
  priority: 178,
127997
127997
  availability: "always"
127998
127998
  },
127999
+ {
128000
+ name: "blockrows",
128001
+ aliases: ["rows"],
128002
+ description: "registry.blockrows_desc",
128003
+ priority: 179,
128004
+ availability: "always"
128005
+ },
128006
+ {
128007
+ name: "codebg",
128008
+ aliases: ["codeblock"],
128009
+ description: "registry.codebg_desc",
128010
+ priority: 180,
128011
+ availability: "always"
128012
+ },
127999
128013
  {
128000
128014
  name: "like",
128001
128015
  aliases: [],
@@ -128578,6 +128592,7 @@ var ApiKeyInputDialogComponent = class extends Container {
128578
128592
  const STATUS_BULLET = "■ ";
128579
128593
  const USER_MESSAGE_BULLET = "■ ";
128580
128594
  const FAILURE_MARK = "✗ ";
128595
+ const DONE_MARK = "✓ ";
128581
128596
  const INTERJECTION_BULLET = "▸ ";
128582
128597
  //#endregion
128583
128598
  //#region src/tui/utils/printable-key.ts
@@ -131783,7 +131798,7 @@ const darkColors = {
131783
131798
  mdLink: "#56B6C2",
131784
131799
  mdCodeBlock: "#9CDCFE",
131785
131800
  mdCodeBlockBorder: "#5C6370",
131786
- mdCodeBlockBg: "#1e232b",
131801
+ mdCodeBlockBg: "#16191f",
131787
131802
  mdQuote: "#7F848E",
131788
131803
  border: dark.gray700,
131789
131804
  borderFocus: dark.yellowGreenLight,
@@ -131817,7 +131832,7 @@ const lightColors = {
131817
131832
  mdLink: "#007A8A",
131818
131833
  mdCodeBlock: "#1565C0",
131819
131834
  mdCodeBlockBorder: "#848484",
131820
- mdCodeBlockBg: "#f2f4f8",
131835
+ mdCodeBlockBg: "#f7f9fb",
131821
131836
  mdQuote: "#616161",
131822
131837
  border: light.gray500,
131823
131838
  borderFocus: light.yellowGreen700,
@@ -131886,6 +131901,113 @@ function createThemeStyles(colors) {
131886
131901
  };
131887
131902
  }
131888
131903
  //#endregion
131904
+ //#region src/tui/utils/ui-preferences.ts
131905
+ /**
131906
+ * TUI-local UI preferences persisted to `<dataDir>/ui-preferences.json`.
131907
+ * Distinct from agent/SDK config files (which are owned by core) — this file
131908
+ * stores lightweight view-state knobs owned by the TUI itself, e.g. whether
131909
+ * the empty-session provider hint has been dismissed by the user.
131910
+ */
131911
+ const EMPTY = {};
131912
+ function getUiPreferencesPath() {
131913
+ return join(getDataDir(), SCREAM_CODE_UI_PREFERENCES_FILE_NAME);
131914
+ }
131915
+ function readUiPreferences() {
131916
+ try {
131917
+ const file = getUiPreferencesPath();
131918
+ if (!existsSync(file)) return { ...EMPTY };
131919
+ const raw = readFileSync(file, "utf8");
131920
+ const parsed = JSON.parse(raw);
131921
+ return {
131922
+ ...EMPTY,
131923
+ ...parsed
131924
+ };
131925
+ } catch {
131926
+ return { ...EMPTY };
131927
+ }
131928
+ }
131929
+ function writeUiPreferences(prefs) {
131930
+ try {
131931
+ const file = getUiPreferencesPath();
131932
+ mkdirSync(dirname$1(file), { recursive: true });
131933
+ writeFileSync(file, JSON.stringify(prefs, null, 2), "utf8");
131934
+ } catch {}
131935
+ }
131936
+ function isEmptySessionHintDismissed() {
131937
+ return readUiPreferences().emptySessionHintDismissed === true;
131938
+ }
131939
+ /** Toggle the empty-session hint on/off (Ctrl+B). Returns the new state:
131940
+ * true = hidden, false = shown. Persisted immediately. */
131941
+ function toggleEmptySessionHint() {
131942
+ const prefs = readUiPreferences();
131943
+ const dismissed = prefs.emptySessionHintDismissed !== true;
131944
+ prefs.emptySessionHintDismissed = dismissed;
131945
+ writeUiPreferences(prefs);
131946
+ return dismissed;
131947
+ }
131948
+ /** Whether the per-turn elapsed marker is enabled (default on). */
131949
+ function isTurnElapsedEnabled() {
131950
+ return readUiPreferences().turnElapsedEnabled !== false;
131951
+ }
131952
+ /** Toggle the per-turn elapsed marker via /snaptimer. Returns the new state:
131953
+ * true = shown, false = hidden. Persisted immediately. */
131954
+ function toggleTurnElapsed() {
131955
+ const prefs = readUiPreferences();
131956
+ const enabled = prefs.turnElapsedEnabled !== false;
131957
+ prefs.turnElapsedEnabled = !enabled;
131958
+ writeUiPreferences(prefs);
131959
+ return !enabled;
131960
+ }
131961
+ /** Row budgets of the activity block as stored, without clamping (see
131962
+ * `utils/activity-lines.ts`, which owns the ranges and defaults). */
131963
+ function readActivityLinePrefs() {
131964
+ const prefs = readUiPreferences();
131965
+ return {
131966
+ activityCollapsedLines: prefs.activityCollapsedLines,
131967
+ activityExpandedToolLines: prefs.activityExpandedToolLines,
131968
+ activityExpandedThinkingLines: prefs.activityExpandedThinkingLines
131969
+ };
131970
+ }
131971
+ /** Persist one activity row budget. Persisted immediately. */
131972
+ function writeActivityLinePref(key, value) {
131973
+ const prefs = readUiPreferences();
131974
+ prefs[key] = value;
131975
+ writeUiPreferences(prefs);
131976
+ }
131977
+ /**
131978
+ * Whether fenced code blocks render as a background panel (default on).
131979
+ *
131980
+ * Cached: the markdown renderer asks once per code line, and reading the file
131981
+ * each time would hit the disk on every frame. `/codebg` updates the cache.
131982
+ */
131983
+ let codeBlockPanelCache;
131984
+ function isCodeBlockPanelEnabled() {
131985
+ codeBlockPanelCache ??= readUiPreferences().codeBlockPanelEnabled !== false;
131986
+ return codeBlockPanelCache;
131987
+ }
131988
+ /** Toggle the code-block panel via /codebg. Returns the new state. */
131989
+ function toggleCodeBlockPanel() {
131990
+ const prefs = readUiPreferences();
131991
+ const enabled = prefs.codeBlockPanelEnabled !== false;
131992
+ prefs.codeBlockPanelEnabled = !enabled;
131993
+ writeUiPreferences(prefs);
131994
+ codeBlockPanelCache = !enabled;
131995
+ return !enabled;
131996
+ }
131997
+ /** Whether user messages render with a highlight background block (default on). */
131998
+ function isUserMessageHighlightEnabled() {
131999
+ return readUiPreferences().userMessageHighlightEnabled !== false;
132000
+ }
132001
+ /** Toggle the user-message highlight block via /hl. Returns the new state:
132002
+ * true = highlighted, false = default (no background). Persisted immediately. */
132003
+ function toggleUserMessageHighlight() {
132004
+ const prefs = readUiPreferences();
132005
+ const enabled = prefs.userMessageHighlightEnabled !== false;
132006
+ prefs.userMessageHighlightEnabled = !enabled;
132007
+ writeUiPreferences(prefs);
132008
+ return !enabled;
132009
+ }
132010
+ //#endregion
131889
132011
  //#region src/tui/theme/pi-tui-theme.ts
131890
132012
  const HEADING_HASH_PREFIX = /^((?:\u001B\[[0-9;]*m)*)#{1,6}[ \t]+/;
131891
132013
  /**
@@ -131962,8 +132084,6 @@ function createMarkdownTheme(colors) {
131962
132084
  codeBlockBorder: () => null,
131963
132085
  codeBlockIndent: " ",
131964
132086
  codeBlockLine: (line, { index, lang, width }) => {
131965
- const panel = chalk.bgHex(colors.mdCodeBlockBg);
131966
- const labelPaint = chalk.bgHex(colors.mdCodeBlockBg).hex(colors.mdCodeBlock);
131967
132087
  const content = visibleWidth(line) > width - 1 ? truncateToWidth(line, Math.max(1, width - 1), "…") : line;
131968
132088
  const contentCells = visibleWidth(content);
131969
132089
  const rawLabel = index === 0 && lang !== void 0 && lang.length > 0 ? ` ${lang}` : "";
@@ -131971,8 +132091,11 @@ function createMarkdownTheme(colors) {
131971
132091
  const showLabel = labelCells > 0 && 1 + contentCells + labelCells + 1 <= width;
131972
132092
  const filler = showLabel ? Math.max(0, width - contentCells - labelCells - 2) : 0;
131973
132093
  const padCells = showLabel ? 1 : Math.max(0, width - contentCells - 1);
132094
+ const panelled = isCodeBlockPanelEnabled();
132095
+ const paint = panelled ? chalk.bgHex(colors.mdCodeBlockBg) : (text) => text;
132096
+ const labelPaint = panelled ? chalk.bgHex(colors.mdCodeBlockBg).hex(colors.mdCodeBlock) : chalk.hex(colors.mdCodeBlock);
131974
132097
  const label = showLabel ? chalk.italic(labelPaint(rawLabel)) : "";
131975
- return `${panel(" ")}${panel(content)}${panel(" ".repeat(filler))}${label}${panel(" ".repeat(padCells))}`;
132098
+ return `${paint(" ")}${paint(content)}${paint(" ".repeat(filler))}${label}${paint(" ".repeat(padCells))}`;
131976
132099
  },
131977
132100
  quote: (text) => chalk.hex(colors.mdQuote)(text),
131978
132101
  quoteBorder: (text) => chalk.hex(colors.mdQuote)(text),
@@ -132441,7 +132564,7 @@ async function loadManagedUsageReport(host) {
132441
132564
  * not hammer the vendor endpoint.
132442
132565
  */
132443
132566
  const BALANCE_CACHE_MS = 6e4;
132444
- const cache = /* @__PURE__ */ new Map();
132567
+ const cache$1 = /* @__PURE__ */ new Map();
132445
132568
  /** Monotonic id so an out-of-order lookup can never overwrite a newer one. */
132446
132569
  let latestRequestId = 0;
132447
132570
  function resolveApiKey(providerName) {
@@ -132484,10 +132607,10 @@ async function loadBalance(providerName) {
132484
132607
  async function getProviderBalanceForModel(model) {
132485
132608
  const providerName = model.split("/")[0] ?? "";
132486
132609
  if (providerName.length === 0) return null;
132487
- const cached = cache.get(providerName);
132610
+ const cached = cache$1.get(providerName);
132488
132611
  if (cached !== void 0 && Date.now() - cached.at < BALANCE_CACHE_MS) return cached.balance;
132489
132612
  const balance = await loadBalance(providerName);
132490
- cache.set(providerName, {
132613
+ cache$1.set(providerName, {
132491
132614
  balance,
132492
132615
  at: Date.now()
132493
132616
  });
@@ -133931,7 +134054,7 @@ async function guidedGoalSetup(host) {
133931
134054
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
133932
134055
  return;
133933
134056
  }
133934
- const { TextInputDialogComponent } = await import("./text-input-dialog-Dp2TUSjQ.mjs");
134057
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DAQfJdHu.mjs");
133935
134058
  const initialDesc = await promptText(host, TextInputDialogComponent, {
133936
134059
  title: t("goal.setup_title_initial"),
133937
134060
  subtitle: t("goal.setup_desc_hint"),
@@ -133952,7 +134075,7 @@ async function guidedGoalSetup(host) {
133952
134075
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
133953
134076
  }
133954
134077
  async function showGoalConfigWizard(host, session, objective, replace) {
133955
- const { TextInputDialogComponent } = await import("./text-input-dialog-Dp2TUSjQ.mjs");
134078
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DAQfJdHu.mjs");
133956
134079
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
133957
134080
  title: t("goal.wizard_title", { objective }),
133958
134081
  subtitle: t("goal.budget_turns_hint"),
@@ -134618,7 +134741,7 @@ var WrappedLine = class {
134618
134741
  };
134619
134742
  //#endregion
134620
134743
  //#region src/tui/components/messages/agent-group.ts
134621
- const THROTTLE_MS$2 = 200;
134744
+ const THROTTLE_MS$1 = 200;
134622
134745
  var AgentGroupComponent = class extends Container {
134623
134746
  colors;
134624
134747
  ui;
@@ -134679,7 +134802,7 @@ var AgentGroupComponent = class extends Container {
134679
134802
  this.throttleTimer = setTimeout(() => {
134680
134803
  this.throttleTimer = null;
134681
134804
  this.flushRender();
134682
- }, THROTTLE_MS$2);
134805
+ }, THROTTLE_MS$1);
134683
134806
  }
134684
134807
  /**
134685
134808
  * Compares each child's current phase with the phase captured at the last
@@ -134801,6 +134924,140 @@ function formatTokens$1(n) {
134801
134924
  return `${String(n)} tok`;
134802
134925
  }
134803
134926
  //#endregion
134927
+ //#region src/tui/components/messages/background-agent-status.ts
134928
+ /**
134929
+ * Builds the bullet + text of a background task notice. Shared by the standalone
134930
+ * card and the activity block row so a notice looks the same in both places.
134931
+ */
134932
+ function renderBackgroundStatus(data, colors, maxCells) {
134933
+ const tone = data.phase === "started" ? colors.primary : data.phase === "completed" ? colors.success : colors.error;
134934
+ const bullet = data.phase === "completed" ? chalk.hex(colors.success)(DONE_MARK) : data.phase === "failed" ? chalk.hex(colors.error)(FAILURE_MARK) : chalk.hex(colors.textDim)(`${BRAILLE_SPINNER_FRAMES[0] ?? "⠋"} `);
134935
+ const hasDetail = data.detail !== void 0 && data.detail.length > 0;
134936
+ const plain = hasDetail ? `${data.headline} (${data.detail ?? ""})` : data.headline;
134937
+ if (maxCells !== void 0 && visibleWidth(plain) > maxCells) return {
134938
+ bullet,
134939
+ text: chalk.hex(tone)(truncateToWidth(plain, maxCells, "…"))
134940
+ };
134941
+ return {
134942
+ bullet,
134943
+ text: chalk.hex(tone)(data.headline) + (hasDetail ? chalk.hex(colors.textDim)(` (${data.detail ?? ""})`) : "")
134944
+ };
134945
+ }
134946
+ var BackgroundAgentStatusComponent = class {
134947
+ data;
134948
+ colors;
134949
+ bullet;
134950
+ textComponent;
134951
+ cachedWidth;
134952
+ cachedLines;
134953
+ constructor(data, colors) {
134954
+ this.data = data;
134955
+ this.colors = colors;
134956
+ const view = renderBackgroundStatus(data, colors);
134957
+ this.bullet = view.bullet;
134958
+ this.textComponent = new Text(view.text, 0, 0);
134959
+ }
134960
+ invalidate() {
134961
+ this.cachedWidth = void 0;
134962
+ this.cachedLines = void 0;
134963
+ }
134964
+ render(width) {
134965
+ if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
134966
+ const contentWidth = Math.max(1, width - 2);
134967
+ const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? this.bullet : " ") + line)];
134968
+ this.cachedWidth = width;
134969
+ this.cachedLines = lines;
134970
+ return lines;
134971
+ }
134972
+ };
134973
+ //#endregion
134974
+ //#region src/tui/utils/activity-lines.ts
134975
+ /**
134976
+ * Row budgets of the activity block.
134977
+ *
134978
+ * The defaults are the constants in `constant/rendering.ts`; `/blockrows` writes
134979
+ * overrides into ui-preferences.json so the block can be tuned without a rebuild.
134980
+ * Values are cached in memory because a block reads them on every rebuild — and
134981
+ * rebuilds happen on every throttled streaming tick — while the file only changes
134982
+ * when the user runs the command.
134983
+ *
134984
+ * Anything out of range (hand-edited file, stale value, non-number) falls back to
134985
+ * the default rather than changing how much content a row may occupy.
134986
+ */
134987
+ const ACTIVITY_LINE_SETTINGS = [
134988
+ {
134989
+ key: "activityCollapsedLines",
134990
+ label: "blockrows.item_collapsed",
134991
+ values: [
134992
+ 2,
134993
+ 3,
134994
+ 4,
134995
+ 5
134996
+ ],
134997
+ fallback: 3
134998
+ },
134999
+ {
135000
+ key: "activityExpandedToolLines",
135001
+ label: "blockrows.item_tool",
135002
+ values: [
135003
+ 5,
135004
+ 10,
135005
+ 15,
135006
+ 20,
135007
+ 30
135008
+ ],
135009
+ fallback: 15
135010
+ },
135011
+ {
135012
+ key: "activityExpandedThinkingLines",
135013
+ label: "blockrows.item_thinking",
135014
+ values: [
135015
+ 5,
135016
+ 10,
135017
+ 15,
135018
+ 20
135019
+ ],
135020
+ fallback: 10
135021
+ }
135022
+ ];
135023
+ let cache;
135024
+ function isActivityLineKey(value) {
135025
+ return ACTIVITY_LINE_SETTINGS.some((setting) => setting.key === value);
135026
+ }
135027
+ function findActivityLineSetting(key) {
135028
+ return ACTIVITY_LINE_SETTINGS.find((candidate) => candidate.key === key) ?? ACTIVITY_LINE_SETTINGS[0];
135029
+ }
135030
+ function resolve$1(stored, setting) {
135031
+ if (typeof stored !== "number" || !Number.isFinite(stored)) return setting.fallback;
135032
+ return setting.values.includes(stored) ? stored : setting.fallback;
135033
+ }
135034
+ function load() {
135035
+ const stored = readActivityLinePrefs();
135036
+ const settings = ACTIVITY_LINE_SETTINGS;
135037
+ return {
135038
+ collapsed: resolve$1(stored.activityCollapsedLines, settings[0]),
135039
+ expandedTool: resolve$1(stored.activityExpandedToolLines, settings[1]),
135040
+ expandedThinking: resolve$1(stored.activityExpandedThinkingLines, settings[2])
135041
+ };
135042
+ }
135043
+ /** Current row budgets. Cached: call `refreshActivityLines` after a change. */
135044
+ function getActivityLines() {
135045
+ cache ??= load();
135046
+ return cache;
135047
+ }
135048
+ /** Value currently in effect for one budget (never undefined). */
135049
+ function getActivityLineValue(key) {
135050
+ const lines = getActivityLines();
135051
+ if (key === "activityCollapsedLines") return lines.collapsed;
135052
+ if (key === "activityExpandedToolLines") return lines.expandedTool;
135053
+ return lines.expandedThinking;
135054
+ }
135055
+ /** Persist one budget and drop the cache so the next render picks it up. */
135056
+ function setActivityLineValue(key, value) {
135057
+ writeActivityLinePref(key, value);
135058
+ cache = void 0;
135059
+ }
135060
+ //#endregion
134804
135061
  //#region src/tui/utils/speed-tracker.ts
134805
135062
  /**
134806
135063
  * Streaming-speed gauge for the live thinking indicator.
@@ -134957,7 +135214,9 @@ const BRANCH_LAST = " └─ ";
134957
135214
  const BRANCH_PIPE = " │ ";
134958
135215
  const TREE_PIPE_ROW = " │";
134959
135216
  const THINKING_BODY_PREFIX = " ";
134960
- const THROTTLE_MS$1 = 200;
135217
+ /** Cells of a background task notice bullet (`⠋ ` / `✓ ` / `✗ `). */
135218
+ const NOTICE_BULLET_WIDTH = 2;
135219
+ const THROTTLE_MS = 200;
134961
135220
  /** Rates below this are noise: the live badge stays hidden until tokens flow. */
134962
135221
  const MIN_RATE = .05;
134963
135222
  const SEPARATOR = " · ";
@@ -134978,6 +135237,10 @@ const CARD_INDENT_RE = /^(\u001B\[[0-9;]*m)*( {2})/;
134978
135237
  function countTokens(text) {
134979
135238
  return text.trim().length === 0 ? 0 : estimateTokens(text);
134980
135239
  }
135240
+ /** Non-empty, trimmed lines of one reasoning run. */
135241
+ function thinkingLines(text) {
135242
+ return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
135243
+ }
134981
135244
  /**
134982
135245
  * Drops the card's own two-cell status marker so a row prefix can take its
134983
135246
  * place without shifting the row.
@@ -135119,10 +135382,8 @@ function condenseRowLabel(header, toolName, args, budget) {
135119
135382
  var ActivityGroupComponent = class extends Container {
135120
135383
  colors;
135121
135384
  ui;
135122
- tools = [];
135385
+ segments = [];
135123
135386
  steps = /* @__PURE__ */ new Set();
135124
- thinkingText = "";
135125
- thinkingLive = false;
135126
135387
  running = true;
135127
135388
  expanded = false;
135128
135389
  headerText;
@@ -135134,7 +135395,10 @@ var ActivityGroupComponent = class extends Container {
135134
135395
  renderedWidth;
135135
135396
  /** Token estimates per borrowed card, keyed by the result text they counted. */
135136
135397
  toolTokenCache = /* @__PURE__ */ new WeakMap();
135137
- thinkingTokenCache;
135398
+ /** Token estimates per reasoning run, keyed by the segment they counted. */
135399
+ thinkingTokenCache = /* @__PURE__ */ new WeakMap();
135400
+ /** Reasoning text already shown when a notice interrupted the newest run. */
135401
+ interruptedThinking;
135138
135402
  constructor(colors, ui) {
135139
135403
  super();
135140
135404
  this.colors = colors;
@@ -135148,7 +135412,7 @@ var ActivityGroupComponent = class extends Container {
135148
135412
  }
135149
135413
  /** True when the group owns neither reasoning nor tool calls. */
135150
135414
  isEmpty() {
135151
- return this.tools.length === 0 && this.thinkingText.trim().length === 0;
135415
+ return this.segments.every((segment) => segment.kind === "thinking" && segment.text.trim().length === 0);
135152
135416
  }
135153
135417
  isExpanded() {
135154
135418
  return this.expanded;
@@ -135158,8 +135422,9 @@ var ActivityGroupComponent = class extends Container {
135158
135422
  * card is a no-op.
135159
135423
  */
135160
135424
  attachTool(tc, step) {
135161
- if (this.tools.some((entry) => entry.tc === tc)) return;
135162
- this.tools.push({
135425
+ if (this.segments.some((segment) => segment.kind === "tool" && segment.tc === tc)) return;
135426
+ this.segments.push({
135427
+ kind: "tool",
135163
135428
  tc,
135164
135429
  step
135165
135430
  });
@@ -135170,18 +135435,55 @@ var ActivityGroupComponent = class extends Container {
135170
135435
  });
135171
135436
  this.flushNow();
135172
135437
  }
135173
- /** Updates the reasoning text; `live` drives the streaming rate in the header. */
135174
- setThinking(text, live) {
135175
- if (text === this.thinkingText && live === this.thinkingLive) return;
135176
- const structureChanged = text.length === 0 !== (this.thinkingText.length === 0);
135177
- this.thinkingText = text;
135178
- this.thinkingLive = live;
135179
- if (structureChanged) this.flushNow();
135180
- else this.scheduleFlush();
135438
+ /** Adds a background task notice as the newest step of the timeline. */
135439
+ attachNotice(data) {
135440
+ const last = this.segments.at(-1);
135441
+ if (last !== void 0 && last.kind === "thinking" && last.live) {
135442
+ last.live = false;
135443
+ this.interruptedThinking = {
135444
+ text: last.text,
135445
+ length: last.text.length
135446
+ };
135447
+ }
135448
+ this.segments.push({
135449
+ kind: "notice",
135450
+ data
135451
+ });
135452
+ this.flushNow();
135453
+ }
135454
+ /**
135455
+ * Appends to the reasoning run streaming right now, opening a new step of the
135456
+ * timeline when the last step is a tool call or a reasoning run that already
135457
+ * ended. `live` drives the streaming rate in the header.
135458
+ */
135459
+ appendThinking(text, live) {
135460
+ const interrupted = this.interruptedThinking;
135461
+ const split = interrupted !== void 0 && text.startsWith(interrupted.text);
135462
+ if (interrupted !== void 0 && !split) this.interruptedThinking = void 0;
135463
+ const body = split && interrupted !== void 0 ? text.slice(interrupted.length) : text;
135464
+ const last = this.segments.at(-1);
135465
+ if (last !== void 0 && last.kind === "thinking" && (last.live || last.text.trim().length === 0)) {
135466
+ if (last.text === body && last.live === live) return;
135467
+ const structureChanged = body.length === 0 !== (last.text.length === 0);
135468
+ last.text = body;
135469
+ last.live = live;
135470
+ if (structureChanged) this.flushNow();
135471
+ else this.scheduleFlush();
135472
+ return;
135473
+ }
135474
+ if (body.trim().length === 0) return;
135475
+ this.segments.push({
135476
+ kind: "thinking",
135477
+ text: body,
135478
+ live
135479
+ });
135480
+ this.flushNow();
135181
135481
  }
135182
135482
  endThinking() {
135183
- if (!this.thinkingLive) return;
135184
- this.thinkingLive = false;
135483
+ this.interruptedThinking = void 0;
135484
+ const last = this.segments.at(-1);
135485
+ if (last === void 0 || last.kind !== "thinking" || !last.live) return;
135486
+ last.live = false;
135185
135487
  this.flushNow();
135186
135488
  }
135187
135489
  /** Marks the producing turn as finished: the header stops spinning. */
@@ -135189,13 +135491,18 @@ var ActivityGroupComponent = class extends Container {
135189
135491
  if (this.running === running) return;
135190
135492
  this.running = running;
135191
135493
  if (running) this.startSpinner();
135192
- else this.stopSpinner();
135494
+ else {
135495
+ this.stopSpinner();
135496
+ this.interruptedThinking = void 0;
135497
+ const last = this.segments.at(-1);
135498
+ if (last !== void 0 && last.kind === "thinking") last.live = false;
135499
+ }
135193
135500
  this.flushNow();
135194
135501
  }
135195
135502
  setExpanded(expanded) {
135196
135503
  if (this.expanded === expanded) return;
135197
135504
  this.expanded = expanded;
135198
- for (const entry of this.tools) entry.tc.setExpanded(expanded);
135505
+ for (const segment of this.segments) if (segment.kind === "tool") segment.tc.setExpanded(expanded);
135199
135506
  this.flushNow();
135200
135507
  }
135201
135508
  /** Repaints after a borrowed card changed (results land via setResult). */
@@ -135224,7 +135531,7 @@ var ActivityGroupComponent = class extends Container {
135224
135531
  */
135225
135532
  invalidate() {
135226
135533
  this.renderedWidth = void 0;
135227
- for (const entry of this.tools) entry.tc.invalidate();
135534
+ for (const segment of this.segments) if (segment.kind === "tool") segment.tc.invalidate();
135228
135535
  super.invalidate();
135229
135536
  }
135230
135537
  scheduleFlush() {
@@ -135233,7 +135540,7 @@ var ActivityGroupComponent = class extends Container {
135233
135540
  this.throttleTimer = null;
135234
135541
  this.rebuild(this.renderedWidth ?? 80);
135235
135542
  this.ui?.requestRender();
135236
- }, THROTTLE_MS$1);
135543
+ }, THROTTLE_MS);
135237
135544
  }
135238
135545
  flushNow() {
135239
135546
  if (this.throttleTimer !== null) {
@@ -135257,11 +135564,12 @@ var ActivityGroupComponent = class extends Container {
135257
135564
  const marker = this.running ? chalk.hex(colors.roleThinking)(`${frame} `) : chalk.hex(colors.success)(STATUS_BULLET);
135258
135565
  const label = this.running ? t("activitygroup.running") : t("activitygroup.done");
135259
135566
  const parts = [];
135567
+ const toolCount = this.toolCount();
135260
135568
  if (this.steps.size > 0) parts.push(t("activitygroup.steps", { count: String(this.steps.size) }));
135261
- if (this.tools.length > 0) parts.push(t("activitygroup.tools", { count: String(this.tools.length) }));
135569
+ if (toolCount > 0) parts.push(t("activitygroup.tools", { count: String(toolCount) }));
135262
135570
  parts.push(t("activitygroup.tokens", { tok: this.formatTokens(this.blockTokens()) }));
135263
135571
  const stats = chalk.dim(SEPARATOR + parts.join(SEPARATOR));
135264
- const speed = this.thinkingLive ? getSharedSpeedTracker().getSpeed() : 0;
135572
+ const speed = this.liveThinking() ? getSharedSpeedTracker().getSpeed() : 0;
135265
135573
  const rate = speed > MIN_RATE ? chalk.hex(lerpHex(colors.textDim, colors.primary, easeSpeedRatio(speed / 200)))(t("activitygroup.rate", { rate: speed.toFixed(1) })) : chalk.dim(t("activitygroup.rate", { rate: "-" }));
135266
135574
  const hint = chalk.dim(this.expanded ? t("activitygroup.hint_collapse") : t("activitygroup.hint_expand"));
135267
135575
  const head = `${marker}${chalk.hex(colors.primary).bold(label)}${stats}`;
@@ -135274,68 +135582,126 @@ var ActivityGroupComponent = class extends Container {
135274
135582
  if (visibleWidth(bare) <= width) return bare;
135275
135583
  return chalk.hex(colors.primary).bold(truncateToWidth(label, Math.max(1, width - 2), "…"));
135276
135584
  }
135277
- /** Collapsed state: at most three rows, matching the documented preview. */
135585
+ /**
135586
+ * Collapsed state: the newest steps of the timeline, kept in chronological
135587
+ * order so the block still reads top-down. Reasoning runs and tool calls share
135588
+ * the same row budget — by default the newest step plus the reasoning run that
135589
+ * led to it.
135590
+ */
135278
135591
  buildCollapsedRows(width) {
135279
- const hasThinking = this.thinkingText.trim().length > 0;
135280
- const latest = this.tools.at(-1);
135281
- if (latest !== void 0) {
135282
- const prefix = hasThinking ? BRANCH_PIPE : BRANCH_LAST;
135283
- const row = this.toolRow(latest.tc, width, 3);
135284
- this.bodyContainer.addChild(new Text(`${prefix}${this.statusGlyph(row)} ${row.header}`, 0, 0));
135285
- }
135286
- if (hasThinking) this.bodyContainer.addChild(new Text(this.thinkingRow(width), 0, 0));
135592
+ const budget = Math.max(1, getActivityLines().collapsed - 1);
135593
+ const shown = this.segments.slice(Math.max(0, this.segments.length - budget));
135594
+ shown.forEach((segment, index) => {
135595
+ const isLast = index === shown.length - 1;
135596
+ if (segment.kind === "tool") {
135597
+ const row = this.toolRow(segment.tc, width, 3);
135598
+ this.bodyContainer.addChild(new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.statusGlyph(row)} ${row.header}`, 0, 0));
135599
+ return;
135600
+ }
135601
+ if (segment.kind === "notice") {
135602
+ this.bodyContainer.addChild(this.noticeRow(segment.data, width, isLast));
135603
+ return;
135604
+ }
135605
+ this.bodyContainer.addChild(this.thinkingSummaryRow(segment, width, isLast));
135606
+ });
135287
135607
  }
135288
- /** Expanded state: one branch per tool plus a reasoning excerpt. */
135608
+ /**
135609
+ * Expanded state: the whole timeline in order. Rows are budgeted for the block
135610
+ * as a whole, so a turn with dozens of steps cannot expand into hundreds of
135611
+ * rows; whatever does not fit is summarised in one row at the end.
135612
+ */
135289
135613
  buildExpandedRows(width) {
135290
- const colors = this.colors;
135291
- const hasThinking = this.thinkingText.trim().length > 0;
135292
135614
  this.bodyContainer.addChild(new Text(TREE_PIPE_ROW, 0, 0));
135293
- const toolRows = this.tools.map((entry) => ({ row: this.toolRow(entry.tc, width, 15) }));
135294
135615
  let used = 1;
135295
- let hiddenTools = 0;
135296
- let shownTools = 0;
135297
- toolRows.forEach(({ row }, index) => {
135298
- const cost = 1 + row.body.length;
135299
- if (shownTools > 0 && used + cost > 60) {
135300
- hiddenTools += 1;
135301
- return;
135616
+ let hidden = 0;
135617
+ for (let index = 0; index < this.segments.length; index += 1) {
135618
+ const segment = this.segments[index];
135619
+ const isLastStep = index === this.segments.length - 1;
135620
+ const rows = this.segmentRows(segment, width, isLastStep);
135621
+ const budget = 60 - (isLastStep ? 0 : 1);
135622
+ if (index > 0 && used + rows.cost > budget) {
135623
+ hidden = this.segments.length - index;
135624
+ break;
135302
135625
  }
135303
- const isLast = index === this.tools.length - 1 && !hasThinking && hiddenTools === 0;
135304
- this.bodyContainer.addChild(new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.statusGlyph(row)} ${row.header}`, 0, 0));
135626
+ for (const component of rows.components) this.bodyContainer.addChild(component);
135627
+ used += rows.cost;
135628
+ }
135629
+ if (hidden > 0) {
135630
+ const summary = t("activitygroup.segments_hidden", { count: String(hidden) });
135631
+ this.bodyContainer.addChild(new Text(`${BRANCH_PIPE}${chalk.dim(summary)}`, 0, 0));
135632
+ }
135633
+ }
135634
+ /** Renders one step of the timeline: its branch row plus its own body rows. */
135635
+ segmentRows(segment, width, isLast) {
135636
+ if (segment.kind === "tool") {
135637
+ const row = this.toolRow(segment.tc, width, getActivityLines().expandedTool);
135638
+ const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.statusGlyph(row)} ${row.header}`, 0, 0)];
135305
135639
  const continuation = isLast ? THINKING_BODY_PREFIX : BRANCH_PIPE;
135306
- for (const line of row.body) this.bodyContainer.addChild(new Text(`${continuation}${line}`, 0, 0));
135307
- used += cost;
135308
- shownTools += 1;
135309
- });
135310
- if (hiddenTools > 0) {
135311
- const prefix = hasThinking ? BRANCH_FIRST : BRANCH_LAST;
135312
- const summary = t("activitygroup.tools_hidden", { count: String(hiddenTools) });
135313
- this.bodyContainer.addChild(new Text(`${prefix}${chalk.dim(summary)}`, 0, 0));
135314
- used += 1;
135315
- }
135316
- if (!hasThinking) return;
135317
- const thinkingLines = this.thinkingLines();
135318
- let excerptBudget = Math.min(10, Math.max(0, 60 - used - 1));
135319
- if (excerptBudget < 10 && thinkingLines.length > excerptBudget) excerptBudget -= 1;
135320
- if (excerptBudget <= 0) return;
135321
- this.bodyContainer.addChild(new Text(`${BRANCH_LAST} ${chalk.dim("·")} ${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0));
135322
- const lines = thinkingLines;
135323
- const inner = Math.max(1, width - visibleWidth(THINKING_BODY_PREFIX));
135324
- const height = (line) => new WrappedLine(THINKING_BODY_PREFIX, THINKING_BODY_PREFIX, line).render(inner).length;
135325
- let selected = this.thinkingLive ? lines.slice(-10) : lines.slice(0, 10);
135326
- while (selected.length > 1 && selected.reduce((sum, line) => sum + height(line), 0) > excerptBudget) selected = this.thinkingLive ? selected.slice(1) : selected.slice(0, -1);
135640
+ for (const line of row.body) components.push(new Text(`${continuation}${line}`, 0, 0));
135641
+ return {
135642
+ components,
135643
+ cost: 1 + row.body.length
135644
+ };
135645
+ }
135646
+ if (segment.kind === "notice") return {
135647
+ components: [this.noticeRow(segment.data, width, isLast)],
135648
+ cost: 1
135649
+ };
135650
+ return this.thinkingSegmentRows(segment, width, isLast);
135651
+ }
135652
+ /** One background task notice, kept to a single row. */
135653
+ noticeRow(data, width, isLast) {
135654
+ const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
135655
+ const view = renderBackgroundStatus(data, this.colors, Math.max(1, width - visibleWidth(prefix) - NOTICE_BULLET_WIDTH));
135656
+ return new Text(`${prefix}${view.bullet}${view.text}`, 0, 0);
135657
+ }
135658
+ /**
135659
+ * One reasoning run: a label row plus as many of its lines as its own budget
135660
+ * allows. Lines keep their order; the oldest end of a live run (or the newest
135661
+ * end of a finished one) is dropped until the budget fits.
135662
+ */
135663
+ thinkingSegmentRows(segment, width, isLast) {
135664
+ const colors = this.colors;
135665
+ const lines = thinkingLines(segment.text);
135666
+ if (lines.length === 0) return {
135667
+ components: [],
135668
+ cost: 0
135669
+ };
135670
+ const continuation = isLast ? THINKING_BODY_PREFIX : BRANCH_PIPE;
135671
+ const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0)];
135672
+ let cost = 1;
135673
+ const budget = getActivityLines().expandedThinking;
135674
+ const inner = Math.max(1, width - visibleWidth(continuation));
135675
+ const height = (line) => new WrappedLine(continuation, continuation, line).render(inner).length;
135676
+ let selected = segment.live ? lines.slice(-budget) : lines.slice(0, budget);
135677
+ while (selected.length > 1 && selected.reduce((sum, line) => sum + height(line), 0) > budget) selected = segment.live ? selected.slice(1) : selected.slice(0, -1);
135327
135678
  const first = selected[0];
135328
- if (selected.length === 1 && first !== void 0 && height(first) > excerptBudget) selected = [truncateToWidth(first, inner * excerptBudget, "…")];
135679
+ if (selected.length === 1 && first !== void 0 && height(first) > budget) selected = [truncateToWidth(first, inner * budget, "…")];
135329
135680
  for (const line of selected) {
135330
- const row = new WrappedLine(THINKING_BODY_PREFIX, THINKING_BODY_PREFIX, chalk.hex(colors.roleThinking)(line));
135331
- this.bodyContainer.addChild(row);
135332
- used += height(line);
135681
+ components.push(new WrappedLine(continuation, continuation, chalk.hex(colors.roleThinking)(line)));
135682
+ cost += height(line);
135333
135683
  }
135334
- const hidden = lines.length - selected.length;
135335
- if (hidden > 0) {
135336
- const hint = this.thinkingLive ? t("activitygroup.thinking_earlier", { count: String(hidden) }) : t("activitygroup.thinking_more", { count: String(hidden) });
135337
- this.bodyContainer.addChild(new Text(`${THINKING_BODY_PREFIX}${chalk.dim(hint)}`, 0, 0));
135684
+ const hiddenLines = lines.length - selected.length;
135685
+ if (hiddenLines > 0) {
135686
+ const hint = segment.live ? t("activitygroup.thinking_earlier", { count: String(hiddenLines) }) : t("activitygroup.thinking_more", { count: String(hiddenLines) });
135687
+ components.push(new Text(`${continuation}${chalk.dim(hint)}`, 0, 0));
135688
+ cost += 1;
135338
135689
  }
135690
+ return {
135691
+ components,
135692
+ cost
135693
+ };
135694
+ }
135695
+ /** Number of tool calls on the timeline. */
135696
+ toolCount() {
135697
+ let count = 0;
135698
+ for (const segment of this.segments) if (segment.kind === "tool") count += 1;
135699
+ return count;
135700
+ }
135701
+ /** True while the newest step of the timeline is a reasoning run still streaming. */
135702
+ liveThinking() {
135703
+ const last = this.segments.at(-1);
135704
+ return last !== void 0 && last.kind === "thinking" && last.live;
135339
135705
  }
135340
135706
  /**
135341
135707
  * Renders one borrowed card at the width left over by the row prefix and
@@ -135373,29 +135739,27 @@ var ActivityGroupComponent = class extends Container {
135373
135739
  }
135374
135740
  return chalk.hex(this.colors.success)("✓");
135375
135741
  }
135376
- thinkingLines() {
135377
- return this.thinkingText.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
135378
- }
135379
135742
  /**
135380
- * Collapsed reasoning row: the first reasoning line clipped to whatever space
135381
- * the row can still hold at this width, so a narrow terminal cannot wrap the
135382
- * block into a fourth row. The token estimate lives in the block header only.
135743
+ * Collapsed reasoning row: the run's first line clipped to whatever space the
135744
+ * row can still hold at this width, so a narrow terminal cannot wrap the block
135745
+ * past its height. The token estimate lives in the block header only.
135383
135746
  */
135384
- thinkingRow(width) {
135385
- const first = this.thinkingLines()[0] ?? "";
135747
+ thinkingSummaryRow(segment, width, isLast) {
135748
+ const first = thinkingLines(segment.text)[0] ?? "";
135386
135749
  const label = t("activitygroup.thinking_summary", { summary: "" });
135387
- const bareFixed = BRANCH_WIDTH + visibleWidth(SEPARATOR) + visibleWidth(label);
135388
- const cells = Math.max(1, Math.min(42, width - bareFixed));
135750
+ const used = visibleWidth(isLast ? BRANCH_LAST : BRANCH_FIRST) + visibleWidth(label);
135751
+ const cells = Math.max(1, width - used);
135389
135752
  const summary = chalk.hex(this.colors.roleThinking)(t("activitygroup.thinking_summary", { summary: truncateToWidth(first, cells, "…") }));
135390
- return `${BRANCH_LAST} ${chalk.dim("·")} ${summary}`;
135753
+ return new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${summary}`, 0, 0);
135391
135754
  }
135392
135755
  /**
135393
135756
  * Token estimate for the whole block: the reasoning text plus every result the
135394
135757
  * block owns, i.e. how much material this stretch of work moved through.
135395
135758
  */
135396
135759
  blockTokens() {
135397
- let tokens = this.thinkingTokens();
135398
- for (const entry of this.tools) tokens += this.toolTokens(entry.tc);
135760
+ let tokens = 0;
135761
+ for (const segment of this.segments) if (segment.kind === "tool") tokens += this.toolTokens(segment.tc);
135762
+ else if (segment.kind === "thinking") tokens += this.thinkingTokens(segment);
135399
135763
  return tokens;
135400
135764
  }
135401
135765
  /** Result tokens of one card, counted once per distinct result text. */
@@ -135416,14 +135780,14 @@ var ActivityGroupComponent = class extends Container {
135416
135780
  * the streaming speed gauge so both numbers agree. Empty reasoning counts as
135417
135781
  * zero: the estimator's floor of one token is for per-delta rates.
135418
135782
  */
135419
- thinkingTokens() {
135420
- const cached = this.thinkingTokenCache;
135421
- if (cached !== void 0 && cached.text === this.thinkingText) return cached.tokens;
135422
- const tokens = countTokens(this.thinkingText);
135423
- this.thinkingTokenCache = {
135424
- text: this.thinkingText,
135783
+ thinkingTokens(segment) {
135784
+ const cached = this.thinkingTokenCache.get(segment);
135785
+ if (cached !== void 0 && cached.text === segment.text) return cached.tokens;
135786
+ const tokens = countTokens(segment.text);
135787
+ this.thinkingTokenCache.set(segment, {
135788
+ text: segment.text,
135425
135789
  tokens
135426
- };
135790
+ });
135427
135791
  return tokens;
135428
135792
  }
135429
135793
  formatTokens(tokens) {
@@ -135442,7 +135806,7 @@ var ActivityGroupComponent = class extends Container {
135442
135806
  this.spinnerFrame = (this.spinnerFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
135443
135807
  this.lastSpinnerTickAt += steps * 80;
135444
135808
  const width = this.renderedWidth ?? 80;
135445
- if (!this.expanded && this.tools.some((entry) => entry.tc.resultView === void 0)) this.rebuild(width);
135809
+ if (!this.expanded && this.segments.some((segment) => segment.kind === "tool" && segment.tc.resultView === void 0)) this.rebuild(width);
135446
135810
  else this.headerText.setText(this.buildHeader(width));
135447
135811
  this.ui?.requestRender();
135448
135812
  }
@@ -135666,244 +136030,6 @@ var AssistantMessageComponent = class {
135666
136030
  }
135667
136031
  };
135668
136032
  //#endregion
135669
- //#region src/tui/components/messages/background-agent-status.ts
135670
- var BackgroundAgentStatusComponent = class {
135671
- data;
135672
- colors;
135673
- bullet;
135674
- textComponent;
135675
- cachedWidth;
135676
- cachedLines;
135677
- constructor(data, colors) {
135678
- this.data = data;
135679
- this.colors = colors;
135680
- const tone = data.phase === "started" ? colors.primary : data.phase === "completed" ? colors.success : colors.error;
135681
- this.bullet = data.phase === "failed" ? chalk.hex(tone)(FAILURE_MARK) : chalk.hex(tone)(STATUS_BULLET);
135682
- const text = chalk.hex(tone)(data.headline) + (data.detail !== void 0 && data.detail.length > 0 ? chalk.hex(colors.textDim)(` (${data.detail})`) : "");
135683
- this.textComponent = new Text(text, 0, 0);
135684
- }
135685
- invalidate() {
135686
- this.cachedWidth = void 0;
135687
- this.cachedLines = void 0;
135688
- }
135689
- render(width) {
135690
- if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
135691
- const contentWidth = Math.max(1, width - 2);
135692
- const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? this.bullet : " ") + line)];
135693
- this.cachedWidth = width;
135694
- this.cachedLines = lines;
135695
- return lines;
135696
- }
135697
- };
135698
- //#endregion
135699
- //#region src/tui/components/messages/read-group.ts
135700
- const THROTTLE_MS = 200;
135701
- const CONFLICT_MARKER_RE = /^(<<<<<<<|=======|>>>>>>>)/;
135702
- function detectConflictMarkers(contentLines) {
135703
- for (const raw of contentLines) {
135704
- const tabIdx = raw.indexOf(" ");
135705
- const line = tabIdx >= 0 ? raw.slice(tabIdx + 1) : raw;
135706
- if (CONFLICT_MARKER_RE.test(line)) return true;
135707
- }
135708
- return false;
135709
- }
135710
- function parseReadGroupOutput(output) {
135711
- const results = [];
135712
- const lines = output.split("\n");
135713
- let currentPath;
135714
- let currentLines = [];
135715
- const finishSection = (path, contentLines) => {
135716
- if (path === void 0) return;
135717
- const text = contentLines.join("\n");
135718
- if (text.trimStart().startsWith("[ERROR]")) {
135719
- results.push({
135720
- filePath: path,
135721
- lines: 0,
135722
- failed: true
135723
- });
135724
- return;
135725
- }
135726
- const systemMatch = text.match(/<system>([\s\S]*?)<\/system>\s*$/);
135727
- let lineCount = 0;
135728
- if (systemMatch !== null) {
135729
- const systemBody = systemMatch[1];
135730
- if (systemBody !== void 0) {
135731
- const lineMatch = systemBody.match(/(\d+)\s+lines?\s+read\s+from\s+file/);
135732
- if (lineMatch !== null) {
135733
- const count = lineMatch[1];
135734
- if (count !== void 0) lineCount = parseInt(count, 10);
135735
- }
135736
- }
135737
- }
135738
- const hasConflicts = detectConflictMarkers(contentLines);
135739
- results.push({
135740
- filePath: path,
135741
- lines: lineCount,
135742
- failed: false,
135743
- hasConflicts
135744
- });
135745
- };
135746
- for (const line of lines) {
135747
- const match = line.match(/^---\s+(.+?)\s+---$/);
135748
- if (match !== null) {
135749
- finishSection(currentPath, currentLines);
135750
- currentPath = match[1];
135751
- currentLines = [];
135752
- } else if (currentPath !== void 0) currentLines.push(line);
135753
- }
135754
- finishSection(currentPath, currentLines);
135755
- return results;
135756
- }
135757
- var ReadGroupComponent = class extends Container {
135758
- colors;
135759
- ui;
135760
- entries = [];
135761
- directResults;
135762
- headerText;
135763
- bodyContainer;
135764
- throttleTimer = null;
135765
- lastFlushPhases = /* @__PURE__ */ new Map();
135766
- constructor(colors, ui) {
135767
- super();
135768
- this.colors = colors;
135769
- this.ui = ui;
135770
- this.addChild(new Spacer(1));
135771
- this.headerText = new Text("", 0, 0);
135772
- this.addChild(this.headerText);
135773
- this.bodyContainer = new Container();
135774
- this.addChild(this.bodyContainer);
135775
- }
135776
- size() {
135777
- return this.directResults?.length ?? this.entries.length;
135778
- }
135779
- setResults(results) {
135780
- this.directResults = results;
135781
- this.flushRender();
135782
- }
135783
- /**
135784
- * Borrows a standalone `ToolCallComponent` into the group as a hidden state
135785
- * container. Snapshot changes trigger throttled refreshes. Re-attaching the
135786
- * same toolCallId is a no-op.
135787
- */
135788
- attach(toolCallId, tc) {
135789
- if (this.entries.some((e) => e.toolCallId === toolCallId)) return;
135790
- this.entries.push({
135791
- toolCallId,
135792
- tc
135793
- });
135794
- tc.setSnapshotListener(() => {
135795
- this.scheduleRender();
135796
- });
135797
- this.flushRender();
135798
- }
135799
- /**
135800
- * The pending -> done/failed transition is the important visible change, so
135801
- * it refreshes immediately. Other changes are throttled.
135802
- */
135803
- scheduleRender() {
135804
- if (this.detectPhaseTransition()) {
135805
- this.flushRender();
135806
- return;
135807
- }
135808
- if (this.throttleTimer !== null) return;
135809
- this.throttleTimer = setTimeout(() => {
135810
- this.throttleTimer = null;
135811
- this.flushRender();
135812
- }, THROTTLE_MS);
135813
- }
135814
- detectPhaseTransition() {
135815
- for (const e of this.entries) {
135816
- const phase = e.tc.getReadSnapshot().phase;
135817
- if (this.lastFlushPhases.get(e.toolCallId) !== phase) return true;
135818
- }
135819
- return false;
135820
- }
135821
- flushRender() {
135822
- if (this.throttleTimer !== null) {
135823
- clearTimeout(this.throttleTimer);
135824
- this.throttleTimer = null;
135825
- }
135826
- if (this.directResults !== void 0) {
135827
- this.renderDirectResults();
135828
- return;
135829
- }
135830
- const snapshots = this.entries.map((e) => e.tc.getReadSnapshot());
135831
- let pending = 0;
135832
- let failed = 0;
135833
- let totalLines = 0;
135834
- for (const snap of snapshots) if (snap.phase === "pending") pending += 1;
135835
- else if (snap.phase === "failed") failed += 1;
135836
- else totalLines += snap.lines;
135837
- this.headerText.setText(this.buildHeader(snapshots.length, pending, failed, totalLines));
135838
- this.bodyContainer.clear();
135839
- const visibleSnapshots = snapshots.filter((snap) => snap.filePath !== void 0 && snap.filePath.length > 0);
135840
- visibleSnapshots.forEach((snap, idx) => {
135841
- const isLast = idx === visibleSnapshots.length - 1;
135842
- this.addBodyBranch(snap.filePath ?? "", isLast, this.buildBodyTail(snap));
135843
- });
135844
- this.lastFlushPhases.clear();
135845
- this.entries.forEach((entry, i) => {
135846
- const snap = snapshots[i];
135847
- if (snap !== void 0) this.lastFlushPhases.set(entry.toolCallId, snap.phase);
135848
- });
135849
- this.invalidate();
135850
- this.ui?.requestRender();
135851
- }
135852
- renderDirectResults() {
135853
- const results = this.directResults ?? [];
135854
- let failed = 0;
135855
- let totalLines = 0;
135856
- for (const r of results) if (r.failed) failed += 1;
135857
- else totalLines += r.lines;
135858
- this.headerText.setText(this.buildHeader(results.length, 0, failed, totalLines));
135859
- this.bodyContainer.clear();
135860
- results.forEach((r, idx) => {
135861
- const isLast = idx === results.length - 1;
135862
- this.addBodyBranch(r.filePath, isLast, this.buildDirectBodyTail(r));
135863
- });
135864
- this.invalidate();
135865
- this.ui?.requestRender();
135866
- }
135867
- buildHeader(total, pending, failed, totalLines) {
135868
- const colors = this.colors;
135869
- const dim = chalk.dim;
135870
- if (pending > 0) return `${chalk.hex(colors.roleAssistant)(STATUS_BULLET)}${chalk.hex(colors.primary).bold(t("readgroup.reading", { count: total }))}`;
135871
- if (failed === total) return `${chalk.hex(colors.error)("✗ ")}${chalk.hex(colors.error).bold(t("readgroup.read", { count: total }))}${chalk.hex(colors.error)(t("readgroup.failed_suffix"))}`;
135872
- return `${chalk.hex(colors.success)(STATUS_BULLET)}${chalk.hex(colors.primary).bold(t("readgroup.read", { count: total }))}${dim(t("readgroup.lines_suffix", { count: totalLines }))}${failed > 0 ? chalk.hex(colors.error)(t("readgroup.failed_count", { count: failed })) : ""}`;
135873
- }
135874
- /**
135875
- * Renders one tree-branch body line with a hanging-indent continuation,
135876
- * so a path longer than the viewport keeps its branch connector instead
135877
- * of breaking the tree.
135878
- */
135879
- addBodyBranch(path, isLast, tail) {
135880
- const prefix = ` ${isLast ? "└─" : "├─"} `;
135881
- const continuation = isLast ? " " : " │ ";
135882
- this.bodyContainer.addChild(new WrappedLine(prefix, continuation, `${chalk.hex(this.colors.text)(path)}${tail}`));
135883
- }
135884
- buildBodyTail(snap) {
135885
- const dim = chalk.dim;
135886
- if (snap.phase === "pending") return dim(t("readgroup.reading_suffix"));
135887
- if (snap.phase === "failed") return chalk.hex(this.colors.error)(t("readgroup.failed_suffix"));
135888
- return dim(t("readgroup.lines_suffix", { count: snap.lines }));
135889
- }
135890
- buildDirectBodyTail(result) {
135891
- const dim = chalk.dim;
135892
- if (result.failed) return chalk.hex(this.colors.error)(t("readgroup.failed_suffix"));
135893
- let tail = dim(t("readgroup.lines_suffix", { count: result.lines }));
135894
- if (result.hasConflicts) tail = `${tail}${chalk.hex(this.colors.warning)(` ⚠ ${t("readgroup.conflict_suffix")}`)}`;
135895
- return tail;
135896
- }
135897
- /** Releases throttle timers so destroyed components cannot refresh later. */
135898
- dispose() {
135899
- if (this.throttleTimer !== null) {
135900
- clearTimeout(this.throttleTimer);
135901
- this.throttleTimer = null;
135902
- }
135903
- for (const e of this.entries) e.tc.setSnapshotListener(void 0);
135904
- }
135905
- };
135906
- //#endregion
135907
136033
  //#region src/tui/components/messages/skill-activation.ts
135908
136034
  /**
135909
136035
  * Skill activation card.
@@ -137058,6 +137184,76 @@ const readMediaSummary = (toolCall, result, ctx) => {
137058
137184
  return out;
137059
137185
  };
137060
137186
  //#endregion
137187
+ //#region src/tui/components/messages/tool-renderers/read-group-list.ts
137188
+ const CONFLICT_MARKER_RE = /^(<<<<<<<|=======|>>>>>>>)/;
137189
+ function detectConflictMarkers(contentLines) {
137190
+ for (const raw of contentLines) {
137191
+ const tabIdx = raw.indexOf(" ");
137192
+ const line = tabIdx >= 0 ? raw.slice(tabIdx + 1) : raw;
137193
+ if (CONFLICT_MARKER_RE.test(line)) return true;
137194
+ }
137195
+ return false;
137196
+ }
137197
+ /** Parses the `--- path ---` sections of the batch reader's output. */
137198
+ function parseReadGroupOutput(output) {
137199
+ const results = [];
137200
+ const lines = output.split("\n");
137201
+ let currentPath;
137202
+ let currentLines = [];
137203
+ const finishSection = (path, contentLines) => {
137204
+ if (path === void 0) return;
137205
+ const text = contentLines.join("\n");
137206
+ if (text.trimStart().startsWith("[ERROR]")) {
137207
+ results.push({
137208
+ filePath: path,
137209
+ lines: 0,
137210
+ failed: true,
137211
+ hasConflicts: false
137212
+ });
137213
+ return;
137214
+ }
137215
+ let lineCount = 0;
137216
+ for (const match of text.matchAll(/(\d+)\s+lines?\s+read\s+from\s+file/g)) {
137217
+ const count = match[1];
137218
+ if (count !== void 0) lineCount = parseInt(count, 10);
137219
+ }
137220
+ const hasConflicts = detectConflictMarkers(contentLines);
137221
+ results.push({
137222
+ filePath: path,
137223
+ lines: lineCount,
137224
+ failed: false,
137225
+ hasConflicts
137226
+ });
137227
+ };
137228
+ for (const line of lines) {
137229
+ const match = line.match(/^---\s+(.+?)\s+---$/);
137230
+ if (match !== null) {
137231
+ finishSection(currentPath, currentLines);
137232
+ currentPath = match[1];
137233
+ currentLines = [];
137234
+ } else if (currentPath !== void 0) currentLines.push(line);
137235
+ }
137236
+ finishSection(currentPath, currentLines);
137237
+ return results;
137238
+ }
137239
+ const readGroupListSummary = (toolCall, result, ctx) => {
137240
+ if (result.is_error) return renderTruncated(toolCall, result, ctx);
137241
+ if (!ctx.expanded) return [];
137242
+ const entries = parseReadGroupOutput(result.output);
137243
+ if (entries.length === 0) return renderTruncated(toolCall, result, ctx);
137244
+ const shown = entries.slice(0, 15);
137245
+ const rows = shown.map((entry) => {
137246
+ const path = chalk.hex(ctx.colors.text)(entry.filePath);
137247
+ if (entry.failed) return `${path}${chalk.hex(ctx.colors.error)(t("readgroup.failed_suffix"))}`;
137248
+ let tail = chalk.dim(t("readgroup.lines_suffix", { count: String(entry.lines) }));
137249
+ if (entry.hasConflicts) tail += chalk.hex(ctx.colors.warning)(` ⚠ ${t("readgroup.conflict_suffix")}`);
137250
+ return `${path}${tail}`;
137251
+ });
137252
+ const remaining = entries.length - shown.length;
137253
+ if (remaining > 0) rows.push(chalk.dim(t("readgroup.more_files", { count: String(remaining) })));
137254
+ return [new Text(rows.map((row) => ` ${row}`).join("\n"), 0, 0)];
137255
+ };
137256
+ //#endregion
137061
137257
  //#region src/tui/components/messages/tool-renderers/chip.ts
137062
137258
  /**
137063
137259
  * Header chip providers — produce a short "stat" suffix appended to the
@@ -137120,6 +137316,18 @@ const editChip = (toolCall) => {
137120
137316
  };
137121
137317
  const writeChip = (toolCall) => formatWriteChip(computeWriteStats(toolCall.args));
137122
137318
  const readChip = (_toolCall, result) => pluralize(countNonEmptyLines(result.output), "line");
137319
+ const readGroupChip = (_toolCall, result) => {
137320
+ const entries = parseReadGroupOutput(result.output);
137321
+ if (entries.length === 0) return "";
137322
+ const failed = entries.filter((entry) => entry.failed).length;
137323
+ const files = pluralize(entries.length, "file");
137324
+ if (failed === entries.length) return `${files} · failed`;
137325
+ const lines = entries.reduce((sum, entry) => entry.failed ? sum : sum + entry.lines, 0);
137326
+ const parts = [files];
137327
+ if (lines > 0) parts.push(pluralize(lines, "line"));
137328
+ if (failed > 0) parts.push(`${String(failed)} failed`);
137329
+ return parts.join(" · ");
137330
+ };
137123
137331
  const grepChip = (_toolCall, result) => {
137124
137332
  const matches = countNonEmptyLines(result.output);
137125
137333
  if (matches === 0) return "no matches";
@@ -137142,6 +137350,7 @@ const REGISTRY = {
137142
137350
  Edit: editChip,
137143
137351
  Write: writeChip,
137144
137352
  Read: readChip,
137353
+ ReadGroup: readGroupChip,
137145
137354
  ReadMediaFile: readMediaChip,
137146
137355
  Grep: grepChip,
137147
137356
  Glob: globChip,
@@ -137397,6 +137606,7 @@ const globSummary = withGlance(globGlance);
137397
137606
  function pickResultRenderer(toolName) {
137398
137607
  switch (toolName) {
137399
137608
  case "Read": return readSummary;
137609
+ case "ReadGroup": return readGroupListSummary;
137400
137610
  case "ReadMediaFile": return readMediaSummary;
137401
137611
  case "Grep": return grepSummary;
137402
137612
  case "Glob": return globSummary;
@@ -137741,12 +137951,12 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
137741
137951
  writeStreamLang;
137742
137952
  static WRITE_STREAM_TAIL_RAW_CHARS = 4096;
137743
137953
  /**
137744
- * Registered by a group container (`AgentGroupComponent` or
137745
- * `ReadGroupComponent`) when this component is borrowed as a hidden state
137746
- * container. Any state change (subagent meta, phase, sub-tool, result, etc.)
137747
- * triggers a throttled group re-render. `undefined` means no group is
137748
- * subscribed and standalone rendering is unaffected. A ToolCallComponent can
137749
- * only belong to one group at a time, so one listener slot is enough.
137954
+ * Registered by a group container (`AgentGroupComponent`, the activity block)
137955
+ * when this component is borrowed as a hidden state container. Any state
137956
+ * change (subagent meta, phase, sub-tool, result, etc.) triggers a throttled
137957
+ * group re-render. `undefined` means no group is subscribed and standalone
137958
+ * rendering is unaffected. A ToolCallComponent can only belong to one group at
137959
+ * a time, so one listener slot is enough.
137750
137960
  */
137751
137961
  onSnapshotChange;
137752
137962
  static HEADER_END_INDEX = 2;
@@ -137922,10 +138132,10 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
137922
138132
  this.ui?.requestRender();
137923
138133
  }
137924
138134
  /**
137925
- * Lets group containers (AgentGroup or ReadGroup) subscribe to this card's
137926
- * state changes. Registration immediately calls back so the group receives
137927
- * the current snapshot without separately calling getSubagentSnapshot or
137928
- * getReadSnapshot. Pass `undefined` to unsubscribe.
138135
+ * Lets group containers (the agent group, the activity block) subscribe to
138136
+ * this card's state changes. Registration immediately calls back so the group
138137
+ * receives the current state without separately calling
138138
+ * getSubagentSnapshot. Pass `undefined` to unsubscribe.
137929
138139
  */
137930
138140
  setSnapshotListener(cb) {
137931
138141
  this.onSnapshotChange = cb;
@@ -137952,35 +138162,6 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
137952
138162
  latestActivity
137953
138163
  };
137954
138164
  }
137955
- /**
137956
- * Used by `ReadGroupComponent` to sum line counts across same-step Read
137957
- * cards. `lines` matches the single-card chip
137958
- * (`pluralize(countNonEmptyLines(...), 'line')`) so group and card counts do
137959
- * not drift.
137960
- */
137961
- getReadSnapshot() {
137962
- const args = this.toolCall.args;
137963
- const filePathRaw = args["file_path"] ?? args["path"];
137964
- const filePath = typeof filePathRaw === "string" ? makeWorkspaceRelativePath(filePathRaw, this.workspaceDir) : void 0;
137965
- if (this.result === void 0) return {
137966
- toolCallId: this.toolCall.id,
137967
- filePath,
137968
- phase: "pending",
137969
- lines: 0
137970
- };
137971
- if (this.result.is_error === true) return {
137972
- toolCallId: this.toolCall.id,
137973
- filePath,
137974
- phase: "failed",
137975
- lines: 0
137976
- };
137977
- return {
137978
- toolCallId: this.toolCall.id,
137979
- filePath,
137980
- phase: "done",
137981
- lines: countNonEmptyLines(this.result.output)
137982
- };
137983
- }
137984
138165
  get toolCallView() {
137985
138166
  return this.toolCall;
137986
138167
  }
@@ -138791,77 +138972,6 @@ function formatActivityLine(verb, toolName, args, workspaceDir) {
138791
138972
  return keyArg ? `${verb} ${toolName} (${keyArg})` : `${verb} ${toolName}`;
138792
138973
  }
138793
138974
  //#endregion
138794
- //#region src/tui/utils/ui-preferences.ts
138795
- /**
138796
- * TUI-local UI preferences persisted to `<dataDir>/ui-preferences.json`.
138797
- * Distinct from agent/SDK config files (which are owned by core) — this file
138798
- * stores lightweight view-state knobs owned by the TUI itself, e.g. whether
138799
- * the empty-session provider hint has been dismissed by the user.
138800
- */
138801
- const EMPTY = {};
138802
- function getUiPreferencesPath() {
138803
- return join(getDataDir(), SCREAM_CODE_UI_PREFERENCES_FILE_NAME);
138804
- }
138805
- function readUiPreferences() {
138806
- try {
138807
- const file = getUiPreferencesPath();
138808
- if (!existsSync(file)) return { ...EMPTY };
138809
- const raw = readFileSync(file, "utf8");
138810
- const parsed = JSON.parse(raw);
138811
- return {
138812
- ...EMPTY,
138813
- ...parsed
138814
- };
138815
- } catch {
138816
- return { ...EMPTY };
138817
- }
138818
- }
138819
- function writeUiPreferences(prefs) {
138820
- try {
138821
- const file = getUiPreferencesPath();
138822
- mkdirSync(dirname$1(file), { recursive: true });
138823
- writeFileSync(file, JSON.stringify(prefs, null, 2), "utf8");
138824
- } catch {}
138825
- }
138826
- function isEmptySessionHintDismissed() {
138827
- return readUiPreferences().emptySessionHintDismissed === true;
138828
- }
138829
- /** Toggle the empty-session hint on/off (Ctrl+B). Returns the new state:
138830
- * true = hidden, false = shown. Persisted immediately. */
138831
- function toggleEmptySessionHint() {
138832
- const prefs = readUiPreferences();
138833
- const dismissed = prefs.emptySessionHintDismissed !== true;
138834
- prefs.emptySessionHintDismissed = dismissed;
138835
- writeUiPreferences(prefs);
138836
- return dismissed;
138837
- }
138838
- /** Whether the per-turn elapsed marker is enabled (default on). */
138839
- function isTurnElapsedEnabled() {
138840
- return readUiPreferences().turnElapsedEnabled !== false;
138841
- }
138842
- /** Toggle the per-turn elapsed marker via /snaptimer. Returns the new state:
138843
- * true = shown, false = hidden. Persisted immediately. */
138844
- function toggleTurnElapsed() {
138845
- const prefs = readUiPreferences();
138846
- const enabled = prefs.turnElapsedEnabled !== false;
138847
- prefs.turnElapsedEnabled = !enabled;
138848
- writeUiPreferences(prefs);
138849
- return !enabled;
138850
- }
138851
- /** Whether user messages render with a highlight background block (default on). */
138852
- function isUserMessageHighlightEnabled() {
138853
- return readUiPreferences().userMessageHighlightEnabled !== false;
138854
- }
138855
- /** Toggle the user-message highlight block via /hl. Returns the new state:
138856
- * true = highlighted, false = default (no background). Persisted immediately. */
138857
- function toggleUserMessageHighlight() {
138858
- const prefs = readUiPreferences();
138859
- const enabled = prefs.userMessageHighlightEnabled !== false;
138860
- prefs.userMessageHighlightEnabled = !enabled;
138861
- writeUiPreferences(prefs);
138862
- return !enabled;
138863
- }
138864
- //#endregion
138865
138975
  //#region src/tui/components/messages/user-message.ts
138866
138976
  var UserMessageComponent = class {
138867
138977
  color;
@@ -139052,7 +139162,7 @@ function isRevokeAnchorComponent(child) {
139052
139162
  return child instanceof UserMessageComponent || child instanceof SkillActivationComponent;
139053
139163
  }
139054
139164
  function isRevokeContextComponent(child) {
139055
- return child instanceof UserMessageComponent || child instanceof AssistantMessageComponent || child instanceof ThinkingComponent || child instanceof ToolCallComponent || child instanceof ActivityGroupComponent || child instanceof AgentGroupComponent || child instanceof ReadGroupComponent || child instanceof SkillActivationComponent || child instanceof BackgroundAgentStatusComponent || child instanceof StatusMessageComponent || child instanceof CompactionComponent;
139165
+ return child instanceof UserMessageComponent || child instanceof AssistantMessageComponent || child instanceof ThinkingComponent || child instanceof ToolCallComponent || child instanceof ActivityGroupComponent || child instanceof AgentGroupComponent || child instanceof SkillActivationComponent || child instanceof BackgroundAgentStatusComponent || child instanceof StatusMessageComponent || child instanceof CompactionComponent;
139056
139166
  }
139057
139167
  function renderWelcome(host) {
139058
139168
  if (host.state.transcriptContainer.children.some((child) => child instanceof WelcomeComponent)) return;
@@ -141021,18 +141131,6 @@ function isExpandable(obj) {
141021
141131
  function isPlanExpandable(obj) {
141022
141132
  return typeof obj === "object" && obj !== null && "setPlanExpanded" in obj && typeof obj.setPlanExpanded === "function";
141023
141133
  }
141024
- /**
141025
- * Reads a component's current expansion state. Components that only accept
141026
- * `setExpanded` (or that return something other than a boolean) yield
141027
- * undefined, so callers fall back to their own last value instead of guessing.
141028
- */
141029
- function readExpanded(obj) {
141030
- if (typeof obj !== "object" || obj === null || !("isExpanded" in obj)) return void 0;
141031
- const reader = obj.isExpanded;
141032
- if (typeof reader !== "function") return void 0;
141033
- const value = reader.call(obj);
141034
- return typeof value === "boolean" ? value : void 0;
141035
- }
141036
141134
  function hasDispose(value) {
141037
141135
  return typeof value === "object" && value !== null && "dispose" in value && typeof value.dispose === "function";
141038
141136
  }
@@ -141602,6 +141700,88 @@ async function handleHighlightCommand(host, _args) {
141602
141700
  host.state.ui.requestRender();
141603
141701
  }
141604
141702
  //#endregion
141703
+ //#region src/tui/utils/transcript-repaint.ts
141704
+ function repaintTranscript(host) {
141705
+ for (const child of host.state.transcriptContainer.children) {
141706
+ const repaintable = child;
141707
+ if (typeof repaintable.invalidate === "function") repaintable.invalidate();
141708
+ }
141709
+ host.state.ui.requestRender();
141710
+ }
141711
+ //#endregion
141712
+ //#region src/tui/commands/blockrows.ts
141713
+ /**
141714
+ * /blockrows — Tune the activity block's row budgets: how tall a collapsed block
141715
+ * is, how much of a tool result and of a reasoning run an expanded block shows.
141716
+ *
141717
+ * Two steps: pick the budget, then pick the value. Choices are persisted to
141718
+ * ui-preferences.json (so they survive restarts) and applied to the transcript
141719
+ * that is already on screen, not just to blocks created afterwards.
141720
+ */
141721
+ function showSettingPicker(host) {
141722
+ host.mountEditorReplacement(new ChoicePickerComponent({
141723
+ title: t("blockrows.title"),
141724
+ hint: t("blockrows.hint"),
141725
+ colors: host.state.theme.colors,
141726
+ options: ACTIVITY_LINE_SETTINGS.map((setting) => ({
141727
+ value: setting.key,
141728
+ label: t(setting.label),
141729
+ description: t("blockrows.current", { value: String(getActivityLineValue(setting.key)) })
141730
+ })),
141731
+ onSelect: (value) => {
141732
+ if (isActivityLineKey(value)) showValuePicker(host, value);
141733
+ },
141734
+ onCancel: () => {
141735
+ host.restoreEditor();
141736
+ }
141737
+ }));
141738
+ }
141739
+ function showValuePicker(host, key) {
141740
+ const setting = findActivityLineSetting(key);
141741
+ host.mountEditorReplacement(new ChoicePickerComponent({
141742
+ title: t(setting.label),
141743
+ hint: t("blockrows.value_hint"),
141744
+ currentValue: String(getActivityLineValue(key)),
141745
+ colors: host.state.theme.colors,
141746
+ options: setting.values.map((value) => ({
141747
+ value: String(value),
141748
+ label: String(value)
141749
+ })),
141750
+ onSelect: (value) => {
141751
+ const parsed = Number(value);
141752
+ if (!Number.isFinite(parsed)) return;
141753
+ setActivityLineValue(key, parsed);
141754
+ host.restoreEditor();
141755
+ host.showStatus(t("blockrows.saved", {
141756
+ label: t(setting.label),
141757
+ value: String(parsed)
141758
+ }), host.state.theme.colors.success);
141759
+ repaintTranscript(host);
141760
+ },
141761
+ onCancel: () => {
141762
+ showSettingPicker(host);
141763
+ }
141764
+ }));
141765
+ }
141766
+ async function handleBlockRowsCommand(host, _args) {
141767
+ showSettingPicker(host);
141768
+ }
141769
+ //#endregion
141770
+ //#region src/tui/commands/codebg.ts
141771
+ /**
141772
+ * /codebg — Toggle the background panel behind fenced code blocks.
141773
+ *
141774
+ * On (default): each code line is painted as a full-width band, which separates
141775
+ * code from prose at a glance. Off: the band disappears and code is set apart by
141776
+ * its own colour, the one-cell gutter and the language label only.
141777
+ * Persisted to ui-preferences.json, so it survives restarts.
141778
+ */
141779
+ async function handleCodeBgCommand(host, _args) {
141780
+ const enabled = toggleCodeBlockPanel();
141781
+ host.showStatus(enabled ? t("codebg.enabled") : t("codebg.disabled"), enabled ? host.state.theme.colors.success : host.state.theme.colors.textDim);
141782
+ repaintTranscript(host);
141783
+ }
141784
+ //#endregion
141605
141785
  //#region src/tui/commands/like.ts
141606
141786
  function promptTextInput$1(host, title, opts) {
141607
141787
  const { promise, resolve } = Promise.withResolvers();
@@ -144446,6 +144626,12 @@ async function handleBuiltInSlashCommand(host, name, args) {
144446
144626
  case "hl":
144447
144627
  await handleHighlightCommand(host, args);
144448
144628
  return;
144629
+ case "blockrows":
144630
+ await handleBlockRowsCommand(host, args);
144631
+ return;
144632
+ case "codebg":
144633
+ await handleCodeBgCommand(host, args);
144634
+ return;
144449
144635
  case "mcp":
144450
144636
  await handleMcpCommand(host, args);
144451
144637
  return;
@@ -144579,4 +144765,4 @@ async function handleBuiltInSlashCommand(host, name, args) {
144579
144765
  }
144580
144766
  }
144581
144767
  //#endregion
144582
- export { getBreathingFrame as $, encodeWorkDirKey as $n, isStreaming as $t, ToolCallComponent as A, setExperimentalFlags as An, showUsage as At, sanitizeShellOutput as B, detectShellEnvironment as Bn, DISABLE_TERMINAL_THEME_REPORTING as Bt, handleCcCommand as C, getCtrlDHint as Cn, showModelPicker as Ct, isEmptySessionHintDismissed as D, BUILTIN_SLASH_COMMANDS as Dn, supportsBalance as Dt, UserMessageComponent as E, buildSkillSlashCommands as En, refreshProviderBalance as Et, CachedContainer as F, PULSE_WAVE_FRAMES as Fn, contrastTextHex as Ft, AssistantMessageComponent as G, CLI_COMMAND_NAME as Gn, OSC11_RESPONSE_PREFIX as Gt, ReadGroupComponent as H, getInputHistoryFile as Hn, ENABLE_TERMINAL_THEME_REPORTING as Ht, ThinkingComponent as I, TuiConfigParseError as In, getColorPalette as It, estimateTokens as J, PRODUCT_NAME as Jn, TERMINAL_FOCUS_IN as Jt, ActivityGroupComponent as K, CLI_UI_MODE as Kn, OSC11_RESPONSE_PREFIX_NO_ESC as Kt, NoticeMessageComponent as L, TuiLikePreferencesSchema as Ln, detectTerminalTheme as Lt, renderDiffLinesClustered as M, isEmbeddingModelCached as Mn, createEditorTheme as Mt, highlightLines as N, startManualEmbeddingDownload as Nn, createMarkdownTheme as Nt, isTurnElapsedEnabled as O, sortSlashCommands as On, clearInfoPanelState as Ot, langFromPath as P, PIXEL_PULSE_FRAMES as Pn, createThemeStyles as Pt, BREATHE_CYCLE_MS as Q, ScreamHarness as Qn, isBusy as Qt, StatusMessageComponent as R, loadTuiConfig as Rn, parseOsc11BackgroundTheme as Rt, readJsonlFile as S, getCtrlCHint as Sn, handleYoloCommand as St, handleRevokeCommand as T, getNoActiveSessionMessage as Tn, showSettingsSelector as Tt, parseReadGroupOutput as U, getLogDir as Un, OSC11_QUERY as Ut, SkillActivationComponent as V, getDataDir as Vn, ENABLE_TERMINAL_FOCUS_REPORTING as Vt, BackgroundAgentStatusComponent as W, detectInstallSource as Wn, OSC11_RESPONSE as Wt, AgentGroupComponent as X, fetchCatalog as Xn, TERMINAL_THEME_DARK as Xt, getSharedSpeedTracker as Y, DEFAULT_CATALOG_URL as Yn, TERMINAL_FOCUS_OUT as Yt, WelcomeComponent as Z, saveCatalogCache as Zn, TERMINAL_THEME_LIGHT as Zt, handleUpdateCommand as _, EMPTY_SESSION_HINT_URL as _n, handleFusionPlanCommand as _t, handleExtensionCommand as a, handleConnectCommand as an, resolveScreamHome as ar, handleExportMdCommand as at, readUpdateCache as b, SESSION_TIPS as bn, handleThemeCommand as bt, hasDispose as c, STATUS_BULLET as cn, log as cr, handleTitleCommand as ct, readExpanded as d, formatErrorMessage as dn, isOrphanedToolCallError as dr, changeThinkingLevel as dt, FooterComponent as en, normalizeWorkDir as er, resetBreathingClock as et, MoonLoader as f, isTodoItemShape as fn, ErrorCodes as fr, getModelCycleLevel as ft, handleMcpCommand as g, truncateErrorMessage as gn, handleEditorCommand as gt, handleChannelCommand as h, stringValue as hn, handleCompactCommand as ht, buildRoleAdditionalText as i, handleSearchCommand as in, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as ir, handleExportDebugZipCommand as it, renderDiffLines as j, getKnowledgeStore as jn, resolveThemeSync as jt, toggleEmptySessionHint as k, isExperimentalFlagEnabled as kn, showStatusReport as kt, isExpandable as l, appendStreamingArgsPreview as ln, resolveGlobalLogPath as lr, isInternalMessage as lt, handleMemoryCommand as m, serializeToolResultOutput as mn, handleBotCommand as mt, clearEvalPanelState as n, SubagentSlots as nn, readSessionIndex as nr, clearGoalState as nt, handleSkillCommand as o, handleLogoutCommand as on, MemoryMemoStore as or, handleForkCommand as ot, formatMemoryMemoForInjection as p, parseStreamingArgs as pn, SCREAM_ERROR_INFO as pr, handleAutoCommand as pt, charsForTokenBudget as q, CLI_USER_AGENT_PRODUCT as qn, QUERY_TERMINAL_THEME as qt, openUrl as r, handleTraceCommand as rn, removeSessionIndexEntry as rr, refineGoal as rt, disposeChildren as s, printableChar as sn, flushDiagnosticLogs as sr, handleInitCommand as st, dispatchInput as t, lerpGradient as tn, appendSessionIndexEntry as tr, CompactionComponent as tt, isPlanExpandable as u, argsRecord as un, isScreamError as ur, toTerminalHyperlink as ut, selectUpdateTarget as v, EXIT_CONFIRM_WINDOW_MS as vn, handleModelCommand as vt, getDaemonInstructions as w, getLlmNotSetMessage as wn, showPermissionPicker as wt, appendJsonlLine as x, TIP_ROTATION_INTERVAL_MS as xn, handleWolfpackCommand as xt, refreshUpdateCache as y, MAIN_AGENT_ID$1 as yn, handlePlanCommand as yt, replaceTabs as z, saveTuiConfig as zn, DISABLE_TERMINAL_FOCUS_REPORTING as zt };
144768
+ export { handleExportMdCommand as $, readSessionIndex as $n, SubagentSlots as $t, langFromPath as A, startManualEmbeddingDownload as An, toggleEmptySessionHint as At, charsForTokenBudget as B, getLogDir as Bn, OSC11_QUERY as Bt, getDaemonInstructions as C, buildSkillSlashCommands as Cn, showStatusReport as Ct, renderDiffLines as D, setExperimentalFlags as Dn, createMarkdownTheme as Dt, ToolCallComponent as E, isExperimentalFlagEnabled as En, createEditorTheme as Et, replaceTabs as F, loadTuiConfig as Fn, parseOsc11BackgroundTheme as Ft, WelcomeComponent as G, PRODUCT_NAME as Gn, TERMINAL_FOCUS_IN as Gt, getSharedSpeedTracker as H, CLI_COMMAND_NAME as Hn, OSC11_RESPONSE_PREFIX as Ht, sanitizeShellOutput as I, saveTuiConfig as In, DISABLE_TERMINAL_FOCUS_REPORTING as It, resetBreathingClock as J, saveCatalogCache as Jn, TERMINAL_THEME_LIGHT as Jt, BREATHE_CYCLE_MS as K, DEFAULT_CATALOG_URL as Kn, TERMINAL_FOCUS_OUT as Kt, SkillActivationComponent as L, detectShellEnvironment as Ln, DISABLE_TERMINAL_THEME_REPORTING as Lt, ThinkingComponent as M, PULSE_WAVE_FRAMES as Mn, contrastTextHex as Mt, NoticeMessageComponent as N, TuiConfigParseError as Nn, getColorPalette as Nt, renderDiffLinesClustered as O, getKnowledgeStore as On, isEmptySessionHintDismissed as Ot, StatusMessageComponent as P, TuiLikePreferencesSchema as Pn, detectTerminalTheme as Pt, handleExportDebugZipCommand as Q, appendSessionIndexEntry as Qn, lerpGradient as Qt, AssistantMessageComponent as R, getDataDir as Rn, ENABLE_TERMINAL_FOCUS_REPORTING as Rt, handleCcCommand as S, getNoActiveSessionMessage as Sn, clearInfoPanelState as St, UserMessageComponent as T, sortSlashCommands as Tn, resolveThemeSync as Tt, BackgroundAgentStatusComponent as U, CLI_UI_MODE as Un, OSC11_RESPONSE_PREFIX_NO_ESC as Ut, estimateTokens as V, detectInstallSource as Vn, OSC11_RESPONSE as Vt, AgentGroupComponent as W, CLI_USER_AGENT_PRODUCT as Wn, QUERY_TERMINAL_THEME as Wt, clearGoalState as X, encodeWorkDirKey as Xn, isStreaming as Xt, CompactionComponent as Y, ScreamHarness as Yn, isBusy as Yt, refineGoal as Z, normalizeWorkDir as Zn, FooterComponent as Zt, selectUpdateTarget as _, SESSION_TIPS as _n, showModelPicker as _t, handleExtensionCommand as a, STATUS_BULLET as an, log as ar, changeThinkingLevel as at, appendJsonlLine as b, getCtrlDHint as bn, refreshProviderBalance as bt, hasDispose as c, formatErrorMessage as cn, isOrphanedToolCallError as cr, handleBotCommand as ct, MoonLoader as d, serializeToolResultOutput as dn, handleFusionPlanCommand as dt, handleTraceCommand as en, removeSessionIndexEntry as er, handleForkCommand as et, formatMemoryMemoForInjection as f, stringValue as fn, handleModelCommand as ft, handleUpdateCommand as g, MAIN_AGENT_ID$1 as gn, handleYoloCommand as gt, handleMcpCommand as h, EXIT_CONFIRM_WINDOW_MS as hn, handleWolfpackCommand as ht, buildRoleAdditionalText as i, printableChar as in, flushDiagnosticLogs as ir, toTerminalHyperlink as it, CachedContainer as j, PIXEL_PULSE_FRAMES as jn, createThemeStyles as jt, highlightLines as k, isEmbeddingModelCached as kn, isTurnElapsedEnabled as kt, isExpandable as l, isTodoItemShape as ln, ErrorCodes as lr, handleCompactCommand as lt, handleChannelCommand as m, EMPTY_SESSION_HINT_URL as mn, handleThemeCommand as mt, clearEvalPanelState as n, handleConnectCommand as nn, resolveScreamHome as nr, handleTitleCommand as nt, handleSkillCommand as o, appendStreamingArgsPreview as on, resolveGlobalLogPath as or, getModelCycleLevel as ot, handleMemoryCommand as p, truncateErrorMessage as pn, handlePlanCommand as pt, getBreathingFrame as q, fetchCatalog as qn, TERMINAL_THEME_DARK as qt, openUrl as r, handleLogoutCommand as rn, MemoryMemoStore as rr, isInternalMessage as rt, disposeChildren as s, argsRecord as sn, isScreamError as sr, handleAutoCommand as st, dispatchInput as t, handleSearchCommand as tn, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as tr, handleInitCommand as tt, isPlanExpandable as u, parseStreamingArgs as un, SCREAM_ERROR_INFO as ur, handleEditorCommand as ut, refreshUpdateCache as v, TIP_ROTATION_INTERVAL_MS as vn, showPermissionPicker as vt, handleRevokeCommand as w, BUILTIN_SLASH_COMMANDS as wn, showUsage as wt, readJsonlFile as x, getLlmNotSetMessage as xn, supportsBalance as xt, readUpdateCache as y, getCtrlCHint as yn, showSettingsSelector as yt, ActivityGroupComponent as z, getInputHistoryFile as zn, ENABLE_TERMINAL_THEME_REPORTING as zt };