agileflow 4.0.0-alpha.16 → 4.0.0-alpha.18

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.16",
3
+ "version": "4.0.0-alpha.18",
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",
@@ -1182,11 +1182,32 @@ async function maybeOfferAutoRestore(prefs) {
1182
1182
  if (ap !== bp) return bp - ap;
1183
1183
  return 0;
1184
1184
  });
1185
- const options = sorted.map((s) => ({
1185
+ // Smart toggle at the top of the picker: one entry that flips the
1186
+ // current selection state. If everything's selected, checking it
1187
+ // deselects all; if anything's unselected, checking it selects all.
1188
+ // Visually separated from session entries by a dash divider line
1189
+ // so it doesn't blend in with real options.
1190
+ const TOGGLE_ALL = "__toggle_all__";
1191
+ const DIVIDER = "__divider__";
1192
+ const allNames = sorted.map((s) => s.name);
1193
+ const sessionOptions = sorted.map((s) => ({
1186
1194
  value: s.name,
1187
- label: `${s.pinned ? " " : " "}${s.name}`,
1195
+ label: `${s.pinned ? "* " : " "}${s.name}`,
1188
1196
  hint: `${s.cli} — ${s.cwd}${s.worktree && s.worktree.branch ? ` [wt ${s.worktree.branch}]` : ""}`,
1189
1197
  }));
1198
+ const options = [
1199
+ {
1200
+ value: TOGGLE_ALL,
1201
+ label: "[ select all / deselect all ]",
1202
+ hint: "toggles every session below",
1203
+ },
1204
+ {
1205
+ value: DIVIDER,
1206
+ label: "─────────────────────────────",
1207
+ hint: "",
1208
+ },
1209
+ ...sessionOptions,
1210
+ ];
1190
1211
  const anyPinned = sorted.some((s) => s.pinned === true);
1191
1212
  const initial = anyPinned
1192
1213
  ? sorted.filter((s) => s.pinned === true).map((s) => s.name)
@@ -1210,7 +1231,19 @@ async function maybeOfferAutoRestore(prefs) {
1210
1231
  process.exit(0);
1211
1232
  }
1212
1233
  /** @type {string[]} */
1213
- const chosen = Array.isArray(selection) ? selection : [];
1234
+ let chosen = Array.isArray(selection) ? selection : [];
1235
+ // Strip the synthetic divider always (it's never a real choice).
1236
+ // Resolve the toggle: if the user checked it, flip the current
1237
+ // selection state — everything selected goes to nothing, anything
1238
+ // partial or empty goes to everything.
1239
+ const toggled = chosen.includes(TOGGLE_ALL);
1240
+ chosen = chosen.filter((v) => v !== TOGGLE_ALL && v !== DIVIDER);
1241
+ if (toggled) {
1242
+ const allSelected =
1243
+ chosen.length === allNames.length &&
1244
+ allNames.every((n) => chosen.includes(n));
1245
+ chosen = allSelected ? [] : [...allNames];
1246
+ }
1214
1247
  if (chosen.length === 0) {
1215
1248
  prompts.outro(
1216
1249
  "Skipped. Run `agileflow launch restore` later to bring them back.",
@@ -19,6 +19,8 @@ const {
19
19
  sessionExists,
20
20
  createSession,
21
21
  applyKeybindPreset,
22
+ applyTabFormat,
23
+ detectTmuxVersion,
22
24
  } = require("./tmux.js");
23
25
  const { loadRegistry } = require("./session-registry.js");
24
26
  const { resolveAgileflowBin } = require("./alias-installer.js");
@@ -117,6 +119,14 @@ function runRestore(opts) {
117
119
  log(`agileflow launch: failed to restore ${entry.name} — ${stderr}`);
118
120
  continue;
119
121
  }
122
+ // Apply the same per-session styling launchInTmux does for fresh
123
+ // sessions so the tab strip looks consistent on restore. Without
124
+ // this, restored sessions show tmux's default green status bar
125
+ // instead of the AgileFlow dark strip.
126
+ runner.runSync(["set-option", "-t", entry.name, "status", "1"]);
127
+ applyTabFormat(entry.name, runner, {
128
+ tmuxVersion: detectTmuxVersion(runner),
129
+ });
120
130
  result.restored++;
121
131
  log(`agileflow launch: restored session ${entry.name} (${entry.cwd})`);
122
132
  }
@@ -273,17 +273,15 @@ const TAB_KEYBINDS = [
273
273
  hint: "Alt+, → rename current tab",
274
274
  },
275
275
  {
276
- // Direct close — no confirm-before. The earlier confirm-before
277
- // path was brittle under tmux's bind-key parsing (deferred-command
278
- // quoting issues caused the keybind to silently no-op on some
279
- // tmux builds). Alt+Shift+T (Alt+T) restores the last closed
280
- // tab in the same cwd, so the close is effectively reversible —
281
- // matches Chrome's instant-close + Ctrl+Shift+T undo.
276
+ // Direct kill-window — no CLI roundtrip. The previous
277
+ // run-shell-to-agileflow approach added Node-startup latency
278
+ // (150ms+) and could no-op silently if the binary path resolution
279
+ // returned stale state (e.g. after npx cache cleanup). Killing
280
+ // via tmux directly is instant and bulletproof. Undo is provided
281
+ // by Alt+Shift+T (which reads the closed-windows log populated by
282
+ // the window-unlinked hook installed in applyTabFormat).
282
283
  key: "M-w",
283
- action: [
284
- "run-shell",
285
- "%AGILEFLOW% launch __close-window #{session_name} #{window_index}",
286
- ],
284
+ action: ["kill-window"],
287
285
  hint: "Alt+w → close current tab (Alt+Shift+T to undo)",
288
286
  },
289
287
  {
@@ -241,24 +241,53 @@ function applyTabFormat(sessionName, runner, opts = {}) {
241
241
  `#[bg=${PILL_BG},fg=${ACCENT}] #h ` +
242
242
  `#[fg=${PILL_BG},bg=${BG}]${HALF_ROUND_CLOSE}`;
243
243
 
244
- // Apply each option and collect failures. If ANYTHING fails we surface
245
- // it on stderr so users debugging "why doesn't my tab strip look right"
246
- // can see the tmux complaint instead of staring at default formatting.
244
+ // Apply each option with the correct tmux scope. Session-scope opts
245
+ // (status-style, status-left/right, status-justify) take
246
+ // `-t <session>`. Window-scope opts (window-status-format,
247
+ // window-status-current-format, window-status-separator) take
248
+ // `-wg` so every window in every session picks them up. Earlier
249
+ // code applied ALL options with `-t session`, which made tmux
250
+ // silently ignore the window-scope ones — the visible symptom
251
+ // was tmux's default green status bar surviving on every session.
247
252
  const ops = [
248
- ["status-style", `bg=${BG},fg=${theme.inactiveFg}`],
249
- ["status-justify", "centre"],
250
- ["status-left", statusLeft],
251
- ["status-left-length", "100"],
252
- ["status-right", statusRight],
253
- ["status-right-length", "100"],
254
- ["window-status-separator", ""],
255
- ["window-status-format", inactiveFormat],
256
- ["window-status-current-format", activeFormat],
253
+ {
254
+ scope: "session",
255
+ option: "status-style",
256
+ value: `bg=${BG},fg=${theme.inactiveFg}`,
257
+ },
258
+ { scope: "session", option: "status-justify", value: "centre" },
259
+ { scope: "session", option: "status-left", value: statusLeft },
260
+ { scope: "session", option: "status-left-length", value: "100" },
261
+ { scope: "session", option: "status-right", value: statusRight },
262
+ { scope: "session", option: "status-right-length", value: "100" },
263
+ // Number windows from 1 so Alt+1 maps to the first tab (matches
264
+ // Chrome's Ctrl+1 mental model). Tmux default is base-index 0,
265
+ // which means Alt+1 with no other tabs open does nothing.
266
+ { scope: "session", option: "base-index", value: "1" },
267
+ // Keep tab indices contiguous after a close — without this,
268
+ // closing window 2 leaves indices 1, 3, 4 and Alt+2 becomes
269
+ // dead. tmux renumbers on close so Alt+1..N always works.
270
+ { scope: "session", option: "renumber-windows", value: "on" },
271
+ { scope: "window-global", option: "window-status-separator", value: "" },
272
+ {
273
+ scope: "window-global",
274
+ option: "window-status-format",
275
+ value: inactiveFormat,
276
+ },
277
+ {
278
+ scope: "window-global",
279
+ option: "window-status-current-format",
280
+ value: activeFormat,
281
+ },
257
282
  ];
258
283
  let lastResult = { status: 0, stderr: "" };
259
284
  const failures = [];
260
- for (const [option, value] of ops) {
261
- const r = runner.runSync(["set-option", "-t", sessionName, option, value]);
285
+ for (const { scope, option, value } of ops) {
286
+ const args =
287
+ scope === "session"
288
+ ? ["set-option", "-t", sessionName, option, value]
289
+ : ["set-option", "-wg", option, value];
290
+ const r = runner.runSync(args);
262
291
  lastResult = r;
263
292
  if (r.status !== 0) {
264
293
  failures.push({ option, stderr: (r.stderr || "").trim() });