sisyphi 1.1.8 → 1.1.10

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
@@ -333,6 +333,12 @@ function createAppState(cwd2) {
333
333
  cachedReportBlocks: /* @__PURE__ */ new Map(),
334
334
  cachedTreeNodes: null,
335
335
  treeCacheKey: "",
336
+ cachedDetailLines: null,
337
+ detailCacheKey: "",
338
+ detailRenderedCache: { lines: [], ansi: [] },
339
+ cachedLogsLines: null,
340
+ logsCacheKey: "",
341
+ logsRenderedCache: { lines: [], ansi: [] },
336
342
  cwd: cwd2
337
343
  };
338
344
  }
@@ -1651,13 +1657,21 @@ function renderLine(segs) {
1651
1657
  }
1652
1658
  return out;
1653
1659
  }
1660
+ var cachedBlank = "";
1661
+ var cachedBlankWidth = 0;
1654
1662
  function createFrameBuffer(width, height) {
1655
- const blank = " ".repeat(width);
1656
- return {
1657
- lines: Array.from({ length: height }, () => blank),
1658
- width,
1659
- height
1660
- };
1663
+ if (width !== cachedBlankWidth) {
1664
+ cachedBlank = " ".repeat(width);
1665
+ cachedBlankWidth = width;
1666
+ }
1667
+ const lines = new Array(height);
1668
+ for (let i = 0; i < height; i++) lines[i] = cachedBlank;
1669
+ return { lines, width, height };
1670
+ }
1671
+ function copyRows(buf, src, startRow, count) {
1672
+ for (let i = 0; i < count && startRow + i < buf.height; i++) {
1673
+ buf.lines[startRow + i] = src[startRow + i];
1674
+ }
1661
1675
  }
1662
1676
  function flushFrame(frame, prevFrame2) {
1663
1677
  let out = "\x1B[?2026h";
@@ -1672,6 +1686,71 @@ function flushFrame(frame, prevFrame2) {
1672
1686
  return out;
1673
1687
  }
1674
1688
  var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
1689
+ function clipAnsi(content, maxWidth) {
1690
+ let out = "";
1691
+ let displayWidth = 0;
1692
+ let i = 0;
1693
+ while (i < content.length) {
1694
+ if (content[i] === "\x1B" && content[i + 1] === "[") {
1695
+ const seqLen = ansiLen(content, i);
1696
+ if (seqLen > 0) {
1697
+ out += content.substring(i, i + seqLen);
1698
+ i += seqLen;
1699
+ continue;
1700
+ }
1701
+ }
1702
+ const cp = content.codePointAt(i);
1703
+ const ch = String.fromCodePoint(cp);
1704
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1705
+ if (displayWidth + chWidth > maxWidth) break;
1706
+ out += ch;
1707
+ displayWidth += chWidth;
1708
+ i += ch.length;
1709
+ }
1710
+ if (out.includes("\x1B[") && !out.endsWith("\x1B[0m")) {
1711
+ out += "\x1B[0m";
1712
+ }
1713
+ const remaining = maxWidth - displayWidth;
1714
+ if (remaining > 0) out += " ".repeat(remaining);
1715
+ return out;
1716
+ }
1717
+ function displayWidthFast(s) {
1718
+ let w = 0;
1719
+ let i = 0;
1720
+ while (i < s.length) {
1721
+ if (s[i] === "\x1B" && s[i + 1] === "[") {
1722
+ const len = ansiLen(s, i);
1723
+ if (len > 0) {
1724
+ i += len;
1725
+ continue;
1726
+ }
1727
+ }
1728
+ const cp = s.codePointAt(i);
1729
+ const ch = String.fromCodePoint(cp);
1730
+ w += cp < 128 ? 1 : stringWidth2(ch);
1731
+ i += ch.length;
1732
+ }
1733
+ return w;
1734
+ }
1735
+ function ansiLen(s, i) {
1736
+ let j = i + 2;
1737
+ const len = s.length;
1738
+ while (j < len) {
1739
+ const c = s.charCodeAt(j);
1740
+ if (c >= 48 && c <= 57 || c === 59) {
1741
+ j++;
1742
+ } else {
1743
+ break;
1744
+ }
1745
+ }
1746
+ if (j < len) {
1747
+ const c = s.charCodeAt(j);
1748
+ if (c >= 65 && c <= 90 || c >= 97 && c <= 122) {
1749
+ return j + 1 - i;
1750
+ }
1751
+ }
1752
+ return 0;
1753
+ }
1675
1754
  function writeAt(buf, x, y, content) {
1676
1755
  if (y < 0 || y >= buf.height) return;
1677
1756
  if (x < 0 || x >= buf.width) return;
@@ -1691,16 +1770,16 @@ function writeClipped(buf, x, y, content, maxWidth) {
1691
1770
  let i = 0;
1692
1771
  while (i < content.length) {
1693
1772
  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;
1773
+ const seqLen = ansiLen(content, i);
1774
+ if (seqLen > 0) {
1775
+ out += content.substring(i, i + seqLen);
1776
+ i += seqLen;
1698
1777
  continue;
1699
1778
  }
1700
1779
  }
1701
1780
  const cp = content.codePointAt(i);
1702
1781
  const ch = String.fromCodePoint(cp);
1703
- const chWidth = stringWidth2(ch);
1782
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1704
1783
  if (displayWidth + chWidth > maxWidth) break;
1705
1784
  out += ch;
1706
1785
  displayWidth += chWidth;
@@ -1713,7 +1792,12 @@ function writeClipped(buf, x, y, content, maxWidth) {
1713
1792
  if (remaining > 0) {
1714
1793
  out += " ".repeat(remaining);
1715
1794
  }
1716
- writeAt(buf, x, y, out);
1795
+ const existing = buf.lines[y];
1796
+ const prefix = sliceDisplayCols(existing, 0, x);
1797
+ const suffix = sliceDisplayCols(existing, x + maxWidth, buf.width);
1798
+ const prefixDisplayW = displayWidthFast(prefix);
1799
+ const paddedPrefix = prefixDisplayW < x ? prefix + " ".repeat(x - prefixDisplayW) : prefix;
1800
+ buf.lines[y] = paddedPrefix + out + suffix;
1717
1801
  }
1718
1802
  function writeCenter(buf, row, content) {
1719
1803
  const textWidth = stringWidth2(content.replace(ANSI_RE, ""));
@@ -1730,28 +1814,75 @@ function drawBorder(buf, x, y, w, h, color) {
1730
1814
  writeAt(buf, x + w - 1, row, sgr + "\u2502" + reset);
1731
1815
  }
1732
1816
  }
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;
1817
+ function buildPanelRows(rect, lines, scrollOffset, focused, borderColor, renderedCache) {
1818
+ const { w, h } = rect;
1819
+ const rows = new Array(h);
1820
+ const color = focused ? "blue" : borderColor;
1821
+ const sgr = `\x1B[${colorToSGR(color)}m`;
1822
+ const reset = "\x1B[0m";
1737
1823
  const innerW = w - 4;
1738
- const innerY = y + 1;
1739
1824
  const innerH = h - 2;
1740
- if (innerW <= 0 || innerH <= 0) return;
1825
+ const blankInner = " ".repeat(innerW);
1826
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
1827
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
1828
+ const borderL = sgr + "\u2502" + reset + " ";
1829
+ const borderR = " " + sgr + "\u2502" + reset;
1830
+ const emptyRow = borderL + blankInner + borderR;
1831
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
1832
+ if (innerW <= 0 || innerH <= 0) return rows;
1833
+ let ansiLines;
1834
+ if (renderedCache && renderedCache.lines === lines) {
1835
+ ansiLines = renderedCache.ansi;
1836
+ } else {
1837
+ ansiLines = new Array(lines.length);
1838
+ for (let i = 0; i < lines.length; i++) {
1839
+ ansiLines[i] = renderLine(lines[i]);
1840
+ }
1841
+ if (renderedCache) {
1842
+ renderedCache.lines = lines;
1843
+ renderedCache.ansi = ansiLines;
1844
+ }
1845
+ }
1741
1846
  const hasOverflow = lines.length > innerH;
1742
1847
  const viewableH = hasOverflow ? innerH - 1 : innerH;
1743
1848
  const maxScroll = Math.max(0, lines.length - viewableH);
1744
1849
  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);
1850
+ for (let i = 0; i < viewableH && effectiveOffset + i < ansiLines.length; i++) {
1851
+ const clipped = clipAnsi(ansiLines[effectiveOffset + i], innerW);
1852
+ rows[1 + i] = borderL + clipped + borderR;
1749
1853
  }
1750
1854
  if (hasOverflow) {
1751
1855
  const scrollPct = maxScroll > 0 ? Math.round(effectiveOffset / maxScroll * 100) : 100;
1752
1856
  const indicator = ` \u2195 ${scrollPct}% \xB7 ${lines.length} lines`;
1753
- writeClipped(buf, innerX, innerY + viewableH, `\x1B[2m${indicator}\x1B[0m`, innerW);
1857
+ const clipped = clipAnsi(`\x1B[2m${indicator}\x1B[0m`, innerW);
1858
+ rows[1 + viewableH] = borderL + clipped + borderR;
1754
1859
  }
1860
+ return rows;
1861
+ }
1862
+ function buildEmptyPanelRows(rect, focused, borderColor, centerText) {
1863
+ const { w, h } = rect;
1864
+ const rows = new Array(h);
1865
+ const color = focused ? "blue" : borderColor;
1866
+ const sgr = `\x1B[${colorToSGR(color)}m`;
1867
+ const reset = "\x1B[0m";
1868
+ const innerW = w - 4;
1869
+ const borderL = sgr + "\u2502" + reset + " ";
1870
+ const borderR = " " + sgr + "\u2502" + reset;
1871
+ const emptyRow = borderL + " ".repeat(innerW) + borderR;
1872
+ rows[0] = sgr + "\u256D" + "\u2500".repeat(w - 2) + "\u256E" + reset;
1873
+ rows[h - 1] = sgr + "\u2570" + "\u2500".repeat(w - 2) + "\u256F" + reset;
1874
+ for (let i = 1; i < h - 1; i++) rows[i] = emptyRow;
1875
+ if (centerText) {
1876
+ const midRow = Math.floor(h / 2);
1877
+ if (midRow > 0 && midRow < h - 1) {
1878
+ const clipped = clipAnsi(centerText, innerW);
1879
+ const textW = displayWidthFast(centerText);
1880
+ const pad = Math.max(0, Math.floor((innerW - textW) / 2));
1881
+ const centered = " ".repeat(pad) + clipped;
1882
+ rows[midRow] = borderL + clipAnsi(centered, innerW) + borderR;
1883
+ }
1884
+ }
1885
+ return rows;
1755
1886
  }
1756
1887
  function sliceDisplayCols(s, start, end) {
1757
1888
  let out = "";
@@ -1761,19 +1892,20 @@ function sliceDisplayCols(s, start, end) {
1761
1892
  let hasOpenSGR = false;
1762
1893
  while (i < s.length && col < end) {
1763
1894
  if (s[i] === "\x1B" && s[i + 1] === "[") {
1764
- const match = s.slice(i).match(/^\x1b\[[0-9;]*[a-zA-Z]/);
1765
- if (match) {
1895
+ const seqLen = ansiLen(s, i);
1896
+ if (seqLen > 0) {
1766
1897
  if (col >= start) {
1767
- out += match[0];
1768
- hasOpenSGR = match[0] !== "\x1B[0m" && match[0] !== "\x1B[m";
1898
+ const seq = s.substring(i, i + seqLen);
1899
+ out += seq;
1900
+ hasOpenSGR = seq !== "\x1B[0m" && seq !== "\x1B[m";
1769
1901
  }
1770
- i += match[0].length;
1902
+ i += seqLen;
1771
1903
  continue;
1772
1904
  }
1773
1905
  }
1774
1906
  const cp = s.codePointAt(i);
1775
1907
  const ch = String.fromCodePoint(cp);
1776
- const chWidth = stringWidth2(ch);
1908
+ const chWidth = cp < 128 ? 1 : stringWidth2(ch);
1777
1909
  if (col >= start) {
1778
1910
  inSlice = true;
1779
1911
  if (col + chWidth > end) break;
@@ -1788,6 +1920,76 @@ function sliceDisplayCols(s, start, end) {
1788
1920
  return out;
1789
1921
  }
1790
1922
 
1923
+ // src/tui/lib/tree-render.ts
1924
+ function renderTreePrefix(node, nodes, index) {
1925
+ if (node.depth === 0) {
1926
+ return node.expandable ? node.expanded ? "\u25BC " : "\u25B8 " : " ";
1927
+ }
1928
+ const parts = [];
1929
+ for (let d = 1; d < node.depth; d++) {
1930
+ parts.push(isAncestorLastSibling(nodes, index, d) ? " " : "\u2502 ");
1931
+ }
1932
+ parts.push(isLastSibling(nodes, index) ? "\u2514\u2500" : "\u251C\u2500");
1933
+ if (node.expandable) {
1934
+ parts.push(node.expanded ? "\u25BC " : "\u25B8 ");
1935
+ } else {
1936
+ parts.push(" ");
1937
+ }
1938
+ return parts.join("");
1939
+ }
1940
+ function isLastSibling(nodes, index) {
1941
+ const depth = nodes[index].depth;
1942
+ for (let i = index + 1; i < nodes.length; i++) {
1943
+ if (nodes[i].depth === depth) return false;
1944
+ if (nodes[i].depth < depth) return true;
1945
+ }
1946
+ return true;
1947
+ }
1948
+ function isAncestorLastSibling(nodes, index, depth) {
1949
+ for (let i = index - 1; i >= 0; i--) {
1950
+ if (nodes[i].depth === depth) {
1951
+ return isLastSibling(nodes, i);
1952
+ }
1953
+ if (nodes[i].depth < depth) return true;
1954
+ }
1955
+ return true;
1956
+ }
1957
+ function precomputePrefixes(nodes) {
1958
+ const len = nodes.length;
1959
+ if (len === 0) return;
1960
+ const isLast = new Array(len);
1961
+ const lastSeenAtDepth = /* @__PURE__ */ new Map();
1962
+ for (let i = len - 1; i >= 0; i--) {
1963
+ const depth = nodes[i].depth;
1964
+ isLast[i] = !lastSeenAtDepth.has(depth);
1965
+ lastSeenAtDepth.set(depth, i);
1966
+ for (const [d] of lastSeenAtDepth) {
1967
+ if (d > depth) lastSeenAtDepth.delete(d);
1968
+ }
1969
+ }
1970
+ const ancestorIsLast = [];
1971
+ for (let i = 0; i < len; i++) {
1972
+ const node = nodes[i];
1973
+ if (node.depth === 0) {
1974
+ node.prefix = node.expandable ? node.expanded ? "\u25BC " : "\u25B8 " : " ";
1975
+ ancestorIsLast[0] = isLast[i];
1976
+ continue;
1977
+ }
1978
+ ancestorIsLast[node.depth] = isLast[i];
1979
+ const parts = [];
1980
+ for (let d = 1; d < node.depth; d++) {
1981
+ parts.push(ancestorIsLast[d] ? " " : "\u2502 ");
1982
+ }
1983
+ parts.push(isLast[i] ? "\u2514\u2500" : "\u251C\u2500");
1984
+ if (node.expandable) {
1985
+ parts.push(node.expanded ? "\u25BC " : "\u25B8 ");
1986
+ } else {
1987
+ parts.push(" ");
1988
+ }
1989
+ node.prefix = parts.join("");
1990
+ }
1991
+ }
1992
+
1791
1993
  // src/tui/lib/client.ts
1792
1994
  function send(request) {
1793
1995
  return rawSend(request, 5e3);
@@ -1804,8 +2006,13 @@ function selectWindow(windowId) {
1804
2006
  function selectPane(paneId) {
1805
2007
  execSafe(`tmux select-pane -t "${paneId}"`);
1806
2008
  }
1807
- function windowExists(windowId) {
1808
- return execSafe(`tmux display-message -t "${windowId}" -p "#{window_id}"`) !== null;
2009
+ function listAllWindowIds() {
2010
+ try {
2011
+ const output = execSync('tmux list-windows -a -F "#{window_id}"', { encoding: "utf-8", env: EXEC_ENV });
2012
+ return new Set(output.trim().split("\n").filter(Boolean));
2013
+ } catch {
2014
+ return /* @__PURE__ */ new Set();
2015
+ }
1809
2016
  }
1810
2017
  var companionPaneId = null;
1811
2018
  function setupCompanionPlugin() {
@@ -1901,41 +2108,6 @@ function copyToClipboard(text) {
1901
2108
  execSync2("pbcopy", { input: text });
1902
2109
  }
1903
2110
 
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
2111
  // src/tui/panels/tree.ts
1940
2112
  function renderNodeContent(node, maxWidth) {
1941
2113
  switch (node.type) {
@@ -2075,7 +2247,7 @@ function renderTreePanel(buf, rect, nodes, cursorIndex, focused) {
2075
2247
  const node = visible[i];
2076
2248
  const realIdx = scrollOffset + i;
2077
2249
  const isSelected = realIdx === cursorIndex;
2078
- const prefix = renderTreePrefix(node, nodes, realIdx);
2250
+ const prefix = node.prefix ?? renderTreePrefix(node, nodes, realIdx);
2079
2251
  const contentWidth = innerW;
2080
2252
  const { icon, label, meta, color, dim, metaColor, suffix, suffixColor } = renderNodeContent(
2081
2253
  node,
@@ -2562,7 +2734,7 @@ function buildLogsLines(cycleLogs, width) {
2562
2734
  }
2563
2735
  return lines;
2564
2736
  }
2565
- function renderDetailContent(buf, rect, state2, detailCtx) {
2737
+ function renderDetailRows(rect, state2, detailCtx) {
2566
2738
  const { session, agents, reportBlocks, detailReportBlocks, contextFileContent } = detailCtx;
2567
2739
  const scrollOffset = state2.detailScroll.offset;
2568
2740
  const focused = state2.focusPane === "detail";
@@ -2570,157 +2742,203 @@ function renderDetailContent(buf, rect, state2, detailCtx) {
2570
2742
  const reportAgent = agents.find((a) => a.id === state2.targetAgentId);
2571
2743
  if (reportAgent) {
2572
2744
  const lines2 = buildReportViewLines(reportAgent, reportBlocks, rect.w);
2573
- renderPanel(buf, rect, lines2, scrollOffset, focused, "cyan");
2574
- return;
2745
+ return buildPanelRows(rect, lines2, scrollOffset, focused, "cyan");
2575
2746
  }
2576
2747
  }
2577
2748
  const cursorNode = detailCtx.nodes[state2.cursorIndex];
2578
2749
  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
- }
2750
+ return buildEmptyPanelRows(rect, false, "gray", "\x1B[2mSelect a session to view details\x1B[0m");
2751
+ }
2752
+ if (cursorNode.sessionId !== session.id) {
2753
+ return buildEmptyPanelRows(rect, false, "gray");
2754
+ }
2755
+ const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
2756
+ const cacheKey = [
2757
+ cursorNode.id,
2758
+ cursorNode.type,
2759
+ state2.mode,
2760
+ state2.targetAgentId,
2761
+ state2.showStrategy,
2762
+ rect.w,
2763
+ session.id,
2764
+ session.agents.length,
2765
+ session.orchestratorCycles.length,
2766
+ lastCycle?.completedAt ?? "",
2767
+ lastCycle?.agentsSpawned.length ?? 0,
2768
+ state2.planContent.length,
2769
+ state2.goalContent.length,
2770
+ state2.strategyContent.length,
2771
+ state2.paneAlive,
2772
+ detailReportBlocks.length,
2773
+ session.messages.length,
2774
+ state2.contextFiles.length,
2775
+ contextFileContent?.length ?? -1
2776
+ ].join(":");
2584
2777
  let lines;
2585
2778
  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) {
2779
+ if (cacheKey === state2.detailCacheKey && state2.cachedDetailLines !== null) {
2780
+ lines = state2.cachedDetailLines;
2781
+ } else {
2782
+ switch (cursorNode.type) {
2783
+ case "session": {
2595
2784
  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);
2785
+ break;
2598
2786
  }
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);
2787
+ case "cycle": {
2788
+ const cycleNode = cursorNode;
2789
+ const cycle = session.orchestratorCycles.find((c) => c.cycle === cycleNode.cycleNumber);
2790
+ if (!cycle) {
2791
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2792
+ } else {
2793
+ lines = buildCycleLines(cycle, session.agents, rect.w);
2794
+ }
2795
+ break;
2608
2796
  }
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);
2797
+ case "agent": {
2798
+ const agentNode = cursorNode;
2799
+ const agent = agents.find((a) => a.id === agentNode.agentId);
2800
+ if (!agent) {
2801
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2802
+ } else {
2803
+ lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2804
+ }
2616
2805
  break;
2617
2806
  }
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);
2807
+ case "report": {
2808
+ const reportNode = cursorNode;
2809
+ const agent = agents.find((a) => a.id === reportNode.agentId);
2810
+ if (!agent) {
2811
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2812
+ break;
2813
+ }
2814
+ const reportIdx = reportNode.reportIndex;
2815
+ const specificBlock = detailReportBlocks.find((_b, i) => {
2816
+ const originalIdx = agent.reports.length - 1 - i;
2817
+ return originalIdx === reportIdx;
2818
+ });
2819
+ if (specificBlock) {
2820
+ const { label: badge, color: badgeColor } = reportBadge(specificBlock.type);
2821
+ lines = [
2822
+ [seg(" "), seg(badge, { color: badgeColor }), seg(` ${agent.id} \xB7 ${agentDisplayName(agent)}`, { bold: true })],
2823
+ singleLine(` ${formatTime(specificBlock.timestamp)}`, { dim: true }),
2824
+ singleLine(" "),
2825
+ [seg(" \u258E CONTENT", { color: badgeColor, bold: true })],
2826
+ ...wrapText(specificBlock.content.trim(), rect.w - 8).map((l) => singleLine(` ${l}`))
2827
+ ];
2828
+ borderColor = badgeColor;
2829
+ } else {
2830
+ lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2831
+ }
2832
+ break;
2635
2833
  }
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
- ]);
2834
+ case "messages": {
2835
+ lines = [singleLine(` Messages (${session.messages.length})`, { bold: true })];
2836
+ if (session.messages.length === 0) {
2837
+ lines.push(singleLine(" No messages", { dim: true, italic: true }));
2838
+ } else {
2839
+ for (const msg of session.messages) {
2840
+ const time = formatTime(msg.timestamp);
2841
+ const agentId = msg.source.type === "agent" ? msg.source.agentId : void 0;
2842
+ const label = messageSourceLabel(msg.source.type, agentId);
2843
+ const labelColor = messageSourceColor(msg.source.type);
2844
+ const maxContent = Math.max(10, rect.w - label.length - 20);
2845
+ lines.push([
2846
+ seg(` [${time}] `, { dim: true }),
2847
+ seg(`${label}: `, { color: labelColor, bold: true }),
2848
+ seg(wrapText(msg.summary.length > 0 ? msg.summary : msg.content, maxContent)[0], {})
2849
+ ]);
2850
+ }
2654
2851
  }
2852
+ break;
2655
2853
  }
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}`));
2854
+ case "message": {
2855
+ const msgNode = cursorNode;
2856
+ const msg = session.messages.find((m) => m.id === msgNode.messageId);
2857
+ lines = [singleLine(" Message", { bold: true })];
2858
+ if (msg) {
2859
+ lines.push(singleLine(` ${msgNode.source} \xB7 ${msgNode.timestamp}`, { dim: true }));
2860
+ for (const l of wrapText(msg.content, rect.w - 8)) {
2861
+ lines.push(singleLine(` ${l}`));
2862
+ }
2863
+ } else {
2864
+ lines.push(singleLine(" Message not found", { dim: true }));
2666
2865
  }
2667
- } else {
2668
- lines.push(singleLine(" Message not found", { dim: true }));
2866
+ break;
2669
2867
  }
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 }));
2868
+ case "context": {
2869
+ lines = [
2870
+ [seg(" "), seg("\u229E", { color: "white" }), seg(` Context (${state2.contextFiles.length})`, { bold: true })]
2871
+ ];
2872
+ if (state2.contextFiles.length === 0) {
2873
+ lines.push(singleLine(" No context files found.", { dim: true }));
2874
+ } else {
2875
+ for (const f of state2.contextFiles) {
2876
+ lines.push(singleLine(` \xB7 ${f}`, { dim: true }));
2877
+ }
2681
2878
  }
2879
+ break;
2682
2880
  }
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 }));
2881
+ case "context-file": {
2882
+ const ctxFileNode = cursorNode;
2883
+ lines = [
2884
+ [seg(" "), seg("\u229E", { color: "white" }), seg(` ${ctxFileNode.label}`, { bold: true })],
2885
+ singleLine(" ")
2886
+ ];
2887
+ if (contextFileContent == null) {
2888
+ lines.push(singleLine(" File not found or unreadable.", { dim: true }));
2697
2889
  } else {
2698
- for (const l of wrapped) {
2699
- lines.push(singleLine(` ${l}`));
2890
+ const wrapped = wrapText(stripFrontmatter(contextFileContent), rect.w - 8);
2891
+ if (wrapped.length === 0) {
2892
+ lines.push(singleLine(" (empty)", { dim: true }));
2893
+ } else {
2894
+ for (const l of wrapped) {
2895
+ lines.push(singleLine(` ${l}`));
2896
+ }
2700
2897
  }
2701
2898
  }
2899
+ borderColor = "white";
2900
+ break;
2901
+ }
2902
+ default: {
2903
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2904
+ break;
2702
2905
  }
2703
- borderColor = "white";
2704
- break;
2705
2906
  }
2706
- default: {
2707
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2708
- break;
2907
+ state2.cachedDetailLines = lines;
2908
+ state2.detailCacheKey = cacheKey;
2909
+ }
2910
+ if (cursorNode.type === "context-file") {
2911
+ borderColor = "white";
2912
+ } else if (cursorNode.type === "report") {
2913
+ const reportNode = cursorNode;
2914
+ const agent = agents.find((a) => a.id === reportNode.agentId);
2915
+ if (agent) {
2916
+ const reportIdx = reportNode.reportIndex;
2917
+ const specificBlock = detailReportBlocks.find((_b, i) => {
2918
+ const originalIdx = agent.reports.length - 1 - i;
2919
+ return originalIdx === reportIdx;
2920
+ });
2921
+ if (specificBlock) borderColor = reportBadge(specificBlock.type).color;
2709
2922
  }
2710
2923
  }
2711
- renderPanel(buf, rect, lines, scrollOffset, focused, borderColor);
2924
+ return buildPanelRows(rect, lines, scrollOffset, focused, borderColor, state2.detailRenderedCache);
2712
2925
  }
2713
- function renderLogsContent(buf, rect, state2) {
2926
+ function renderLogsRows(rect, state2) {
2714
2927
  const focused = state2.focusPane === "logs";
2715
2928
  const scrollOffset = state2.logsScroll.offset;
2716
2929
  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;
2930
+ return buildEmptyPanelRows(rect, focused, "gray", "\x1B[2mNo logs\x1B[0m");
2931
+ }
2932
+ const logsCacheKey = `${state2.logsCycles.length}:${rect.w}:${state2.logsCycles.map((c) => c.cycle).join(",")}`;
2933
+ let lines;
2934
+ if (logsCacheKey === state2.logsCacheKey && state2.cachedLogsLines !== null) {
2935
+ lines = state2.cachedLogsLines;
2936
+ } else {
2937
+ lines = buildLogsLines(state2.logsCycles, rect.w);
2938
+ state2.cachedLogsLines = lines;
2939
+ state2.logsCacheKey = logsCacheKey;
2721
2940
  }
2722
- const lines = buildLogsLines(state2.logsCycles, rect.w);
2723
- renderPanel(buf, rect, lines, scrollOffset, focused, "gray");
2941
+ return buildPanelRows(rect, lines, scrollOffset, focused, "gray", state2.logsRenderedCache);
2724
2942
  }
2725
2943
 
2726
2944
  // src/tui/panels/bottom.ts
@@ -2889,6 +3107,10 @@ var latestNodes = [];
2889
3107
  var cachedContextFilePath = null;
2890
3108
  var cachedContextFileContent = null;
2891
3109
  var prevFrame = [];
3110
+ var prevTreeInputs = "";
3111
+ var prevBottomInputs = "";
3112
+ var prevOverlayMode = "";
3113
+ var cachedTreeRows = [];
2892
3114
  var cachedLogSessionId = null;
2893
3115
  var cachedLogFiles = /* @__PURE__ */ new Map();
2894
3116
  function getAgentForNode(node, agents) {
@@ -2901,6 +3123,7 @@ function getAgentForNode(node, agents) {
2901
3123
  function startApp(state2, cleanup2) {
2902
3124
  const config = loadConfig(state2.cwd);
2903
3125
  let prevSelectedSessionId = void 0;
3126
+ let debouncedPollTimer = null;
2904
3127
  async function poll() {
2905
3128
  try {
2906
3129
  let selectedSession = null;
@@ -2918,13 +3141,10 @@ function startApp(state2, cleanup2) {
2918
3141
  statusPromise ?? Promise.resolve(null)
2919
3142
  ]);
2920
3143
  const sessions = listRes.ok ? listRes.data?.sessions ?? [] : [];
3144
+ const aliveWindows = listAllWindowIds();
2921
3145
  for (const s of sessions) {
2922
3146
  if (s.status !== "completed" && s.tmuxWindowId) {
2923
- try {
2924
- s.windowAlive = windowExists(s.tmuxWindowId);
2925
- } catch {
2926
- s.windowAlive = false;
2927
- }
3147
+ s.windowAlive = aliveWindows.has(s.tmuxWindowId);
2928
3148
  }
2929
3149
  }
2930
3150
  if (state2.selectedSessionId) {
@@ -3027,7 +3247,7 @@ function startApp(state2, cleanup2) {
3027
3247
  writeCenter(buf, Math.floor(state2.rows / 2), "Terminal too small \u2014 resize to continue");
3028
3248
  const out2 = flushFrame(buf.lines, prevFrame);
3029
3249
  writeToStdout(out2);
3030
- prevFrame = [...buf.lines];
3250
+ prevFrame = buf.lines;
3031
3251
  return;
3032
3252
  }
3033
3253
  const treeWidth = 36;
@@ -3055,6 +3275,7 @@ function startApp(state2, cleanup2) {
3055
3275
  state2.cwd,
3056
3276
  state2.contextFiles
3057
3277
  );
3278
+ precomputePrefixes(nodes);
3058
3279
  state2.cachedTreeNodes = nodes;
3059
3280
  state2.treeCacheKey = cacheKey;
3060
3281
  }
@@ -3067,11 +3288,19 @@ function startApp(state2, cleanup2) {
3067
3288
  state2.selectedSessionId = newSessionId;
3068
3289
  state2.detailScroll.reset();
3069
3290
  state2.logsScroll.reset();
3291
+ state2.cachedDetailLines = null;
3292
+ state2.detailCacheKey = "";
3293
+ state2.cachedLogsLines = null;
3294
+ state2.logsCacheKey = "";
3070
3295
  }
3071
3296
  if (state2.selectedSessionId !== prevSelectedSessionId) {
3072
3297
  prevSelectedSessionId = state2.selectedSessionId;
3298
+ if (debouncedPollTimer !== null) clearTimeout(debouncedPollTimer);
3073
3299
  if (state2.selectedSessionId !== null) {
3074
- void poll();
3300
+ debouncedPollTimer = setTimeout(() => {
3301
+ debouncedPollTimer = null;
3302
+ void poll();
3303
+ }, 80);
3075
3304
  }
3076
3305
  }
3077
3306
  autoExpandCycle(state2);
@@ -3099,13 +3328,37 @@ function startApp(state2, cleanup2) {
3099
3328
  cachedContextFilePath = null;
3100
3329
  cachedContextFileContent = null;
3101
3330
  }
3102
- renderTreePanel(
3103
- buf,
3104
- treeRect,
3105
- nodes,
3106
- state2.cursorIndex,
3107
- state2.mode === "navigate" && state2.focusPane === "tree"
3108
- );
3331
+ const treeFocused = state2.mode === "navigate" && state2.focusPane === "tree";
3332
+ const treeInputs = `${state2.treeCacheKey}:${state2.cursorIndex}:${treeFocused}`;
3333
+ const bottomInputs = `${state2.notification}:${state2.error}:${state2.mode}:${state2.inputText}:${state2.inputCursorPos}:${cursorNode?.type}`;
3334
+ const overlayMode = state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help" ? state2.mode : "";
3335
+ const hasPrev = prevFrame.length === buf.height;
3336
+ const treeDirty = !hasPrev || treeInputs !== prevTreeInputs;
3337
+ const bottomDirty = !hasPrev || bottomInputs !== prevBottomInputs;
3338
+ const overlayDirty = !hasPrev || overlayMode !== prevOverlayMode;
3339
+ prevTreeInputs = treeInputs;
3340
+ prevBottomInputs = bottomInputs;
3341
+ prevOverlayMode = overlayMode;
3342
+ let treeRows;
3343
+ if (treeDirty) {
3344
+ const treeBlank = " ".repeat(treeWidth);
3345
+ const treeBuf = {
3346
+ lines: Array.from({ length: contentHeight }, () => treeBlank),
3347
+ width: treeWidth,
3348
+ height: contentHeight
3349
+ };
3350
+ renderTreePanel(
3351
+ treeBuf,
3352
+ { x: 0, y: 0, w: treeWidth, h: contentHeight },
3353
+ nodes,
3354
+ state2.cursorIndex,
3355
+ treeFocused
3356
+ );
3357
+ cachedTreeRows = treeBuf.lines;
3358
+ treeRows = treeBuf.lines;
3359
+ } else {
3360
+ treeRows = cachedTreeRows;
3361
+ }
3109
3362
  const detailCtx = {
3110
3363
  nodes,
3111
3364
  session: state2.selectedSession,
@@ -3114,19 +3367,30 @@ function startApp(state2, cleanup2) {
3114
3367
  detailReportBlocks,
3115
3368
  contextFileContent
3116
3369
  };
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);
3370
+ const detailRows = renderDetailRows(detailRect, state2, detailCtx);
3371
+ const logsRows = logsRect ? renderLogsRows(logsRect, state2) : null;
3372
+ for (let i = 0; i < contentHeight; i++) {
3373
+ if (logsRows) {
3374
+ buf.lines[i] = treeRows[i] + detailRows[i] + logsRows[i];
3375
+ } else {
3376
+ buf.lines[i] = treeRows[i] + detailRows[i];
3377
+ }
3378
+ }
3379
+ if (bottomDirty || overlayDirty) {
3380
+ renderNotificationRow(buf, bottomY, state2.notification, state2.error);
3381
+ renderInputBar(buf, bottomY + 1, state2);
3382
+ renderStatusLine(buf, bottomY + 2, state2, cursorNode?.type);
3383
+ } else {
3384
+ copyRows(buf, prevFrame, bottomY, 3);
3385
+ }
3386
+ if (overlayMode) {
3387
+ if (state2.mode === "leader") renderLeaderOverlay(buf, state2.rows, state2.cols);
3388
+ if (state2.mode === "copy-menu") renderCopyMenuOverlay(buf, state2.rows, state2.cols);
3389
+ if (state2.mode === "help") renderHelpOverlay(buf, state2.rows, state2.cols);
3390
+ }
3127
3391
  const out = flushFrame(buf.lines, prevFrame);
3128
3392
  writeToStdout(out);
3129
- prevFrame = [...buf.lines];
3393
+ prevFrame = buf.lines;
3130
3394
  }
3131
3395
  const inputActions = {
3132
3396
  getNodes: () => latestNodes,
@@ -3187,6 +3451,7 @@ function startApp(state2, cleanup2) {
3187
3451
  const origCleanup = inputActions.cleanup;
3188
3452
  inputActions.cleanup = () => {
3189
3453
  clearInterval(pollInterval);
3454
+ if (debouncedPollTimer !== null) clearTimeout(debouncedPollTimer);
3190
3455
  stopKeypress();
3191
3456
  stopResize();
3192
3457
  state2.detailScroll.destroy();