opencode-context-tree 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,15 +19787,28 @@ 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";
@@ -19671,6 +19817,8 @@ import { For, Show, createEffect, createMemo, createSignal as createSignal2, on,
19671
19817
 
19672
19818
  // src/core/actions.ts
19673
19819
  function planJump(row, ctx) {
19820
+ if (row.kind === "separator")
19821
+ return { kind: "noop", reason: "nothing to go to" };
19674
19822
  if (row.kind === "branch")
19675
19823
  return row.sessionID === ctx.currentSessionID ? { kind: "noop", reason: "you are here" } : { kind: "switch", sessionID: row.sessionID };
19676
19824
  const tr = ctx.transcripts[row.sessionID];
@@ -19698,15 +19846,33 @@ function planJump(row, ctx) {
19698
19846
  }
19699
19847
 
19700
19848
  // src/core/navigation.ts
19849
+ function scan(rows, index, dir) {
19850
+ for (let i = index;i >= 0 && i < rows.length; i += dir)
19851
+ if (rows[i].kind !== "separator")
19852
+ return i;
19853
+ return -1;
19854
+ }
19701
19855
  function moveSelection(rows, index, delta) {
19702
19856
  if (rows.length === 0)
19703
19857
  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;
19858
+ const dir = delta < 0 ? -1 : 1;
19859
+ let i = Math.min(Math.max(index, 0), rows.length - 1);
19860
+ for (let step = Math.abs(delta);step > 0; step--) {
19861
+ const next = scan(rows, i + dir, dir);
19862
+ if (next === -1)
19863
+ break;
19864
+ i = next;
19865
+ }
19866
+ if (rows[i].kind !== "separator")
19867
+ return i;
19868
+ const ahead = scan(rows, i, dir);
19869
+ return ahead === -1 ? scan(rows, i, dir === 1 ? -1 : 1) : ahead;
19870
+ }
19871
+ function firstIndex(rows) {
19872
+ return scan(rows, 0, 1);
19873
+ }
19874
+ function lastIndex(rows) {
19875
+ return scan(rows, rows.length - 1, -1);
19710
19876
  }
19711
19877
  function nextBranchIndex(rows, index, dir) {
19712
19878
  if (rows.length === 0)
@@ -19729,39 +19895,40 @@ function toggleExpanded(expanded, sessionID) {
19729
19895
  next.add(sessionID);
19730
19896
  return next;
19731
19897
  }
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
19898
  function resolveSelection(view, preferredId, currentRowId, previousIndex) {
19738
19899
  if (view.rows.length === 0)
19739
19900
  return -1;
19901
+ const land = (i) => {
19902
+ const ahead = scan(view.rows, i, 1);
19903
+ return ahead === -1 ? scan(view.rows, i, -1) : ahead;
19904
+ };
19740
19905
  if (preferredId !== undefined) {
19741
19906
  const exact = view.indexById[preferredId];
19742
19907
  if (exact !== undefined)
19743
- return exact;
19908
+ return land(exact);
19744
19909
  const owner = preferredId.split(":").slice(0, 2).join(":");
19745
19910
  for (let i = 0;i < view.rows.length; i++) {
19746
19911
  const row = view.rows[i];
19912
+ if (row.kind === "separator")
19913
+ continue;
19747
19914
  const rowOwner = row.kind === "branch" ? row.id : `${row.sessionID}:${row.messageID}`;
19748
19915
  if (rowOwner === owner)
19749
19916
  return i;
19750
19917
  }
19751
19918
  }
19752
19919
  if (previousIndex !== undefined && previousIndex >= 0)
19753
- return Math.min(previousIndex, view.rows.length - 1);
19920
+ return land(Math.min(previousIndex, view.rows.length - 1));
19754
19921
  if (currentRowId !== undefined) {
19755
19922
  const idx = view.indexById[currentRowId];
19756
19923
  if (idx !== undefined)
19757
- return idx;
19924
+ return land(idx);
19758
19925
  }
19759
19926
  if (view.currentRowId !== undefined) {
19760
19927
  const idx = view.indexById[view.currentRowId];
19761
19928
  if (idx !== undefined)
19762
- return idx;
19929
+ return land(idx);
19763
19930
  }
19764
- return 0;
19931
+ return land(0);
19765
19932
  }
19766
19933
 
19767
19934
  // src/core/transcript.ts
@@ -19833,6 +20000,7 @@ function messagePreview(message) {
19833
20000
  }
19834
20001
 
19835
20002
  // src/core/tree.ts
20003
+ var OFF_PATH_TEXT = "\u2500\u2500 not in this branch's context \u2500\u2500";
19836
20004
  var WARN_TOKENS = 1e4;
19837
20005
  function userText(message) {
19838
20006
  return message.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
@@ -19959,7 +20127,7 @@ function branchAllowed(filter) {
19959
20127
  function isCropped(ctx, messageID, partID) {
19960
20128
  return ctx.crops.some((c) => c.messageID === messageID && (c.partID === undefined || c.partID === partID));
19961
20129
  }
19962
- function emitAssistantRows(ctx, sessionID, message, depth, gutter, out) {
20130
+ function emitAssistantRows(ctx, sessionID, message, depth, gutter, inContext, out) {
19963
20131
  if (message.summary) {
19964
20132
  if (!stepAllowed(ctx.filter, "text"))
19965
20133
  return;
@@ -19985,19 +20153,29 @@ function emitAssistantRows(ctx, sessionID, message, depth, gutter, out) {
19985
20153
  durationMs: durationOfParts(message.parts),
19986
20154
  isError: false,
19987
20155
  isCropped: false,
19988
- warn: tokens >= WARN_TOKENS
20156
+ warn: tokens >= WARN_TOKENS,
20157
+ inContext
19989
20158
  });
19990
20159
  return;
19991
20160
  }
20161
+ const collapseThinking = ctx.filter !== "all";
20162
+ const rows = [];
20163
+ let thinkingMs;
19992
20164
  let first = true;
19993
20165
  for (const part of message.parts) {
19994
20166
  const kind = stepKind(part);
20167
+ if (collapseThinking && kind === "reasoning") {
20168
+ const ms = durationOfPart(part);
20169
+ if (ms !== undefined)
20170
+ thinkingMs = (thinkingMs ?? 0) + ms;
20171
+ continue;
20172
+ }
19995
20173
  const label = first ? ctx.labels[message.id] : undefined;
19996
20174
  if (!stepAllowed(ctx.filter, kind, Boolean(label)))
19997
20175
  continue;
19998
20176
  const { tokens, estimated } = stepTokensFor(part, message);
19999
20177
  first = false;
20000
- out.push({
20178
+ rows.push({
20001
20179
  kind: "step",
20002
20180
  id: `${sessionID}:${message.id}:${part.id}`,
20003
20181
  sessionID,
@@ -20013,9 +20191,40 @@ function emitAssistantRows(ctx, sessionID, message, depth, gutter, out) {
20013
20191
  isError: part.state?.status === "error",
20014
20192
  isCropped: isCropped(ctx, message.id, part.id),
20015
20193
  warn: tokens >= WARN_TOKENS,
20016
- label
20194
+ label,
20195
+ inContext
20017
20196
  });
20018
20197
  }
20198
+ if (rows.length > 0) {
20199
+ if (thinkingMs !== undefined)
20200
+ rows[0].thinkingMs = thinkingMs;
20201
+ } else if (collapseThinking) {
20202
+ const thinking = message.parts.filter((p) => stepKind(p) === "reasoning");
20203
+ const label = ctx.labels[message.id];
20204
+ if (thinking.length > 0 && stepAllowed(ctx.filter, "reasoning", Boolean(label))) {
20205
+ const tokens = thinking.reduce((sum, p) => sum + stepTokensFor(p, message).tokens, 0);
20206
+ rows.push({
20207
+ kind: "step",
20208
+ id: `${sessionID}:${message.id}:${thinking[0].id}`,
20209
+ sessionID,
20210
+ messageID: message.id,
20211
+ partID: thinking[0].id,
20212
+ depth,
20213
+ gutter,
20214
+ glyph: "\u25CB",
20215
+ preview: partPreview(thinking[0]),
20216
+ tokens,
20217
+ estimated: thinking.some((p) => stepTokensFor(p, message).estimated),
20218
+ durationMs: thinkingMs,
20219
+ isError: false,
20220
+ isCropped: isCropped(ctx, message.id, thinking[0].id),
20221
+ warn: tokens >= WARN_TOKENS,
20222
+ label,
20223
+ inContext
20224
+ });
20225
+ }
20226
+ }
20227
+ out.push(...rows);
20019
20228
  }
20020
20229
  function shownExpanded(ctx, sessionID) {
20021
20230
  const flagged = ctx.expanded.has(sessionID);
@@ -20076,11 +20285,21 @@ function emitChildBranches(ctx, sessionID, anchorMessageID, depth, gutter, out)
20076
20285
  pushBranch(ctx, branch, depth, gutter, i === visible.length - 1, showHeaders, out);
20077
20286
  });
20078
20287
  }
20288
+ function forkAnchorOf(ctx, sessionID) {
20289
+ const childID = ctx.onPathChild.get(sessionID);
20290
+ return childID ? ctx.state.sessions[childID]?.anchorMessageID : undefined;
20291
+ }
20079
20292
  function walkSession(ctx, sessionID, messages, depth, gutter, turnStart, out) {
20080
20293
  const lastUserIndex = findLastUserIndex(messages, ctx.filter);
20081
20294
  const counter = { turn: turnStart };
20082
20295
  let inPluginCommand = false;
20296
+ const forkAnchor = forkAnchorOf(ctx, sessionID);
20297
+ let inContext = ctx.onPath.has(sessionID);
20298
+ let separatorDue = inContext && forkAnchor === "";
20299
+ if (separatorDue)
20300
+ inContext = false;
20083
20301
  messages.forEach((message, i) => {
20302
+ const before = out.length;
20084
20303
  if (message.role === "user") {
20085
20304
  inPluginCommand = hiddenPluginTurn(ctx.filter, message);
20086
20305
  if (!inPluginCommand) {
@@ -20106,14 +20325,23 @@ function walkSession(ctx, sessionID, messages, depth, gutter, turnStart, out) {
20106
20325
  isCurrent: sessionID === ctx.currentSessionID,
20107
20326
  isTip: i === lastUserIndex,
20108
20327
  isDecision,
20109
- isSummary
20328
+ isSummary,
20329
+ inContext
20110
20330
  });
20111
20331
  }
20112
20332
  }
20113
20333
  } else if (!inPluginCommand) {
20114
- emitAssistantRows(ctx, sessionID, message, depth, gutter, out);
20334
+ emitAssistantRows(ctx, sessionID, message, depth, gutter, inContext, out);
20115
20335
  }
20116
20336
  emitChildBranches(ctx, sessionID, message.id, depth, gutter, out);
20337
+ if (separatorDue && out.length > before) {
20338
+ out.splice(before, 0, { kind: "separator", id: `separator:${sessionID}`, depth, gutter, text: OFF_PATH_TEXT });
20339
+ separatorDue = false;
20340
+ }
20341
+ if (inContext && message.id === forkAnchor) {
20342
+ inContext = false;
20343
+ separatorDue = true;
20344
+ }
20117
20345
  });
20118
20346
  }
20119
20347
  function rowSearchFields(row) {
@@ -20124,6 +20352,8 @@ function rowSearchFields(row) {
20124
20352
  return row.label ? [row.preview, row.label] : [row.preview];
20125
20353
  case "branch":
20126
20354
  return row.model ? [row.name, row.model, row.status] : [row.name, row.status];
20355
+ case "separator":
20356
+ return [];
20127
20357
  }
20128
20358
  }
20129
20359
  function rowMatches(row, needle) {
@@ -20156,17 +20386,17 @@ function applySearch(rows, search) {
20156
20386
  return rows.filter((_, i) => keep[i]);
20157
20387
  }
20158
20388
  function computeTotalTokens(transcript) {
20159
- let lastIndex = -1;
20389
+ let lastIndex2 = -1;
20160
20390
  let lastInput = 0;
20161
20391
  transcript.messages.forEach((m, idx) => {
20162
20392
  if (m.role === "assistant" && typeof m.tokens?.input === "number") {
20163
- lastIndex = idx;
20393
+ lastIndex2 = idx;
20164
20394
  lastInput = m.tokens.input;
20165
20395
  }
20166
20396
  });
20167
20397
  let counted = 0;
20168
20398
  let guessed = 0;
20169
- for (let i = Math.max(lastIndex, 0);i < transcript.messages.length; i++) {
20399
+ for (let i = Math.max(lastIndex2, 0);i < transcript.messages.length; i++) {
20170
20400
  const m = transcript.messages[i];
20171
20401
  if (m.role === "user") {
20172
20402
  guessed += estimateTokens(userText(m));
@@ -20179,7 +20409,7 @@ function computeTotalTokens(transcript) {
20179
20409
  if (p.type === "tool" || typeof output2 !== "number")
20180
20410
  guessed += estimateTokens(partText2(p));
20181
20411
  }
20182
- return { tokens: lastInput + counted + guessed, estimated: lastIndex === -1 || guessed > 0 };
20412
+ return { tokens: lastInput + counted + guessed, estimated: lastIndex2 === -1 || guessed > 0 };
20183
20413
  }
20184
20414
  function currentChainOf(state, currentSessionID) {
20185
20415
  return [...ancestorChainOf(state, currentSessionID), currentSessionID];
@@ -20231,7 +20461,7 @@ function buildTreeView(o) {
20231
20461
  let currentRowId;
20232
20462
  for (let i = rows.length - 1;i >= 0; i--) {
20233
20463
  const r = rows[i];
20234
- if (r.kind !== "branch" && r.sessionID === o.currentSessionID) {
20464
+ if ((r.kind === "turn" || r.kind === "step") && r.sessionID === o.currentSessionID) {
20235
20465
  currentRowId = r.id;
20236
20466
  break;
20237
20467
  }
@@ -20323,111 +20553,141 @@ function spanOf(m) {
20323
20553
  const end = m.time.completed ?? m.parts.reduce((e, p) => Math.max(e, p.state?.time?.end ?? p.time?.end ?? 0), 0);
20324
20554
  return end > m.time.created ? end - m.time.created : 0;
20325
20555
  }
20326
- function buildLanes(transcript, mode) {
20327
- const columns = [];
20556
+ var EVENT_GLYPH = "\u25AC";
20557
+ function ctreeKindOf2(message) {
20558
+ for (const p of message.parts) {
20559
+ const ctree = p.metadata?.["ctree"];
20560
+ if (ctree?.kind)
20561
+ return ctree.kind;
20562
+ }
20563
+ return;
20564
+ }
20565
+ function isContextMessage(message) {
20566
+ return message.summary === true || ctreeKindOf2(message) === "summary";
20567
+ }
20568
+ function messageEvent(message, turn) {
20569
+ const ms = spanOf(message);
20570
+ return {
20571
+ lane: "input",
20572
+ kind: isContextMessage(message) ? "context" : "user",
20573
+ messageID: message.id,
20574
+ turn,
20575
+ startMs: message.time.created,
20576
+ ...ms > 0 ? { durationMs: ms } : {},
20577
+ tokens: message.parts.reduce((s, p) => s + estimateTokens(p.text ?? ""), 0)
20578
+ };
20579
+ }
20580
+ function partEvent(message, part, turn) {
20581
+ const t = part.state?.time ?? part.time;
20582
+ const base = {
20583
+ messageID: message.id,
20584
+ partID: part.id,
20585
+ turn,
20586
+ startMs: t?.start ?? message.time.created,
20587
+ ...t?.start !== undefined && t?.end !== undefined ? { durationMs: Math.max(0, t.end - t.start) } : {}
20588
+ };
20589
+ if (part.type === "tool")
20590
+ return { ...base, lane: "tools", kind: "tool", error: part.state?.status === "error", tokens: estimateTokens(part.state?.output ?? "") + estimateTokens(JSON.stringify(part.state?.input ?? "")) };
20591
+ return { ...base, lane: "model", kind: part.type === "reasoning" ? "reasoning" : "text", tokens: estimateTokens(part.text ?? "") };
20592
+ }
20593
+ function eventsOf(transcript) {
20594
+ const out = [];
20328
20595
  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;
20596
+ if (turn.user)
20597
+ out.push(messageEvent(turn.user, turn.index));
20352
20598
  for (const m of turn.assistants) {
20353
- output2 += m.tokens?.output ?? 0;
20354
- ms += spanOf(m);
20599
+ const context = isContextMessage(m);
20600
+ if (context)
20601
+ out.push(messageEvent(m, turn.index));
20355
20602
  for (const p of m.parts) {
20356
- if (p.type !== "tool")
20603
+ const kind = stepKind(p);
20604
+ if (kind === "other" || context && kind !== "tool")
20357
20605
  continue;
20358
- tool += estimateTokens(p.state?.output ?? "");
20359
- if (p.state?.status === "error")
20360
- toolError = true;
20606
+ out.push(partEvent(m, p, turn.index));
20361
20607
  }
20362
20608
  }
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
20609
  }
20365
- return { mode, columns };
20610
+ return out;
20366
20611
  }
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("");
20612
+ function gapBefore(events, i, mode) {
20613
+ return mode === "turns" && events[i].turn !== events[i - 1].turn ? 2 : 1;
20374
20614
  }
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;
20615
+ function firstFitting(events, mode, width) {
20616
+ let cells = 0;
20617
+ for (let i = events.length - 1;i >= 0; i--) {
20618
+ const next = cells === 0 ? 1 : cells + 1 + gapBefore(events, i + 1, mode);
20619
+ if (next > width)
20620
+ return i + 1;
20621
+ cells = next;
20389
20622
  }
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;
20623
+ return 0;
20394
20624
  }
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
- }
20625
+ function durationWidths(events, budget) {
20626
+ const total = events.reduce((s, e) => s + Math.max(0, e.durationMs ?? 0), 0);
20627
+ if (total <= 0)
20628
+ return events.map(() => 1);
20629
+ const widths = events.map((e) => Math.max(1, Math.round(Math.max(0, e.durationMs ?? 0) / total * budget)));
20630
+ let over = widths.reduce((s, v) => s + v, 0) - budget;
20631
+ while (over > 0) {
20632
+ let widest = -1;
20633
+ for (let i = 0;i < widths.length; i++)
20634
+ if ((widths[i] ?? 0) > 1 && (widest < 0 || (widths[i] ?? 0) > (widths[widest] ?? 0)))
20635
+ widest = i;
20636
+ if (widest < 0)
20637
+ break;
20638
+ widths[widest] = (widths[widest] ?? 1) - 1;
20639
+ over--;
20640
+ }
20641
+ return widths;
20642
+ }
20643
+ function buildEventStrip(transcript, mode, width) {
20644
+ const w = Math.max(0, Math.floor(width));
20645
+ const all = eventsOf(transcript);
20646
+ const truncatedLeft = firstFitting(all, mode, w);
20647
+ const events = all.slice(truncatedLeft);
20648
+ const gaps = events.map((_, i) => i === 0 ? 0 : gapBefore(events, i, mode));
20649
+ const widths = mode === "duration" ? durationWidths(events, w - gaps.reduce((s, g) => s + g, 0)) : events.map(() => 1);
20650
+ const blank = () => Array.from({ length: w }, () => null);
20651
+ const lanes = { input: blank(), model: blank(), tools: blank() };
20652
+ const empty = { input: true, model: true, tools: true };
20653
+ const spans = [];
20654
+ let cursor = 0;
20655
+ events.forEach((e, i) => {
20656
+ const start = cursor + (gaps[i] ?? 0);
20657
+ const end = Math.min(w, start + (widths[i] ?? 1));
20658
+ for (let c = start;c < end; c++)
20659
+ lanes[e.lane][c] = { lane: e.lane, eventIndex: i, glyph: EVENT_GLYPH, ...e.error ? { error: true } : {} };
20660
+ spans.push({ start, end });
20661
+ empty[e.lane] = false;
20662
+ cursor = start + (widths[i] ?? 1);
20411
20663
  });
20412
- return { input: input2, output: output2, tool, toolError, columnAt: (cell) => owner[Math.min(owner.length - 1, Math.max(0, cell))] ?? 0 };
20664
+ return { events, width: w, lanes, spans, empty, truncatedLeft };
20413
20665
  }
20414
- function columnFor(lanes, messageID, partID) {
20666
+ function stripIndexFor(strip, messageID, partID) {
20415
20667
  if (partID) {
20416
- const i2 = lanes.columns.findIndex((c) => c.partID === partID);
20417
- if (i2 >= 0)
20418
- return i2;
20668
+ const byPart = strip.events.findIndex((e) => e.partID === partID);
20669
+ if (byPart >= 0)
20670
+ return byPart;
20419
20671
  }
20420
- const i = lanes.columns.findIndex((c) => c.messageID === messageID || c.userMessageID === messageID);
20421
- return i >= 0 ? i : -1;
20672
+ return strip.events.findIndex((e) => e.messageID === messageID);
20422
20673
  }
20423
20674
 
20424
20675
  // src/core/consumers.ts
20676
+ var THINKING = "(thinking)";
20677
+ var THINKING_NOTE = "provider reasoning \xB7 not croppable";
20425
20678
  function consumers(transcript, opts = {}) {
20426
20679
  const acc = new Map;
20427
- const add = (source, kind, tokens) => {
20428
- const c = acc.get(source) ?? { source, kind, tokens: 0, count: 0, share: 0 };
20680
+ const add = (source, kind, tokens, message, part) => {
20681
+ const c = acc.get(source) ?? { source, kind, tokens: 0, count: 0, share: 0, entries: [] };
20429
20682
  c.tokens += tokens;
20430
20683
  c.count += 1;
20684
+ c.entries.push({
20685
+ messageID: message.id,
20686
+ partID: part.id,
20687
+ tokens,
20688
+ preview: partPreview(part),
20689
+ croppable: part.type === "tool" && part.state?.status === "completed"
20690
+ });
20431
20691
  acc.set(source, c);
20432
20692
  };
20433
20693
  for (const m of transcript.messages) {
@@ -20435,25 +20695,32 @@ function consumers(transcript, opts = {}) {
20435
20695
  if (opts.cropped?.has(p.id))
20436
20696
  continue;
20437
20697
  if (p.type === "tool")
20438
- add(p.tool ?? "tool", "tool", estimateTokens(p.state?.output ?? "") + estimateTokens(JSON.stringify(p.state?.input ?? "")));
20698
+ add(p.tool ?? "tool", "tool", estimateTokens(p.state?.output ?? "") + estimateTokens(JSON.stringify(p.state?.input ?? "")), m, p);
20439
20699
  else if (p.type === "text") {
20440
20700
  const kind = p.metadata?.["ctree"]?.kind;
20441
20701
  if (kind === "decision")
20442
- add("\u25C6 decisions", "decision", estimateTokens(p.text ?? ""));
20702
+ add("\u25C6 decisions", "decision", estimateTokens(p.text ?? ""), m, p);
20443
20703
  else if (kind === "summary")
20444
- add("\u25C7 branch summaries", "summary", estimateTokens(p.text ?? ""));
20704
+ add("\u25C7 branch summaries", "summary", estimateTokens(p.text ?? ""), m, p);
20445
20705
  else if (m.role === "user")
20446
- add("\u25CF user prompts", "user", estimateTokens(p.text ?? ""));
20706
+ add("\u25CF user prompts", "user", estimateTokens(p.text ?? ""), m, p);
20447
20707
  else if (m.summary)
20448
- add("\u25C7 compaction summaries", "summary", estimateTokens(p.text ?? ""));
20708
+ add("\u25C7 compaction summaries", "summary", estimateTokens(p.text ?? ""), m, p);
20449
20709
  else
20450
- add("\u25CB assistant text", "assistant", estimateTokens(p.text ?? ""));
20710
+ add("\u25CB assistant text", "assistant", estimateTokens(p.text ?? ""), m, p);
20451
20711
  } else if (p.type === "reasoning")
20452
- add("(thinking)", "reasoning", estimateTokens(p.text ?? ""));
20712
+ add(THINKING, "reasoning", estimateTokens(p.text ?? ""), m, p);
20453
20713
  }
20454
20714
  }
20455
20715
  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);
20716
+ const limit = opts.limit !== undefined && opts.limit > 0 ? opts.limit : undefined;
20717
+ return [...acc.values()].map((c) => ({
20718
+ ...c,
20719
+ share: c.tokens / total,
20720
+ ...limit === undefined ? {} : { shareOfWindow: c.tokens / limit },
20721
+ ...c.kind === "reasoning" ? { note: THINKING_NOTE } : {},
20722
+ entries: c.entries.sort((a, b) => b.tokens - a.tokens)
20723
+ })).sort((a, b) => b.tokens - a.tokens);
20457
20724
  }
20458
20725
  function bar(share, width) {
20459
20726
  const n = Math.round(share * width);
@@ -20461,8 +20728,8 @@ function bar(share, width) {
20461
20728
  }
20462
20729
 
20463
20730
  // src/tui/route.tsx
20464
- import fs4 from "fs";
20465
- import path3 from "path";
20731
+ import fs5 from "fs";
20732
+ import path4 from "path";
20466
20733
 
20467
20734
  // src/core/cropplan.ts
20468
20735
  function sha8(text) {
@@ -20692,7 +20959,14 @@ function textOf(row) {
20692
20959
  return plain(row.preview);
20693
20960
  return `assistant: ${plain(row.preview)}`;
20694
20961
  }
20962
+ function thoughtOf(row) {
20963
+ if (row.kind !== "step" || row.thinkingMs === undefined)
20964
+ return "";
20965
+ return ` \xB7 ${(row.thinkingMs / 1000).toFixed(row.thinkingMs < 1e4 ? 1 : 0)}s thought`;
20966
+ }
20695
20967
  function rowLine(row, width, here) {
20968
+ if (row.kind === "separator")
20969
+ return `${row.gutter}${row.text}`;
20696
20970
  const tokens = `${row.kind !== "branch" && row.estimated ? "~" : ""}${formatK(row.tokens)}`;
20697
20971
  const marker = here ? " \u2190 here" : "";
20698
20972
  let body;
@@ -20705,32 +20979,82 @@ function rowLine(row, width, here) {
20705
20979
  } else {
20706
20980
  const flags = row.kind === "step" ? `${row.label ? ` [${row.label}]` : ""}${row.isCropped ? " \u2702" : ""}${row.warn ? " \u26A0" : ""}${row.isError ? " \u2717" : ""}` : row.label ? ` [${row.label}]` : "";
20707
20981
  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}`;
20982
+ body = `${row.gutter}${glyphOf(row)} ${textOf(row)}${flags}${dur}${thoughtOf(row)}${marker}`;
20709
20983
  }
20710
20984
  return fitRow(body, tokens, width);
20711
20985
  }
20986
+ function segmentsOf(line, query, thought) {
20987
+ const ranges = [];
20988
+ if (query) {
20989
+ const at = line.toLowerCase().indexOf(query.toLowerCase());
20990
+ if (at >= 0)
20991
+ ranges.push({
20992
+ at,
20993
+ len: query.length,
20994
+ kind: "match"
20995
+ });
20996
+ }
20997
+ if (thought) {
20998
+ const at = line.lastIndexOf(thought);
20999
+ if (at >= 0)
21000
+ ranges.push({
21001
+ at,
21002
+ len: thought.length,
21003
+ kind: "dim"
21004
+ });
21005
+ }
21006
+ if (ranges.length === 0)
21007
+ return [{
21008
+ text: line,
21009
+ kind: "plain"
21010
+ }];
21011
+ const out = [];
21012
+ let cursor = 0;
21013
+ for (const r of ranges.sort((a, b) => a.at - b.at)) {
21014
+ if (r.at < cursor)
21015
+ continue;
21016
+ if (r.at > cursor)
21017
+ out.push({
21018
+ text: line.slice(cursor, r.at),
21019
+ kind: "plain"
21020
+ });
21021
+ out.push({
21022
+ text: line.slice(r.at, r.at + r.len),
21023
+ kind: r.kind
21024
+ });
21025
+ cursor = r.at + r.len;
21026
+ }
21027
+ if (cursor < line.length)
21028
+ out.push({
21029
+ text: line.slice(cursor),
21030
+ kind: "plain"
21031
+ });
21032
+ return out;
21033
+ }
20712
21034
  var DEFAULT_KEYS = {
20713
21035
  up: ["up", "k"],
20714
21036
  down: ["down", "j"],
20715
21037
  jump_up: ["shift+up", "shift+k"],
20716
21038
  jump_down: ["shift+down", "shift+j"],
20717
- first: ["g"],
21039
+ half_up: ["ctrl+u"],
21040
+ half_down: ["ctrl+d"],
21041
+ first: ["gg"],
20718
21042
  last: ["shift+g"],
20719
21043
  prev_branch: ["["],
20720
21044
  next_branch: ["]"],
20721
21045
  fold: ["left", "h"],
20722
21046
  unfold: ["right", "l"],
20723
- toggle: ["e"],
21047
+ toggle: ["tab", "e"],
20724
21048
  go: ["return"],
20725
21049
  branch: ["b"],
20726
21050
  crop: ["c"],
20727
21051
  crop_toggle_mode: ["t"],
20728
21052
  mark: ["space"],
20729
21053
  auto: ["a"],
20730
- undo: ["x"],
21054
+ undo: ["u", "x"],
20731
21055
  merge: ["m"],
20732
21056
  inspector: ["i"],
20733
- consumers: ["u"],
21057
+ consumers: ["s"],
20734
21058
  copy: ["y"],
20735
21059
  mode_duration: ["1"],
20736
21060
  mode_turns: ["2"],
@@ -20739,13 +21063,43 @@ var DEFAULT_KEYS = {
20739
21063
  decisions: ["shift+d"],
20740
21064
  export: ["shift+e"],
20741
21065
  label: ["shift+l"],
20742
- filter: ["f"],
21066
+ filter_pick: ["f"],
21067
+ filter_prev: ["shift+f"],
20743
21068
  search: ["/"],
21069
+ search_next: ["n"],
21070
+ search_prev: ["shift+n"],
20744
21071
  help: ["?", "shift+/"],
20745
21072
  back: ["q", "escape"]
20746
21073
  };
21074
+ var EMPTY_TRANSCRIPT = {
21075
+ sessionID: "",
21076
+ title: "",
21077
+ status: "available",
21078
+ messages: []
21079
+ };
20747
21080
  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"];
21081
+ var HELP = ["? help \xB7 ? or esc closes", "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"];
21082
+ var FILTERS = [{
21083
+ title: "default",
21084
+ value: "default",
21085
+ description: "user turns, assistant text, tool steps"
21086
+ }, {
21087
+ title: "no-tools",
21088
+ value: "no-tools",
21089
+ description: "hide \u2699 tool steps"
21090
+ }, {
21091
+ title: "user-only",
21092
+ value: "user-only",
21093
+ description: "\u25CF user turns only"
21094
+ }, {
21095
+ title: "labeled",
21096
+ value: "labeled",
21097
+ description: "labelled rows only"
21098
+ }, {
21099
+ title: "all",
21100
+ value: "all",
21101
+ description: "everything, thinking parts included"
21102
+ }];
20749
21103
  function bindingsFor(overrides) {
20750
21104
  const out = [];
20751
21105
  for (const [cmd, keys] of Object.entries(DEFAULT_KEYS))
@@ -20777,6 +21131,8 @@ function TreeRoute(props) {
20777
21131
  const [expanded, setExpanded] = createSignal2(new Set(api2.kv.get(`ctree.expanded.${sessionID}`, [])));
20778
21132
  const [filter, setFilter] = createSignal2(api2.kv.get("ctree.filter", "default"));
20779
21133
  const [search, setSearch] = createSignal2("");
21134
+ const [searchMode, setSearchMode] = createSignal2(false);
21135
+ let searchBefore = "";
20780
21136
  const [selected, setSelected] = createSignal2(0);
20781
21137
  const [others, setOthers] = createSignal2({});
20782
21138
  const [busy, setBusy] = createSignal2();
@@ -20786,7 +21142,9 @@ function TreeRoute(props) {
20786
21142
  const [lanesOn, setLanesOn] = createSignal2(api2.kv.get("ctree.lanesOn", false));
20787
21143
  const [inspector, setInspector] = createSignal2(api2.kv.get("ctree.inspector", false));
20788
21144
  const [consumerIndex, setConsumerIndex] = createSignal2(0);
21145
+ const [consumerOpen, setConsumerOpen] = createSignal2(new Set);
20789
21146
  const [decisionIndex, setDecisionIndex] = createSignal2(0);
21147
+ const [decisionScroll, setDecisionScroll] = createSignal2(0);
20790
21148
  const [marked, setMarked] = createSignal2(new Set);
20791
21149
  const state = createMemo(() => {
20792
21150
  tick();
@@ -20901,19 +21259,23 @@ function TreeRoute(props) {
20901
21259
  api2.renderer.on("resize", onResize);
20902
21260
  onCleanup(() => void api2.renderer.off("resize", onResize));
20903
21261
  const cols = () => size().cols;
20904
- const height = () => Math.max(8, size().rows - 8 - (lanesOn() && size().rows >= 12 ? 3 : 0));
21262
+ const helpHeight = () => panel() === "help" ? Math.min(HELP.length, Math.max(0, size().rows - 12)) : 0;
21263
+ const height = () => Math.max(4, size().rows - 8 - (lanesOn() && size().rows >= 12 ? 3 : 0) - helpHeight());
20905
21264
  const width = () => Math.max(60, cols() - 4);
21265
+ const overflow = () => view().rows.length > height() - 2;
21266
+ const rowsHeight = () => overflow() ? height() - 2 : height();
20906
21267
  const windowStart = createMemo(() => {
20907
- const h = height();
21268
+ const h = rowsHeight();
20908
21269
  const s = selected();
20909
21270
  const n = view().rows.length;
20910
- const start = Math.max(0, Math.min(s - Math.floor(h / 2), n - h));
20911
- return start;
21271
+ return Math.max(0, Math.min(s - Math.floor(h / 2), n - h));
20912
21272
  });
20913
- const visible = createMemo(() => view().rows.slice(windowStart(), windowStart() + height()));
21273
+ const visible = createMemo(() => view().rows.slice(windowStart(), windowStart() + rowsHeight()));
21274
+ const hiddenAbove = () => windowStart();
21275
+ const hiddenBelow = () => Math.max(0, view().rows.length - windowStart() - rowsHeight());
20914
21276
  const live = () => sessionID ? liveTranscript(api2, sessionID) : undefined;
20915
21277
  const currentMessageOf = (row) => {
20916
- if (row.kind === "branch")
21278
+ if (row.kind === "branch" || row.kind === "separator")
20917
21279
  return;
20918
21280
  if (row.sessionID === sessionID)
20919
21281
  return row.messageID;
@@ -20958,10 +21320,35 @@ function TreeRoute(props) {
20958
21320
  const list = cropMode() === "result" ? resultCands() : turnCands();
20959
21321
  return list.filter((c) => m.has(markKey(c)));
20960
21322
  });
21323
+ function modeForRow(row) {
21324
+ if (row.kind === "step" && resultCands().some((c) => c.partID === (currentPartOf(row) ?? row.partID)))
21325
+ return "result";
21326
+ if (row.kind !== "branch" && row.kind !== "separator" && turnCands().some((c) => c.anchorMessageID === currentMessageOf(row)))
21327
+ return "turn";
21328
+ return;
21329
+ }
21330
+ const armed = () => {
21331
+ const row = current();
21332
+ const c = row ? candidateOf(row) : undefined;
21333
+ if (!c)
21334
+ return false;
21335
+ const key = markKey(c);
21336
+ return marked().has(`${key}:warned`) && !marked().has(key);
21337
+ };
20961
21338
  function toggleMark() {
20962
21339
  const row = current();
20963
21340
  if (!row)
20964
21341
  return;
21342
+ if (!cropMode()) {
21343
+ const mode = modeForRow(row);
21344
+ if (!mode) {
21345
+ api2.ui.toast({
21346
+ message: "nothing croppable on this row \u2014 c opens crop mode"
21347
+ });
21348
+ return;
21349
+ }
21350
+ setCropMode(mode);
21351
+ }
20965
21352
  const c = candidateOf(row);
20966
21353
  debug("crop.mark", {
20967
21354
  row: row.id,
@@ -20993,6 +21380,13 @@ function TreeRoute(props) {
20993
21380
  next.add(key);
20994
21381
  setMarked(next);
20995
21382
  }
21383
+ async function leaveCropMode() {
21384
+ const n = selectedCandidates().length;
21385
+ 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."))
21386
+ return;
21387
+ setCropMode(undefined);
21388
+ setMarked(new Set);
21389
+ }
20996
21390
  function autoMarkAll() {
20997
21391
  if (cropMode() !== "result")
20998
21392
  return;
@@ -21070,69 +21464,54 @@ function TreeRoute(props) {
21070
21464
  },
21071
21465
  parts: m.parts
21072
21466
  }))));
21073
- const band = () => bandFor(contextSize().tokens);
21467
+ const band = () => bandFor(contextSize().tokens, contextLimit());
21074
21468
  const branchOfCurrent = () => sessionID ? state().sessions[sessionID] : undefined;
21075
21469
  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
21470
  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(() => {
21471
+ const strip = createMemo(() => buildEventStrip(live() ?? EMPTY_TRANSCRIPT, laneMode(), laneWidth()));
21472
+ const cursorEvent = createMemo(() => {
21104
21473
  const row = current();
21105
- if (!row || row.kind === "branch")
21474
+ if (!row || row.kind === "branch" || row.kind === "separator")
21106
21475
  return -1;
21107
21476
  const mid = currentMessageOf(row) ?? row.messageID;
21108
21477
  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);
21478
+ return stripIndexFor(strip(), mid, pid);
21111
21479
  });
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());
21480
+ const cellColor = (cell) => {
21481
+ if (cell.error)
21482
+ return t.error;
21483
+ const e = strip().events[cell.eventIndex];
21484
+ if (!e)
21485
+ return t.textMuted;
21486
+ if (e.lane === "tools")
21487
+ return t.warning;
21488
+ if (e.lane === "input")
21489
+ return e.kind === "user" ? t.success : t.textMuted;
21490
+ return e.kind === "reasoning" ? t.textMuted : t.accent;
21491
+ };
21492
+ const laneRuns = (lane) => {
21493
+ const cur = cursorEvent();
21122
21494
  const runs = [];
21123
- for (let i = 0;i < line.length; i++) {
21124
- const error61 = (mask[i] ?? 0) > 0;
21495
+ for (const cell of strip().lanes[lane]) {
21496
+ const sel = cell !== null && cell.eventIndex === cur;
21497
+ const color = cell === null ? t.textMuted : cellColor(cell);
21498
+ const fg = sel ? t.background : color;
21499
+ const bg = sel ? color : undefined;
21125
21500
  const last = runs[runs.length - 1];
21126
- if (last && last.error === error61)
21127
- last.text += line[i];
21501
+ if (last && last.fg === fg && last.bg === bg)
21502
+ last.text += cell?.glyph ?? " ";
21128
21503
  else
21129
21504
  runs.push({
21130
- text: line[i],
21131
- error: error61
21505
+ text: cell?.glyph ?? " ",
21506
+ fg,
21507
+ bg
21132
21508
  });
21133
21509
  }
21134
21510
  return runs;
21135
- });
21511
+ };
21512
+ const inputRuns = createMemo(() => laneRuns("input"));
21513
+ const modelRuns = createMemo(() => laneRuns("model"));
21514
+ const toolRuns = createMemo(() => laneRuns("tools"));
21136
21515
  const contextLimit = createMemo(() => sessionID ? modelContextLimit(api2, sessionID) : undefined);
21137
21516
  const laneRoom = () => height() >= 12 && panel() === "tree";
21138
21517
  const showLanes = () => laneRoom() && lanesOn() && userTurns() >= 3;
@@ -21154,7 +21533,7 @@ function TreeRoute(props) {
21154
21533
  const noBranchesLines = () => NO_BRANCHES.length + 2 <= rowWidth() ? [NO_BRANCHES] : NO_BRANCHES.split(/(?<=;) /);
21155
21534
  const inspectorLines = createMemo(() => {
21156
21535
  const row = current();
21157
- if (!row)
21536
+ if (!row || row.kind === "separator")
21158
21537
  return [];
21159
21538
  const w = inspectorWidth() - 3;
21160
21539
  const clip2 = (x) => x.length > w ? `${x.slice(0, w - 1)}\u2026` : x;
@@ -21201,19 +21580,37 @@ function TreeRoute(props) {
21201
21580
  const msg = tr?.messages.find((m) => m.id === row.messageID);
21202
21581
  const turn = view().rows.slice(0, view().indexById[row.id] + 1).filter((r) => r.kind === "turn").at(-1);
21203
21582
  if (row.kind === "turn") {
21204
- head(`${row.isDecision ? "\u25C6 decision" : row.isSummary ? "\u25C7 summary" : "\u25CF user"} \xB7 T${row.turn}`);
21583
+ const text = msg?.parts.map((p) => p.text ?? "").join(`
21584
+ `) ?? row.preview;
21585
+ if (row.isDecision) {
21586
+ head(`\u25C6 ${decisionSummary(text).title}`);
21587
+ kv("Tokens", `~${formatK(row.tokens)}`);
21588
+ const lines = renderDecision(text, w);
21589
+ for (const l of lines.slice(0, 16))
21590
+ out.push({
21591
+ fg: t.text,
21592
+ text: l
21593
+ });
21594
+ if (lines.length > 16)
21595
+ muted(`\u2026 ${lines.length - 16} more lines (y to copy)`);
21596
+ return out;
21597
+ }
21598
+ head(`${row.isSummary ? "\u25C7 summary" : "\u25CF user"} \xB7 T${row.turn}`);
21205
21599
  if (row.label)
21206
21600
  kv("Label", row.label);
21207
21601
  kv("Tokens", `~${formatK(row.tokens)}`);
21208
21602
  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);
21603
+ if (!row.inContext)
21604
+ muted("not in this branch's context");
21605
+ block("Text", text, 14);
21211
21606
  return out;
21212
21607
  }
21213
21608
  const part = msg?.parts.find((p) => p.id === row.partID);
21214
21609
  const stepNo = msg ? msg.parts.filter((p) => p.type === "tool" || p.type === "text").findIndex((p) => p.id === row.partID) + 1 : 0;
21215
21610
  head(`${row.glyph} ${part?.type === "tool" ? part.tool : row.glyph === "\u25C7" ? "compaction" : "assistant"} \xB7 T${turn?.kind === "turn" ? turn.turn : "?"} \xB7 step ${stepNo}`);
21216
21611
  kv("Hierarchy", `T${turn?.kind === "turn" ? turn.turn : "?"} \u203A assistant \u203A step ${stepNo}`);
21612
+ if (!row.inContext)
21613
+ muted("not in this branch's context");
21217
21614
  if (part?.type === "tool") {
21218
21615
  const st = part.state;
21219
21616
  const dur = st?.time?.start !== undefined && st?.time?.end !== undefined ? `${st.time.end - st.time.start} ms` : "?";
@@ -21223,25 +21620,66 @@ function TreeRoute(props) {
21223
21620
  block("Result", String(st?.output ?? ""), 10);
21224
21621
  kv("Timing", st?.time?.start ? `started ${new Date(st.time.start).toISOString().slice(11, 23)} \xB7 ${dur} \xB7 session ts` : "n/a");
21225
21622
  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");
21623
+ 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
21624
  } else {
21228
21625
  kv("Tokens", `~${formatK(row.tokens)}`);
21229
21626
  if (row.durationMs !== undefined)
21230
21627
  kv("Duration", `${(row.durationMs / 1000).toFixed(1)} s`);
21628
+ if (row.thinkingMs !== undefined)
21629
+ kv("Thought", `${(row.thinkingMs / 1000).toFixed(1)} s`);
21231
21630
  block("Text", part?.text ?? row.preview, 14);
21232
21631
  }
21233
21632
  return out;
21234
21633
  });
21235
21634
  const consumerRows = createMemo(() => live() ? consumers(live(), {
21236
- cropped: alreadyCropped()
21635
+ cropped: alreadyCropped(),
21636
+ limit: contextLimit()
21237
21637
  }) : []);
21638
+ const consumerLines = createMemo(() => consumerRows().flatMap((c) => [{
21639
+ bucket: c
21640
+ }, ...consumerOpen().has(c.source) ? c.entries.map((e) => ({
21641
+ bucket: c,
21642
+ entry: e
21643
+ })) : []]));
21644
+ const consumerLine = () => consumerLines()[Math.min(consumerIndex(), consumerLines().length - 1)];
21645
+ const consumerMax = () => Math.max(1, ...consumerRows().map((c) => c.tokens));
21646
+ function toggleConsumer(open2) {
21647
+ const line = consumerLine();
21648
+ if (!line)
21649
+ return;
21650
+ const next = new Set(consumerOpen());
21651
+ if (open2)
21652
+ next.add(line.bucket.source);
21653
+ else
21654
+ next.delete(line.bucket.source);
21655
+ setConsumerOpen(next);
21656
+ }
21657
+ function markConsumerEntry() {
21658
+ const line = consumerLine();
21659
+ if (!line)
21660
+ return;
21661
+ if (!line.entry) {
21662
+ toggleConsumer(true);
21663
+ return;
21664
+ }
21665
+ const cand = line.entry.croppable ? resultCands().find((r) => r.partID === line.entry?.partID) : undefined;
21666
+ if (!cand)
21667
+ return;
21668
+ setCropMode("result");
21669
+ const next = new Set(marked());
21670
+ if (next.has(cand.partID))
21671
+ next.delete(cand.partID);
21672
+ else
21673
+ next.add(cand.partID);
21674
+ setMarked(next);
21675
+ }
21238
21676
  function cropConsumer() {
21239
- const c = consumerRows()[consumerIndex()];
21677
+ const c = consumerLine()?.bucket;
21240
21678
  setPanel("tree");
21241
21679
  if (!c || c.kind !== "tool") {
21242
21680
  setCropMode("result");
21243
21681
  api2.ui.toast({
21244
- message: c ? `${c.source} is not a tool result; mark rows by hand` : "nothing to crop"
21682
+ message: c ? c.note ?? `${c.source} is not a tool result; mark rows by hand` : "nothing to crop"
21245
21683
  });
21246
21684
  return;
21247
21685
  }
@@ -21254,20 +21692,18 @@ function TreeRoute(props) {
21254
21692
  }
21255
21693
  function copySelected() {
21256
21694
  const row = current();
21257
- if (!row || row.kind === "branch")
21695
+ if (!row || row.kind === "branch" || row.kind === "separator")
21258
21696
  return;
21259
21697
  const tr = row.sessionID === sessionID ? live() : others()[row.sessionID];
21260
21698
  const msg = tr?.messages.find((m) => m.id === row.messageID);
21261
21699
  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
21700
  `) ?? "";
21263
- const file2 = path3.join(directory, ".opencode", "context-tree", "last-copy.txt");
21264
21701
  try {
21265
- fs4.mkdirSync(path3.dirname(file2), {
21266
- recursive: true
21267
- });
21268
- fs4.writeFileSync(file2, text);
21702
+ const {
21703
+ hint
21704
+ } = copyText(api2, text, directory);
21269
21705
  api2.ui.toast({
21270
- message: `saved ${text.length} chars \u2192 .opencode/context-tree/last-copy.txt`
21706
+ message: `copied ${text.length} chars \u2192 ${hint}`
21271
21707
  });
21272
21708
  } catch (e) {
21273
21709
  api2.ui.toast({
@@ -21393,6 +21829,10 @@ function TreeRoute(props) {
21393
21829
  bump();
21394
21830
  }
21395
21831
  }
21832
+ const sessionLabel = (id) => {
21833
+ const name = state().sessions[id]?.name;
21834
+ return name ? `\u2387 ${name}` : others()[id]?.title ?? api2.state.session.get(id)?.title ?? id;
21835
+ };
21396
21836
  async function jump() {
21397
21837
  const row = current();
21398
21838
  if (!row || !sessionID)
@@ -21415,16 +21855,19 @@ function TreeRoute(props) {
21415
21855
  });
21416
21856
  return;
21417
21857
  }
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
- }
21858
+ const from = sessionLabel(plan.sessionID);
21859
+ 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.`);
21860
+ if (!ok)
21861
+ return;
21423
21862
  const summary = await askSummary();
21424
- await executeJump(ctx, plan, {
21863
+ const target2 = await executeJump(ctx, plan, {
21425
21864
  currentSessionID: sessionID,
21426
21865
  summary
21427
21866
  });
21867
+ if (target2)
21868
+ api2.ui.toast({
21869
+ message: `moved to ${sessionLabel(target2)} \xB7 ${UNDO_KEY} undoes it`
21870
+ });
21428
21871
  });
21429
21872
  }
21430
21873
  async function branch() {
@@ -21477,7 +21920,7 @@ function TreeRoute(props) {
21477
21920
  }
21478
21921
  async function label() {
21479
21922
  const row = current();
21480
- if (!row || row.kind === "branch")
21923
+ if (!row || row.kind === "branch" || row.kind === "separator")
21481
21924
  return;
21482
21925
  const st = state();
21483
21926
  const existing = st.labels[row.messageID]?.label;
@@ -21495,7 +21938,7 @@ function TreeRoute(props) {
21495
21938
  const row = current();
21496
21939
  if (!row)
21497
21940
  return;
21498
- const target2 = row.kind === "branch" ? row.sessionID : row.depth > 0 ? row.sessionID : undefined;
21941
+ const target2 = row.kind === "separator" ? undefined : row.kind === "branch" || row.depth > 0 ? row.sessionID : undefined;
21499
21942
  if (!target2)
21500
21943
  return;
21501
21944
  const shown = row.kind === "branch" ? row.expanded : true;
@@ -21529,14 +21972,18 @@ function TreeRoute(props) {
21529
21972
  return;
21530
21973
  }
21531
21974
  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
21975
+ const parent = others()[b.parentSessionID];
21976
+ const target2 = mergeTargetOf(b.parentSessionID === state().root ? TRUNK_LABEL : state().sessions[b.parentSessionID]?.name ?? TRUNK_LABEL, parent?.messages ?? []);
21977
+ const turns = ownTurnCount(live()?.messages ?? [], {
21978
+ messageID: b.anchorMessageID,
21979
+ parentMessageIDs: parent?.messages.map((m) => m.id) ?? []
21980
+ });
21981
+ const mode = await select(mergeDialogTitle(b.name ?? "branch", target2), mergeDialogOptions({
21982
+ siblings,
21983
+ turns
21534
21984
  }));
21535
21985
  if (!mode)
21536
21986
  return;
21537
- let note;
21538
- if (mode === "discard")
21539
- note = await prompt("Why? (optional note on the close marker)", "dead end") ?? undefined;
21540
21987
  const inApp = !hasEditor() ? async (draft) => {
21541
21988
  const ok = await confirm("Accept the drafted record as-is?", `${draft.slice(0, 400)}${draft.length > 400 ? "\u2026" : ""}
21542
21989
 
@@ -21549,7 +21996,6 @@ ${MERGE_TRUST}
21549
21996
  await mergeBranch(ctx, {
21550
21997
  sessionID,
21551
21998
  mode,
21552
- note,
21553
21999
  confirm: inApp
21554
22000
  });
21555
22001
  });
@@ -21561,9 +22007,9 @@ ${MERGE_TRUST}
21561
22007
  sessionID: d.sessionID,
21562
22008
  at: d.recordedAt
21563
22009
  }));
21564
- const file2 = path3.join(directory, "ctree-decisions.md");
22010
+ const file2 = path4.join(directory, "ctree-decisions.md");
21565
22011
  try {
21566
- fs4.writeFileSync(file2, exportDecisions(records));
22012
+ fs5.writeFileSync(file2, exportDecisions(records));
21567
22013
  api2.ui.toast({
21568
22014
  variant: "success",
21569
22015
  message: `wrote ${records.length} record${records.length === 1 ? "" : "s"} \u2192 ${file2}`
@@ -21579,7 +22025,7 @@ ${MERGE_TRUST}
21579
22025
  const d = decisions()[decisionIndex()];
21580
22026
  if (!d)
21581
22027
  return;
21582
- const idx = view().rows.findIndex((r) => r.kind !== "branch" && r.messageID === d.messageID);
22028
+ const idx = view().rows.findIndex((r) => (r.kind === "turn" || r.kind === "step") && r.messageID === d.messageID);
21583
22029
  setPanel("tree");
21584
22030
  if (idx >= 0)
21585
22031
  setSelected(idx);
@@ -21588,125 +22034,251 @@ ${MERGE_TRUST}
21588
22034
  message: "that record lives in another session"
21589
22035
  });
21590
22036
  }
22037
+ const treePanel = () => panel() === "tree";
22038
+ const inCrop = () => cropMode() !== undefined;
22039
+ const treeIdle = () => treePanel() && !inCrop();
22040
+ const listPanel = () => treePanel() || panel() === "consumers";
22041
+ function setFilterTo(next) {
22042
+ setFilter(next);
22043
+ api2.kv.set("ctree.filter", next);
22044
+ }
22045
+ async function pickFilter() {
22046
+ const next = await select("Filter rows", FILTERS.map((f) => ({
22047
+ title: `${f.value === filter() ? "\u25CF" : " "} ${f.title}`,
22048
+ value: f.value,
22049
+ description: f.description
22050
+ })));
22051
+ if (next)
22052
+ setFilterTo(next);
22053
+ }
22054
+ function moveIndex(delta) {
22055
+ if (panel() === "decisions") {
22056
+ setDecisionIndex((i) => Math.min(Math.max(0, decisions().length - 1), Math.max(0, i + delta)));
22057
+ setDecisionScroll(0);
22058
+ return;
22059
+ }
22060
+ if (panel() === "consumers") {
22061
+ setConsumerIndex((i) => Math.min(Math.max(0, consumerLines().length - 1), Math.max(0, i + delta)));
22062
+ return;
22063
+ }
22064
+ setSelected((i) => moveSelection(view().rows, i, delta));
22065
+ }
22066
+ function halfPage(dir) {
22067
+ const half = Math.max(1, Math.floor(height() / 2));
22068
+ if (panel() === "decisions")
22069
+ setDecisionScroll((s) => Math.max(0, s + dir * half));
22070
+ else
22071
+ moveIndex(dir * half);
22072
+ }
22073
+ function gotoEdge(dir) {
22074
+ if (panel() === "decisions") {
22075
+ setDecisionIndex(dir === -1 ? 0 : Math.max(0, decisions().length - 1));
22076
+ setDecisionScroll(0);
22077
+ return;
22078
+ }
22079
+ if (panel() === "consumers") {
22080
+ setConsumerIndex(dir === -1 ? 0 : Math.max(0, consumerLines().length - 1));
22081
+ return;
22082
+ }
22083
+ const i = dir === -1 ? firstIndex(view().rows) : lastIndex(view().rows);
22084
+ if (i >= 0)
22085
+ setSelected(i);
22086
+ }
22087
+ const matchIn = (line) => {
22088
+ const q = search().trim().toLowerCase();
22089
+ return q ? line.toLowerCase().indexOf(q) : -1;
22090
+ };
22091
+ function moveMatch(dir) {
22092
+ const rows = view().rows;
22093
+ const q = search().trim();
22094
+ if (rows.length === 0 || !q)
22095
+ return;
22096
+ for (let step = 1;step <= rows.length; step++) {
22097
+ const i = ((selected() + dir * step) % rows.length + rows.length) % rows.length;
22098
+ if (rows[i].kind !== "separator" && matchIn(rowLine(rows[i], rowWidth(), false)) >= 0) {
22099
+ setSelected(i);
22100
+ return;
22101
+ }
22102
+ }
22103
+ api2.ui.toast({
22104
+ message: `no other row matches "${q}"`
22105
+ });
22106
+ }
22107
+ function enterSearch() {
22108
+ searchBefore = search();
22109
+ setSearchMode(true);
22110
+ }
22111
+ function exitSearch(commit) {
22112
+ if (!commit)
22113
+ setSearch(searchBefore);
22114
+ setSearchMode(false);
22115
+ }
22116
+ const stopTyping = api2.keymap.intercept("key", (input2) => {
22117
+ if (!searchMode())
22118
+ return;
22119
+ const ev = input2.event;
22120
+ if (ev.eventType === "release" || ev.ctrl || ev.meta)
22121
+ return;
22122
+ const take = () => input2.consume({
22123
+ preventDefault: true,
22124
+ stopPropagation: true
22125
+ });
22126
+ if (ev.name === "escape")
22127
+ return void (exitSearch(false), take());
22128
+ if (ev.name === "return" || ev.name === "enter")
22129
+ return void (exitSearch(true), take());
22130
+ if (ev.name === "backspace")
22131
+ return void (setSearch((q) => q.slice(0, -1)), take());
22132
+ const char = ev.sequence?.length === 1 && ev.sequence >= " " && ev.sequence !== "\x7F" ? ev.sequence : ev.name.length === 1 ? ev.name : undefined;
22133
+ if (char === undefined)
22134
+ return;
22135
+ setSearch((q) => q + char);
22136
+ take();
22137
+ });
22138
+ onCleanup(() => stopTyping());
21591
22139
  const off = api2.keymap.registerLayer({
21592
22140
  mode: "base",
21593
22141
  commands: [{
21594
22142
  name: "ctree.up",
21595
22143
  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))
22144
+ run: () => moveIndex(-1)
21597
22145
  }, {
21598
22146
  name: "ctree.down",
21599
22147
  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))
22148
+ run: () => moveIndex(1)
21601
22149
  }, {
21602
22150
  name: "ctree.jump_up",
21603
22151
  hidden: true,
21604
- run: () => setSelected((i) => moveSelection(view().rows, i, -20))
22152
+ enabled: treePanel,
22153
+ run: () => moveIndex(-20)
21605
22154
  }, {
21606
22155
  name: "ctree.jump_down",
21607
22156
  hidden: true,
21608
- run: () => setSelected((i) => moveSelection(view().rows, i, 20))
22157
+ enabled: treePanel,
22158
+ run: () => moveIndex(20)
22159
+ }, {
22160
+ name: "ctree.half_up",
22161
+ hidden: true,
22162
+ run: () => halfPage(-1)
22163
+ }, {
22164
+ name: "ctree.half_down",
22165
+ hidden: true,
22166
+ run: () => halfPage(1)
21609
22167
  }, {
21610
22168
  name: "ctree.first",
21611
22169
  hidden: true,
21612
- run: () => setSelected(0)
22170
+ run: () => gotoEdge(-1)
21613
22171
  }, {
21614
22172
  name: "ctree.last",
21615
22173
  hidden: true,
21616
- run: () => setSelected(Math.max(0, view().rows.length - 1))
22174
+ run: () => gotoEdge(1)
21617
22175
  }, {
21618
22176
  name: "ctree.prev_branch",
21619
22177
  hidden: true,
22178
+ enabled: treePanel,
21620
22179
  run: () => setSelected((i) => nextBranchIndex(view().rows, i, -1))
21621
22180
  }, {
21622
22181
  name: "ctree.next_branch",
21623
22182
  hidden: true,
22183
+ enabled: treePanel,
21624
22184
  run: () => setSelected((i) => nextBranchIndex(view().rows, i, 1))
21625
22185
  }, {
21626
22186
  name: "ctree.fold",
21627
22187
  hidden: true,
21628
- run: () => foldOrUnfold(false)
22188
+ enabled: listPanel,
22189
+ run: () => panel() === "consumers" ? toggleConsumer(false) : foldOrUnfold(false)
21629
22190
  }, {
21630
22191
  name: "ctree.unfold",
21631
22192
  hidden: true,
21632
- run: () => foldOrUnfold(true)
22193
+ enabled: listPanel,
22194
+ run: () => panel() === "consumers" ? toggleConsumer(true) : foldOrUnfold(true)
21633
22195
  }, {
21634
22196
  name: "ctree.toggle",
21635
22197
  hidden: true,
22198
+ enabled: treePanel,
21636
22199
  run: () => foldOrUnfold(!(current()?.kind === "branch" && current().expanded))
21637
22200
  }, {
21638
22201
  name: "ctree.go",
21639
22202
  hidden: true,
21640
- run: () => void (panel() === "decisions" ? jumpToDecision() : panel() === "consumers" ? cropConsumer() : cropMode() ? applyMarked() : jump())
22203
+ run: () => void (panel() === "decisions" ? jumpToDecision() : panel() === "consumers" ? toggleConsumer(!consumerOpen().has(consumerLine()?.bucket.source ?? "")) : cropMode() ? applyMarked() : jump())
21641
22204
  }, {
21642
22205
  name: "ctree.branch",
21643
22206
  hidden: true,
22207
+ enabled: treeIdle,
21644
22208
  run: () => void branch()
21645
22209
  }, {
21646
22210
  name: "ctree.label",
21647
22211
  hidden: true,
22212
+ enabled: treeIdle,
21648
22213
  run: () => void label()
21649
22214
  }, {
21650
- name: "ctree.filter",
22215
+ name: "ctree.filter_pick",
21651
22216
  hidden: true,
21652
- run: () => {
21653
- const next = cycleFilter(filter());
21654
- setFilter(next);
21655
- api2.kv.set("ctree.filter", next);
21656
- }
22217
+ enabled: () => !inCrop(),
22218
+ run: () => void pickFilter()
22219
+ }, {
22220
+ name: "ctree.filter_prev",
22221
+ hidden: true,
22222
+ enabled: () => !inCrop(),
22223
+ run: () => setFilterTo(FILTERS[(FILTERS.findIndex((f) => f.value === filter()) - 1 + FILTERS.length) % FILTERS.length].value)
21657
22224
  }, {
21658
22225
  name: "ctree.search",
21659
22226
  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
- })
22227
+ enabled: treeIdle,
22228
+ run: () => enterSearch()
22229
+ }, {
22230
+ name: "ctree.search_next",
22231
+ hidden: true,
22232
+ enabled: treePanel,
22233
+ run: () => moveMatch(1)
22234
+ }, {
22235
+ name: "ctree.search_prev",
22236
+ hidden: true,
22237
+ enabled: treePanel,
22238
+ run: () => moveMatch(-1)
21664
22239
  }, {
21665
22240
  name: "ctree.crop",
21666
22241
  hidden: true,
22242
+ enabled: listPanel,
21667
22243
  run: () => {
21668
22244
  if (panel() === "consumers") {
21669
22245
  cropConsumer();
21670
22246
  return;
21671
22247
  }
21672
- if (panel() !== "tree")
21673
- return;
21674
- if (cropMode()) {
21675
- setCropMode(undefined);
21676
- setMarked(new Set);
21677
- } else
22248
+ if (cropMode())
22249
+ leaveCropMode();
22250
+ else
21678
22251
  setCropMode("result");
21679
22252
  }
21680
22253
  }, {
21681
22254
  name: "ctree.crop_toggle_mode",
21682
22255
  hidden: true,
21683
- run: () => {
21684
- if (!cropMode())
21685
- return;
21686
- setCropMode(cropMode() === "result" ? "turn" : "result");
21687
- setMarked(new Set);
21688
- }
22256
+ enabled: inCrop,
22257
+ run: () => setCropMode(cropMode() === "result" ? "turn" : "result")
21689
22258
  }, {
21690
22259
  name: "ctree.mark",
21691
22260
  hidden: true,
21692
- enabled: () => Boolean(cropMode()),
21693
- run: () => toggleMark()
22261
+ enabled: listPanel,
22262
+ run: () => panel() === "consumers" ? markConsumerEntry() : toggleMark()
21694
22263
  }, {
21695
22264
  name: "ctree.auto",
21696
22265
  hidden: true,
21697
- enabled: () => Boolean(cropMode()),
22266
+ enabled: inCrop,
21698
22267
  run: () => autoMarkAll()
21699
22268
  }, {
21700
22269
  name: "ctree.undo",
21701
22270
  hidden: true,
22271
+ enabled: treeIdle,
21702
22272
  run: () => void undo()
21703
22273
  }, {
21704
22274
  name: "ctree.merge",
21705
22275
  hidden: true,
22276
+ enabled: treeIdle,
21706
22277
  run: () => void merge2()
21707
22278
  }, {
21708
22279
  name: "ctree.inspector",
21709
22280
  hidden: true,
22281
+ enabled: () => !inCrop(),
21710
22282
  run: () => {
21711
22283
  setInspector(!inspector());
21712
22284
  api2.kv.set("ctree.inspector", inspector());
@@ -21714,26 +22286,32 @@ ${MERGE_TRUST}
21714
22286
  }, {
21715
22287
  name: "ctree.consumers",
21716
22288
  hidden: true,
22289
+ enabled: () => !inCrop(),
21717
22290
  run: () => setPanel(panel() === "consumers" ? "tree" : "consumers")
21718
22291
  }, {
21719
22292
  name: "ctree.copy",
21720
22293
  hidden: true,
22294
+ enabled: treeIdle,
21721
22295
  run: () => copySelected()
21722
22296
  }, {
21723
22297
  name: "ctree.mode_duration",
21724
22298
  hidden: true,
22299
+ enabled: treePanel,
21725
22300
  run: () => setLane("duration")
21726
22301
  }, {
21727
22302
  name: "ctree.mode_turns",
21728
22303
  hidden: true,
22304
+ enabled: treePanel,
21729
22305
  run: () => setLane("turns")
21730
22306
  }, {
21731
22307
  name: "ctree.mode_calls",
21732
22308
  hidden: true,
22309
+ enabled: treePanel,
21733
22310
  run: () => setLane("calls")
21734
22311
  }, {
21735
22312
  name: "ctree.lanes_off",
21736
22313
  hidden: true,
22314
+ enabled: treePanel,
21737
22315
  run: () => {
21738
22316
  setLanesOn(false);
21739
22317
  api2.kv.set("ctree.lanesOn", false);
@@ -21741,6 +22319,7 @@ ${MERGE_TRUST}
21741
22319
  }, {
21742
22320
  name: "ctree.decisions",
21743
22321
  hidden: true,
22322
+ enabled: () => !inCrop(),
21744
22323
  run: () => setPanel(panel() === "decisions" ? "tree" : "decisions")
21745
22324
  }, {
21746
22325
  name: "ctree.export",
@@ -21760,8 +22339,11 @@ ${MERGE_TRUST}
21760
22339
  return;
21761
22340
  }
21762
22341
  if (cropMode()) {
21763
- setCropMode(undefined);
21764
- setMarked(new Set);
22342
+ leaveCropMode();
22343
+ return;
22344
+ }
22345
+ if (search()) {
22346
+ setSearch("");
21765
22347
  return;
21766
22348
  }
21767
22349
  back();
@@ -21775,18 +22357,64 @@ ${MERGE_TRUST}
21775
22357
  const root = state().root;
21776
22358
  return (root && root !== sessionID ? others()[root]?.title : undefined) ?? sessionTitle();
21777
22359
  };
21778
- const where = () => {
22360
+ const modeTag = () => cropMode() ? " \xB7 crop mode" : searchMode() ? " \xB7 search" : "";
22361
+ const headLine = () => {
21779
22362
  const b = branchOfCurrent();
21780
- if (!b)
21781
- return "trunk";
21782
- return `\u2387 ${b.name ?? sessionTitle()} (${b.status}${b.model ? ` \xB7 ${b.model.split("/").pop()}` : ""})`;
22363
+ const lead = "\u250C Context tree \xB7 ";
22364
+ const where = b ? `\u2387 ${clip(b.name ?? sessionTitle(), 28)}${b.status === "open" ? "" : ` (${b.status})`} \u2190 ` : "";
22365
+ const tail = b ? "" : " \xB7 trunk";
22366
+ const room = cols() - 4 - formatContext(contextSize(), contextLimit()).length - modeTag().length - lead.length - where.length - tail.length - 3;
22367
+ return `${lead}${where}${clip(title(), Math.max(8, room))}${tail}${modeTag()} `;
22368
+ };
22369
+ const statusLine = () => {
22370
+ const n = view().rows.length;
22371
+ const pos = `${n ? Math.min(selected() + 1, n) : 0}/${n}`;
22372
+ if (cropMode()) {
22373
+ const a = armed();
22374
+ 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" : ""}`;
22375
+ }
22376
+ if (searchMode())
22377
+ return `search: ${search()}\u258F \xB7 ${pos} rows \xB7 \u23CE keeps it \xB7 esc clears`;
22378
+ return `filter: ${filter()}${search() ? ` search: "${search()}"` : ""}${busy() ? ` \u2026 ${busy()}` : ""} ${pos} rows`;
22379
+ };
22380
+ const goVerb = () => {
22381
+ const row = current();
22382
+ if (!row)
22383
+ return "\u23CE go";
22384
+ if (row.kind === "branch")
22385
+ return row.isCurrent ? "\u23CE you are here" : `\u23CE switch to \u2387 ${clip(row.name, 20)}`;
22386
+ if (row.kind === "separator")
22387
+ return "\u23CE go";
22388
+ if (row.id === view().currentRowId)
22389
+ return "\u23CE you are here";
22390
+ return row.kind === "turn" ? "\u23CE fork & prefill this turn" : "\u23CE fork after this step";
22391
+ };
22392
+ const footer = () => {
22393
+ if (cropMode())
22394
+ return "space mark a auto t result\u21C4turn \u23CE apply esc leave";
22395
+ if (panel() === "decisions")
22396
+ return "\u23CE jump to record E export q back";
22397
+ if (panel() === "consumers")
22398
+ return "\u23CE expand space mark c crop q back";
22399
+ if (panel() === "help")
22400
+ return "esc/q back";
22401
+ return `${goVerb()} b branch m merge c crop ${UNDO_KEY} undo s consumers ? help q back`;
22402
+ };
22403
+ const showsTree = () => panel() === "tree" || panel() === "help";
22404
+ const emptyText = () => {
22405
+ const q = search().trim();
22406
+ if (q)
22407
+ return `no rows match "${q}" \xB7 esc clears`;
22408
+ if (filter() !== "default")
22409
+ return `no rows match filter: ${filter()} \xB7 f changes it`;
22410
+ return "(no messages yet \u2014 chat first, then open the tree)";
21783
22411
  };
21784
22412
  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 `);
22413
+ 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
22414
  _$insertNode(_el$, _el$2);
21787
- _$insertNode(_el$, _el$17);
21788
- _$insertNode(_el$, _el$31);
21789
- _$insertNode(_el$, _el$34);
22415
+ _$insertNode(_el$, _el$16);
22416
+ _$insertNode(_el$, _el$28);
22417
+ _$insertNode(_el$, _el$35);
21790
22418
  _$setProp(_el$, "flexDirection", "column");
21791
22419
  _$setProp(_el$, "padding", 1);
21792
22420
  _$setProp(_el$, "width", "100%");
@@ -21794,7 +22422,7 @@ ${MERGE_TRUST}
21794
22422
  _$insertNode(_el$2, _el$3);
21795
22423
  _$insertNode(_el$2, _el$4);
21796
22424
  _$setProp(_el$2, "flexDirection", "row");
21797
- _$insert(_el$3, () => `\u250C Context tree \xB7 ${title()} \xB7 ${where()} `);
22425
+ _$insert(_el$3, headLine);
21798
22426
  _$insert(_el$4, () => formatContext(contextSize(), contextLimit()));
21799
22427
  _$insert(_el$, _$createComponent(Show, {
21800
22428
  get when() {
@@ -21802,16 +22430,55 @@ ${MERGE_TRUST}
21802
22430
  },
21803
22431
  get children() {
21804
22432
  return [(() => {
21805
- var _el$5 = _$createElement("text"), _el$6 = _$createTextNode(`\u2502 Input `), _el$7 = _$createTextNode(` `), _el$8 = _$createTextNode(` \xB7 `), _el$9 = _$createTextNode(` \xB7 `);
22433
+ var _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$7 = _$createElement("text");
21806
22434
  _$insertNode(_el$5, _el$6);
21807
22435
  _$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));
22436
+ _$setProp(_el$5, "flexDirection", "row");
22437
+ _$insert(_el$6, () => `\u2502 Input ${strip().truncatedLeft > 0 ? `\u2026${strip().truncatedLeft}` : ""}`);
22438
+ _$insert(_el$5, _$createComponent(Show, {
22439
+ get when() {
22440
+ return !strip().empty.input;
22441
+ },
22442
+ get fallback() {
22443
+ return (() => {
22444
+ var _el$37 = _$createElement("text");
22445
+ _$insert(_el$37, () => "no input".padEnd(laneWidth()));
22446
+ _$effect((_$p) => _$setProp(_el$37, "fg", t.textMuted, _$p));
22447
+ return _el$37;
22448
+ })();
22449
+ },
22450
+ get children() {
22451
+ return _$createComponent(For, {
22452
+ get each() {
22453
+ return inputRuns();
22454
+ },
22455
+ children: (r) => (() => {
22456
+ var _el$38 = _$createElement("text");
22457
+ _$insert(_el$38, () => r.text);
22458
+ _$effect((_p$) => {
22459
+ var { fg: _v$0, bg: _v$1 } = r;
22460
+ _v$0 !== _p$.e && (_p$.e = _$setProp(_el$38, "fg", _v$0, _p$.e));
22461
+ _v$1 !== _p$.t && (_p$.t = _$setProp(_el$38, "bg", _v$1, _p$.t));
22462
+ return _p$;
22463
+ }, {
22464
+ e: undefined,
22465
+ t: undefined
22466
+ });
22467
+ return _el$38;
22468
+ })()
22469
+ });
22470
+ }
22471
+ }), _el$7);
22472
+ _$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`);
22473
+ _$effect((_p$) => {
22474
+ var { textMuted: _v$, textMuted: _v$2 } = t;
22475
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$, _p$.e));
22476
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "fg", _v$2, _p$.t));
22477
+ return _p$;
22478
+ }, {
22479
+ e: undefined,
22480
+ t: undefined
22481
+ });
21815
22482
  return _el$5;
21816
22483
  })(), _$createComponent(Show, {
21817
22484
  get when() {
@@ -21819,92 +22486,136 @@ ${MERGE_TRUST}
21819
22486
  },
21820
22487
  get children() {
21821
22488
  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;
22489
+ var _el$8 = _$createElement("box"), _el$9 = _$createElement("text");
22490
+ _$insertNode(_el$8, _el$9);
22491
+ _$setProp(_el$8, "flexDirection", "row");
22492
+ _$insertNode(_el$9, _$createTextNode(`\u2502 Model `));
22493
+ _$insert(_el$8, _$createComponent(Show, {
22494
+ get when() {
22495
+ return !strip().empty.model;
22496
+ },
22497
+ get fallback() {
22498
+ return (() => {
22499
+ var _el$39 = _$createElement("text");
22500
+ _$insert(_el$39, () => "no model steps".padEnd(laneWidth()));
22501
+ _$effect((_$p) => _$setProp(_el$39, "fg", t.textMuted, _$p));
22502
+ return _el$39;
22503
+ })();
22504
+ },
22505
+ get children() {
22506
+ return _$createComponent(For, {
22507
+ get each() {
22508
+ return modelRuns();
22509
+ },
22510
+ children: (r) => (() => {
22511
+ var _el$40 = _$createElement("text");
22512
+ _$insert(_el$40, () => r.text);
22513
+ _$effect((_p$) => {
22514
+ var { fg: _v$10, bg: _v$11 } = r;
22515
+ _v$10 !== _p$.e && (_p$.e = _$setProp(_el$40, "fg", _v$10, _p$.e));
22516
+ _v$11 !== _p$.t && (_p$.t = _$setProp(_el$40, "bg", _v$11, _p$.t));
22517
+ return _p$;
22518
+ }, {
22519
+ e: undefined,
22520
+ t: undefined
22521
+ });
22522
+ return _el$40;
22523
+ })()
22524
+ });
22525
+ }
22526
+ }), null);
22527
+ _$effect((_$p) => _$setProp(_el$9, "fg", t.textMuted, _$p));
22528
+ return _el$8;
21827
22529
  })(), (() => {
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();
22530
+ var _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$12 = _$createElement("text");
22531
+ _$insertNode(_el$1, _el$10);
22532
+ _$insertNode(_el$1, _el$12);
22533
+ _$setProp(_el$1, "flexDirection", "row");
22534
+ _$insertNode(_el$10, _$createTextNode(`\u2502 Tools `));
22535
+ _$insert(_el$1, _$createComponent(Show, {
22536
+ get when() {
22537
+ return !strip().empty.tools;
21836
22538
  },
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`));
22539
+ get fallback() {
22540
+ return (() => {
22541
+ var _el$41 = _$createElement("text");
22542
+ _$insert(_el$41, () => "no tool calls".padEnd(laneWidth()));
22543
+ _$effect((_$p) => _$setProp(_el$41, "fg", t.textMuted, _$p));
22544
+ return _el$41;
22545
+ })();
22546
+ },
22547
+ get children() {
22548
+ return _$createComponent(For, {
22549
+ get each() {
22550
+ return toolRuns();
22551
+ },
22552
+ children: (r) => (() => {
22553
+ var _el$42 = _$createElement("text");
22554
+ _$insert(_el$42, () => r.text);
22555
+ _$effect((_p$) => {
22556
+ var { fg: _v$12, bg: _v$13 } = r;
22557
+ _v$12 !== _p$.e && (_p$.e = _$setProp(_el$42, "fg", _v$12, _p$.e));
22558
+ _v$13 !== _p$.t && (_p$.t = _$setProp(_el$42, "bg", _v$13, _p$.t));
22559
+ return _p$;
22560
+ }, {
22561
+ e: undefined,
22562
+ t: undefined
22563
+ });
22564
+ return _el$42;
22565
+ })()
22566
+ });
22567
+ }
22568
+ }), _el$12);
22569
+ _$insertNode(_el$12, _$createTextNode(` i inspector \xB7 s consumers`));
21845
22570
  _$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));
22571
+ var { textMuted: _v$3, textMuted: _v$4 } = t;
22572
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$10, "fg", _v$3, _p$.e));
22573
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$12, "fg", _v$4, _p$.t));
21849
22574
  return _p$;
21850
22575
  }, {
21851
22576
  e: undefined,
21852
22577
  t: undefined
21853
22578
  });
21854
- return _el$10;
22579
+ return _el$1;
21855
22580
  })()];
21856
22581
  }
21857
22582
  })];
21858
22583
  }
21859
- }), _el$17);
22584
+ }), _el$16);
21860
22585
  _$insert(_el$, _$createComponent(Show, {
21861
22586
  get when() {
21862
22587
  return _$memo(() => !!(laneRoom() && lanesOn()))() && !showLanes();
21863
22588
  },
21864
22589
  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);
22590
+ var _el$14 = _$createElement("text");
22591
+ _$insertNode(_el$14, _$createTextNode(`\u2502 lanes appear after 3 turns`));
22592
+ _$effect((_$p) => _$setProp(_el$14, "fg", t.textMuted, _$p));
22593
+ return _el$14;
22594
+ }
22595
+ }), _el$16);
22596
+ _$insertNode(_el$16, _el$17);
22597
+ _$insert(_el$16, statusLine, null);
21887
22598
  _$insert(_el$, _$createComponent(Show, {
21888
22599
  get when() {
21889
22600
  return panel() === "decisions";
21890
22601
  },
21891
22602
  get children() {
21892
22603
  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;
22604
+ 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`);
22605
+ _$insertNode(_el$18, _el$19);
22606
+ _$insertNode(_el$18, _el$20);
22607
+ _$insert(_el$18, () => decisions().length, _el$20);
22608
+ _$effect((_$p) => _$setProp(_el$18, "fg", t.accent, _$p));
22609
+ return _el$18;
21899
22610
  })(), _$createComponent(Show, {
21900
22611
  get when() {
21901
22612
  return decisions().length === 0;
21902
22613
  },
21903
22614
  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;
22615
+ var _el$21 = _$createElement("text");
22616
+ _$insertNode(_el$21, _$createTextNode(`\u2502 (none yet \u2014 /merge a branch to write one)`));
22617
+ _$effect((_$p) => _$setProp(_el$21, "fg", t.textMuted, _$p));
22618
+ return _el$21;
21908
22619
  }
21909
22620
  }), _$createComponent(For, {
21910
22621
  get each() {
@@ -21912,132 +22623,142 @@ ${MERGE_TRUST}
21912
22623
  },
21913
22624
  children: (d, i) => {
21914
22625
  const sel = () => i() === decisionIndex();
21915
- const lines = () => (d.text ?? "").split(`
21916
- `).slice(0, sel() ? 12 : 1);
22626
+ const body = () => renderDecision(d.text ?? "", width() - 6);
22627
+ const room = () => Math.max(3, height() - decisions().length);
22628
+ const start = () => Math.min(decisionScroll(), Math.max(0, body().length - room()));
22629
+ const more = () => Math.max(0, body().length - start() - room());
21917
22630
  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(", ")}` : "";
22631
+ var _el$43 = _$createElement("box"), _el$44 = _$createElement("text"), _el$45 = _$createTextNode(` `), _el$46 = _$createTextNode(` \xB7 `);
22632
+ _$insertNode(_el$43, _el$44);
22633
+ _$setProp(_el$43, "flexDirection", "column");
22634
+ _$insertNode(_el$44, _el$45);
22635
+ _$insertNode(_el$44, _el$46);
22636
+ _$insert(_el$44, () => sel() ? "\u203A" : "\u2502", _el$45);
22637
+ _$insert(_el$44, () => d.hidden ? "\u25C7 (hidden from model) " : "\u25C6 ", _el$46);
22638
+ _$insert(_el$44, () => clip(decisionSummary(d.text ?? "").title || d.branchName, 48), _el$46);
22639
+ _$insert(_el$44, () => new Date(d.recordedAt).toISOString().slice(0, 16).replace("T", " "), null);
22640
+ _$insert(_el$44, (() => {
22641
+ var _c$3 = _$memo(() => !!d.siblings.length);
22642
+ return () => _c$3() ? ` \xB7 \u2717 ${d.siblings.map((x) => x.name).join(", ")}` : "";
21930
22643
  })(), null);
21931
- _$insert(_el$37, _$createComponent(For, {
22644
+ _$insert(_el$43, _$createComponent(For, {
21932
22645
  get each() {
21933
- return _$memo(() => !!sel())() ? lines().slice(1) : [];
22646
+ return _$memo(() => !!sel())() ? body().slice(start(), start() + room()) : [];
21934
22647
  },
21935
22648
  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;
22649
+ var _el$48 = _$createElement("text");
22650
+ _$insert(_el$48, `\u2502 ${l}`);
22651
+ _$effect((_$p) => _$setProp(_el$48, "fg", t.text, _$p));
22652
+ return _el$48;
21941
22653
  })()
21942
22654
  }), null);
22655
+ _$insert(_el$43, _$createComponent(Show, {
22656
+ get when() {
22657
+ return _$memo(() => !!sel())() && more() > 0;
22658
+ },
22659
+ get children() {
22660
+ var _el$47 = _$createElement("text");
22661
+ _$insert(_el$47, () => `\u2502 \u2026 ${more()} more lines \u2193 (ctrl+d)`);
22662
+ _$effect((_$p) => _$setProp(_el$47, "fg", t.textMuted, _$p));
22663
+ return _el$47;
22664
+ }
22665
+ }), null);
21943
22666
  _$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));
22667
+ var _v$14 = sel() ? t.background : t.accent, _v$15 = sel() ? t.primary : undefined;
22668
+ _v$14 !== _p$.e && (_p$.e = _$setProp(_el$44, "fg", _v$14, _p$.e));
22669
+ _v$15 !== _p$.t && (_p$.t = _$setProp(_el$44, "bg", _v$15, _p$.t));
21947
22670
  return _p$;
21948
22671
  }, {
21949
22672
  e: undefined,
21950
22673
  t: undefined
21951
22674
  });
21952
- return _el$37;
22675
+ return _el$43;
21953
22676
  })();
21954
22677
  }
21955
22678
  })];
21956
22679
  }
21957
- }), _el$31);
22680
+ }), _el$28);
21958
22681
  _$insert(_el$, _$createComponent(Show, {
21959
22682
  get when() {
21960
22683
  return panel() === "consumers";
21961
22684
  },
21962
22685
  get children() {
21963
22686
  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;
22687
+ 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`);
22688
+ _$insertNode(_el$23, _el$24);
22689
+ _$insertNode(_el$23, _el$25);
22690
+ _$insert(_el$23, () => formatK(view().totalTokens), _el$25);
22691
+ _$effect((_$p) => _$setProp(_el$23, "fg", t.accent, _$p));
22692
+ return _el$23;
21970
22693
  })(), _$createComponent(For, {
21971
22694
  get each() {
21972
- return consumerRows();
22695
+ return consumerLines();
21973
22696
  },
21974
- children: (c, i) => {
22697
+ children: (line, i) => {
21975
22698
  const sel = () => i() === consumerIndex();
22699
+ const c = line.bucket;
22700
+ const fg = () => sel() ? t.background : line.entry ? t.textMuted : c.kind === "tool" ? t.warning : t.text;
22701
+ const window = () => c.shareOfWindow === undefined ? "\u2013" : `${(c.shareOfWindow * 100).toFixed(0)}%`;
22702
+ const entry = line.entry;
21976
22703
  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);
22704
+ var _el$49 = _$createElement("text"), _el$50 = _$createTextNode(` `);
22705
+ _$insertNode(_el$49, _el$50);
22706
+ _$insert(_el$49, () => sel() ? "\u203A" : "\u2502", _el$50);
22707
+ _$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
22708
  _$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));
22709
+ var _v$16 = fg(), _v$17 = sel() ? t.primary : undefined;
22710
+ _v$16 !== _p$.e && (_p$.e = _$setProp(_el$49, "fg", _v$16, _p$.e));
22711
+ _v$17 !== _p$.t && (_p$.t = _$setProp(_el$49, "bg", _v$17, _p$.t));
21995
22712
  return _p$;
21996
22713
  }, {
21997
22714
  e: undefined,
21998
22715
  t: undefined
21999
22716
  });
22000
- return _el$43;
22717
+ return _el$49;
22001
22718
  })();
22002
22719
  }
22003
22720
  })];
22004
22721
  }
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);
22722
+ }), _el$28);
22018
22723
  _$insert(_el$, _$createComponent(Show, {
22019
22724
  get when() {
22020
- return _$memo(() => panel() === "tree")() && view().rows.length === 0;
22725
+ return _$memo(() => !!showsTree())() && view().rows.length === 0;
22726
+ },
22727
+ get children() {
22728
+ var _el$26 = _$createElement("text"), _el$27 = _$createTextNode(`\u2502 `);
22729
+ _$insertNode(_el$26, _el$27);
22730
+ _$insert(_el$26, emptyText, null);
22731
+ _$effect((_$p) => _$setProp(_el$26, "fg", t.textMuted, _$p));
22732
+ return _el$26;
22733
+ }
22734
+ }), _el$28);
22735
+ _$insertNode(_el$28, _el$29);
22736
+ _$setProp(_el$28, "flexDirection", "row");
22737
+ _$setProp(_el$28, "flexGrow", 1);
22738
+ _$setProp(_el$29, "flexDirection", "column");
22739
+ _$setProp(_el$29, "flexGrow", 1);
22740
+ _$insert(_el$29, _$createComponent(Show, {
22741
+ get when() {
22742
+ return _$memo(() => !!showsTree())() && overflow();
22021
22743
  },
22022
22744
  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, {
22745
+ var _el$30 = _$createElement("text"), _el$31 = _$createTextNode(`\u2502 `);
22746
+ _$insertNode(_el$30, _el$31);
22747
+ _$insert(_el$30, (() => {
22748
+ var _c$ = _$memo(() => hiddenAbove() > 0);
22749
+ return () => _c$() ? `\u2191 ${hiddenAbove()} more` : "";
22750
+ })(), null);
22751
+ _$effect((_$p) => _$setProp(_el$30, "fg", t.textMuted, _$p));
22752
+ return _el$30;
22753
+ }
22754
+ }), null);
22755
+ _$insert(_el$29, _$createComponent(For, {
22035
22756
  get each() {
22036
- return _$memo(() => panel() === "tree")() ? visible() : [];
22757
+ return _$memo(() => !!showsTree())() ? visible() : [];
22037
22758
  },
22038
22759
  children: (row, i) => {
22039
22760
  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;
22761
+ 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
22762
  const mark = () => {
22042
22763
  if (!cropMode())
22043
22764
  return "";
@@ -22048,70 +22769,139 @@ ${MERGE_TRUST}
22048
22769
  const prot = c.protections.filter((p) => p !== "too-small");
22049
22770
  return `${on2 ? "[x]" : "[ ]"}${prot.length ? "!" : " "}`;
22050
22771
  };
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
- })();
22772
+ const prefix = () => `${isSel() ? "\u203A" : "\u2502"} ${mark()}`;
22773
+ const segs = () => segmentsOf(rowLine(row, rowWidth(), row.id === view().currentRowId), search().trim(), thoughtOf(row));
22774
+ return _$createComponent(Show, {
22775
+ get when() {
22776
+ return segs().length > 1;
22777
+ },
22778
+ get fallback() {
22779
+ return (() => {
22780
+ var _el$53 = _$createElement("text");
22781
+ _$insert(_el$53, prefix, null);
22782
+ _$insert(_el$53, () => segs()[0]?.text, null);
22783
+ _$effect((_p$) => {
22784
+ var _v$20 = isSel() ? t.background : color(), _v$21 = isSel() ? t.primary : undefined;
22785
+ _v$20 !== _p$.e && (_p$.e = _$setProp(_el$53, "fg", _v$20, _p$.e));
22786
+ _v$21 !== _p$.t && (_p$.t = _$setProp(_el$53, "bg", _v$21, _p$.t));
22787
+ return _p$;
22788
+ }, {
22789
+ e: undefined,
22790
+ t: undefined
22791
+ });
22792
+ return _el$53;
22793
+ })();
22794
+ },
22795
+ get children() {
22796
+ var _el$51 = _$createElement("box"), _el$52 = _$createElement("text");
22797
+ _$insertNode(_el$51, _el$52);
22798
+ _$setProp(_el$51, "flexDirection", "row");
22799
+ _$insert(_el$52, prefix);
22800
+ _$insert(_el$51, _$createComponent(For, {
22801
+ get each() {
22802
+ return segs();
22803
+ },
22804
+ children: (s) => (() => {
22805
+ var _el$54 = _$createElement("text");
22806
+ _$insert(_el$54, () => s.text);
22807
+ _$effect((_p$) => {
22808
+ 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;
22809
+ _v$22 !== _p$.e && (_p$.e = _$setProp(_el$54, "fg", _v$22, _p$.e));
22810
+ _v$23 !== _p$.t && (_p$.t = _$setProp(_el$54, "bg", _v$23, _p$.t));
22811
+ return _p$;
22812
+ }, {
22813
+ e: undefined,
22814
+ t: undefined
22815
+ });
22816
+ return _el$54;
22817
+ })()
22818
+ }), null);
22819
+ _$effect((_p$) => {
22820
+ var _v$18 = isSel() ? t.background : color(), _v$19 = isSel() ? t.primary : undefined;
22821
+ _v$18 !== _p$.e && (_p$.e = _$setProp(_el$52, "fg", _v$18, _p$.e));
22822
+ _v$19 !== _p$.t && (_p$.t = _$setProp(_el$52, "bg", _v$19, _p$.t));
22823
+ return _p$;
22824
+ }, {
22825
+ e: undefined,
22826
+ t: undefined
22827
+ });
22828
+ return _el$51;
22829
+ }
22830
+ });
22831
+ }
22832
+ }), null);
22833
+ _$insert(_el$29, _$createComponent(Show, {
22834
+ get when() {
22835
+ return _$memo(() => !!showsTree())() && overflow();
22836
+ },
22837
+ get children() {
22838
+ var _el$32 = _$createElement("text"), _el$33 = _$createTextNode(`\u2502 `);
22839
+ _$insertNode(_el$32, _el$33);
22840
+ _$insert(_el$32, (() => {
22841
+ var _c$2 = _$memo(() => hiddenBelow() > 0);
22842
+ return () => _c$2() ? `\u2026 ${hiddenBelow()} more \u2193` : "";
22843
+ })(), null);
22844
+ _$effect((_$p) => _$setProp(_el$32, "fg", t.textMuted, _$p));
22845
+ return _el$32;
22068
22846
  }
22069
22847
  }), null);
22070
- _$insert(_el$32, _$createComponent(For, {
22848
+ _$insert(_el$29, _$createComponent(For, {
22849
+ get each() {
22850
+ return _$memo(() => !!(showsTree() && view().rows.length > 0 && !view().rows.some((r) => r.kind === "branch")))() ? noBranchesLines() : [];
22851
+ },
22852
+ children: (l) => (() => {
22853
+ var _el$55 = _$createElement("text"), _el$56 = _$createTextNode(`\u2502 `);
22854
+ _$insertNode(_el$55, _el$56);
22855
+ _$insert(_el$55, l, null);
22856
+ _$effect((_$p) => _$setProp(_el$55, "fg", t.textMuted, _$p));
22857
+ return _el$55;
22858
+ })()
22859
+ }), null);
22860
+ _$insert(_el$29, _$createComponent(For, {
22071
22861
  get each() {
22072
- return _$memo(() => !!(panel() === "tree" && view().rows.length > 0 && !view().rows.some((r) => r.kind === "branch")))() ? noBranchesLines() : [];
22862
+ return _$memo(() => panel() === "help")() ? HELP.slice(0, helpHeight()) : [];
22073
22863
  },
22074
22864
  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;
22865
+ var _el$57 = _$createElement("text"), _el$58 = _$createTextNode(`\u2502 `);
22866
+ _$insertNode(_el$57, _el$58);
22867
+ _$insert(_el$57, l, null);
22868
+ _$effect((_$p) => _$setProp(_el$57, "fg", l.startsWith(" ") ? t.textMuted : t.accent, _$p));
22869
+ return _el$57;
22080
22870
  })()
22081
22871
  }), null);
22082
- _$insert(_el$31, _$createComponent(Show, {
22872
+ _$insert(_el$28, _$createComponent(Show, {
22083
22873
  get when() {
22084
22874
  return showInspector();
22085
22875
  },
22086
22876
  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, {
22877
+ var _el$34 = _$createElement("box");
22878
+ _$setProp(_el$34, "flexDirection", "column");
22879
+ _$setProp(_el$34, "paddingLeft", 1);
22880
+ _$insert(_el$34, _$createComponent(For, {
22091
22881
  get each() {
22092
22882
  return inspectorLines();
22093
22883
  },
22094
22884
  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;
22885
+ var _el$59 = _$createElement("text"), _el$60 = _$createTextNode(`\u2503 `);
22886
+ _$insertNode(_el$59, _el$60);
22887
+ _$insert(_el$59, () => l.text, null);
22888
+ _$effect((_$p) => _$setProp(_el$59, "fg", l.fg, _$p));
22889
+ return _el$59;
22100
22890
  })()
22101
22891
  }));
22102
- _$effect((_$p) => _$setProp(_el$33, "width", inspectorWidth(), _$p));
22103
- return _el$33;
22892
+ _$effect((_$p) => _$setProp(_el$34, "width", inspectorWidth(), _$p));
22893
+ return _el$34;
22104
22894
  }
22105
22895
  }), 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);
22896
+ _$insertNode(_el$35, _el$36);
22897
+ _$insert(_el$35, footer, null);
22108
22898
  _$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));
22899
+ 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;
22900
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$, "backgroundColor", _v$5, _p$.e));
22901
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$6, _p$.t));
22902
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$4, "fg", _v$7, _p$.a));
22903
+ _v$8 !== _p$.o && (_p$.o = _$setProp(_el$16, "fg", _v$8, _p$.o));
22904
+ _v$9 !== _p$.i && (_p$.i = _$setProp(_el$35, "fg", _v$9, _p$.i));
22115
22905
  return _p$;
22116
22906
  }, {
22117
22907
  e: undefined,
@@ -22342,7 +23132,7 @@ var tui = async (api2, rawOptions) => {
22342
23132
  adoptSoon();
22343
23133
  });
22344
23134
  api2.lifecycle?.onDispose(offCreated);
22345
- const promptDialog = (title, placeholder) => new Promise((resolve) => {
23135
+ const promptDialog2 = (title, placeholder) => new Promise((resolve) => {
22346
23136
  api2.ui.dialog.replace(() => api2.ui.DialogPrompt({
22347
23137
  title,
22348
23138
  placeholder,
@@ -22399,7 +23189,7 @@ var tui = async (api2, rawOptions) => {
22399
23189
  if (!sessionID)
22400
23190
  return;
22401
23191
  await new Promise((r) => setTimeout(r, 30));
22402
- const name = await promptDialog(BRANCH_DIALOG.title, BRANCH_DIALOG.placeholder);
23192
+ const name = await promptDialog2(BRANCH_DIALOG.title, BRANCH_DIALOG.placeholder);
22403
23193
  debug("branch.named", {
22404
23194
  name
22405
23195
  });
@@ -22437,7 +23227,7 @@ var tui = async (api2, rawOptions) => {
22437
23227
  const last = api2.state.session.messages(sessionID).at(-1);
22438
23228
  if (!last)
22439
23229
  return;
22440
- const value = await promptDialog("Label (empty to remove)", "checkpoint");
23230
+ const value = await promptDialog2("Label (empty to remove)", "checkpoint");
22441
23231
  if (value === undefined)
22442
23232
  return;
22443
23233
  setLabel({
@@ -22482,11 +23272,20 @@ var tui = async (api2, rawOptions) => {
22482
23272
  });
22483
23273
  return;
22484
23274
  }
23275
+ const parent = await fetchTranscript(api2, branch.parentSessionID, directory).catch(() => {
23276
+ return;
23277
+ });
23278
+ const parentLabel = branch.parentSessionID === state.root ? TRUNK_LABEL : state.sessions[branch.parentSessionID]?.name ?? TRUNK_LABEL;
23279
+ const turns = ownTurnCount(api2.state.session.messages(sessionID), {
23280
+ messageID: branch.anchorMessageID,
23281
+ parentMessageIDs: parent?.messages.map((m) => m.id) ?? []
23282
+ });
22485
23283
  const mode = await new Promise((resolve) => {
22486
23284
  api2.ui.dialog.replace(() => api2.ui.DialogSelect({
22487
- title: mergeDialogTitle(branch.name ?? "branch", api2.state.session.get(branch.parentSessionID)?.title),
23285
+ title: mergeDialogTitle(branch.name ?? "branch", parent ? mergeTargetOf(parentLabel, parent.messages) : undefined),
22488
23286
  options: mergeDialogOptions({
22489
- siblings: openSiblings(state, sessionID).length
23287
+ siblings: openSiblings(state, sessionID).length,
23288
+ turns
22490
23289
  }),
22491
23290
  onSelect: (o) => {
22492
23291
  resolve(o.value);
@@ -22581,6 +23380,8 @@ ${MERGE_TRUST}`,
22581
23380
  return store.stateForSession(props.session_id);
22582
23381
  });
22583
23382
  const branch = () => st()?.sessions[props.session_id];
23383
+ const size = createMemo2(() => contextSizeOf(toMinimalMessages(api2.state.session.messages(props.session_id), api2.state.part)));
23384
+ const limit = createMemo2(() => modelContextLimit(api2, props.session_id));
22584
23385
  const crops = () => st()?.activeCrops(props.session_id) ?? [];
22585
23386
  const hidden = () => crops().reduce((s, c) => s + c.targets.reduce((x, y) => x + y.estTokens, 0), 0);
22586
23387
  const siblings = () => Object.values(st()?.sessions ?? {}).filter((b) => b.parentSessionID === props.session_id && b.status === "open").length;
@@ -22591,9 +23392,10 @@ ${MERGE_TRUST}`,
22591
23392
  return `${b.status}${title && room > 3 ? ` \xB7 from "${clip(title, room)}"` : ""}`;
22592
23393
  };
22593
23394
  return (() => {
22594
- var _el$ = _$createElement2("box"), _el$2 = _$createElement2("text"), _el$3 = _$createElement2("b"), _el$8 = _$createElement2("text");
23395
+ var _el$ = _$createElement2("box"), _el$2 = _$createElement2("text"), _el$3 = _$createElement2("b"), _el$7 = _$createElement2("text"), _el$9 = _$createElement2("text");
22595
23396
  _$insertNode2(_el$, _el$2);
22596
- _$insertNode2(_el$, _el$8);
23397
+ _$insertNode2(_el$, _el$7);
23398
+ _$insertNode2(_el$, _el$9);
22597
23399
  _$setProp2(_el$, "flexDirection", "column");
22598
23400
  _$insertNode2(_el$2, _el$3);
22599
23401
  _$insertNode2(_el$3, _$createTextNode2(`Context tree`));
@@ -22603,10 +23405,10 @@ ${MERGE_TRUST}`,
22603
23405
  },
22604
23406
  get fallback() {
22605
23407
  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;
23408
+ var _el$1 = _$createElement2("text");
23409
+ _$insert2(_el$1, () => `trunk${siblings() ? ` \xB7 ${siblings()} branch${siblings() === 1 ? "" : "es"}` : ""}`);
23410
+ _$effect2((_$p) => _$setProp2(_el$1, "fg", t.text, _$p));
23411
+ return _el$1;
22610
23412
  })();
22611
23413
  },
22612
23414
  get children() {
@@ -22622,27 +23424,30 @@ ${MERGE_TRUST}`,
22622
23424
  return _el$6;
22623
23425
  })()];
22624
23426
  }
22625
- }), _el$8);
23427
+ }), _el$7);
23428
+ _$insert2(_el$7, () => formatContext(size(), limit()));
22626
23429
  _$insert2(_el$, _$createComponent2(Show2, {
22627
23430
  get when() {
22628
23431
  return crops().length;
22629
23432
  },
22630
23433
  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;
23434
+ var _el$8 = _$createElement2("text");
23435
+ _$insert2(_el$8, () => `\u2702 ${crops().length} crop${crops().length === 1 ? "" : "s"} \xB7 ~${formatK(hidden())} hidden`);
23436
+ _$effect2((_$p) => _$setProp2(_el$8, "fg", t.warning, _$p));
23437
+ return _el$8;
22635
23438
  }
22636
- }), _el$8);
22637
- _$insertNode2(_el$8, _$createTextNode2(`/tree \xB7 ctrl+q`));
23439
+ }), _el$9);
23440
+ _$insertNode2(_el$9, _$createTextNode2(`/tree \xB7 ctrl+q`));
22638
23441
  _$effect2((_p$) => {
22639
- var { text: _v$, textMuted: _v$2 } = t;
23442
+ var _v$ = t.text, _v$2 = t[BAND_COLOR[bandFor(size().tokens, limit())]], _v$3 = t.textMuted;
22640
23443
  _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));
23444
+ _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$7, "fg", _v$2, _p$.t));
23445
+ _v$3 !== _p$.a && (_p$.a = _$setProp2(_el$9, "fg", _v$3, _p$.a));
22642
23446
  return _p$;
22643
23447
  }, {
22644
23448
  e: undefined,
22645
- t: undefined
23449
+ t: undefined,
23450
+ a: undefined
22646
23451
  });
22647
23452
  return _el$;
22648
23453
  })();
@@ -22650,12 +23455,12 @@ ${MERGE_TRUST}`,
22650
23455
  session_prompt_right: (_ctx, props) => {
22651
23456
  const t = api2.theme.current;
22652
23457
  const size = createMemo2(() => contextSizeOf(toMinimalMessages(api2.state.session.messages(props.session_id), api2.state.part)));
22653
- const band = () => bandFor(size().tokens);
22654
23458
  const branch = () => {
22655
23459
  journalRevision();
22656
23460
  return store.stateForSession(props.session_id)?.sessions[props.session_id];
22657
23461
  };
22658
23462
  const limit = createMemo2(() => modelContextLimit(api2, props.session_id));
23463
+ const band = () => bandFor(size().tokens, limit());
22659
23464
  const reserve = () => api2.state.config.compaction?.reserved ?? 16384;
22660
23465
  const [trend, setTrend] = createSignal3("");
22661
23466
  let prevTokens = 0;
@@ -22711,7 +23516,7 @@ ${MERGE_TRUST}`,
22711
23516
  redNudged = true;
22712
23517
  api2.ui.toast({
22713
23518
  variant: "warning",
22714
- message: "context is in the red band (\u226564k) \u2014 consider /tree \u2192 c crop, or /merge a branch",
23519
+ message: `context is in the red band (${limit() ? "\u226585% of the window" : "\u226564k"}) \u2014 consider /tree \u2192 c crop, or /merge a branch`,
22715
23520
  duration: 6000
22716
23521
  });
22717
23522
  } else if (b === "low" || b === "healthy")
@@ -22728,10 +23533,10 @@ ${MERGE_TRUST}`,
22728
23533
  guardNudged = false;
22729
23534
  });
22730
23535
  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;
23536
+ var _el$10 = _$createElement2("text");
23537
+ _$insert2(_el$10, () => `${branch() ? `\u2387 ${branchLabel(api2, props.session_id, branch().name, 24)} \xB7 ` : ""}${formatContext(size(), limit())}${trend()}`);
23538
+ _$effect2((_$p) => _$setProp2(_el$10, "fg", t[BAND_COLOR[band()]], _$p));
23539
+ return _el$10;
22735
23540
  })();
22736
23541
  }
22737
23542
  }