agileflow 4.0.0-alpha.10 → 4.0.0-alpha.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/package.json +1 -1
- package/src/cli/commands/launch.js +61 -25
- package/src/runtime/launch/tabs.js +16 -6
- package/src/runtime/launch/tmux.js +24 -9
package/package.json
CHANGED
|
@@ -980,17 +980,40 @@ async function runWhere() {
|
|
|
980
980
|
async function runInternalCloseWindow(deps = {}) {
|
|
981
981
|
const runner = deps.runner || defaultTmuxRunner();
|
|
982
982
|
const pushClosedImpl = deps.pushClosedImpl || closedWindows.pushClosed;
|
|
983
|
+
const exit = deps.exit || ((code) => process.exit(code));
|
|
983
984
|
// ASCII Unit Separator — never appears in a session/window name or
|
|
984
985
|
// filesystem path, so splitting on it is unambiguous.
|
|
985
986
|
const DELIM = "\x1f";
|
|
986
|
-
|
|
987
|
-
|
|
987
|
+
// When the tmux keybind passes session+index positionally, target
|
|
988
|
+
// that exact window. This avoids a wrong-window kill if focus shifts
|
|
989
|
+
// between Alt+w being pressed and this subprocess starting.
|
|
990
|
+
const argSession = (deps.targetSession || "").trim();
|
|
991
|
+
const argIndex = (deps.targetIndex || "").trim();
|
|
992
|
+
let probeArgs;
|
|
993
|
+
if (argSession && argIndex) {
|
|
994
|
+
probeArgs = [
|
|
995
|
+
"display-message",
|
|
996
|
+
"-p",
|
|
997
|
+
"-t",
|
|
998
|
+
`${argSession}:${argIndex}`,
|
|
999
|
+
"-F",
|
|
1000
|
+
`#S${DELIM}#I${DELIM}#W${DELIM}#{pane_current_path}`,
|
|
1001
|
+
];
|
|
1002
|
+
} else {
|
|
1003
|
+
probeArgs = [
|
|
1004
|
+
"display-message",
|
|
1005
|
+
"-p",
|
|
1006
|
+
"-F",
|
|
1007
|
+
`#S${DELIM}#I${DELIM}#W${DELIM}#{pane_current_path}`,
|
|
1008
|
+
];
|
|
1009
|
+
}
|
|
1010
|
+
const probe = runner.runSync(probeArgs);
|
|
988
1011
|
if (probe.status !== 0) {
|
|
989
1012
|
// eslint-disable-next-line no-console
|
|
990
1013
|
console.error(
|
|
991
1014
|
`agileflow launch __close-window: tmux display-message failed: ${probe.stderr || "unknown"}`,
|
|
992
1015
|
);
|
|
993
|
-
return;
|
|
1016
|
+
return exit(1);
|
|
994
1017
|
}
|
|
995
1018
|
const parts = (probe.stdout || "").trimEnd().split(DELIM);
|
|
996
1019
|
if (parts.length !== 4) {
|
|
@@ -998,7 +1021,7 @@ async function runInternalCloseWindow(deps = {}) {
|
|
|
998
1021
|
console.error(
|
|
999
1022
|
`agileflow launch __close-window: unexpected display-message output (got ${parts.length} fields)`,
|
|
1000
1023
|
);
|
|
1001
|
-
return;
|
|
1024
|
+
return exit(1);
|
|
1002
1025
|
}
|
|
1003
1026
|
const [sessionName, windowIndex, windowName, cwd] = parts;
|
|
1004
1027
|
if (!sessionName || !windowIndex || !cwd) {
|
|
@@ -1006,7 +1029,7 @@ async function runInternalCloseWindow(deps = {}) {
|
|
|
1006
1029
|
console.error(
|
|
1007
1030
|
"agileflow launch __close-window: missing session/index/cwd; skipping kill",
|
|
1008
1031
|
);
|
|
1009
|
-
return;
|
|
1032
|
+
return exit(1);
|
|
1010
1033
|
}
|
|
1011
1034
|
// Kill first with the explicit target captured above. If this fails
|
|
1012
1035
|
// we abort without touching the log — the window is still alive and
|
|
@@ -1021,7 +1044,7 @@ async function runInternalCloseWindow(deps = {}) {
|
|
|
1021
1044
|
console.error(
|
|
1022
1045
|
`agileflow launch __close-window: kill-window failed: ${kill.stderr || "unknown"}`,
|
|
1023
1046
|
);
|
|
1024
|
-
return;
|
|
1047
|
+
return exit(1);
|
|
1025
1048
|
}
|
|
1026
1049
|
try {
|
|
1027
1050
|
pushClosedImpl({ sessionName, name: windowName || "", cwd });
|
|
@@ -1053,16 +1076,23 @@ async function runInternalRestoreWindow(deps = {}) {
|
|
|
1053
1076
|
const runner = deps.runner || defaultTmuxRunner();
|
|
1054
1077
|
const popClosedImpl = deps.popClosedImpl || closedWindows.popClosed;
|
|
1055
1078
|
const pushClosedImpl = deps.pushClosedImpl || closedWindows.pushClosed;
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1079
|
+
const exit = deps.exit || ((code) => process.exit(code));
|
|
1080
|
+
// Keybind passes #{session_name} so the restore targets the session
|
|
1081
|
+
// the user actually pressed Alt+T from. Fall back to display-message
|
|
1082
|
+
// for manual invocations (which only works inside tmux).
|
|
1083
|
+
let sessionName = (deps.targetSession || "").trim();
|
|
1084
|
+
if (!sessionName) {
|
|
1085
|
+
const probe = runner.runSync(["display-message", "-p", "-F", "#S"]);
|
|
1086
|
+
if (probe.status !== 0) {
|
|
1087
|
+
// eslint-disable-next-line no-console
|
|
1088
|
+
console.error(
|
|
1089
|
+
`agileflow launch __restore-window: tmux display-message failed: ${probe.stderr || "unknown"}`,
|
|
1090
|
+
);
|
|
1091
|
+
return exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
sessionName = (probe.stdout || "").trim();
|
|
1063
1094
|
}
|
|
1064
|
-
|
|
1065
|
-
if (!sessionName) return;
|
|
1095
|
+
if (!sessionName) return exit(1);
|
|
1066
1096
|
/** @type {ReturnType<typeof closedWindows.popClosed>} */
|
|
1067
1097
|
let entry;
|
|
1068
1098
|
try {
|
|
@@ -1072,7 +1102,7 @@ async function runInternalRestoreWindow(deps = {}) {
|
|
|
1072
1102
|
console.error(
|
|
1073
1103
|
`agileflow launch __restore-window: log pop failed: ${err && err.message ? err.message : err}`,
|
|
1074
1104
|
);
|
|
1075
|
-
return;
|
|
1105
|
+
return exit(1);
|
|
1076
1106
|
}
|
|
1077
1107
|
if (!entry) {
|
|
1078
1108
|
// Empty stack — silent. The user pressed Alt+T with nothing to undo.
|
|
@@ -1102,6 +1132,7 @@ async function runInternalRestoreWindow(deps = {}) {
|
|
|
1102
1132
|
`agileflow launch __restore-window: failed to re-push entry after new-window failure: ${pushErr && pushErr.message ? pushErr.message : pushErr}`,
|
|
1103
1133
|
);
|
|
1104
1134
|
}
|
|
1135
|
+
return exit(1);
|
|
1105
1136
|
}
|
|
1106
1137
|
}
|
|
1107
1138
|
|
|
@@ -1269,19 +1300,24 @@ async function launch(sub, nameArg, _options) {
|
|
|
1269
1300
|
return;
|
|
1270
1301
|
}
|
|
1271
1302
|
if (sub === "__close-window") {
|
|
1272
|
-
// Hidden subcommand invoked from tmux keybind (Alt+w).
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1303
|
+
// Hidden subcommand invoked from tmux keybind (Alt+w). The keybind
|
|
1304
|
+
// passes session name + window index as positional args so we
|
|
1305
|
+
// target the exact tab the user pressed Alt+w on, regardless of
|
|
1306
|
+
// any focus shift during the confirmation prompt. nameArg is the
|
|
1307
|
+
// session name; we read the window index from raw argv since
|
|
1308
|
+
// commander's signature only declares two positionals.
|
|
1309
|
+
const targetSession = nameArg || "";
|
|
1310
|
+
const targetIndex = (process.argv && process.argv[5]) || "";
|
|
1311
|
+
await runInternalCloseWindow({ targetSession, targetIndex });
|
|
1277
1312
|
return;
|
|
1278
1313
|
}
|
|
1279
1314
|
if (sub === "__restore-window") {
|
|
1280
1315
|
// Hidden subcommand invoked from tmux keybind (Alt+T). Pops the
|
|
1281
|
-
// most recent closed entry for the
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
|
|
1316
|
+
// most recent closed entry for the session the user pressed
|
|
1317
|
+
// Alt+T from (passed positionally via #{session_name}) and
|
|
1318
|
+
// spawns a new window in that cwd with the original name. No-op
|
|
1319
|
+
// when the log is empty for this session.
|
|
1320
|
+
await runInternalRestoreWindow({ targetSession: nameArg || "" });
|
|
1285
1321
|
return;
|
|
1286
1322
|
}
|
|
1287
1323
|
if (sub && sub !== "setup") {
|
|
@@ -259,9 +259,11 @@ function buildTabFormat(opts = {}) {
|
|
|
259
259
|
*/
|
|
260
260
|
const TAB_KEYBINDS = [
|
|
261
261
|
{
|
|
262
|
-
|
|
262
|
+
// Alt+t = new tab. Matches Chrome/Safari's Ctrl+T / Cmd+T —
|
|
263
|
+
// browser muscle memory carries straight over.
|
|
264
|
+
key: "M-t",
|
|
263
265
|
action: ["new-window"],
|
|
264
|
-
hint: "Alt+
|
|
266
|
+
hint: "Alt+t → new tab",
|
|
265
267
|
},
|
|
266
268
|
{
|
|
267
269
|
// -I prefills the prompt with the current name so the user can
|
|
@@ -272,14 +274,16 @@ const TAB_KEYBINDS = [
|
|
|
272
274
|
},
|
|
273
275
|
{
|
|
274
276
|
// confirm-before runs the command on `y` and does nothing on `n`.
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
+
// We pass session+index as positional args so the callback targets
|
|
278
|
+
// the exact window the user pressed Alt+w on — without this, the
|
|
279
|
+
// callback would re-probe display-message and could close the
|
|
280
|
+
// wrong tab if focus moved during the confirmation prompt.
|
|
277
281
|
key: "M-w",
|
|
278
282
|
action: [
|
|
279
283
|
"confirm-before",
|
|
280
284
|
"-p",
|
|
281
285
|
"kill tab #W? (y/n)",
|
|
282
|
-
"run-shell '%AGILEFLOW% launch __close-window'",
|
|
286
|
+
"run-shell '%AGILEFLOW% launch __close-window #{session_name} #{window_index}'",
|
|
283
287
|
],
|
|
284
288
|
hint: "Alt+w → close current tab (with confirm)",
|
|
285
289
|
},
|
|
@@ -291,8 +295,14 @@ const TAB_KEYBINDS = [
|
|
|
291
295
|
hint: "Alt+W → tab picker",
|
|
292
296
|
},
|
|
293
297
|
{
|
|
298
|
+
// Pass session name explicitly so the callback restores into the
|
|
299
|
+
// session the user actually triggered from — works even if the
|
|
300
|
+
// active session shifts before run-shell fires.
|
|
294
301
|
key: "M-T",
|
|
295
|
-
action: [
|
|
302
|
+
action: [
|
|
303
|
+
"run-shell",
|
|
304
|
+
"%AGILEFLOW% launch __restore-window #{session_name}",
|
|
305
|
+
],
|
|
296
306
|
hint: "Alt+T → reopen last closed tab",
|
|
297
307
|
},
|
|
298
308
|
// Numeric switchers Alt+1..Alt+9 → select-window -t :N
|
|
@@ -197,15 +197,29 @@ function detectTmuxVersion(runner) {
|
|
|
197
197
|
* @returns {{ applied: boolean, stderr: string }}
|
|
198
198
|
*/
|
|
199
199
|
function applyTabFormat(sessionName, runner, opts = {}) {
|
|
200
|
+
const theme = { ...tabs.DEFAULT_TAB_THEME, ...(opts.theme || {}) };
|
|
200
201
|
const format = tabs.buildTabFormat({
|
|
201
202
|
tmuxVersion: opts.tmuxVersion,
|
|
202
203
|
theme: opts.theme,
|
|
203
204
|
});
|
|
205
|
+
// Override tmux's default green status-style so the strip's dark
|
|
206
|
+
// background isn't broken up by tmux's stock green bar. Also clear
|
|
207
|
+
// status-left / status-right — they default to session info + clock
|
|
208
|
+
// on green; the tab strip already shows what the user needs.
|
|
209
|
+
runner.runSync([
|
|
210
|
+
"set-option",
|
|
211
|
+
"-t",
|
|
212
|
+
sessionName,
|
|
213
|
+
"status-style",
|
|
214
|
+
`bg=${theme.stripBg} fg=${theme.inactiveFg}`,
|
|
215
|
+
]);
|
|
216
|
+
runner.runSync(["set-option", "-t", sessionName, "status-left", ""]);
|
|
217
|
+
runner.runSync(["set-option", "-t", sessionName, "status-right", ""]);
|
|
204
218
|
const result = runner.runSync([
|
|
205
219
|
"set-option",
|
|
206
220
|
"-t",
|
|
207
221
|
sessionName,
|
|
208
|
-
"status-format[
|
|
222
|
+
"status-format[0]",
|
|
209
223
|
format,
|
|
210
224
|
]);
|
|
211
225
|
return {
|
|
@@ -538,8 +552,9 @@ async function launchInTmux(opts) {
|
|
|
538
552
|
// Re-apply the tab strip every attach so prefs / theme changes
|
|
539
553
|
// since session creation take effect (and so a session created by
|
|
540
554
|
// an older agileflow without a strip picks one up on reattach).
|
|
541
|
-
//
|
|
542
|
-
|
|
555
|
+
// Single dark status line — the tab strip on line[0] replaces
|
|
556
|
+
// tmux's default green status bar entirely.
|
|
557
|
+
runner.runSync(["set-option", "-t", base, "status", "1"]);
|
|
543
558
|
applyTabFormat(base, runner, { tmuxVersion });
|
|
544
559
|
log(`agileflow launch: resuming session ${base}`);
|
|
545
560
|
return attachSession(base, runner);
|
|
@@ -604,7 +619,7 @@ async function launchInTmux(opts) {
|
|
|
604
619
|
log(`agileflow launch: keybind skipped — ${f.hint}`);
|
|
605
620
|
}
|
|
606
621
|
}
|
|
607
|
-
runner.runSync(["set-option", "-t", name, "status", "
|
|
622
|
+
runner.runSync(["set-option", "-t", name, "status", "1"]);
|
|
608
623
|
applyTabFormat(name, runner, { tmuxVersion });
|
|
609
624
|
return attachSession(name, runner);
|
|
610
625
|
}
|
|
@@ -624,11 +639,11 @@ async function launchInTmux(opts) {
|
|
|
624
639
|
log(`agileflow launch: keybind skipped — ${f.hint}`);
|
|
625
640
|
}
|
|
626
641
|
}
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
// the attach.
|
|
631
|
-
runner.runSync(["set-option", "-t", name, "status", "
|
|
642
|
+
// Single dark status line — the tab strip on status-format[0]
|
|
643
|
+
// replaces tmux's default green status bar entirely. Per-session
|
|
644
|
+
// so other tmux clients are unaffected. Then write the tab format
|
|
645
|
+
// itself. Both are best-effort; failure shouldn't block the attach.
|
|
646
|
+
runner.runSync(["set-option", "-t", name, "status", "1"]);
|
|
632
647
|
applyTabFormat(name, runner, { tmuxVersion });
|
|
633
648
|
log(`agileflow launch: starting new session ${name}`);
|
|
634
649
|
return attachSession(name, runner);
|