sisyphi 1.1.12 → 1.1.13
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/{chunk-ZSIYQB45.js → chunk-CAJEBTUE.js} +6 -2
- package/dist/chunk-CAJEBTUE.js.map +1 -0
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +53 -47
- package/dist/daemon.js.map +1 -1
- package/dist/templates/orchestrator-base.md +9 -7
- package/dist/templates/orchestrator-completion.md +5 -0
- package/dist/templates/orchestrator-impl.md +5 -0
- package/dist/templates/orchestrator-planning.md +5 -0
- package/dist/templates/orchestrator-plugin/hooks/hooks.json +10 -0
- package/dist/templates/orchestrator-plugin/hooks/idle-notify.sh +71 -0
- package/dist/templates/orchestrator-strategy.md +5 -0
- package/dist/templates/orchestrator-validation.md +5 -0
- package/dist/tui.js +577 -125
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/templates/orchestrator-base.md +9 -7
- package/templates/orchestrator-completion.md +5 -0
- package/templates/orchestrator-impl.md +5 -0
- package/templates/orchestrator-planning.md +5 -0
- package/templates/orchestrator-plugin/hooks/hooks.json +10 -0
- package/templates/orchestrator-plugin/hooks/idle-notify.sh +71 -0
- package/templates/orchestrator-strategy.md +5 -0
- package/templates/orchestrator-validation.md +5 -0
- package/dist/chunk-ZSIYQB45.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
exec,
|
|
6
6
|
execSafe,
|
|
7
7
|
loadConfig
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CAJEBTUE.js";
|
|
9
9
|
import {
|
|
10
10
|
buildSessionContext,
|
|
11
11
|
computeActiveTimeMs,
|
|
@@ -226,6 +226,15 @@ function onResize(callback) {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// src/tui/state.ts
|
|
229
|
+
var OPTIONAL_COMPOSE = /* @__PURE__ */ new Set(["resume", "continue"]);
|
|
230
|
+
var COMPOSE_HEADERS = {
|
|
231
|
+
"new-session": "New Session",
|
|
232
|
+
"message-orchestrator": "Message Orchestrator",
|
|
233
|
+
"resume": "Resume Session",
|
|
234
|
+
"continue": "Continue Session",
|
|
235
|
+
"spawn-agent": "Spawn Agent",
|
|
236
|
+
"message-agent": "Message Agent"
|
|
237
|
+
};
|
|
229
238
|
var INPUT_MODES = /* @__PURE__ */ new Set([
|
|
230
239
|
"resume",
|
|
231
240
|
"continue",
|
|
@@ -352,6 +361,11 @@ function createAppState(cwd2) {
|
|
|
352
361
|
prevNvimFile: null,
|
|
353
362
|
nvimEditable: false,
|
|
354
363
|
nvimOpenTabs: /* @__PURE__ */ new Map(),
|
|
364
|
+
composeAction: null,
|
|
365
|
+
composeTempFile: null,
|
|
366
|
+
composeSignalFile: null,
|
|
367
|
+
composePollTimer: null,
|
|
368
|
+
composePrevNvimFile: null,
|
|
355
369
|
cwd: cwd2
|
|
356
370
|
};
|
|
357
371
|
}
|
|
@@ -415,8 +429,13 @@ function autoExpandCycle(state2) {
|
|
|
415
429
|
}
|
|
416
430
|
|
|
417
431
|
// src/tui/app.ts
|
|
418
|
-
import { readFileSync as
|
|
419
|
-
import { join as
|
|
432
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
|
|
433
|
+
import { join as join6 } from "path";
|
|
434
|
+
|
|
435
|
+
// src/tui/input.ts
|
|
436
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "fs";
|
|
437
|
+
import { join as join2 } from "path";
|
|
438
|
+
import { tmpdir } from "os";
|
|
420
439
|
|
|
421
440
|
// src/tui/lib/tree.ts
|
|
422
441
|
import { join } from "path";
|
|
@@ -806,7 +825,18 @@ function findParentIndex(nodes, index) {
|
|
|
806
825
|
// src/tui/input.ts
|
|
807
826
|
function activateNvimBypass(state2) {
|
|
808
827
|
setRawBypass((data) => {
|
|
828
|
+
if (!state2.nvimBridge?.ready) {
|
|
829
|
+
deactivateNvimBypass();
|
|
830
|
+
state2.focusPane = "tree";
|
|
831
|
+
if (state2.mode === "compose") cancelCompose(state2);
|
|
832
|
+
requestRender();
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
809
835
|
if (data === " ") {
|
|
836
|
+
if (state2.mode === "compose") {
|
|
837
|
+
cancelCompose(state2);
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
810
840
|
deactivateNvimBypass();
|
|
811
841
|
state2.focusPane = state2.showCombinedView ? "logs" : "tree";
|
|
812
842
|
requestRender();
|
|
@@ -819,6 +849,148 @@ function activateNvimBypass(state2) {
|
|
|
819
849
|
function deactivateNvimBypass() {
|
|
820
850
|
setRawBypass(null);
|
|
821
851
|
}
|
|
852
|
+
var COMPOSE_DIR = join2(tmpdir(), "sisyphus-nvim");
|
|
853
|
+
function enterComposeMode(state2, action, actions) {
|
|
854
|
+
if (!state2.nvimEnabled || !state2.nvimBridge?.ready) return false;
|
|
855
|
+
mkdirSync(COMPOSE_DIR, { recursive: true });
|
|
856
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
857
|
+
const tempFile = join2(COMPOSE_DIR, `compose-${id}.md`);
|
|
858
|
+
const signalFile = join2(COMPOSE_DIR, `compose-signal-${id}`);
|
|
859
|
+
writeFileSync(tempFile, "", "utf-8");
|
|
860
|
+
state2.composePrevNvimFile = state2.prevNvimFile;
|
|
861
|
+
state2.composeAction = action;
|
|
862
|
+
state2.composeTempFile = tempFile;
|
|
863
|
+
state2.composeSignalFile = signalFile;
|
|
864
|
+
state2.mode = "compose";
|
|
865
|
+
state2.focusPane = "detail";
|
|
866
|
+
state2.nvimBridge.openComposeFile(tempFile, signalFile);
|
|
867
|
+
activateNvimBypass(state2);
|
|
868
|
+
state2.composePollTimer = setInterval(() => {
|
|
869
|
+
checkComposeSignal(state2, actions);
|
|
870
|
+
}, 100);
|
|
871
|
+
requestRender();
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
function cancelCompose(state2) {
|
|
875
|
+
if (state2.composePollTimer !== null) {
|
|
876
|
+
clearInterval(state2.composePollTimer);
|
|
877
|
+
state2.composePollTimer = null;
|
|
878
|
+
}
|
|
879
|
+
if (state2.composeTempFile) {
|
|
880
|
+
try {
|
|
881
|
+
unlinkSync(state2.composeTempFile);
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (state2.composeSignalFile) {
|
|
886
|
+
try {
|
|
887
|
+
unlinkSync(state2.composeSignalFile);
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
state2.prevNvimFile = null;
|
|
892
|
+
state2.composePrevNvimFile = null;
|
|
893
|
+
state2.composeAction = null;
|
|
894
|
+
state2.composeTempFile = null;
|
|
895
|
+
state2.composeSignalFile = null;
|
|
896
|
+
state2.mode = "navigate";
|
|
897
|
+
state2.focusPane = "tree";
|
|
898
|
+
deactivateNvimBypass();
|
|
899
|
+
requestRender();
|
|
900
|
+
}
|
|
901
|
+
function checkComposeSignal(state2, actions) {
|
|
902
|
+
if (!state2.composeSignalFile || !state2.composeAction) return;
|
|
903
|
+
if (!state2.nvimBridge?.ready) {
|
|
904
|
+
cancelCompose(state2);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (!existsSync(state2.composeSignalFile)) return;
|
|
908
|
+
let signalContent = "";
|
|
909
|
+
try {
|
|
910
|
+
signalContent = readFileSync(state2.composeSignalFile, "utf-8").trim();
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
if (signalContent === "cancel") {
|
|
914
|
+
cancelCompose(state2);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
let content = "";
|
|
918
|
+
if (state2.composeTempFile) {
|
|
919
|
+
try {
|
|
920
|
+
content = readFileSync(state2.composeTempFile, "utf-8").trim();
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const action = state2.composeAction;
|
|
925
|
+
const required = !OPTIONAL_COMPOSE.has(action.kind);
|
|
926
|
+
if (required && !content) {
|
|
927
|
+
try {
|
|
928
|
+
unlinkSync(state2.composeSignalFile);
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
notify(state2, "Content required");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
dispatchComposeAction(action, content, state2, actions);
|
|
935
|
+
cancelCompose(state2);
|
|
936
|
+
}
|
|
937
|
+
function dispatchComposeAction(action, content, state2, actions) {
|
|
938
|
+
switch (action.kind) {
|
|
939
|
+
case "new-session":
|
|
940
|
+
actions.sendAndNotify(
|
|
941
|
+
{ type: "start", task: content, cwd: state2.cwd },
|
|
942
|
+
"Session created"
|
|
943
|
+
);
|
|
944
|
+
break;
|
|
945
|
+
case "message-orchestrator":
|
|
946
|
+
actions.sendAndNotify(
|
|
947
|
+
{ type: "message", sessionId: action.sessionId, content },
|
|
948
|
+
"Message queued"
|
|
949
|
+
);
|
|
950
|
+
break;
|
|
951
|
+
case "resume":
|
|
952
|
+
actions.sendAndNotify(
|
|
953
|
+
{ type: "resume", sessionId: action.sessionId, cwd: state2.cwd, message: content || void 0 },
|
|
954
|
+
"Session resumed"
|
|
955
|
+
);
|
|
956
|
+
break;
|
|
957
|
+
case "continue":
|
|
958
|
+
void (async () => {
|
|
959
|
+
try {
|
|
960
|
+
const contRes = await actions.send({ type: "continue", sessionId: action.sessionId });
|
|
961
|
+
if (!contRes.ok) {
|
|
962
|
+
notify(state2, `Error: ${contRes.error}`);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
actions.sendAndNotify(
|
|
966
|
+
{ type: "resume", sessionId: action.sessionId, cwd: state2.cwd, message: content || void 0 },
|
|
967
|
+
"Session continued"
|
|
968
|
+
);
|
|
969
|
+
} catch (err) {
|
|
970
|
+
notify(state2, `Error: ${err.message}`);
|
|
971
|
+
}
|
|
972
|
+
})();
|
|
973
|
+
break;
|
|
974
|
+
case "spawn-agent":
|
|
975
|
+
actions.sendAndNotify(
|
|
976
|
+
{
|
|
977
|
+
type: "spawn",
|
|
978
|
+
sessionId: action.sessionId,
|
|
979
|
+
agentType: "default",
|
|
980
|
+
name: "agent",
|
|
981
|
+
instruction: content
|
|
982
|
+
},
|
|
983
|
+
"Agent spawned"
|
|
984
|
+
);
|
|
985
|
+
break;
|
|
986
|
+
case "message-agent":
|
|
987
|
+
actions.sendAndNotify(
|
|
988
|
+
{ type: "message", sessionId: action.sessionId, content, source: { type: "agent", agentId: action.agentId } },
|
|
989
|
+
`Message sent to ${action.agentId}`
|
|
990
|
+
);
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
822
994
|
function handleCancel(state2) {
|
|
823
995
|
state2.mode = "navigate";
|
|
824
996
|
state2.targetAgentId = null;
|
|
@@ -1128,6 +1300,7 @@ function handleLeaderAction(action, state2, actions) {
|
|
|
1128
1300
|
notify(state2, "No session selected");
|
|
1129
1301
|
break;
|
|
1130
1302
|
}
|
|
1303
|
+
if (enterComposeMode(state2, { kind: "spawn-agent", sessionId: selectedSessionId }, actions)) return;
|
|
1131
1304
|
state2.mode = "spawn-agent";
|
|
1132
1305
|
requestRender();
|
|
1133
1306
|
return;
|
|
@@ -1138,6 +1311,7 @@ function handleLeaderAction(action, state2, actions) {
|
|
|
1138
1311
|
notify(state2, "Cursor must be on an agent");
|
|
1139
1312
|
break;
|
|
1140
1313
|
}
|
|
1314
|
+
if (enterComposeMode(state2, { kind: "message-agent", sessionId: selectedSessionId, agentId: agent.id }, actions)) return;
|
|
1141
1315
|
state2.targetAgentId = agent.id;
|
|
1142
1316
|
state2.mode = "message-agent";
|
|
1143
1317
|
requestRender();
|
|
@@ -1408,6 +1582,7 @@ function handleNavigateKey(input, key, state2, actions) {
|
|
|
1408
1582
|
notify(state2, "No session selected");
|
|
1409
1583
|
return;
|
|
1410
1584
|
}
|
|
1585
|
+
if (enterComposeMode(state2, { kind: "message-orchestrator", sessionId: state2.selectedSessionId }, actions)) return;
|
|
1411
1586
|
const editor = actions.resolveEditor();
|
|
1412
1587
|
try {
|
|
1413
1588
|
const content = actions.editInPopup(state2.cwd, editor);
|
|
@@ -1427,6 +1602,22 @@ function handleNavigateKey(input, key, state2, actions) {
|
|
|
1427
1602
|
notify(state2, "No session selected");
|
|
1428
1603
|
return;
|
|
1429
1604
|
}
|
|
1605
|
+
if (session.status === "completed") {
|
|
1606
|
+
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
1607
|
+
const claudeSessionId = lastCycle?.claudeSessionId;
|
|
1608
|
+
if (!claudeSessionId) {
|
|
1609
|
+
notify(state2, "No orchestrator Claude session ID available");
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
try {
|
|
1613
|
+
const label = session.name ?? state2.selectedSessionId.slice(0, 8);
|
|
1614
|
+
const sessionName = actions.openClaudeResumeSession(state2.cwd, claudeSessionId, label);
|
|
1615
|
+
actions.switchToSession(sessionName);
|
|
1616
|
+
} catch {
|
|
1617
|
+
notify(state2, "Failed to open Claude session");
|
|
1618
|
+
}
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1430
1621
|
if (state2.paneAlive && session.tmuxWindowId) {
|
|
1431
1622
|
if (session.tmuxSessionName) actions.switchToSession(session.tmuxSessionName);
|
|
1432
1623
|
actions.selectWindow(session.tmuxWindowId);
|
|
@@ -1487,6 +1678,7 @@ function handleNavigateKey(input, key, state2, actions) {
|
|
|
1487
1678
|
return;
|
|
1488
1679
|
}
|
|
1489
1680
|
if (input === "n") {
|
|
1681
|
+
if (enterComposeMode(state2, { kind: "new-session" }, actions)) return;
|
|
1490
1682
|
const editor = actions.resolveEditor();
|
|
1491
1683
|
try {
|
|
1492
1684
|
const content = actions.editInPopup(state2.cwd, editor);
|
|
@@ -1553,6 +1745,7 @@ function handleNavigateKey(input, key, state2, actions) {
|
|
|
1553
1745
|
notify(state2, "Session already active");
|
|
1554
1746
|
return;
|
|
1555
1747
|
}
|
|
1748
|
+
if (enterComposeMode(state2, { kind: "resume", sessionId: state2.selectedSessionId }, actions)) return;
|
|
1556
1749
|
state2.mode = "resume";
|
|
1557
1750
|
state2.inputText = "";
|
|
1558
1751
|
state2.inputCursorPos = 0;
|
|
@@ -1568,6 +1761,7 @@ function handleNavigateKey(input, key, state2, actions) {
|
|
|
1568
1761
|
notify(state2, "Session not completed");
|
|
1569
1762
|
return;
|
|
1570
1763
|
}
|
|
1764
|
+
if (enterComposeMode(state2, { kind: "continue", sessionId: state2.selectedSessionId }, actions)) return;
|
|
1571
1765
|
state2.mode = "continue";
|
|
1572
1766
|
state2.inputText = "";
|
|
1573
1767
|
state2.inputCursorPos = 0;
|
|
@@ -1638,6 +1832,7 @@ function handleNavigateKey(input, key, state2, actions) {
|
|
|
1638
1832
|
}
|
|
1639
1833
|
}
|
|
1640
1834
|
function handleKeypress(input, key, state2, actions) {
|
|
1835
|
+
if (state2.mode === "compose") return;
|
|
1641
1836
|
if (INPUT_MODES.has(state2.mode)) {
|
|
1642
1837
|
handleInputBarKey(input, key, state2, actions);
|
|
1643
1838
|
} else if (state2.mode === "leader" || state2.mode === "copy-menu" || state2.mode === "help") {
|
|
@@ -2025,9 +2220,9 @@ function send(request) {
|
|
|
2025
2220
|
|
|
2026
2221
|
// src/tui/lib/tmux.ts
|
|
2027
2222
|
import { execSync } from "child_process";
|
|
2028
|
-
import { join as
|
|
2029
|
-
import { readFileSync, writeFileSync, mkdtempSync, rmSync, cpSync, existsSync, mkdirSync } from "fs";
|
|
2030
|
-
import { tmpdir } from "os";
|
|
2223
|
+
import { join as join3 } from "path";
|
|
2224
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdtempSync, rmSync, cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
2225
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2031
2226
|
function selectWindow(windowId) {
|
|
2032
2227
|
execSafe(`tmux select-window -t "${windowId}"`);
|
|
2033
2228
|
}
|
|
@@ -2044,9 +2239,9 @@ function listAllWindowIds() {
|
|
|
2044
2239
|
}
|
|
2045
2240
|
var companionPaneId = null;
|
|
2046
2241
|
function setupCompanionPlugin() {
|
|
2047
|
-
const srcDir =
|
|
2048
|
-
const destDir =
|
|
2049
|
-
if (!
|
|
2242
|
+
const srcDir = join3(import.meta.dirname, "templates", "companion-plugin");
|
|
2243
|
+
const destDir = join3(globalDir(), "companion-plugin");
|
|
2244
|
+
if (!existsSync2(destDir)) mkdirSync2(destDir, { recursive: true });
|
|
2050
2245
|
cpSync(srcDir, destDir, { recursive: true });
|
|
2051
2246
|
return destDir;
|
|
2052
2247
|
}
|
|
@@ -2059,18 +2254,18 @@ function openCompanionPane(cwd2) {
|
|
|
2059
2254
|
return;
|
|
2060
2255
|
}
|
|
2061
2256
|
const pluginDir = setupCompanionPlugin();
|
|
2062
|
-
const templatePath =
|
|
2257
|
+
const templatePath = join3(import.meta.dirname, "templates", "dashboard-claude.md");
|
|
2063
2258
|
let template;
|
|
2064
2259
|
try {
|
|
2065
|
-
template =
|
|
2260
|
+
template = readFileSync2(templatePath, "utf-8");
|
|
2066
2261
|
} catch {
|
|
2067
2262
|
template = `You are a Sisyphus dashboard companion. Help the user manage multi-agent sessions.
|
|
2068
2263
|
Project: ${cwd2}
|
|
2069
2264
|
Run \`sisyphus list\` and \`sisyphus status\` to see current state.`;
|
|
2070
2265
|
}
|
|
2071
2266
|
const rendered = template.replace(/\{\{CWD\}\}/g, cwd2);
|
|
2072
|
-
const promptPath =
|
|
2073
|
-
|
|
2267
|
+
const promptPath = join3(globalDir(), "dashboard-companion-prompt.md");
|
|
2268
|
+
writeFileSync2(promptPath, rendered, "utf-8");
|
|
2074
2269
|
const pathEnv = augmentedPath();
|
|
2075
2270
|
const claudeCmd = `SISYPHUS_COMPANION_CWD=${shellQuote(cwd2)} PATH=${shellQuote(pathEnv)} claude --dangerously-skip-permissions --plugin-dir ${shellQuote(pluginDir)} --append-system-prompt "$(cat ${shellQuote(promptPath)})"`;
|
|
2076
2271
|
const result = exec(
|
|
@@ -2083,12 +2278,12 @@ function switchToSession(sessionName) {
|
|
|
2083
2278
|
execSafe(`tmux switch-client -t "${sessionName}"`);
|
|
2084
2279
|
}
|
|
2085
2280
|
function editInPopup(cwd2, editor, opts) {
|
|
2086
|
-
const tmpDir = mkdtempSync(
|
|
2087
|
-
const filePath =
|
|
2281
|
+
const tmpDir = mkdtempSync(join3(tmpdir2(), "sisyphus-"));
|
|
2282
|
+
const filePath = join3(tmpDir, "input.md");
|
|
2088
2283
|
try {
|
|
2089
|
-
|
|
2284
|
+
writeFileSync2(filePath, opts?.content ? opts.content : "", "utf-8");
|
|
2090
2285
|
openEditorPopup(cwd2, editor, filePath, opts?.size);
|
|
2091
|
-
const result =
|
|
2286
|
+
const result = readFileSync2(filePath, "utf-8").trim();
|
|
2092
2287
|
return result || null;
|
|
2093
2288
|
} finally {
|
|
2094
2289
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -2117,6 +2312,19 @@ function openClaudeResumePopup(cwd2, claudeSessionId) {
|
|
|
2117
2312
|
{ stdio: "inherit", env: EXEC_ENV }
|
|
2118
2313
|
);
|
|
2119
2314
|
}
|
|
2315
|
+
function openClaudeResumeSession(cwd2, claudeSessionId, sessionLabel) {
|
|
2316
|
+
const pathEnv = augmentedPath();
|
|
2317
|
+
const cmd = `PATH=${shellQuote(pathEnv)} claude --resume ${shellQuote(claudeSessionId)}`;
|
|
2318
|
+
const sessionName = `sisyphus-${sessionLabel}-resume`;
|
|
2319
|
+
exec(`tmux new-session -d -s ${shellQuote(sessionName)} -c ${shellQuote(cwd2)} ${shellQuote(cmd)}`);
|
|
2320
|
+
execSafe(`tmux set-option -t ${shellQuote(sessionName)} @sisyphus_cwd ${shellQuote(cwd2.replace(/\/+$/, ""))}`);
|
|
2321
|
+
const paneTarget = `${sessionName}:`;
|
|
2322
|
+
execSafe(`tmux set -w -t ${shellQuote(paneTarget)} pane-border-status top`);
|
|
2323
|
+
execSafe(`tmux set -w -t ${shellQuote(paneTarget)} allow-rename off`);
|
|
2324
|
+
execSafe(`tmux set -w -t ${shellQuote(paneTarget)} automatic-rename off`);
|
|
2325
|
+
execSafe(`tmux select-pane -t ${shellQuote(paneTarget)} -T ${shellQuote(`ssph:resume ${sessionLabel}`)}`);
|
|
2326
|
+
return sessionName;
|
|
2327
|
+
}
|
|
2120
2328
|
function openEditorPopup(cwd2, editor, filePath, size) {
|
|
2121
2329
|
const { w = "90%", h = "90%" } = size ?? {};
|
|
2122
2330
|
const editorBin = editor.split(/\s+/)[0].split("/").pop();
|
|
@@ -2961,7 +3169,7 @@ function renderLogsRows(rect, state2) {
|
|
|
2961
3169
|
}
|
|
2962
3170
|
|
|
2963
3171
|
// src/tui/panels/nvim-detail.ts
|
|
2964
|
-
function renderNvimDetailRows(rect, bridge, focused, editable, statusRows) {
|
|
3172
|
+
function renderNvimDetailRows(rect, bridge, focused, editable, statusRows, composing = false) {
|
|
2965
3173
|
const { w, h } = rect;
|
|
2966
3174
|
const rows = new Array(h);
|
|
2967
3175
|
const borderColor = focused ? "cyan" : "gray";
|
|
@@ -2969,7 +3177,7 @@ function renderNvimDetailRows(rect, bridge, focused, editable, statusRows) {
|
|
|
2969
3177
|
const reset = "\x1B[0m";
|
|
2970
3178
|
const innerW = w - 4;
|
|
2971
3179
|
if (focused) {
|
|
2972
|
-
const badgeText = editable ? " EDIT " : " NVIM ";
|
|
3180
|
+
const badgeText = composing ? " COMPOSE " : editable ? " EDIT " : " NVIM ";
|
|
2973
3181
|
const badgeLen = badgeText.length;
|
|
2974
3182
|
const dashesLeft = 2;
|
|
2975
3183
|
const dashesRight = Math.max(0, w - 2 - dashesLeft - badgeLen);
|
|
@@ -3019,6 +3227,13 @@ function renderNotificationRow(buf, y, notification, error) {
|
|
|
3019
3227
|
}
|
|
3020
3228
|
function renderInputBar(buf, y, state2) {
|
|
3021
3229
|
const { mode, inputText, inputCursorPos } = state2;
|
|
3230
|
+
if (mode === "compose") {
|
|
3231
|
+
const action = state2.composeAction;
|
|
3232
|
+
const label = action ? COMPOSE_HEADERS[action.kind] : "Compose";
|
|
3233
|
+
const content2 = `\x1B[1;33mCOMPOSE\x1B[0m\x1B[2m ${label} \xB7 :w to submit \xB7 Tab to cancel\x1B[0m`;
|
|
3234
|
+
writeClipped(buf, 1, y, content2, buf.width - 2);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3022
3237
|
if (mode === "navigate") {
|
|
3023
3238
|
const content2 = `\x1B[2mPress [m] to message orchestrator, [n] for new session\x1B[0m`;
|
|
3024
3239
|
writeClipped(buf, 1, y, content2, buf.width - 2);
|
|
@@ -3040,6 +3255,7 @@ var SEP = D("\u2502 ");
|
|
|
3040
3255
|
function renderStatusLine(buf, y, state2, cursorNodeType) {
|
|
3041
3256
|
const { mode, focusPane } = state2;
|
|
3042
3257
|
if (mode === "report-detail") return;
|
|
3258
|
+
if (mode === "compose") return;
|
|
3043
3259
|
let content;
|
|
3044
3260
|
if (mode === "leader") {
|
|
3045
3261
|
content = `\x1B[1;35mLEADER\x1B[0m` + D(" press a command key or [esc] to cancel");
|
|
@@ -3169,9 +3385,17 @@ function renderHelpOverlay(buf, rows, cols) {
|
|
|
3169
3385
|
|
|
3170
3386
|
// src/tui/lib/nvim-bridge.ts
|
|
3171
3387
|
import { execSync as execSync3 } from "child_process";
|
|
3172
|
-
import { writeFileSync as
|
|
3173
|
-
import { join as
|
|
3174
|
-
import { tmpdir as
|
|
3388
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2, readFileSync as readFileSync3, statSync, existsSync as existsSync3 } from "fs";
|
|
3389
|
+
import { join as join4 } from "path";
|
|
3390
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
3391
|
+
function simpleHash(str) {
|
|
3392
|
+
let hash = 0;
|
|
3393
|
+
for (let i = 0; i < str.length; i++) {
|
|
3394
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
3395
|
+
hash |= 0;
|
|
3396
|
+
}
|
|
3397
|
+
return Math.abs(hash).toString(36);
|
|
3398
|
+
}
|
|
3175
3399
|
var NvimBridge = class {
|
|
3176
3400
|
pty = null;
|
|
3177
3401
|
xterm = null;
|
|
@@ -3183,20 +3407,28 @@ var NvimBridge = class {
|
|
|
3183
3407
|
ready = false;
|
|
3184
3408
|
dirty = true;
|
|
3185
3409
|
available = false;
|
|
3410
|
+
respawning = false;
|
|
3411
|
+
/** Set true once nvim has been ready at least once — prevents respawn during initial startup */
|
|
3412
|
+
wasReady = false;
|
|
3186
3413
|
/** DECSCUSR cursor style: 0=default, 1=blinking block, 2=steady block, 3=blinking underline, 4=steady underline, 5=blinking bar, 6=steady bar */
|
|
3187
3414
|
cursorStyle = 0;
|
|
3188
3415
|
cachedRows = null;
|
|
3189
3416
|
nvimPath = "nvim";
|
|
3190
3417
|
pendingFiles = null;
|
|
3191
3418
|
fileDebounceTimer = null;
|
|
3419
|
+
cmdDir;
|
|
3192
3420
|
cmdFile;
|
|
3421
|
+
/** Tracked editable files: path → { basePath (snapshot), mtimeMs } */
|
|
3422
|
+
editableFiles = /* @__PURE__ */ new Map();
|
|
3423
|
+
mergeStatusFile;
|
|
3193
3424
|
constructor(cols, rows, onRender) {
|
|
3194
3425
|
this._cols = cols;
|
|
3195
3426
|
this._rows = rows;
|
|
3196
3427
|
this.onRender = onRender;
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
this.cmdFile =
|
|
3428
|
+
this.cmdDir = join4(tmpdir3(), "sisyphus-nvim");
|
|
3429
|
+
mkdirSync3(this.cmdDir, { recursive: true });
|
|
3430
|
+
this.cmdFile = join4(this.cmdDir, `cmd-${process.pid}.lua`);
|
|
3431
|
+
this.mergeStatusFile = join4(this.cmdDir, `merge-status-${process.pid}.txt`);
|
|
3200
3432
|
try {
|
|
3201
3433
|
this.nvimPath = execSync3("which nvim", { stdio: "pipe" }).toString().trim();
|
|
3202
3434
|
this.available = true;
|
|
@@ -3209,71 +3441,111 @@ var NvimBridge = class {
|
|
|
3209
3441
|
this.ready = false;
|
|
3210
3442
|
});
|
|
3211
3443
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
[
|
|
3225
|
-
|
|
3226
|
-
"
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
"
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
this.
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
this.ready =
|
|
3444
|
+
spawn() {
|
|
3445
|
+
return Promise.all([
|
|
3446
|
+
import("node-pty"),
|
|
3447
|
+
import("@xterm/headless")
|
|
3448
|
+
]).then(([nodePty, xtermModule]) => new Promise((resolve) => {
|
|
3449
|
+
const { spawn } = nodePty;
|
|
3450
|
+
const { Terminal } = xtermModule.default;
|
|
3451
|
+
this.xterm = new Terminal({
|
|
3452
|
+
cols: this._cols,
|
|
3453
|
+
rows: this._rows,
|
|
3454
|
+
allowProposedApi: true
|
|
3455
|
+
});
|
|
3456
|
+
const nvimArgs = [
|
|
3457
|
+
// Pre-init: only settings needed before user config loads
|
|
3458
|
+
"--cmd",
|
|
3459
|
+
[
|
|
3460
|
+
"set noswapfile",
|
|
3461
|
+
"set nobackup",
|
|
3462
|
+
"set nowritebackup",
|
|
3463
|
+
"set hidden",
|
|
3464
|
+
"set autoread"
|
|
3465
|
+
].join(" | "),
|
|
3466
|
+
// Post-init: cosmetic overrides applied AFTER user config (LazyVim, etc.)
|
|
3467
|
+
"-c",
|
|
3468
|
+
[
|
|
3469
|
+
"set laststatus=0",
|
|
3470
|
+
"set showtabline=2",
|
|
3471
|
+
"set signcolumn=no",
|
|
3472
|
+
"set nonumber",
|
|
3473
|
+
"set noruler",
|
|
3474
|
+
"set noshowcmd",
|
|
3475
|
+
"set noshowmode",
|
|
3476
|
+
"set shortmess+=F",
|
|
3477
|
+
"set fillchars=eob:\\ ",
|
|
3478
|
+
"set scrolloff=3"
|
|
3479
|
+
].join(" | "),
|
|
3480
|
+
// Suppress LSP — prevent servers from ever starting (avoids exit warnings)
|
|
3481
|
+
"--cmd",
|
|
3482
|
+
"lua vim.lsp.start = function() end",
|
|
3483
|
+
// Poll-based command executor: reads lua from temp file — no command-line flash
|
|
3484
|
+
"-c",
|
|
3485
|
+
`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))`
|
|
3486
|
+
];
|
|
3487
|
+
this.pty = spawn(this.nvimPath, nvimArgs, {
|
|
3488
|
+
name: "xterm-256color",
|
|
3489
|
+
cols: this._cols,
|
|
3490
|
+
rows: this._rows,
|
|
3491
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
3492
|
+
});
|
|
3493
|
+
let settled = false;
|
|
3494
|
+
this.pty.onData((data) => {
|
|
3495
|
+
const csMatch = data.match(/\x1b\[(\d+) q/);
|
|
3496
|
+
if (csMatch) this.cursorStyle = parseInt(csMatch[1], 10);
|
|
3497
|
+
this.xterm.write(data);
|
|
3498
|
+
this.dirty = true;
|
|
3499
|
+
this.cachedRows = null;
|
|
3500
|
+
this.debouncedRender();
|
|
3501
|
+
});
|
|
3502
|
+
this.pty.onExit(() => {
|
|
3503
|
+
this.ready = false;
|
|
3272
3504
|
this.dirty = true;
|
|
3273
3505
|
this.cachedRows = null;
|
|
3274
3506
|
this.onRender();
|
|
3507
|
+
if (!settled) {
|
|
3508
|
+
settled = true;
|
|
3509
|
+
resolve();
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
setTimeout(() => {
|
|
3513
|
+
if (this.pty) {
|
|
3514
|
+
this.ready = true;
|
|
3515
|
+
this.wasReady = true;
|
|
3516
|
+
this.dirty = true;
|
|
3517
|
+
this.cachedRows = null;
|
|
3518
|
+
this.onRender();
|
|
3519
|
+
}
|
|
3520
|
+
if (!settled) {
|
|
3521
|
+
settled = true;
|
|
3522
|
+
resolve();
|
|
3523
|
+
}
|
|
3524
|
+
}, 500);
|
|
3525
|
+
}));
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Respawn nvim after it exited (e.g. user quit during compose).
|
|
3529
|
+
* Cleans up dead instances and re-runs spawn().
|
|
3530
|
+
*/
|
|
3531
|
+
async respawn() {
|
|
3532
|
+
if (!this.available) return;
|
|
3533
|
+
if (this.xterm) {
|
|
3534
|
+
this.xterm.dispose();
|
|
3535
|
+
this.xterm = null;
|
|
3536
|
+
}
|
|
3537
|
+
if (this.pty) {
|
|
3538
|
+
try {
|
|
3539
|
+
this.pty.kill();
|
|
3540
|
+
} catch {
|
|
3275
3541
|
}
|
|
3276
|
-
|
|
3542
|
+
this.pty = null;
|
|
3543
|
+
}
|
|
3544
|
+
this.ready = false;
|
|
3545
|
+
this.dirty = true;
|
|
3546
|
+
this.cachedRows = null;
|
|
3547
|
+
this.currentFile = null;
|
|
3548
|
+
await this.spawn();
|
|
3277
3549
|
}
|
|
3278
3550
|
debouncedRender() {
|
|
3279
3551
|
if (this.renderTimer !== null) return;
|
|
@@ -3287,7 +3559,7 @@ var NvimBridge = class {
|
|
|
3287
3559
|
* Writes lua to a temp file — a libuv timer in nvim polls and executes it.
|
|
3288
3560
|
*/
|
|
3289
3561
|
execLua(lua) {
|
|
3290
|
-
|
|
3562
|
+
writeFileSync3(this.cmdFile, lua);
|
|
3291
3563
|
}
|
|
3292
3564
|
openFile(path, readonly = true) {
|
|
3293
3565
|
if (!this.pty || !this.ready) return;
|
|
@@ -3312,6 +3584,7 @@ var NvimBridge = class {
|
|
|
3312
3584
|
}
|
|
3313
3585
|
executeOpenFiles(files) {
|
|
3314
3586
|
if (!this.pty || !this.ready) return;
|
|
3587
|
+
this.trackEditableFiles(files);
|
|
3315
3588
|
const escapeLua = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3316
3589
|
const stmts = [
|
|
3317
3590
|
"for _, b in ipairs(vim.api.nvim_list_bufs()) do pcall(vim.api.nvim_buf_delete, b, {force=true}) end"
|
|
@@ -3337,6 +3610,37 @@ var NvimBridge = class {
|
|
|
3337
3610
|
this.pty.write(":setlocal noreadonly modifiable\r");
|
|
3338
3611
|
}
|
|
3339
3612
|
}
|
|
3613
|
+
/**
|
|
3614
|
+
* Open a temp file for compose mode: clears buffers, opens writable,
|
|
3615
|
+
* installs BufWritePost autocmd that writes a signal file on :w,
|
|
3616
|
+
* and enters insert mode.
|
|
3617
|
+
*/
|
|
3618
|
+
openComposeFile(tempPath, signalPath) {
|
|
3619
|
+
if (!this.pty || !this.ready) return;
|
|
3620
|
+
if (this.fileDebounceTimer !== null) {
|
|
3621
|
+
clearTimeout(this.fileDebounceTimer);
|
|
3622
|
+
this.fileDebounceTimer = null;
|
|
3623
|
+
this.pendingFiles = null;
|
|
3624
|
+
}
|
|
3625
|
+
const escapeLua = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3626
|
+
const eSig = escapeLua(signalPath);
|
|
3627
|
+
const lua = [
|
|
3628
|
+
// Clear all existing buffers
|
|
3629
|
+
"for _, b in ipairs(vim.api.nvim_list_bufs()) do pcall(vim.api.nvim_buf_delete, b, {force=true}) end",
|
|
3630
|
+
// Open temp file as writable
|
|
3631
|
+
`vim.cmd('edit! ${escapeLua(tempPath)}')`,
|
|
3632
|
+
"vim.bo.readonly = false",
|
|
3633
|
+
"vim.bo.modifiable = true",
|
|
3634
|
+
// Install BufWritePost autocmd — writes submit signal on :w
|
|
3635
|
+
`vim.api.nvim_create_autocmd('BufWritePost', { buffer = 0, callback = function() local f = io.open('${eSig}', 'w'); if f then f:write('1'); f:close() end end })`,
|
|
3636
|
+
// Install QuitPre autocmd — writes cancel signal if no submit signal exists (quit proceeds, nvim may exit)
|
|
3637
|
+
`vim.api.nvim_create_autocmd('QuitPre', { buffer = 0, callback = function() local sf = io.open('${eSig}', 'r'); if sf then sf:close() else local f = io.open('${eSig}', 'w'); if f then f:write('cancel'); f:close() end end end })`,
|
|
3638
|
+
// Enter insert mode
|
|
3639
|
+
"vim.cmd('startinsert')"
|
|
3640
|
+
].join("; ");
|
|
3641
|
+
this.execLua(`(function() ${lua} end)()`);
|
|
3642
|
+
this.currentFile = tempPath;
|
|
3643
|
+
}
|
|
3340
3644
|
closeAllTabs() {
|
|
3341
3645
|
if (!this.pty || !this.ready) return;
|
|
3342
3646
|
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!")');
|
|
@@ -3470,6 +3774,113 @@ var NvimBridge = class {
|
|
|
3470
3774
|
y: this.xterm.buffer.active.cursorY
|
|
3471
3775
|
};
|
|
3472
3776
|
}
|
|
3777
|
+
/**
|
|
3778
|
+
* Snapshot editable files on disk so we have a base for 3-way merge.
|
|
3779
|
+
* Called when files are opened in nvim tabs.
|
|
3780
|
+
*/
|
|
3781
|
+
trackEditableFiles(files) {
|
|
3782
|
+
for (const [, info] of this.editableFiles) {
|
|
3783
|
+
try {
|
|
3784
|
+
unlinkSync2(info.basePath);
|
|
3785
|
+
} catch {
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
this.editableFiles.clear();
|
|
3789
|
+
for (const file of files) {
|
|
3790
|
+
if (file.readonly) continue;
|
|
3791
|
+
try {
|
|
3792
|
+
const content = readFileSync3(file.path, "utf-8");
|
|
3793
|
+
const mtime = statSync(file.path).mtimeMs;
|
|
3794
|
+
const basePath = join4(this.cmdDir, `base-${simpleHash(file.path)}.md`);
|
|
3795
|
+
writeFileSync3(basePath, content, "utf-8");
|
|
3796
|
+
this.editableFiles.set(file.path, { basePath, mtimeMs: mtime });
|
|
3797
|
+
} catch {
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
/**
|
|
3802
|
+
* Check editable files for external changes and 3-way merge if the buffer
|
|
3803
|
+
* is dirty. Falls back to regular checktime for clean/readonly buffers.
|
|
3804
|
+
*
|
|
3805
|
+
* Returns a merge status string from the *previous* cycle ('clean' or 'union')
|
|
3806
|
+
* if a merge completed, or null.
|
|
3807
|
+
*/
|
|
3808
|
+
mergeCheckOrReload() {
|
|
3809
|
+
if (!this.pty || !this.ready) return null;
|
|
3810
|
+
let mergeResult = null;
|
|
3811
|
+
try {
|
|
3812
|
+
if (existsSync3(this.mergeStatusFile)) {
|
|
3813
|
+
const content = readFileSync3(this.mergeStatusFile, "utf-8").trim();
|
|
3814
|
+
unlinkSync2(this.mergeStatusFile);
|
|
3815
|
+
if (content) {
|
|
3816
|
+
const lines = content.split("\n");
|
|
3817
|
+
mergeResult = lines.some((l) => l === "union") ? "union" : "clean";
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
} catch {
|
|
3821
|
+
}
|
|
3822
|
+
if (mergeResult) {
|
|
3823
|
+
for (const [filePath, info] of this.editableFiles) {
|
|
3824
|
+
try {
|
|
3825
|
+
info.mtimeMs = statSync(filePath).mtimeMs;
|
|
3826
|
+
} catch {
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
this.execLua('vim.cmd("checktime")');
|
|
3830
|
+
return mergeResult;
|
|
3831
|
+
}
|
|
3832
|
+
const changedFiles = [];
|
|
3833
|
+
for (const [filePath, info] of this.editableFiles) {
|
|
3834
|
+
try {
|
|
3835
|
+
const currentMtime = statSync(filePath).mtimeMs;
|
|
3836
|
+
if (currentMtime !== info.mtimeMs) {
|
|
3837
|
+
changedFiles.push({ filePath, basePath: info.basePath });
|
|
3838
|
+
info.mtimeMs = currentMtime;
|
|
3839
|
+
}
|
|
3840
|
+
} catch {
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
if (changedFiles.length === 0) {
|
|
3844
|
+
this.execLua('vim.cmd("checktime")');
|
|
3845
|
+
return null;
|
|
3846
|
+
}
|
|
3847
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3848
|
+
const mergeBlocks = changedFiles.map(({ filePath, basePath }) => `
|
|
3849
|
+
do
|
|
3850
|
+
local bufnr = vim.fn.bufnr('${esc(filePath)}')
|
|
3851
|
+
if bufnr ~= -1 and vim.api.nvim_buf_is_loaded(bufnr) then
|
|
3852
|
+
if vim.bo[bufnr].modified then
|
|
3853
|
+
local buf_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
3854
|
+
local buf_content = table.concat(buf_lines, '\\n') .. '\\n'
|
|
3855
|
+
local tmp = '${esc(this.cmdDir)}/merge-current-${process.pid}.md'
|
|
3856
|
+
local f = io.open(tmp, 'w')
|
|
3857
|
+
if f then f:write(buf_content); f:close() end
|
|
3858
|
+
local result = vim.fn.system({'git', 'merge-file', '-p', '--union', tmp, '${esc(basePath)}', '${esc(filePath)}'})
|
|
3859
|
+
local merged_lines = vim.split(result, '\\n', {trimempty = false})
|
|
3860
|
+
if #merged_lines > 0 and merged_lines[#merged_lines] == '' then
|
|
3861
|
+
table.remove(merged_lines)
|
|
3862
|
+
end
|
|
3863
|
+
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, merged_lines)
|
|
3864
|
+
local out = io.open('${esc(filePath)}', 'w')
|
|
3865
|
+
if out then out:write(result); out:close() end
|
|
3866
|
+
vim.bo[bufnr].modified = false
|
|
3867
|
+
local bf = io.open('${esc(basePath)}', 'w')
|
|
3868
|
+
if bf then bf:write(result); bf:close() end
|
|
3869
|
+
local sf = io.open('${esc(this.mergeStatusFile)}', 'a')
|
|
3870
|
+
if sf then sf:write(vim.v.shell_error == 0 and 'clean' or 'union'); sf:write('\\n'); sf:close() end
|
|
3871
|
+
else
|
|
3872
|
+
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
3873
|
+
local bf = io.open('${esc(basePath)}', 'w')
|
|
3874
|
+
if bf then bf:write(table.concat(lines, '\\n') .. '\\n'); bf:close() end
|
|
3875
|
+
end
|
|
3876
|
+
end
|
|
3877
|
+
end`).join("\n");
|
|
3878
|
+
this.execLua(`(function()
|
|
3879
|
+
pcall(function() vim.cmd('checktime') end)
|
|
3880
|
+
${mergeBlocks}
|
|
3881
|
+
end)()`);
|
|
3882
|
+
return null;
|
|
3883
|
+
}
|
|
3473
3884
|
checktime() {
|
|
3474
3885
|
if (this.pty && this.ready) {
|
|
3475
3886
|
this.execLua('vim.cmd("checktime")');
|
|
@@ -3497,23 +3908,34 @@ var NvimBridge = class {
|
|
|
3497
3908
|
}
|
|
3498
3909
|
this.ready = false;
|
|
3499
3910
|
try {
|
|
3500
|
-
|
|
3911
|
+
unlinkSync2(this.cmdFile);
|
|
3501
3912
|
} catch {
|
|
3502
3913
|
}
|
|
3914
|
+
try {
|
|
3915
|
+
unlinkSync2(this.mergeStatusFile);
|
|
3916
|
+
} catch {
|
|
3917
|
+
}
|
|
3918
|
+
for (const [, info] of this.editableFiles) {
|
|
3919
|
+
try {
|
|
3920
|
+
unlinkSync2(info.basePath);
|
|
3921
|
+
} catch {
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
this.editableFiles.clear();
|
|
3503
3925
|
}
|
|
3504
3926
|
};
|
|
3505
3927
|
|
|
3506
3928
|
// src/tui/lib/overview-writer.ts
|
|
3507
|
-
import { writeFileSync as
|
|
3508
|
-
import { join as
|
|
3929
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, renameSync, existsSync as existsSync4 } from "fs";
|
|
3930
|
+
import { join as join5 } from "path";
|
|
3509
3931
|
function atomicWrite(filePath, content) {
|
|
3510
3932
|
const tmp = filePath + ".tmp." + process.pid;
|
|
3511
|
-
|
|
3933
|
+
writeFileSync4(tmp, content, "utf-8");
|
|
3512
3934
|
renameSync(tmp, filePath);
|
|
3513
3935
|
}
|
|
3514
3936
|
function ensureTuiDir(cwd2, sessionId) {
|
|
3515
3937
|
const dir = tuiScratchDir(cwd2, sessionId);
|
|
3516
|
-
|
|
3938
|
+
mkdirSync4(dir, { recursive: true });
|
|
3517
3939
|
return dir;
|
|
3518
3940
|
}
|
|
3519
3941
|
function formatTimestamp(iso) {
|
|
@@ -3691,11 +4113,11 @@ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
|
|
|
3691
4113
|
if (!session) return null;
|
|
3692
4114
|
const files = [];
|
|
3693
4115
|
const gp = goalPath(cwd2, sessionId);
|
|
3694
|
-
if (
|
|
4116
|
+
if (existsSync4(gp)) files.push({ path: gp, readonly: false });
|
|
3695
4117
|
const rp = roadmapPath(cwd2, sessionId);
|
|
3696
|
-
if (
|
|
4118
|
+
if (existsSync4(rp)) files.push({ path: rp, readonly: false });
|
|
3697
4119
|
const sp = strategyPath(cwd2, sessionId);
|
|
3698
|
-
if (
|
|
4120
|
+
if (existsSync4(sp)) files.push({ path: sp, readonly: false });
|
|
3699
4121
|
if (files.length === 0) return null;
|
|
3700
4122
|
return { files };
|
|
3701
4123
|
}
|
|
@@ -3706,7 +4128,7 @@ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
|
|
|
3706
4128
|
);
|
|
3707
4129
|
if (!cycle) return null;
|
|
3708
4130
|
const dir = ensureTuiDir(cwd2, sessionId);
|
|
3709
|
-
const filePath =
|
|
4131
|
+
const filePath = join5(dir, `cycle-${cursorNode.cycleNumber}.md`);
|
|
3710
4132
|
atomicWrite(filePath, composeCycleDetail(session, cycle));
|
|
3711
4133
|
return { files: [{ path: filePath, readonly: true }] };
|
|
3712
4134
|
}
|
|
@@ -3715,7 +4137,7 @@ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
|
|
|
3715
4137
|
const agent = session.agents.find((a) => a.id === cursorNode.agentId);
|
|
3716
4138
|
if (!agent) return null;
|
|
3717
4139
|
const dir = ensureTuiDir(cwd2, sessionId);
|
|
3718
|
-
const filePath =
|
|
4140
|
+
const filePath = join5(dir, `${agent.id}.md`);
|
|
3719
4141
|
atomicWrite(filePath, composeAgentDetail(agent, state2.cachedReportBlocks.get(agent.id) ?? []));
|
|
3720
4142
|
return { files: [{ path: filePath, readonly: true }] };
|
|
3721
4143
|
}
|
|
@@ -3723,14 +4145,14 @@ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
|
|
|
3723
4145
|
const agent = session?.agents.find((a) => a.id === cursorNode.agentId);
|
|
3724
4146
|
if (agent && agent.reports.length > 0) {
|
|
3725
4147
|
const report = agent.reports[cursorNode.reportIndex];
|
|
3726
|
-
if (report?.filePath &&
|
|
4148
|
+
if (report?.filePath && existsSync4(report.filePath)) {
|
|
3727
4149
|
return { files: [{ path: report.filePath, readonly: true }] };
|
|
3728
4150
|
}
|
|
3729
4151
|
}
|
|
3730
4152
|
return null;
|
|
3731
4153
|
}
|
|
3732
4154
|
case "context-file": {
|
|
3733
|
-
if (cursorNode.filePath &&
|
|
4155
|
+
if (cursorNode.filePath && existsSync4(cursorNode.filePath)) {
|
|
3734
4156
|
return { files: [{ path: cursorNode.filePath, readonly: false }] };
|
|
3735
4157
|
}
|
|
3736
4158
|
return null;
|
|
@@ -3739,7 +4161,7 @@ function resolveNvimFile(state2, cursorNode, detailCtx, cwd2) {
|
|
|
3739
4161
|
case "message": {
|
|
3740
4162
|
if (!session || session.messages.length === 0) return null;
|
|
3741
4163
|
const dir = ensureTuiDir(cwd2, sessionId);
|
|
3742
|
-
const filePath =
|
|
4164
|
+
const filePath = join5(dir, "messages.md");
|
|
3743
4165
|
atomicWrite(filePath, composeMessages(session));
|
|
3744
4166
|
return { files: [{ path: filePath, readonly: true }] };
|
|
3745
4167
|
}
|
|
@@ -3877,28 +4299,28 @@ function startApp(state2, cleanup2) {
|
|
|
3877
4299
|
}
|
|
3878
4300
|
try {
|
|
3879
4301
|
const pp = roadmapPath(state2.cwd, state2.selectedSessionId);
|
|
3880
|
-
if (
|
|
3881
|
-
planContent =
|
|
4302
|
+
if (existsSync5(pp)) {
|
|
4303
|
+
planContent = readFileSync4(pp, "utf-8");
|
|
3882
4304
|
}
|
|
3883
4305
|
} catch {
|
|
3884
4306
|
}
|
|
3885
4307
|
try {
|
|
3886
4308
|
const gp = goalPath(state2.cwd, state2.selectedSessionId);
|
|
3887
|
-
if (
|
|
3888
|
-
goalContent =
|
|
4309
|
+
if (existsSync5(gp)) {
|
|
4310
|
+
goalContent = readFileSync4(gp, "utf-8");
|
|
3889
4311
|
}
|
|
3890
4312
|
} catch {
|
|
3891
4313
|
}
|
|
3892
4314
|
try {
|
|
3893
4315
|
const sp = strategyPath(state2.cwd, state2.selectedSessionId);
|
|
3894
|
-
if (
|
|
3895
|
-
strategyContent =
|
|
4316
|
+
if (existsSync5(sp)) {
|
|
4317
|
+
strategyContent = readFileSync4(sp, "utf-8");
|
|
3896
4318
|
}
|
|
3897
4319
|
} catch {
|
|
3898
4320
|
}
|
|
3899
4321
|
try {
|
|
3900
4322
|
const ld = logsDir(state2.cwd, state2.selectedSessionId);
|
|
3901
|
-
if (
|
|
4323
|
+
if (existsSync5(ld)) {
|
|
3902
4324
|
if (state2.selectedSessionId !== cachedLogSessionId) {
|
|
3903
4325
|
cachedLogFiles = /* @__PURE__ */ new Map();
|
|
3904
4326
|
cachedLogSessionId = state2.selectedSessionId;
|
|
@@ -3909,13 +4331,13 @@ function startApp(state2, cleanup2) {
|
|
|
3909
4331
|
if (!fileSet.has(key)) cachedLogFiles.delete(key);
|
|
3910
4332
|
}
|
|
3911
4333
|
for (const f of files) {
|
|
3912
|
-
const filePath =
|
|
3913
|
-
const mtime =
|
|
4334
|
+
const filePath = join6(ld, f);
|
|
4335
|
+
const mtime = statSync2(filePath).mtimeMs;
|
|
3914
4336
|
const cached = cachedLogFiles.get(f);
|
|
3915
4337
|
if (!cached || cached.mtime !== mtime) {
|
|
3916
4338
|
const match = f.match(/cycle-(\d+)\.md$/);
|
|
3917
4339
|
const cycle = match ? parseInt(match[1], 10) : 0;
|
|
3918
|
-
const content =
|
|
4340
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
3919
4341
|
cachedLogFiles.set(f, { mtime, cycle, content });
|
|
3920
4342
|
}
|
|
3921
4343
|
}
|
|
@@ -3929,7 +4351,7 @@ function startApp(state2, cleanup2) {
|
|
|
3929
4351
|
}
|
|
3930
4352
|
try {
|
|
3931
4353
|
const cd = contextDir(state2.cwd, state2.selectedSessionId);
|
|
3932
|
-
if (
|
|
4354
|
+
if (existsSync5(cd)) {
|
|
3933
4355
|
contextFiles = readdirSync(cd).filter((f) => !f.startsWith(".")).sort();
|
|
3934
4356
|
}
|
|
3935
4357
|
} catch {
|
|
@@ -3952,7 +4374,12 @@ function startApp(state2, cleanup2) {
|
|
|
3952
4374
|
state2.contextFiles = contextFiles;
|
|
3953
4375
|
state2.error = null;
|
|
3954
4376
|
if (state2.nvimEnabled && state2.nvimBridge?.ready && state2.prevNvimFile) {
|
|
3955
|
-
state2.nvimBridge.
|
|
4377
|
+
const mergeStatus = state2.nvimBridge.mergeCheckOrReload();
|
|
4378
|
+
if (mergeStatus === "clean") {
|
|
4379
|
+
notify(state2, "Auto-merged external changes");
|
|
4380
|
+
} else if (mergeStatus === "union") {
|
|
4381
|
+
notify(state2, "Auto-merged overlapping edits \u2014 review buffer");
|
|
4382
|
+
}
|
|
3956
4383
|
}
|
|
3957
4384
|
requestRender();
|
|
3958
4385
|
} catch (err) {
|
|
@@ -4038,8 +4465,8 @@ function startApp(state2, cleanup2) {
|
|
|
4038
4465
|
if (cursorNode.filePath !== cachedContextFilePath) {
|
|
4039
4466
|
cachedContextFilePath = cursorNode.filePath;
|
|
4040
4467
|
try {
|
|
4041
|
-
if (
|
|
4042
|
-
cachedContextFileContent =
|
|
4468
|
+
if (existsSync5(cursorNode.filePath)) {
|
|
4469
|
+
cachedContextFileContent = readFileSync4(cursorNode.filePath, "utf-8");
|
|
4043
4470
|
} else {
|
|
4044
4471
|
cachedContextFileContent = null;
|
|
4045
4472
|
}
|
|
@@ -4092,19 +4519,42 @@ function startApp(state2, cleanup2) {
|
|
|
4092
4519
|
contextFileContent
|
|
4093
4520
|
};
|
|
4094
4521
|
let detailRows;
|
|
4522
|
+
const composing = state2.mode === "compose";
|
|
4523
|
+
if (state2.nvimEnabled && state2.nvimBridge && state2.nvimBridge.wasReady && !state2.nvimBridge.ready && !state2.nvimBridge.respawning) {
|
|
4524
|
+
state2.nvimBridge.respawning = true;
|
|
4525
|
+
state2.prevNvimFile = null;
|
|
4526
|
+
state2.nvimBridge.respawn().then(() => {
|
|
4527
|
+
state2.nvimBridge.respawning = false;
|
|
4528
|
+
requestRender();
|
|
4529
|
+
}).catch(() => {
|
|
4530
|
+
state2.nvimBridge.respawning = false;
|
|
4531
|
+
state2.nvimEnabled = false;
|
|
4532
|
+
requestRender();
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4095
4535
|
if (state2.nvimEnabled && state2.nvimBridge?.ready) {
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
state2.
|
|
4104
|
-
|
|
4536
|
+
if (composing) {
|
|
4537
|
+
const action = state2.composeAction;
|
|
4538
|
+
const label = action ? COMPOSE_HEADERS[action.kind] : "Compose";
|
|
4539
|
+
const statusRows = [
|
|
4540
|
+
" " + ansiColor(label, "yellow", true),
|
|
4541
|
+
" " + ansiDim(":w to submit \xB7 Tab to cancel")
|
|
4542
|
+
];
|
|
4543
|
+
detailRows = renderNvimDetailRows(detailRect, state2.nvimBridge, true, false, statusRows, true);
|
|
4544
|
+
} else {
|
|
4545
|
+
const result = resolveNvimFile(state2, cursorNode, detailCtx, state2.cwd);
|
|
4546
|
+
const resultKey = result ? result.files.map((f) => f.path).join("|") : null;
|
|
4547
|
+
if (resultKey && resultKey !== state2.prevNvimFile) {
|
|
4548
|
+
state2.nvimBridge.openTabFiles(result.files);
|
|
4549
|
+
state2.prevNvimFile = resultKey;
|
|
4550
|
+
state2.nvimEditable = result.files.some((f) => !f.readonly);
|
|
4551
|
+
} else if (!resultKey) {
|
|
4552
|
+
state2.prevNvimFile = null;
|
|
4553
|
+
state2.nvimEditable = false;
|
|
4554
|
+
}
|
|
4555
|
+
const statusRows = buildStatusRows(cursorNode, state2.selectedSession, state2);
|
|
4556
|
+
detailRows = renderNvimDetailRows(detailRect, state2.nvimBridge, state2.focusPane === "detail", state2.nvimEditable, statusRows);
|
|
4105
4557
|
}
|
|
4106
|
-
const statusRows = buildStatusRows(cursorNode, state2.selectedSession, state2);
|
|
4107
|
-
detailRows = renderNvimDetailRows(detailRect, state2.nvimBridge, state2.focusPane === "detail", state2.nvimEditable, statusRows);
|
|
4108
4558
|
} else {
|
|
4109
4559
|
detailRows = renderDetailRows(detailRect, state2, detailCtx);
|
|
4110
4560
|
}
|
|
@@ -4165,6 +4615,7 @@ function startApp(state2, cleanup2) {
|
|
|
4165
4615
|
editInPopup,
|
|
4166
4616
|
openCompanionPane,
|
|
4167
4617
|
openClaudeResumePopup,
|
|
4618
|
+
openClaudeResumeSession,
|
|
4168
4619
|
selectWindow,
|
|
4169
4620
|
selectPane,
|
|
4170
4621
|
switchToSession,
|
|
@@ -4206,6 +4657,7 @@ function startApp(state2, cleanup2) {
|
|
|
4206
4657
|
inputActions.cleanup = () => {
|
|
4207
4658
|
clearInterval(pollInterval);
|
|
4208
4659
|
if (debouncedPollTimer !== null) clearTimeout(debouncedPollTimer);
|
|
4660
|
+
if (state2.composePollTimer !== null) clearInterval(state2.composePollTimer);
|
|
4209
4661
|
stopKeypress();
|
|
4210
4662
|
stopResize();
|
|
4211
4663
|
state2.detailScroll.destroy();
|