sisyphi 1.1.16 → 1.1.18

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
@@ -4,20 +4,28 @@ import {
4
4
  augmentedPath,
5
5
  exec,
6
6
  execSafe
7
- } from "./chunk-6TIO23U3.js";
7
+ } from "./chunk-22ZGZTGY.js";
8
8
  import {
9
9
  buildSessionContext,
10
10
  computeActiveTimeMs,
11
+ createBadgeGallery,
11
12
  formatDuration,
13
+ galleryNext,
14
+ galleryPrev,
12
15
  rawSend,
16
+ renderBadgeCard,
13
17
  resolveReports,
14
18
  statusColor
15
- } from "./chunk-HQZOAX6D.js";
19
+ } from "./chunk-C2XKXERJ.js";
16
20
  import {
21
+ ACHIEVEMENTS,
22
+ getMoodFace,
17
23
  loadConfig,
24
+ renderCompanion,
18
25
  shellQuote
19
- } from "./chunk-IF55HPWX.js";
26
+ } from "./chunk-V36NXMHP.js";
20
27
  import {
28
+ companionPath,
21
29
  contextDir,
22
30
  globalDir,
23
31
  goalPath,
@@ -25,8 +33,9 @@ import {
25
33
  roadmapPath,
26
34
  sessionDir,
27
35
  strategyPath,
36
+ tmuxSessionName,
28
37
  tuiScratchDir
29
- } from "./chunk-GSXF3TCZ.js";
38
+ } from "./chunk-TMBAVPHH.js";
30
39
 
31
40
  // src/tui/terminal.ts
32
41
  function emptyKey() {
@@ -235,27 +244,6 @@ var COMPOSE_HEADERS = {
235
244
  "spawn-agent": "Spawn Agent",
236
245
  "message-agent": "Message Agent"
237
246
  };
238
- var INPUT_MODES = /* @__PURE__ */ new Set([
239
- "resume",
240
- "continue",
241
- "rollback",
242
- "delete-confirm",
243
- "spawn-agent",
244
- "search",
245
- "message-agent",
246
- "shell-command"
247
- ]);
248
- var OPTIONAL_INPUT = /* @__PURE__ */ new Set(["resume", "continue", "search"]);
249
- var PROMPTS = {
250
- resume: "resume instructions (optional)",
251
- continue: "new direction (optional)",
252
- rollback: "cycle number",
253
- "delete-confirm": "type 'yes' to confirm delete:",
254
- "spawn-agent": "agent instruction:",
255
- search: "filter:",
256
- "message-agent": "message:",
257
- "shell-command": "$ "
258
- };
259
247
  var renderScheduled = false;
260
248
  var renderFn = null;
261
249
  function setRenderFunction(fn) {
@@ -326,12 +314,11 @@ function createAppState(cwd2) {
326
314
  focusPane: "tree",
327
315
  selectedSessionId: null,
328
316
  searchFilter: null,
317
+ searchText: "",
329
318
  targetAgentId: null,
330
319
  notification: null,
331
320
  notificationTimer: null,
332
321
  showCombinedView: false,
333
- inputText: "",
334
- inputCursorPos: 0,
335
322
  detailScroll,
336
323
  logsScroll,
337
324
  sessions: [],
@@ -822,356 +809,911 @@ function findParentIndex(nodes, index) {
822
809
  return 0;
823
810
  }
824
811
 
825
- // src/tui/input.ts
826
- function activateNvimBypass(state2) {
827
- setRawBypass((data) => {
828
- if (!state2.nvimBridge?.ready) {
829
- deactivateNvimBypass();
830
- state2.focusPane = "tree";
831
- if (state2.mode === "compose") cancelCompose(state2);
832
- requestRender();
833
- return false;
834
- }
835
- if (data === " ") {
836
- if (state2.mode === "compose") {
837
- cancelCompose(state2);
838
- return true;
839
- }
840
- deactivateNvimBypass();
841
- state2.focusPane = state2.showCombinedView ? "logs" : "tree";
842
- requestRender();
843
- return true;
844
- }
845
- state2.nvimBridge.write(data);
846
- return true;
847
- });
848
- }
849
- function deactivateNvimBypass() {
850
- setRawBypass(null);
812
+ // src/tui/render.ts
813
+ import stringWidth2 from "string-width";
814
+ var COLOR_SGR = {
815
+ black: 30,
816
+ red: 31,
817
+ green: 32,
818
+ yellow: 33,
819
+ blue: 34,
820
+ magenta: 35,
821
+ cyan: 36,
822
+ white: 37,
823
+ gray: 90
824
+ };
825
+ function colorToSGR(color) {
826
+ const code = COLOR_SGR[color];
827
+ if (code === void 0) throw new Error(`Unknown color: ${color}`);
828
+ return code;
851
829
  }
852
- var COMPOSE_DIR = join2(tmpdir(), "sisyphus-nvim");
853
- function enterComposeMode(state2, action, actions) {
854
- if (!state2.nvimEnabled || !state2.nvimBridge?.ready) return false;
855
- mkdirSync(COMPOSE_DIR, { recursive: true });
856
- const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
857
- const tempFile = join2(COMPOSE_DIR, `compose-${id}.md`);
858
- const signalFile = join2(COMPOSE_DIR, `compose-signal-${id}`);
859
- writeFileSync(tempFile, "", "utf-8");
860
- state2.composePrevNvimFile = state2.prevNvimFile;
861
- state2.composeAction = action;
862
- state2.composeTempFile = tempFile;
863
- state2.composeSignalFile = signalFile;
864
- state2.mode = "compose";
865
- state2.focusPane = "detail";
866
- state2.nvimBridge.openComposeFile(tempFile, signalFile);
867
- activateNvimBypass(state2);
868
- state2.composePollTimer = setInterval(() => {
869
- checkComposeSignal(state2, actions);
870
- }, 100);
871
- requestRender();
872
- return true;
830
+ function renderLine(segs) {
831
+ let out = "";
832
+ for (const s of segs) {
833
+ const codes = [];
834
+ if (s.bold) codes.push(1);
835
+ if (s.dim) codes.push(2);
836
+ if (s.italic) codes.push(3);
837
+ if (s.inverse) codes.push(7);
838
+ if (s.color) codes.push(colorToSGR(s.color));
839
+ if (codes.length > 0) {
840
+ out += `\x1B[${codes.join(";")}m${s.text}\x1B[0m`;
841
+ } else {
842
+ out += s.text;
843
+ }
844
+ }
845
+ return out;
873
846
  }
874
- function cancelCompose(state2) {
875
- if (state2.composePollTimer !== null) {
876
- clearInterval(state2.composePollTimer);
877
- state2.composePollTimer = null;
847
+ var cachedBlank = "";
848
+ var cachedBlankWidth = 0;
849
+ function createFrameBuffer(width, height) {
850
+ if (width !== cachedBlankWidth) {
851
+ cachedBlank = " ".repeat(width);
852
+ cachedBlankWidth = width;
878
853
  }
879
- if (state2.composeTempFile) {
880
- try {
881
- unlinkSync(state2.composeTempFile);
882
- } catch {
883
- }
854
+ const lines = new Array(height);
855
+ for (let i = 0; i < height; i++) lines[i] = cachedBlank;
856
+ return { lines, width, height };
857
+ }
858
+ function copyRows(buf, src, startRow, count) {
859
+ for (let i = 0; i < count && startRow + i < buf.height; i++) {
860
+ buf.lines[startRow + i] = src[startRow + i];
884
861
  }
885
- if (state2.composeSignalFile) {
886
- try {
887
- unlinkSync(state2.composeSignalFile);
888
- } catch {
862
+ }
863
+ function flushFrame(frame, prevFrame2, suffix) {
864
+ let out = "\x1B[?2026h";
865
+ for (let i = 0; i < frame.length; i++) {
866
+ if (frame[i] !== prevFrame2[i]) {
867
+ out += `\x1B[${i + 1};1H`;
868
+ out += "\x1B[2K";
869
+ out += frame[i];
889
870
  }
890
871
  }
891
- state2.prevNvimFile = null;
892
- state2.composePrevNvimFile = null;
893
- state2.composeAction = null;
894
- state2.composeTempFile = null;
895
- state2.composeSignalFile = null;
896
- state2.mode = "navigate";
897
- state2.focusPane = "tree";
898
- deactivateNvimBypass();
899
- requestRender();
872
+ if (suffix) out += suffix;
873
+ out += "\x1B[?2026l";
874
+ return out;
900
875
  }
901
- function checkComposeSignal(state2, actions) {
902
- if (!state2.composeSignalFile || !state2.composeAction) return;
903
- if (!state2.nvimBridge?.ready) {
904
- cancelCompose(state2);
905
- return;
876
+ var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
877
+ function clipAnsi(content, maxWidth) {
878
+ let out = "";
879
+ let displayWidth = 0;
880
+ let i = 0;
881
+ while (i < content.length) {
882
+ if (content[i] === "\x1B" && content[i + 1] === "[") {
883
+ const seqLen = ansiLen(content, i);
884
+ if (seqLen > 0) {
885
+ out += content.substring(i, i + seqLen);
886
+ i += seqLen;
887
+ continue;
888
+ }
889
+ }
890
+ const cp = content.codePointAt(i);
891
+ const ch = String.fromCodePoint(cp);
892
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
893
+ if (displayWidth + chWidth > maxWidth) break;
894
+ out += ch;
895
+ displayWidth += chWidth;
896
+ i += ch.length;
906
897
  }
907
- if (!existsSync(state2.composeSignalFile)) return;
908
- let signalContent = "";
909
- try {
910
- signalContent = readFileSync(state2.composeSignalFile, "utf-8").trim();
911
- } catch {
898
+ if (out.includes("\x1B[") && !out.endsWith("\x1B[0m")) {
899
+ out += "\x1B[0m";
912
900
  }
913
- if (signalContent === "cancel") {
914
- cancelCompose(state2);
915
- return;
901
+ const remaining = maxWidth - displayWidth;
902
+ if (remaining > 0) out += " ".repeat(remaining);
903
+ return out;
904
+ }
905
+ function displayWidthFast(s) {
906
+ let w = 0;
907
+ let i = 0;
908
+ while (i < s.length) {
909
+ if (s[i] === "\x1B" && s[i + 1] === "[") {
910
+ const len = ansiLen(s, i);
911
+ if (len > 0) {
912
+ i += len;
913
+ continue;
914
+ }
915
+ }
916
+ const cp = s.codePointAt(i);
917
+ const ch = String.fromCodePoint(cp);
918
+ w += cp < 128 ? 1 : stringWidth2(ch);
919
+ i += ch.length;
916
920
  }
917
- let content = "";
918
- if (state2.composeTempFile) {
919
- try {
920
- content = readFileSync(state2.composeTempFile, "utf-8").trim();
921
- } catch {
921
+ return w;
922
+ }
923
+ function ansiLen(s, i) {
924
+ let j = i + 2;
925
+ const len = s.length;
926
+ while (j < len) {
927
+ const c = s.charCodeAt(j);
928
+ if (c >= 48 && c <= 57 || c === 59) {
929
+ j++;
930
+ } else {
931
+ break;
922
932
  }
923
933
  }
924
- const action = state2.composeAction;
925
- const required = !OPTIONAL_COMPOSE.has(action.kind);
926
- if (required && !content) {
927
- try {
928
- unlinkSync(state2.composeSignalFile);
929
- } catch {
934
+ if (j < len) {
935
+ const c = s.charCodeAt(j);
936
+ if (c >= 65 && c <= 90 || c >= 97 && c <= 122) {
937
+ return j + 1 - i;
930
938
  }
931
- notify(state2, "Content required");
932
- return;
933
939
  }
934
- dispatchComposeAction(action, content, state2, actions);
935
- cancelCompose(state2);
940
+ return 0;
936
941
  }
937
- function dispatchComposeAction(action, content, state2, actions) {
938
- switch (action.kind) {
939
- case "new-session":
940
- actions.sendAndNotify(
941
- { type: "start", task: content, cwd: state2.cwd },
942
- "Session created"
943
- );
944
- break;
945
- case "message-orchestrator":
946
- actions.sendAndNotify(
947
- { type: "message", sessionId: action.sessionId, content },
948
- "Message queued"
949
- );
950
- break;
951
- case "resume":
952
- actions.sendAndNotify(
953
- { type: "resume", sessionId: action.sessionId, cwd: state2.cwd, message: content || void 0 },
954
- "Session resumed"
955
- );
956
- break;
957
- case "continue":
958
- void (async () => {
959
- try {
960
- const contRes = await actions.send({ type: "continue", sessionId: action.sessionId });
961
- if (!contRes.ok) {
962
- notify(state2, `Error: ${contRes.error}`);
963
- return;
964
- }
965
- actions.sendAndNotify(
966
- { type: "resume", sessionId: action.sessionId, cwd: state2.cwd, message: content || void 0 },
967
- "Session continued"
968
- );
969
- } catch (err) {
970
- notify(state2, `Error: ${err.message}`);
971
- }
972
- })();
973
- break;
974
- case "spawn-agent":
975
- actions.sendAndNotify(
976
- {
977
- type: "spawn",
978
- sessionId: action.sessionId,
979
- agentType: "default",
980
- name: "agent",
981
- instruction: content
982
- },
983
- "Agent spawned"
984
- );
985
- break;
986
- case "message-agent":
987
- actions.sendAndNotify(
988
- { type: "message", sessionId: action.sessionId, content, source: { type: "agent", agentId: action.agentId } },
989
- `Message sent to ${action.agentId}`
990
- );
991
- break;
942
+ function writeAt(buf, x, y, content) {
943
+ if (y < 0 || y >= buf.height) return;
944
+ if (x < 0 || x >= buf.width) return;
945
+ const existing = buf.lines[y];
946
+ const contentDisplayWidth = stringWidth2(content.replace(ANSI_RE, ""));
947
+ const prefix = sliceDisplayCols(existing, 0, x);
948
+ const suffix = sliceDisplayCols(existing, x + contentDisplayWidth, buf.width, true);
949
+ const prefixWidth = stringWidth2(prefix.replace(ANSI_RE, ""));
950
+ const paddedPrefix = prefix + " ".repeat(Math.max(0, x - prefixWidth));
951
+ buf.lines[y] = paddedPrefix + content + suffix;
952
+ }
953
+ function writeClipped(buf, x, y, content, maxWidth) {
954
+ if (y < 0 || y >= buf.height) return;
955
+ if (x < 0 || x >= buf.width) return;
956
+ let out = "";
957
+ let displayWidth = 0;
958
+ let i = 0;
959
+ while (i < content.length) {
960
+ if (content[i] === "\x1B" && content[i + 1] === "[") {
961
+ const seqLen = ansiLen(content, i);
962
+ if (seqLen > 0) {
963
+ out += content.substring(i, i + seqLen);
964
+ i += seqLen;
965
+ continue;
966
+ }
967
+ }
968
+ const cp = content.codePointAt(i);
969
+ const ch = String.fromCodePoint(cp);
970
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
971
+ if (displayWidth + chWidth > maxWidth) break;
972
+ out += ch;
973
+ displayWidth += chWidth;
974
+ i += ch.length;
975
+ }
976
+ if (out.includes("\x1B[") && !out.endsWith("\x1B[0m")) {
977
+ out += "\x1B[0m";
978
+ }
979
+ const remaining = maxWidth - displayWidth;
980
+ if (remaining > 0) {
981
+ out += " ".repeat(remaining);
982
+ }
983
+ const existing = buf.lines[y];
984
+ const prefix = sliceDisplayCols(existing, 0, x);
985
+ const suffix = sliceDisplayCols(existing, x + maxWidth, buf.width, true);
986
+ const prefixDisplayW = displayWidthFast(prefix);
987
+ const paddedPrefix = prefixDisplayW < x ? prefix + " ".repeat(x - prefixDisplayW) : prefix;
988
+ buf.lines[y] = paddedPrefix + out + suffix;
989
+ }
990
+ function writeCenter(buf, row, content) {
991
+ const textWidth = stringWidth2(content.replace(ANSI_RE, ""));
992
+ const x = Math.max(0, Math.floor((buf.width - textWidth) / 2));
993
+ writeAt(buf, x, row, content);
994
+ }
995
+ function drawBorder(buf, x, y, w, h, color) {
996
+ const sgr = `\x1B[${colorToSGR(color)}m`;
997
+ const reset = "\x1B[0m";
998
+ writeAt(buf, x, y, sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset);
999
+ writeAt(buf, x, y + h - 1, sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset);
1000
+ for (let row = y + 1; row < y + h - 1; row++) {
1001
+ writeAt(buf, x, row, sgr + "\u2502" + reset);
1002
+ writeAt(buf, x + w - 1, row, sgr + "\u2502" + reset);
1003
+ }
1004
+ }
1005
+ function buildPanelRows(rect, lines, scrollOffset, focused, borderColor, renderedCache) {
1006
+ const { w, h } = rect;
1007
+ const rows = new Array(h);
1008
+ const color = focused ? "blue" : borderColor;
1009
+ const sgr = `\x1B[${colorToSGR(color)}m`;
1010
+ const reset = "\x1B[0m";
1011
+ const innerW = w - 4;
1012
+ const innerH = h - 2;
1013
+ const blankInner = " ".repeat(innerW);
1014
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
1015
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
1016
+ const borderL = sgr + "\u2502" + reset + " ";
1017
+ const borderR = " " + sgr + "\u2502" + reset;
1018
+ const emptyRow = borderL + blankInner + borderR;
1019
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
1020
+ if (innerW <= 0 || innerH <= 0) return rows;
1021
+ let ansiLines;
1022
+ if (renderedCache && renderedCache.lines === lines) {
1023
+ ansiLines = renderedCache.ansi;
1024
+ } else {
1025
+ ansiLines = new Array(lines.length);
1026
+ for (let i = 0; i < lines.length; i++) {
1027
+ ansiLines[i] = renderLine(lines[i]);
1028
+ }
1029
+ if (renderedCache) {
1030
+ renderedCache.lines = lines;
1031
+ renderedCache.ansi = ansiLines;
1032
+ }
1033
+ }
1034
+ const hasOverflow = lines.length > innerH;
1035
+ const viewableH = hasOverflow ? innerH - 1 : innerH;
1036
+ const maxScroll = Math.max(0, lines.length - viewableH);
1037
+ const effectiveOffset = Math.min(scrollOffset, maxScroll);
1038
+ for (let i = 0; i < viewableH && effectiveOffset + i < ansiLines.length; i++) {
1039
+ const clipped = clipAnsi(ansiLines[effectiveOffset + i], innerW);
1040
+ rows[1 + i] = borderL + clipped + borderR;
1041
+ }
1042
+ if (hasOverflow) {
1043
+ const scrollPct = maxScroll > 0 ? Math.round(effectiveOffset / maxScroll * 100) : 100;
1044
+ const indicator = ` \u2195 ${scrollPct}% \xB7 ${lines.length} lines`;
1045
+ const clipped = clipAnsi(`\x1B[2m${indicator}\x1B[0m`, innerW);
1046
+ rows[1 + viewableH] = borderL + clipped + borderR;
1047
+ }
1048
+ return rows;
1049
+ }
1050
+ function buildEmptyPanelRows(rect, focused, borderColor, centerText) {
1051
+ const { w, h } = rect;
1052
+ const rows = new Array(h);
1053
+ const color = focused ? "blue" : borderColor;
1054
+ const sgr = `\x1B[${colorToSGR(color)}m`;
1055
+ const reset = "\x1B[0m";
1056
+ const innerW = w - 4;
1057
+ const borderL = sgr + "\u2502" + reset + " ";
1058
+ const borderR = " " + sgr + "\u2502" + reset;
1059
+ const emptyRow = borderL + " ".repeat(innerW) + borderR;
1060
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
1061
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
1062
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
1063
+ if (centerText) {
1064
+ const midRow = Math.floor(h / 2);
1065
+ if (midRow > 0 && midRow < h - 1) {
1066
+ const clipped = clipAnsi(centerText, innerW);
1067
+ const textW = displayWidthFast(centerText);
1068
+ const pad = Math.max(0, Math.floor((innerW - textW) / 2));
1069
+ const centered = " ".repeat(pad) + clipped;
1070
+ rows[midRow] = borderL + clipAnsi(centered, innerW) + borderR;
1071
+ }
1072
+ }
1073
+ return rows;
1074
+ }
1075
+ function sliceDisplayCols(s, start, end, restoreState = false) {
1076
+ let out = "";
1077
+ let col = 0;
1078
+ let i = 0;
1079
+ let inSlice = false;
1080
+ let hasOpenSGR = false;
1081
+ let pendingSGR = "";
1082
+ while (i < s.length && col < end) {
1083
+ if (s[i] === "\x1B" && s[i + 1] === "[") {
1084
+ const seqLen = ansiLen(s, i);
1085
+ if (seqLen > 0) {
1086
+ const seq = s.substring(i, i + seqLen);
1087
+ if (col >= start) {
1088
+ out += seq;
1089
+ hasOpenSGR = seq !== "\x1B[0m" && seq !== "\x1B[m";
1090
+ } else if (restoreState) {
1091
+ if (seq === "\x1B[0m" || seq === "\x1B[m") {
1092
+ pendingSGR = "";
1093
+ } else {
1094
+ pendingSGR += seq;
1095
+ }
1096
+ }
1097
+ i += seqLen;
1098
+ continue;
1099
+ }
1100
+ }
1101
+ const cp = s.codePointAt(i);
1102
+ const ch = String.fromCodePoint(cp);
1103
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1104
+ if (col >= start) {
1105
+ inSlice = true;
1106
+ if (col + chWidth > end) break;
1107
+ out += ch;
1108
+ }
1109
+ col += chWidth;
1110
+ i += ch.length;
1111
+ }
1112
+ if (inSlice && hasOpenSGR) {
1113
+ out += "\x1B[0m";
1114
+ }
1115
+ if (restoreState && pendingSGR) {
1116
+ out = pendingSGR + out;
1117
+ }
1118
+ return out;
1119
+ }
1120
+
1121
+ // src/tui/panels/overlays.ts
1122
+ var LEADER_WIDTH = 26;
1123
+ var LEADER_HEIGHT = 20;
1124
+ var COPY_HEIGHT = 9;
1125
+ var HELP_WIDTH = 62;
1126
+ var COMPANION_WIDTH = 52;
1127
+ var DEBUG_WIDTH = 50;
1128
+ function helpRow(left, right, innerWidth) {
1129
+ const col = Math.floor(innerWidth / 2);
1130
+ return (left.padEnd(col) + right).padEnd(innerWidth);
1131
+ }
1132
+ function renderLeaderOverlay(buf, rows, cols) {
1133
+ const x = cols - LEADER_WIDTH - 1;
1134
+ const y = rows - LEADER_HEIGHT - 2;
1135
+ const innerWidth = LEADER_WIDTH - 2;
1136
+ drawBorder(buf, x, y, LEADER_WIDTH, LEADER_HEIGHT, "magenta");
1137
+ const lines = [
1138
+ ansiColor(" LEADER".padEnd(innerWidth), "magenta", true),
1139
+ " ".padEnd(innerWidth),
1140
+ " y copy menu".padEnd(innerWidth),
1141
+ " d delete session".padEnd(innerWidth),
1142
+ " l daemon logs".padEnd(innerWidth),
1143
+ " o open session dir".padEnd(innerWidth),
1144
+ " a spawn agent".padEnd(innerWidth),
1145
+ " m message agent".padEnd(innerWidth),
1146
+ " / search".padEnd(innerWidth),
1147
+ " ! shell command".padEnd(innerWidth),
1148
+ " j jump to pane".padEnd(innerWidth),
1149
+ " k kill session/agent".padEnd(innerWidth),
1150
+ " c companion overlay".padEnd(innerWidth),
1151
+ " q quit".padEnd(innerWidth),
1152
+ " ? help".padEnd(innerWidth),
1153
+ " 1-9 jump to session".padEnd(innerWidth),
1154
+ " ".padEnd(innerWidth),
1155
+ ansiDim(" esc dismiss".padEnd(innerWidth))
1156
+ ];
1157
+ for (let i = 0; i < lines.length; i++) {
1158
+ writeClipped(buf, x + 1, y + 1 + i, lines[i], innerWidth);
1159
+ }
1160
+ }
1161
+ function renderCopyMenuOverlay(buf, rows, cols) {
1162
+ const x = cols - LEADER_WIDTH - 1;
1163
+ const y = rows - COPY_HEIGHT - 2;
1164
+ const innerWidth = LEADER_WIDTH - 2;
1165
+ drawBorder(buf, x, y, LEADER_WIDTH, COPY_HEIGHT, "cyan");
1166
+ const lines = [
1167
+ ansiColor(" COPY".padEnd(innerWidth), "cyan", true),
1168
+ " ".padEnd(innerWidth),
1169
+ " p session path".padEnd(innerWidth),
1170
+ " C LLM context".padEnd(innerWidth),
1171
+ " l logs content".padEnd(innerWidth),
1172
+ " s session ID".padEnd(innerWidth),
1173
+ ansiDim(" esc cancel".padEnd(innerWidth))
1174
+ ];
1175
+ for (let i = 0; i < lines.length; i++) {
1176
+ writeClipped(buf, x + 1, y + 1 + i, lines[i], innerWidth);
1177
+ }
1178
+ }
1179
+ function renderHelpOverlay(buf, rows, cols) {
1180
+ const innerWidth = HELP_WIDTH - 2;
1181
+ const x = Math.max(0, Math.floor((cols - HELP_WIDTH) / 2));
1182
+ const contentLines = [
1183
+ helpRow(" hjkl/\u2191\u2193\u2190\u2192 navigate", " tab switch pane", innerWidth),
1184
+ helpRow(" enter expand/open", " t toggle logs", innerWidth),
1185
+ " ".padEnd(innerWidth),
1186
+ helpRow(" n new session", " m message orch.", innerWidth),
1187
+ helpRow(" R resume session", " C continue session", innerWidth),
1188
+ helpRow(" b rollback cycle", " x restart agent", innerWidth),
1189
+ helpRow(" r re-run agent", " g edit goal", innerWidth),
1190
+ helpRow(" p open roadmap", " s toggle strategy", innerWidth),
1191
+ helpRow(" S edit strategy", " w go to window", innerWidth),
1192
+ helpRow(" o resume claude session", " c claude companion", innerWidth),
1193
+ helpRow(" q quit", "", innerWidth),
1194
+ " ".padEnd(innerWidth),
1195
+ helpRow(" space \u2192 y copy submenu", " space \u2192 d delete session", innerWidth),
1196
+ helpRow(" space \u2192 j jump to pane", " space \u2192 k kill", innerWidth),
1197
+ helpRow(" space \u2192 q quit", " space \u2192 o open dir", innerWidth),
1198
+ helpRow(" space \u2192 l tail logs", " space \u2192 / search", innerWidth),
1199
+ helpRow(" space \u2192 a spawn agent", " space \u2192 m msg agent", innerWidth),
1200
+ helpRow(" space \u2192 ? help", " space \u2192 1-9 jump", innerWidth),
1201
+ " ".padEnd(innerWidth),
1202
+ helpRow(" y \u2192 p session path", " y \u2192 C LLM context", innerWidth),
1203
+ helpRow(" y \u2192 l logs content", " y \u2192 s session ID", innerWidth)
1204
+ ];
1205
+ const height = Math.min(contentLines.length + 4, rows - 2);
1206
+ const y = Math.max(0, Math.floor((rows - height) / 2));
1207
+ drawBorder(buf, x, y, HELP_WIDTH, height, "yellow");
1208
+ writeClipped(buf, x + 1, y + 1, ansiColor(" KEYBINDINGS (esc or ? to close)".padEnd(innerWidth), "yellow", true), innerWidth);
1209
+ writeClipped(buf, x + 1, y + 2, " ".padEnd(innerWidth), innerWidth);
1210
+ const availableContentRows = height - 4;
1211
+ for (let i = 0; i < Math.min(contentLines.length, availableContentRows); i++) {
1212
+ writeClipped(buf, x + 1, y + 3 + i, contentLines[i], innerWidth);
1213
+ }
1214
+ const trailingBlankRow = y + 3 + Math.min(contentLines.length, availableContentRows);
1215
+ if (trailingBlankRow < y + height - 1) {
1216
+ writeClipped(buf, x + 1, trailingBlankRow, " ".padEnd(innerWidth), innerWidth);
1217
+ }
1218
+ }
1219
+ var _page = "profile";
1220
+ var _prevPage = "profile";
1221
+ var _gallery = null;
1222
+ function companionOverlayNextPage() {
1223
+ if (_page === "help") {
1224
+ _page = _prevPage;
1225
+ return;
992
1226
  }
1227
+ _page = _page === "profile" ? "badges" : "profile";
1228
+ }
1229
+ function companionOverlayShowHelp() {
1230
+ if (_page !== "help") _prevPage = _page;
1231
+ _page = "help";
1232
+ }
1233
+ function companionOverlayDismissHelp() {
1234
+ _page = _prevPage;
1235
+ }
1236
+ function badgeGalleryLeft() {
1237
+ if (_gallery) _gallery.currentIndex = galleryPrev(_gallery);
1238
+ }
1239
+ function badgeGalleryRight() {
1240
+ if (_gallery) _gallery.currentIndex = galleryNext(_gallery);
1241
+ }
1242
+ function closeBadgeGallery() {
1243
+ _page = "profile";
1244
+ _gallery = null;
1245
+ _badgeScroll = 0;
1246
+ }
1247
+ function getCompanionPage() {
1248
+ return _page;
1249
+ }
1250
+ var MOOD_ICONS = {
1251
+ happy: "\u25C9",
1252
+ grinding: "\u25C8",
1253
+ frustrated: "\u25C6",
1254
+ zen: "\u25CE",
1255
+ sleepy: "\u25CC",
1256
+ excited: "\u2726",
1257
+ existential: "\u25C9"
1258
+ };
1259
+ var MOOD_COLORS = {
1260
+ happy: "green",
1261
+ grinding: "yellow",
1262
+ frustrated: "red",
1263
+ zen: "cyan",
1264
+ sleepy: "gray",
1265
+ excited: "white",
1266
+ existential: "magenta"
1267
+ };
1268
+ function statBar(value, max, width, color) {
1269
+ const filled = max > 0 ? Math.min(width, Math.round(value / max * width)) : 0;
1270
+ const bar = ansiColor("\u2593".repeat(filled), color) + ansiDim("\u2591".repeat(width - filled));
1271
+ return bar;
1272
+ }
1273
+ function wrapText2(text, maxWidth) {
1274
+ const words = text.split(" ");
1275
+ const lines = [];
1276
+ let current = "";
1277
+ for (const word of words) {
1278
+ if (current.length + word.length + 1 > maxWidth && current.length > 0) {
1279
+ lines.push(current);
1280
+ current = word;
1281
+ } else {
1282
+ current = current.length > 0 ? `${current} ${word}` : word;
1283
+ }
1284
+ }
1285
+ if (current.length > 0) lines.push(current);
1286
+ return lines;
1287
+ }
1288
+ function renderProfilePage(buf, rows, cols, companion) {
1289
+ const innerWidth = COMPANION_WIDTH - 2;
1290
+ const x = Math.max(0, Math.floor((cols - COMPANION_WIDTH) / 2));
1291
+ const unlockedCount = companion.achievements.length;
1292
+ const totalAchievements = ACHIEVEMENTS.length;
1293
+ const endH = Math.floor(companion.stats.endurance / 36e5);
1294
+ const face = getMoodFace(companion.mood);
1295
+ const moodColor = MOOD_COLORS[companion.mood];
1296
+ const moodIcon = MOOD_ICONS[companion.mood];
1297
+ const faceColored = ansiColor(`(${face})`, moodColor, true);
1298
+ const repoNicknames = Object.values(companion.repos).map((r) => r.nickname).filter((n) => n !== null);
1299
+ const repoDisplay = repoNicknames.length > 0 ? ansiDim(` "${repoNicknames[0]}"`) : "";
1300
+ const barW = 18;
1301
+ const xpInLevel = companion.xp % 50;
1302
+ const xpBar = statBar(xpInLevel, 50, 20, "cyan");
1303
+ const lastAchievement = companion.achievements.length > 0 ? companion.achievements[companion.achievements.length - 1] : null;
1304
+ const lastDef = lastAchievement ? ACHIEVEMENTS.find((a) => a.id === lastAchievement.id) : null;
1305
+ const contentLines = [];
1306
+ contentLines.push(` ${faceColored} .${repoDisplay}`.padEnd(innerWidth));
1307
+ contentLines.push(" ".padEnd(innerWidth));
1308
+ contentLines.push(` ${ansiColor(`Lv ${companion.level}`, "cyan", true)} ${ansiBold(companion.title)}`.padEnd(innerWidth));
1309
+ contentLines.push(` ${xpBar} ${ansiDim(`${companion.xp} xp`)}`.padEnd(innerWidth));
1310
+ contentLines.push(" ".padEnd(innerWidth));
1311
+ contentLines.push(` ${ansiColor(moodIcon, moodColor)} ${ansiColor(companion.mood, moodColor)}`.padEnd(innerWidth));
1312
+ contentLines.push(" ".padEnd(innerWidth));
1313
+ contentLines.push(` ${ansiColor("STR", "red")} ${String(companion.stats.strength).padStart(4)} ${statBar(companion.stats.strength, 100, barW, "red")}`.padEnd(innerWidth));
1314
+ contentLines.push(` ${ansiColor("END", "yellow")} ${String(endH + "h").padStart(4)} ${statBar(endH, 500, barW, "yellow")}`.padEnd(innerWidth));
1315
+ contentLines.push(` ${ansiColor("WIS", "blue")} ${String(companion.stats.wisdom).padStart(4)} ${statBar(companion.stats.wisdom, 50, barW, "blue")}`.padEnd(innerWidth));
1316
+ contentLines.push(` ${ansiColor("PAT", "magenta")} ${String(companion.stats.patience).padStart(4)} ${statBar(companion.stats.patience, 200, barW, "magenta")}`.padEnd(innerWidth));
1317
+ contentLines.push(" ".padEnd(innerWidth));
1318
+ if (lastDef) {
1319
+ contentLines.push(` ${ansiColor("\u2605", "yellow")} ${lastDef.name} ${ansiDim(`${unlockedCount}/${totalAchievements}`)}`.padEnd(innerWidth));
1320
+ } else {
1321
+ contentLines.push(` ${ansiDim(`\u25C7 ${unlockedCount}/${totalAchievements} achievements`)}`.padEnd(innerWidth));
1322
+ }
1323
+ contentLines.push(" ".padEnd(innerWidth));
1324
+ if (companion.lastCommentary) {
1325
+ const raw = companion.lastCommentary.text;
1326
+ const wrapped = wrapText2(raw, innerWidth - 6);
1327
+ contentLines.push(` ${ansiDim("\u250A")} ${ansiColor(wrapped[0] ?? "", "white")}`.padEnd(innerWidth));
1328
+ for (let i = 1; i < wrapped.length; i++) {
1329
+ contentLines.push(` ${ansiDim("\u250A")} ${ansiColor(wrapped[i] ?? "", "white")}`.padEnd(innerWidth));
1330
+ }
1331
+ }
1332
+ contentLines.push(" ".padEnd(innerWidth));
1333
+ contentLines.push(` ${ansiDim("tab \u2192 badges ? \u2192 stat guide")}`.padEnd(innerWidth));
1334
+ const height = Math.min(contentLines.length + 4, rows - 2);
1335
+ const y = Math.max(0, Math.floor((rows - height) / 2));
1336
+ drawBorder(buf, x, y, COMPANION_WIDTH, height, "cyan");
1337
+ writeClipped(buf, x + 1, y + 1, ansiColor(" COMPANION (esc to close)".padEnd(innerWidth), "cyan", true), innerWidth);
1338
+ writeClipped(buf, x + 1, y + 2, " ".padEnd(innerWidth), innerWidth);
1339
+ const availableContentRows = height - 4;
1340
+ for (let i = 0; i < Math.min(contentLines.length, availableContentRows); i++) {
1341
+ writeClipped(buf, x + 1, y + 3 + i, contentLines[i], innerWidth);
1342
+ }
1343
+ }
1344
+ var GALLERY_WIDTH = 50;
1345
+ var _badgeScroll = 0;
1346
+ function badgeListScrollUp() {
1347
+ _badgeScroll = Math.max(0, _badgeScroll - 1);
1348
+ }
1349
+ function badgeListScrollDown() {
1350
+ _badgeScroll++;
1351
+ }
1352
+ function renderBadgesPage(buf, rows, cols, companion) {
1353
+ if (!_gallery) _gallery = createBadgeGallery(companion.achievements);
1354
+ const gallery = _gallery;
1355
+ const innerWidth = GALLERY_WIDTH - 2;
1356
+ const x = Math.max(0, Math.floor((cols - GALLERY_WIDTH) / 2));
1357
+ const unlockedCount = companion.achievements.length;
1358
+ const totalAchievements = ACHIEVEMENTS.length;
1359
+ const currentDef = gallery.achievements[gallery.currentIndex];
1360
+ const currentUnlock = gallery.unlocked.get(currentDef.id) ?? null;
1361
+ const card = renderBadgeCard(currentDef, currentUnlock);
1362
+ const contentLines = [];
1363
+ for (const cardLine of card.lines) {
1364
+ const stripped = cardLine.replace(/\x1b\[[0-9;]*m/g, "");
1365
+ const pad = Math.max(0, Math.floor((innerWidth - stripped.length) / 2));
1366
+ const padded = " ".repeat(pad) + cardLine + " ".repeat(Math.max(0, innerWidth - stripped.length - pad));
1367
+ contentLines.push(padded);
1368
+ }
1369
+ contentLines.push(" ".padEnd(innerWidth));
1370
+ const navIdx = gallery.currentIndex + 1;
1371
+ const navTotal = gallery.total;
1372
+ const unlockLabel = currentUnlock !== null ? " \u2713 unlocked" : " \xB7 locked";
1373
+ const navLine = ` \u2190 ${navIdx}/${navTotal} \u2192 ${unlockedCount}/${totalAchievements} earned${unlockLabel}`;
1374
+ contentLines.push(navLine.padEnd(innerWidth));
1375
+ contentLines.push(" ".padEnd(innerWidth));
1376
+ const listStartIdx = contentLines.length;
1377
+ const maxListRows = Math.min(6, Math.max(4, rows - 2 - 4 - listStartIdx - 2));
1378
+ const maxScroll = Math.max(0, gallery.total - maxListRows + 1);
1379
+ if (_badgeScroll > maxScroll) _badgeScroll = maxScroll;
1380
+ if (gallery.currentIndex < _badgeScroll) _badgeScroll = gallery.currentIndex;
1381
+ for (let pass = 0; pass < 3; pass++) {
1382
+ const a = _badgeScroll > 0 ? 1 : 0;
1383
+ const b = _badgeScroll + maxListRows < gallery.total ? 1 : 0;
1384
+ const vis = maxListRows - a - b;
1385
+ if (gallery.currentIndex >= _badgeScroll + vis) {
1386
+ _badgeScroll = gallery.currentIndex - vis + 1;
1387
+ } else break;
1388
+ }
1389
+ if (_badgeScroll > maxScroll) _badgeScroll = maxScroll;
1390
+ const hasMoreAbove = _badgeScroll > 0;
1391
+ const hasMoreBelow = _badgeScroll + maxListRows < gallery.total;
1392
+ const itemRows = maxListRows - (hasMoreAbove ? 1 : 0) - (hasMoreBelow ? 1 : 0);
1393
+ if (hasMoreAbove) {
1394
+ contentLines.push(ansiDim(` \u2191 ${_badgeScroll} more`.padEnd(innerWidth)));
1395
+ }
1396
+ for (let i = 0; i < itemRows && _badgeScroll + i < gallery.total; i++) {
1397
+ const idx = _badgeScroll + i;
1398
+ const def = gallery.achievements[idx];
1399
+ const u = gallery.unlocked.has(def.id);
1400
+ const icon = u ? "\u2713" : "\xB7";
1401
+ const isCurrent = idx === gallery.currentIndex;
1402
+ let line = ` ${icon} ${def.name}`.padEnd(innerWidth);
1403
+ if (isCurrent) line = ansiColor(line, "cyan", false);
1404
+ else if (!u) line = ansiDim(line);
1405
+ contentLines.push(line);
1406
+ }
1407
+ if (hasMoreBelow) {
1408
+ const below = gallery.total - _badgeScroll - itemRows;
1409
+ contentLines.push(ansiDim(` \u2193 ${below} more`.padEnd(innerWidth)));
1410
+ }
1411
+ contentLines.push(" ".padEnd(innerWidth));
1412
+ contentLines.push(ansiDim(" tab \u2192 profile ? \u2192 stat guide".padEnd(innerWidth)));
1413
+ const height = Math.min(contentLines.length + 4, rows - 2);
1414
+ const y = Math.max(0, Math.floor((rows - height) / 2));
1415
+ drawBorder(buf, x, y, GALLERY_WIDTH, height, "cyan");
1416
+ writeClipped(buf, x + 1, y + 1, ansiColor(" BADGES (\u2191\u2193 navigate, esc close)".padEnd(innerWidth), "cyan", true), innerWidth);
1417
+ writeClipped(buf, x + 1, y + 2, " ".padEnd(innerWidth), innerWidth);
1418
+ const availableContentRows = height - 4;
1419
+ for (let i = 0; i < Math.min(contentLines.length, availableContentRows); i++) {
1420
+ writeClipped(buf, x + 1, y + 3 + i, contentLines[i], innerWidth);
1421
+ }
1422
+ }
1423
+ function renderHelpPage(buf, rows, cols) {
1424
+ const innerWidth = COMPANION_WIDTH - 2;
1425
+ const x = Math.max(0, Math.floor((cols - COMPANION_WIDTH) / 2));
1426
+ const divider2 = (label, color) => {
1427
+ const rest = innerWidth - label.length - 5;
1428
+ return ` ${ansiColor(label, color, true)} ${ansiDim("\u2500".repeat(Math.max(0, rest)))}`;
1429
+ };
1430
+ const contentLines = [];
1431
+ contentLines.push(divider2("STR (Strength)", "red"));
1432
+ contentLines.push(" +1 per completed session".padEnd(innerWidth));
1433
+ contentLines.push(" ".padEnd(innerWidth));
1434
+ contentLines.push(divider2("END (Endurance)", "yellow"));
1435
+ contentLines.push(" Total active time across sessions".padEnd(innerWidth));
1436
+ contentLines.push(" (displayed in hours)".padEnd(innerWidth));
1437
+ contentLines.push(" ".padEnd(innerWidth));
1438
+ contentLines.push(divider2("WIS (Wisdom)", "blue"));
1439
+ contentLines.push(" +1 per efficient session".padEnd(innerWidth));
1440
+ contentLines.push(ansiDim(" agents have <30% stddev in active").padEnd(innerWidth));
1441
+ contentLines.push(ansiDim(" time, 2+ agents required").padEnd(innerWidth));
1442
+ contentLines.push(" ".padEnd(innerWidth));
1443
+ contentLines.push(divider2("PAT (Patience)", "magenta"));
1444
+ contentLines.push(" +cycles per completed session".padEnd(innerWidth));
1445
+ contentLines.push(ansiDim(" +3 if validation mode").padEnd(innerWidth));
1446
+ contentLines.push(ansiDim(" +2 if completion mode").padEnd(innerWidth));
1447
+ contentLines.push(" ".padEnd(innerWidth));
1448
+ contentLines.push(divider2("XP & Level", "cyan"));
1449
+ contentLines.push(" STR\xD780 + END/h\xD715 + WIS\xD740".padEnd(innerWidth));
1450
+ contentLines.push(" + PAT\xD75".padEnd(innerWidth));
1451
+ contentLines.push(ansiDim(" level: 150 base xp, \xD71.35/lvl").padEnd(innerWidth));
1452
+ contentLines.push(" ".padEnd(innerWidth));
1453
+ contentLines.push(divider2("Mood", "white"));
1454
+ contentLines.push(" Real-time scoring from signals:".padEnd(innerWidth));
1455
+ contentLines.push(ansiDim(" time of day, idle time, crashes,").padEnd(innerWidth));
1456
+ contentLines.push(ansiDim(" streaks, session length, agents").padEnd(innerWidth));
1457
+ contentLines.push(" ".padEnd(innerWidth));
1458
+ contentLines.push(divider2("Badges", "yellow"));
1459
+ contentLines.push(" Milestones, session feats, time".padEnd(innerWidth));
1460
+ contentLines.push(" patterns, and behavioral checks".padEnd(innerWidth));
1461
+ contentLines.push(" ".padEnd(innerWidth));
1462
+ contentLines.push(ansiDim(" tab \u2192 back ? \u2192 close".padEnd(innerWidth)));
1463
+ const height = Math.min(contentLines.length + 4, rows - 2);
1464
+ const y = Math.max(0, Math.floor((rows - height) / 2));
1465
+ drawBorder(buf, x, y, COMPANION_WIDTH, height, "cyan");
1466
+ writeClipped(buf, x + 1, y + 1, ansiColor(" STAT GUIDE (? or esc to close)".padEnd(innerWidth), "cyan", true), innerWidth);
1467
+ writeClipped(buf, x + 1, y + 2, " ".padEnd(innerWidth), innerWidth);
1468
+ const availableContentRows = height - 4;
1469
+ for (let i = 0; i < Math.min(contentLines.length, availableContentRows); i++) {
1470
+ writeClipped(buf, x + 1, y + 3 + i, contentLines[i], innerWidth);
1471
+ }
1472
+ const trailingBlankRow = y + 3 + Math.min(contentLines.length, availableContentRows);
1473
+ if (trailingBlankRow < y + height - 1) {
1474
+ writeClipped(buf, x + 1, trailingBlankRow, " ".padEnd(innerWidth), innerWidth);
1475
+ }
1476
+ }
1477
+ function renderCompanionOverlay(buf, rows, cols, companion) {
1478
+ if (_page === "help") {
1479
+ renderHelpPage(buf, rows, cols);
1480
+ } else if (_page === "badges") {
1481
+ renderBadgesPage(buf, rows, cols, companion);
1482
+ } else {
1483
+ renderProfilePage(buf, rows, cols, companion);
1484
+ }
1485
+ }
1486
+ function renderCompanionDebugOverlay(buf, rows, cols, companion) {
1487
+ const innerWidth = DEBUG_WIDTH - 2;
1488
+ const x = Math.max(0, Math.floor((cols - DEBUG_WIDTH) / 2));
1489
+ const face = getMoodFace(companion.mood);
1490
+ const debug = companion.debugMood;
1491
+ const contentLines = [
1492
+ ` (${face}) mood: ${companion.mood}`.padEnd(innerWidth),
1493
+ " ".padEnd(innerWidth)
1494
+ ];
1495
+ if (debug) {
1496
+ const { signals, scores } = debug;
1497
+ contentLines.push(ansiDim(" \u2500\u2500 Signals \u2500\u2500".padEnd(innerWidth)));
1498
+ contentLines.push(` hourOfDay: ${signals.hourOfDay}`.padEnd(innerWidth));
1499
+ contentLines.push(` sessionLengthMs: ${signals.sessionLengthMs} (${Math.round(signals.sessionLengthMs / 6e4)}min)`.padEnd(innerWidth));
1500
+ contentLines.push(` idleDurationMs: ${signals.idleDurationMs} (${Math.round(signals.idleDurationMs / 6e4)}min)`.padEnd(innerWidth));
1501
+ contentLines.push(` recentCrashes: ${signals.recentCrashes}`.padEnd(innerWidth));
1502
+ contentLines.push(` cleanStreak: ${signals.cleanStreak}`.padEnd(innerWidth));
1503
+ contentLines.push(` justCompleted: ${signals.justCompleted}`.padEnd(innerWidth));
1504
+ contentLines.push(` justCrashed: ${signals.justCrashed}`.padEnd(innerWidth));
1505
+ contentLines.push(` justLeveledUp: ${signals.justLeveledUp}`.padEnd(innerWidth));
1506
+ contentLines.push(" ".padEnd(innerWidth));
1507
+ contentLines.push(ansiDim(" \u2500\u2500 Scores \u2500\u2500".padEnd(innerWidth)));
1508
+ const moodOrder = ["happy", "grinding", "frustrated", "zen", "sleepy", "excited", "existential"];
1509
+ for (const mood of moodOrder) {
1510
+ const score = scores[mood] ?? 0;
1511
+ const bar = score > 0 ? ansiDim("\u2588".repeat(Math.min(Math.round(score / 5), 12))) : "";
1512
+ const marker = mood === debug.winner ? " \u25C0" : "";
1513
+ contentLines.push(` ${mood.padEnd(12)} ${String(score).padStart(3)} ${bar}${marker}`.padEnd(innerWidth));
1514
+ }
1515
+ } else {
1516
+ contentLines.push(ansiDim(" No mood signals yet".padEnd(innerWidth)));
1517
+ contentLines.push(ansiDim(" (mood is time-of-day only)".padEnd(innerWidth)));
1518
+ }
1519
+ contentLines.push(" ".padEnd(innerWidth));
1520
+ const height = Math.min(contentLines.length + 4, rows - 2);
1521
+ const y = Math.max(0, Math.floor((rows - height) / 2));
1522
+ drawBorder(buf, x, y, DEBUG_WIDTH, height, "yellow");
1523
+ writeClipped(buf, x + 1, y + 1, ansiColor(" COMPANION DEBUG (esc to close)".padEnd(innerWidth), "yellow", true), innerWidth);
1524
+ writeClipped(buf, x + 1, y + 2, " ".padEnd(innerWidth), innerWidth);
1525
+ const availableContentRows = height - 4;
1526
+ for (let i = 0; i < Math.min(contentLines.length, availableContentRows); i++) {
1527
+ writeClipped(buf, x + 1, y + 3 + i, contentLines[i], innerWidth);
1528
+ }
1529
+ const trailingRow = y + 3 + Math.min(contentLines.length, availableContentRows);
1530
+ if (trailingRow < y + height - 1) {
1531
+ writeClipped(buf, x + 1, trailingRow, " ".padEnd(innerWidth), innerWidth);
1532
+ }
1533
+ }
1534
+
1535
+ // src/tui/input.ts
1536
+ function activateNvimBypass(state2) {
1537
+ setRawBypass((data) => {
1538
+ if (!state2.nvimBridge?.ready) {
1539
+ deactivateNvimBypass();
1540
+ state2.focusPane = "tree";
1541
+ if (state2.mode === "compose") cancelCompose(state2);
1542
+ requestRender();
1543
+ return false;
1544
+ }
1545
+ if (data === " ") {
1546
+ if (state2.mode === "compose") {
1547
+ cancelCompose(state2);
1548
+ return true;
1549
+ }
1550
+ deactivateNvimBypass();
1551
+ state2.focusPane = state2.showCombinedView ? "logs" : "tree";
1552
+ requestRender();
1553
+ return true;
1554
+ }
1555
+ state2.nvimBridge.write(data);
1556
+ return true;
1557
+ });
1558
+ }
1559
+ function deactivateNvimBypass() {
1560
+ setRawBypass(null);
1561
+ }
1562
+ var COMPOSE_DIR = join2(tmpdir(), "sisyphus-nvim");
1563
+ function enterComposeMode(state2, action, actions) {
1564
+ if (!state2.nvimEnabled || !state2.nvimBridge?.ready) return false;
1565
+ mkdirSync(COMPOSE_DIR, { recursive: true });
1566
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1567
+ const tempFile = join2(COMPOSE_DIR, `compose-${id}.md`);
1568
+ const signalFile = join2(COMPOSE_DIR, `compose-signal-${id}`);
1569
+ writeFileSync(tempFile, "", "utf-8");
1570
+ state2.composePrevNvimFile = state2.prevNvimFile;
1571
+ state2.composeAction = action;
1572
+ state2.composeTempFile = tempFile;
1573
+ state2.composeSignalFile = signalFile;
1574
+ state2.mode = "compose";
1575
+ state2.focusPane = "detail";
1576
+ state2.nvimBridge.openComposeFile(tempFile, signalFile);
1577
+ activateNvimBypass(state2);
1578
+ state2.composePollTimer = setInterval(() => {
1579
+ checkComposeSignal(state2, actions);
1580
+ }, 100);
1581
+ requestRender();
1582
+ return true;
993
1583
  }
994
- function handleCancel(state2) {
1584
+ function cancelCompose(state2) {
1585
+ if (state2.composePollTimer !== null) {
1586
+ clearInterval(state2.composePollTimer);
1587
+ state2.composePollTimer = null;
1588
+ }
1589
+ if (state2.composeTempFile) {
1590
+ try {
1591
+ unlinkSync(state2.composeTempFile);
1592
+ } catch {
1593
+ }
1594
+ }
1595
+ if (state2.composeSignalFile) {
1596
+ try {
1597
+ unlinkSync(state2.composeSignalFile);
1598
+ } catch {
1599
+ }
1600
+ }
1601
+ state2.prevNvimFile = null;
1602
+ state2.composePrevNvimFile = null;
1603
+ state2.composeAction = null;
1604
+ state2.composeTempFile = null;
1605
+ state2.composeSignalFile = null;
995
1606
  state2.mode = "navigate";
996
- state2.targetAgentId = null;
997
- state2.inputText = "";
998
- state2.inputCursorPos = 0;
1607
+ state2.focusPane = "tree";
1608
+ deactivateNvimBypass();
999
1609
  requestRender();
1000
1610
  }
1001
- function expandSessionLatestCycle(state2, node) {
1002
- if (node.type === "session" && state2.selectedSession?.id === node.sessionId) {
1003
- const cycles = state2.selectedSession.orchestratorCycles;
1004
- if (cycles.length > 0) {
1005
- const latest = cycles[cycles.length - 1];
1006
- state2.expanded.add(`cycle:${node.sessionId}:${latest.cycle}`);
1611
+ function checkComposeSignal(state2, actions) {
1612
+ if (!state2.composeSignalFile || !state2.composeAction) return;
1613
+ if (!state2.nvimBridge?.ready) {
1614
+ cancelCompose(state2);
1615
+ return;
1616
+ }
1617
+ if (!existsSync(state2.composeSignalFile)) return;
1618
+ let signalContent = "";
1619
+ try {
1620
+ signalContent = readFileSync(state2.composeSignalFile, "utf-8").trim();
1621
+ } catch {
1622
+ }
1623
+ if (signalContent === "cancel") {
1624
+ cancelCompose(state2);
1625
+ return;
1626
+ }
1627
+ let content = "";
1628
+ if (state2.composeTempFile) {
1629
+ try {
1630
+ content = readFileSync(state2.composeTempFile, "utf-8").trim();
1631
+ } catch {
1632
+ }
1633
+ }
1634
+ const action = state2.composeAction;
1635
+ const required = !OPTIONAL_COMPOSE.has(action.kind);
1636
+ if (required && !content) {
1637
+ try {
1638
+ unlinkSync(state2.composeSignalFile);
1639
+ } catch {
1007
1640
  }
1641
+ notify(state2, "Content required");
1642
+ return;
1008
1643
  }
1644
+ dispatchComposeAction(action, content, state2, actions);
1645
+ cancelCompose(state2);
1009
1646
  }
1010
- async function handleSubmit(text, state2, actions) {
1011
- const selectedSessionId = state2.selectedSessionId;
1012
- switch (state2.mode) {
1013
- case "resume": {
1014
- if (!selectedSessionId) break;
1647
+ function dispatchComposeAction(action, content, state2, actions) {
1648
+ switch (action.kind) {
1649
+ case "new-session":
1015
1650
  actions.sendAndNotify(
1016
- { type: "resume", sessionId: selectedSessionId, cwd: state2.cwd, message: text || void 0 },
1017
- "Session resumed"
1651
+ { type: "start", task: content, cwd: state2.cwd },
1652
+ "Session created"
1018
1653
  );
1019
1654
  break;
1020
- }
1021
- case "continue": {
1022
- if (!selectedSessionId) break;
1023
- try {
1024
- const contRes = await actions.send({ type: "continue", sessionId: selectedSessionId });
1025
- if (!contRes.ok) {
1026
- notify(state2, `Error: ${contRes.error}`);
1027
- break;
1028
- }
1029
- actions.sendAndNotify(
1030
- { type: "resume", sessionId: selectedSessionId, cwd: state2.cwd, message: text || void 0 },
1031
- "Session continued"
1032
- );
1033
- } catch (err) {
1034
- notify(state2, `Error: ${err.message}`);
1035
- }
1036
- break;
1037
- }
1038
- case "rollback": {
1039
- if (!selectedSessionId) break;
1040
- const toCycle = parseInt(text, 10);
1041
- if (isNaN(toCycle) || toCycle < 1) {
1042
- notify(state2, "Invalid cycle number");
1043
- break;
1044
- }
1655
+ case "message-orchestrator":
1045
1656
  actions.sendAndNotify(
1046
- { type: "rollback", sessionId: selectedSessionId, cwd: state2.cwd, toCycle },
1047
- `Rolled back to cycle ${toCycle} \u2014 use [R]esume to respawn`
1657
+ { type: "message", sessionId: action.sessionId, content },
1658
+ "Message queued"
1048
1659
  );
1049
1660
  break;
1050
- }
1051
- case "delete-confirm": {
1052
- if (!selectedSessionId) break;
1053
- if (text !== "yes") {
1054
- notify(state2, 'Delete cancelled (type "yes" to confirm)');
1055
- break;
1056
- }
1661
+ case "resume":
1057
1662
  actions.sendAndNotify(
1058
- { type: "delete", sessionId: selectedSessionId, cwd: state2.cwd },
1059
- "Session deleted"
1663
+ { type: "resume", sessionId: action.sessionId, cwd: state2.cwd, message: content || void 0 },
1664
+ "Session resumed"
1060
1665
  );
1061
1666
  break;
1062
- }
1063
- case "spawn-agent": {
1064
- if (!selectedSessionId) break;
1065
- if (!text.trim()) {
1066
- notify(state2, "Instruction required");
1067
- break;
1068
- }
1667
+ case "continue":
1668
+ void (async () => {
1669
+ try {
1670
+ const contRes = await actions.send({ type: "continue", sessionId: action.sessionId });
1671
+ if (!contRes.ok) {
1672
+ notify(state2, `Error: ${contRes.error}`);
1673
+ return;
1674
+ }
1675
+ actions.sendAndNotify(
1676
+ { type: "resume", sessionId: action.sessionId, cwd: state2.cwd, message: content || void 0 },
1677
+ "Session continued"
1678
+ );
1679
+ } catch (err) {
1680
+ notify(state2, `Error: ${err.message}`);
1681
+ }
1682
+ })();
1683
+ break;
1684
+ case "spawn-agent":
1069
1685
  actions.sendAndNotify(
1070
1686
  {
1071
1687
  type: "spawn",
1072
- sessionId: selectedSessionId,
1688
+ sessionId: action.sessionId,
1073
1689
  agentType: "default",
1074
1690
  name: "agent",
1075
- instruction: text
1691
+ instruction: content
1076
1692
  },
1077
1693
  "Agent spawned"
1078
1694
  );
1079
1695
  break;
1080
- }
1081
- case "search": {
1082
- state2.searchFilter = text.trim() || null;
1083
- break;
1084
- }
1085
- case "message-agent": {
1086
- if (!selectedSessionId || !state2.targetAgentId) break;
1696
+ case "message-agent":
1087
1697
  actions.sendAndNotify(
1088
- { type: "message", sessionId: selectedSessionId, content: text, source: { type: "agent", agentId: state2.targetAgentId } },
1089
- `Message sent to ${state2.targetAgentId}`
1698
+ { type: "message", sessionId: action.sessionId, content, source: { type: "agent", agentId: action.agentId } },
1699
+ `Message sent to ${action.agentId}`
1090
1700
  );
1091
- state2.targetAgentId = null;
1092
- break;
1093
- }
1094
- case "shell-command": {
1095
- if (!text.trim()) break;
1096
- try {
1097
- actions.openShellPopup(state2.cwd, text);
1098
- } catch {
1099
- notify(state2, "Failed to run shell command");
1100
- }
1101
1701
  break;
1102
- }
1103
1702
  }
1104
- state2.mode = "navigate";
1105
- state2.inputText = "";
1106
- state2.inputCursorPos = 0;
1107
- requestRender();
1108
1703
  }
1109
- function handleInputBarKey(input, key, state2, actions) {
1110
- if (key.return) {
1111
- if (OPTIONAL_INPUT.has(state2.mode) || state2.inputText.trim()) {
1112
- void handleSubmit(state2.inputText.trim(), state2, actions);
1113
- }
1114
- return;
1115
- }
1116
- if (key.escape) {
1117
- handleCancel(state2);
1118
- return;
1119
- }
1120
- if (key.leftArrow) {
1121
- state2.inputCursorPos = Math.max(0, state2.inputCursorPos - 1);
1122
- requestRender();
1123
- return;
1124
- }
1125
- if (key.rightArrow) {
1126
- state2.inputCursorPos = Math.min(state2.inputText.length, state2.inputCursorPos + 1);
1127
- requestRender();
1128
- return;
1129
- }
1130
- if (key.ctrl && input === "a") {
1131
- state2.inputCursorPos = 0;
1132
- requestRender();
1133
- return;
1134
- }
1135
- if (key.ctrl && input === "e") {
1136
- state2.inputCursorPos = state2.inputText.length;
1137
- requestRender();
1138
- return;
1139
- }
1140
- if (key.ctrl && input === "k") {
1141
- state2.inputText = state2.inputText.slice(0, state2.inputCursorPos);
1142
- requestRender();
1143
- return;
1144
- }
1145
- if (key.ctrl && input === "u") {
1146
- state2.inputText = state2.inputText.slice(state2.inputCursorPos);
1147
- state2.inputCursorPos = 0;
1148
- requestRender();
1149
- return;
1150
- }
1151
- if (key.backspace) {
1152
- if (state2.inputCursorPos > 0) {
1153
- state2.inputText = state2.inputText.slice(0, state2.inputCursorPos - 1) + state2.inputText.slice(state2.inputCursorPos);
1154
- state2.inputCursorPos -= 1;
1155
- requestRender();
1156
- }
1157
- return;
1158
- }
1159
- if (key.delete) {
1160
- if (state2.inputCursorPos < state2.inputText.length) {
1161
- state2.inputText = state2.inputText.slice(0, state2.inputCursorPos) + state2.inputText.slice(state2.inputCursorPos + 1);
1162
- requestRender();
1704
+ function expandSessionLatestCycle(state2, node) {
1705
+ if (node.type === "session" && state2.selectedSession?.id === node.sessionId) {
1706
+ const cycles = state2.selectedSession.orchestratorCycles;
1707
+ if (cycles.length > 0) {
1708
+ const latest = cycles[cycles.length - 1];
1709
+ state2.expanded.add(`cycle:${node.sessionId}:${latest.cycle}`);
1163
1710
  }
1164
- return;
1165
- }
1166
- if (input && !key.ctrl && !key.meta) {
1167
- state2.inputText = state2.inputText.slice(0, state2.inputCursorPos) + input + state2.inputText.slice(state2.inputCursorPos);
1168
- state2.inputCursorPos += input.length;
1169
- requestRender();
1170
1711
  }
1171
1712
  }
1172
- function handleReportDetailKey(input, key, state2, actions) {
1713
+ function handleReportDetailKey(input, key, state2, _actions) {
1173
1714
  if (key.escape || key.return) {
1174
- handleCancel(state2);
1715
+ state2.mode = "navigate";
1716
+ requestRender();
1175
1717
  return;
1176
1718
  }
1177
1719
  if (key.upArrow) {
@@ -1253,9 +1795,18 @@ function handleLeaderAction(action, state2, actions) {
1253
1795
  notify(state2, "No session selected");
1254
1796
  break;
1255
1797
  }
1256
- state2.mode = "delete-confirm";
1257
- requestRender();
1258
- return;
1798
+ const editor = actions.resolveEditor();
1799
+ try {
1800
+ const text = actions.editInPopup(state2.cwd, editor);
1801
+ if (text?.trim() === "yes") {
1802
+ actions.sendAndNotify({ type: "delete", sessionId: selectedSessionId, cwd: state2.cwd }, "Session deleted");
1803
+ } else {
1804
+ notify(state2, 'Delete cancelled (type "yes" to confirm)');
1805
+ }
1806
+ } catch {
1807
+ notify(state2, "Failed to open editor");
1808
+ }
1809
+ break;
1259
1810
  }
1260
1811
  case "open-logs": {
1261
1812
  try {
@@ -1277,10 +1828,12 @@ function handleLeaderAction(action, state2, actions) {
1277
1828
  }
1278
1829
  break;
1279
1830
  }
1280
- case "search":
1831
+ case "search": {
1281
1832
  state2.mode = "search";
1833
+ state2.searchText = "";
1282
1834
  requestRender();
1283
1835
  return;
1836
+ }
1284
1837
  case "jump-to-session": {
1285
1838
  let count = 0;
1286
1839
  for (let i = 0; i < nodes.length; i++) {
@@ -1301,9 +1854,21 @@ function handleLeaderAction(action, state2, actions) {
1301
1854
  break;
1302
1855
  }
1303
1856
  if (enterComposeMode(state2, { kind: "spawn-agent", sessionId: selectedSessionId }, actions)) return;
1304
- state2.mode = "spawn-agent";
1305
- requestRender();
1306
- return;
1857
+ {
1858
+ const editor = actions.resolveEditor();
1859
+ try {
1860
+ const content = actions.editInPopup(state2.cwd, editor);
1861
+ if (content?.trim()) {
1862
+ actions.sendAndNotify(
1863
+ { type: "spawn", sessionId: selectedSessionId, agentType: "default", name: "agent", instruction: content },
1864
+ "Agent spawned"
1865
+ );
1866
+ }
1867
+ } catch {
1868
+ notify(state2, "Failed to open editor");
1869
+ }
1870
+ }
1871
+ break;
1307
1872
  }
1308
1873
  case "message-agent": {
1309
1874
  const agent = actions.getAgentForNode(cursorNode);
@@ -1312,23 +1877,49 @@ function handleLeaderAction(action, state2, actions) {
1312
1877
  break;
1313
1878
  }
1314
1879
  if (enterComposeMode(state2, { kind: "message-agent", sessionId: selectedSessionId, agentId: agent.id }, actions)) return;
1315
- state2.targetAgentId = agent.id;
1316
- state2.mode = "message-agent";
1317
- requestRender();
1318
- return;
1880
+ {
1881
+ const editor = actions.resolveEditor();
1882
+ try {
1883
+ const content = actions.editInPopup(state2.cwd, editor);
1884
+ if (content?.trim()) {
1885
+ actions.sendAndNotify(
1886
+ { type: "message", sessionId: selectedSessionId, content, source: { type: "agent", agentId: agent.id } },
1887
+ `Message sent to ${agent.id}`
1888
+ );
1889
+ }
1890
+ } catch {
1891
+ notify(state2, "Failed to open editor");
1892
+ }
1893
+ }
1894
+ break;
1319
1895
  }
1320
1896
  case "help":
1321
1897
  state2.mode = "help";
1322
1898
  requestRender();
1323
1899
  return;
1900
+ case "companion-overlay":
1901
+ state2.mode = "companion-overlay";
1902
+ requestRender();
1903
+ return;
1904
+ case "companion-debug":
1905
+ state2.mode = "companion-debug";
1906
+ requestRender();
1907
+ return;
1324
1908
  case "shell-command": {
1325
1909
  if (!selectedSessionId) {
1326
1910
  notify(state2, "No session selected");
1327
1911
  break;
1328
1912
  }
1329
- state2.mode = "shell-command";
1330
- requestRender();
1331
- return;
1913
+ const editor = actions.resolveEditor();
1914
+ try {
1915
+ const text = actions.editInPopup(state2.cwd, editor);
1916
+ if (text?.trim()) {
1917
+ actions.openShellPopup(state2.cwd, text.trim());
1918
+ }
1919
+ } catch {
1920
+ notify(state2, "Failed to open editor");
1921
+ }
1922
+ break;
1332
1923
  }
1333
1924
  case "jump-to-pane": {
1334
1925
  const agent = actions.getAgentForNode(cursorNode);
@@ -1364,6 +1955,7 @@ function handleLeaderAction(action, state2, actions) {
1364
1955
  actions.cleanup();
1365
1956
  return;
1366
1957
  case "dismiss":
1958
+ closeBadgeGallery();
1367
1959
  break;
1368
1960
  }
1369
1961
  state2.mode = "navigate";
@@ -1407,6 +1999,14 @@ function handleLeaderKey(input, key, state2, actions) {
1407
1999
  handleLeaderAction({ type: "help" }, state2, actions);
1408
2000
  return;
1409
2001
  }
2002
+ if (input === "c") {
2003
+ handleLeaderAction({ type: "companion-overlay" }, state2, actions);
2004
+ return;
2005
+ }
2006
+ if (input === "D") {
2007
+ handleLeaderAction({ type: "companion-debug" }, state2, actions);
2008
+ return;
2009
+ }
1410
2010
  if (input === "!") {
1411
2011
  handleLeaderAction({ type: "shell-command" }, state2, actions);
1412
2012
  return;
@@ -1461,6 +2061,54 @@ function handleLeaderKey(input, key, state2, actions) {
1461
2061
  return;
1462
2062
  }
1463
2063
  }
2064
+ if (state2.mode === "companion-overlay") {
2065
+ if (input === "?") {
2066
+ if (getCompanionPage() === "help") companionOverlayDismissHelp();
2067
+ else companionOverlayShowHelp();
2068
+ requestRender();
2069
+ return;
2070
+ }
2071
+ if (key.escape) {
2072
+ if (getCompanionPage() === "help") {
2073
+ companionOverlayDismissHelp();
2074
+ requestRender();
2075
+ return;
2076
+ }
2077
+ handleLeaderAction({ type: "dismiss" }, state2, actions);
2078
+ return;
2079
+ }
2080
+ if (key.tab) {
2081
+ companionOverlayNextPage();
2082
+ requestRender();
2083
+ return;
2084
+ }
2085
+ if (getCompanionPage() === "badges") {
2086
+ if (key.upArrow || input === "k") {
2087
+ badgeGalleryLeft();
2088
+ requestRender();
2089
+ return;
2090
+ }
2091
+ if (key.downArrow || input === "j") {
2092
+ badgeGalleryRight();
2093
+ requestRender();
2094
+ return;
2095
+ }
2096
+ if (key.leftArrow || input === "h") {
2097
+ badgeListScrollUp();
2098
+ requestRender();
2099
+ return;
2100
+ }
2101
+ if (key.rightArrow || input === "l") {
2102
+ badgeListScrollDown();
2103
+ requestRender();
2104
+ return;
2105
+ }
2106
+ }
2107
+ return;
2108
+ }
2109
+ if (state2.mode === "companion-debug") {
2110
+ handleLeaderAction({ type: "dismiss" }, state2, actions);
2111
+ }
1464
2112
  }
1465
2113
  function handleNavigateKey(input, key, state2, actions) {
1466
2114
  const nodes = actions.getNodes();
@@ -1602,41 +2250,24 @@ function handleNavigateKey(input, key, state2, actions) {
1602
2250
  notify(state2, "No session selected");
1603
2251
  return;
1604
2252
  }
1605
- if (session.status === "completed") {
1606
- const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
1607
- const claudeSessionId = lastCycle?.claudeSessionId;
1608
- if (!claudeSessionId) {
1609
- notify(state2, "No orchestrator Claude session ID available");
1610
- return;
1611
- }
1612
- try {
1613
- const label = session.name ?? state2.selectedSessionId.slice(0, 8);
1614
- const sessionName = actions.openClaudeResumeSession(state2.cwd, claudeSessionId, label);
1615
- actions.switchToSession(sessionName);
1616
- } catch {
1617
- notify(state2, "Failed to open Claude session");
1618
- }
1619
- return;
1620
- }
1621
- if (state2.paneAlive && session.tmuxWindowId) {
2253
+ if (session.status !== "completed" && state2.paneAlive && session.tmuxWindowId) {
1622
2254
  if (session.tmuxSessionName) actions.switchToSession(session.tmuxSessionName);
1623
2255
  actions.selectWindow(session.tmuxWindowId);
1624
2256
  return;
1625
2257
  }
1626
- void (async () => {
1627
- try {
1628
- const res = await actions.send({ type: "reopen-window", sessionId: state2.selectedSessionId, cwd: state2.cwd });
1629
- if (!res.ok) {
1630
- notify(state2, `Error: ${res.error}`);
1631
- return;
1632
- }
1633
- const data = res.data;
1634
- actions.switchToSession(data.tmuxSessionName);
1635
- actions.selectWindow(data.tmuxWindowId);
1636
- } catch (err) {
1637
- notify(state2, `Error: ${err.message}`);
1638
- }
1639
- })();
2258
+ const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
2259
+ const claudeSessionId = lastCycle?.claudeSessionId;
2260
+ if (!claudeSessionId) {
2261
+ notify(state2, "No orchestrator Claude session ID available");
2262
+ return;
2263
+ }
2264
+ try {
2265
+ const label = session.name ?? state2.selectedSessionId.slice(0, 8);
2266
+ const sessionName = actions.openClaudeResumeSession(state2.cwd, claudeSessionId, label, lastCycle.resumeEnv, lastCycle.resumeArgs);
2267
+ actions.switchToSession(sessionName);
2268
+ } catch {
2269
+ notify(state2, "Failed to open Claude session");
2270
+ }
1640
2271
  return;
1641
2272
  }
1642
2273
  if (input === "o") {
@@ -1645,19 +2276,25 @@ function handleNavigateKey(input, key, state2, actions) {
1645
2276
  return;
1646
2277
  }
1647
2278
  let claudeSessionId;
2279
+ let resumeEnv;
2280
+ let resumeArgs;
1648
2281
  if (cursorNode.type === "agent" || cursorNode.type === "report") {
1649
2282
  const agent = actions.getAgentForNode(cursorNode);
1650
2283
  claudeSessionId = agent?.claudeSessionId ?? void 0;
2284
+ resumeEnv = agent?.resumeEnv;
2285
+ resumeArgs = agent?.resumeArgs;
1651
2286
  } else if (cursorNode.type === "cycle" && session) {
1652
2287
  const cycle = session.orchestratorCycles.find((c) => c.cycle === cursorNode.cycleNumber);
1653
2288
  claudeSessionId = cycle?.claudeSessionId;
2289
+ resumeEnv = cycle?.resumeEnv;
2290
+ resumeArgs = cycle?.resumeArgs;
1654
2291
  }
1655
2292
  if (!claudeSessionId) {
1656
2293
  notify(state2, "No Claude session ID available");
1657
2294
  return;
1658
2295
  }
1659
2296
  try {
1660
- actions.openClaudeResumePopup(state2.cwd, claudeSessionId);
2297
+ actions.openClaudeResumePopup(state2.cwd, claudeSessionId, resumeEnv, resumeArgs);
1661
2298
  } catch {
1662
2299
  notify(state2, "Failed to open Claude session");
1663
2300
  }
@@ -1668,479 +2305,245 @@ function handleNavigateKey(input, key, state2, actions) {
1668
2305
  notify(state2, "No session selected");
1669
2306
  return;
1670
2307
  }
1671
- const gp = goalPath(state2.cwd, state2.selectedSessionId);
1672
- const editor = actions.resolveEditor();
1673
- try {
1674
- actions.openEditorPopup(state2.cwd, editor, gp, { w: "80", h: "50%" });
1675
- } catch {
1676
- notify(state2, `Failed to open goal in ${editor}`);
1677
- }
1678
- return;
1679
- }
1680
- if (input === "n") {
1681
- if (enterComposeMode(state2, { kind: "new-session" }, actions)) return;
1682
- const editor = actions.resolveEditor();
1683
- try {
1684
- const content = actions.editInPopup(state2.cwd, editor);
1685
- if (content) {
1686
- actions.sendAndNotify(
1687
- { type: "start", task: content, cwd: state2.cwd },
1688
- "Session created"
1689
- );
1690
- }
1691
- } catch {
1692
- notify(state2, "Failed to open editor");
1693
- }
1694
- return;
1695
- }
1696
- if (input === "c") {
1697
- try {
1698
- actions.openCompanionPane(state2.cwd);
1699
- } catch {
1700
- notify(state2, "Failed to open companion pane");
1701
- }
1702
- return;
1703
- }
1704
- if (input === "p") {
1705
- if (!state2.selectedSessionId) {
1706
- notify(state2, "No session selected");
1707
- return;
1708
- }
1709
- const pp = roadmapPath(state2.cwd, state2.selectedSessionId);
1710
- const editor = actions.resolveEditor();
1711
- try {
1712
- actions.openEditorPopup(state2.cwd, editor, pp);
1713
- } catch {
1714
- notify(state2, `Failed to open roadmap in ${editor}`);
1715
- }
1716
- return;
1717
- }
1718
- if (input === "q") {
1719
- actions.cleanup();
1720
- }
1721
- if (input === "r") {
1722
- const agent = actions.getAgentForNode(cursorNode);
1723
- if (!agent || !state2.selectedSessionId) {
1724
- notify(state2, "Select an agent to re-run");
1725
- return;
1726
- }
1727
- actions.sendAndNotify(
1728
- {
1729
- type: "spawn",
1730
- sessionId: state2.selectedSessionId,
1731
- agentType: agent.agentType,
1732
- name: `${agent.name}-retry`,
1733
- instruction: agent.instruction
1734
- },
1735
- `Re-spawned ${agent.name}`
1736
- );
1737
- return;
1738
- }
1739
- if (input === "R") {
1740
- if (!state2.selectedSessionId) {
1741
- notify(state2, "No session selected");
1742
- return;
1743
- }
1744
- if (session?.status === "active" && state2.paneAlive) {
1745
- notify(state2, "Session already active");
1746
- return;
1747
- }
1748
- if (enterComposeMode(state2, { kind: "resume", sessionId: state2.selectedSessionId }, actions)) return;
1749
- state2.mode = "resume";
1750
- state2.inputText = "";
1751
- state2.inputCursorPos = 0;
1752
- requestRender();
1753
- return;
1754
- }
1755
- if (input === "C") {
1756
- if (!state2.selectedSessionId) {
1757
- notify(state2, "No session selected");
1758
- return;
1759
- }
1760
- if (session?.status !== "completed") {
1761
- notify(state2, "Session not completed");
1762
- return;
1763
- }
1764
- if (enterComposeMode(state2, { kind: "continue", sessionId: state2.selectedSessionId }, actions)) return;
1765
- state2.mode = "continue";
1766
- state2.inputText = "";
1767
- state2.inputCursorPos = 0;
1768
- requestRender();
1769
- return;
1770
- }
1771
- if (input === "x") {
1772
- const agent = actions.getAgentForNode(cursorNode);
1773
- if (!agent || !state2.selectedSessionId) {
1774
- notify(state2, "Select an agent to restart");
1775
- return;
1776
- }
1777
- actions.sendAndNotify(
1778
- { type: "restart-agent", sessionId: state2.selectedSessionId, agentId: agent.id },
1779
- `Restarted ${agent.id}`
1780
- );
1781
- return;
1782
- }
1783
- if (input === "b") {
1784
- if (!state2.selectedSessionId) {
1785
- notify(state2, "No session selected");
1786
- return;
1787
- }
1788
- const defaultText = cursorNode?.type === "cycle" ? String(cursorNode.cycleNumber) : void 0;
1789
- state2.mode = "rollback";
1790
- if (defaultText) {
1791
- state2.inputText = defaultText;
1792
- state2.inputCursorPos = defaultText.length;
1793
- } else {
1794
- state2.inputText = "";
1795
- state2.inputCursorPos = 0;
1796
- }
1797
- requestRender();
1798
- return;
1799
- }
1800
- if (input === "e") {
1801
- if (!cursorNode || cursorNode.type !== "context-file") return;
1802
- const editor = actions.resolveEditor();
1803
- try {
1804
- actions.openEditorPopup(state2.cwd, editor, cursorNode.filePath);
1805
- } catch {
1806
- notify(state2, "Failed to open file in editor");
1807
- }
1808
- return;
1809
- }
1810
- if (input === "S") {
1811
- if (!state2.selectedSessionId) {
1812
- notify(state2, "No session selected");
1813
- return;
1814
- }
1815
- const sp = strategyPath(state2.cwd, state2.selectedSessionId);
1816
- const editor = actions.resolveEditor();
1817
- try {
1818
- actions.openEditorPopup(state2.cwd, editor, sp);
1819
- } catch {
1820
- notify(state2, `Failed to open strategy in ${editor}`);
1821
- }
1822
- return;
1823
- }
1824
- if (input === "t") {
1825
- if (state2.showCombinedView) {
1826
- if (state2.focusPane === "logs") state2.focusPane = "detail";
1827
- state2.logsScroll.reset();
1828
- }
1829
- state2.showCombinedView = !state2.showCombinedView;
1830
- requestRender();
1831
- return;
1832
- }
1833
- }
1834
- function handleKeypress(input, key, state2, actions) {
1835
- if (state2.mode === "compose") return;
1836
- if (INPUT_MODES.has(state2.mode)) {
1837
- handleInputBarKey(input, key, state2, actions);
1838
- } else if (state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help") {
1839
- handleLeaderKey(input, key, state2, actions);
1840
- } else if (state2.mode === "report-detail") {
1841
- handleReportDetailKey(input, key, state2, actions);
1842
- } else {
1843
- handleNavigateKey(input, key, state2, actions);
1844
- }
1845
- }
1846
-
1847
- // src/tui/render.ts
1848
- import stringWidth2 from "string-width";
1849
- var COLOR_SGR = {
1850
- black: 30,
1851
- red: 31,
1852
- green: 32,
1853
- yellow: 33,
1854
- blue: 34,
1855
- magenta: 35,
1856
- cyan: 36,
1857
- white: 37,
1858
- gray: 90
1859
- };
1860
- function colorToSGR(color) {
1861
- const code = COLOR_SGR[color];
1862
- if (code === void 0) throw new Error(`Unknown color: ${color}`);
1863
- return code;
1864
- }
1865
- function renderLine(segs) {
1866
- let out = "";
1867
- for (const s of segs) {
1868
- const codes = [];
1869
- if (s.bold) codes.push(1);
1870
- if (s.dim) codes.push(2);
1871
- if (s.italic) codes.push(3);
1872
- if (s.inverse) codes.push(7);
1873
- if (s.color) codes.push(colorToSGR(s.color));
1874
- if (codes.length > 0) {
1875
- out += `\x1B[${codes.join(";")}m${s.text}\x1B[0m`;
1876
- } else {
1877
- out += s.text;
1878
- }
1879
- }
1880
- return out;
1881
- }
1882
- var cachedBlank = "";
1883
- var cachedBlankWidth = 0;
1884
- function createFrameBuffer(width, height) {
1885
- if (width !== cachedBlankWidth) {
1886
- cachedBlank = " ".repeat(width);
1887
- cachedBlankWidth = width;
1888
- }
1889
- const lines = new Array(height);
1890
- for (let i = 0; i < height; i++) lines[i] = cachedBlank;
1891
- return { lines, width, height };
1892
- }
1893
- function copyRows(buf, src, startRow, count) {
1894
- for (let i = 0; i < count && startRow + i < buf.height; i++) {
1895
- buf.lines[startRow + i] = src[startRow + i];
1896
- }
1897
- }
1898
- function flushFrame(frame, prevFrame2, suffix) {
1899
- let out = "\x1B[?2026h";
1900
- for (let i = 0; i < frame.length; i++) {
1901
- if (frame[i] !== prevFrame2[i]) {
1902
- out += `\x1B[${i + 1};1H`;
1903
- out += "\x1B[2K";
1904
- out += frame[i];
1905
- }
2308
+ const gp = goalPath(state2.cwd, state2.selectedSessionId);
2309
+ const editor = actions.resolveEditor();
2310
+ try {
2311
+ actions.openEditorPopup(state2.cwd, editor, gp, { w: "80", h: "50%" });
2312
+ } catch {
2313
+ notify(state2, `Failed to open goal in ${editor}`);
2314
+ }
2315
+ return;
1906
2316
  }
1907
- if (suffix) out += suffix;
1908
- out += "\x1B[?2026l";
1909
- return out;
1910
- }
1911
- var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
1912
- function clipAnsi(content, maxWidth) {
1913
- let out = "";
1914
- let displayWidth = 0;
1915
- let i = 0;
1916
- while (i < content.length) {
1917
- if (content[i] === "\x1B" && content[i + 1] === "[") {
1918
- const seqLen = ansiLen(content, i);
1919
- if (seqLen > 0) {
1920
- out += content.substring(i, i + seqLen);
1921
- i += seqLen;
1922
- continue;
2317
+ if (input === "n") {
2318
+ if (enterComposeMode(state2, { kind: "new-session" }, actions)) return;
2319
+ const editor = actions.resolveEditor();
2320
+ try {
2321
+ const content = actions.editInPopup(state2.cwd, editor);
2322
+ if (content) {
2323
+ actions.sendAndNotify(
2324
+ { type: "start", task: content, cwd: state2.cwd },
2325
+ "Session created"
2326
+ );
1923
2327
  }
2328
+ } catch {
2329
+ notify(state2, "Failed to open editor");
1924
2330
  }
1925
- const cp = content.codePointAt(i);
1926
- const ch = String.fromCodePoint(cp);
1927
- const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1928
- if (displayWidth + chWidth > maxWidth) break;
1929
- out += ch;
1930
- displayWidth += chWidth;
1931
- i += ch.length;
1932
- }
1933
- if (out.includes("\x1B[") && !out.endsWith("\x1B[0m")) {
1934
- out += "\x1B[0m";
2331
+ return;
1935
2332
  }
1936
- const remaining = maxWidth - displayWidth;
1937
- if (remaining > 0) out += " ".repeat(remaining);
1938
- return out;
1939
- }
1940
- function displayWidthFast(s) {
1941
- let w = 0;
1942
- let i = 0;
1943
- while (i < s.length) {
1944
- if (s[i] === "\x1B" && s[i + 1] === "[") {
1945
- const len = ansiLen(s, i);
1946
- if (len > 0) {
1947
- i += len;
1948
- continue;
1949
- }
2333
+ if (input === "c") {
2334
+ try {
2335
+ actions.openCompanionPane(state2.cwd);
2336
+ } catch {
2337
+ notify(state2, "Failed to open companion pane");
1950
2338
  }
1951
- const cp = s.codePointAt(i);
1952
- const ch = String.fromCodePoint(cp);
1953
- w += cp < 128 ? 1 : stringWidth2(ch);
1954
- i += ch.length;
2339
+ return;
1955
2340
  }
1956
- return w;
1957
- }
1958
- function ansiLen(s, i) {
1959
- let j = i + 2;
1960
- const len = s.length;
1961
- while (j < len) {
1962
- const c = s.charCodeAt(j);
1963
- if (c >= 48 && c <= 57 || c === 59) {
1964
- j++;
1965
- } else {
1966
- break;
2341
+ if (input === "p") {
2342
+ if (!state2.selectedSessionId) {
2343
+ notify(state2, "No session selected");
2344
+ return;
2345
+ }
2346
+ const pp = roadmapPath(state2.cwd, state2.selectedSessionId);
2347
+ const editor = actions.resolveEditor();
2348
+ try {
2349
+ actions.openEditorPopup(state2.cwd, editor, pp);
2350
+ } catch {
2351
+ notify(state2, `Failed to open roadmap in ${editor}`);
1967
2352
  }
2353
+ return;
1968
2354
  }
1969
- if (j < len) {
1970
- const c = s.charCodeAt(j);
1971
- if (c >= 65 && c <= 90 || c >= 97 && c <= 122) {
1972
- return j + 1 - i;
2355
+ if (input === "q") {
2356
+ actions.cleanup();
2357
+ }
2358
+ if (input === "r") {
2359
+ const agent = actions.getAgentForNode(cursorNode);
2360
+ if (!agent || !state2.selectedSessionId) {
2361
+ notify(state2, "Select an agent to re-run");
2362
+ return;
1973
2363
  }
2364
+ actions.sendAndNotify(
2365
+ {
2366
+ type: "spawn",
2367
+ sessionId: state2.selectedSessionId,
2368
+ agentType: agent.agentType,
2369
+ name: `${agent.name}-retry`,
2370
+ instruction: agent.instruction
2371
+ },
2372
+ `Re-spawned ${agent.name}`
2373
+ );
2374
+ return;
1974
2375
  }
1975
- return 0;
1976
- }
1977
- function writeAt(buf, x, y, content) {
1978
- if (y < 0 || y >= buf.height) return;
1979
- if (x < 0 || x >= buf.width) return;
1980
- const existing = buf.lines[y];
1981
- const contentDisplayWidth = stringWidth2(content.replace(ANSI_RE, ""));
1982
- const prefix = sliceDisplayCols(existing, 0, x);
1983
- const suffix = sliceDisplayCols(existing, x + contentDisplayWidth, buf.width);
1984
- const prefixWidth = stringWidth2(prefix.replace(ANSI_RE, ""));
1985
- const paddedPrefix = prefix + " ".repeat(Math.max(0, x - prefixWidth));
1986
- buf.lines[y] = paddedPrefix + content + suffix;
1987
- }
1988
- function writeClipped(buf, x, y, content, maxWidth) {
1989
- if (y < 0 || y >= buf.height) return;
1990
- if (x < 0 || x >= buf.width) return;
1991
- let out = "";
1992
- let displayWidth = 0;
1993
- let i = 0;
1994
- while (i < content.length) {
1995
- if (content[i] === "\x1B" && content[i + 1] === "[") {
1996
- const seqLen = ansiLen(content, i);
1997
- if (seqLen > 0) {
1998
- out += content.substring(i, i + seqLen);
1999
- i += seqLen;
2000
- continue;
2376
+ if (input === "R") {
2377
+ if (!state2.selectedSessionId) {
2378
+ notify(state2, "No session selected");
2379
+ return;
2380
+ }
2381
+ if (session?.status === "active" && state2.paneAlive) {
2382
+ notify(state2, "Session already active");
2383
+ return;
2384
+ }
2385
+ if (enterComposeMode(state2, { kind: "resume", sessionId: state2.selectedSessionId }, actions)) return;
2386
+ {
2387
+ const sessionId = state2.selectedSessionId;
2388
+ const editor = actions.resolveEditor();
2389
+ try {
2390
+ const content = actions.editInPopup(state2.cwd, editor);
2391
+ actions.sendAndNotify(
2392
+ { type: "resume", sessionId, cwd: state2.cwd, message: content?.trim() || void 0 },
2393
+ "Session resumed"
2394
+ );
2395
+ } catch {
2396
+ notify(state2, "Failed to open editor");
2001
2397
  }
2002
2398
  }
2003
- const cp = content.codePointAt(i);
2004
- const ch = String.fromCodePoint(cp);
2005
- const chWidth = cp < 128 ? 1 : stringWidth2(ch);
2006
- if (displayWidth + chWidth > maxWidth) break;
2007
- out += ch;
2008
- displayWidth += chWidth;
2009
- i += ch.length;
2010
- }
2011
- if (out.includes("\x1B[") && !out.endsWith("\x1B[0m")) {
2012
- out += "\x1B[0m";
2399
+ return;
2013
2400
  }
2014
- const remaining = maxWidth - displayWidth;
2015
- if (remaining > 0) {
2016
- out += " ".repeat(remaining);
2401
+ if (input === "C") {
2402
+ if (!state2.selectedSessionId) {
2403
+ notify(state2, "No session selected");
2404
+ return;
2405
+ }
2406
+ if (session?.status !== "completed") {
2407
+ notify(state2, "Session not completed");
2408
+ return;
2409
+ }
2410
+ if (enterComposeMode(state2, { kind: "continue", sessionId: state2.selectedSessionId }, actions)) return;
2411
+ {
2412
+ const sessionId = state2.selectedSessionId;
2413
+ const editor = actions.resolveEditor();
2414
+ void (async () => {
2415
+ try {
2416
+ const content = actions.editInPopup(state2.cwd, editor);
2417
+ const contRes = await actions.send({ type: "continue", sessionId });
2418
+ if (!contRes.ok) {
2419
+ notify(state2, `Error: ${contRes.error}`);
2420
+ return;
2421
+ }
2422
+ actions.sendAndNotify(
2423
+ { type: "resume", sessionId, cwd: state2.cwd, message: content?.trim() || void 0 },
2424
+ "Session continued"
2425
+ );
2426
+ } catch (err) {
2427
+ notify(state2, `Error: ${err.message}`);
2428
+ }
2429
+ })();
2430
+ }
2431
+ return;
2017
2432
  }
2018
- const existing = buf.lines[y];
2019
- const prefix = sliceDisplayCols(existing, 0, x);
2020
- const suffix = sliceDisplayCols(existing, x + maxWidth, buf.width);
2021
- const prefixDisplayW = displayWidthFast(prefix);
2022
- const paddedPrefix = prefixDisplayW < x ? prefix + " ".repeat(x - prefixDisplayW) : prefix;
2023
- buf.lines[y] = paddedPrefix + out + suffix;
2024
- }
2025
- function writeCenter(buf, row, content) {
2026
- const textWidth = stringWidth2(content.replace(ANSI_RE, ""));
2027
- const x = Math.max(0, Math.floor((buf.width - textWidth) / 2));
2028
- writeAt(buf, x, row, content);
2029
- }
2030
- function drawBorder(buf, x, y, w, h, color) {
2031
- const sgr = `\x1B[${colorToSGR(color)}m`;
2032
- const reset = "\x1B[0m";
2033
- writeAt(buf, x, y, sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset);
2034
- writeAt(buf, x, y + h - 1, sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset);
2035
- for (let row = y + 1; row < y + h - 1; row++) {
2036
- writeAt(buf, x, row, sgr + "\u2502" + reset);
2037
- writeAt(buf, x + w - 1, row, sgr + "\u2502" + reset);
2433
+ if (input === "x") {
2434
+ const agent = actions.getAgentForNode(cursorNode);
2435
+ if (!agent || !state2.selectedSessionId) {
2436
+ notify(state2, "Select an agent to restart");
2437
+ return;
2438
+ }
2439
+ actions.sendAndNotify(
2440
+ { type: "restart-agent", sessionId: state2.selectedSessionId, agentId: agent.id },
2441
+ `Restarted ${agent.id}`
2442
+ );
2443
+ return;
2038
2444
  }
2039
- }
2040
- function buildPanelRows(rect, lines, scrollOffset, focused, borderColor, renderedCache) {
2041
- const { w, h } = rect;
2042
- const rows = new Array(h);
2043
- const color = focused ? "blue" : borderColor;
2044
- const sgr = `\x1B[${colorToSGR(color)}m`;
2045
- const reset = "\x1B[0m";
2046
- const innerW = w - 4;
2047
- const innerH = h - 2;
2048
- const blankInner = " ".repeat(innerW);
2049
- rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
2050
- rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
2051
- const borderL = sgr + "\u2502" + reset + " ";
2052
- const borderR = " " + sgr + "\u2502" + reset;
2053
- const emptyRow = borderL + blankInner + borderR;
2054
- for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
2055
- if (innerW <= 0 || innerH <= 0) return rows;
2056
- let ansiLines;
2057
- if (renderedCache && renderedCache.lines === lines) {
2058
- ansiLines = renderedCache.ansi;
2059
- } else {
2060
- ansiLines = new Array(lines.length);
2061
- for (let i = 0; i < lines.length; i++) {
2062
- ansiLines[i] = renderLine(lines[i]);
2445
+ if (input === "b") {
2446
+ if (!state2.selectedSessionId) {
2447
+ notify(state2, "No session selected");
2448
+ return;
2449
+ }
2450
+ {
2451
+ const sessionId = state2.selectedSessionId;
2452
+ const editor = actions.resolveEditor();
2453
+ try {
2454
+ const text = actions.editInPopup(state2.cwd, editor);
2455
+ if (text?.trim()) {
2456
+ const toCycle = parseInt(text.trim(), 10);
2457
+ if (isNaN(toCycle) || toCycle < 1) {
2458
+ notify(state2, "Invalid cycle number");
2459
+ return;
2460
+ }
2461
+ actions.sendAndNotify(
2462
+ { type: "rollback", sessionId, cwd: state2.cwd, toCycle },
2463
+ `Rolled back to cycle ${toCycle} \u2014 use [R]esume to respawn`
2464
+ );
2465
+ }
2466
+ } catch {
2467
+ notify(state2, "Failed to open editor");
2468
+ }
2063
2469
  }
2064
- if (renderedCache) {
2065
- renderedCache.lines = lines;
2066
- renderedCache.ansi = ansiLines;
2470
+ return;
2471
+ }
2472
+ if (input === "e") {
2473
+ if (!cursorNode || cursorNode.type !== "context-file") return;
2474
+ const editor = actions.resolveEditor();
2475
+ try {
2476
+ actions.openEditorPopup(state2.cwd, editor, cursorNode.filePath);
2477
+ } catch {
2478
+ notify(state2, "Failed to open file in editor");
2067
2479
  }
2480
+ return;
2068
2481
  }
2069
- const hasOverflow = lines.length > innerH;
2070
- const viewableH = hasOverflow ? innerH - 1 : innerH;
2071
- const maxScroll = Math.max(0, lines.length - viewableH);
2072
- const effectiveOffset = Math.min(scrollOffset, maxScroll);
2073
- for (let i = 0; i < viewableH && effectiveOffset + i < ansiLines.length; i++) {
2074
- const clipped = clipAnsi(ansiLines[effectiveOffset + i], innerW);
2075
- rows[1 + i] = borderL + clipped + borderR;
2482
+ if (input === "S") {
2483
+ if (!state2.selectedSessionId) {
2484
+ notify(state2, "No session selected");
2485
+ return;
2486
+ }
2487
+ const sp = strategyPath(state2.cwd, state2.selectedSessionId);
2488
+ const editor = actions.resolveEditor();
2489
+ try {
2490
+ actions.openEditorPopup(state2.cwd, editor, sp);
2491
+ } catch {
2492
+ notify(state2, `Failed to open strategy in ${editor}`);
2493
+ }
2494
+ return;
2076
2495
  }
2077
- if (hasOverflow) {
2078
- const scrollPct = maxScroll > 0 ? Math.round(effectiveOffset / maxScroll * 100) : 100;
2079
- const indicator = ` \u2195 ${scrollPct}% \xB7 ${lines.length} lines`;
2080
- const clipped = clipAnsi(`\x1B[2m${indicator}\x1B[0m`, innerW);
2081
- rows[1 + viewableH] = borderL + clipped + borderR;
2496
+ if (input === "/") {
2497
+ state2.mode = "search";
2498
+ state2.searchText = "";
2499
+ requestRender();
2500
+ return;
2082
2501
  }
2083
- return rows;
2084
- }
2085
- function buildEmptyPanelRows(rect, focused, borderColor, centerText) {
2086
- const { w, h } = rect;
2087
- const rows = new Array(h);
2088
- const color = focused ? "blue" : borderColor;
2089
- const sgr = `\x1B[${colorToSGR(color)}m`;
2090
- const reset = "\x1B[0m";
2091
- const innerW = w - 4;
2092
- const borderL = sgr + "\u2502" + reset + " ";
2093
- const borderR = " " + sgr + "\u2502" + reset;
2094
- const emptyRow = borderL + " ".repeat(innerW) + borderR;
2095
- rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
2096
- rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
2097
- for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
2098
- if (centerText) {
2099
- const midRow = Math.floor(h / 2);
2100
- if (midRow > 0 && midRow < h - 1) {
2101
- const clipped = clipAnsi(centerText, innerW);
2102
- const textW = displayWidthFast(centerText);
2103
- const pad = Math.max(0, Math.floor((innerW - textW) / 2));
2104
- const centered = " ".repeat(pad) + clipped;
2105
- rows[midRow] = borderL + clipAnsi(centered, innerW) + borderR;
2502
+ if (input === "t") {
2503
+ if (state2.showCombinedView) {
2504
+ if (state2.focusPane === "logs") state2.focusPane = "detail";
2505
+ state2.logsScroll.reset();
2106
2506
  }
2507
+ state2.showCombinedView = !state2.showCombinedView;
2508
+ requestRender();
2509
+ return;
2107
2510
  }
2108
- return rows;
2109
2511
  }
2110
- function sliceDisplayCols(s, start, end) {
2111
- let out = "";
2112
- let col = 0;
2113
- let i = 0;
2114
- let inSlice = false;
2115
- let hasOpenSGR = false;
2116
- while (i < s.length && col < end) {
2117
- if (s[i] === "\x1B" && s[i + 1] === "[") {
2118
- const seqLen = ansiLen(s, i);
2119
- if (seqLen > 0) {
2120
- if (col >= start) {
2121
- const seq = s.substring(i, i + seqLen);
2122
- out += seq;
2123
- hasOpenSGR = seq !== "\x1B[0m" && seq !== "\x1B[m";
2124
- }
2125
- i += seqLen;
2126
- continue;
2127
- }
2128
- }
2129
- const cp = s.codePointAt(i);
2130
- const ch = String.fromCodePoint(cp);
2131
- const chWidth = cp < 128 ? 1 : stringWidth2(ch);
2132
- if (col >= start) {
2133
- inSlice = true;
2134
- if (col + chWidth > end) break;
2135
- out += ch;
2136
- }
2137
- col += chWidth;
2138
- i += ch.length;
2512
+ function handleSearchKey(input, key, state2) {
2513
+ if (key.return) {
2514
+ state2.mode = "navigate";
2515
+ requestRender();
2516
+ return;
2139
2517
  }
2140
- if (inSlice && hasOpenSGR) {
2141
- out += "\x1B[0m";
2518
+ if (key.escape) {
2519
+ state2.searchFilter = null;
2520
+ state2.searchText = "";
2521
+ state2.mode = "navigate";
2522
+ requestRender();
2523
+ return;
2524
+ }
2525
+ if (key.backspace) {
2526
+ state2.searchText = state2.searchText.slice(0, -1);
2527
+ state2.searchFilter = state2.searchText || null;
2528
+ requestRender();
2529
+ return;
2530
+ }
2531
+ if (key.ctrl || key.meta || !input || input.length !== 1) return;
2532
+ state2.searchText += input;
2533
+ state2.searchFilter = state2.searchText;
2534
+ requestRender();
2535
+ }
2536
+ function handleKeypress(input, key, state2, actions) {
2537
+ if (state2.mode === "compose") return;
2538
+ if (state2.mode === "search") {
2539
+ handleSearchKey(input, key, state2);
2540
+ } else if (state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help" || state2.mode === "companion-overlay" || state2.mode === "companion-debug") {
2541
+ handleLeaderKey(input, key, state2, actions);
2542
+ } else if (state2.mode === "report-detail") {
2543
+ handleReportDetailKey(input, key, state2, actions);
2544
+ } else {
2545
+ handleNavigateKey(input, key, state2, actions);
2142
2546
  }
2143
- return out;
2144
2547
  }
2145
2548
 
2146
2549
  // src/tui/lib/tree-render.ts
@@ -2310,18 +2713,22 @@ function openShellPopup(cwd2, command) {
2310
2713
  function openInFileManager(path) {
2311
2714
  execSync(`open ${shellQuote(path)}`, { stdio: "inherit", env: EXEC_ENV });
2312
2715
  }
2313
- function openClaudeResumePopup(cwd2, claudeSessionId) {
2716
+ function openClaudeResumePopup(cwd2, claudeSessionId, resumeEnv, resumeArgs) {
2314
2717
  const pathEnv = augmentedPath();
2315
- const cmd = `PATH=${shellQuote(pathEnv)} claude --resume ${shellQuote(claudeSessionId)}`;
2718
+ const envPrefix = resumeEnv ? `${resumeEnv} && ` : "";
2719
+ const args2 = resumeArgs ? `${resumeArgs} --resume ${shellQuote(claudeSessionId)}` : `--resume ${shellQuote(claudeSessionId)}`;
2720
+ const cmd = `${envPrefix}PATH=${shellQuote(pathEnv)} claude ${args2}`;
2316
2721
  execSync(
2317
2722
  `tmux display-popup -E -w 90% -h 80% -d ${shellQuote(cwd2)} ${shellQuote(cmd)}`,
2318
2723
  { stdio: "inherit", env: EXEC_ENV }
2319
2724
  );
2320
2725
  }
2321
- function openClaudeResumeSession(cwd2, claudeSessionId, sessionLabel) {
2726
+ function openClaudeResumeSession(cwd2, claudeSessionId, sessionLabel, resumeEnv, resumeArgs) {
2322
2727
  const pathEnv = augmentedPath();
2323
- const cmd = `PATH=${shellQuote(pathEnv)} claude --resume ${shellQuote(claudeSessionId)}`;
2324
- const sessionName = `sisyphus-${sessionLabel}-resume`;
2728
+ const envPrefix = resumeEnv ? `${resumeEnv} && ` : "";
2729
+ const args2 = resumeArgs ? `${resumeArgs} --resume ${shellQuote(claudeSessionId)}` : `--resume ${shellQuote(claudeSessionId)}`;
2730
+ const cmd = `${envPrefix}PATH=${shellQuote(pathEnv)} claude ${args2}`;
2731
+ const sessionName = tmuxSessionName(cwd2, `${sessionLabel}-resume`);
2325
2732
  exec(`tmux new-session -d -s ${shellQuote(sessionName)} -c ${shellQuote(cwd2)} ${shellQuote(cmd)}`);
2326
2733
  execSafe(`tmux set-option -t ${shellQuote(sessionName)} @sisyphus_cwd ${shellQuote(cwd2.replace(/\/+$/, ""))}`);
2327
2734
  const paneTarget = `${sessionName}:`;
@@ -2449,7 +2856,7 @@ function renderNodeContent(node, maxWidth) {
2449
2856
  }
2450
2857
  }
2451
2858
  }
2452
- function renderTreePanel(buf, rect, nodes, cursorIndex, focused) {
2859
+ function renderTreePanel(buf, rect, nodes, cursorIndex, focused, companion) {
2453
2860
  const { x, y, w, h } = rect;
2454
2861
  drawBorder(buf, x, y, w, h, focused ? "yellow" : "gray");
2455
2862
  const innerX = x + 2;
@@ -2464,7 +2871,26 @@ function renderTreePanel(buf, rect, nodes, cursorIndex, focused) {
2464
2871
  writeClipped(buf, innerX, innerY + 3, "\x1B[2mPress [?] for all keybindings.\x1B[0m", innerW);
2465
2872
  return;
2466
2873
  }
2467
- const maxVisible = Math.max(1, innerH);
2874
+ let companionRows = 0;
2875
+ let _companionCommentaryLines = [];
2876
+ if (companion) {
2877
+ const commentaryText = companion.lastCommentary?.text;
2878
+ if (commentaryText) {
2879
+ const words = commentaryText.split(" ");
2880
+ let current = "";
2881
+ for (const word of words) {
2882
+ if (current.length + word.length + 1 > innerW && current.length > 0) {
2883
+ _companionCommentaryLines.push(current);
2884
+ current = word;
2885
+ } else {
2886
+ current = current.length > 0 ? `${current} ${word}` : word;
2887
+ }
2888
+ }
2889
+ if (current.length > 0) _companionCommentaryLines.push(current);
2890
+ }
2891
+ companionRows = 1 + 1 + _companionCommentaryLines.length;
2892
+ }
2893
+ const maxVisible = Math.max(1, innerH - companionRows);
2468
2894
  const halfVisible = Math.floor(maxVisible / 2);
2469
2895
  const scrollOffset = Math.max(
2470
2896
  0,
@@ -2524,6 +2950,15 @@ function renderTreePanel(buf, rect, nodes, cursorIndex, focused) {
2524
2950
  const bottomRow = rowStart + availRows;
2525
2951
  writeClipped(buf, innerX, bottomRow, `\x1B[2m\u2193 ${bottomMore} more\x1B[0m`, innerW);
2526
2952
  }
2953
+ if (companion) {
2954
+ const commentaryCount = _companionCommentaryLines.length;
2955
+ const faceRow = y + h - 2 - commentaryCount;
2956
+ const faceLine = renderCompanion(companion, ["face", "boulder"], { maxWidth: innerW, color: true });
2957
+ writeClipped(buf, innerX, faceRow, faceLine, innerW);
2958
+ for (let i = 0; i < commentaryCount; i++) {
2959
+ writeClipped(buf, innerX, faceRow + 1 + i, `\x1B[2m${_companionCommentaryLines[i]}\x1B[0m`, innerW);
2960
+ }
2961
+ }
2527
2962
  }
2528
2963
 
2529
2964
  // src/tui/panels/detail.ts
@@ -3221,66 +3656,28 @@ function renderNvimDetailRows(rect, bridge, focused, editable, statusRows, compo
3221
3656
  }
3222
3657
 
3223
3658
  // src/tui/panels/bottom.ts
3224
- function renderNotificationRow(buf, y, notification, error) {
3225
- if (notification !== null) {
3226
- const icon = /error|failed/i.test(notification) ? "\u2715" : /success|created|killed|sent|copied|deleted/i.test(notification) ? "\u2713" : "\u2139";
3227
- const content = `\x1B[1;33m${icon} ${notification}\x1B[0m`;
3228
- writeClipped(buf, 1, y, content, buf.width - 2);
3229
- } else if (error !== null) {
3230
- const content = `\x1B[31m\u26A0 ${error}\x1B[0m`;
3231
- writeClipped(buf, 1, y, content, buf.width - 2);
3232
- }
3233
- }
3234
- function renderInputBar(buf, y, state2) {
3235
- const { mode, inputText, inputCursorPos } = state2;
3236
- if (mode === "compose") {
3237
- const action = state2.composeAction;
3238
- const label = action ? COMPOSE_HEADERS[action.kind] : "Compose";
3239
- const content2 = `\x1B[1;33mCOMPOSE\x1B[0m\x1B[2m ${label} \xB7 :w to submit \xB7 Tab to cancel\x1B[0m`;
3240
- writeClipped(buf, 1, y, content2, buf.width - 2);
3241
- return;
3242
- }
3243
- if (mode === "navigate") {
3244
- const content2 = `\x1B[2mPress [m] to message orchestrator, [n] for new session\x1B[0m`;
3245
- writeClipped(buf, 1, y, content2, buf.width - 2);
3246
- return;
3247
- }
3248
- if (mode === "report-detail" || mode === "leader" || mode === "copy-menu" || mode === "help" || !INPUT_MODES.has(mode)) {
3249
- return;
3250
- }
3251
- const prompt = PROMPTS[mode] ?? mode;
3252
- const cursorChar = inputCursorPos < inputText.length ? inputText[inputCursorPos] : " ";
3253
- const before = inputText.slice(0, inputCursorPos);
3254
- const after = inputText.slice(inputCursorPos + 1);
3255
- const content = `\x1B[33m${prompt} > \x1B[0m` + before + `\x1B[7m${cursorChar}\x1B[0m` + after + `\x1B[2m (enter to send, esc to cancel)\x1B[0m`;
3256
- writeClipped(buf, 1, y, content, buf.width - 2);
3257
- }
3258
3659
  var B = ansiBold;
3259
3660
  var D = ansiDim;
3260
3661
  var SEP = D("\u2502 ");
3261
3662
  function renderStatusLine(buf, y, state2, cursorNodeType) {
3262
- const { mode, focusPane } = state2;
3663
+ const { mode, focusPane, notification, error } = state2;
3263
3664
  if (mode === "report-detail") return;
3264
3665
  if (mode === "compose") return;
3265
3666
  let content;
3266
- if (mode === "leader") {
3667
+ if (notification !== null) {
3668
+ const icon = /error|failed/i.test(notification) ? "\u2715" : /success|created|killed|sent|copied|deleted/i.test(notification) ? "\u2713" : "\u2139";
3669
+ content = `\x1B[1;33m${icon} ${notification}\x1B[0m`;
3670
+ } else if (error !== null) {
3671
+ content = `\x1B[31m\u26A0 ${error}\x1B[0m`;
3672
+ } else if (mode === "search") {
3673
+ const cursor = `\x1B[7m \x1B[0m`;
3674
+ content = `\x1B[1;34m/\x1B[0m${state2.searchText}${cursor}` + D(" enter to apply \xB7 esc to clear");
3675
+ } else if (mode === "leader") {
3267
3676
  content = `\x1B[1;35mLEADER\x1B[0m` + D(" press a command key or [esc] to cancel");
3268
3677
  } else if (mode === "copy-menu") {
3269
3678
  content = `\x1B[1;36mCOPY\x1B[0m` + D(" [p] path [C] context [l] logs [s] session ID [esc] cancel");
3270
3679
  } else if (mode === "help") {
3271
3680
  content = `\x1B[1;33mHELP\x1B[0m` + D(" [esc] or [?] to dismiss");
3272
- } else if (mode === "delete-confirm") {
3273
- content = `\x1B[1;31mDELETE\x1B[0m` + D(" type 'yes' to confirm, [esc] to cancel");
3274
- } else if (mode === "spawn-agent") {
3275
- content = `\x1B[1;32mSPAWN\x1B[0m` + D(" enter agent instruction, [esc] to cancel");
3276
- } else if (mode === "search") {
3277
- content = `\x1B[1;34mSEARCH\x1B[0m` + D(" type to filter, enter to apply, [esc] to cancel");
3278
- } else if (mode === "message-agent") {
3279
- content = `\x1B[1;36mMESSAGE\x1B[0m` + D(" enter message for agent, [esc] to cancel");
3280
- } else if (mode === "shell-command") {
3281
- content = `\x1B[1;35mSHELL\x1B[0m` + D(" enter command, [esc] to cancel");
3282
- } else if (mode !== "navigate") {
3283
- content = D("[enter] send [esc] cancel");
3284
3681
  } else if (focusPane === "logs" || focusPane === "detail") {
3285
3682
  content = B("[jk/\u2191\u2193]") + D(" scroll ") + B("[h/\u2190/tab]") + D(" back ") + B("[t]") + D("oggle view ") + SEP + B("[m]") + D("sg ") + B("[g]") + D("oal ") + B("[n]") + D("ew ") + B("[p]") + D("lan ") + B("[w]") + D("indow ") + B("[R]") + D("esume ") + B("[q]") + D("uit");
3286
3683
  } else {
@@ -3293,102 +3690,6 @@ function renderStatusLine(buf, y, state2, cursorNodeType) {
3293
3690
  writeClipped(buf, 1, y, content, buf.width - 2);
3294
3691
  }
3295
3692
 
3296
- // src/tui/panels/overlays.ts
3297
- var LEADER_WIDTH = 26;
3298
- var LEADER_HEIGHT = 19;
3299
- var COPY_HEIGHT = 9;
3300
- var HELP_WIDTH = 62;
3301
- function helpRow(left, right, innerWidth) {
3302
- const col = Math.floor(innerWidth / 2);
3303
- return (left.padEnd(col) + right).padEnd(innerWidth);
3304
- }
3305
- function renderLeaderOverlay(buf, rows, cols) {
3306
- const x = cols - LEADER_WIDTH - 1;
3307
- const y = rows - LEADER_HEIGHT - 2;
3308
- const innerWidth = LEADER_WIDTH - 2;
3309
- drawBorder(buf, x, y, LEADER_WIDTH, LEADER_HEIGHT, "magenta");
3310
- const lines = [
3311
- ansiColor(" LEADER".padEnd(innerWidth), "magenta", true),
3312
- " ".padEnd(innerWidth),
3313
- " y copy menu".padEnd(innerWidth),
3314
- " d delete session".padEnd(innerWidth),
3315
- " l daemon logs".padEnd(innerWidth),
3316
- " o open session dir".padEnd(innerWidth),
3317
- " a spawn agent".padEnd(innerWidth),
3318
- " m message agent".padEnd(innerWidth),
3319
- " / search".padEnd(innerWidth),
3320
- " ! shell command".padEnd(innerWidth),
3321
- " j jump to pane".padEnd(innerWidth),
3322
- " k kill session/agent".padEnd(innerWidth),
3323
- " q quit".padEnd(innerWidth),
3324
- " ? help".padEnd(innerWidth),
3325
- " 1-9 jump to session".padEnd(innerWidth),
3326
- " ".padEnd(innerWidth),
3327
- ansiDim(" esc dismiss".padEnd(innerWidth))
3328
- ];
3329
- for (let i = 0; i < lines.length; i++) {
3330
- writeClipped(buf, x + 1, y + 1 + i, lines[i], innerWidth);
3331
- }
3332
- }
3333
- function renderCopyMenuOverlay(buf, rows, cols) {
3334
- const x = cols - LEADER_WIDTH - 1;
3335
- const y = rows - COPY_HEIGHT - 2;
3336
- const innerWidth = LEADER_WIDTH - 2;
3337
- drawBorder(buf, x, y, LEADER_WIDTH, COPY_HEIGHT, "cyan");
3338
- const lines = [
3339
- ansiColor(" COPY".padEnd(innerWidth), "cyan", true),
3340
- " ".padEnd(innerWidth),
3341
- " p session path".padEnd(innerWidth),
3342
- " C LLM context".padEnd(innerWidth),
3343
- " l logs content".padEnd(innerWidth),
3344
- " s session ID".padEnd(innerWidth),
3345
- ansiDim(" esc cancel".padEnd(innerWidth))
3346
- ];
3347
- for (let i = 0; i < lines.length; i++) {
3348
- writeClipped(buf, x + 1, y + 1 + i, lines[i], innerWidth);
3349
- }
3350
- }
3351
- function renderHelpOverlay(buf, rows, cols) {
3352
- const innerWidth = HELP_WIDTH - 2;
3353
- const x = Math.max(0, Math.floor((cols - HELP_WIDTH) / 2));
3354
- const contentLines = [
3355
- helpRow(" hjkl/\u2191\u2193\u2190\u2192 navigate", " tab switch pane", innerWidth),
3356
- helpRow(" enter expand/open", " t toggle logs", innerWidth),
3357
- " ".padEnd(innerWidth),
3358
- helpRow(" n new session", " m message orch.", innerWidth),
3359
- helpRow(" R resume session", " C continue session", innerWidth),
3360
- helpRow(" b rollback cycle", " x restart agent", innerWidth),
3361
- helpRow(" r re-run agent", " g edit goal", innerWidth),
3362
- helpRow(" p open roadmap", " s toggle strategy", innerWidth),
3363
- helpRow(" S edit strategy", " w go to window", innerWidth),
3364
- helpRow(" o resume claude session", " c claude companion", innerWidth),
3365
- helpRow(" q quit", "", innerWidth),
3366
- " ".padEnd(innerWidth),
3367
- helpRow(" space \u2192 y copy submenu", " space \u2192 d delete session", innerWidth),
3368
- helpRow(" space \u2192 j jump to pane", " space \u2192 k kill", innerWidth),
3369
- helpRow(" space \u2192 q quit", " space \u2192 o open dir", innerWidth),
3370
- helpRow(" space \u2192 l tail logs", " space \u2192 / search", innerWidth),
3371
- helpRow(" space \u2192 a spawn agent", " space \u2192 m msg agent", innerWidth),
3372
- helpRow(" space \u2192 ? help", " space \u2192 1-9 jump", innerWidth),
3373
- " ".padEnd(innerWidth),
3374
- helpRow(" y \u2192 p session path", " y \u2192 C LLM context", innerWidth),
3375
- helpRow(" y \u2192 l logs content", " y \u2192 s session ID", innerWidth)
3376
- ];
3377
- const height = Math.min(contentLines.length + 4, rows - 2);
3378
- const y = Math.max(0, Math.floor((rows - height) / 2));
3379
- drawBorder(buf, x, y, HELP_WIDTH, height, "yellow");
3380
- writeClipped(buf, x + 1, y + 1, ansiColor(" KEYBINDINGS (esc or ? to close)".padEnd(innerWidth), "yellow", true), innerWidth);
3381
- writeClipped(buf, x + 1, y + 2, " ".padEnd(innerWidth), innerWidth);
3382
- const availableContentRows = height - 4;
3383
- for (let i = 0; i < Math.min(contentLines.length, availableContentRows); i++) {
3384
- writeClipped(buf, x + 1, y + 3 + i, contentLines[i], innerWidth);
3385
- }
3386
- const trailingBlankRow = y + 3 + Math.min(contentLines.length, availableContentRows);
3387
- if (trailingBlankRow < y + height - 1) {
3388
- writeClipped(buf, x + 1, trailingBlankRow, " ".padEnd(innerWidth), innerWidth);
3389
- }
3390
- }
3391
-
3392
3693
  // src/tui/lib/nvim-bridge.ts
3393
3694
  import { execSync as execSync3 } from "child_process";
3394
3695
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2, readFileSync as readFileSync3, statSync, existsSync as existsSync3 } from "fs";
@@ -4180,6 +4481,19 @@ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
4180
4481
  }
4181
4482
 
4182
4483
  // src/tui/app.ts
4484
+ var _cachedCompanion = null;
4485
+ var _companionMtime = 0;
4486
+ function getCompanion() {
4487
+ try {
4488
+ const { mtimeMs } = statSync2(companionPath());
4489
+ if (_cachedCompanion && mtimeMs === _companionMtime) return _cachedCompanion;
4490
+ _companionMtime = mtimeMs;
4491
+ _cachedCompanion = JSON.parse(readFileSync4(companionPath(), "utf-8"));
4492
+ return _cachedCompanion;
4493
+ } catch {
4494
+ return _cachedCompanion;
4495
+ }
4496
+ }
4183
4497
  var latestNodes = [];
4184
4498
  var cachedContextFilePath = null;
4185
4499
  var cachedContextFileContent = null;
@@ -4262,7 +4576,7 @@ function startApp(state2, cleanup2) {
4262
4576
  const config = loadConfig(state2.cwd);
4263
4577
  const treeWidth = 36;
4264
4578
  const initialDetailW = state2.cols - treeWidth - 4;
4265
- const initialDetailH = state2.rows - 3 - 2 - STATUS_ROW_COUNT - 1;
4579
+ const initialDetailH = state2.rows - 1 - 2 - STATUS_ROW_COUNT - 1;
4266
4580
  const bridge = new NvimBridge(
4267
4581
  Math.max(1, initialDetailW),
4268
4582
  Math.max(1, initialDetailH),
@@ -4410,7 +4724,7 @@ function startApp(state2, cleanup2) {
4410
4724
  const remaining = state2.cols - treeWidth2;
4411
4725
  const detailWidth = state2.showCombinedView ? Math.floor(remaining * 0.6) : remaining;
4412
4726
  const logsWidth = state2.showCombinedView ? remaining - detailWidth : 0;
4413
- const contentHeight = state2.rows - 3;
4727
+ const contentHeight = state2.rows - 1;
4414
4728
  const treeRect = { x: 0, y: 0, w: treeWidth2, h: contentHeight };
4415
4729
  const detailRect = { x: treeWidth2, y: 0, w: detailWidth, h: contentHeight };
4416
4730
  const logsRect = state2.showCombinedView ? { x: treeWidth2 + detailWidth, y: 0, w: logsWidth, h: contentHeight } : null;
@@ -4487,15 +4801,24 @@ function startApp(state2, cleanup2) {
4487
4801
  }
4488
4802
  const treeFocused = state2.mode === "navigate" && state2.focusPane === "tree";
4489
4803
  const treeInputs = `${state2.treeCacheKey}:${state2.cursorIndex}:${treeFocused}`;
4490
- const bottomInputs = `${state2.notification}:${state2.error}:${state2.mode}:${state2.inputText}:${state2.inputCursorPos}:${cursorNode?.type}`;
4491
- const overlayMode = state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help" ? state2.mode : "";
4804
+ const bottomInputs = `${state2.notification}:${state2.error}:${state2.mode}:${state2.searchText}:${cursorNode?.type}`;
4805
+ const overlayMode = state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help" || state2.mode === "companion-overlay" || state2.mode === "companion-debug" ? state2.mode : "";
4806
+ let companionFP = "";
4807
+ if (state2.mode === "companion-overlay" || state2.mode === "companion-debug") {
4808
+ const c = getCompanion();
4809
+ const ts = c && c.lastCommentary ? c.lastCommentary.timestamp : "";
4810
+ const xp = c ? c.xp : 0;
4811
+ const dm = c?.debugMood ? `${c.debugMood.winner}:${c.debugMood.scores[c.debugMood.winner]}` : "";
4812
+ companionFP = `${ts}:${xp}:${dm}`;
4813
+ }
4814
+ const overlayInputs = `${overlayMode}:${companionFP}`;
4492
4815
  const hasPrev = prevFrame.length === buf.height;
4493
4816
  const treeDirty = !hasPrev || treeInputs !== prevTreeInputs;
4494
4817
  const bottomDirty = !hasPrev || bottomInputs !== prevBottomInputs;
4495
- const overlayDirty = !hasPrev || overlayMode !== prevOverlayMode;
4818
+ const overlayDirty = !hasPrev || overlayInputs !== prevOverlayMode;
4496
4819
  prevTreeInputs = treeInputs;
4497
4820
  prevBottomInputs = bottomInputs;
4498
- prevOverlayMode = overlayMode;
4821
+ prevOverlayMode = overlayInputs;
4499
4822
  let treeRows;
4500
4823
  if (treeDirty) {
4501
4824
  const treeBlank = " ".repeat(treeWidth2);
@@ -4509,7 +4832,8 @@ function startApp(state2, cleanup2) {
4509
4832
  { x: 0, y: 0, w: treeWidth2, h: contentHeight },
4510
4833
  nodes,
4511
4834
  state2.cursorIndex,
4512
- treeFocused
4835
+ treeFocused,
4836
+ getCompanion()
4513
4837
  );
4514
4838
  cachedTreeRows = treeBuf.lines;
4515
4839
  treeRows = treeBuf.lines;
@@ -4573,16 +4897,22 @@ function startApp(state2, cleanup2) {
4573
4897
  }
4574
4898
  }
4575
4899
  if (bottomDirty || overlayDirty) {
4576
- renderNotificationRow(buf, bottomY, state2.notification, state2.error);
4577
- renderInputBar(buf, bottomY + 1, state2);
4578
- renderStatusLine(buf, bottomY + 2, state2, cursorNode?.type);
4900
+ renderStatusLine(buf, bottomY, state2, cursorNode?.type);
4579
4901
  } else {
4580
- copyRows(buf, prevFrame, bottomY, 3);
4902
+ copyRows(buf, prevFrame, bottomY, 1);
4581
4903
  }
4582
4904
  if (overlayMode) {
4583
4905
  if (state2.mode === "leader") renderLeaderOverlay(buf, state2.rows, state2.cols);
4584
4906
  if (state2.mode === "copy-menu") renderCopyMenuOverlay(buf, state2.rows, state2.cols);
4585
4907
  if (state2.mode === "help") renderHelpOverlay(buf, state2.rows, state2.cols);
4908
+ if (state2.mode === "companion-overlay") {
4909
+ const companion = getCompanion();
4910
+ if (companion) renderCompanionOverlay(buf, state2.rows, state2.cols, companion);
4911
+ }
4912
+ if (state2.mode === "companion-debug") {
4913
+ const companion = getCompanion();
4914
+ if (companion) renderCompanionDebugOverlay(buf, state2.rows, state2.cols, companion);
4915
+ }
4586
4916
  }
4587
4917
  let cursorSuffix;
4588
4918
  if (state2.focusPane === "detail" && state2.nvimBridge?.ready) {
@@ -4652,7 +4982,7 @@ function startApp(state2, cleanup2) {
4652
4982
  prevFrame = [];
4653
4983
  if (state2.nvimBridge) {
4654
4984
  const detailW = state2.cols - 36;
4655
- const contentH = state2.rows - 3;
4985
+ const contentH = state2.rows - 1;
4656
4986
  state2.nvimBridge.resize(Math.max(1, detailW - 4), Math.max(1, contentH - 2 - STATUS_ROW_COUNT - 1));
4657
4987
  }
4658
4988
  requestRender();