sisyphi 1.1.9 → 1.1.11

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
@@ -5,7 +5,7 @@ import {
5
5
  exec,
6
6
  execSafe,
7
7
  loadConfig
8
- } from "./chunk-Z32YVDMY.js";
8
+ } from "./chunk-ZSIYQB45.js";
9
9
  import {
10
10
  buildSessionContext,
11
11
  computeActiveTimeMs,
@@ -13,7 +13,7 @@ import {
13
13
  rawSend,
14
14
  resolveReports,
15
15
  statusColor
16
- } from "./chunk-M7LZ2ZHD.js";
16
+ } from "./chunk-HQZOAX6D.js";
17
17
  import {
18
18
  shellQuote
19
19
  } from "./chunk-6G226ZK7.js";
@@ -24,8 +24,9 @@ import {
24
24
  logsDir,
25
25
  roadmapPath,
26
26
  sessionDir,
27
- strategyPath
28
- } from "./chunk-REUQ4B45.js";
27
+ strategyPath,
28
+ tuiScratchDir
29
+ } from "./chunk-GSXF3TCZ.js";
29
30
 
30
31
  // src/tui/terminal.ts
31
32
  function emptyKey() {
@@ -172,10 +173,18 @@ function parseBuffer(buf) {
172
173
  }
173
174
  return { events, remaining: "" };
174
175
  }
176
+ var rawBypassHandler = null;
177
+ function setRawBypass(handler) {
178
+ rawBypassHandler = handler;
179
+ }
175
180
  function startKeypressListener(handler) {
176
181
  let buffer = "";
177
182
  let escTimer = null;
178
183
  const onData = (data) => {
184
+ if (rawBypassHandler) {
185
+ const handled = rawBypassHandler(data);
186
+ if (handled) return;
187
+ }
179
188
  if (escTimer !== null) {
180
189
  clearTimeout(escTimer);
181
190
  escTimer = null;
@@ -311,8 +320,7 @@ function createAppState(cwd2) {
311
320
  targetAgentId: null,
312
321
  notification: null,
313
322
  notificationTimer: null,
314
- showLogs: false,
315
- showStrategy: false,
323
+ showCombinedView: false,
316
324
  inputText: "",
317
325
  inputCursorPos: 0,
318
326
  detailScroll,
@@ -333,6 +341,17 @@ function createAppState(cwd2) {
333
341
  cachedReportBlocks: /* @__PURE__ */ new Map(),
334
342
  cachedTreeNodes: null,
335
343
  treeCacheKey: "",
344
+ cachedDetailLines: null,
345
+ detailCacheKey: "",
346
+ detailRenderedCache: { lines: [], ansi: [] },
347
+ cachedLogsLines: null,
348
+ logsCacheKey: "",
349
+ logsRenderedCache: { lines: [], ansi: [] },
350
+ nvimBridge: null,
351
+ nvimEnabled: true,
352
+ prevNvimFile: null,
353
+ nvimEditable: false,
354
+ nvimOpenTabs: /* @__PURE__ */ new Map(),
336
355
  cwd: cwd2
337
356
  };
338
357
  }
@@ -396,8 +415,8 @@ function autoExpandCycle(state2) {
396
415
  }
397
416
 
398
417
  // src/tui/app.ts
399
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync, statSync } from "fs";
400
- import { join as join3 } from "path";
418
+ import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync, statSync } from "fs";
419
+ import { join as join5 } from "path";
401
420
 
402
421
  // src/tui/lib/tree.ts
403
422
  import { join } from "path";
@@ -785,6 +804,21 @@ function findParentIndex(nodes, index) {
785
804
  }
786
805
 
787
806
  // src/tui/input.ts
807
+ function activateNvimBypass(state2) {
808
+ setRawBypass((data) => {
809
+ if (data === " ") {
810
+ deactivateNvimBypass();
811
+ state2.focusPane = state2.showCombinedView ? "logs" : "tree";
812
+ requestRender();
813
+ return true;
814
+ }
815
+ state2.nvimBridge.write(data);
816
+ return true;
817
+ });
818
+ }
819
+ function deactivateNvimBypass() {
820
+ setRawBypass(null);
821
+ }
788
822
  function handleCancel(state2) {
789
823
  state2.mode = "navigate";
790
824
  state2.targetAgentId = null;
@@ -1285,10 +1319,14 @@ function handleNavigateKey(input, key, state2, actions) {
1285
1319
  if (key.leftArrow || input === "h") {
1286
1320
  if (state2.focusPane === "logs") {
1287
1321
  state2.focusPane = "detail";
1322
+ if (state2.nvimEnabled && state2.nvimBridge?.ready) {
1323
+ activateNvimBypass(state2);
1324
+ }
1288
1325
  requestRender();
1289
1326
  return;
1290
1327
  }
1291
1328
  if (state2.focusPane === "detail") {
1329
+ deactivateNvimBypass();
1292
1330
  state2.focusPane = "tree";
1293
1331
  requestRender();
1294
1332
  return;
@@ -1327,8 +1365,12 @@ function handleNavigateKey(input, key, state2, actions) {
1327
1365
  if (key.tab) {
1328
1366
  if (state2.focusPane === "tree") {
1329
1367
  state2.focusPane = "detail";
1368
+ if (state2.nvimEnabled && state2.nvimBridge?.ready) {
1369
+ activateNvimBypass(state2);
1370
+ }
1330
1371
  } else if (state2.focusPane === "detail") {
1331
- state2.focusPane = state2.showLogs ? "logs" : "tree";
1372
+ deactivateNvimBypass();
1373
+ state2.focusPane = state2.showCombinedView ? "logs" : "tree";
1332
1374
  } else {
1333
1375
  state2.focusPane = "tree";
1334
1376
  }
@@ -1571,15 +1613,6 @@ function handleNavigateKey(input, key, state2, actions) {
1571
1613
  }
1572
1614
  return;
1573
1615
  }
1574
- if (input === "s") {
1575
- if (!state2.strategyContent) {
1576
- notify(state2, "No strategy for this session");
1577
- return;
1578
- }
1579
- state2.showStrategy = !state2.showStrategy;
1580
- requestRender();
1581
- return;
1582
- }
1583
1616
  if (input === "S") {
1584
1617
  if (!state2.selectedSessionId) {
1585
1618
  notify(state2, "No session selected");
@@ -1595,11 +1628,11 @@ function handleNavigateKey(input, key, state2, actions) {
1595
1628
  return;
1596
1629
  }
1597
1630
  if (input === "t") {
1598
- if (state2.showLogs) {
1631
+ if (state2.showCombinedView) {
1599
1632
  if (state2.focusPane === "logs") state2.focusPane = "detail";
1600
1633
  state2.logsScroll.reset();
1601
1634
  }
1602
- state2.showLogs = !state2.showLogs;
1635
+ state2.showCombinedView = !state2.showCombinedView;
1603
1636
  requestRender();
1604
1637
  return;
1605
1638
  }
@@ -1651,15 +1684,23 @@ function renderLine(segs) {
1651
1684
  }
1652
1685
  return out;
1653
1686
  }
1687
+ var cachedBlank = "";
1688
+ var cachedBlankWidth = 0;
1654
1689
  function createFrameBuffer(width, height) {
1655
- const blank = " ".repeat(width);
1656
- return {
1657
- lines: Array.from({ length: height }, () => blank),
1658
- width,
1659
- height
1660
- };
1690
+ if (width !== cachedBlankWidth) {
1691
+ cachedBlank = " ".repeat(width);
1692
+ cachedBlankWidth = width;
1693
+ }
1694
+ const lines = new Array(height);
1695
+ for (let i = 0; i < height; i++) lines[i] = cachedBlank;
1696
+ return { lines, width, height };
1697
+ }
1698
+ function copyRows(buf, src, startRow, count) {
1699
+ for (let i = 0; i < count && startRow + i < buf.height; i++) {
1700
+ buf.lines[startRow + i] = src[startRow + i];
1701
+ }
1661
1702
  }
1662
- function flushFrame(frame, prevFrame2) {
1703
+ function flushFrame(frame, prevFrame2, suffix) {
1663
1704
  let out = "\x1B[?2026h";
1664
1705
  for (let i = 0; i < frame.length; i++) {
1665
1706
  if (frame[i] !== prevFrame2[i]) {
@@ -1668,10 +1709,76 @@ function flushFrame(frame, prevFrame2) {
1668
1709
  out += frame[i];
1669
1710
  }
1670
1711
  }
1712
+ if (suffix) out += suffix;
1671
1713
  out += "\x1B[?2026l";
1672
1714
  return out;
1673
1715
  }
1674
1716
  var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
1717
+ function clipAnsi(content, maxWidth) {
1718
+ let out = "";
1719
+ let displayWidth = 0;
1720
+ let i = 0;
1721
+ while (i < content.length) {
1722
+ if (content[i] === "\x1B" && content[i + 1] === "[") {
1723
+ const seqLen = ansiLen(content, i);
1724
+ if (seqLen > 0) {
1725
+ out += content.substring(i, i + seqLen);
1726
+ i += seqLen;
1727
+ continue;
1728
+ }
1729
+ }
1730
+ const cp = content.codePointAt(i);
1731
+ const ch = String.fromCodePoint(cp);
1732
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1733
+ if (displayWidth + chWidth > maxWidth) break;
1734
+ out += ch;
1735
+ displayWidth += chWidth;
1736
+ i += ch.length;
1737
+ }
1738
+ if (out.includes("\x1B[") && !out.endsWith("\x1B[0m")) {
1739
+ out += "\x1B[0m";
1740
+ }
1741
+ const remaining = maxWidth - displayWidth;
1742
+ if (remaining > 0) out += " ".repeat(remaining);
1743
+ return out;
1744
+ }
1745
+ function displayWidthFast(s) {
1746
+ let w = 0;
1747
+ let i = 0;
1748
+ while (i < s.length) {
1749
+ if (s[i] === "\x1B" && s[i + 1] === "[") {
1750
+ const len = ansiLen(s, i);
1751
+ if (len > 0) {
1752
+ i += len;
1753
+ continue;
1754
+ }
1755
+ }
1756
+ const cp = s.codePointAt(i);
1757
+ const ch = String.fromCodePoint(cp);
1758
+ w += cp < 128 ? 1 : stringWidth2(ch);
1759
+ i += ch.length;
1760
+ }
1761
+ return w;
1762
+ }
1763
+ function ansiLen(s, i) {
1764
+ let j = i + 2;
1765
+ const len = s.length;
1766
+ while (j < len) {
1767
+ const c = s.charCodeAt(j);
1768
+ if (c >= 48 && c <= 57 || c === 59) {
1769
+ j++;
1770
+ } else {
1771
+ break;
1772
+ }
1773
+ }
1774
+ if (j < len) {
1775
+ const c = s.charCodeAt(j);
1776
+ if (c >= 65 && c <= 90 || c >= 97 && c <= 122) {
1777
+ return j + 1 - i;
1778
+ }
1779
+ }
1780
+ return 0;
1781
+ }
1675
1782
  function writeAt(buf, x, y, content) {
1676
1783
  if (y < 0 || y >= buf.height) return;
1677
1784
  if (x < 0 || x >= buf.width) return;
@@ -1691,16 +1798,16 @@ function writeClipped(buf, x, y, content, maxWidth) {
1691
1798
  let i = 0;
1692
1799
  while (i < content.length) {
1693
1800
  if (content[i] === "\x1B" && content[i + 1] === "[") {
1694
- const match = content.slice(i).match(/^\x1b\[[0-9;]*[a-zA-Z]/);
1695
- if (match) {
1696
- out += match[0];
1697
- i += match[0].length;
1801
+ const seqLen = ansiLen(content, i);
1802
+ if (seqLen > 0) {
1803
+ out += content.substring(i, i + seqLen);
1804
+ i += seqLen;
1698
1805
  continue;
1699
1806
  }
1700
1807
  }
1701
1808
  const cp = content.codePointAt(i);
1702
1809
  const ch = String.fromCodePoint(cp);
1703
- const chWidth = stringWidth2(ch);
1810
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1704
1811
  if (displayWidth + chWidth > maxWidth) break;
1705
1812
  out += ch;
1706
1813
  displayWidth += chWidth;
@@ -1713,7 +1820,12 @@ function writeClipped(buf, x, y, content, maxWidth) {
1713
1820
  if (remaining > 0) {
1714
1821
  out += " ".repeat(remaining);
1715
1822
  }
1716
- writeAt(buf, x, y, out);
1823
+ const existing = buf.lines[y];
1824
+ const prefix = sliceDisplayCols(existing, 0, x);
1825
+ const suffix = sliceDisplayCols(existing, x + maxWidth, buf.width);
1826
+ const prefixDisplayW = displayWidthFast(prefix);
1827
+ const paddedPrefix = prefixDisplayW < x ? prefix + " ".repeat(x - prefixDisplayW) : prefix;
1828
+ buf.lines[y] = paddedPrefix + out + suffix;
1717
1829
  }
1718
1830
  function writeCenter(buf, row, content) {
1719
1831
  const textWidth = stringWidth2(content.replace(ANSI_RE, ""));
@@ -1730,28 +1842,75 @@ function drawBorder(buf, x, y, w, h, color) {
1730
1842
  writeAt(buf, x + w - 1, row, sgr + "\u2502" + reset);
1731
1843
  }
1732
1844
  }
1733
- function renderPanel(buf, rect, lines, scrollOffset, focused, borderColor) {
1734
- const { x, y, w, h } = rect;
1735
- drawBorder(buf, x, y, w, h, focused ? "blue" : borderColor);
1736
- const innerX = x + 2;
1845
+ function buildPanelRows(rect, lines, scrollOffset, focused, borderColor, renderedCache) {
1846
+ const { w, h } = rect;
1847
+ const rows = new Array(h);
1848
+ const color = focused ? "blue" : borderColor;
1849
+ const sgr = `\x1B[${colorToSGR(color)}m`;
1850
+ const reset = "\x1B[0m";
1737
1851
  const innerW = w - 4;
1738
- const innerY = y + 1;
1739
1852
  const innerH = h - 2;
1740
- if (innerW <= 0 || innerH <= 0) return;
1853
+ const blankInner = " ".repeat(innerW);
1854
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
1855
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
1856
+ const borderL = sgr + "\u2502" + reset + " ";
1857
+ const borderR = " " + sgr + "\u2502" + reset;
1858
+ const emptyRow = borderL + blankInner + borderR;
1859
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
1860
+ if (innerW <= 0 || innerH <= 0) return rows;
1861
+ let ansiLines;
1862
+ if (renderedCache && renderedCache.lines === lines) {
1863
+ ansiLines = renderedCache.ansi;
1864
+ } else {
1865
+ ansiLines = new Array(lines.length);
1866
+ for (let i = 0; i < lines.length; i++) {
1867
+ ansiLines[i] = renderLine(lines[i]);
1868
+ }
1869
+ if (renderedCache) {
1870
+ renderedCache.lines = lines;
1871
+ renderedCache.ansi = ansiLines;
1872
+ }
1873
+ }
1741
1874
  const hasOverflow = lines.length > innerH;
1742
1875
  const viewableH = hasOverflow ? innerH - 1 : innerH;
1743
1876
  const maxScroll = Math.max(0, lines.length - viewableH);
1744
1877
  const effectiveOffset = Math.min(scrollOffset, maxScroll);
1745
- const visible = lines.slice(effectiveOffset, effectiveOffset + viewableH);
1746
- for (let i = 0; i < visible.length; i++) {
1747
- const ansi = renderLine(visible[i]);
1748
- writeClipped(buf, innerX, innerY + i, ansi, innerW);
1878
+ for (let i = 0; i < viewableH && effectiveOffset + i < ansiLines.length; i++) {
1879
+ const clipped = clipAnsi(ansiLines[effectiveOffset + i], innerW);
1880
+ rows[1 + i] = borderL + clipped + borderR;
1749
1881
  }
1750
1882
  if (hasOverflow) {
1751
1883
  const scrollPct = maxScroll > 0 ? Math.round(effectiveOffset / maxScroll * 100) : 100;
1752
1884
  const indicator = ` \u2195 ${scrollPct}% \xB7 ${lines.length} lines`;
1753
- writeClipped(buf, innerX, innerY + viewableH, `\x1B[2m${indicator}\x1B[0m`, innerW);
1885
+ const clipped = clipAnsi(`\x1B[2m${indicator}\x1B[0m`, innerW);
1886
+ rows[1 + viewableH] = borderL + clipped + borderR;
1754
1887
  }
1888
+ return rows;
1889
+ }
1890
+ function buildEmptyPanelRows(rect, focused, borderColor, centerText) {
1891
+ const { w, h } = rect;
1892
+ const rows = new Array(h);
1893
+ const color = focused ? "blue" : borderColor;
1894
+ const sgr = `\x1B[${colorToSGR(color)}m`;
1895
+ const reset = "\x1B[0m";
1896
+ const innerW = w - 4;
1897
+ const borderL = sgr + "\u2502" + reset + " ";
1898
+ const borderR = " " + sgr + "\u2502" + reset;
1899
+ const emptyRow = borderL + " ".repeat(innerW) + borderR;
1900
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
1901
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
1902
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
1903
+ if (centerText) {
1904
+ const midRow = Math.floor(h / 2);
1905
+ if (midRow > 0 && midRow < h - 1) {
1906
+ const clipped = clipAnsi(centerText, innerW);
1907
+ const textW = displayWidthFast(centerText);
1908
+ const pad = Math.max(0, Math.floor((innerW - textW) / 2));
1909
+ const centered = " ".repeat(pad) + clipped;
1910
+ rows[midRow] = borderL + clipAnsi(centered, innerW) + borderR;
1911
+ }
1912
+ }
1913
+ return rows;
1755
1914
  }
1756
1915
  function sliceDisplayCols(s, start, end) {
1757
1916
  let out = "";
@@ -1761,19 +1920,20 @@ function sliceDisplayCols(s, start, end) {
1761
1920
  let hasOpenSGR = false;
1762
1921
  while (i < s.length && col < end) {
1763
1922
  if (s[i] === "\x1B" && s[i + 1] === "[") {
1764
- const match = s.slice(i).match(/^\x1b\[[0-9;]*[a-zA-Z]/);
1765
- if (match) {
1923
+ const seqLen = ansiLen(s, i);
1924
+ if (seqLen > 0) {
1766
1925
  if (col >= start) {
1767
- out += match[0];
1768
- hasOpenSGR = match[0] !== "\x1B[0m" && match[0] !== "\x1B[m";
1926
+ const seq = s.substring(i, i + seqLen);
1927
+ out += seq;
1928
+ hasOpenSGR = seq !== "\x1B[0m" && seq !== "\x1B[m";
1769
1929
  }
1770
- i += match[0].length;
1930
+ i += seqLen;
1771
1931
  continue;
1772
1932
  }
1773
1933
  }
1774
1934
  const cp = s.codePointAt(i);
1775
1935
  const ch = String.fromCodePoint(cp);
1776
- const chWidth = stringWidth2(ch);
1936
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1777
1937
  if (col >= start) {
1778
1938
  inSlice = true;
1779
1939
  if (col + chWidth > end) break;
@@ -1788,6 +1948,76 @@ function sliceDisplayCols(s, start, end) {
1788
1948
  return out;
1789
1949
  }
1790
1950
 
1951
+ // src/tui/lib/tree-render.ts
1952
+ function renderTreePrefix(node, nodes, index) {
1953
+ if (node.depth === 0) {
1954
+ return node.expandable ? node.expanded ? "\u25BC " : "\u25B8 " : " ";
1955
+ }
1956
+ const parts = [];
1957
+ for (let d = 1; d < node.depth; d++) {
1958
+ parts.push(isAncestorLastSibling(nodes, index, d) ? " " : "\u2502 ");
1959
+ }
1960
+ parts.push(isLastSibling(nodes, index) ? "\u2514\u2500" : "\u251C\u2500");
1961
+ if (node.expandable) {
1962
+ parts.push(node.expanded ? "\u25BC " : "\u25B8 ");
1963
+ } else {
1964
+ parts.push(" ");
1965
+ }
1966
+ return parts.join("");
1967
+ }
1968
+ function isLastSibling(nodes, index) {
1969
+ const depth = nodes[index].depth;
1970
+ for (let i = index + 1; i < nodes.length; i++) {
1971
+ if (nodes[i].depth === depth) return false;
1972
+ if (nodes[i].depth < depth) return true;
1973
+ }
1974
+ return true;
1975
+ }
1976
+ function isAncestorLastSibling(nodes, index, depth) {
1977
+ for (let i = index - 1; i >= 0; i--) {
1978
+ if (nodes[i].depth === depth) {
1979
+ return isLastSibling(nodes, i);
1980
+ }
1981
+ if (nodes[i].depth < depth) return true;
1982
+ }
1983
+ return true;
1984
+ }
1985
+ function precomputePrefixes(nodes) {
1986
+ const len = nodes.length;
1987
+ if (len === 0) return;
1988
+ const isLast = new Array(len);
1989
+ const lastSeenAtDepth = /* @__PURE__ */ new Map();
1990
+ for (let i = len - 1; i >= 0; i--) {
1991
+ const depth = nodes[i].depth;
1992
+ isLast[i] = !lastSeenAtDepth.has(depth);
1993
+ lastSeenAtDepth.set(depth, i);
1994
+ for (const [d] of lastSeenAtDepth) {
1995
+ if (d > depth) lastSeenAtDepth.delete(d);
1996
+ }
1997
+ }
1998
+ const ancestorIsLast = [];
1999
+ for (let i = 0; i < len; i++) {
2000
+ const node = nodes[i];
2001
+ if (node.depth === 0) {
2002
+ node.prefix = node.expandable ? node.expanded ? "\u25BC " : "\u25B8 " : " ";
2003
+ ancestorIsLast[0] = isLast[i];
2004
+ continue;
2005
+ }
2006
+ ancestorIsLast[node.depth] = isLast[i];
2007
+ const parts = [];
2008
+ for (let d = 1; d < node.depth; d++) {
2009
+ parts.push(ancestorIsLast[d] ? " " : "\u2502 ");
2010
+ }
2011
+ parts.push(isLast[i] ? "\u2514\u2500" : "\u251C\u2500");
2012
+ if (node.expandable) {
2013
+ parts.push(node.expanded ? "\u25BC " : "\u25B8 ");
2014
+ } else {
2015
+ parts.push(" ");
2016
+ }
2017
+ node.prefix = parts.join("");
2018
+ }
2019
+ }
2020
+
1791
2021
  // src/tui/lib/client.ts
1792
2022
  function send(request) {
1793
2023
  return rawSend(request, 5e3);
@@ -1804,8 +2034,13 @@ function selectWindow(windowId) {
1804
2034
  function selectPane(paneId) {
1805
2035
  execSafe(`tmux select-pane -t "${paneId}"`);
1806
2036
  }
1807
- function windowExists(windowId) {
1808
- return execSafe(`tmux display-message -t "${windowId}" -p "#{window_id}"`) !== null;
2037
+ function listAllWindowIds() {
2038
+ try {
2039
+ const output = execSync('tmux list-windows -a -F "#{window_id}"', { encoding: "utf-8", env: EXEC_ENV });
2040
+ return new Set(output.trim().split("\n").filter(Boolean));
2041
+ } catch {
2042
+ return /* @__PURE__ */ new Set();
2043
+ }
1809
2044
  }
1810
2045
  var companionPaneId = null;
1811
2046
  function setupCompanionPlugin() {
@@ -1901,41 +2136,6 @@ function copyToClipboard(text) {
1901
2136
  execSync2("pbcopy", { input: text });
1902
2137
  }
1903
2138
 
1904
- // src/tui/lib/tree-render.ts
1905
- function renderTreePrefix(node, nodes, index) {
1906
- if (node.depth === 0) {
1907
- return node.expandable ? node.expanded ? "\u25BC " : "\u25B8 " : " ";
1908
- }
1909
- const parts = [];
1910
- for (let d = 1; d < node.depth; d++) {
1911
- parts.push(isAncestorLastSibling(nodes, index, d) ? " " : "\u2502 ");
1912
- }
1913
- parts.push(isLastSibling(nodes, index) ? "\u2514\u2500" : "\u251C\u2500");
1914
- if (node.expandable) {
1915
- parts.push(node.expanded ? "\u25BC " : "\u25B8 ");
1916
- } else {
1917
- parts.push(" ");
1918
- }
1919
- return parts.join("");
1920
- }
1921
- function isLastSibling(nodes, index) {
1922
- const depth = nodes[index].depth;
1923
- for (let i = index + 1; i < nodes.length; i++) {
1924
- if (nodes[i].depth === depth) return false;
1925
- if (nodes[i].depth < depth) return true;
1926
- }
1927
- return true;
1928
- }
1929
- function isAncestorLastSibling(nodes, index, depth) {
1930
- for (let i = index - 1; i >= 0; i--) {
1931
- if (nodes[i].depth === depth) {
1932
- return isLastSibling(nodes, i);
1933
- }
1934
- if (nodes[i].depth < depth) return true;
1935
- }
1936
- return true;
1937
- }
1938
-
1939
2139
  // src/tui/panels/tree.ts
1940
2140
  function renderNodeContent(node, maxWidth) {
1941
2141
  switch (node.type) {
@@ -2075,7 +2275,7 @@ function renderTreePanel(buf, rect, nodes, cursorIndex, focused) {
2075
2275
  const node = visible[i];
2076
2276
  const realIdx = scrollOffset + i;
2077
2277
  const isSelected = realIdx === cursorIndex;
2078
- const prefix = renderTreePrefix(node, nodes, realIdx);
2278
+ const prefix = node.prefix ?? renderTreePrefix(node, nodes, realIdx);
2079
2279
  const contentWidth = innerW;
2080
2280
  const { icon, label, meta, color, dim, metaColor, suffix, suffixColor } = renderNodeContent(
2081
2281
  node,
@@ -2187,7 +2387,7 @@ function buildPlanLines(content, maxLines, width) {
2187
2387
  }
2188
2388
  return lines;
2189
2389
  }
2190
- function buildSessionLines(session, planContent, goalContent, width, paneAlive, strategyContent = "", showStrategy = false) {
2390
+ function buildSessionLines(session, planContent, goalContent, width, paneAlive, strategyContent = "") {
2191
2391
  const lines = [];
2192
2392
  const contentWidth = width - 4;
2193
2393
  const agents = session.agents;
@@ -2228,12 +2428,8 @@ function buildSessionLines(session, planContent, goalContent, width, paneAlive,
2228
2428
  ]);
2229
2429
  }
2230
2430
  lines.push(singleLine(" "));
2231
- if (showStrategy && strategyContent) {
2232
- const stratHint = strategyContent ? " [s] toggle plan" : "";
2233
- lines.push([
2234
- seg(" \u258E \u25C8 STRATEGY", { color: "yellow", bold: true }),
2235
- seg(stratHint, { dim: true })
2236
- ]);
2431
+ if (strategyContent) {
2432
+ lines.push([seg(" \u258E \u25C8 STRATEGY", { color: "yellow", bold: true })]);
2237
2433
  const stratLines = buildPlanLines(strategyContent, 99999, width);
2238
2434
  if (stratLines.length === 0) {
2239
2435
  lines.push(singleLine(" (empty)", { dim: true, italic: true }));
@@ -2243,11 +2439,7 @@ function buildSessionLines(session, planContent, goalContent, width, paneAlive,
2243
2439
  }
2244
2440
  }
2245
2441
  } else {
2246
- const toggleHint = strategyContent ? " [s] toggle strategy" : "";
2247
- lines.push([
2248
- seg(" \u258E \u25C8 PLAN", { color: "yellow", bold: true }),
2249
- seg(toggleHint, { dim: true })
2250
- ]);
2442
+ lines.push([seg(" \u258E \u25C8 PLAN", { color: "yellow", bold: true })]);
2251
2443
  const planLines = buildPlanLines(planContent, 99999, width);
2252
2444
  if (planLines.length === 0) {
2253
2445
  lines.push(singleLine(" orchestrator will create one", { dim: true, italic: true }));
@@ -2562,7 +2754,7 @@ function buildLogsLines(cycleLogs, width) {
2562
2754
  }
2563
2755
  return lines;
2564
2756
  }
2565
- function renderDetailContent(buf, rect, state2, detailCtx) {
2757
+ function renderDetailRows(rect, state2, detailCtx) {
2566
2758
  const { session, agents, reportBlocks, detailReportBlocks, contextFileContent } = detailCtx;
2567
2759
  const scrollOffset = state2.detailScroll.offset;
2568
2760
  const focused = state2.focusPane === "detail";
@@ -2570,157 +2762,248 @@ function renderDetailContent(buf, rect, state2, detailCtx) {
2570
2762
  const reportAgent = agents.find((a) => a.id === state2.targetAgentId);
2571
2763
  if (reportAgent) {
2572
2764
  const lines2 = buildReportViewLines(reportAgent, reportBlocks, rect.w);
2573
- renderPanel(buf, rect, lines2, scrollOffset, focused, "cyan");
2574
- return;
2765
+ return buildPanelRows(rect, lines2, scrollOffset, focused, "cyan");
2575
2766
  }
2576
2767
  }
2577
2768
  const cursorNode = detailCtx.nodes[state2.cursorIndex];
2578
2769
  if (!cursorNode || !session) {
2579
- drawBorder(buf, rect.x, rect.y, rect.w, rect.h, "gray");
2580
- const midRow = rect.y + Math.floor(rect.h / 2);
2581
- writeCenter(buf, midRow, "\x1B[2mSelect a session to view details\x1B[0m");
2582
- return;
2583
- }
2770
+ return buildEmptyPanelRows(rect, false, "gray", "\x1B[2mSelect a session to view details\x1B[0m");
2771
+ }
2772
+ if (cursorNode.sessionId !== session.id) {
2773
+ return buildEmptyPanelRows(rect, false, "gray");
2774
+ }
2775
+ const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
2776
+ const cacheKey = [
2777
+ cursorNode.id,
2778
+ cursorNode.type,
2779
+ state2.mode,
2780
+ state2.targetAgentId,
2781
+ rect.w,
2782
+ session.id,
2783
+ session.agents.length,
2784
+ session.orchestratorCycles.length,
2785
+ lastCycle?.completedAt ?? "",
2786
+ lastCycle?.agentsSpawned.length ?? 0,
2787
+ state2.planContent.length,
2788
+ state2.goalContent.length,
2789
+ state2.strategyContent.length,
2790
+ state2.paneAlive,
2791
+ detailReportBlocks.length,
2792
+ session.messages.length,
2793
+ state2.contextFiles.length,
2794
+ contextFileContent?.length ?? -1
2795
+ ].join(":");
2584
2796
  let lines;
2585
2797
  let borderColor = "gray";
2586
- switch (cursorNode.type) {
2587
- case "session": {
2588
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2589
- break;
2590
- }
2591
- case "cycle": {
2592
- const cycleNode = cursorNode;
2593
- const cycle = session.orchestratorCycles.find((c) => c.cycle === cycleNode.cycleNumber);
2594
- if (!cycle) {
2595
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2596
- } else {
2597
- lines = buildCycleLines(cycle, session.agents, rect.w);
2798
+ if (cacheKey === state2.detailCacheKey && state2.cachedDetailLines !== null) {
2799
+ lines = state2.cachedDetailLines;
2800
+ } else {
2801
+ switch (cursorNode.type) {
2802
+ case "session": {
2803
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2804
+ break;
2598
2805
  }
2599
- break;
2600
- }
2601
- case "agent": {
2602
- const agentNode = cursorNode;
2603
- const agent = agents.find((a) => a.id === agentNode.agentId);
2604
- if (!agent) {
2605
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2606
- } else {
2607
- lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2806
+ case "cycle": {
2807
+ const cycleNode = cursorNode;
2808
+ const cycle = session.orchestratorCycles.find((c) => c.cycle === cycleNode.cycleNumber);
2809
+ if (!cycle) {
2810
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2811
+ } else {
2812
+ lines = buildCycleLines(cycle, session.agents, rect.w);
2813
+ }
2814
+ break;
2608
2815
  }
2609
- break;
2610
- }
2611
- case "report": {
2612
- const reportNode = cursorNode;
2613
- const agent = agents.find((a) => a.id === reportNode.agentId);
2614
- if (!agent) {
2615
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2816
+ case "agent": {
2817
+ const agentNode = cursorNode;
2818
+ const agent = agents.find((a) => a.id === agentNode.agentId);
2819
+ if (!agent) {
2820
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2821
+ } else {
2822
+ lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2823
+ }
2616
2824
  break;
2617
2825
  }
2618
- const reportIdx = reportNode.reportIndex;
2619
- const specificBlock = detailReportBlocks.find((_b, i) => {
2620
- const originalIdx = agent.reports.length - 1 - i;
2621
- return originalIdx === reportIdx;
2622
- });
2623
- if (specificBlock) {
2624
- const { label: badge, color: badgeColor } = reportBadge(specificBlock.type);
2625
- lines = [
2626
- [seg(" "), seg(badge, { color: badgeColor }), seg(` ${agent.id} \xB7 ${agentDisplayName(agent)}`, { bold: true })],
2627
- singleLine(` ${formatTime(specificBlock.timestamp)}`, { dim: true }),
2628
- singleLine(" "),
2629
- [seg(" \u258E CONTENT", { color: badgeColor, bold: true })],
2630
- ...wrapText(specificBlock.content.trim(), rect.w - 8).map((l) => singleLine(` ${l}`))
2631
- ];
2632
- borderColor = badgeColor;
2633
- } else {
2634
- lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2826
+ case "report": {
2827
+ const reportNode = cursorNode;
2828
+ const agent = agents.find((a) => a.id === reportNode.agentId);
2829
+ if (!agent) {
2830
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2831
+ break;
2832
+ }
2833
+ const reportIdx = reportNode.reportIndex;
2834
+ const specificBlock = detailReportBlocks.find((_b, i) => {
2835
+ const originalIdx = agent.reports.length - 1 - i;
2836
+ return originalIdx === reportIdx;
2837
+ });
2838
+ if (specificBlock) {
2839
+ const { label: badge, color: badgeColor } = reportBadge(specificBlock.type);
2840
+ lines = [
2841
+ [seg(" "), seg(badge, { color: badgeColor }), seg(` ${agent.id} \xB7 ${agentDisplayName(agent)}`, { bold: true })],
2842
+ singleLine(` ${formatTime(specificBlock.timestamp)}`, { dim: true }),
2843
+ singleLine(" "),
2844
+ [seg(" \u258E CONTENT", { color: badgeColor, bold: true })],
2845
+ ...wrapText(specificBlock.content.trim(), rect.w - 8).map((l) => singleLine(` ${l}`))
2846
+ ];
2847
+ borderColor = badgeColor;
2848
+ } else {
2849
+ lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2850
+ }
2851
+ break;
2635
2852
  }
2636
- break;
2637
- }
2638
- case "messages": {
2639
- lines = [singleLine(` Messages (${session.messages.length})`, { bold: true })];
2640
- if (session.messages.length === 0) {
2641
- lines.push(singleLine(" No messages", { dim: true, italic: true }));
2642
- } else {
2643
- for (const msg of session.messages) {
2644
- const time = formatTime(msg.timestamp);
2645
- const agentId = msg.source.type === "agent" ? msg.source.agentId : void 0;
2646
- const label = messageSourceLabel(msg.source.type, agentId);
2647
- const labelColor = messageSourceColor(msg.source.type);
2648
- const maxContent = Math.max(10, rect.w - label.length - 20);
2649
- lines.push([
2650
- seg(` [${time}] `, { dim: true }),
2651
- seg(`${label}: `, { color: labelColor, bold: true }),
2652
- seg(wrapText(msg.summary.length > 0 ? msg.summary : msg.content, maxContent)[0], {})
2653
- ]);
2853
+ case "messages": {
2854
+ lines = [singleLine(` Messages (${session.messages.length})`, { bold: true })];
2855
+ if (session.messages.length === 0) {
2856
+ lines.push(singleLine(" No messages", { dim: true, italic: true }));
2857
+ } else {
2858
+ for (const msg of session.messages) {
2859
+ const time = formatTime(msg.timestamp);
2860
+ const agentId = msg.source.type === "agent" ? msg.source.agentId : void 0;
2861
+ const label = messageSourceLabel(msg.source.type, agentId);
2862
+ const labelColor = messageSourceColor(msg.source.type);
2863
+ const maxContent = Math.max(10, rect.w - label.length - 20);
2864
+ lines.push([
2865
+ seg(` [${time}] `, { dim: true }),
2866
+ seg(`${label}: `, { color: labelColor, bold: true }),
2867
+ seg(wrapText(msg.summary.length > 0 ? msg.summary : msg.content, maxContent)[0], {})
2868
+ ]);
2869
+ }
2654
2870
  }
2871
+ break;
2655
2872
  }
2656
- break;
2657
- }
2658
- case "message": {
2659
- const msgNode = cursorNode;
2660
- const msg = session.messages.find((m) => m.id === msgNode.messageId);
2661
- lines = [singleLine(" Message", { bold: true })];
2662
- if (msg) {
2663
- lines.push(singleLine(` ${msgNode.source} \xB7 ${msgNode.timestamp}`, { dim: true }));
2664
- for (const l of wrapText(msg.content, rect.w - 8)) {
2665
- lines.push(singleLine(` ${l}`));
2873
+ case "message": {
2874
+ const msgNode = cursorNode;
2875
+ const msg = session.messages.find((m) => m.id === msgNode.messageId);
2876
+ lines = [singleLine(" Message", { bold: true })];
2877
+ if (msg) {
2878
+ lines.push(singleLine(` ${msgNode.source} \xB7 ${msgNode.timestamp}`, { dim: true }));
2879
+ for (const l of wrapText(msg.content, rect.w - 8)) {
2880
+ lines.push(singleLine(` ${l}`));
2881
+ }
2882
+ } else {
2883
+ lines.push(singleLine(" Message not found", { dim: true }));
2666
2884
  }
2667
- } else {
2668
- lines.push(singleLine(" Message not found", { dim: true }));
2885
+ break;
2669
2886
  }
2670
- break;
2671
- }
2672
- case "context": {
2673
- lines = [
2674
- [seg(" "), seg("\u229E", { color: "white" }), seg(` Context (${state2.contextFiles.length})`, { bold: true })]
2675
- ];
2676
- if (state2.contextFiles.length === 0) {
2677
- lines.push(singleLine(" No context files found.", { dim: true }));
2678
- } else {
2679
- for (const f of state2.contextFiles) {
2680
- lines.push(singleLine(` \xB7 ${f}`, { dim: true }));
2887
+ case "context": {
2888
+ lines = [
2889
+ [seg(" "), seg("\u229E", { color: "white" }), seg(` Context (${state2.contextFiles.length})`, { bold: true })]
2890
+ ];
2891
+ if (state2.contextFiles.length === 0) {
2892
+ lines.push(singleLine(" No context files found.", { dim: true }));
2893
+ } else {
2894
+ for (const f of state2.contextFiles) {
2895
+ lines.push(singleLine(` \xB7 ${f}`, { dim: true }));
2896
+ }
2681
2897
  }
2898
+ break;
2682
2899
  }
2683
- break;
2684
- }
2685
- case "context-file": {
2686
- const ctxFileNode = cursorNode;
2687
- lines = [
2688
- [seg(" "), seg("\u229E", { color: "white" }), seg(` ${ctxFileNode.label}`, { bold: true })],
2689
- singleLine(" ")
2690
- ];
2691
- if (contextFileContent == null) {
2692
- lines.push(singleLine(" File not found or unreadable.", { dim: true }));
2693
- } else {
2694
- const wrapped = wrapText(stripFrontmatter(contextFileContent), rect.w - 8);
2695
- if (wrapped.length === 0) {
2696
- lines.push(singleLine(" (empty)", { dim: true }));
2900
+ case "context-file": {
2901
+ const ctxFileNode = cursorNode;
2902
+ lines = [
2903
+ [seg(" "), seg("\u229E", { color: "white" }), seg(` ${ctxFileNode.label}`, { bold: true })],
2904
+ singleLine(" ")
2905
+ ];
2906
+ if (contextFileContent == null) {
2907
+ lines.push(singleLine(" File not found or unreadable.", { dim: true }));
2697
2908
  } else {
2698
- for (const l of wrapped) {
2699
- lines.push(singleLine(` ${l}`));
2909
+ const wrapped = wrapText(stripFrontmatter(contextFileContent), rect.w - 8);
2910
+ if (wrapped.length === 0) {
2911
+ lines.push(singleLine(" (empty)", { dim: true }));
2912
+ } else {
2913
+ for (const l of wrapped) {
2914
+ lines.push(singleLine(` ${l}`));
2915
+ }
2700
2916
  }
2701
2917
  }
2918
+ borderColor = "white";
2919
+ break;
2920
+ }
2921
+ default: {
2922
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2923
+ break;
2702
2924
  }
2703
- borderColor = "white";
2704
- break;
2705
2925
  }
2706
- default: {
2707
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2708
- break;
2926
+ state2.cachedDetailLines = lines;
2927
+ state2.detailCacheKey = cacheKey;
2928
+ }
2929
+ if (cursorNode.type === "context-file") {
2930
+ borderColor = "white";
2931
+ } else if (cursorNode.type === "report") {
2932
+ const reportNode = cursorNode;
2933
+ const agent = agents.find((a) => a.id === reportNode.agentId);
2934
+ if (agent) {
2935
+ const reportIdx = reportNode.reportIndex;
2936
+ const specificBlock = detailReportBlocks.find((_b, i) => {
2937
+ const originalIdx = agent.reports.length - 1 - i;
2938
+ return originalIdx === reportIdx;
2939
+ });
2940
+ if (specificBlock) borderColor = reportBadge(specificBlock.type).color;
2709
2941
  }
2710
2942
  }
2711
- renderPanel(buf, rect, lines, scrollOffset, focused, borderColor);
2943
+ return buildPanelRows(rect, lines, scrollOffset, focused, borderColor, state2.detailRenderedCache);
2712
2944
  }
2713
- function renderLogsContent(buf, rect, state2) {
2945
+ function renderLogsRows(rect, state2) {
2714
2946
  const focused = state2.focusPane === "logs";
2715
2947
  const scrollOffset = state2.logsScroll.offset;
2716
2948
  if (state2.logsCycles.length === 0) {
2717
- drawBorder(buf, rect.x, rect.y, rect.w, rect.h, focused ? "blue" : "gray");
2718
- const midRow = rect.y + Math.floor(rect.h / 2);
2719
- writeCenter(buf, midRow, "\x1B[2mNo logs\x1B[0m");
2720
- return;
2949
+ return buildEmptyPanelRows(rect, focused, "gray", "\x1B[2mNo logs\x1B[0m");
2950
+ }
2951
+ const logsCacheKey = `${state2.logsCycles.length}:${rect.w}:${state2.logsCycles.map((c) => c.cycle).join(",")}`;
2952
+ let lines;
2953
+ if (logsCacheKey === state2.logsCacheKey && state2.cachedLogsLines !== null) {
2954
+ lines = state2.cachedLogsLines;
2955
+ } else {
2956
+ lines = buildLogsLines(state2.logsCycles, rect.w);
2957
+ state2.cachedLogsLines = lines;
2958
+ state2.logsCacheKey = logsCacheKey;
2721
2959
  }
2722
- const lines = buildLogsLines(state2.logsCycles, rect.w);
2723
- renderPanel(buf, rect, lines, scrollOffset, focused, "gray");
2960
+ return buildPanelRows(rect, lines, scrollOffset, focused, "gray", state2.logsRenderedCache);
2961
+ }
2962
+
2963
+ // src/tui/panels/nvim-detail.ts
2964
+ function renderNvimDetailRows(rect, bridge, focused, editable, statusRows) {
2965
+ const { w, h } = rect;
2966
+ const rows = new Array(h);
2967
+ const borderColor = focused ? "cyan" : "gray";
2968
+ const sgr = `\x1B[${colorToSGR(borderColor)}m`;
2969
+ const reset = "\x1B[0m";
2970
+ const innerW = w - 4;
2971
+ if (focused) {
2972
+ const badgeText = editable ? " EDIT " : " NVIM ";
2973
+ const badgeLen = badgeText.length;
2974
+ const dashesLeft = 2;
2975
+ const dashesRight = Math.max(0, w - 2 - dashesLeft - badgeLen);
2976
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(dashesLeft) + reset + `\x1B[${colorToSGR("cyan")};1m` + badgeText + reset + sgr + "\u2500".repeat(dashesRight) + "\u256E" + reset;
2977
+ } else {
2978
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
2979
+ }
2980
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
2981
+ const borderL = sgr + "\u2502" + reset + " ";
2982
+ const borderR = " " + sgr + "\u2502" + reset;
2983
+ const blankInner = " ".repeat(innerW);
2984
+ const emptyRow = borderL + blankInner + borderR;
2985
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
2986
+ if (innerW <= 0 || h <= 2) return rows;
2987
+ const statusCount = statusRows.length;
2988
+ for (let i = 0; i < statusCount && i < h - 3; i++) {
2989
+ const clipped = clipAnsi(statusRows[i], innerW);
2990
+ rows[1 + i] = borderL + clipped + borderR;
2991
+ }
2992
+ const separatorRow = 1 + statusCount;
2993
+ if (separatorRow < h - 1) {
2994
+ rows[separatorRow] = sgr + "\u251C" + "\u2500".repeat(w - 2) + "\u2524" + reset;
2995
+ }
2996
+ const nvimStartRow = separatorRow + 1;
2997
+ const nvimRows = bridge.getRows();
2998
+ for (let i = nvimStartRow; i < h - 1; i++) {
2999
+ const nvimIdx = i - nvimStartRow;
3000
+ const nvimRow = nvimRows[nvimIdx];
3001
+ if (nvimRow !== void 0) {
3002
+ const clipped = clipAnsi(nvimRow, innerW);
3003
+ rows[i] = borderL + clipped + borderR;
3004
+ }
3005
+ }
3006
+ return rows;
2724
3007
  }
2725
3008
 
2726
3009
  // src/tui/panels/bottom.ts
@@ -2777,13 +3060,13 @@ function renderStatusLine(buf, y, state2, cursorNodeType) {
2777
3060
  } else if (mode !== "navigate") {
2778
3061
  content = D("[enter] send [esc] cancel");
2779
3062
  } else if (focusPane === "logs" || focusPane === "detail") {
2780
- content = B("[jk/\u2191\u2193]") + D(" scroll ") + B("[h/\u2190/tab]") + D(" back ") + B("[t]") + D("oggle logs ") + SEP + B("[m]") + D("sg ") + B("[g]") + D("oal ") + B("[n]") + D("ew ") + B("[p]") + D("lan ") + B("[s]") + D("trat ") + B("[w]") + D("indow ") + B("[R]") + D("esume ") + B("[q]") + D("uit");
3063
+ 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");
2781
3064
  } else {
2782
3065
  let contextFilePart = "";
2783
3066
  if (cursorNodeType === "context-file") {
2784
3067
  contextFilePart = B("[e]") + D("dit ") + B("[\u23CE]") + D(" open ");
2785
3068
  }
2786
- content = B("[hjkl]") + D(" navigate ") + SEP + contextFilePart + B("[space]") + D(" leader ") + B("[tab]") + D(" detail ") + B("[t]") + D("oggle logs ") + SEP + B("[m]") + D("sg ") + B("[n]") + D("ew ") + B("[R]") + D("esume ") + B("[q]") + D("uit");
3069
+ content = B("[hjkl]") + D(" navigate ") + SEP + contextFilePart + B("[space]") + D(" leader ") + B("[tab]") + D(" detail ") + B("[t]") + D("oggle view ") + SEP + B("[m]") + D("sg ") + B("[n]") + D("ew ") + B("[R]") + D("esume ") + B("[q]") + D("uit");
2787
3070
  }
2788
3071
  writeClipped(buf, 1, y, content, buf.width - 2);
2789
3072
  }
@@ -2884,13 +3167,662 @@ function renderHelpOverlay(buf, rows, cols) {
2884
3167
  }
2885
3168
  }
2886
3169
 
3170
+ // src/tui/lib/nvim-bridge.ts
3171
+ import { execSync as execSync3 } from "child_process";
3172
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, unlinkSync } from "fs";
3173
+ import { join as join3 } from "path";
3174
+ import { tmpdir as tmpdir2 } from "os";
3175
+ var NvimBridge = class {
3176
+ pty = null;
3177
+ xterm = null;
3178
+ _cols;
3179
+ _rows;
3180
+ onRender;
3181
+ renderTimer = null;
3182
+ currentFile = null;
3183
+ ready = false;
3184
+ dirty = true;
3185
+ available = false;
3186
+ /** DECSCUSR cursor style: 0=default, 1=blinking block, 2=steady block, 3=blinking underline, 4=steady underline, 5=blinking bar, 6=steady bar */
3187
+ cursorStyle = 0;
3188
+ cachedRows = null;
3189
+ nvimPath = "nvim";
3190
+ pendingFiles = null;
3191
+ fileDebounceTimer = null;
3192
+ cmdFile;
3193
+ constructor(cols, rows, onRender) {
3194
+ this._cols = cols;
3195
+ this._rows = rows;
3196
+ this.onRender = onRender;
3197
+ const cmdDir = join3(tmpdir2(), "sisyphus-nvim");
3198
+ mkdirSync2(cmdDir, { recursive: true });
3199
+ this.cmdFile = join3(cmdDir, `cmd-${process.pid}.lua`);
3200
+ try {
3201
+ this.nvimPath = execSync3("which nvim", { stdio: "pipe" }).toString().trim();
3202
+ this.available = true;
3203
+ } catch {
3204
+ this.available = false;
3205
+ return;
3206
+ }
3207
+ this.spawn().catch(() => {
3208
+ this.available = false;
3209
+ this.ready = false;
3210
+ });
3211
+ }
3212
+ async spawn() {
3213
+ const { spawn } = await import("node-pty");
3214
+ const xtermModule = await import("@xterm/headless");
3215
+ const { Terminal } = xtermModule.default;
3216
+ this.xterm = new Terminal({
3217
+ cols: this._cols,
3218
+ rows: this._rows,
3219
+ allowProposedApi: true
3220
+ });
3221
+ const nvimArgs = [
3222
+ // Pre-init: only settings needed before user config loads
3223
+ "--cmd",
3224
+ [
3225
+ "set noswapfile",
3226
+ "set nobackup",
3227
+ "set nowritebackup",
3228
+ "set hidden",
3229
+ "set autoread"
3230
+ ].join(" | "),
3231
+ // Post-init: cosmetic overrides applied AFTER user config (LazyVim, etc.)
3232
+ "-c",
3233
+ [
3234
+ "set laststatus=0",
3235
+ "set showtabline=2",
3236
+ "set signcolumn=no",
3237
+ "set nonumber",
3238
+ "set noruler",
3239
+ "set noshowcmd",
3240
+ "set noshowmode",
3241
+ "set shortmess+=F",
3242
+ "set fillchars=eob:\\ ",
3243
+ "set scrolloff=3"
3244
+ ].join(" | "),
3245
+ // Suppress LSP — prevent servers from ever starting (avoids exit warnings)
3246
+ "--cmd",
3247
+ "lua vim.lsp.start = function() end",
3248
+ // Poll-based command executor: reads lua from temp file — no command-line flash
3249
+ "-c",
3250
+ `lua local _t = vim.loop.new_timer(); _t:start(100, 50, vim.schedule_wrap(function() local f = io.open('${this.cmdFile.replace(/'/g, "\\'")}', 'r'); if not f then return end; local c = f:read('*a'); f:close(); os.remove('${this.cmdFile.replace(/'/g, "\\'")}'); if c and #c > 0 then local fn = loadstring(c); if fn then pcall(fn) end end end))`
3251
+ ];
3252
+ this.pty = spawn(this.nvimPath, nvimArgs, {
3253
+ name: "xterm-256color",
3254
+ cols: this._cols,
3255
+ rows: this._rows,
3256
+ env: { ...process.env, TERM: "xterm-256color" }
3257
+ });
3258
+ this.pty.onData((data) => {
3259
+ const csMatch = data.match(/\x1b\[(\d+) q/);
3260
+ if (csMatch) this.cursorStyle = parseInt(csMatch[1], 10);
3261
+ this.xterm.write(data);
3262
+ this.dirty = true;
3263
+ this.cachedRows = null;
3264
+ this.debouncedRender();
3265
+ });
3266
+ this.pty.onExit(() => {
3267
+ this.ready = false;
3268
+ });
3269
+ setTimeout(() => {
3270
+ if (this.pty) {
3271
+ this.ready = true;
3272
+ this.dirty = true;
3273
+ this.cachedRows = null;
3274
+ this.onRender();
3275
+ }
3276
+ }, 500);
3277
+ }
3278
+ debouncedRender() {
3279
+ if (this.renderTimer !== null) return;
3280
+ this.renderTimer = setTimeout(() => {
3281
+ this.renderTimer = null;
3282
+ this.onRender();
3283
+ }, 16);
3284
+ }
3285
+ /**
3286
+ * Execute lua in nvim without flashing the command line.
3287
+ * Writes lua to a temp file — a libuv timer in nvim polls and executes it.
3288
+ */
3289
+ execLua(lua) {
3290
+ writeFileSync2(this.cmdFile, lua);
3291
+ }
3292
+ openFile(path, readonly = true) {
3293
+ if (!this.pty || !this.ready) return;
3294
+ this.currentFile = path;
3295
+ const escapeLua = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
3296
+ const ro = readonly ? "vim.bo.readonly = true; vim.bo.modifiable = false" : "vim.bo.readonly = false; vim.bo.modifiable = true";
3297
+ this.execLua(`vim.cmd('edit! ${escapeLua(path)}'); ${ro}`);
3298
+ }
3299
+ openTabFiles(files) {
3300
+ if (!this.pty || !this.ready || files.length === 0) return;
3301
+ const key = files.map((f) => f.path).join("|");
3302
+ this.pendingFiles = { files, key };
3303
+ if (this.fileDebounceTimer !== null) clearTimeout(this.fileDebounceTimer);
3304
+ this.fileDebounceTimer = setTimeout(() => {
3305
+ this.fileDebounceTimer = null;
3306
+ if (this.pendingFiles) {
3307
+ this.executeOpenFiles(this.pendingFiles.files);
3308
+ this.currentFile = this.pendingFiles.key;
3309
+ this.pendingFiles = null;
3310
+ }
3311
+ }, 150);
3312
+ }
3313
+ executeOpenFiles(files) {
3314
+ if (!this.pty || !this.ready) return;
3315
+ const escapeLua = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
3316
+ const stmts = [
3317
+ "for _, b in ipairs(vim.api.nvim_list_bufs()) do pcall(vim.api.nvim_buf_delete, b, {force=true}) end"
3318
+ ];
3319
+ for (let i = 0; i < files.length; i++) {
3320
+ const path = escapeLua(files[i].path);
3321
+ stmts.push(i === 0 ? `vim.cmd('edit! ${path}')` : `vim.cmd('edit ${path}')`);
3322
+ if (files[i].readonly) {
3323
+ stmts.push("vim.bo.readonly = true", "vim.bo.modifiable = false");
3324
+ } else {
3325
+ stmts.push("vim.bo.readonly = false", "vim.bo.modifiable = true");
3326
+ }
3327
+ }
3328
+ stmts.push("vim.cmd('bfirst')");
3329
+ this.execLua(`(function() ${stmts.join("; ")} end)()`);
3330
+ }
3331
+ openTabFile(path, readonly) {
3332
+ if (!this.pty || !this.ready) return;
3333
+ this.pty.write(`:tabedit ${path}\r`);
3334
+ if (readonly) {
3335
+ this.pty.write(":setlocal readonly nomodifiable\r");
3336
+ } else {
3337
+ this.pty.write(":setlocal noreadonly modifiable\r");
3338
+ }
3339
+ }
3340
+ closeAllTabs() {
3341
+ if (!this.pty || !this.ready) return;
3342
+ this.execLua('for _, b in ipairs(vim.api.nvim_list_bufs()) do pcall(vim.api.nvim_buf_delete, b, {force=true}) end; vim.cmd("enew!")');
3343
+ this.currentFile = null;
3344
+ }
3345
+ resize(cols, rows) {
3346
+ this._cols = cols;
3347
+ this._rows = rows;
3348
+ this.cachedRows = null;
3349
+ this.dirty = true;
3350
+ if (this.pty) this.pty.resize(cols, rows);
3351
+ if (this.xterm) this.xterm.resize(cols, rows);
3352
+ }
3353
+ write(data) {
3354
+ if (this.pty) this.pty.write(data);
3355
+ }
3356
+ getRows() {
3357
+ if (!this.dirty && this.cachedRows) return this.cachedRows;
3358
+ if (!this.xterm) return Array.from({ length: this._rows }, () => " ".repeat(this._cols));
3359
+ const rows = [];
3360
+ const buffer = this.xterm.buffer.active;
3361
+ const reusableCell = buffer.getNullCell();
3362
+ for (let y = 0; y < this._rows; y++) {
3363
+ const line = buffer.getLine(y);
3364
+ if (!line) {
3365
+ rows.push(" ".repeat(this._cols));
3366
+ continue;
3367
+ }
3368
+ let row = "";
3369
+ let prevFg = void 0;
3370
+ let prevBg = void 0;
3371
+ let prevFgMode = "default";
3372
+ let prevBgMode = "default";
3373
+ let prevBold = false;
3374
+ let prevDim = false;
3375
+ let prevItalic = false;
3376
+ let prevUnderline = false;
3377
+ let prevInverse = false;
3378
+ let hasOpenSGR = false;
3379
+ for (let x = 0; x < this._cols; x++) {
3380
+ const cell = line.getCell(x, reusableCell);
3381
+ if (!cell) {
3382
+ row += " ";
3383
+ continue;
3384
+ }
3385
+ const char = cell.getChars() || " ";
3386
+ const fgDefault = cell.isFgDefault();
3387
+ const fgPalette = cell.isFgPalette();
3388
+ const fgRGB = cell.isFgRGB();
3389
+ const fg = fgDefault ? void 0 : cell.getFgColor();
3390
+ let fgMode;
3391
+ if (fgDefault) fgMode = "default";
3392
+ else if (fgPalette) fgMode = "palette";
3393
+ else if (fgRGB) fgMode = "rgb";
3394
+ else throw new Error(`Unknown fg color mode at cell (${x}, ${y})`);
3395
+ const bgDefault = cell.isBgDefault();
3396
+ const bgPalette = cell.isBgPalette();
3397
+ const bgRGB = cell.isBgRGB();
3398
+ const bg = bgDefault ? void 0 : cell.getBgColor();
3399
+ let bgMode;
3400
+ if (bgDefault) bgMode = "default";
3401
+ else if (bgPalette) bgMode = "palette";
3402
+ else if (bgRGB) bgMode = "rgb";
3403
+ else throw new Error(`Unknown bg color mode at cell (${x}, ${y})`);
3404
+ const bold = cell.isBold() !== 0;
3405
+ const dim = cell.isDim() !== 0;
3406
+ const italic = cell.isItalic() !== 0;
3407
+ const underline = cell.isUnderline() !== 0;
3408
+ const inverse = cell.isInverse() !== 0;
3409
+ const attrChanged = fg !== prevFg || bg !== prevBg || fgMode !== prevFgMode || bgMode !== prevBgMode || bold !== prevBold || dim !== prevDim || italic !== prevItalic || underline !== prevUnderline || inverse !== prevInverse;
3410
+ if (attrChanged) {
3411
+ if (hasOpenSGR) {
3412
+ row += "\x1B[0m";
3413
+ hasOpenSGR = false;
3414
+ }
3415
+ const codes = [];
3416
+ if (bold) codes.push("1");
3417
+ if (dim) codes.push("2");
3418
+ if (italic) codes.push("3");
3419
+ if (underline) codes.push("4");
3420
+ if (inverse) codes.push("7");
3421
+ if (fg !== void 0) {
3422
+ if (fgMode === "palette") {
3423
+ codes.push(`38;5;${fg}`);
3424
+ } else if (fgMode === "rgb") {
3425
+ const r = fg >> 16 & 255;
3426
+ const g = fg >> 8 & 255;
3427
+ const b = fg & 255;
3428
+ codes.push(`38;2;${r};${g};${b}`);
3429
+ }
3430
+ }
3431
+ if (bg !== void 0) {
3432
+ if (bgMode === "palette") {
3433
+ codes.push(`48;5;${bg}`);
3434
+ } else if (bgMode === "rgb") {
3435
+ const r = bg >> 16 & 255;
3436
+ const g = bg >> 8 & 255;
3437
+ const b = bg & 255;
3438
+ codes.push(`48;2;${r};${g};${b}`);
3439
+ }
3440
+ }
3441
+ if (codes.length > 0) {
3442
+ row += `\x1B[${codes.join(";")}m`;
3443
+ hasOpenSGR = true;
3444
+ }
3445
+ prevFg = fg;
3446
+ prevBg = bg;
3447
+ prevFgMode = fgMode;
3448
+ prevBgMode = bgMode;
3449
+ prevBold = bold;
3450
+ prevDim = dim;
3451
+ prevItalic = italic;
3452
+ prevUnderline = underline;
3453
+ prevInverse = inverse;
3454
+ }
3455
+ row += char;
3456
+ }
3457
+ if (hasOpenSGR) {
3458
+ row += "\x1B[0m";
3459
+ }
3460
+ rows.push(row);
3461
+ }
3462
+ this.cachedRows = rows;
3463
+ this.dirty = false;
3464
+ return rows;
3465
+ }
3466
+ getCursorPos() {
3467
+ if (!this.xterm) return { x: 0, y: 0 };
3468
+ return {
3469
+ x: this.xterm.buffer.active.cursorX,
3470
+ y: this.xterm.buffer.active.cursorY
3471
+ };
3472
+ }
3473
+ checktime() {
3474
+ if (this.pty && this.ready) {
3475
+ this.execLua('vim.cmd("checktime")');
3476
+ }
3477
+ }
3478
+ destroy() {
3479
+ if (this.renderTimer !== null) {
3480
+ clearTimeout(this.renderTimer);
3481
+ this.renderTimer = null;
3482
+ }
3483
+ if (this.fileDebounceTimer !== null) {
3484
+ clearTimeout(this.fileDebounceTimer);
3485
+ this.fileDebounceTimer = null;
3486
+ }
3487
+ try {
3488
+ if (this.pty) {
3489
+ this.pty.kill();
3490
+ this.pty = null;
3491
+ }
3492
+ } catch {
3493
+ }
3494
+ if (this.xterm) {
3495
+ this.xterm.dispose();
3496
+ this.xterm = null;
3497
+ }
3498
+ this.ready = false;
3499
+ try {
3500
+ unlinkSync(this.cmdFile);
3501
+ } catch {
3502
+ }
3503
+ }
3504
+ };
3505
+
3506
+ // src/tui/lib/overview-writer.ts
3507
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, renameSync, existsSync as existsSync2 } from "fs";
3508
+ import { join as join4 } from "path";
3509
+ function atomicWrite(filePath, content) {
3510
+ const tmp = filePath + ".tmp." + process.pid;
3511
+ writeFileSync3(tmp, content, "utf-8");
3512
+ renameSync(tmp, filePath);
3513
+ }
3514
+ function ensureTuiDir(cwd2, sessionId) {
3515
+ const dir = tuiScratchDir(cwd2, sessionId);
3516
+ mkdirSync3(dir, { recursive: true });
3517
+ return dir;
3518
+ }
3519
+ function formatTimestamp(iso) {
3520
+ try {
3521
+ const d = new Date(iso);
3522
+ return d.toLocaleString("en-US", {
3523
+ month: "short",
3524
+ day: "numeric",
3525
+ hour: "2-digit",
3526
+ minute: "2-digit",
3527
+ second: "2-digit",
3528
+ hour12: false
3529
+ });
3530
+ } catch {
3531
+ return iso;
3532
+ }
3533
+ }
3534
+ function formatDurationMs(ms) {
3535
+ if (ms < 1e3) return `${ms}ms`;
3536
+ const s = Math.floor(ms / 1e3);
3537
+ if (s < 60) return `${s}s`;
3538
+ const m = Math.floor(s / 60);
3539
+ const rem = s % 60;
3540
+ if (m < 60) return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
3541
+ const h = Math.floor(m / 60);
3542
+ const remM = m % 60;
3543
+ return remM > 0 ? `${h}h ${remM}m` : `${h}h`;
3544
+ }
3545
+ function sectionBreak() {
3546
+ return ["", "", "---", "", ""];
3547
+ }
3548
+ function composeCycleDetail(session, cycle) {
3549
+ const isRunning = !cycle.completedAt;
3550
+ const dur = isRunning ? "running" : formatDurationMs(cycle.activeMs);
3551
+ const cycleAgents = session.agents.filter((a) => cycle.agentsSpawned.includes(a.id));
3552
+ const lines = [];
3553
+ lines.push(`# Cycle ${cycle.cycle}`);
3554
+ lines.push("");
3555
+ lines.push(`**Status:** ${isRunning ? "running" : "completed"} | **Duration:** ${dur}`);
3556
+ lines.push(`**Started:** ${formatTimestamp(cycle.timestamp)}`);
3557
+ if (cycle.completedAt) {
3558
+ lines.push(`**Completed:** ${formatTimestamp(cycle.completedAt)}`);
3559
+ }
3560
+ if (cycle.mode) {
3561
+ lines.push(`**Mode:** ${cycle.mode}`);
3562
+ }
3563
+ if (cycle.claudeSessionId) {
3564
+ lines.push(`**Claude Session:** ${cycle.claudeSessionId}`);
3565
+ }
3566
+ lines.push(...sectionBreak());
3567
+ lines.push("## Agents");
3568
+ lines.push("");
3569
+ if (cycleAgents.length === 0) {
3570
+ lines.push("_No agents spawned yet._");
3571
+ } else {
3572
+ for (const agent of cycleAgents) {
3573
+ const agentDur = formatDurationMs(agent.activeMs);
3574
+ lines.push(`### ${agent.id} \u2014 ${agent.name || agent.agentType || agent.id}`);
3575
+ lines.push(`- **Status:** ${agent.status} | **Duration:** ${agentDur}`);
3576
+ lines.push(`- **Type:** ${agent.agentType || "\u2014"}`);
3577
+ if (agent.killedReason) {
3578
+ lines.push(`- **Killed reason:** ${agent.killedReason}`);
3579
+ }
3580
+ lines.push("");
3581
+ lines.push("**Instruction:**");
3582
+ lines.push("");
3583
+ lines.push(agent.instruction);
3584
+ const latestReport = agent.reports.length > 0 ? agent.reports[agent.reports.length - 1] : null;
3585
+ if (latestReport) {
3586
+ lines.push("");
3587
+ lines.push(`**Latest report** (${latestReport.type}, ${formatTimestamp(latestReport.timestamp)}):**`);
3588
+ lines.push("");
3589
+ lines.push(latestReport.summary);
3590
+ }
3591
+ lines.push("");
3592
+ }
3593
+ }
3594
+ if (cycle.nextPrompt) {
3595
+ lines.push(...sectionBreak());
3596
+ lines.push("## Next Prompt");
3597
+ lines.push("");
3598
+ lines.push(cycle.nextPrompt.trim());
3599
+ lines.push("");
3600
+ }
3601
+ return lines.join("\n") + "\n";
3602
+ }
3603
+ function composeAgentDetail(agent, reportBlocks) {
3604
+ const dur = formatDurationMs(agent.activeMs);
3605
+ const lines = [];
3606
+ lines.push(`# ${agent.id} \u2014 ${agent.name || agent.agentType || agent.id}`);
3607
+ lines.push("");
3608
+ lines.push(
3609
+ `**Status:** ${agent.status} | **Duration:** ${dur} | **Type:** ${agent.agentType || "\u2014"}`
3610
+ );
3611
+ lines.push(`**Spawned:** ${formatTimestamp(agent.spawnedAt)}`);
3612
+ if (agent.completedAt) {
3613
+ lines.push(`**Completed:** ${formatTimestamp(agent.completedAt)}`);
3614
+ }
3615
+ if (agent.killedReason) {
3616
+ lines.push(`**Killed reason:** ${agent.killedReason}`);
3617
+ }
3618
+ if (agent.claudeSessionId) {
3619
+ lines.push(`**Claude Session:** ${agent.claudeSessionId}`);
3620
+ }
3621
+ lines.push(...sectionBreak());
3622
+ lines.push("## Instruction");
3623
+ lines.push("");
3624
+ lines.push(agent.instruction.trim());
3625
+ if (reportBlocks.length > 0) {
3626
+ lines.push(...sectionBreak());
3627
+ lines.push(`## Reports (${reportBlocks.length})`);
3628
+ for (const block of reportBlocks) {
3629
+ lines.push("");
3630
+ const badge = block.type === "final" ? "FINAL" : "UPDATE";
3631
+ lines.push(`### ${badge} \u2014 ${formatTimestamp(block.timestamp)}`);
3632
+ lines.push("");
3633
+ lines.push(block.content.trim());
3634
+ }
3635
+ } else if (agent.reports.length > 0) {
3636
+ lines.push(...sectionBreak());
3637
+ lines.push(`## Reports (${agent.reports.length})`);
3638
+ for (const report of agent.reports) {
3639
+ const badge = report.type === "final" ? "FINAL" : "UPDATE";
3640
+ lines.push("");
3641
+ lines.push(`### ${badge} \u2014 ${formatTimestamp(report.timestamp)}`);
3642
+ lines.push("");
3643
+ lines.push(report.summary);
3644
+ }
3645
+ }
3646
+ return lines.join("\n") + "\n";
3647
+ }
3648
+ function composeMessages(session) {
3649
+ const lines = [];
3650
+ lines.push("# Messages");
3651
+ lines.push("");
3652
+ lines.push(`**Session:** ${session.name ?? session.task.slice(0, 60)}`);
3653
+ lines.push(`**Total messages:** ${session.messages.length}`);
3654
+ if (session.messages.length === 0) {
3655
+ lines.push("");
3656
+ lines.push("_No messages yet._");
3657
+ return lines.join("\n") + "\n";
3658
+ }
3659
+ const sorted = [...session.messages].sort(
3660
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
3661
+ );
3662
+ for (const msg of sorted) {
3663
+ lines.push("");
3664
+ lines.push("---");
3665
+ lines.push("");
3666
+ const src = msg.source;
3667
+ let sourceLabel;
3668
+ if (src.type === "agent") {
3669
+ sourceLabel = src.agentId;
3670
+ } else if (src.type === "user") {
3671
+ sourceLabel = "You";
3672
+ } else {
3673
+ sourceLabel = src.detail ? `system (${src.detail})` : "system";
3674
+ }
3675
+ lines.push(`**From:** ${sourceLabel} | **Time:** ${formatTimestamp(msg.timestamp)}`);
3676
+ if (msg.summary && msg.summary !== msg.content) {
3677
+ lines.push(`**Summary:** ${msg.summary}`);
3678
+ }
3679
+ lines.push("");
3680
+ lines.push(msg.content.trim());
3681
+ }
3682
+ return lines.join("\n") + "\n";
3683
+ }
3684
+ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
3685
+ if (!cursorNode) return null;
3686
+ const sessionId = cursorNode.sessionId;
3687
+ if (!sessionId) return null;
3688
+ const session = detailCtx.session;
3689
+ switch (cursorNode.type) {
3690
+ case "session": {
3691
+ if (!session) return null;
3692
+ const files = [];
3693
+ const gp = goalPath(cwd2, sessionId);
3694
+ if (existsSync2(gp)) files.push({ path: gp, readonly: false });
3695
+ const rp = roadmapPath(cwd2, sessionId);
3696
+ if (existsSync2(rp)) files.push({ path: rp, readonly: false });
3697
+ const sp = strategyPath(cwd2, sessionId);
3698
+ if (existsSync2(sp)) files.push({ path: sp, readonly: false });
3699
+ if (files.length === 0) return null;
3700
+ return { files };
3701
+ }
3702
+ case "cycle": {
3703
+ if (!session) return null;
3704
+ const cycle = session.orchestratorCycles.find(
3705
+ (c) => c.cycle === cursorNode.cycleNumber
3706
+ );
3707
+ if (!cycle) return null;
3708
+ const dir = ensureTuiDir(cwd2, sessionId);
3709
+ const filePath = join4(dir, `cycle-${cursorNode.cycleNumber}.md`);
3710
+ atomicWrite(filePath, composeCycleDetail(session, cycle));
3711
+ return { files: [{ path: filePath, readonly: true }] };
3712
+ }
3713
+ case "agent": {
3714
+ if (!session) return null;
3715
+ const agent = session.agents.find((a) => a.id === cursorNode.agentId);
3716
+ if (!agent) return null;
3717
+ const dir = ensureTuiDir(cwd2, sessionId);
3718
+ const filePath = join4(dir, `${agent.id}.md`);
3719
+ atomicWrite(filePath, composeAgentDetail(agent, state2.cachedReportBlocks.get(agent.id) ?? []));
3720
+ return { files: [{ path: filePath, readonly: true }] };
3721
+ }
3722
+ case "report": {
3723
+ const agent = session?.agents.find((a) => a.id === cursorNode.agentId);
3724
+ if (agent && agent.reports.length > 0) {
3725
+ const report = agent.reports[cursorNode.reportIndex];
3726
+ if (report?.filePath && existsSync2(report.filePath)) {
3727
+ return { files: [{ path: report.filePath, readonly: true }] };
3728
+ }
3729
+ }
3730
+ return null;
3731
+ }
3732
+ case "context-file": {
3733
+ if (cursorNode.filePath && existsSync2(cursorNode.filePath)) {
3734
+ return { files: [{ path: cursorNode.filePath, readonly: false }] };
3735
+ }
3736
+ return null;
3737
+ }
3738
+ case "messages":
3739
+ case "message": {
3740
+ if (!session || session.messages.length === 0) return null;
3741
+ const dir = ensureTuiDir(cwd2, sessionId);
3742
+ const filePath = join4(dir, "messages.md");
3743
+ atomicWrite(filePath, composeMessages(session));
3744
+ return { files: [{ path: filePath, readonly: true }] };
3745
+ }
3746
+ case "context": {
3747
+ return null;
3748
+ }
3749
+ default:
3750
+ return null;
3751
+ }
3752
+ }
3753
+
2887
3754
  // src/tui/app.ts
2888
3755
  var latestNodes = [];
2889
3756
  var cachedContextFilePath = null;
2890
3757
  var cachedContextFileContent = null;
2891
3758
  var prevFrame = [];
3759
+ var prevTreeInputs = "";
3760
+ var prevBottomInputs = "";
3761
+ var prevOverlayMode = "";
3762
+ var cachedTreeRows = [];
2892
3763
  var cachedLogSessionId = null;
2893
3764
  var cachedLogFiles = /* @__PURE__ */ new Map();
3765
+ var STATUS_ROW_COUNT = 2;
3766
+ function buildStatusRows(cursorNode, session, state2) {
3767
+ if (!cursorNode || !session) {
3768
+ return [ansiDim(" No session selected"), ""];
3769
+ }
3770
+ const dur = formatDuration(session.createdAt, session.completedAt);
3771
+ const indicator = statusIndicator(session.status);
3772
+ const sColor = statusColor(session.status);
3773
+ const title = truncate(session.name ?? session.task, 40);
3774
+ switch (cursorNode.type) {
3775
+ case "session": {
3776
+ return [
3777
+ " " + ansiColor(indicator, sColor, true) + " " + ansiColor(title, "white", true),
3778
+ " " + ansiDim(`${session.status} \xB7 ${session.orchestratorCycles.length} cycles \xB7 ${session.agents.length} agents \xB7 ${dur}`)
3779
+ ];
3780
+ }
3781
+ case "cycle": {
3782
+ const cycle = session.orchestratorCycles.find((c) => c.cycle === cursorNode.cycleNumber);
3783
+ if (!cycle) return [" " + ansiColor(title, "white", true), ""];
3784
+ const cDur = cycle.completedAt ? formatDuration(cycle.timestamp, cycle.completedAt) : "running";
3785
+ const cStatus = cycle.completedAt ? "completed" : "running";
3786
+ return [
3787
+ " " + ansiColor(indicator, sColor, true) + " " + ansiColor(title, "white", true) + ansiDim(` \xB7 Cycle ${cycle.cycle}`),
3788
+ " " + ansiDim(`${cStatus} \xB7 ${cDur} \xB7 ${cycle.agentsSpawned.length} agents`)
3789
+ ];
3790
+ }
3791
+ case "agent":
3792
+ case "report": {
3793
+ const agentId = cursorNode.type === "agent" ? cursorNode.agentId : cursorNode.agentId;
3794
+ const agent = session.agents.find((a) => a.id === agentId);
3795
+ if (!agent) return [" " + ansiColor(title, "white", true), ""];
3796
+ const aIcon = agentStatusIcon(agent.status);
3797
+ const aDur = formatDuration(agent.spawnedAt, agent.completedAt);
3798
+ const aName = agentDisplayName(agent);
3799
+ return [
3800
+ " " + ansiColor(aIcon, statusColor(agent.status === "running" ? "active" : agent.status), true) + " " + ansiColor(`${agent.id} \xB7 ${aName}`, "white", true),
3801
+ " " + ansiDim(`${agent.status} \xB7 ${agent.agentType || "\u2014"} \xB7 ${aDur}`)
3802
+ ];
3803
+ }
3804
+ case "context-file": {
3805
+ const name = cursorNode.filePath.split("/").pop() ?? cursorNode.filePath;
3806
+ return [
3807
+ " " + ansiColor("\u229E", "white") + " " + ansiColor(name, "white", true),
3808
+ " " + ansiDim(`context file \xB7 ${session.status}`)
3809
+ ];
3810
+ }
3811
+ case "messages":
3812
+ case "message": {
3813
+ return [
3814
+ " " + ansiColor(indicator, sColor, true) + " " + ansiColor(title, "white", true),
3815
+ " " + ansiDim(`${session.messages.length} messages`)
3816
+ ];
3817
+ }
3818
+ default: {
3819
+ return [
3820
+ " " + ansiColor(indicator, sColor, true) + " " + ansiColor(title, "white", true),
3821
+ " " + ansiDim(`${session.status} \xB7 ${dur}`)
3822
+ ];
3823
+ }
3824
+ }
3825
+ }
2894
3826
  function getAgentForNode(node, agents) {
2895
3827
  if (!node) return null;
2896
3828
  if (node.type === "agent" || node.type === "report") {
@@ -2900,7 +3832,18 @@ function getAgentForNode(node, agents) {
2900
3832
  }
2901
3833
  function startApp(state2, cleanup2) {
2902
3834
  const config = loadConfig(state2.cwd);
3835
+ const treeWidth = 36;
3836
+ const initialDetailW = state2.cols - treeWidth - 4;
3837
+ const initialDetailH = state2.rows - 3 - 2 - STATUS_ROW_COUNT - 1;
3838
+ const bridge = new NvimBridge(
3839
+ Math.max(1, initialDetailW),
3840
+ Math.max(1, initialDetailH),
3841
+ requestRender
3842
+ );
3843
+ state2.nvimBridge = bridge.available ? bridge : null;
3844
+ state2.nvimEnabled = bridge.available;
2903
3845
  let prevSelectedSessionId = void 0;
3846
+ let debouncedPollTimer = null;
2904
3847
  async function poll() {
2905
3848
  try {
2906
3849
  let selectedSession = null;
@@ -2918,13 +3861,10 @@ function startApp(state2, cleanup2) {
2918
3861
  statusPromise ?? Promise.resolve(null)
2919
3862
  ]);
2920
3863
  const sessions = listRes.ok ? listRes.data?.sessions ?? [] : [];
3864
+ const aliveWindows = listAllWindowIds();
2921
3865
  for (const s of sessions) {
2922
3866
  if (s.status !== "completed" && s.tmuxWindowId) {
2923
- try {
2924
- s.windowAlive = windowExists(s.tmuxWindowId);
2925
- } catch {
2926
- s.windowAlive = false;
2927
- }
3867
+ s.windowAlive = aliveWindows.has(s.tmuxWindowId);
2928
3868
  }
2929
3869
  }
2930
3870
  if (state2.selectedSessionId) {
@@ -2937,28 +3877,28 @@ function startApp(state2, cleanup2) {
2937
3877
  }
2938
3878
  try {
2939
3879
  const pp = roadmapPath(state2.cwd, state2.selectedSessionId);
2940
- if (existsSync2(pp)) {
3880
+ if (existsSync3(pp)) {
2941
3881
  planContent = readFileSync2(pp, "utf-8");
2942
3882
  }
2943
3883
  } catch {
2944
3884
  }
2945
3885
  try {
2946
3886
  const gp = goalPath(state2.cwd, state2.selectedSessionId);
2947
- if (existsSync2(gp)) {
3887
+ if (existsSync3(gp)) {
2948
3888
  goalContent = readFileSync2(gp, "utf-8");
2949
3889
  }
2950
3890
  } catch {
2951
3891
  }
2952
3892
  try {
2953
3893
  const sp = strategyPath(state2.cwd, state2.selectedSessionId);
2954
- if (existsSync2(sp)) {
3894
+ if (existsSync3(sp)) {
2955
3895
  strategyContent = readFileSync2(sp, "utf-8");
2956
3896
  }
2957
3897
  } catch {
2958
3898
  }
2959
3899
  try {
2960
3900
  const ld = logsDir(state2.cwd, state2.selectedSessionId);
2961
- if (existsSync2(ld)) {
3901
+ if (existsSync3(ld)) {
2962
3902
  if (state2.selectedSessionId !== cachedLogSessionId) {
2963
3903
  cachedLogFiles = /* @__PURE__ */ new Map();
2964
3904
  cachedLogSessionId = state2.selectedSessionId;
@@ -2969,7 +3909,7 @@ function startApp(state2, cleanup2) {
2969
3909
  if (!fileSet.has(key)) cachedLogFiles.delete(key);
2970
3910
  }
2971
3911
  for (const f of files) {
2972
- const filePath = join3(ld, f);
3912
+ const filePath = join5(ld, f);
2973
3913
  const mtime = statSync(filePath).mtimeMs;
2974
3914
  const cached = cachedLogFiles.get(f);
2975
3915
  if (!cached || cached.mtime !== mtime) {
@@ -2989,7 +3929,7 @@ function startApp(state2, cleanup2) {
2989
3929
  }
2990
3930
  try {
2991
3931
  const cd = contextDir(state2.cwd, state2.selectedSessionId);
2992
- if (existsSync2(cd)) {
3932
+ if (existsSync3(cd)) {
2993
3933
  contextFiles = readdirSync(cd).filter((f) => !f.startsWith(".")).sort();
2994
3934
  }
2995
3935
  } catch {
@@ -3011,6 +3951,9 @@ function startApp(state2, cleanup2) {
3011
3951
  state2.paneAlive = paneAlive;
3012
3952
  state2.contextFiles = contextFiles;
3013
3953
  state2.error = null;
3954
+ if (state2.nvimEnabled && state2.nvimBridge?.ready && state2.prevNvimFile) {
3955
+ state2.nvimBridge.checktime();
3956
+ }
3014
3957
  requestRender();
3015
3958
  } catch (err) {
3016
3959
  state2.error = err.message;
@@ -3027,17 +3970,17 @@ function startApp(state2, cleanup2) {
3027
3970
  writeCenter(buf, Math.floor(state2.rows / 2), "Terminal too small \u2014 resize to continue");
3028
3971
  const out2 = flushFrame(buf.lines, prevFrame);
3029
3972
  writeToStdout(out2);
3030
- prevFrame = [...buf.lines];
3973
+ prevFrame = buf.lines;
3031
3974
  return;
3032
3975
  }
3033
- const treeWidth = 36;
3034
- const remaining = state2.cols - treeWidth;
3035
- const detailWidth = state2.showLogs ? Math.floor(remaining * 0.6) : remaining;
3036
- const logsWidth = state2.showLogs ? remaining - detailWidth : 0;
3976
+ const treeWidth2 = 36;
3977
+ const remaining = state2.cols - treeWidth2;
3978
+ const detailWidth = state2.showCombinedView ? Math.floor(remaining * 0.6) : remaining;
3979
+ const logsWidth = state2.showCombinedView ? remaining - detailWidth : 0;
3037
3980
  const contentHeight = state2.rows - 3;
3038
- const treeRect = { x: 0, y: 0, w: treeWidth, h: contentHeight };
3039
- const detailRect = { x: treeWidth, y: 0, w: detailWidth, h: contentHeight };
3040
- const logsRect = state2.showLogs ? { x: treeWidth + detailWidth, y: 0, w: logsWidth, h: contentHeight } : null;
3981
+ const treeRect = { x: 0, y: 0, w: treeWidth2, h: contentHeight };
3982
+ const detailRect = { x: treeWidth2, y: 0, w: detailWidth, h: contentHeight };
3983
+ const logsRect = state2.showCombinedView ? { x: treeWidth2 + detailWidth, y: 0, w: logsWidth, h: contentHeight } : null;
3041
3984
  const bottomY = contentHeight;
3042
3985
  const filteredSessions = state2.searchFilter ? state2.sessions.filter((s) => {
3043
3986
  const q = state2.searchFilter.toLowerCase();
@@ -3055,6 +3998,7 @@ function startApp(state2, cleanup2) {
3055
3998
  state2.cwd,
3056
3999
  state2.contextFiles
3057
4000
  );
4001
+ precomputePrefixes(nodes);
3058
4002
  state2.cachedTreeNodes = nodes;
3059
4003
  state2.treeCacheKey = cacheKey;
3060
4004
  }
@@ -3067,11 +4011,20 @@ function startApp(state2, cleanup2) {
3067
4011
  state2.selectedSessionId = newSessionId;
3068
4012
  state2.detailScroll.reset();
3069
4013
  state2.logsScroll.reset();
4014
+ state2.cachedDetailLines = null;
4015
+ state2.detailCacheKey = "";
4016
+ state2.prevNvimFile = null;
4017
+ state2.cachedLogsLines = null;
4018
+ state2.logsCacheKey = "";
3070
4019
  }
3071
4020
  if (state2.selectedSessionId !== prevSelectedSessionId) {
3072
4021
  prevSelectedSessionId = state2.selectedSessionId;
4022
+ if (debouncedPollTimer !== null) clearTimeout(debouncedPollTimer);
3073
4023
  if (state2.selectedSessionId !== null) {
3074
- void poll();
4024
+ debouncedPollTimer = setTimeout(() => {
4025
+ debouncedPollTimer = null;
4026
+ void poll();
4027
+ }, 80);
3075
4028
  }
3076
4029
  }
3077
4030
  autoExpandCycle(state2);
@@ -3085,7 +4038,7 @@ function startApp(state2, cleanup2) {
3085
4038
  if (cursorNode.filePath !== cachedContextFilePath) {
3086
4039
  cachedContextFilePath = cursorNode.filePath;
3087
4040
  try {
3088
- if (existsSync2(cursorNode.filePath)) {
4041
+ if (existsSync3(cursorNode.filePath)) {
3089
4042
  cachedContextFileContent = readFileSync2(cursorNode.filePath, "utf-8");
3090
4043
  } else {
3091
4044
  cachedContextFileContent = null;
@@ -3099,13 +4052,37 @@ function startApp(state2, cleanup2) {
3099
4052
  cachedContextFilePath = null;
3100
4053
  cachedContextFileContent = null;
3101
4054
  }
3102
- renderTreePanel(
3103
- buf,
3104
- treeRect,
3105
- nodes,
3106
- state2.cursorIndex,
3107
- state2.mode === "navigate" && state2.focusPane === "tree"
3108
- );
4055
+ const treeFocused = state2.mode === "navigate" && state2.focusPane === "tree";
4056
+ const treeInputs = `${state2.treeCacheKey}:${state2.cursorIndex}:${treeFocused}`;
4057
+ const bottomInputs = `${state2.notification}:${state2.error}:${state2.mode}:${state2.inputText}:${state2.inputCursorPos}:${cursorNode?.type}`;
4058
+ const overlayMode = state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help" ? state2.mode : "";
4059
+ const hasPrev = prevFrame.length === buf.height;
4060
+ const treeDirty = !hasPrev || treeInputs !== prevTreeInputs;
4061
+ const bottomDirty = !hasPrev || bottomInputs !== prevBottomInputs;
4062
+ const overlayDirty = !hasPrev || overlayMode !== prevOverlayMode;
4063
+ prevTreeInputs = treeInputs;
4064
+ prevBottomInputs = bottomInputs;
4065
+ prevOverlayMode = overlayMode;
4066
+ let treeRows;
4067
+ if (treeDirty) {
4068
+ const treeBlank = " ".repeat(treeWidth2);
4069
+ const treeBuf = {
4070
+ lines: Array.from({ length: contentHeight }, () => treeBlank),
4071
+ width: treeWidth2,
4072
+ height: contentHeight
4073
+ };
4074
+ renderTreePanel(
4075
+ treeBuf,
4076
+ { x: 0, y: 0, w: treeWidth2, h: contentHeight },
4077
+ nodes,
4078
+ state2.cursorIndex,
4079
+ treeFocused
4080
+ );
4081
+ cachedTreeRows = treeBuf.lines;
4082
+ treeRows = treeBuf.lines;
4083
+ } else {
4084
+ treeRows = cachedTreeRows;
4085
+ }
3109
4086
  const detailCtx = {
3110
4087
  nodes,
3111
4088
  session: state2.selectedSession,
@@ -3114,19 +4091,55 @@ function startApp(state2, cleanup2) {
3114
4091
  detailReportBlocks,
3115
4092
  contextFileContent
3116
4093
  };
3117
- renderDetailContent(buf, detailRect, state2, detailCtx);
3118
- if (logsRect) {
3119
- renderLogsContent(buf, logsRect, state2);
3120
- }
3121
- renderNotificationRow(buf, bottomY, state2.notification, state2.error);
3122
- renderInputBar(buf, bottomY + 1, state2);
3123
- renderStatusLine(buf, bottomY + 2, state2, cursorNode?.type);
3124
- if (state2.mode === "leader") renderLeaderOverlay(buf, state2.rows, state2.cols);
3125
- if (state2.mode === "copy-menu") renderCopyMenuOverlay(buf, state2.rows, state2.cols);
3126
- if (state2.mode === "help") renderHelpOverlay(buf, state2.rows, state2.cols);
3127
- const out = flushFrame(buf.lines, prevFrame);
4094
+ let detailRows;
4095
+ if (state2.nvimEnabled && state2.nvimBridge?.ready) {
4096
+ const result = resolveNvimFile(state2, cursorNode, detailCtx, state2.cwd);
4097
+ const resultKey = result ? result.files.map((f) => f.path).join("|") : null;
4098
+ if (resultKey && resultKey !== state2.prevNvimFile) {
4099
+ state2.nvimBridge.openTabFiles(result.files);
4100
+ state2.prevNvimFile = resultKey;
4101
+ state2.nvimEditable = result.files.some((f) => !f.readonly);
4102
+ } else if (!resultKey) {
4103
+ state2.prevNvimFile = null;
4104
+ state2.nvimEditable = false;
4105
+ }
4106
+ const statusRows = buildStatusRows(cursorNode, state2.selectedSession, state2);
4107
+ detailRows = renderNvimDetailRows(detailRect, state2.nvimBridge, state2.focusPane === "detail", state2.nvimEditable, statusRows);
4108
+ } else {
4109
+ detailRows = renderDetailRows(detailRect, state2, detailCtx);
4110
+ }
4111
+ const logsRows = logsRect ? renderLogsRows(logsRect, state2) : null;
4112
+ for (let i = 0; i < contentHeight; i++) {
4113
+ if (logsRows) {
4114
+ buf.lines[i] = treeRows[i] + detailRows[i] + logsRows[i];
4115
+ } else {
4116
+ buf.lines[i] = treeRows[i] + detailRows[i];
4117
+ }
4118
+ }
4119
+ if (bottomDirty || overlayDirty) {
4120
+ renderNotificationRow(buf, bottomY, state2.notification, state2.error);
4121
+ renderInputBar(buf, bottomY + 1, state2);
4122
+ renderStatusLine(buf, bottomY + 2, state2, cursorNode?.type);
4123
+ } else {
4124
+ copyRows(buf, prevFrame, bottomY, 3);
4125
+ }
4126
+ if (overlayMode) {
4127
+ if (state2.mode === "leader") renderLeaderOverlay(buf, state2.rows, state2.cols);
4128
+ if (state2.mode === "copy-menu") renderCopyMenuOverlay(buf, state2.rows, state2.cols);
4129
+ if (state2.mode === "help") renderHelpOverlay(buf, state2.rows, state2.cols);
4130
+ }
4131
+ let cursorSuffix;
4132
+ if (state2.focusPane === "detail" && state2.nvimBridge?.ready) {
4133
+ const cursor = state2.nvimBridge.getCursorPos();
4134
+ const absX = detailRect.x + 2 + cursor.x;
4135
+ const absY = detailRect.y + 1 + STATUS_ROW_COUNT + 1 + cursor.y;
4136
+ cursorSuffix = `\x1B[${state2.nvimBridge.cursorStyle} q\x1B[?25h\x1B[${absY + 1};${absX + 1}H`;
4137
+ } else {
4138
+ cursorSuffix = "\x1B[0 q\x1B[?25l";
4139
+ }
4140
+ const out = flushFrame(buf.lines, prevFrame, cursorSuffix);
3128
4141
  writeToStdout(out);
3129
- prevFrame = [...buf.lines];
4142
+ prevFrame = buf.lines;
3130
4143
  }
3131
4144
  const inputActions = {
3132
4145
  getNodes: () => latestNodes,
@@ -3180,6 +4193,11 @@ function startApp(state2, cleanup2) {
3180
4193
  state2.rows = typeof stdoutRows === "number" && stdoutRows > 0 ? stdoutRows : 24;
3181
4194
  state2.cols = typeof stdoutCols === "number" && stdoutCols > 0 ? stdoutCols : 80;
3182
4195
  prevFrame = [];
4196
+ if (state2.nvimBridge) {
4197
+ const detailW = state2.cols - 36;
4198
+ const contentH = state2.rows - 3;
4199
+ state2.nvimBridge.resize(Math.max(1, detailW - 4), Math.max(1, contentH - 2 - STATUS_ROW_COUNT - 1));
4200
+ }
3183
4201
  requestRender();
3184
4202
  });
3185
4203
  void poll();
@@ -3187,10 +4205,12 @@ function startApp(state2, cleanup2) {
3187
4205
  const origCleanup = inputActions.cleanup;
3188
4206
  inputActions.cleanup = () => {
3189
4207
  clearInterval(pollInterval);
4208
+ if (debouncedPollTimer !== null) clearTimeout(debouncedPollTimer);
3190
4209
  stopKeypress();
3191
4210
  stopResize();
3192
4211
  state2.detailScroll.destroy();
3193
4212
  state2.logsScroll.destroy();
4213
+ state2.nvimBridge?.destroy();
3194
4214
  origCleanup();
3195
4215
  };
3196
4216
  requestRender();