sisyphi 1.1.10 → 1.1.12

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,
@@ -339,6 +347,11 @@ function createAppState(cwd2) {
339
347
  cachedLogsLines: null,
340
348
  logsCacheKey: "",
341
349
  logsRenderedCache: { lines: [], ansi: [] },
350
+ nvimBridge: null,
351
+ nvimEnabled: true,
352
+ prevNvimFile: null,
353
+ nvimEditable: false,
354
+ nvimOpenTabs: /* @__PURE__ */ new Map(),
342
355
  cwd: cwd2
343
356
  };
344
357
  }
@@ -402,8 +415,8 @@ function autoExpandCycle(state2) {
402
415
  }
403
416
 
404
417
  // src/tui/app.ts
405
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync, statSync } from "fs";
406
- 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";
407
420
 
408
421
  // src/tui/lib/tree.ts
409
422
  import { join } from "path";
@@ -791,6 +804,21 @@ function findParentIndex(nodes, index) {
791
804
  }
792
805
 
793
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
+ }
794
822
  function handleCancel(state2) {
795
823
  state2.mode = "navigate";
796
824
  state2.targetAgentId = null;
@@ -1291,10 +1319,14 @@ function handleNavigateKey(input, key, state2, actions) {
1291
1319
  if (key.leftArrow || input === "h") {
1292
1320
  if (state2.focusPane === "logs") {
1293
1321
  state2.focusPane = "detail";
1322
+ if (state2.nvimEnabled && state2.nvimBridge?.ready) {
1323
+ activateNvimBypass(state2);
1324
+ }
1294
1325
  requestRender();
1295
1326
  return;
1296
1327
  }
1297
1328
  if (state2.focusPane === "detail") {
1329
+ deactivateNvimBypass();
1298
1330
  state2.focusPane = "tree";
1299
1331
  requestRender();
1300
1332
  return;
@@ -1333,8 +1365,12 @@ function handleNavigateKey(input, key, state2, actions) {
1333
1365
  if (key.tab) {
1334
1366
  if (state2.focusPane === "tree") {
1335
1367
  state2.focusPane = "detail";
1368
+ if (state2.nvimEnabled && state2.nvimBridge?.ready) {
1369
+ activateNvimBypass(state2);
1370
+ }
1336
1371
  } else if (state2.focusPane === "detail") {
1337
- state2.focusPane = state2.showLogs ? "logs" : "tree";
1372
+ deactivateNvimBypass();
1373
+ state2.focusPane = state2.showCombinedView ? "logs" : "tree";
1338
1374
  } else {
1339
1375
  state2.focusPane = "tree";
1340
1376
  }
@@ -1577,15 +1613,6 @@ function handleNavigateKey(input, key, state2, actions) {
1577
1613
  }
1578
1614
  return;
1579
1615
  }
1580
- if (input === "s") {
1581
- if (!state2.strategyContent) {
1582
- notify(state2, "No strategy for this session");
1583
- return;
1584
- }
1585
- state2.showStrategy = !state2.showStrategy;
1586
- requestRender();
1587
- return;
1588
- }
1589
1616
  if (input === "S") {
1590
1617
  if (!state2.selectedSessionId) {
1591
1618
  notify(state2, "No session selected");
@@ -1601,11 +1628,11 @@ function handleNavigateKey(input, key, state2, actions) {
1601
1628
  return;
1602
1629
  }
1603
1630
  if (input === "t") {
1604
- if (state2.showLogs) {
1631
+ if (state2.showCombinedView) {
1605
1632
  if (state2.focusPane === "logs") state2.focusPane = "detail";
1606
1633
  state2.logsScroll.reset();
1607
1634
  }
1608
- state2.showLogs = !state2.showLogs;
1635
+ state2.showCombinedView = !state2.showCombinedView;
1609
1636
  requestRender();
1610
1637
  return;
1611
1638
  }
@@ -1673,7 +1700,7 @@ function copyRows(buf, src, startRow, count) {
1673
1700
  buf.lines[startRow + i] = src[startRow + i];
1674
1701
  }
1675
1702
  }
1676
- function flushFrame(frame, prevFrame2) {
1703
+ function flushFrame(frame, prevFrame2, suffix) {
1677
1704
  let out = "\x1B[?2026h";
1678
1705
  for (let i = 0; i < frame.length; i++) {
1679
1706
  if (frame[i] !== prevFrame2[i]) {
@@ -1682,6 +1709,7 @@ function flushFrame(frame, prevFrame2) {
1682
1709
  out += frame[i];
1683
1710
  }
1684
1711
  }
1712
+ if (suffix) out += suffix;
1685
1713
  out += "\x1B[?2026l";
1686
1714
  return out;
1687
1715
  }
@@ -2359,7 +2387,7 @@ function buildPlanLines(content, maxLines, width) {
2359
2387
  }
2360
2388
  return lines;
2361
2389
  }
2362
- function buildSessionLines(session, planContent, goalContent, width, paneAlive, strategyContent = "", showStrategy = false) {
2390
+ function buildSessionLines(session, planContent, goalContent, width, paneAlive, strategyContent = "") {
2363
2391
  const lines = [];
2364
2392
  const contentWidth = width - 4;
2365
2393
  const agents = session.agents;
@@ -2400,12 +2428,8 @@ function buildSessionLines(session, planContent, goalContent, width, paneAlive,
2400
2428
  ]);
2401
2429
  }
2402
2430
  lines.push(singleLine(" "));
2403
- if (showStrategy && strategyContent) {
2404
- const stratHint = strategyContent ? " [s] toggle plan" : "";
2405
- lines.push([
2406
- seg(" \u258E \u25C8 STRATEGY", { color: "yellow", bold: true }),
2407
- seg(stratHint, { dim: true })
2408
- ]);
2431
+ if (strategyContent) {
2432
+ lines.push([seg(" \u258E \u25C8 STRATEGY", { color: "yellow", bold: true })]);
2409
2433
  const stratLines = buildPlanLines(strategyContent, 99999, width);
2410
2434
  if (stratLines.length === 0) {
2411
2435
  lines.push(singleLine(" (empty)", { dim: true, italic: true }));
@@ -2415,11 +2439,7 @@ function buildSessionLines(session, planContent, goalContent, width, paneAlive,
2415
2439
  }
2416
2440
  }
2417
2441
  } else {
2418
- const toggleHint = strategyContent ? " [s] toggle strategy" : "";
2419
- lines.push([
2420
- seg(" \u258E \u25C8 PLAN", { color: "yellow", bold: true }),
2421
- seg(toggleHint, { dim: true })
2422
- ]);
2442
+ lines.push([seg(" \u258E \u25C8 PLAN", { color: "yellow", bold: true })]);
2423
2443
  const planLines = buildPlanLines(planContent, 99999, width);
2424
2444
  if (planLines.length === 0) {
2425
2445
  lines.push(singleLine(" orchestrator will create one", { dim: true, italic: true }));
@@ -2758,7 +2778,6 @@ function renderDetailRows(rect, state2, detailCtx) {
2758
2778
  cursorNode.type,
2759
2779
  state2.mode,
2760
2780
  state2.targetAgentId,
2761
- state2.showStrategy,
2762
2781
  rect.w,
2763
2782
  session.id,
2764
2783
  session.agents.length,
@@ -2781,14 +2800,14 @@ function renderDetailRows(rect, state2, detailCtx) {
2781
2800
  } else {
2782
2801
  switch (cursorNode.type) {
2783
2802
  case "session": {
2784
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2803
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2785
2804
  break;
2786
2805
  }
2787
2806
  case "cycle": {
2788
2807
  const cycleNode = cursorNode;
2789
2808
  const cycle = session.orchestratorCycles.find((c) => c.cycle === cycleNode.cycleNumber);
2790
2809
  if (!cycle) {
2791
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2810
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2792
2811
  } else {
2793
2812
  lines = buildCycleLines(cycle, session.agents, rect.w);
2794
2813
  }
@@ -2798,7 +2817,7 @@ function renderDetailRows(rect, state2, detailCtx) {
2798
2817
  const agentNode = cursorNode;
2799
2818
  const agent = agents.find((a) => a.id === agentNode.agentId);
2800
2819
  if (!agent) {
2801
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2820
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2802
2821
  } else {
2803
2822
  lines = buildAgentLines(agent, detailReportBlocks, rect.w);
2804
2823
  }
@@ -2808,7 +2827,7 @@ function renderDetailRows(rect, state2, detailCtx) {
2808
2827
  const reportNode = cursorNode;
2809
2828
  const agent = agents.find((a) => a.id === reportNode.agentId);
2810
2829
  if (!agent) {
2811
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2830
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2812
2831
  break;
2813
2832
  }
2814
2833
  const reportIdx = reportNode.reportIndex;
@@ -2900,7 +2919,7 @@ function renderDetailRows(rect, state2, detailCtx) {
2900
2919
  break;
2901
2920
  }
2902
2921
  default: {
2903
- lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent, state2.showStrategy);
2922
+ lines = buildSessionLines(session, state2.planContent, state2.goalContent, rect.w, state2.paneAlive, state2.strategyContent);
2904
2923
  break;
2905
2924
  }
2906
2925
  }
@@ -2941,6 +2960,52 @@ function renderLogsRows(rect, state2) {
2941
2960
  return buildPanelRows(rect, lines, scrollOffset, focused, "gray", state2.logsRenderedCache);
2942
2961
  }
2943
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;
3007
+ }
3008
+
2944
3009
  // src/tui/panels/bottom.ts
2945
3010
  function renderNotificationRow(buf, y, notification, error) {
2946
3011
  if (notification !== null) {
@@ -2995,13 +3060,13 @@ function renderStatusLine(buf, y, state2, cursorNodeType) {
2995
3060
  } else if (mode !== "navigate") {
2996
3061
  content = D("[enter] send [esc] cancel");
2997
3062
  } else if (focusPane === "logs" || focusPane === "detail") {
2998
- 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");
2999
3064
  } else {
3000
3065
  let contextFilePart = "";
3001
3066
  if (cursorNodeType === "context-file") {
3002
3067
  contextFilePart = B("[e]") + D("dit ") + B("[\u23CE]") + D(" open ");
3003
3068
  }
3004
- 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");
3005
3070
  }
3006
3071
  writeClipped(buf, 1, y, content, buf.width - 2);
3007
3072
  }
@@ -3102,6 +3167,590 @@ function renderHelpOverlay(buf, rows, cols) {
3102
3167
  }
3103
3168
  }
3104
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
+
3105
3754
  // src/tui/app.ts
3106
3755
  var latestNodes = [];
3107
3756
  var cachedContextFilePath = null;
@@ -3113,6 +3762,67 @@ var prevOverlayMode = "";
3113
3762
  var cachedTreeRows = [];
3114
3763
  var cachedLogSessionId = null;
3115
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
+ }
3116
3826
  function getAgentForNode(node, agents) {
3117
3827
  if (!node) return null;
3118
3828
  if (node.type === "agent" || node.type === "report") {
@@ -3122,6 +3832,16 @@ function getAgentForNode(node, agents) {
3122
3832
  }
3123
3833
  function startApp(state2, cleanup2) {
3124
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;
3125
3845
  let prevSelectedSessionId = void 0;
3126
3846
  let debouncedPollTimer = null;
3127
3847
  async function poll() {
@@ -3157,28 +3877,28 @@ function startApp(state2, cleanup2) {
3157
3877
  }
3158
3878
  try {
3159
3879
  const pp = roadmapPath(state2.cwd, state2.selectedSessionId);
3160
- if (existsSync2(pp)) {
3880
+ if (existsSync3(pp)) {
3161
3881
  planContent = readFileSync2(pp, "utf-8");
3162
3882
  }
3163
3883
  } catch {
3164
3884
  }
3165
3885
  try {
3166
3886
  const gp = goalPath(state2.cwd, state2.selectedSessionId);
3167
- if (existsSync2(gp)) {
3887
+ if (existsSync3(gp)) {
3168
3888
  goalContent = readFileSync2(gp, "utf-8");
3169
3889
  }
3170
3890
  } catch {
3171
3891
  }
3172
3892
  try {
3173
3893
  const sp = strategyPath(state2.cwd, state2.selectedSessionId);
3174
- if (existsSync2(sp)) {
3894
+ if (existsSync3(sp)) {
3175
3895
  strategyContent = readFileSync2(sp, "utf-8");
3176
3896
  }
3177
3897
  } catch {
3178
3898
  }
3179
3899
  try {
3180
3900
  const ld = logsDir(state2.cwd, state2.selectedSessionId);
3181
- if (existsSync2(ld)) {
3901
+ if (existsSync3(ld)) {
3182
3902
  if (state2.selectedSessionId !== cachedLogSessionId) {
3183
3903
  cachedLogFiles = /* @__PURE__ */ new Map();
3184
3904
  cachedLogSessionId = state2.selectedSessionId;
@@ -3189,7 +3909,7 @@ function startApp(state2, cleanup2) {
3189
3909
  if (!fileSet.has(key)) cachedLogFiles.delete(key);
3190
3910
  }
3191
3911
  for (const f of files) {
3192
- const filePath = join3(ld, f);
3912
+ const filePath = join5(ld, f);
3193
3913
  const mtime = statSync(filePath).mtimeMs;
3194
3914
  const cached = cachedLogFiles.get(f);
3195
3915
  if (!cached || cached.mtime !== mtime) {
@@ -3209,7 +3929,7 @@ function startApp(state2, cleanup2) {
3209
3929
  }
3210
3930
  try {
3211
3931
  const cd = contextDir(state2.cwd, state2.selectedSessionId);
3212
- if (existsSync2(cd)) {
3932
+ if (existsSync3(cd)) {
3213
3933
  contextFiles = readdirSync(cd).filter((f) => !f.startsWith(".")).sort();
3214
3934
  }
3215
3935
  } catch {
@@ -3231,6 +3951,9 @@ function startApp(state2, cleanup2) {
3231
3951
  state2.paneAlive = paneAlive;
3232
3952
  state2.contextFiles = contextFiles;
3233
3953
  state2.error = null;
3954
+ if (state2.nvimEnabled && state2.nvimBridge?.ready && state2.prevNvimFile) {
3955
+ state2.nvimBridge.checktime();
3956
+ }
3234
3957
  requestRender();
3235
3958
  } catch (err) {
3236
3959
  state2.error = err.message;
@@ -3250,14 +3973,14 @@ function startApp(state2, cleanup2) {
3250
3973
  prevFrame = buf.lines;
3251
3974
  return;
3252
3975
  }
3253
- const treeWidth = 36;
3254
- const remaining = state2.cols - treeWidth;
3255
- const detailWidth = state2.showLogs ? Math.floor(remaining * 0.6) : remaining;
3256
- 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;
3257
3980
  const contentHeight = state2.rows - 3;
3258
- const treeRect = { x: 0, y: 0, w: treeWidth, h: contentHeight };
3259
- const detailRect = { x: treeWidth, y: 0, w: detailWidth, h: contentHeight };
3260
- 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;
3261
3984
  const bottomY = contentHeight;
3262
3985
  const filteredSessions = state2.searchFilter ? state2.sessions.filter((s) => {
3263
3986
  const q = state2.searchFilter.toLowerCase();
@@ -3290,6 +4013,7 @@ function startApp(state2, cleanup2) {
3290
4013
  state2.logsScroll.reset();
3291
4014
  state2.cachedDetailLines = null;
3292
4015
  state2.detailCacheKey = "";
4016
+ state2.prevNvimFile = null;
3293
4017
  state2.cachedLogsLines = null;
3294
4018
  state2.logsCacheKey = "";
3295
4019
  }
@@ -3314,7 +4038,7 @@ function startApp(state2, cleanup2) {
3314
4038
  if (cursorNode.filePath !== cachedContextFilePath) {
3315
4039
  cachedContextFilePath = cursorNode.filePath;
3316
4040
  try {
3317
- if (existsSync2(cursorNode.filePath)) {
4041
+ if (existsSync3(cursorNode.filePath)) {
3318
4042
  cachedContextFileContent = readFileSync2(cursorNode.filePath, "utf-8");
3319
4043
  } else {
3320
4044
  cachedContextFileContent = null;
@@ -3341,15 +4065,15 @@ function startApp(state2, cleanup2) {
3341
4065
  prevOverlayMode = overlayMode;
3342
4066
  let treeRows;
3343
4067
  if (treeDirty) {
3344
- const treeBlank = " ".repeat(treeWidth);
4068
+ const treeBlank = " ".repeat(treeWidth2);
3345
4069
  const treeBuf = {
3346
4070
  lines: Array.from({ length: contentHeight }, () => treeBlank),
3347
- width: treeWidth,
4071
+ width: treeWidth2,
3348
4072
  height: contentHeight
3349
4073
  };
3350
4074
  renderTreePanel(
3351
4075
  treeBuf,
3352
- { x: 0, y: 0, w: treeWidth, h: contentHeight },
4076
+ { x: 0, y: 0, w: treeWidth2, h: contentHeight },
3353
4077
  nodes,
3354
4078
  state2.cursorIndex,
3355
4079
  treeFocused
@@ -3367,7 +4091,23 @@ function startApp(state2, cleanup2) {
3367
4091
  detailReportBlocks,
3368
4092
  contextFileContent
3369
4093
  };
3370
- const detailRows = renderDetailRows(detailRect, state2, detailCtx);
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
+ }
3371
4111
  const logsRows = logsRect ? renderLogsRows(logsRect, state2) : null;
3372
4112
  for (let i = 0; i < contentHeight; i++) {
3373
4113
  if (logsRows) {
@@ -3388,7 +4128,16 @@ function startApp(state2, cleanup2) {
3388
4128
  if (state2.mode === "copy-menu") renderCopyMenuOverlay(buf, state2.rows, state2.cols);
3389
4129
  if (state2.mode === "help") renderHelpOverlay(buf, state2.rows, state2.cols);
3390
4130
  }
3391
- const out = flushFrame(buf.lines, prevFrame);
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);
3392
4141
  writeToStdout(out);
3393
4142
  prevFrame = buf.lines;
3394
4143
  }
@@ -3444,6 +4193,11 @@ function startApp(state2, cleanup2) {
3444
4193
  state2.rows = typeof stdoutRows === "number" && stdoutRows > 0 ? stdoutRows : 24;
3445
4194
  state2.cols = typeof stdoutCols === "number" && stdoutCols > 0 ? stdoutCols : 80;
3446
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
+ }
3447
4201
  requestRender();
3448
4202
  });
3449
4203
  void poll();
@@ -3456,6 +4210,7 @@ function startApp(state2, cleanup2) {
3456
4210
  stopResize();
3457
4211
  state2.detailScroll.destroy();
3458
4212
  state2.logsScroll.destroy();
4213
+ state2.nvimBridge?.destroy();
3459
4214
  origCleanup();
3460
4215
  };
3461
4216
  requestRender();