opencode-context-tree 0.1.1 → 0.2.1

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/tui.js CHANGED
@@ -66,7 +66,17 @@ function contextSizeOf(messages) {
66
66
  }
67
67
  return { tokens: lastAssistantInput + newer, estimated: newer > 0 };
68
68
  }
69
- function bandFor(tokens) {
69
+ function bandFor(tokens, limit) {
70
+ if (limit && limit > 0) {
71
+ const r = tokens / limit;
72
+ if (r < 0.25)
73
+ return "low";
74
+ if (r < 0.6)
75
+ return "healthy";
76
+ if (r < 0.85)
77
+ return "filling";
78
+ return "red";
79
+ }
70
80
  if (tokens < 8000)
71
81
  return "low";
72
82
  if (tokens < 32000)
@@ -75,6 +85,12 @@ function bandFor(tokens) {
75
85
  return "filling";
76
86
  return "red";
77
87
  }
88
+ function contextBar(tokens, limit, cells = 5) {
89
+ if (!(limit > 0))
90
+ return "";
91
+ const filled = tokens <= 0 ? 0 : Math.min(cells, Math.max(1, Math.round(tokens / limit * cells)));
92
+ return "\u2593".repeat(filled) + "\u2591".repeat(cells - filled);
93
+ }
78
94
  function formatK(tokens) {
79
95
  if (tokens < 1000)
80
96
  return String(tokens);
@@ -82,7 +98,8 @@ function formatK(tokens) {
82
98
  return `${k.endsWith(".0") ? k.slice(0, -2) : k}k`;
83
99
  }
84
100
  function formatContext(size, limit) {
85
- return `ctx ${size.estimated ? "~" : ""}${formatK(size.tokens)}${limit ? `/${formatK(limit)}` : ""} \xB7 ${bandFor(size.tokens)}`;
101
+ const bar = limit ? `${contextBar(size.tokens, limit)} ` : "";
102
+ return `ctx ${bar}${size.estimated ? "~" : ""}${formatK(size.tokens)}${limit ? `/${formatK(limit)}` : ""} \xB7 ${bandFor(size.tokens, limit)}`;
86
103
  }
87
104
 
88
105
  // src/shared/store.ts
@@ -19113,6 +19130,8 @@ function debug(event, data) {
19113
19130
 
19114
19131
  // src/tui/actions.ts
19115
19132
  import { createSignal } from "solid-js";
19133
+ import fs4 from "fs";
19134
+ import path3 from "path";
19116
19135
 
19117
19136
  // src/core/decision.ts
19118
19137
  var DECISION_SYSTEM = "You write concise engineering decision records. You are given the transcript of a side branch of a coding session. Do NOT continue the conversation. Output ONLY the record in the exact markdown template requested, nothing else.";
@@ -19181,6 +19200,55 @@ function decisionMessageText(record2, branchName) {
19181
19200
  return body.startsWith("## Decision:") ? `\u25C6 ${body}` : `\u25C6 ## Decision: ${branchName}
19182
19201
  ${body}`;
19183
19202
  }
19203
+ var BULLET = /^(\s*)[-*\u2022]\s+/;
19204
+ var HEADING = /^(\s*(?:\u25C6\s*)?)#{1,6}\s*/;
19205
+ function stripMarkdown(text) {
19206
+ return text.replace(/[*`]/g, "");
19207
+ }
19208
+ function wrapLine(body, width, marker) {
19209
+ const room = Math.max(1, width - marker.length);
19210
+ const indent = " ".repeat(marker.length);
19211
+ const lines = [];
19212
+ let current = "";
19213
+ for (const raw of body.split(/\s+/).filter(Boolean)) {
19214
+ const word = raw.length > room ? `${raw.slice(0, Math.max(1, room - 1))}\u2026` : raw;
19215
+ if (current === "")
19216
+ current = word;
19217
+ else if (current.length + 1 + word.length <= room)
19218
+ current += ` ${word}`;
19219
+ else {
19220
+ lines.push(current);
19221
+ current = word;
19222
+ }
19223
+ }
19224
+ lines.push(current);
19225
+ return lines.map((line, i) => `${i === 0 ? marker : indent}${line}`.trimEnd());
19226
+ }
19227
+ function renderDecision(text, width) {
19228
+ const out = [];
19229
+ for (const raw of text.replace(/\r/g, "").trim().split(`
19230
+ `)) {
19231
+ const line = raw.trimEnd();
19232
+ if (line.trim() === "") {
19233
+ if (out.length > 0 && out[out.length - 1] !== "")
19234
+ out.push("");
19235
+ continue;
19236
+ }
19237
+ const bullet = BULLET.exec(line);
19238
+ const marker = bullet ? `${bullet[1]}- ` : "";
19239
+ const body = stripMarkdown(bullet ? line.slice(bullet[0].length) : line.replace(HEADING, "$1"));
19240
+ out.push(...wrapLine(body, width, marker));
19241
+ }
19242
+ return out;
19243
+ }
19244
+ function decisionSummary(text) {
19245
+ const lines = text.replace(/\r/g, "").split(`
19246
+ `).map((l) => stripMarkdown(l.replace(/^\s*\u25C6\s*/, "").replace(HEADING, "$1").replace(BULLET, "")).trim());
19247
+ const heading = lines.find((l) => /^Decision:\s*\S/i.test(l));
19248
+ const title = heading ? heading.replace(/^Decision:\s*/i, "").trim() : lines.find((l) => l !== "") ?? "";
19249
+ const outcome = lines.find((l) => /^Outcome:\s*\S/i.test(l))?.replace(/^Outcome:\s*/i, "").trim();
19250
+ return outcome ? { title, outcome } : { title };
19251
+ }
19184
19252
  function openSiblings(state, sessionID) {
19185
19253
  const me = state.sessions[sessionID];
19186
19254
  if (!me)
@@ -19562,19 +19630,67 @@ async function draftWithHelper(ctx, input2) {
19562
19630
  }
19563
19631
  }
19564
19632
  var MERGE_TRUST = "Your transcript is never rewritten; the record is appended to the trunk as a normal message.";
19633
+ var UNDO_KEY = "u";
19565
19634
  var MERGE_GATE_NOTICE = `Edit the \u25C6 decision record, then save to confirm (empty file or a non-zero exit aborts the merge).
19566
19635
  ${MERGE_TRUST}`;
19567
- function mergeDialogTitle(branchName, trunkTitle) {
19568
- return `Merge \u2387 ${branchName} \u2192 ${trunkTitle ? clip(trunkTitle, 28) : "the trunk"}`;
19636
+ var DISCARD_NOTICE = `${MERGE_TRUST}
19637
+ The branch is only marked rejected \u2014 ${UNDO_KEY} (alias x) undoes it.`;
19638
+ var TRUNK_LABEL = "trunk";
19639
+ function mergeTargetOf(label, messages) {
19640
+ const minimal = messages.map((m) => ({ info: m.role === "assistant" ? { role: "assistant", tokens: m.tokens } : { role: "user" }, parts: m.parts }));
19641
+ return { label, turns: messages.filter((m) => m.role === "user").length, tokens: contextSizeOf(minimal).tokens };
19642
+ }
19643
+ function ownTurnCount(messages, anchor2) {
19644
+ const anchorIndex = anchor2.messageID ? anchor2.parentMessageIDs.indexOf(anchor2.messageID) : -1;
19645
+ return messages.slice(anchorIndex + 1).filter((m) => m.role === "user").length;
19646
+ }
19647
+ function mergeDialogTitle(branchName, target2) {
19648
+ if (!target2 || typeof target2 === "string")
19649
+ return `Merge \u2387 ${branchName} \u2192 the trunk`;
19650
+ const where = target2.label === TRUNK_LABEL ? TRUNK_LABEL : `\u2387 ${clip(target2.label, 24)}`;
19651
+ return `Merge \u2387 ${branchName} \u2192 ${where} (${plural(target2.turns, "turn")}, ~${formatK(target2.tokens)})`;
19569
19652
  }
19570
19653
  function mergeDialogOptions(input2) {
19654
+ const folds = input2.turns === undefined ? "the branch" : plural(input2.turns, "turn");
19571
19655
  return [
19572
- { title: "Squash", value: "squash", description: "drafts a \u25C6 decision record you confirm" },
19573
- { title: "Squash without LLM", value: "squash-no-llm", description: "you write the record yourself" },
19574
- { title: "Discard", value: "discard", description: "rejected; nothing lands in the trunk" },
19656
+ { title: "Squash", value: "squash", description: `1 model call \xB7 folds ${folds} into one \u25C6 record` },
19657
+ { title: "Squash without LLM", value: "squash-no-llm", description: "you write it \xB7 no model call" },
19658
+ { title: "Discard", value: "discard", description: "rejected \xB7 nothing lands in the trunk" },
19575
19659
  ...input2.siblings > 0 ? [{ title: "Tournament", value: "tournament", description: "compare sibling branches and keep one" }] : []
19576
19660
  ];
19577
19661
  }
19662
+ function confirmDialog(ctx, title, message) {
19663
+ return new Promise((resolve) => {
19664
+ ctx.api.ui.dialog.replace(() => ctx.api.ui.DialogConfirm({
19665
+ title,
19666
+ message,
19667
+ onConfirm: () => {
19668
+ resolve(true);
19669
+ ctx.api.ui.dialog.clear();
19670
+ },
19671
+ onCancel: () => {
19672
+ resolve(false);
19673
+ ctx.api.ui.dialog.clear();
19674
+ }
19675
+ }), () => resolve(false));
19676
+ });
19677
+ }
19678
+ function promptDialog(ctx, title, placeholder) {
19679
+ return new Promise((resolve) => {
19680
+ ctx.api.ui.dialog.replace(() => ctx.api.ui.DialogPrompt({
19681
+ title,
19682
+ placeholder,
19683
+ onConfirm: (value) => {
19684
+ resolve(value);
19685
+ ctx.api.ui.dialog.clear();
19686
+ },
19687
+ onCancel: () => {
19688
+ resolve(undefined);
19689
+ ctx.api.ui.dialog.clear();
19690
+ }
19691
+ }), () => resolve(undefined));
19692
+ });
19693
+ }
19578
19694
  async function mergeBranch(ctx, input2) {
19579
19695
  const treeId = ctx.store.ensureTree(input2.sessionID, "tui");
19580
19696
  const state = ctx.store.stateFor(treeId);
@@ -19585,16 +19701,33 @@ async function mergeBranch(ctx, input2) {
19585
19701
  const name = branch.name ?? "branch";
19586
19702
  debug("merge.start", { mode: input2.mode, sessionID: input2.sessionID, parentID });
19587
19703
  await abortIfBusy(ctx, input2.sessionID);
19704
+ const parentMsgs = await ctx.api.client.session.messages({ sessionID: parentID, directory: ctx.directory }).catch(() => {
19705
+ return;
19706
+ });
19707
+ const parentMessageIDs = (parentMsgs?.data ?? []).map((m) => String(m.info.id));
19708
+ const own2 = await fetchOwnTranscript(ctx, input2.sessionID);
19709
+ const turns = ownTurnCount(own2.messages, { messageID: branch.anchorMessageID, parentMessageIDs });
19588
19710
  if (input2.mode === "discard") {
19589
- record2(ctx, treeId, "branch.closed", { sessionID: input2.sessionID, status: "rejected", note: input2.note });
19711
+ const ok = await confirmDialog(ctx, `Discard \u2387 ${name} (${plural(turns, "turn")})?`, DISCARD_NOTICE);
19712
+ if (!ok) {
19713
+ ctx.api.ui.toast({ variant: "warning", message: `\u2387 ${name} kept \u2014 nothing discarded` });
19714
+ return;
19715
+ }
19716
+ let note = input2.note;
19717
+ if (note === undefined) {
19718
+ const answer = await promptDialog(ctx, "Why? (optional note on the close marker)", "dead end");
19719
+ if (answer === undefined) {
19720
+ ctx.api.ui.toast({ variant: "warning", message: `\u2387 ${name} kept \u2014 nothing discarded` });
19721
+ return;
19722
+ }
19723
+ note = answer.trim() || undefined;
19724
+ }
19725
+ record2(ctx, treeId, "branch.closed", { sessionID: input2.sessionID, status: "rejected", note });
19590
19726
  await mirrorMetadata(ctx, input2.sessionID, { status: "rejected" });
19591
19727
  navigateToSession(ctx, parentID);
19592
19728
  ctx.api.ui.toast({ variant: "success", message: `\u2387 ${name} discarded \u2014 back on the trunk` });
19593
19729
  return parentID;
19594
19730
  }
19595
- const parentMsgs = await ctx.api.client.session.messages({ sessionID: parentID, directory: ctx.directory });
19596
- const parentMessageIDs = (parentMsgs.data ?? []).map((m) => String(m.info.id));
19597
- const own2 = await fetchOwnTranscript(ctx, input2.sessionID);
19598
19731
  const transcript = branchTranscriptText(own2, { messageID: branch.anchorMessageID, parentMessageIDs });
19599
19732
  const model = branch.branchModel ?? branch.trunkModel;
19600
19733
  const modelRef = model ? { providerID: model.split("/")[0], modelID: model.split("/").slice(1).join("/") } : undefined;
@@ -19654,23 +19787,43 @@ async function fetchOwnTranscript(ctx, sessionID) {
19654
19787
  return { sessionID, title: sessionID, status: "available", messages };
19655
19788
  }
19656
19789
  var BRANCH_DIALOG = { title: "Branch here \u2192 new OpenCode session", placeholder: "name, e.g. try-redis", modelTitle: "Model for this branch (Enter keeps the current one)" };
19790
+ var COPY_HINT = ".opencode/context-tree/last-copy.txt";
19791
+ function copyText(api2, text, directory) {
19792
+ const renderer = api2.renderer;
19793
+ if (text && renderer?.copyToClipboardOSC52?.(text))
19794
+ return { target: "clipboard", hint: "clipboard" };
19795
+ const file2 = path3.join(directory, COPY_HINT);
19796
+ fs4.mkdirSync(path3.dirname(file2), { recursive: true });
19797
+ fs4.writeFileSync(file2, text);
19798
+ return { target: "file", hint: COPY_HINT };
19799
+ }
19657
19800
  function clip(text, max) {
19658
19801
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
19659
19802
  }
19803
+ function plural(n, noun) {
19804
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
19805
+ }
19660
19806
 
19661
19807
  // src/tui/route.tsx
19662
- import { createComponent as _$createComponent } from "@opentui/solid";
19663
- import { effect as _$effect } from "@opentui/solid";
19664
- import { createTextNode as _$createTextNode } from "@opentui/solid";
19665
19808
  import { memo as _$memo } from "@opentui/solid";
19809
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
19810
+ import { effect as _$effect } from "@opentui/solid";
19811
+ import { createComponent as _$createComponent } from "@opentui/solid";
19666
19812
  import { insertNode as _$insertNode } from "@opentui/solid";
19667
19813
  import { insert as _$insert } from "@opentui/solid";
19668
19814
  import { setProp as _$setProp } from "@opentui/solid";
19669
19815
  import { createElement as _$createElement } from "@opentui/solid";
19816
+
19817
+ // src/shared/version.ts
19818
+ var PLUGIN_VERSION = "0.2.1";
19819
+
19820
+ // src/tui/route.tsx
19670
19821
  import { For, Show, createEffect, createMemo, createSignal as createSignal2, on, onCleanup } from "solid-js";
19671
19822
 
19672
19823
  // src/core/actions.ts
19673
19824
  function planJump(row, ctx) {
19825
+ if (row.kind === "separator")
19826
+ return { kind: "noop", reason: "nothing to go to" };
19674
19827
  if (row.kind === "branch")
19675
19828
  return row.sessionID === ctx.currentSessionID ? { kind: "noop", reason: "you are here" } : { kind: "switch", sessionID: row.sessionID };
19676
19829
  const tr = ctx.transcripts[row.sessionID];
@@ -19698,15 +19851,33 @@ function planJump(row, ctx) {
19698
19851
  }
19699
19852
 
19700
19853
  // src/core/navigation.ts
19854
+ function scan(rows, index, dir) {
19855
+ for (let i = index;i >= 0 && i < rows.length; i += dir)
19856
+ if (rows[i].kind !== "separator")
19857
+ return i;
19858
+ return -1;
19859
+ }
19701
19860
  function moveSelection(rows, index, delta) {
19702
19861
  if (rows.length === 0)
19703
19862
  return -1;
19704
- const next = index + delta;
19705
- if (next < 0)
19706
- return 0;
19707
- if (next > rows.length - 1)
19708
- return rows.length - 1;
19709
- return next;
19863
+ const dir = delta < 0 ? -1 : 1;
19864
+ let i = Math.min(Math.max(index, 0), rows.length - 1);
19865
+ for (let step = Math.abs(delta);step > 0; step--) {
19866
+ const next = scan(rows, i + dir, dir);
19867
+ if (next === -1)
19868
+ break;
19869
+ i = next;
19870
+ }
19871
+ if (rows[i].kind !== "separator")
19872
+ return i;
19873
+ const ahead = scan(rows, i, dir);
19874
+ return ahead === -1 ? scan(rows, i, dir === 1 ? -1 : 1) : ahead;
19875
+ }
19876
+ function firstIndex(rows) {
19877
+ return scan(rows, 0, 1);
19878
+ }
19879
+ function lastIndex(rows) {
19880
+ return scan(rows, rows.length - 1, -1);
19710
19881
  }
19711
19882
  function nextBranchIndex(rows, index, dir) {
19712
19883
  if (rows.length === 0)
@@ -19729,39 +19900,40 @@ function toggleExpanded(expanded, sessionID) {
19729
19900
  next.add(sessionID);
19730
19901
  return next;
19731
19902
  }
19732
- var FILTER_ORDER = ["default", "no-tools", "user-only", "labeled", "all"];
19733
- function cycleFilter(filter) {
19734
- const idx = FILTER_ORDER.indexOf(filter);
19735
- return FILTER_ORDER[(idx + 1) % FILTER_ORDER.length];
19736
- }
19737
19903
  function resolveSelection(view, preferredId, currentRowId, previousIndex) {
19738
19904
  if (view.rows.length === 0)
19739
19905
  return -1;
19906
+ const land = (i) => {
19907
+ const ahead = scan(view.rows, i, 1);
19908
+ return ahead === -1 ? scan(view.rows, i, -1) : ahead;
19909
+ };
19740
19910
  if (preferredId !== undefined) {
19741
19911
  const exact = view.indexById[preferredId];
19742
19912
  if (exact !== undefined)
19743
- return exact;
19913
+ return land(exact);
19744
19914
  const owner = preferredId.split(":").slice(0, 2).join(":");
19745
19915
  for (let i = 0;i < view.rows.length; i++) {
19746
19916
  const row = view.rows[i];
19917
+ if (row.kind === "separator")
19918
+ continue;
19747
19919
  const rowOwner = row.kind === "branch" ? row.id : `${row.sessionID}:${row.messageID}`;
19748
19920
  if (rowOwner === owner)
19749
19921
  return i;
19750
19922
  }
19751
19923
  }
19752
19924
  if (previousIndex !== undefined && previousIndex >= 0)
19753
- return Math.min(previousIndex, view.rows.length - 1);
19925
+ return land(Math.min(previousIndex, view.rows.length - 1));
19754
19926
  if (currentRowId !== undefined) {
19755
19927
  const idx = view.indexById[currentRowId];
19756
19928
  if (idx !== undefined)
19757
- return idx;
19929
+ return land(idx);
19758
19930
  }
19759
19931
  if (view.currentRowId !== undefined) {
19760
19932
  const idx = view.indexById[view.currentRowId];
19761
19933
  if (idx !== undefined)
19762
- return idx;
19934
+ return land(idx);
19763
19935
  }
19764
- return 0;
19936
+ return land(0);
19765
19937
  }
19766
19938
 
19767
19939
  // src/core/transcript.ts
@@ -19833,6 +20005,7 @@ function messagePreview(message) {
19833
20005
  }
19834
20006
 
19835
20007
  // src/core/tree.ts
20008
+ var OFF_PATH_TEXT = "\u2500\u2500 not in this branch's context \u2500\u2500";
19836
20009
  var WARN_TOKENS = 1e4;
19837
20010
  function userText(message) {
19838
20011
  return message.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
@@ -19959,7 +20132,7 @@ function branchAllowed(filter) {
19959
20132
  function isCropped(ctx, messageID, partID) {
19960
20133
  return ctx.crops.some((c) => c.messageID === messageID && (c.partID === undefined || c.partID === partID));
19961
20134
  }
19962
- function emitAssistantRows(ctx, sessionID, message, depth, gutter, out) {
20135
+ function emitAssistantRows(ctx, sessionID, message, depth, gutter, inContext, out) {
19963
20136
  if (message.summary) {
19964
20137
  if (!stepAllowed(ctx.filter, "text"))
19965
20138
  return;
@@ -19985,19 +20158,29 @@ function emitAssistantRows(ctx, sessionID, message, depth, gutter, out) {
19985
20158
  durationMs: durationOfParts(message.parts),
19986
20159
  isError: false,
19987
20160
  isCropped: false,
19988
- warn: tokens >= WARN_TOKENS
20161
+ warn: tokens >= WARN_TOKENS,
20162
+ inContext
19989
20163
  });
19990
20164
  return;
19991
20165
  }
20166
+ const collapseThinking = ctx.filter !== "all";
20167
+ const rows = [];
20168
+ let thinkingMs;
19992
20169
  let first = true;
19993
20170
  for (const part of message.parts) {
19994
20171
  const kind = stepKind(part);
20172
+ if (collapseThinking && kind === "reasoning") {
20173
+ const ms = durationOfPart(part);
20174
+ if (ms !== undefined)
20175
+ thinkingMs = (thinkingMs ?? 0) + ms;
20176
+ continue;
20177
+ }
19995
20178
  const label = first ? ctx.labels[message.id] : undefined;
19996
20179
  if (!stepAllowed(ctx.filter, kind, Boolean(label)))
19997
20180
  continue;
19998
20181
  const { tokens, estimated } = stepTokensFor(part, message);
19999
20182
  first = false;
20000
- out.push({
20183
+ rows.push({
20001
20184
  kind: "step",
20002
20185
  id: `${sessionID}:${message.id}:${part.id}`,
20003
20186
  sessionID,
@@ -20013,9 +20196,40 @@ function emitAssistantRows(ctx, sessionID, message, depth, gutter, out) {
20013
20196
  isError: part.state?.status === "error",
20014
20197
  isCropped: isCropped(ctx, message.id, part.id),
20015
20198
  warn: tokens >= WARN_TOKENS,
20016
- label
20199
+ label,
20200
+ inContext
20017
20201
  });
20018
20202
  }
20203
+ if (rows.length > 0) {
20204
+ if (thinkingMs !== undefined)
20205
+ rows[0].thinkingMs = thinkingMs;
20206
+ } else if (collapseThinking) {
20207
+ const thinking = message.parts.filter((p) => stepKind(p) === "reasoning");
20208
+ const label = ctx.labels[message.id];
20209
+ if (thinking.length > 0 && stepAllowed(ctx.filter, "reasoning", Boolean(label))) {
20210
+ const tokens = thinking.reduce((sum, p) => sum + stepTokensFor(p, message).tokens, 0);
20211
+ rows.push({
20212
+ kind: "step",
20213
+ id: `${sessionID}:${message.id}:${thinking[0].id}`,
20214
+ sessionID,
20215
+ messageID: message.id,
20216
+ partID: thinking[0].id,
20217
+ depth,
20218
+ gutter,
20219
+ glyph: "\u25CB",
20220
+ preview: partPreview(thinking[0]),
20221
+ tokens,
20222
+ estimated: thinking.some((p) => stepTokensFor(p, message).estimated),
20223
+ durationMs: thinkingMs,
20224
+ isError: false,
20225
+ isCropped: isCropped(ctx, message.id, thinking[0].id),
20226
+ warn: tokens >= WARN_TOKENS,
20227
+ label,
20228
+ inContext
20229
+ });
20230
+ }
20231
+ }
20232
+ out.push(...rows);
20019
20233
  }
20020
20234
  function shownExpanded(ctx, sessionID) {
20021
20235
  const flagged = ctx.expanded.has(sessionID);
@@ -20076,11 +20290,21 @@ function emitChildBranches(ctx, sessionID, anchorMessageID, depth, gutter, out)
20076
20290
  pushBranch(ctx, branch, depth, gutter, i === visible.length - 1, showHeaders, out);
20077
20291
  });
20078
20292
  }
20293
+ function forkAnchorOf(ctx, sessionID) {
20294
+ const childID = ctx.onPathChild.get(sessionID);
20295
+ return childID ? ctx.state.sessions[childID]?.anchorMessageID : undefined;
20296
+ }
20079
20297
  function walkSession(ctx, sessionID, messages, depth, gutter, turnStart, out) {
20080
20298
  const lastUserIndex = findLastUserIndex(messages, ctx.filter);
20081
20299
  const counter = { turn: turnStart };
20082
20300
  let inPluginCommand = false;
20301
+ const forkAnchor = forkAnchorOf(ctx, sessionID);
20302
+ let inContext = ctx.onPath.has(sessionID);
20303
+ let separatorDue = inContext && forkAnchor === "";
20304
+ if (separatorDue)
20305
+ inContext = false;
20083
20306
  messages.forEach((message, i) => {
20307
+ const before = out.length;
20084
20308
  if (message.role === "user") {
20085
20309
  inPluginCommand = hiddenPluginTurn(ctx.filter, message);
20086
20310
  if (!inPluginCommand) {
@@ -20106,14 +20330,23 @@ function walkSession(ctx, sessionID, messages, depth, gutter, turnStart, out) {
20106
20330
  isCurrent: sessionID === ctx.currentSessionID,
20107
20331
  isTip: i === lastUserIndex,
20108
20332
  isDecision,
20109
- isSummary
20333
+ isSummary,
20334
+ inContext
20110
20335
  });
20111
20336
  }
20112
20337
  }
20113
20338
  } else if (!inPluginCommand) {
20114
- emitAssistantRows(ctx, sessionID, message, depth, gutter, out);
20339
+ emitAssistantRows(ctx, sessionID, message, depth, gutter, inContext, out);
20115
20340
  }
20116
20341
  emitChildBranches(ctx, sessionID, message.id, depth, gutter, out);
20342
+ if (separatorDue && out.length > before) {
20343
+ out.splice(before, 0, { kind: "separator", id: `separator:${sessionID}`, depth, gutter, text: OFF_PATH_TEXT });
20344
+ separatorDue = false;
20345
+ }
20346
+ if (inContext && message.id === forkAnchor) {
20347
+ inContext = false;
20348
+ separatorDue = true;
20349
+ }
20117
20350
  });
20118
20351
  }
20119
20352
  function rowSearchFields(row) {
@@ -20124,6 +20357,8 @@ function rowSearchFields(row) {
20124
20357
  return row.label ? [row.preview, row.label] : [row.preview];
20125
20358
  case "branch":
20126
20359
  return row.model ? [row.name, row.model, row.status] : [row.name, row.status];
20360
+ case "separator":
20361
+ return [];
20127
20362
  }
20128
20363
  }
20129
20364
  function rowMatches(row, needle) {
@@ -20156,17 +20391,17 @@ function applySearch(rows, search) {
20156
20391
  return rows.filter((_, i) => keep[i]);
20157
20392
  }
20158
20393
  function computeTotalTokens(transcript) {
20159
- let lastIndex = -1;
20394
+ let lastIndex2 = -1;
20160
20395
  let lastInput = 0;
20161
20396
  transcript.messages.forEach((m, idx) => {
20162
20397
  if (m.role === "assistant" && typeof m.tokens?.input === "number") {
20163
- lastIndex = idx;
20398
+ lastIndex2 = idx;
20164
20399
  lastInput = m.tokens.input;
20165
20400
  }
20166
20401
  });
20167
20402
  let counted = 0;
20168
20403
  let guessed = 0;
20169
- for (let i = Math.max(lastIndex, 0);i < transcript.messages.length; i++) {
20404
+ for (let i = Math.max(lastIndex2, 0);i < transcript.messages.length; i++) {
20170
20405
  const m = transcript.messages[i];
20171
20406
  if (m.role === "user") {
20172
20407
  guessed += estimateTokens(userText(m));
@@ -20179,7 +20414,7 @@ function computeTotalTokens(transcript) {
20179
20414
  if (p.type === "tool" || typeof output2 !== "number")
20180
20415
  guessed += estimateTokens(partText2(p));
20181
20416
  }
20182
- return { tokens: lastInput + counted + guessed, estimated: lastIndex === -1 || guessed > 0 };
20417
+ return { tokens: lastInput + counted + guessed, estimated: lastIndex2 === -1 || guessed > 0 };
20183
20418
  }
20184
20419
  function currentChainOf(state, currentSessionID) {
20185
20420
  return [...ancestorChainOf(state, currentSessionID), currentSessionID];
@@ -20231,7 +20466,7 @@ function buildTreeView(o) {
20231
20466
  let currentRowId;
20232
20467
  for (let i = rows.length - 1;i >= 0; i--) {
20233
20468
  const r = rows[i];
20234
- if (r.kind !== "branch" && r.sessionID === o.currentSessionID) {
20469
+ if ((r.kind === "turn" || r.kind === "step") && r.sessionID === o.currentSessionID) {
20235
20470
  currentRowId = r.id;
20236
20471
  break;
20237
20472
  }
@@ -20323,111 +20558,141 @@ function spanOf(m) {
20323
20558
  const end = m.time.completed ?? m.parts.reduce((e, p) => Math.max(e, p.state?.time?.end ?? p.time?.end ?? 0), 0);
20324
20559
  return end > m.time.created ? end - m.time.created : 0;
20325
20560
  }
20326
- function buildLanes(transcript, mode) {
20327
- const columns = [];
20561
+ var EVENT_GLYPH = "\u25AC";
20562
+ function ctreeKindOf2(message) {
20563
+ for (const p of message.parts) {
20564
+ const ctree = p.metadata?.["ctree"];
20565
+ if (ctree?.kind)
20566
+ return ctree.kind;
20567
+ }
20568
+ return;
20569
+ }
20570
+ function isContextMessage(message) {
20571
+ return message.summary === true || ctreeKindOf2(message) === "summary";
20572
+ }
20573
+ function messageEvent(message, turn) {
20574
+ const ms = spanOf(message);
20575
+ return {
20576
+ lane: "input",
20577
+ kind: isContextMessage(message) ? "context" : "user",
20578
+ messageID: message.id,
20579
+ turn,
20580
+ startMs: message.time.created,
20581
+ ...ms > 0 ? { durationMs: ms } : {},
20582
+ tokens: message.parts.reduce((s, p) => s + estimateTokens(p.text ?? ""), 0)
20583
+ };
20584
+ }
20585
+ function partEvent(message, part, turn) {
20586
+ const t = part.state?.time ?? part.time;
20587
+ const base = {
20588
+ messageID: message.id,
20589
+ partID: part.id,
20590
+ turn,
20591
+ startMs: t?.start ?? message.time.created,
20592
+ ...t?.start !== undefined && t?.end !== undefined ? { durationMs: Math.max(0, t.end - t.start) } : {}
20593
+ };
20594
+ if (part.type === "tool")
20595
+ return { ...base, lane: "tools", kind: "tool", error: part.state?.status === "error", tokens: estimateTokens(part.state?.output ?? "") + estimateTokens(JSON.stringify(part.state?.input ?? "")) };
20596
+ return { ...base, lane: "model", kind: part.type === "reasoning" ? "reasoning" : "text", tokens: estimateTokens(part.text ?? "") };
20597
+ }
20598
+ function eventsOf(transcript) {
20599
+ const out = [];
20328
20600
  for (const turn of turnsOf(transcript.messages)) {
20329
- if (mode === "calls") {
20330
- let any2 = false;
20331
- for (const m of turn.assistants) {
20332
- for (const p of m.parts) {
20333
- if (p.type !== "tool")
20334
- continue;
20335
- any2 = true;
20336
- const out = p.state?.output ?? "";
20337
- const t = p.state?.time;
20338
- columns.push({ messageID: m.id, userMessageID: turn.user?.id, partID: p.id, turn: turn.index, input: m.tokens?.input ?? 0, output: 0, tool: estimateTokens(out), toolError: p.state?.status === "error", ms: t?.start !== undefined && t?.end !== undefined ? t.end - t.start : 0 });
20339
- }
20340
- }
20341
- if (!any2) {
20342
- const last2 = turn.assistants.at(-1);
20343
- columns.push({ messageID: last2?.id ?? turn.user?.id ?? "", userMessageID: turn.user?.id, turn: turn.index, input: last2?.tokens?.input ?? 0, output: last2?.tokens?.output ?? 0, tool: 0, toolError: false, ms: last2 ? spanOf(last2) : 0 });
20344
- }
20345
- continue;
20346
- }
20347
- const last = turn.assistants.at(-1);
20348
- let tool = 0;
20349
- let toolError = false;
20350
- let ms = 0;
20351
- let output2 = 0;
20601
+ if (turn.user)
20602
+ out.push(messageEvent(turn.user, turn.index));
20352
20603
  for (const m of turn.assistants) {
20353
- output2 += m.tokens?.output ?? 0;
20354
- ms += spanOf(m);
20604
+ const context = isContextMessage(m);
20605
+ if (context)
20606
+ out.push(messageEvent(m, turn.index));
20355
20607
  for (const p of m.parts) {
20356
- if (p.type !== "tool")
20608
+ const kind = stepKind(p);
20609
+ if (kind === "other" || context && kind !== "tool")
20357
20610
  continue;
20358
- tool += estimateTokens(p.state?.output ?? "");
20359
- if (p.state?.status === "error")
20360
- toolError = true;
20611
+ out.push(partEvent(m, p, turn.index));
20361
20612
  }
20362
20613
  }
20363
- columns.push({ messageID: last?.id ?? turn.user?.id ?? "", userMessageID: turn.user?.id, turn: turn.index, input: last?.tokens?.input ?? 0, output: output2, tool, toolError, ms });
20364
20614
  }
20365
- return { mode, columns };
20615
+ return out;
20366
20616
  }
20367
- var BLOCKS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
20368
- function sparkline(values, width, scale) {
20369
- if (values.length === 0 || width <= 0)
20370
- return "";
20371
- const cells = fitColumns(values, width);
20372
- const max = Math.max(1, scale ?? 0, ...cells);
20373
- return cells.map((v) => v <= 0 ? " " : BLOCKS[Math.min(7, Math.floor(v / max * 7.999))]).join("");
20617
+ function gapBefore(events, i, mode) {
20618
+ return mode === "turns" && events[i].turn !== events[i - 1].turn ? 2 : 1;
20374
20619
  }
20375
- function fitColumns(values, width) {
20376
- if (values.length === 0)
20377
- return [];
20378
- if (values.length === width)
20379
- return values.slice();
20380
- const out = [];
20381
- if (values.length > width) {
20382
- const per = values.length / width;
20383
- for (let i = 0;i < width; i++) {
20384
- const a = Math.floor(i * per);
20385
- const b = Math.max(a + 1, Math.floor((i + 1) * per));
20386
- out.push(Math.max(...values.slice(a, b)));
20387
- }
20388
- return out;
20620
+ function firstFitting(events, mode, width) {
20621
+ let cells = 0;
20622
+ for (let i = events.length - 1;i >= 0; i--) {
20623
+ const next = cells === 0 ? 1 : cells + 1 + gapBefore(events, i + 1, mode);
20624
+ if (next > width)
20625
+ return i + 1;
20626
+ cells = next;
20389
20627
  }
20390
- const rep = width / values.length;
20391
- for (let i = 0;i < width; i++)
20392
- out.push(values[Math.min(values.length - 1, Math.floor(i / rep))]);
20393
- return out;
20628
+ return 0;
20394
20629
  }
20395
- function durationWeighted(lanes, width) {
20396
- const total = lanes.columns.reduce((s, c) => s + Math.max(1, c.ms), 0) || 1;
20397
- const input2 = [];
20398
- const output2 = [];
20399
- const tool = [];
20400
- const toolError = [];
20401
- const owner = [];
20402
- lanes.columns.forEach((c, i) => {
20403
- const cells = Math.max(1, Math.round(Math.max(1, c.ms) / total * width));
20404
- for (let k = 0;k < cells; k++) {
20405
- input2.push(c.input);
20406
- output2.push(c.output);
20407
- tool.push(c.tool);
20408
- toolError.push(c.toolError);
20409
- owner.push(i);
20410
- }
20630
+ function durationWidths(events, budget) {
20631
+ const total = events.reduce((s, e) => s + Math.max(0, e.durationMs ?? 0), 0);
20632
+ if (total <= 0)
20633
+ return events.map(() => 1);
20634
+ const widths = events.map((e) => Math.max(1, Math.round(Math.max(0, e.durationMs ?? 0) / total * budget)));
20635
+ let over = widths.reduce((s, v) => s + v, 0) - budget;
20636
+ while (over > 0) {
20637
+ let widest = -1;
20638
+ for (let i = 0;i < widths.length; i++)
20639
+ if ((widths[i] ?? 0) > 1 && (widest < 0 || (widths[i] ?? 0) > (widths[widest] ?? 0)))
20640
+ widest = i;
20641
+ if (widest < 0)
20642
+ break;
20643
+ widths[widest] = (widths[widest] ?? 1) - 1;
20644
+ over--;
20645
+ }
20646
+ return widths;
20647
+ }
20648
+ function buildEventStrip(transcript, mode, width) {
20649
+ const w = Math.max(0, Math.floor(width));
20650
+ const all = eventsOf(transcript);
20651
+ const truncatedLeft = firstFitting(all, mode, w);
20652
+ const events = all.slice(truncatedLeft);
20653
+ const gaps = events.map((_, i) => i === 0 ? 0 : gapBefore(events, i, mode));
20654
+ const widths = mode === "duration" ? durationWidths(events, w - gaps.reduce((s, g) => s + g, 0)) : events.map(() => 1);
20655
+ const blank = () => Array.from({ length: w }, () => null);
20656
+ const lanes = { input: blank(), model: blank(), tools: blank() };
20657
+ const empty = { input: true, model: true, tools: true };
20658
+ const spans = [];
20659
+ let cursor = 0;
20660
+ events.forEach((e, i) => {
20661
+ const start = cursor + (gaps[i] ?? 0);
20662
+ const end = Math.min(w, start + (widths[i] ?? 1));
20663
+ for (let c = start;c < end; c++)
20664
+ lanes[e.lane][c] = { lane: e.lane, eventIndex: i, glyph: EVENT_GLYPH, ...e.error ? { error: true } : {} };
20665
+ spans.push({ start, end });
20666
+ empty[e.lane] = false;
20667
+ cursor = start + (widths[i] ?? 1);
20411
20668
  });
20412
- return { input: input2, output: output2, tool, toolError, columnAt: (cell) => owner[Math.min(owner.length - 1, Math.max(0, cell))] ?? 0 };
20669
+ return { events, width: w, lanes, spans, empty, truncatedLeft };
20413
20670
  }
20414
- function columnFor(lanes, messageID, partID) {
20671
+ function stripIndexFor(strip, messageID, partID) {
20415
20672
  if (partID) {
20416
- const i2 = lanes.columns.findIndex((c) => c.partID === partID);
20417
- if (i2 >= 0)
20418
- return i2;
20673
+ const byPart = strip.events.findIndex((e) => e.partID === partID);
20674
+ if (byPart >= 0)
20675
+ return byPart;
20419
20676
  }
20420
- const i = lanes.columns.findIndex((c) => c.messageID === messageID || c.userMessageID === messageID);
20421
- return i >= 0 ? i : -1;
20677
+ return strip.events.findIndex((e) => e.messageID === messageID);
20422
20678
  }
20423
20679
 
20424
20680
  // src/core/consumers.ts
20681
+ var THINKING = "(thinking)";
20682
+ var THINKING_NOTE = "provider reasoning \xB7 not croppable";
20425
20683
  function consumers(transcript, opts = {}) {
20426
20684
  const acc = new Map;
20427
- const add = (source, kind, tokens) => {
20428
- const c = acc.get(source) ?? { source, kind, tokens: 0, count: 0, share: 0 };
20685
+ const add = (source, kind, tokens, message, part) => {
20686
+ const c = acc.get(source) ?? { source, kind, tokens: 0, count: 0, share: 0, entries: [] };
20429
20687
  c.tokens += tokens;
20430
20688
  c.count += 1;
20689
+ c.entries.push({
20690
+ messageID: message.id,
20691
+ partID: part.id,
20692
+ tokens,
20693
+ preview: partPreview(part),
20694
+ croppable: part.type === "tool" && part.state?.status === "completed"
20695
+ });
20431
20696
  acc.set(source, c);
20432
20697
  };
20433
20698
  for (const m of transcript.messages) {
@@ -20435,25 +20700,32 @@ function consumers(transcript, opts = {}) {
20435
20700
  if (opts.cropped?.has(p.id))
20436
20701
  continue;
20437
20702
  if (p.type === "tool")
20438
- add(p.tool ?? "tool", "tool", estimateTokens(p.state?.output ?? "") + estimateTokens(JSON.stringify(p.state?.input ?? "")));
20703
+ add(p.tool ?? "tool", "tool", estimateTokens(p.state?.output ?? "") + estimateTokens(JSON.stringify(p.state?.input ?? "")), m, p);
20439
20704
  else if (p.type === "text") {
20440
20705
  const kind = p.metadata?.["ctree"]?.kind;
20441
20706
  if (kind === "decision")
20442
- add("\u25C6 decisions", "decision", estimateTokens(p.text ?? ""));
20707
+ add("\u25C6 decisions", "decision", estimateTokens(p.text ?? ""), m, p);
20443
20708
  else if (kind === "summary")
20444
- add("\u25C7 branch summaries", "summary", estimateTokens(p.text ?? ""));
20709
+ add("\u25C7 branch summaries", "summary", estimateTokens(p.text ?? ""), m, p);
20445
20710
  else if (m.role === "user")
20446
- add("\u25CF user prompts", "user", estimateTokens(p.text ?? ""));
20711
+ add("\u25CF user prompts", "user", estimateTokens(p.text ?? ""), m, p);
20447
20712
  else if (m.summary)
20448
- add("\u25C7 compaction summaries", "summary", estimateTokens(p.text ?? ""));
20713
+ add("\u25C7 compaction summaries", "summary", estimateTokens(p.text ?? ""), m, p);
20449
20714
  else
20450
- add("\u25CB assistant text", "assistant", estimateTokens(p.text ?? ""));
20715
+ add("\u25CB assistant text", "assistant", estimateTokens(p.text ?? ""), m, p);
20451
20716
  } else if (p.type === "reasoning")
20452
- add("(thinking)", "reasoning", estimateTokens(p.text ?? ""));
20717
+ add(THINKING, "reasoning", estimateTokens(p.text ?? ""), m, p);
20453
20718
  }
20454
20719
  }
20455
20720
  const total = [...acc.values()].reduce((s, c) => s + c.tokens, 0) || 1;
20456
- return [...acc.values()].map((c) => ({ ...c, share: c.tokens / total })).sort((a, b) => b.tokens - a.tokens);
20721
+ const limit = opts.limit !== undefined && opts.limit > 0 ? opts.limit : undefined;
20722
+ return [...acc.values()].map((c) => ({
20723
+ ...c,
20724
+ share: c.tokens / total,
20725
+ ...limit === undefined ? {} : { shareOfWindow: c.tokens / limit },
20726
+ ...c.kind === "reasoning" ? { note: THINKING_NOTE } : {},
20727
+ entries: c.entries.sort((a, b) => b.tokens - a.tokens)
20728
+ })).sort((a, b) => b.tokens - a.tokens);
20457
20729
  }
20458
20730
  function bar(share, width) {
20459
20731
  const n = Math.round(share * width);
@@ -20461,8 +20733,8 @@ function bar(share, width) {
20461
20733
  }
20462
20734
 
20463
20735
  // src/tui/route.tsx
20464
- import fs4 from "fs";
20465
- import path3 from "path";
20736
+ import fs5 from "fs";
20737
+ import path4 from "path";
20466
20738
 
20467
20739
  // src/core/cropplan.ts
20468
20740
  function sha8(text) {
@@ -20692,7 +20964,14 @@ function textOf(row) {
20692
20964
  return plain(row.preview);
20693
20965
  return `assistant: ${plain(row.preview)}`;
20694
20966
  }
20967
+ function thoughtOf(row) {
20968
+ if (row.kind !== "step" || row.thinkingMs === undefined)
20969
+ return "";
20970
+ return ` \xB7 ${(row.thinkingMs / 1000).toFixed(row.thinkingMs < 1e4 ? 1 : 0)}s thought`;
20971
+ }
20695
20972
  function rowLine(row, width, here) {
20973
+ if (row.kind === "separator")
20974
+ return `${row.gutter}${row.text}`;
20696
20975
  const tokens = `${row.kind !== "branch" && row.estimated ? "~" : ""}${formatK(row.tokens)}`;
20697
20976
  const marker = here ? " \u2190 here" : "";
20698
20977
  let body;
@@ -20705,32 +20984,82 @@ function rowLine(row, width, here) {
20705
20984
  } else {
20706
20985
  const flags = row.kind === "step" ? `${row.label ? ` [${row.label}]` : ""}${row.isCropped ? " \u2702" : ""}${row.warn ? " \u26A0" : ""}${row.isError ? " \u2717" : ""}` : row.label ? ` [${row.label}]` : "";
20707
20986
  const dur = row.kind === "step" && row.durationMs !== undefined ? ` ${(row.durationMs / 1000).toFixed(row.durationMs < 1e4 ? 1 : 0)}s` : "";
20708
- body = `${row.gutter}${glyphOf(row)} ${textOf(row)}${flags}${dur}${marker}`;
20987
+ body = `${row.gutter}${glyphOf(row)} ${textOf(row)}${flags}${dur}${thoughtOf(row)}${marker}`;
20709
20988
  }
20710
20989
  return fitRow(body, tokens, width);
20711
20990
  }
20991
+ function segmentsOf(line, query, thought) {
20992
+ const ranges = [];
20993
+ if (query) {
20994
+ const at = line.toLowerCase().indexOf(query.toLowerCase());
20995
+ if (at >= 0)
20996
+ ranges.push({
20997
+ at,
20998
+ len: query.length,
20999
+ kind: "match"
21000
+ });
21001
+ }
21002
+ if (thought) {
21003
+ const at = line.lastIndexOf(thought);
21004
+ if (at >= 0)
21005
+ ranges.push({
21006
+ at,
21007
+ len: thought.length,
21008
+ kind: "dim"
21009
+ });
21010
+ }
21011
+ if (ranges.length === 0)
21012
+ return [{
21013
+ text: line,
21014
+ kind: "plain"
21015
+ }];
21016
+ const out = [];
21017
+ let cursor = 0;
21018
+ for (const r of ranges.sort((a, b) => a.at - b.at)) {
21019
+ if (r.at < cursor)
21020
+ continue;
21021
+ if (r.at > cursor)
21022
+ out.push({
21023
+ text: line.slice(cursor, r.at),
21024
+ kind: "plain"
21025
+ });
21026
+ out.push({
21027
+ text: line.slice(r.at, r.at + r.len),
21028
+ kind: r.kind
21029
+ });
21030
+ cursor = r.at + r.len;
21031
+ }
21032
+ if (cursor < line.length)
21033
+ out.push({
21034
+ text: line.slice(cursor),
21035
+ kind: "plain"
21036
+ });
21037
+ return out;
21038
+ }
20712
21039
  var DEFAULT_KEYS = {
20713
21040
  up: ["up", "k"],
20714
21041
  down: ["down", "j"],
20715
21042
  jump_up: ["shift+up", "shift+k"],
20716
21043
  jump_down: ["shift+down", "shift+j"],
20717
- first: ["g"],
21044
+ half_up: ["ctrl+u"],
21045
+ half_down: ["ctrl+d"],
21046
+ first: ["gg"],
20718
21047
  last: ["shift+g"],
20719
21048
  prev_branch: ["["],
20720
21049
  next_branch: ["]"],
20721
21050
  fold: ["left", "h"],
20722
21051
  unfold: ["right", "l"],
20723
- toggle: ["e"],
21052
+ toggle: ["tab", "e"],
20724
21053
  go: ["return"],
20725
21054
  branch: ["b"],
20726
21055
  crop: ["c"],
20727
21056
  crop_toggle_mode: ["t"],
20728
21057
  mark: ["space"],
20729
21058
  auto: ["a"],
20730
- undo: ["x"],
21059
+ undo: ["u", "x"],
20731
21060
  merge: ["m"],
20732
21061
  inspector: ["i"],
20733
- consumers: ["u"],
21062
+ consumers: ["s"],
20734
21063
  copy: ["y"],
20735
21064
  mode_duration: ["1"],
20736
21065
  mode_turns: ["2"],
@@ -20739,13 +21068,43 @@ var DEFAULT_KEYS = {
20739
21068
  decisions: ["shift+d"],
20740
21069
  export: ["shift+e"],
20741
21070
  label: ["shift+l"],
20742
- filter: ["f"],
21071
+ filter_pick: ["f"],
21072
+ filter_prev: ["shift+f"],
20743
21073
  search: ["/"],
21074
+ search_next: ["n"],
21075
+ search_prev: ["shift+n"],
20744
21076
  help: ["?", "shift+/"],
20745
21077
  back: ["q", "escape"]
20746
21078
  };
21079
+ var EMPTY_TRANSCRIPT = {
21080
+ sessionID: "",
21081
+ title: "",
21082
+ status: "available",
21083
+ messages: []
21084
+ };
20747
21085
  var NO_BRANCHES = "No branches yet \xB7 b forks here into a real OpenCode session; nothing is copied or deleted.";
20748
- var HELP = ["? help \xB7 ? or esc closes", "Reading the screen \u2014 a whole-tree outline (Pi) fused with a trajectory (DSH)", " the entire tree, oldest first: trunk at the left, branches nested at their fork point", " \u25CF user: \u2026 \xB7 \u25CB assistant: \u2026 \xB7 \u2699 [bash $ \u2026] / [tool: arg] \u2192 out \xB7 \u25C6 decision \xB7 \u2263 summary", " \u2387 = a branch: a real, separate OpenCode session, hung off the message it forked from", " \u2502 \u251C \u2570 connectors draw the branch topology; \u2190 here marks the session you are in", " \u25BE open / \u25B8 folded \u2014 the path to where you are is open by default; \u2192 \u2190 (or e) toggle a branch", " right column is tokens; ~ means estimated \xB7 \u26A0 \u226510k \xB7 \u2702 cropped \xB7 \u2717 tool error", "Keys", " move \u2191\u2193 j k \xB7 J K by 20 \xB7 g G first/last \xB7 [ ] branch rows \xB7 \u2192 \u2190 e fold", " act \u23CE go/switch \xB7 b branch \xB7 m merge \xB7 c crop \xB7 x undo \xB7 L label \xB7 y copy", " in crop mode: space mark \xB7 a auto \xB7 t result\u21C4turn \xB7 \u23CE apply \xB7 esc leave", " DSH i inspector (Status/Payload/Result/Timing) \xB7 1 2 3 lanes (Duration/Turns/Calls) \xB7 0 off", " views u consumers \xB7 D decisions \xB7 E export \xB7 f filter \xB7 / search \xB7 q back"];
21086
+ var HELP = [`? help \xB7 ? or esc closes \xB7 opencode-context-tree ${PLUGIN_VERSION}`, "Move", " \u2191\u2193 j k \xB7 J K by 20 \xB7 ctrl+d ctrl+u half page \xB7 gg top \xB7 G bottom \xB7 [ ] branch rows", " h l \u2190 \u2192 fold/unfold a branch \xB7 Tab (or e) toggle \xB7 / live search \xB7 n N next/prev match", "Act", " \u23CE go \u2014 a \u2387 header switches to it \xB7 a user turn forks & prefills it \xB7 a step forks after it", " b branch \xB7 m merge \xB7 c crop mode (space mark \xB7 a auto \xB7 t result\u21C4turn \xB7 \u23CE apply \xB7 esc leave)", " u undo (alias x) \xB7 L label \xB7 y copy \xB7 E export decisions", "Views", " i inspector \xB7 1 2 3 lanes (duration/turns/calls) \xB7 0 off \xB7 s consumers \xB7 D decisions \xB7 f F filter", "Legend", " \u25CF user \xB7 \u25CB assistant \xB7 \u2699 tool step \xB7 \u25C6 decision \xB7 \u2263 summary \xB7 \u2387 branch (a real OpenCode session)", " \u2502 \u251C \u2570 draw the topology \xB7 \u25BE open \u25B8 folded \xB7 \u2190 here is the session you are in", " dim rows are not sent to the model; \u2500\u2500 not in this branch's context \u2500\u2500 is where your path forked", " right column is tokens; ~ estimated \xB7 \u26A0 \u226510k \xB7 \u2702 cropped \xB7 \u2717 tool error", " \u2387 colours: open green \xB7 squashed blue \xB7 rejected/discarded red \xB7 abandoned grey", " lanes: Input green you / grey context \xB7 Model purple answer / grey thinking \xB7 Tools orange call / red failed"];
21087
+ var FILTERS = [{
21088
+ title: "default",
21089
+ value: "default",
21090
+ description: "user turns, assistant text, tool steps"
21091
+ }, {
21092
+ title: "no-tools",
21093
+ value: "no-tools",
21094
+ description: "hide \u2699 tool steps"
21095
+ }, {
21096
+ title: "user-only",
21097
+ value: "user-only",
21098
+ description: "\u25CF user turns only"
21099
+ }, {
21100
+ title: "labeled",
21101
+ value: "labeled",
21102
+ description: "labelled rows only"
21103
+ }, {
21104
+ title: "all",
21105
+ value: "all",
21106
+ description: "everything, thinking parts included"
21107
+ }];
20749
21108
  function bindingsFor(overrides) {
20750
21109
  const out = [];
20751
21110
  for (const [cmd, keys] of Object.entries(DEFAULT_KEYS))
@@ -20777,6 +21136,8 @@ function TreeRoute(props) {
20777
21136
  const [expanded, setExpanded] = createSignal2(new Set(api2.kv.get(`ctree.expanded.${sessionID}`, [])));
20778
21137
  const [filter, setFilter] = createSignal2(api2.kv.get("ctree.filter", "default"));
20779
21138
  const [search, setSearch] = createSignal2("");
21139
+ const [searchMode, setSearchMode] = createSignal2(false);
21140
+ let searchBefore = "";
20780
21141
  const [selected, setSelected] = createSignal2(0);
20781
21142
  const [others, setOthers] = createSignal2({});
20782
21143
  const [busy, setBusy] = createSignal2();
@@ -20786,7 +21147,9 @@ function TreeRoute(props) {
20786
21147
  const [lanesOn, setLanesOn] = createSignal2(api2.kv.get("ctree.lanesOn", false));
20787
21148
  const [inspector, setInspector] = createSignal2(api2.kv.get("ctree.inspector", false));
20788
21149
  const [consumerIndex, setConsumerIndex] = createSignal2(0);
21150
+ const [consumerOpen, setConsumerOpen] = createSignal2(new Set);
20789
21151
  const [decisionIndex, setDecisionIndex] = createSignal2(0);
21152
+ const [decisionScroll, setDecisionScroll] = createSignal2(0);
20790
21153
  const [marked, setMarked] = createSignal2(new Set);
20791
21154
  const state = createMemo(() => {
20792
21155
  tick();
@@ -20901,19 +21264,23 @@ function TreeRoute(props) {
20901
21264
  api2.renderer.on("resize", onResize);
20902
21265
  onCleanup(() => void api2.renderer.off("resize", onResize));
20903
21266
  const cols = () => size().cols;
20904
- const height = () => Math.max(8, size().rows - 8 - (lanesOn() && size().rows >= 12 ? 3 : 0));
21267
+ const helpHeight = () => panel() === "help" ? Math.min(HELP.length, Math.max(0, size().rows - 12)) : 0;
21268
+ const height = () => Math.max(4, size().rows - 8 - (lanesOn() && size().rows >= 12 ? 3 : 0) - helpHeight());
20905
21269
  const width = () => Math.max(60, cols() - 4);
21270
+ const overflow = () => view().rows.length > height() - 2;
21271
+ const rowsHeight = () => overflow() ? height() - 2 : height();
20906
21272
  const windowStart = createMemo(() => {
20907
- const h = height();
21273
+ const h = rowsHeight();
20908
21274
  const s = selected();
20909
21275
  const n = view().rows.length;
20910
- const start = Math.max(0, Math.min(s - Math.floor(h / 2), n - h));
20911
- return start;
21276
+ return Math.max(0, Math.min(s - Math.floor(h / 2), n - h));
20912
21277
  });
20913
- const visible = createMemo(() => view().rows.slice(windowStart(), windowStart() + height()));
21278
+ const visible = createMemo(() => view().rows.slice(windowStart(), windowStart() + rowsHeight()));
21279
+ const hiddenAbove = () => windowStart();
21280
+ const hiddenBelow = () => Math.max(0, view().rows.length - windowStart() - rowsHeight());
20914
21281
  const live = () => sessionID ? liveTranscript(api2, sessionID) : undefined;
20915
21282
  const currentMessageOf = (row) => {
20916
- if (row.kind === "branch")
21283
+ if (row.kind === "branch" || row.kind === "separator")
20917
21284
  return;
20918
21285
  if (row.sessionID === sessionID)
20919
21286
  return row.messageID;
@@ -20958,10 +21325,35 @@ function TreeRoute(props) {
20958
21325
  const list = cropMode() === "result" ? resultCands() : turnCands();
20959
21326
  return list.filter((c) => m.has(markKey(c)));
20960
21327
  });
21328
+ function modeForRow(row) {
21329
+ if (row.kind === "step" && resultCands().some((c) => c.partID === (currentPartOf(row) ?? row.partID)))
21330
+ return "result";
21331
+ if (row.kind !== "branch" && row.kind !== "separator" && turnCands().some((c) => c.anchorMessageID === currentMessageOf(row)))
21332
+ return "turn";
21333
+ return;
21334
+ }
21335
+ const armed = () => {
21336
+ const row = current();
21337
+ const c = row ? candidateOf(row) : undefined;
21338
+ if (!c)
21339
+ return false;
21340
+ const key = markKey(c);
21341
+ return marked().has(`${key}:warned`) && !marked().has(key);
21342
+ };
20961
21343
  function toggleMark() {
20962
21344
  const row = current();
20963
21345
  if (!row)
20964
21346
  return;
21347
+ if (!cropMode()) {
21348
+ const mode = modeForRow(row);
21349
+ if (!mode) {
21350
+ api2.ui.toast({
21351
+ message: "nothing croppable on this row \u2014 c opens crop mode"
21352
+ });
21353
+ return;
21354
+ }
21355
+ setCropMode(mode);
21356
+ }
20965
21357
  const c = candidateOf(row);
20966
21358
  debug("crop.mark", {
20967
21359
  row: row.id,
@@ -20993,6 +21385,13 @@ function TreeRoute(props) {
20993
21385
  next.add(key);
20994
21386
  setMarked(next);
20995
21387
  }
21388
+ async function leaveCropMode() {
21389
+ const n = selectedCandidates().length;
21390
+ if (n > 0 && !await confirm(`Drop ${n} mark${n === 1 ? "" : "s"}?`, "Nothing has been cropped yet \u2014 the marks are lost, the transcript is untouched."))
21391
+ return;
21392
+ setCropMode(undefined);
21393
+ setMarked(new Set);
21394
+ }
20996
21395
  function autoMarkAll() {
20997
21396
  if (cropMode() !== "result")
20998
21397
  return;
@@ -21070,69 +21469,64 @@ function TreeRoute(props) {
21070
21469
  },
21071
21470
  parts: m.parts
21072
21471
  }))));
21073
- const band = () => bandFor(contextSize().tokens);
21472
+ const band = () => bandFor(contextSize().tokens, contextLimit());
21074
21473
  const branchOfCurrent = () => sessionID ? state().sessions[sessionID] : undefined;
21075
21474
  const userTurns = () => (live()?.messages ?? []).filter((m) => m.role === "user").length;
21076
- const lanes = createMemo(() => live() ? buildLanes(live(), laneMode() === "duration" ? "turns" : laneMode()) : {
21077
- mode: laneMode(),
21078
- columns: []
21079
- });
21080
21475
  const laneWidth = () => Math.max(10, Math.min(width() - 46, 80));
21081
- const laneSeries = createMemo(() => {
21082
- const l = lanes();
21083
- if (laneMode() === "duration") {
21084
- const w = durationWeighted(l, laneWidth());
21085
- return {
21086
- input: w.input,
21087
- output: w.output,
21088
- tool: w.tool,
21089
- toolError: w.toolError,
21090
- cellFor: (col) => w.input.findIndex((_, i) => w.columnAt(i) === col)
21091
- };
21092
- }
21093
- const n = l.columns.length;
21094
- const cellFor = (col) => n === 0 ? -1 : Math.floor(col * laneWidth() / Math.max(n, laneWidth())) + (n < laneWidth() ? Math.floor(laneWidth() / n / 2) : 0);
21095
- return {
21096
- input: l.columns.map((c) => c.input),
21097
- output: l.columns.map((c) => c.output),
21098
- tool: l.columns.map((c) => c.tool),
21099
- toolError: l.columns.map((c) => c.toolError),
21100
- cellFor
21101
- };
21102
- });
21103
- const cursorCell = createMemo(() => {
21476
+ const strip = createMemo(() => buildEventStrip(live() ?? EMPTY_TRANSCRIPT, laneMode(), laneWidth()));
21477
+ const cursorEvents = createMemo(() => {
21104
21478
  const row = current();
21105
- if (!row || row.kind === "branch")
21106
- return -1;
21479
+ const hit = new Set;
21480
+ if (!row || row.kind === "branch" || row.kind === "separator")
21481
+ return hit;
21107
21482
  const mid = currentMessageOf(row) ?? row.messageID;
21108
21483
  const pid = row.kind === "step" ? currentPartOf(row) ?? row.partID : undefined;
21109
- const col = columnFor(lanes(), mid, pid);
21110
- return col < 0 ? -1 : laneSeries().cellFor(col);
21484
+ const own2 = stripIndexFor(strip(), mid, pid);
21485
+ if (own2 >= 0)
21486
+ hit.add(own2);
21487
+ strip().events.forEach((e, i) => {
21488
+ if (e.messageID !== mid)
21489
+ return;
21490
+ if (row.kind === "turn" ? e.lane === "input" : e.kind === "reasoning")
21491
+ hit.add(i);
21492
+ });
21493
+ return hit;
21111
21494
  });
21112
- const laneLine = (values, scale) => {
21113
- const line = sparkline(fitColumns(values, laneWidth()), laneWidth(), scale);
21114
- const cur = cursorCell();
21115
- if (cur < 0 || cur >= line.length)
21116
- return line;
21117
- return `${line.slice(0, cur)}\u25AE${line.slice(cur + 1)}`;
21118
- };
21119
- const toolRuns = createMemo(() => {
21120
- const line = laneLine(laneSeries().tool);
21121
- const mask = fitColumns(laneSeries().toolError.map((e) => e ? 1 : 0), laneWidth());
21495
+ const cellColor = (cell) => {
21496
+ if (cell.error)
21497
+ return t.error;
21498
+ const e = strip().events[cell.eventIndex];
21499
+ if (!e)
21500
+ return t.textMuted;
21501
+ if (e.lane === "tools")
21502
+ return t.warning;
21503
+ if (e.lane === "input")
21504
+ return e.kind === "user" ? t.success : t.textMuted;
21505
+ return e.kind === "reasoning" ? t.textMuted : t.accent;
21506
+ };
21507
+ const laneRuns = (lane) => {
21508
+ const cur = cursorEvents();
21122
21509
  const runs = [];
21123
- for (let i = 0;i < line.length; i++) {
21124
- const error61 = (mask[i] ?? 0) > 0;
21510
+ for (const cell of strip().lanes[lane]) {
21511
+ const sel = cell !== null && cur.has(cell.eventIndex);
21512
+ const color = cell === null ? t.textMuted : cellColor(cell);
21513
+ const fg = sel ? t.background : color;
21514
+ const bg = sel ? color : undefined;
21125
21515
  const last = runs[runs.length - 1];
21126
- if (last && last.error === error61)
21127
- last.text += line[i];
21516
+ if (last && last.fg === fg && last.bg === bg)
21517
+ last.text += cell?.glyph ?? " ";
21128
21518
  else
21129
21519
  runs.push({
21130
- text: line[i],
21131
- error: error61
21520
+ text: cell?.glyph ?? " ",
21521
+ fg,
21522
+ bg
21132
21523
  });
21133
21524
  }
21134
21525
  return runs;
21135
- });
21526
+ };
21527
+ const inputRuns = createMemo(() => laneRuns("input"));
21528
+ const modelRuns = createMemo(() => laneRuns("model"));
21529
+ const toolRuns = createMemo(() => laneRuns("tools"));
21136
21530
  const contextLimit = createMemo(() => sessionID ? modelContextLimit(api2, sessionID) : undefined);
21137
21531
  const laneRoom = () => height() >= 12 && panel() === "tree";
21138
21532
  const showLanes = () => laneRoom() && lanesOn() && userTurns() >= 3;
@@ -21154,7 +21548,7 @@ function TreeRoute(props) {
21154
21548
  const noBranchesLines = () => NO_BRANCHES.length + 2 <= rowWidth() ? [NO_BRANCHES] : NO_BRANCHES.split(/(?<=;) /);
21155
21549
  const inspectorLines = createMemo(() => {
21156
21550
  const row = current();
21157
- if (!row)
21551
+ if (!row || row.kind === "separator")
21158
21552
  return [];
21159
21553
  const w = inspectorWidth() - 3;
21160
21554
  const clip2 = (x) => x.length > w ? `${x.slice(0, w - 1)}\u2026` : x;
@@ -21201,19 +21595,37 @@ function TreeRoute(props) {
21201
21595
  const msg = tr?.messages.find((m) => m.id === row.messageID);
21202
21596
  const turn = view().rows.slice(0, view().indexById[row.id] + 1).filter((r) => r.kind === "turn").at(-1);
21203
21597
  if (row.kind === "turn") {
21204
- head(`${row.isDecision ? "\u25C6 decision" : row.isSummary ? "\u25C7 summary" : "\u25CF user"} \xB7 T${row.turn}`);
21598
+ const text = msg?.parts.map((p) => p.text ?? "").join(`
21599
+ `) ?? row.preview;
21600
+ if (row.isDecision) {
21601
+ head(`\u25C6 ${decisionSummary(text).title}`);
21602
+ kv("Tokens", `~${formatK(row.tokens)}`);
21603
+ const lines = renderDecision(text, w);
21604
+ for (const l of lines.slice(0, 16))
21605
+ out.push({
21606
+ fg: t.text,
21607
+ text: l
21608
+ });
21609
+ if (lines.length > 16)
21610
+ muted(`\u2026 ${lines.length - 16} more lines (y to copy)`);
21611
+ return out;
21612
+ }
21613
+ head(`${row.isSummary ? "\u25C7 summary" : "\u25CF user"} \xB7 T${row.turn}`);
21205
21614
  if (row.label)
21206
21615
  kv("Label", row.label);
21207
21616
  kv("Tokens", `~${formatK(row.tokens)}`);
21208
21617
  kv("At", msg ? new Date(msg.time.created).toISOString().slice(11, 19) : "?");
21209
- block("Text", msg?.parts.map((p) => p.text ?? "").join(`
21210
- `) ?? row.preview, 14);
21618
+ if (!row.inContext)
21619
+ muted("not in this branch's context");
21620
+ block("Text", text, 14);
21211
21621
  return out;
21212
21622
  }
21213
21623
  const part = msg?.parts.find((p) => p.id === row.partID);
21214
21624
  const stepNo = msg ? msg.parts.filter((p) => p.type === "tool" || p.type === "text").findIndex((p) => p.id === row.partID) + 1 : 0;
21215
21625
  head(`${row.glyph} ${part?.type === "tool" ? part.tool : row.glyph === "\u25C7" ? "compaction" : "assistant"} \xB7 T${turn?.kind === "turn" ? turn.turn : "?"} \xB7 step ${stepNo}`);
21216
21626
  kv("Hierarchy", `T${turn?.kind === "turn" ? turn.turn : "?"} \u203A assistant \u203A step ${stepNo}`);
21627
+ if (!row.inContext)
21628
+ muted("not in this branch's context");
21217
21629
  if (part?.type === "tool") {
21218
21630
  const st = part.state;
21219
21631
  const dur = st?.time?.start !== undefined && st?.time?.end !== undefined ? `${st.time.end - st.time.start} ms` : "?";
@@ -21223,25 +21635,66 @@ function TreeRoute(props) {
21223
21635
  block("Result", String(st?.output ?? ""), 10);
21224
21636
  kv("Timing", st?.time?.start ? `started ${new Date(st.time.start).toISOString().slice(11, 23)} \xB7 ${dur} \xB7 session ts` : "n/a");
21225
21637
  const cand = resultCands().find((c) => c.partID === (currentPartOf(row) ?? row.partID));
21226
- kv("Crop", row.isCropped ? "\u2702 cropped (x to restore)" : cand ? cand.protections.length ? `protected: ${cand.protections.join(", ")}` : "c then space to stub this result" : "n/a");
21638
+ kv("Crop", row.isCropped ? `\u2702 cropped (${UNDO_KEY} to restore)` : cand ? cand.protections.length ? `protected: ${cand.protections.join(", ")}` : "c then space to stub this result" : "n/a");
21227
21639
  } else {
21228
21640
  kv("Tokens", `~${formatK(row.tokens)}`);
21229
21641
  if (row.durationMs !== undefined)
21230
21642
  kv("Duration", `${(row.durationMs / 1000).toFixed(1)} s`);
21643
+ if (row.thinkingMs !== undefined)
21644
+ kv("Thought", `${(row.thinkingMs / 1000).toFixed(1)} s`);
21231
21645
  block("Text", part?.text ?? row.preview, 14);
21232
21646
  }
21233
21647
  return out;
21234
21648
  });
21235
21649
  const consumerRows = createMemo(() => live() ? consumers(live(), {
21236
- cropped: alreadyCropped()
21650
+ cropped: alreadyCropped(),
21651
+ limit: contextLimit()
21237
21652
  }) : []);
21653
+ const consumerLines = createMemo(() => consumerRows().flatMap((c) => [{
21654
+ bucket: c
21655
+ }, ...consumerOpen().has(c.source) ? c.entries.map((e) => ({
21656
+ bucket: c,
21657
+ entry: e
21658
+ })) : []]));
21659
+ const consumerLine = () => consumerLines()[Math.min(consumerIndex(), consumerLines().length - 1)];
21660
+ const consumerMax = () => Math.max(1, ...consumerRows().map((c) => c.tokens));
21661
+ function toggleConsumer(open2) {
21662
+ const line = consumerLine();
21663
+ if (!line)
21664
+ return;
21665
+ const next = new Set(consumerOpen());
21666
+ if (open2)
21667
+ next.add(line.bucket.source);
21668
+ else
21669
+ next.delete(line.bucket.source);
21670
+ setConsumerOpen(next);
21671
+ }
21672
+ function markConsumerEntry() {
21673
+ const line = consumerLine();
21674
+ if (!line)
21675
+ return;
21676
+ if (!line.entry) {
21677
+ toggleConsumer(true);
21678
+ return;
21679
+ }
21680
+ const cand = line.entry.croppable ? resultCands().find((r) => r.partID === line.entry?.partID) : undefined;
21681
+ if (!cand)
21682
+ return;
21683
+ setCropMode("result");
21684
+ const next = new Set(marked());
21685
+ if (next.has(cand.partID))
21686
+ next.delete(cand.partID);
21687
+ else
21688
+ next.add(cand.partID);
21689
+ setMarked(next);
21690
+ }
21238
21691
  function cropConsumer() {
21239
- const c = consumerRows()[consumerIndex()];
21692
+ const c = consumerLine()?.bucket;
21240
21693
  setPanel("tree");
21241
21694
  if (!c || c.kind !== "tool") {
21242
21695
  setCropMode("result");
21243
21696
  api2.ui.toast({
21244
- message: c ? `${c.source} is not a tool result; mark rows by hand` : "nothing to crop"
21697
+ message: c ? c.note ?? `${c.source} is not a tool result; mark rows by hand` : "nothing to crop"
21245
21698
  });
21246
21699
  return;
21247
21700
  }
@@ -21254,20 +21707,18 @@ function TreeRoute(props) {
21254
21707
  }
21255
21708
  function copySelected() {
21256
21709
  const row = current();
21257
- if (!row || row.kind === "branch")
21710
+ if (!row || row.kind === "branch" || row.kind === "separator")
21258
21711
  return;
21259
21712
  const tr = row.sessionID === sessionID ? live() : others()[row.sessionID];
21260
21713
  const msg = tr?.messages.find((m) => m.id === row.messageID);
21261
21714
  const text = row.kind === "step" ? String(msg?.parts.find((p) => p.id === row.partID)?.state?.output ?? msg?.parts.find((p) => p.id === row.partID)?.text ?? "") : msg?.parts.map((p) => p.text ?? "").join(`
21262
21715
  `) ?? "";
21263
- const file2 = path3.join(directory, ".opencode", "context-tree", "last-copy.txt");
21264
21716
  try {
21265
- fs4.mkdirSync(path3.dirname(file2), {
21266
- recursive: true
21267
- });
21268
- fs4.writeFileSync(file2, text);
21717
+ const {
21718
+ hint
21719
+ } = copyText(api2, text, directory);
21269
21720
  api2.ui.toast({
21270
- message: `saved ${text.length} chars \u2192 .opencode/context-tree/last-copy.txt`
21721
+ message: `copied ${text.length} chars \u2192 ${hint}`
21271
21722
  });
21272
21723
  } catch (e) {
21273
21724
  api2.ui.toast({
@@ -21393,6 +21844,10 @@ function TreeRoute(props) {
21393
21844
  bump();
21394
21845
  }
21395
21846
  }
21847
+ const sessionLabel = (id) => {
21848
+ const name = state().sessions[id]?.name;
21849
+ return name ? `\u2387 ${name}` : others()[id]?.title ?? api2.state.session.get(id)?.title ?? id;
21850
+ };
21396
21851
  async function jump() {
21397
21852
  const row = current();
21398
21853
  if (!row || !sessionID)
@@ -21415,16 +21870,19 @@ function TreeRoute(props) {
21415
21870
  });
21416
21871
  return;
21417
21872
  }
21418
- if (plan.kind === "fork" && plan.mode === "redo") {
21419
- const ok = await confirm("Redo this turn on a new branch?", "The message is copied into the prompt; nothing is deleted.");
21420
- if (!ok)
21421
- return;
21422
- }
21873
+ const from = sessionLabel(plan.sessionID);
21874
+ const ok = await confirm(plan.kind === "switch" ? `Switch to ${from}?` : plan.mode === "redo" ? "Fork & prefill this turn?" : "Fork after this step?", plan.kind === "switch" ? `The session you are on now stays exactly as it is. ${UNDO_KEY} undoes this.` : `A new OpenCode session forks from ${from} at this point; nothing is deleted. ${UNDO_KEY} undoes this.`);
21875
+ if (!ok)
21876
+ return;
21423
21877
  const summary = await askSummary();
21424
- await executeJump(ctx, plan, {
21878
+ const target2 = await executeJump(ctx, plan, {
21425
21879
  currentSessionID: sessionID,
21426
21880
  summary
21427
21881
  });
21882
+ if (target2)
21883
+ api2.ui.toast({
21884
+ message: `moved to ${sessionLabel(target2)} \xB7 ${UNDO_KEY} undoes it`
21885
+ });
21428
21886
  });
21429
21887
  }
21430
21888
  async function branch() {
@@ -21477,7 +21935,7 @@ function TreeRoute(props) {
21477
21935
  }
21478
21936
  async function label() {
21479
21937
  const row = current();
21480
- if (!row || row.kind === "branch")
21938
+ if (!row || row.kind === "branch" || row.kind === "separator")
21481
21939
  return;
21482
21940
  const st = state();
21483
21941
  const existing = st.labels[row.messageID]?.label;
@@ -21495,7 +21953,7 @@ function TreeRoute(props) {
21495
21953
  const row = current();
21496
21954
  if (!row)
21497
21955
  return;
21498
- const target2 = row.kind === "branch" ? row.sessionID : row.depth > 0 ? row.sessionID : undefined;
21956
+ const target2 = row.kind === "separator" ? undefined : row.kind === "branch" || row.depth > 0 ? row.sessionID : undefined;
21499
21957
  if (!target2)
21500
21958
  return;
21501
21959
  const shown = row.kind === "branch" ? row.expanded : true;
@@ -21529,14 +21987,18 @@ function TreeRoute(props) {
21529
21987
  return;
21530
21988
  }
21531
21989
  const siblings = Object.values(state().sessions).filter((x) => x.parentSessionID === b.parentSessionID && x.sessionID !== sessionID && x.status === "open").length;
21532
- const mode = await select(mergeDialogTitle(b.name ?? "branch", others()[b.parentSessionID]?.title), mergeDialogOptions({
21533
- siblings
21990
+ const parent = others()[b.parentSessionID];
21991
+ const target2 = mergeTargetOf(b.parentSessionID === state().root ? TRUNK_LABEL : state().sessions[b.parentSessionID]?.name ?? TRUNK_LABEL, parent?.messages ?? []);
21992
+ const turns = ownTurnCount(live()?.messages ?? [], {
21993
+ messageID: b.anchorMessageID,
21994
+ parentMessageIDs: parent?.messages.map((m) => m.id) ?? []
21995
+ });
21996
+ const mode = await select(mergeDialogTitle(b.name ?? "branch", target2), mergeDialogOptions({
21997
+ siblings,
21998
+ turns
21534
21999
  }));
21535
22000
  if (!mode)
21536
22001
  return;
21537
- let note;
21538
- if (mode === "discard")
21539
- note = await prompt("Why? (optional note on the close marker)", "dead end") ?? undefined;
21540
22002
  const inApp = !hasEditor() ? async (draft) => {
21541
22003
  const ok = await confirm("Accept the drafted record as-is?", `${draft.slice(0, 400)}${draft.length > 400 ? "\u2026" : ""}
21542
22004
 
@@ -21549,7 +22011,6 @@ ${MERGE_TRUST}
21549
22011
  await mergeBranch(ctx, {
21550
22012
  sessionID,
21551
22013
  mode,
21552
- note,
21553
22014
  confirm: inApp
21554
22015
  });
21555
22016
  });
@@ -21561,9 +22022,9 @@ ${MERGE_TRUST}
21561
22022
  sessionID: d.sessionID,
21562
22023
  at: d.recordedAt
21563
22024
  }));
21564
- const file2 = path3.join(directory, "ctree-decisions.md");
22025
+ const file2 = path4.join(directory, "ctree-decisions.md");
21565
22026
  try {
21566
- fs4.writeFileSync(file2, exportDecisions(records));
22027
+ fs5.writeFileSync(file2, exportDecisions(records));
21567
22028
  api2.ui.toast({
21568
22029
  variant: "success",
21569
22030
  message: `wrote ${records.length} record${records.length === 1 ? "" : "s"} \u2192 ${file2}`
@@ -21579,7 +22040,7 @@ ${MERGE_TRUST}
21579
22040
  const d = decisions()[decisionIndex()];
21580
22041
  if (!d)
21581
22042
  return;
21582
- const idx = view().rows.findIndex((r) => r.kind !== "branch" && r.messageID === d.messageID);
22043
+ const idx = view().rows.findIndex((r) => (r.kind === "turn" || r.kind === "step") && r.messageID === d.messageID);
21583
22044
  setPanel("tree");
21584
22045
  if (idx >= 0)
21585
22046
  setSelected(idx);
@@ -21588,125 +22049,251 @@ ${MERGE_TRUST}
21588
22049
  message: "that record lives in another session"
21589
22050
  });
21590
22051
  }
22052
+ const treePanel = () => panel() === "tree";
22053
+ const inCrop = () => cropMode() !== undefined;
22054
+ const treeIdle = () => treePanel() && !inCrop();
22055
+ const listPanel = () => treePanel() || panel() === "consumers";
22056
+ function setFilterTo(next) {
22057
+ setFilter(next);
22058
+ api2.kv.set("ctree.filter", next);
22059
+ }
22060
+ async function pickFilter() {
22061
+ const next = await select("Filter rows", FILTERS.map((f) => ({
22062
+ title: `${f.value === filter() ? "\u25CF" : " "} ${f.title}`,
22063
+ value: f.value,
22064
+ description: f.description
22065
+ })));
22066
+ if (next)
22067
+ setFilterTo(next);
22068
+ }
22069
+ function moveIndex(delta) {
22070
+ if (panel() === "decisions") {
22071
+ setDecisionIndex((i) => Math.min(Math.max(0, decisions().length - 1), Math.max(0, i + delta)));
22072
+ setDecisionScroll(0);
22073
+ return;
22074
+ }
22075
+ if (panel() === "consumers") {
22076
+ setConsumerIndex((i) => Math.min(Math.max(0, consumerLines().length - 1), Math.max(0, i + delta)));
22077
+ return;
22078
+ }
22079
+ setSelected((i) => moveSelection(view().rows, i, delta));
22080
+ }
22081
+ function halfPage(dir) {
22082
+ const half = Math.max(1, Math.floor(height() / 2));
22083
+ if (panel() === "decisions")
22084
+ setDecisionScroll((s) => Math.max(0, s + dir * half));
22085
+ else
22086
+ moveIndex(dir * half);
22087
+ }
22088
+ function gotoEdge(dir) {
22089
+ if (panel() === "decisions") {
22090
+ setDecisionIndex(dir === -1 ? 0 : Math.max(0, decisions().length - 1));
22091
+ setDecisionScroll(0);
22092
+ return;
22093
+ }
22094
+ if (panel() === "consumers") {
22095
+ setConsumerIndex(dir === -1 ? 0 : Math.max(0, consumerLines().length - 1));
22096
+ return;
22097
+ }
22098
+ const i = dir === -1 ? firstIndex(view().rows) : lastIndex(view().rows);
22099
+ if (i >= 0)
22100
+ setSelected(i);
22101
+ }
22102
+ const matchIn = (line) => {
22103
+ const q = search().trim().toLowerCase();
22104
+ return q ? line.toLowerCase().indexOf(q) : -1;
22105
+ };
22106
+ function moveMatch(dir) {
22107
+ const rows = view().rows;
22108
+ const q = search().trim();
22109
+ if (rows.length === 0 || !q)
22110
+ return;
22111
+ for (let step = 1;step <= rows.length; step++) {
22112
+ const i = ((selected() + dir * step) % rows.length + rows.length) % rows.length;
22113
+ if (rows[i].kind !== "separator" && matchIn(rowLine(rows[i], rowWidth(), false)) >= 0) {
22114
+ setSelected(i);
22115
+ return;
22116
+ }
22117
+ }
22118
+ api2.ui.toast({
22119
+ message: `no other row matches "${q}"`
22120
+ });
22121
+ }
22122
+ function enterSearch() {
22123
+ searchBefore = search();
22124
+ setSearchMode(true);
22125
+ }
22126
+ function exitSearch(commit) {
22127
+ if (!commit)
22128
+ setSearch(searchBefore);
22129
+ setSearchMode(false);
22130
+ }
22131
+ const stopTyping = api2.keymap.intercept("key", (input2) => {
22132
+ if (!searchMode())
22133
+ return;
22134
+ const ev = input2.event;
22135
+ if (ev.eventType === "release" || ev.ctrl || ev.meta)
22136
+ return;
22137
+ const take = () => input2.consume({
22138
+ preventDefault: true,
22139
+ stopPropagation: true
22140
+ });
22141
+ if (ev.name === "escape")
22142
+ return void (exitSearch(false), take());
22143
+ if (ev.name === "return" || ev.name === "enter")
22144
+ return void (exitSearch(true), take());
22145
+ if (ev.name === "backspace")
22146
+ return void (setSearch((q) => q.slice(0, -1)), take());
22147
+ const char = ev.sequence?.length === 1 && ev.sequence >= " " && ev.sequence !== "\x7F" ? ev.sequence : ev.name.length === 1 ? ev.name : undefined;
22148
+ if (char === undefined)
22149
+ return;
22150
+ setSearch((q) => q + char);
22151
+ take();
22152
+ });
22153
+ onCleanup(() => stopTyping());
21591
22154
  const off = api2.keymap.registerLayer({
21592
22155
  mode: "base",
21593
22156
  commands: [{
21594
22157
  name: "ctree.up",
21595
22158
  hidden: true,
21596
- run: () => panel() === "decisions" ? setDecisionIndex((i) => Math.max(0, i - 1)) : panel() === "consumers" ? setConsumerIndex((i) => Math.max(0, i - 1)) : setSelected((i) => moveSelection(view().rows, i, -1))
22159
+ run: () => moveIndex(-1)
21597
22160
  }, {
21598
22161
  name: "ctree.down",
21599
22162
  hidden: true,
21600
- run: () => panel() === "decisions" ? setDecisionIndex((i) => Math.min(Math.max(0, decisions().length - 1), i + 1)) : panel() === "consumers" ? setConsumerIndex((i) => Math.min(Math.max(0, consumerRows().length - 1), i + 1)) : setSelected((i) => moveSelection(view().rows, i, 1))
22163
+ run: () => moveIndex(1)
21601
22164
  }, {
21602
22165
  name: "ctree.jump_up",
21603
22166
  hidden: true,
21604
- run: () => setSelected((i) => moveSelection(view().rows, i, -20))
22167
+ enabled: treePanel,
22168
+ run: () => moveIndex(-20)
21605
22169
  }, {
21606
22170
  name: "ctree.jump_down",
21607
22171
  hidden: true,
21608
- run: () => setSelected((i) => moveSelection(view().rows, i, 20))
22172
+ enabled: treePanel,
22173
+ run: () => moveIndex(20)
22174
+ }, {
22175
+ name: "ctree.half_up",
22176
+ hidden: true,
22177
+ run: () => halfPage(-1)
22178
+ }, {
22179
+ name: "ctree.half_down",
22180
+ hidden: true,
22181
+ run: () => halfPage(1)
21609
22182
  }, {
21610
22183
  name: "ctree.first",
21611
22184
  hidden: true,
21612
- run: () => setSelected(0)
22185
+ run: () => gotoEdge(-1)
21613
22186
  }, {
21614
22187
  name: "ctree.last",
21615
22188
  hidden: true,
21616
- run: () => setSelected(Math.max(0, view().rows.length - 1))
22189
+ run: () => gotoEdge(1)
21617
22190
  }, {
21618
22191
  name: "ctree.prev_branch",
21619
22192
  hidden: true,
22193
+ enabled: treePanel,
21620
22194
  run: () => setSelected((i) => nextBranchIndex(view().rows, i, -1))
21621
22195
  }, {
21622
22196
  name: "ctree.next_branch",
21623
22197
  hidden: true,
22198
+ enabled: treePanel,
21624
22199
  run: () => setSelected((i) => nextBranchIndex(view().rows, i, 1))
21625
22200
  }, {
21626
22201
  name: "ctree.fold",
21627
22202
  hidden: true,
21628
- run: () => foldOrUnfold(false)
22203
+ enabled: listPanel,
22204
+ run: () => panel() === "consumers" ? toggleConsumer(false) : foldOrUnfold(false)
21629
22205
  }, {
21630
22206
  name: "ctree.unfold",
21631
22207
  hidden: true,
21632
- run: () => foldOrUnfold(true)
22208
+ enabled: listPanel,
22209
+ run: () => panel() === "consumers" ? toggleConsumer(true) : foldOrUnfold(true)
21633
22210
  }, {
21634
22211
  name: "ctree.toggle",
21635
22212
  hidden: true,
22213
+ enabled: treePanel,
21636
22214
  run: () => foldOrUnfold(!(current()?.kind === "branch" && current().expanded))
21637
22215
  }, {
21638
22216
  name: "ctree.go",
21639
22217
  hidden: true,
21640
- run: () => void (panel() === "decisions" ? jumpToDecision() : panel() === "consumers" ? cropConsumer() : cropMode() ? applyMarked() : jump())
22218
+ run: () => void (panel() === "decisions" ? jumpToDecision() : panel() === "consumers" ? toggleConsumer(!consumerOpen().has(consumerLine()?.bucket.source ?? "")) : cropMode() ? applyMarked() : jump())
21641
22219
  }, {
21642
22220
  name: "ctree.branch",
21643
22221
  hidden: true,
22222
+ enabled: treeIdle,
21644
22223
  run: () => void branch()
21645
22224
  }, {
21646
22225
  name: "ctree.label",
21647
22226
  hidden: true,
22227
+ enabled: treeIdle,
21648
22228
  run: () => void label()
21649
22229
  }, {
21650
- name: "ctree.filter",
22230
+ name: "ctree.filter_pick",
21651
22231
  hidden: true,
21652
- run: () => {
21653
- const next = cycleFilter(filter());
21654
- setFilter(next);
21655
- api2.kv.set("ctree.filter", next);
21656
- }
22232
+ enabled: () => !inCrop(),
22233
+ run: () => void pickFilter()
22234
+ }, {
22235
+ name: "ctree.filter_prev",
22236
+ hidden: true,
22237
+ enabled: () => !inCrop(),
22238
+ run: () => setFilterTo(FILTERS[(FILTERS.findIndex((f) => f.value === filter()) - 1 + FILTERS.length) % FILTERS.length].value)
21657
22239
  }, {
21658
22240
  name: "ctree.search",
21659
22241
  hidden: true,
21660
- run: () => void prompt("Search rows (empty to clear)", "bash, redis, label\u2026", search()).then((v) => {
21661
- if (v !== undefined)
21662
- setSearch(v.trim());
21663
- })
22242
+ enabled: treeIdle,
22243
+ run: () => enterSearch()
22244
+ }, {
22245
+ name: "ctree.search_next",
22246
+ hidden: true,
22247
+ enabled: treePanel,
22248
+ run: () => moveMatch(1)
22249
+ }, {
22250
+ name: "ctree.search_prev",
22251
+ hidden: true,
22252
+ enabled: treePanel,
22253
+ run: () => moveMatch(-1)
21664
22254
  }, {
21665
22255
  name: "ctree.crop",
21666
22256
  hidden: true,
22257
+ enabled: listPanel,
21667
22258
  run: () => {
21668
22259
  if (panel() === "consumers") {
21669
22260
  cropConsumer();
21670
22261
  return;
21671
22262
  }
21672
- if (panel() !== "tree")
21673
- return;
21674
- if (cropMode()) {
21675
- setCropMode(undefined);
21676
- setMarked(new Set);
21677
- } else
22263
+ if (cropMode())
22264
+ leaveCropMode();
22265
+ else
21678
22266
  setCropMode("result");
21679
22267
  }
21680
22268
  }, {
21681
22269
  name: "ctree.crop_toggle_mode",
21682
22270
  hidden: true,
21683
- run: () => {
21684
- if (!cropMode())
21685
- return;
21686
- setCropMode(cropMode() === "result" ? "turn" : "result");
21687
- setMarked(new Set);
21688
- }
22271
+ enabled: inCrop,
22272
+ run: () => setCropMode(cropMode() === "result" ? "turn" : "result")
21689
22273
  }, {
21690
22274
  name: "ctree.mark",
21691
22275
  hidden: true,
21692
- enabled: () => Boolean(cropMode()),
21693
- run: () => toggleMark()
22276
+ enabled: listPanel,
22277
+ run: () => panel() === "consumers" ? markConsumerEntry() : toggleMark()
21694
22278
  }, {
21695
22279
  name: "ctree.auto",
21696
22280
  hidden: true,
21697
- enabled: () => Boolean(cropMode()),
22281
+ enabled: inCrop,
21698
22282
  run: () => autoMarkAll()
21699
22283
  }, {
21700
22284
  name: "ctree.undo",
21701
22285
  hidden: true,
22286
+ enabled: treeIdle,
21702
22287
  run: () => void undo()
21703
22288
  }, {
21704
22289
  name: "ctree.merge",
21705
22290
  hidden: true,
22291
+ enabled: treeIdle,
21706
22292
  run: () => void merge2()
21707
22293
  }, {
21708
22294
  name: "ctree.inspector",
21709
22295
  hidden: true,
22296
+ enabled: () => !inCrop(),
21710
22297
  run: () => {
21711
22298
  setInspector(!inspector());
21712
22299
  api2.kv.set("ctree.inspector", inspector());
@@ -21714,26 +22301,32 @@ ${MERGE_TRUST}
21714
22301
  }, {
21715
22302
  name: "ctree.consumers",
21716
22303
  hidden: true,
22304
+ enabled: () => !inCrop(),
21717
22305
  run: () => setPanel(panel() === "consumers" ? "tree" : "consumers")
21718
22306
  }, {
21719
22307
  name: "ctree.copy",
21720
22308
  hidden: true,
22309
+ enabled: treeIdle,
21721
22310
  run: () => copySelected()
21722
22311
  }, {
21723
22312
  name: "ctree.mode_duration",
21724
22313
  hidden: true,
22314
+ enabled: treePanel,
21725
22315
  run: () => setLane("duration")
21726
22316
  }, {
21727
22317
  name: "ctree.mode_turns",
21728
22318
  hidden: true,
22319
+ enabled: treePanel,
21729
22320
  run: () => setLane("turns")
21730
22321
  }, {
21731
22322
  name: "ctree.mode_calls",
21732
22323
  hidden: true,
22324
+ enabled: treePanel,
21733
22325
  run: () => setLane("calls")
21734
22326
  }, {
21735
22327
  name: "ctree.lanes_off",
21736
22328
  hidden: true,
22329
+ enabled: treePanel,
21737
22330
  run: () => {
21738
22331
  setLanesOn(false);
21739
22332
  api2.kv.set("ctree.lanesOn", false);
@@ -21741,6 +22334,7 @@ ${MERGE_TRUST}
21741
22334
  }, {
21742
22335
  name: "ctree.decisions",
21743
22336
  hidden: true,
22337
+ enabled: () => !inCrop(),
21744
22338
  run: () => setPanel(panel() === "decisions" ? "tree" : "decisions")
21745
22339
  }, {
21746
22340
  name: "ctree.export",
@@ -21760,8 +22354,11 @@ ${MERGE_TRUST}
21760
22354
  return;
21761
22355
  }
21762
22356
  if (cropMode()) {
21763
- setCropMode(undefined);
21764
- setMarked(new Set);
22357
+ leaveCropMode();
22358
+ return;
22359
+ }
22360
+ if (search()) {
22361
+ setSearch("");
21765
22362
  return;
21766
22363
  }
21767
22364
  back();
@@ -21775,18 +22372,64 @@ ${MERGE_TRUST}
21775
22372
  const root = state().root;
21776
22373
  return (root && root !== sessionID ? others()[root]?.title : undefined) ?? sessionTitle();
21777
22374
  };
21778
- const where = () => {
22375
+ const modeTag = () => cropMode() ? " \xB7 crop mode" : searchMode() ? " \xB7 search" : "";
22376
+ const headLine = () => {
21779
22377
  const b = branchOfCurrent();
21780
- if (!b)
21781
- return "trunk";
21782
- return `\u2387 ${b.name ?? sessionTitle()} (${b.status}${b.model ? ` \xB7 ${b.model.split("/").pop()}` : ""})`;
22378
+ const lead = "\u250C Context tree \xB7 ";
22379
+ const where = b ? `\u2387 ${clip(b.name ?? sessionTitle(), 28)}${b.status === "open" ? "" : ` (${b.status})`} \u2190 ` : "";
22380
+ const tail = b ? "" : " \xB7 trunk";
22381
+ const room = cols() - 4 - formatContext(contextSize(), contextLimit()).length - modeTag().length - lead.length - where.length - tail.length - 3;
22382
+ return `${lead}${where}${clip(title(), Math.max(8, room))}${tail}${modeTag()} `;
22383
+ };
22384
+ const statusLine = () => {
22385
+ const n = view().rows.length;
22386
+ const pos = `${n ? Math.min(selected() + 1, n) : 0}/${n}`;
22387
+ if (cropMode()) {
22388
+ const a = armed();
22389
+ return `\u2702 crop mode (${cropMode()}) \xB7 space mark \xB7 a auto \xB7 t result\u21C4turn \xB7 \u23CE apply \xB7 esc leave \xB7 marked ${selectedCandidates().length} ~${formatK(reclaimed(selectedCandidates()))}${a ? " \xB7 armed \u2014 space again to override" : ""}`;
22390
+ }
22391
+ if (searchMode())
22392
+ return `search: ${search()}\u258F \xB7 ${pos} rows \xB7 \u23CE keeps it \xB7 esc clears`;
22393
+ return `filter: ${filter()}${search() ? ` search: "${search()}"` : ""}${busy() ? ` \u2026 ${busy()}` : ""} ${pos} rows`;
22394
+ };
22395
+ const goVerb = () => {
22396
+ const row = current();
22397
+ if (!row)
22398
+ return "\u23CE go";
22399
+ if (row.kind === "branch")
22400
+ return row.isCurrent ? "\u23CE you are here" : `\u23CE switch to \u2387 ${clip(row.name, 20)}`;
22401
+ if (row.kind === "separator")
22402
+ return "\u23CE go";
22403
+ if (row.id === view().currentRowId)
22404
+ return "\u23CE you are here";
22405
+ return row.kind === "turn" ? "\u23CE fork & prefill this turn" : "\u23CE fork after this step";
22406
+ };
22407
+ const footer = () => {
22408
+ if (cropMode())
22409
+ return "space mark a auto t result\u21C4turn \u23CE apply esc leave";
22410
+ if (panel() === "decisions")
22411
+ return "\u23CE jump to record E export q back";
22412
+ if (panel() === "consumers")
22413
+ return "\u23CE expand space mark c crop q back";
22414
+ if (panel() === "help")
22415
+ return "esc/q back";
22416
+ return `${goVerb()} b branch m merge c crop ${UNDO_KEY} undo s consumers ? help q back`;
22417
+ };
22418
+ const showsTree = () => panel() === "tree" || panel() === "help";
22419
+ const emptyText = () => {
22420
+ const q = search().trim();
22421
+ if (q)
22422
+ return `no rows match "${q}" \xB7 esc clears`;
22423
+ if (filter() !== "default")
22424
+ return `no rows match filter: ${filter()} \xB7 f changes it`;
22425
+ return "(no messages yet \u2014 chat first, then open the tree)";
21783
22426
  };
21784
22427
  return (() => {
21785
- var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text"), _el$17 = _$createElement("text"), _el$18 = _$createTextNode(`\u2502 `), _el$19 = _$createTextNode(` `), _el$20 = _$createTextNode(` rows`), _el$31 = _$createElement("box"), _el$32 = _$createElement("box"), _el$34 = _$createElement("text"), _el$35 = _$createTextNode(`\u2514 `);
22428
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text"), _el$16 = _$createElement("text"), _el$17 = _$createTextNode(`\u2502 `), _el$28 = _$createElement("box"), _el$29 = _$createElement("box"), _el$35 = _$createElement("text"), _el$36 = _$createTextNode(`\u2514 `);
21786
22429
  _$insertNode(_el$, _el$2);
21787
- _$insertNode(_el$, _el$17);
21788
- _$insertNode(_el$, _el$31);
21789
- _$insertNode(_el$, _el$34);
22430
+ _$insertNode(_el$, _el$16);
22431
+ _$insertNode(_el$, _el$28);
22432
+ _$insertNode(_el$, _el$35);
21790
22433
  _$setProp(_el$, "flexDirection", "column");
21791
22434
  _$setProp(_el$, "padding", 1);
21792
22435
  _$setProp(_el$, "width", "100%");
@@ -21794,7 +22437,7 @@ ${MERGE_TRUST}
21794
22437
  _$insertNode(_el$2, _el$3);
21795
22438
  _$insertNode(_el$2, _el$4);
21796
22439
  _$setProp(_el$2, "flexDirection", "row");
21797
- _$insert(_el$3, () => `\u250C Context tree \xB7 ${title()} \xB7 ${where()} `);
22440
+ _$insert(_el$3, headLine);
21798
22441
  _$insert(_el$4, () => formatContext(contextSize(), contextLimit()));
21799
22442
  _$insert(_el$, _$createComponent(Show, {
21800
22443
  get when() {
@@ -21802,16 +22445,55 @@ ${MERGE_TRUST}
21802
22445
  },
21803
22446
  get children() {
21804
22447
  return [(() => {
21805
- var _el$5 = _$createElement("text"), _el$6 = _$createTextNode(`\u2502 Input `), _el$7 = _$createTextNode(` `), _el$8 = _$createTextNode(` \xB7 `), _el$9 = _$createTextNode(` \xB7 `);
22448
+ var _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$7 = _$createElement("text");
21806
22449
  _$insertNode(_el$5, _el$6);
21807
22450
  _$insertNode(_el$5, _el$7);
21808
- _$insertNode(_el$5, _el$8);
21809
- _$insertNode(_el$5, _el$9);
21810
- _$insert(_el$5, () => laneLine(laneSeries().input, contextLimit()), _el$7);
21811
- _$insert(_el$5, () => laneMode() === "duration" ? "[1] Duration" : " 1 duration", _el$8);
21812
- _$insert(_el$5, () => laneMode() === "turns" ? "[2] Turns" : " 2 turns", _el$9);
21813
- _$insert(_el$5, () => laneMode() === "calls" ? "[3] Calls" : " 3 calls", null);
21814
- _$effect((_$p) => _$setProp(_el$5, "fg", t.info, _$p));
22451
+ _$setProp(_el$5, "flexDirection", "row");
22452
+ _$insert(_el$6, () => `\u2502 Input ${strip().truncatedLeft > 0 ? `\u2026${strip().truncatedLeft}` : ""}`);
22453
+ _$insert(_el$5, _$createComponent(Show, {
22454
+ get when() {
22455
+ return !strip().empty.input;
22456
+ },
22457
+ get fallback() {
22458
+ return (() => {
22459
+ var _el$37 = _$createElement("text");
22460
+ _$insert(_el$37, () => "no input".padEnd(laneWidth()));
22461
+ _$effect((_$p) => _$setProp(_el$37, "fg", t.textMuted, _$p));
22462
+ return _el$37;
22463
+ })();
22464
+ },
22465
+ get children() {
22466
+ return _$createComponent(For, {
22467
+ get each() {
22468
+ return inputRuns();
22469
+ },
22470
+ children: (r) => (() => {
22471
+ var _el$38 = _$createElement("text");
22472
+ _$insert(_el$38, () => r.text);
22473
+ _$effect((_p$) => {
22474
+ var { fg: _v$0, bg: _v$1 } = r;
22475
+ _v$0 !== _p$.e && (_p$.e = _$setProp(_el$38, "fg", _v$0, _p$.e));
22476
+ _v$1 !== _p$.t && (_p$.t = _$setProp(_el$38, "bg", _v$1, _p$.t));
22477
+ return _p$;
22478
+ }, {
22479
+ e: undefined,
22480
+ t: undefined
22481
+ });
22482
+ return _el$38;
22483
+ })()
22484
+ });
22485
+ }
22486
+ }), _el$7);
22487
+ _$insert(_el$7, () => ` ${laneMode() === "duration" ? "[1] Duration" : " 1 duration"} \xB7 ${laneMode() === "turns" ? "[2] Turns" : " 2 turns"} \xB7 ${laneMode() === "calls" ? "[3] Calls" : " 3 calls"} \xB7 0 off`);
22488
+ _$effect((_p$) => {
22489
+ var { textMuted: _v$, textMuted: _v$2 } = t;
22490
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$, _p$.e));
22491
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "fg", _v$2, _p$.t));
22492
+ return _p$;
22493
+ }, {
22494
+ e: undefined,
22495
+ t: undefined
22496
+ });
21815
22497
  return _el$5;
21816
22498
  })(), _$createComponent(Show, {
21817
22499
  get when() {
@@ -21819,92 +22501,136 @@ ${MERGE_TRUST}
21819
22501
  },
21820
22502
  get children() {
21821
22503
  return [(() => {
21822
- var _el$0 = _$createElement("text"), _el$1 = _$createTextNode(`\u2502 Model `);
21823
- _$insertNode(_el$0, _el$1);
21824
- _$insert(_el$0, () => laneLine(laneSeries().output), null);
21825
- _$effect((_$p) => _$setProp(_el$0, "fg", t.accent, _$p));
21826
- return _el$0;
22504
+ var _el$8 = _$createElement("box"), _el$9 = _$createElement("text");
22505
+ _$insertNode(_el$8, _el$9);
22506
+ _$setProp(_el$8, "flexDirection", "row");
22507
+ _$insertNode(_el$9, _$createTextNode(`\u2502 Model `));
22508
+ _$insert(_el$8, _$createComponent(Show, {
22509
+ get when() {
22510
+ return !strip().empty.model;
22511
+ },
22512
+ get fallback() {
22513
+ return (() => {
22514
+ var _el$39 = _$createElement("text");
22515
+ _$insert(_el$39, () => "no model steps".padEnd(laneWidth()));
22516
+ _$effect((_$p) => _$setProp(_el$39, "fg", t.textMuted, _$p));
22517
+ return _el$39;
22518
+ })();
22519
+ },
22520
+ get children() {
22521
+ return _$createComponent(For, {
22522
+ get each() {
22523
+ return modelRuns();
22524
+ },
22525
+ children: (r) => (() => {
22526
+ var _el$40 = _$createElement("text");
22527
+ _$insert(_el$40, () => r.text);
22528
+ _$effect((_p$) => {
22529
+ var { fg: _v$10, bg: _v$11 } = r;
22530
+ _v$10 !== _p$.e && (_p$.e = _$setProp(_el$40, "fg", _v$10, _p$.e));
22531
+ _v$11 !== _p$.t && (_p$.t = _$setProp(_el$40, "bg", _v$11, _p$.t));
22532
+ return _p$;
22533
+ }, {
22534
+ e: undefined,
22535
+ t: undefined
22536
+ });
22537
+ return _el$40;
22538
+ })()
22539
+ });
22540
+ }
22541
+ }), null);
22542
+ _$effect((_$p) => _$setProp(_el$9, "fg", t.textMuted, _$p));
22543
+ return _el$8;
21827
22544
  })(), (() => {
21828
- var _el$10 = _$createElement("box"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text");
21829
- _$insertNode(_el$10, _el$11);
21830
- _$insertNode(_el$10, _el$13);
21831
- _$setProp(_el$10, "flexDirection", "row");
21832
- _$insertNode(_el$11, _$createTextNode(`\u2502 Tools `));
21833
- _$insert(_el$10, _$createComponent(For, {
21834
- get each() {
21835
- return toolRuns();
22545
+ var _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$12 = _$createElement("text");
22546
+ _$insertNode(_el$1, _el$10);
22547
+ _$insertNode(_el$1, _el$12);
22548
+ _$setProp(_el$1, "flexDirection", "row");
22549
+ _$insertNode(_el$10, _$createTextNode(`\u2502 Tools `));
22550
+ _$insert(_el$1, _$createComponent(Show, {
22551
+ get when() {
22552
+ return !strip().empty.tools;
21836
22553
  },
21837
- children: (run) => (() => {
21838
- var _el$36 = _$createElement("text");
21839
- _$insert(_el$36, () => run.text);
21840
- _$effect((_$p) => _$setProp(_el$36, "fg", run.error ? t.error : t.warning, _$p));
21841
- return _el$36;
21842
- })()
21843
- }), _el$13);
21844
- _$insertNode(_el$13, _$createTextNode(` i inspector \xB7 u consumers`));
22554
+ get fallback() {
22555
+ return (() => {
22556
+ var _el$41 = _$createElement("text");
22557
+ _$insert(_el$41, () => "no tool calls".padEnd(laneWidth()));
22558
+ _$effect((_$p) => _$setProp(_el$41, "fg", t.textMuted, _$p));
22559
+ return _el$41;
22560
+ })();
22561
+ },
22562
+ get children() {
22563
+ return _$createComponent(For, {
22564
+ get each() {
22565
+ return toolRuns();
22566
+ },
22567
+ children: (r) => (() => {
22568
+ var _el$42 = _$createElement("text");
22569
+ _$insert(_el$42, () => r.text);
22570
+ _$effect((_p$) => {
22571
+ var { fg: _v$12, bg: _v$13 } = r;
22572
+ _v$12 !== _p$.e && (_p$.e = _$setProp(_el$42, "fg", _v$12, _p$.e));
22573
+ _v$13 !== _p$.t && (_p$.t = _$setProp(_el$42, "bg", _v$13, _p$.t));
22574
+ return _p$;
22575
+ }, {
22576
+ e: undefined,
22577
+ t: undefined
22578
+ });
22579
+ return _el$42;
22580
+ })()
22581
+ });
22582
+ }
22583
+ }), _el$12);
22584
+ _$insertNode(_el$12, _$createTextNode(` i inspector \xB7 s consumers`));
21845
22585
  _$effect((_p$) => {
21846
- var { warning: _v$, warning: _v$2 } = t;
21847
- _v$ !== _p$.e && (_p$.e = _$setProp(_el$11, "fg", _v$, _p$.e));
21848
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$13, "fg", _v$2, _p$.t));
22586
+ var { textMuted: _v$3, textMuted: _v$4 } = t;
22587
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$10, "fg", _v$3, _p$.e));
22588
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$12, "fg", _v$4, _p$.t));
21849
22589
  return _p$;
21850
22590
  }, {
21851
22591
  e: undefined,
21852
22592
  t: undefined
21853
22593
  });
21854
- return _el$10;
22594
+ return _el$1;
21855
22595
  })()];
21856
22596
  }
21857
22597
  })];
21858
22598
  }
21859
- }), _el$17);
22599
+ }), _el$16);
21860
22600
  _$insert(_el$, _$createComponent(Show, {
21861
22601
  get when() {
21862
22602
  return _$memo(() => !!(laneRoom() && lanesOn()))() && !showLanes();
21863
22603
  },
21864
22604
  get children() {
21865
- var _el$15 = _$createElement("text");
21866
- _$insertNode(_el$15, _$createTextNode(`\u2502 lanes appear after 3 turns`));
21867
- _$effect((_$p) => _$setProp(_el$15, "fg", t.textMuted, _$p));
21868
- return _el$15;
21869
- }
21870
- }), _el$17);
21871
- _$insertNode(_el$17, _el$18);
21872
- _$insertNode(_el$17, _el$19);
21873
- _$insertNode(_el$17, _el$20);
21874
- _$insert(_el$17, (() => {
21875
- var _c$ = _$memo(() => !!cropMode());
21876
- return () => _c$() ? `\u2702 crop mode (${cropMode()}) \xB7 space mark \xB7 a auto \xB7 t result\u21C4turn \xB7 \u23CE apply \xB7 esc leave \xB7 marked ${selectedCandidates().length} ~${formatK(reclaimed(selectedCandidates()))}` : `filter: ${filter()}`;
21877
- })(), _el$19);
21878
- _$insert(_el$17, (() => {
21879
- var _c$2 = _$memo(() => !!search());
21880
- return () => _c$2() ? ` search: "${search()}"` : "";
21881
- })(), _el$19);
21882
- _$insert(_el$17, (() => {
21883
- var _c$3 = _$memo(() => !!busy());
21884
- return () => _c$3() ? ` \u2026 ${busy()}` : "";
21885
- })(), _el$19);
21886
- _$insert(_el$17, () => view().rows.length, _el$20);
22605
+ var _el$14 = _$createElement("text");
22606
+ _$insertNode(_el$14, _$createTextNode(`\u2502 lanes appear after 3 turns`));
22607
+ _$effect((_$p) => _$setProp(_el$14, "fg", t.textMuted, _$p));
22608
+ return _el$14;
22609
+ }
22610
+ }), _el$16);
22611
+ _$insertNode(_el$16, _el$17);
22612
+ _$insert(_el$16, statusLine, null);
21887
22613
  _$insert(_el$, _$createComponent(Show, {
21888
22614
  get when() {
21889
22615
  return panel() === "decisions";
21890
22616
  },
21891
22617
  get children() {
21892
22618
  return [(() => {
21893
- var _el$21 = _$createElement("text"), _el$22 = _$createTextNode(`\u2502 \u25C6 decisions on this tree (`), _el$23 = _$createTextNode(`) \xB7 \u23CE jump to record \xB7 E export markdown \xB7 D back`);
21894
- _$insertNode(_el$21, _el$22);
21895
- _$insertNode(_el$21, _el$23);
21896
- _$insert(_el$21, () => decisions().length, _el$23);
21897
- _$effect((_$p) => _$setProp(_el$21, "fg", t.accent, _$p));
21898
- return _el$21;
22619
+ var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`\u2502 \u25C6 decisions on this tree (`), _el$20 = _$createTextNode(`) \xB7 \u23CE jump to record \xB7 E export markdown \xB7 q back`);
22620
+ _$insertNode(_el$18, _el$19);
22621
+ _$insertNode(_el$18, _el$20);
22622
+ _$insert(_el$18, () => decisions().length, _el$20);
22623
+ _$effect((_$p) => _$setProp(_el$18, "fg", t.accent, _$p));
22624
+ return _el$18;
21899
22625
  })(), _$createComponent(Show, {
21900
22626
  get when() {
21901
22627
  return decisions().length === 0;
21902
22628
  },
21903
22629
  get children() {
21904
- var _el$24 = _$createElement("text");
21905
- _$insertNode(_el$24, _$createTextNode(`\u2502 (none yet \u2014 /merge a branch to write one)`));
21906
- _$effect((_$p) => _$setProp(_el$24, "fg", t.textMuted, _$p));
21907
- return _el$24;
22630
+ var _el$21 = _$createElement("text");
22631
+ _$insertNode(_el$21, _$createTextNode(`\u2502 (none yet \u2014 /merge a branch to write one)`));
22632
+ _$effect((_$p) => _$setProp(_el$21, "fg", t.textMuted, _$p));
22633
+ return _el$21;
21908
22634
  }
21909
22635
  }), _$createComponent(For, {
21910
22636
  get each() {
@@ -21912,132 +22638,142 @@ ${MERGE_TRUST}
21912
22638
  },
21913
22639
  children: (d, i) => {
21914
22640
  const sel = () => i() === decisionIndex();
21915
- const lines = () => (d.text ?? "").split(`
21916
- `).slice(0, sel() ? 12 : 1);
22641
+ const body = () => renderDecision(d.text ?? "", width() - 6);
22642
+ const room = () => Math.max(3, height() - decisions().length);
22643
+ const start = () => Math.min(decisionScroll(), Math.max(0, body().length - room()));
22644
+ const more = () => Math.max(0, body().length - start() - room());
21917
22645
  return (() => {
21918
- var _el$37 = _$createElement("box"), _el$38 = _$createElement("text"), _el$39 = _$createTextNode(` `), _el$40 = _$createTextNode(` \xB7 `);
21919
- _$insertNode(_el$37, _el$38);
21920
- _$setProp(_el$37, "flexDirection", "column");
21921
- _$insertNode(_el$38, _el$39);
21922
- _$insertNode(_el$38, _el$40);
21923
- _$insert(_el$38, () => sel() ? "\u203A" : "\u2502", _el$39);
21924
- _$insert(_el$38, () => d.hidden ? "\u25C7 (hidden from model) " : "\u25C6 ", _el$40);
21925
- _$insert(_el$38, () => d.branchName, _el$40);
21926
- _$insert(_el$38, () => new Date(d.recordedAt).toISOString().slice(0, 16).replace("T", " "), null);
21927
- _$insert(_el$38, (() => {
21928
- var _c$4 = _$memo(() => !!d.siblings.length);
21929
- return () => _c$4() ? ` \xB7 \u2717 ${d.siblings.map((x) => x.name).join(", ")}` : "";
22646
+ var _el$43 = _$createElement("box"), _el$44 = _$createElement("text"), _el$45 = _$createTextNode(` `), _el$46 = _$createTextNode(` \xB7 `);
22647
+ _$insertNode(_el$43, _el$44);
22648
+ _$setProp(_el$43, "flexDirection", "column");
22649
+ _$insertNode(_el$44, _el$45);
22650
+ _$insertNode(_el$44, _el$46);
22651
+ _$insert(_el$44, () => sel() ? "\u203A" : "\u2502", _el$45);
22652
+ _$insert(_el$44, () => d.hidden ? "\u25C7 (hidden from model) " : "\u25C6 ", _el$46);
22653
+ _$insert(_el$44, () => clip(decisionSummary(d.text ?? "").title || d.branchName, 48), _el$46);
22654
+ _$insert(_el$44, () => new Date(d.recordedAt).toISOString().slice(0, 16).replace("T", " "), null);
22655
+ _$insert(_el$44, (() => {
22656
+ var _c$3 = _$memo(() => !!d.siblings.length);
22657
+ return () => _c$3() ? ` \xB7 \u2717 ${d.siblings.map((x) => x.name).join(", ")}` : "";
21930
22658
  })(), null);
21931
- _$insert(_el$37, _$createComponent(For, {
22659
+ _$insert(_el$43, _$createComponent(For, {
21932
22660
  get each() {
21933
- return _$memo(() => !!sel())() ? lines().slice(1) : [];
22661
+ return _$memo(() => !!sel())() ? body().slice(start(), start() + room()) : [];
21934
22662
  },
21935
22663
  children: (l) => (() => {
21936
- var _el$41 = _$createElement("text"), _el$42 = _$createTextNode(`\u2502 `);
21937
- _$insertNode(_el$41, _el$42);
21938
- _$insert(_el$41, () => l.slice(0, width() - 6), null);
21939
- _$effect((_$p) => _$setProp(_el$41, "fg", t.text, _$p));
21940
- return _el$41;
22664
+ var _el$48 = _$createElement("text");
22665
+ _$insert(_el$48, `\u2502 ${l}`);
22666
+ _$effect((_$p) => _$setProp(_el$48, "fg", t.text, _$p));
22667
+ return _el$48;
21941
22668
  })()
21942
22669
  }), null);
22670
+ _$insert(_el$43, _$createComponent(Show, {
22671
+ get when() {
22672
+ return _$memo(() => !!sel())() && more() > 0;
22673
+ },
22674
+ get children() {
22675
+ var _el$47 = _$createElement("text");
22676
+ _$insert(_el$47, () => `\u2502 \u2026 ${more()} more lines \u2193 (ctrl+d)`);
22677
+ _$effect((_$p) => _$setProp(_el$47, "fg", t.textMuted, _$p));
22678
+ return _el$47;
22679
+ }
22680
+ }), null);
21943
22681
  _$effect((_p$) => {
21944
- var _v$8 = sel() ? t.background : t.accent, _v$9 = sel() ? t.primary : undefined;
21945
- _v$8 !== _p$.e && (_p$.e = _$setProp(_el$38, "fg", _v$8, _p$.e));
21946
- _v$9 !== _p$.t && (_p$.t = _$setProp(_el$38, "bg", _v$9, _p$.t));
22682
+ var _v$14 = sel() ? t.background : t.accent, _v$15 = sel() ? t.primary : undefined;
22683
+ _v$14 !== _p$.e && (_p$.e = _$setProp(_el$44, "fg", _v$14, _p$.e));
22684
+ _v$15 !== _p$.t && (_p$.t = _$setProp(_el$44, "bg", _v$15, _p$.t));
21947
22685
  return _p$;
21948
22686
  }, {
21949
22687
  e: undefined,
21950
22688
  t: undefined
21951
22689
  });
21952
- return _el$37;
22690
+ return _el$43;
21953
22691
  })();
21954
22692
  }
21955
22693
  })];
21956
22694
  }
21957
- }), _el$31);
22695
+ }), _el$28);
21958
22696
  _$insert(_el$, _$createComponent(Show, {
21959
22697
  get when() {
21960
22698
  return panel() === "consumers";
21961
22699
  },
21962
22700
  get children() {
21963
22701
  return [(() => {
21964
- var _el$26 = _$createElement("text"), _el$27 = _$createTextNode(`\u2502 what's filling the context \xB7 `), _el$28 = _$createTextNode(` total \xB7 c crop \xB7 u/esc back`);
21965
- _$insertNode(_el$26, _el$27);
21966
- _$insertNode(_el$26, _el$28);
21967
- _$insert(_el$26, () => formatK(view().totalTokens), _el$28);
21968
- _$effect((_$p) => _$setProp(_el$26, "fg", t.accent, _$p));
21969
- return _el$26;
22702
+ var _el$23 = _$createElement("text"), _el$24 = _$createTextNode(`\u2502 what's filling the context \xB7 `), _el$25 = _$createTextNode(` total \xB7 source \xB7 %tree \xB7 %window \xB7 tokens \xB7 entries`);
22703
+ _$insertNode(_el$23, _el$24);
22704
+ _$insertNode(_el$23, _el$25);
22705
+ _$insert(_el$23, () => formatK(view().totalTokens), _el$25);
22706
+ _$effect((_$p) => _$setProp(_el$23, "fg", t.accent, _$p));
22707
+ return _el$23;
21970
22708
  })(), _$createComponent(For, {
21971
22709
  get each() {
21972
- return consumerRows();
22710
+ return consumerLines();
21973
22711
  },
21974
- children: (c, i) => {
22712
+ children: (line, i) => {
21975
22713
  const sel = () => i() === consumerIndex();
22714
+ const c = line.bucket;
22715
+ const fg = () => sel() ? t.background : line.entry ? t.textMuted : c.kind === "tool" ? t.warning : t.text;
22716
+ const window = () => c.shareOfWindow === undefined ? "\u2013" : `${(c.shareOfWindow * 100).toFixed(0)}%`;
22717
+ const entry = line.entry;
21976
22718
  return (() => {
21977
- var _el$43 = _$createElement("text"), _el$44 = _$createTextNode(` `), _el$45 = _$createTextNode(` `), _el$46 = _$createTextNode(` `), _el$47 = _$createTextNode(` `), _el$48 = _$createTextNode(` \xB7 `), _el$49 = _$createTextNode(` entr`);
21978
- _$insertNode(_el$43, _el$44);
21979
- _$insertNode(_el$43, _el$45);
21980
- _$insertNode(_el$43, _el$46);
21981
- _$insertNode(_el$43, _el$47);
21982
- _$insertNode(_el$43, _el$48);
21983
- _$insertNode(_el$43, _el$49);
21984
- _$insert(_el$43, () => sel() ? "\u203A" : "\u2502", _el$44);
21985
- _$insert(_el$43, () => c.source.padEnd(22).slice(0, 22), _el$45);
21986
- _$insert(_el$43, () => `${(c.share * 100).toFixed(0)}%`.padStart(4), _el$46);
21987
- _$insert(_el$43, () => bar(c.share, 24), _el$47);
21988
- _$insert(_el$43, () => formatK(c.tokens).padStart(6), _el$48);
21989
- _$insert(_el$43, () => c.count, _el$49);
21990
- _$insert(_el$43, () => c.count === 1 ? "y" : "ies", null);
22719
+ var _el$49 = _$createElement("text"), _el$50 = _$createTextNode(` `);
22720
+ _$insertNode(_el$49, _el$50);
22721
+ _$insert(_el$49, () => sel() ? "\u203A" : "\u2502", _el$50);
22722
+ _$insert(_el$49, () => entry ? fitRow(` ${entry.croppable ? marked().has(entry.partID ?? "") ? "[x]" : "[ ]" : " "} ${plain(entry.preview)}${entry.croppable ? "" : ` \xB7 ${c.note ?? "not a completed tool result"}`}`, formatK(entry.tokens), width() - 4) : `${consumerOpen().has(c.source) ? "\u25BE" : "\u25B8"} ${c.source.padEnd(20).slice(0, 20)} ${`${(c.share * 100).toFixed(0)}%`.padStart(4)} ${window().padStart(5)} ${bar(c.tokens / consumerMax(), 18)} ${formatK(c.tokens).padStart(6)} \xB7 ${c.count} entr${c.count === 1 ? "y" : "ies"}${c.note ? ` \xB7 ${c.note}` : ""}`, null);
21991
22723
  _$effect((_p$) => {
21992
- var _v$0 = sel() ? t.background : c.kind === "tool" ? t.warning : t.text, _v$1 = sel() ? t.primary : undefined;
21993
- _v$0 !== _p$.e && (_p$.e = _$setProp(_el$43, "fg", _v$0, _p$.e));
21994
- _v$1 !== _p$.t && (_p$.t = _$setProp(_el$43, "bg", _v$1, _p$.t));
22724
+ var _v$16 = fg(), _v$17 = sel() ? t.primary : undefined;
22725
+ _v$16 !== _p$.e && (_p$.e = _$setProp(_el$49, "fg", _v$16, _p$.e));
22726
+ _v$17 !== _p$.t && (_p$.t = _$setProp(_el$49, "bg", _v$17, _p$.t));
21995
22727
  return _p$;
21996
22728
  }, {
21997
22729
  e: undefined,
21998
22730
  t: undefined
21999
22731
  });
22000
- return _el$43;
22732
+ return _el$49;
22001
22733
  })();
22002
22734
  }
22003
22735
  })];
22004
22736
  }
22005
- }), _el$31);
22006
- _$insert(_el$, _$createComponent(For, {
22007
- get each() {
22008
- return _$memo(() => panel() === "help")() ? HELP.slice(0, Math.max(6, size().rows - 5)) : [];
22009
- },
22010
- children: (l) => (() => {
22011
- var _el$50 = _$createElement("text"), _el$51 = _$createTextNode(`\u2502 `);
22012
- _$insertNode(_el$50, _el$51);
22013
- _$insert(_el$50, l, null);
22014
- _$effect((_$p) => _$setProp(_el$50, "fg", l.startsWith(" ") ? t.textMuted : t.accent, _$p));
22015
- return _el$50;
22016
- })()
22017
- }), _el$31);
22737
+ }), _el$28);
22018
22738
  _$insert(_el$, _$createComponent(Show, {
22019
22739
  get when() {
22020
- return _$memo(() => panel() === "tree")() && view().rows.length === 0;
22740
+ return _$memo(() => !!showsTree())() && view().rows.length === 0;
22741
+ },
22742
+ get children() {
22743
+ var _el$26 = _$createElement("text"), _el$27 = _$createTextNode(`\u2502 `);
22744
+ _$insertNode(_el$26, _el$27);
22745
+ _$insert(_el$26, emptyText, null);
22746
+ _$effect((_$p) => _$setProp(_el$26, "fg", t.textMuted, _$p));
22747
+ return _el$26;
22748
+ }
22749
+ }), _el$28);
22750
+ _$insertNode(_el$28, _el$29);
22751
+ _$setProp(_el$28, "flexDirection", "row");
22752
+ _$setProp(_el$28, "flexGrow", 1);
22753
+ _$setProp(_el$29, "flexDirection", "column");
22754
+ _$setProp(_el$29, "flexGrow", 1);
22755
+ _$insert(_el$29, _$createComponent(Show, {
22756
+ get when() {
22757
+ return _$memo(() => !!showsTree())() && overflow();
22021
22758
  },
22022
22759
  get children() {
22023
- var _el$29 = _$createElement("text");
22024
- _$insertNode(_el$29, _$createTextNode(`\u2502 (no messages yet \u2014 chat first, then open the tree)`));
22025
- _$effect((_$p) => _$setProp(_el$29, "fg", t.textMuted, _$p));
22026
- return _el$29;
22027
- }
22028
- }), _el$31);
22029
- _$insertNode(_el$31, _el$32);
22030
- _$setProp(_el$31, "flexDirection", "row");
22031
- _$setProp(_el$31, "flexGrow", 1);
22032
- _$setProp(_el$32, "flexDirection", "column");
22033
- _$setProp(_el$32, "flexGrow", 1);
22034
- _$insert(_el$32, _$createComponent(For, {
22760
+ var _el$30 = _$createElement("text"), _el$31 = _$createTextNode(`\u2502 `);
22761
+ _$insertNode(_el$30, _el$31);
22762
+ _$insert(_el$30, (() => {
22763
+ var _c$ = _$memo(() => hiddenAbove() > 0);
22764
+ return () => _c$() ? `\u2191 ${hiddenAbove()} more` : "";
22765
+ })(), null);
22766
+ _$effect((_$p) => _$setProp(_el$30, "fg", t.textMuted, _$p));
22767
+ return _el$30;
22768
+ }
22769
+ }), null);
22770
+ _$insert(_el$29, _$createComponent(For, {
22035
22771
  get each() {
22036
- return _$memo(() => panel() === "tree")() ? visible() : [];
22772
+ return _$memo(() => !!showsTree())() ? visible() : [];
22037
22773
  },
22038
22774
  children: (row, i) => {
22039
22775
  const isSel = () => windowStart() + i() === selected();
22040
- const color = () => row.kind === "branch" ? statusColor(t, row) : row.kind === "turn" ? row.isDecision ? t.accent : t.text : row.isError ? t.error : row.warn ? t.warning : t.textMuted;
22776
+ const color = () => row.kind === "separator" ? t.textMuted : row.kind === "branch" ? statusColor(t, row) : !row.inContext ? t.textMuted : row.kind === "turn" ? row.isDecision ? t.accent : t.text : row.isError ? t.error : row.warn ? t.warning : t.textMuted;
22041
22777
  const mark = () => {
22042
22778
  if (!cropMode())
22043
22779
  return "";
@@ -22048,70 +22784,139 @@ ${MERGE_TRUST}
22048
22784
  const prot = c.protections.filter((p) => p !== "too-small");
22049
22785
  return `${on2 ? "[x]" : "[ ]"}${prot.length ? "!" : " "}`;
22050
22786
  };
22051
- return (() => {
22052
- var _el$52 = _$createElement("text"), _el$53 = _$createTextNode(` `);
22053
- _$insertNode(_el$52, _el$53);
22054
- _$insert(_el$52, () => isSel() ? "\u203A" : "\u2502", _el$53);
22055
- _$insert(_el$52, mark, null);
22056
- _$insert(_el$52, () => rowLine(row, rowWidth(), row.id === view().currentRowId), null);
22057
- _$effect((_p$) => {
22058
- var _v$10 = isSel() ? t.background : color(), _v$11 = isSel() ? t.primary : undefined;
22059
- _v$10 !== _p$.e && (_p$.e = _$setProp(_el$52, "fg", _v$10, _p$.e));
22060
- _v$11 !== _p$.t && (_p$.t = _$setProp(_el$52, "bg", _v$11, _p$.t));
22061
- return _p$;
22062
- }, {
22063
- e: undefined,
22064
- t: undefined
22065
- });
22066
- return _el$52;
22067
- })();
22787
+ const prefix = () => `${isSel() ? "\u203A" : "\u2502"} ${mark()}`;
22788
+ const segs = () => segmentsOf(rowLine(row, rowWidth(), row.id === view().currentRowId), search().trim(), thoughtOf(row));
22789
+ return _$createComponent(Show, {
22790
+ get when() {
22791
+ return segs().length > 1;
22792
+ },
22793
+ get fallback() {
22794
+ return (() => {
22795
+ var _el$53 = _$createElement("text");
22796
+ _$insert(_el$53, prefix, null);
22797
+ _$insert(_el$53, () => segs()[0]?.text, null);
22798
+ _$effect((_p$) => {
22799
+ var _v$20 = isSel() ? t.background : color(), _v$21 = isSel() ? t.primary : undefined;
22800
+ _v$20 !== _p$.e && (_p$.e = _$setProp(_el$53, "fg", _v$20, _p$.e));
22801
+ _v$21 !== _p$.t && (_p$.t = _$setProp(_el$53, "bg", _v$21, _p$.t));
22802
+ return _p$;
22803
+ }, {
22804
+ e: undefined,
22805
+ t: undefined
22806
+ });
22807
+ return _el$53;
22808
+ })();
22809
+ },
22810
+ get children() {
22811
+ var _el$51 = _$createElement("box"), _el$52 = _$createElement("text");
22812
+ _$insertNode(_el$51, _el$52);
22813
+ _$setProp(_el$51, "flexDirection", "row");
22814
+ _$insert(_el$52, prefix);
22815
+ _$insert(_el$51, _$createComponent(For, {
22816
+ get each() {
22817
+ return segs();
22818
+ },
22819
+ children: (s) => (() => {
22820
+ var _el$54 = _$createElement("text");
22821
+ _$insert(_el$54, () => s.text);
22822
+ _$effect((_p$) => {
22823
+ var _v$22 = s.kind === "match" ? t.background : s.kind === "dim" ? t.textMuted : isSel() ? t.background : color(), _v$23 = s.kind === "match" ? t.accent : isSel() ? t.primary : undefined;
22824
+ _v$22 !== _p$.e && (_p$.e = _$setProp(_el$54, "fg", _v$22, _p$.e));
22825
+ _v$23 !== _p$.t && (_p$.t = _$setProp(_el$54, "bg", _v$23, _p$.t));
22826
+ return _p$;
22827
+ }, {
22828
+ e: undefined,
22829
+ t: undefined
22830
+ });
22831
+ return _el$54;
22832
+ })()
22833
+ }), null);
22834
+ _$effect((_p$) => {
22835
+ var _v$18 = isSel() ? t.background : color(), _v$19 = isSel() ? t.primary : undefined;
22836
+ _v$18 !== _p$.e && (_p$.e = _$setProp(_el$52, "fg", _v$18, _p$.e));
22837
+ _v$19 !== _p$.t && (_p$.t = _$setProp(_el$52, "bg", _v$19, _p$.t));
22838
+ return _p$;
22839
+ }, {
22840
+ e: undefined,
22841
+ t: undefined
22842
+ });
22843
+ return _el$51;
22844
+ }
22845
+ });
22846
+ }
22847
+ }), null);
22848
+ _$insert(_el$29, _$createComponent(Show, {
22849
+ get when() {
22850
+ return _$memo(() => !!showsTree())() && overflow();
22851
+ },
22852
+ get children() {
22853
+ var _el$32 = _$createElement("text"), _el$33 = _$createTextNode(`\u2502 `);
22854
+ _$insertNode(_el$32, _el$33);
22855
+ _$insert(_el$32, (() => {
22856
+ var _c$2 = _$memo(() => hiddenBelow() > 0);
22857
+ return () => _c$2() ? `\u2026 ${hiddenBelow()} more \u2193` : "";
22858
+ })(), null);
22859
+ _$effect((_$p) => _$setProp(_el$32, "fg", t.textMuted, _$p));
22860
+ return _el$32;
22068
22861
  }
22069
22862
  }), null);
22070
- _$insert(_el$32, _$createComponent(For, {
22863
+ _$insert(_el$29, _$createComponent(For, {
22864
+ get each() {
22865
+ return _$memo(() => !!(showsTree() && view().rows.length > 0 && !view().rows.some((r) => r.kind === "branch")))() ? noBranchesLines() : [];
22866
+ },
22867
+ children: (l) => (() => {
22868
+ var _el$55 = _$createElement("text"), _el$56 = _$createTextNode(`\u2502 `);
22869
+ _$insertNode(_el$55, _el$56);
22870
+ _$insert(_el$55, l, null);
22871
+ _$effect((_$p) => _$setProp(_el$55, "fg", t.textMuted, _$p));
22872
+ return _el$55;
22873
+ })()
22874
+ }), null);
22875
+ _$insert(_el$29, _$createComponent(For, {
22071
22876
  get each() {
22072
- return _$memo(() => !!(panel() === "tree" && view().rows.length > 0 && !view().rows.some((r) => r.kind === "branch")))() ? noBranchesLines() : [];
22877
+ return _$memo(() => panel() === "help")() ? HELP.slice(0, helpHeight()) : [];
22073
22878
  },
22074
22879
  children: (l) => (() => {
22075
- var _el$54 = _$createElement("text"), _el$55 = _$createTextNode(`\u2502 `);
22076
- _$insertNode(_el$54, _el$55);
22077
- _$insert(_el$54, l, null);
22078
- _$effect((_$p) => _$setProp(_el$54, "fg", t.textMuted, _$p));
22079
- return _el$54;
22880
+ var _el$57 = _$createElement("text"), _el$58 = _$createTextNode(`\u2502 `);
22881
+ _$insertNode(_el$57, _el$58);
22882
+ _$insert(_el$57, l, null);
22883
+ _$effect((_$p) => _$setProp(_el$57, "fg", l.startsWith(" ") ? t.textMuted : t.accent, _$p));
22884
+ return _el$57;
22080
22885
  })()
22081
22886
  }), null);
22082
- _$insert(_el$31, _$createComponent(Show, {
22887
+ _$insert(_el$28, _$createComponent(Show, {
22083
22888
  get when() {
22084
22889
  return showInspector();
22085
22890
  },
22086
22891
  get children() {
22087
- var _el$33 = _$createElement("box");
22088
- _$setProp(_el$33, "flexDirection", "column");
22089
- _$setProp(_el$33, "paddingLeft", 1);
22090
- _$insert(_el$33, _$createComponent(For, {
22892
+ var _el$34 = _$createElement("box");
22893
+ _$setProp(_el$34, "flexDirection", "column");
22894
+ _$setProp(_el$34, "paddingLeft", 1);
22895
+ _$insert(_el$34, _$createComponent(For, {
22091
22896
  get each() {
22092
22897
  return inspectorLines();
22093
22898
  },
22094
22899
  children: (l) => (() => {
22095
- var _el$56 = _$createElement("text"), _el$57 = _$createTextNode(`\u2503 `);
22096
- _$insertNode(_el$56, _el$57);
22097
- _$insert(_el$56, () => l.text, null);
22098
- _$effect((_$p) => _$setProp(_el$56, "fg", l.fg, _$p));
22099
- return _el$56;
22900
+ var _el$59 = _$createElement("text"), _el$60 = _$createTextNode(`\u2503 `);
22901
+ _$insertNode(_el$59, _el$60);
22902
+ _$insert(_el$59, () => l.text, null);
22903
+ _$effect((_$p) => _$setProp(_el$59, "fg", l.fg, _$p));
22904
+ return _el$59;
22100
22905
  })()
22101
22906
  }));
22102
- _$effect((_$p) => _$setProp(_el$33, "width", inspectorWidth(), _$p));
22103
- return _el$33;
22907
+ _$effect((_$p) => _$setProp(_el$34, "width", inspectorWidth(), _$p));
22908
+ return _el$34;
22104
22909
  }
22105
22910
  }), null);
22106
- _$insertNode(_el$34, _el$35);
22107
- _$insert(_el$34, () => cropMode() ? "space mark a auto t result\u21C4turn \u23CE apply esc leave" : "\u23CE go b branch m merge c crop i inspector 1\xB72\xB73 lanes x undo ? help q back", null);
22911
+ _$insertNode(_el$35, _el$36);
22912
+ _$insert(_el$35, footer, null);
22108
22913
  _$effect((_p$) => {
22109
- var { background: _v$3, primary: _v$4 } = t, _v$5 = t[BAND_KEY[band()]], _v$6 = cropMode() ? t.warning : t.textMuted, _v$7 = cropMode() ? t.warning : t.textMuted;
22110
- _v$3 !== _p$.e && (_p$.e = _$setProp(_el$, "backgroundColor", _v$3, _p$.e));
22111
- _v$4 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$4, _p$.t));
22112
- _v$5 !== _p$.a && (_p$.a = _$setProp(_el$4, "fg", _v$5, _p$.a));
22113
- _v$6 !== _p$.o && (_p$.o = _$setProp(_el$17, "fg", _v$6, _p$.o));
22114
- _v$7 !== _p$.i && (_p$.i = _$setProp(_el$34, "fg", _v$7, _p$.i));
22914
+ var { background: _v$5, primary: _v$6 } = t, _v$7 = t[BAND_KEY[band()]], _v$8 = cropMode() ? t.warning : searchMode() ? t.accent : t.textMuted, _v$9 = cropMode() ? t.warning : t.textMuted;
22915
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$, "backgroundColor", _v$5, _p$.e));
22916
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$6, _p$.t));
22917
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$4, "fg", _v$7, _p$.a));
22918
+ _v$8 !== _p$.o && (_p$.o = _$setProp(_el$16, "fg", _v$8, _p$.o));
22919
+ _v$9 !== _p$.i && (_p$.i = _$setProp(_el$35, "fg", _v$9, _p$.i));
22115
22920
  return _p$;
22116
22921
  }, {
22117
22922
  e: undefined,
@@ -22342,7 +23147,7 @@ var tui = async (api2, rawOptions) => {
22342
23147
  adoptSoon();
22343
23148
  });
22344
23149
  api2.lifecycle?.onDispose(offCreated);
22345
- const promptDialog = (title, placeholder) => new Promise((resolve) => {
23150
+ const promptDialog2 = (title, placeholder) => new Promise((resolve) => {
22346
23151
  api2.ui.dialog.replace(() => api2.ui.DialogPrompt({
22347
23152
  title,
22348
23153
  placeholder,
@@ -22399,7 +23204,7 @@ var tui = async (api2, rawOptions) => {
22399
23204
  if (!sessionID)
22400
23205
  return;
22401
23206
  await new Promise((r) => setTimeout(r, 30));
22402
- const name = await promptDialog(BRANCH_DIALOG.title, BRANCH_DIALOG.placeholder);
23207
+ const name = await promptDialog2(BRANCH_DIALOG.title, BRANCH_DIALOG.placeholder);
22403
23208
  debug("branch.named", {
22404
23209
  name
22405
23210
  });
@@ -22437,7 +23242,7 @@ var tui = async (api2, rawOptions) => {
22437
23242
  const last = api2.state.session.messages(sessionID).at(-1);
22438
23243
  if (!last)
22439
23244
  return;
22440
- const value = await promptDialog("Label (empty to remove)", "checkpoint");
23245
+ const value = await promptDialog2("Label (empty to remove)", "checkpoint");
22441
23246
  if (value === undefined)
22442
23247
  return;
22443
23248
  setLabel({
@@ -22482,11 +23287,20 @@ var tui = async (api2, rawOptions) => {
22482
23287
  });
22483
23288
  return;
22484
23289
  }
23290
+ const parent = await fetchTranscript(api2, branch.parentSessionID, directory).catch(() => {
23291
+ return;
23292
+ });
23293
+ const parentLabel = branch.parentSessionID === state.root ? TRUNK_LABEL : state.sessions[branch.parentSessionID]?.name ?? TRUNK_LABEL;
23294
+ const turns = ownTurnCount(api2.state.session.messages(sessionID), {
23295
+ messageID: branch.anchorMessageID,
23296
+ parentMessageIDs: parent?.messages.map((m) => m.id) ?? []
23297
+ });
22485
23298
  const mode = await new Promise((resolve) => {
22486
23299
  api2.ui.dialog.replace(() => api2.ui.DialogSelect({
22487
- title: mergeDialogTitle(branch.name ?? "branch", api2.state.session.get(branch.parentSessionID)?.title),
23300
+ title: mergeDialogTitle(branch.name ?? "branch", parent ? mergeTargetOf(parentLabel, parent.messages) : undefined),
22488
23301
  options: mergeDialogOptions({
22489
- siblings: openSiblings(state, sessionID).length
23302
+ siblings: openSiblings(state, sessionID).length,
23303
+ turns
22490
23304
  }),
22491
23305
  onSelect: (o) => {
22492
23306
  resolve(o.value);
@@ -22581,6 +23395,8 @@ ${MERGE_TRUST}`,
22581
23395
  return store.stateForSession(props.session_id);
22582
23396
  });
22583
23397
  const branch = () => st()?.sessions[props.session_id];
23398
+ const size = createMemo2(() => contextSizeOf(toMinimalMessages(api2.state.session.messages(props.session_id), api2.state.part)));
23399
+ const limit = createMemo2(() => modelContextLimit(api2, props.session_id));
22584
23400
  const crops = () => st()?.activeCrops(props.session_id) ?? [];
22585
23401
  const hidden = () => crops().reduce((s, c) => s + c.targets.reduce((x, y) => x + y.estTokens, 0), 0);
22586
23402
  const siblings = () => Object.values(st()?.sessions ?? {}).filter((b) => b.parentSessionID === props.session_id && b.status === "open").length;
@@ -22591,9 +23407,10 @@ ${MERGE_TRUST}`,
22591
23407
  return `${b.status}${title && room > 3 ? ` \xB7 from "${clip(title, room)}"` : ""}`;
22592
23408
  };
22593
23409
  return (() => {
22594
- var _el$ = _$createElement2("box"), _el$2 = _$createElement2("text"), _el$3 = _$createElement2("b"), _el$8 = _$createElement2("text");
23410
+ var _el$ = _$createElement2("box"), _el$2 = _$createElement2("text"), _el$3 = _$createElement2("b"), _el$7 = _$createElement2("text"), _el$9 = _$createElement2("text");
22595
23411
  _$insertNode2(_el$, _el$2);
22596
- _$insertNode2(_el$, _el$8);
23412
+ _$insertNode2(_el$, _el$7);
23413
+ _$insertNode2(_el$, _el$9);
22597
23414
  _$setProp2(_el$, "flexDirection", "column");
22598
23415
  _$insertNode2(_el$2, _el$3);
22599
23416
  _$insertNode2(_el$3, _$createTextNode2(`Context tree`));
@@ -22603,10 +23420,10 @@ ${MERGE_TRUST}`,
22603
23420
  },
22604
23421
  get fallback() {
22605
23422
  return (() => {
22606
- var _el$0 = _$createElement2("text");
22607
- _$insert2(_el$0, () => `trunk${siblings() ? ` \xB7 ${siblings()} branch${siblings() === 1 ? "" : "es"}` : ""}`);
22608
- _$effect2((_$p) => _$setProp2(_el$0, "fg", t.text, _$p));
22609
- return _el$0;
23423
+ var _el$1 = _$createElement2("text");
23424
+ _$insert2(_el$1, () => `trunk${siblings() ? ` \xB7 ${siblings()} branch${siblings() === 1 ? "" : "es"}` : ""}`);
23425
+ _$effect2((_$p) => _$setProp2(_el$1, "fg", t.text, _$p));
23426
+ return _el$1;
22610
23427
  })();
22611
23428
  },
22612
23429
  get children() {
@@ -22622,27 +23439,30 @@ ${MERGE_TRUST}`,
22622
23439
  return _el$6;
22623
23440
  })()];
22624
23441
  }
22625
- }), _el$8);
23442
+ }), _el$7);
23443
+ _$insert2(_el$7, () => formatContext(size(), limit()));
22626
23444
  _$insert2(_el$, _$createComponent2(Show2, {
22627
23445
  get when() {
22628
23446
  return crops().length;
22629
23447
  },
22630
23448
  get children() {
22631
- var _el$7 = _$createElement2("text");
22632
- _$insert2(_el$7, () => `\u2702 ${crops().length} crop${crops().length === 1 ? "" : "s"} \xB7 ~${formatK(hidden())} hidden`);
22633
- _$effect2((_$p) => _$setProp2(_el$7, "fg", t.warning, _$p));
22634
- return _el$7;
23449
+ var _el$8 = _$createElement2("text");
23450
+ _$insert2(_el$8, () => `\u2702 ${crops().length} crop${crops().length === 1 ? "" : "s"} \xB7 ~${formatK(hidden())} hidden`);
23451
+ _$effect2((_$p) => _$setProp2(_el$8, "fg", t.warning, _$p));
23452
+ return _el$8;
22635
23453
  }
22636
- }), _el$8);
22637
- _$insertNode2(_el$8, _$createTextNode2(`/tree \xB7 ctrl+q`));
23454
+ }), _el$9);
23455
+ _$insertNode2(_el$9, _$createTextNode2(`/tree \xB7 ctrl+q`));
22638
23456
  _$effect2((_p$) => {
22639
- var { text: _v$, textMuted: _v$2 } = t;
23457
+ var _v$ = t.text, _v$2 = t[BAND_COLOR[bandFor(size().tokens, limit())]], _v$3 = t.textMuted;
22640
23458
  _v$ !== _p$.e && (_p$.e = _$setProp2(_el$2, "fg", _v$, _p$.e));
22641
- _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$8, "fg", _v$2, _p$.t));
23459
+ _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$7, "fg", _v$2, _p$.t));
23460
+ _v$3 !== _p$.a && (_p$.a = _$setProp2(_el$9, "fg", _v$3, _p$.a));
22642
23461
  return _p$;
22643
23462
  }, {
22644
23463
  e: undefined,
22645
- t: undefined
23464
+ t: undefined,
23465
+ a: undefined
22646
23466
  });
22647
23467
  return _el$;
22648
23468
  })();
@@ -22650,12 +23470,12 @@ ${MERGE_TRUST}`,
22650
23470
  session_prompt_right: (_ctx, props) => {
22651
23471
  const t = api2.theme.current;
22652
23472
  const size = createMemo2(() => contextSizeOf(toMinimalMessages(api2.state.session.messages(props.session_id), api2.state.part)));
22653
- const band = () => bandFor(size().tokens);
22654
23473
  const branch = () => {
22655
23474
  journalRevision();
22656
23475
  return store.stateForSession(props.session_id)?.sessions[props.session_id];
22657
23476
  };
22658
23477
  const limit = createMemo2(() => modelContextLimit(api2, props.session_id));
23478
+ const band = () => bandFor(size().tokens, limit());
22659
23479
  const reserve = () => api2.state.config.compaction?.reserved ?? 16384;
22660
23480
  const [trend, setTrend] = createSignal3("");
22661
23481
  let prevTokens = 0;
@@ -22711,7 +23531,7 @@ ${MERGE_TRUST}`,
22711
23531
  redNudged = true;
22712
23532
  api2.ui.toast({
22713
23533
  variant: "warning",
22714
- message: "context is in the red band (\u226564k) \u2014 consider /tree \u2192 c crop, or /merge a branch",
23534
+ message: `context is in the red band (${limit() ? "\u226585% of the window" : "\u226564k"}) \u2014 consider /tree \u2192 c crop, or /merge a branch`,
22715
23535
  duration: 6000
22716
23536
  });
22717
23537
  } else if (b === "low" || b === "healthy")
@@ -22728,10 +23548,10 @@ ${MERGE_TRUST}`,
22728
23548
  guardNudged = false;
22729
23549
  });
22730
23550
  return (() => {
22731
- var _el$1 = _$createElement2("text");
22732
- _$insert2(_el$1, () => `${branch() ? `\u2387 ${branchLabel(api2, props.session_id, branch().name, 24)} \xB7 ` : ""}${formatContext(size(), limit())}${trend()}`);
22733
- _$effect2((_$p) => _$setProp2(_el$1, "fg", t[BAND_COLOR[band()]], _$p));
22734
- return _el$1;
23551
+ var _el$10 = _$createElement2("text");
23552
+ _$insert2(_el$10, () => `${branch() ? `\u2387 ${branchLabel(api2, props.session_id, branch().name, 24)} \xB7 ` : ""}${formatContext(size(), limit())}${trend()}`);
23553
+ _$effect2((_$p) => _$setProp2(_el$10, "fg", t[BAND_COLOR[band()]], _$p));
23554
+ return _el$10;
22735
23555
  })();
22736
23556
  }
22737
23557
  }