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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "4.0.0-alpha.10",
3
+ "version": "4.0.0-alpha.12",
4
4
  "description": "AI-driven agile development toolkit for Claude Code — skills-first architecture with opt-in plugins (v4)",
5
5
  "keywords": [
6
6
  "agile",
@@ -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
- const fmt = `#S${DELIM}#I${DELIM}#W${DELIM}#{pane_current_path}`;
987
- const probe = runner.runSync(["display-message", "-p", "-F", fmt]);
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 probe = runner.runSync(["display-message", "-p", "-F", "#S"]);
1057
- if (probe.status !== 0) {
1058
- // eslint-disable-next-line no-console
1059
- console.error(
1060
- `agileflow launch __restore-window: tmux display-message failed: ${probe.stderr || "unknown"}`,
1061
- );
1062
- return;
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
- const sessionName = (probe.stdout || "").trim();
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). Captures
1273
- // the current window's name + pane cwd, pushes onto the
1274
- // closed-windows log, then issues kill-window so Alt+T can
1275
- // resurrect it later. No UI; failures are silent.
1276
- await runInternalCloseWindow();
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 current session and spawns
1282
- // a new window in that cwd with the original name. No-op when
1283
- // the log is empty for this session.
1284
- await runInternalRestoreWindow();
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
- key: "M-c",
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+c → new tab",
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
- // The bookkeeping happens server-side via the agileflow callback
276
- // it captures #W + cwd, pushes onto the closed log, then kill-windows.
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: ["run-shell", "%AGILEFLOW% launch __restore-window"],
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[1]",
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
- // Requires status lines = 2 so the tab strip on line[1] renders.
542
- runner.runSync(["set-option", "-t", base, "status", "2"]);
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", "2"]);
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
- // Two-line status so the tab strip on status-format[1] is visible.
628
- // Per-session so other tmux clients are unaffected. Then write the
629
- // tab format itself. Both are best-effort; failure shouldn't block
630
- // the attach.
631
- runner.runSync(["set-option", "-t", name, "status", "2"]);
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);