@bastani/atomic 0.6.6-1 → 0.6.7-0

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.
Files changed (53) hide show
  1. package/README.md +22 -16
  2. package/dist/sdk/components/compact-switcher.d.ts.map +1 -1
  3. package/dist/sdk/components/connectors.d.ts +1 -0
  4. package/dist/sdk/components/connectors.d.ts.map +1 -1
  5. package/dist/sdk/components/edge.d.ts +1 -1
  6. package/dist/sdk/components/edge.d.ts.map +1 -1
  7. package/dist/sdk/components/graph-theme.d.ts.map +1 -1
  8. package/dist/sdk/components/header.d.ts.map +1 -1
  9. package/dist/sdk/components/node-card.d.ts.map +1 -1
  10. package/dist/sdk/components/orchestrator-panel.d.ts +7 -1
  11. package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -1
  12. package/dist/sdk/components/renderer-background.d.ts +9 -0
  13. package/dist/sdk/components/renderer-background.d.ts.map +1 -0
  14. package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
  15. package/dist/sdk/components/statusline.d.ts.map +1 -1
  16. package/dist/sdk/components/tui-diagnostics.d.ts +56 -0
  17. package/dist/sdk/components/tui-diagnostics.d.ts.map +1 -0
  18. package/dist/sdk/components/workflow-picker-panel.d.ts +2 -1
  19. package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
  20. package/dist/sdk/providers/copilot.d.ts +3 -2
  21. package/dist/sdk/providers/copilot.d.ts.map +1 -1
  22. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  23. package/dist/sdk/runtime/theme.d.ts +4 -0
  24. package/dist/sdk/runtime/theme.d.ts.map +1 -1
  25. package/dist/theme/colors.d.ts +2 -0
  26. package/dist/theme/colors.d.ts.map +1 -1
  27. package/package.json +2 -1
  28. package/src/cli.ts +3 -3
  29. package/src/commands/cli/chat/index.ts +10 -4
  30. package/src/commands/cli/management-commands.ts +4 -3
  31. package/src/commands/cli/session.test.ts +79 -6
  32. package/src/commands/cli/session.ts +65 -9
  33. package/src/completions/fish.ts +9 -3
  34. package/src/completions/powershell.ts +27 -3
  35. package/src/completions/zsh.ts +9 -2
  36. package/src/sdk/components/compact-switcher.tsx +10 -5
  37. package/src/sdk/components/connectors.ts +4 -0
  38. package/src/sdk/components/edge.tsx +5 -3
  39. package/src/sdk/components/graph-theme.ts +2 -3
  40. package/src/sdk/components/header.tsx +21 -9
  41. package/src/sdk/components/node-card.tsx +13 -7
  42. package/src/sdk/components/orchestrator-panel.tsx +47 -2
  43. package/src/sdk/components/renderer-background.ts +49 -0
  44. package/src/sdk/components/session-graph-panel.tsx +9 -2
  45. package/src/sdk/components/statusline.tsx +26 -22
  46. package/src/sdk/components/tui-diagnostics.ts +273 -0
  47. package/src/sdk/components/workflow-picker-panel.tsx +33 -22
  48. package/src/sdk/providers/copilot.ts +10 -4
  49. package/src/sdk/runtime/executor.ts +28 -1
  50. package/src/sdk/runtime/theme.ts +28 -36
  51. package/src/services/system/install-ui.ts +16 -17
  52. package/src/theme/colors.ts +14 -9
  53. package/src/theme/logo.ts +23 -12
@@ -288,6 +288,8 @@ const tmuxMocks = {
288
288
  spawnMuxAttach: mock(() => ({ exited: Promise.resolve(0) }) as never),
289
289
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
290
  select: mock<(...args: any[]) => Promise<string | symbol>>(() => Promise.resolve("my-session")),
291
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
+ multiselect: mock<(...args: any[]) => Promise<string[] | symbol>>(() => Promise.resolve([])),
291
293
  killSession: mock<(name: string) => void>(() => {}),
292
294
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
295
  confirm: mock<(...args: any[]) => Promise<boolean | symbol>>(() => Promise.resolve(true)),
@@ -309,6 +311,7 @@ function resetTmuxMocks(): void {
309
311
  tmuxMocks.detachAndAttachAtomic.mockReset();
310
312
  tmuxMocks.spawnMuxAttach.mockReset().mockReturnValue({ exited: Promise.resolve(0) } as never);
311
313
  tmuxMocks.select.mockReset().mockResolvedValue("my-session");
314
+ tmuxMocks.multiselect.mockReset().mockResolvedValue([]);
312
315
  tmuxMocks.killSession.mockReset();
313
316
  tmuxMocks.confirm.mockReset().mockResolvedValue(true);
314
317
  }
@@ -628,24 +631,29 @@ describe("sessionKillCommand", () => {
628
631
  }
629
632
  });
630
633
 
631
- // (h) omitted id, N sessions, user confirms → killSession called for each, return 0
632
- test("omitted id prompts and kills all sessions on confirm", async () => {
634
+ // (h) omitted id, N sessions, user selects sessions and confirms → killSession called for selected, return 0
635
+ test("omitted id prompts with multiselect and kills selected sessions on confirm", async () => {
633
636
  const now = new Date().toISOString();
634
637
  tmuxMocks.listSessions.mockReturnValue([
635
638
  { name: "session-a", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
636
639
  { name: "session-b", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
637
640
  { name: "session-c", windows: 1, created: now, attached: false, type: "chat" as const, agent: "copilot" },
638
641
  ]);
642
+ tmuxMocks.multiselect.mockResolvedValue(["session-a", "session-c"]);
639
643
  tmuxMocks.confirm.mockResolvedValue(true);
640
644
  const origWrite = process.stdout.write;
641
645
  process.stdout.write = (() => true) as typeof process.stdout.write;
642
646
  try {
643
647
  const code = await sessionKillCommand(undefined, [], "all", makeDeps());
644
648
  expect(code).toBe(0);
645
- expect(tmuxMocks.killSession).toHaveBeenCalledTimes(3);
649
+ expect(tmuxMocks.multiselect).toHaveBeenCalledTimes(1);
650
+ expect(tmuxMocks.multiselect).toHaveBeenCalledWith(expect.objectContaining({
651
+ message: "Select sessions to kill (Space toggles, Enter continues)",
652
+ }));
653
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(2);
646
654
  expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-a");
647
- expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-b");
648
655
  expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-c");
656
+ expect(tmuxMocks.killSession).not.toHaveBeenCalledWith("session-b");
649
657
  } finally {
650
658
  process.stdout.write = origWrite;
651
659
  }
@@ -658,6 +666,7 @@ describe("sessionKillCommand", () => {
658
666
  { name: "chat-session", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
659
667
  { name: "wf-session", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
660
668
  ]);
669
+ tmuxMocks.multiselect.mockResolvedValue(["chat-session"]);
661
670
  tmuxMocks.confirm.mockResolvedValue(true);
662
671
  const origWrite = process.stdout.write;
663
672
  process.stdout.write = (() => true) as typeof process.stdout.write;
@@ -679,6 +688,7 @@ describe("sessionKillCommand", () => {
679
688
  { name: "chat-session", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
680
689
  { name: "wf-session", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
681
690
  ]);
691
+ tmuxMocks.multiselect.mockResolvedValue(["wf-session"]);
682
692
  tmuxMocks.confirm.mockResolvedValue(true);
683
693
  const origWrite = process.stdout.write;
684
694
  process.stdout.write = (() => true) as typeof process.stdout.write;
@@ -700,6 +710,7 @@ describe("sessionKillCommand", () => {
700
710
  { name: "session-x", windows: 1, created: now, attached: false, type: "chat" as const },
701
711
  { name: "session-y", windows: 1, created: now, attached: false, type: "workflow" as const },
702
712
  ]);
713
+ tmuxMocks.multiselect.mockResolvedValue(["session-x", "session-y"]);
703
714
  tmuxMocks.confirm.mockResolvedValue(false);
704
715
  const origWrite = process.stdout.write;
705
716
  process.stdout.write = (() => true) as typeof process.stdout.write;
@@ -736,19 +747,81 @@ describe("sessionKillCommand", () => {
736
747
  }
737
748
  });
738
749
 
739
- // (m) -y on kill-all: skip prompt and kill every in-scope session
740
- test("yes flag skips the prompt for kill-all and kills every in-scope session", async () => {
750
+ // (m) -y on omitted id: skip confirm after selection and kill selected sessions
751
+ test("yes flag skips the confirmation prompt after selecting sessions", async () => {
741
752
  const now = new Date().toISOString();
742
753
  tmuxMocks.listSessions.mockReturnValue([
743
754
  { name: "session-a", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
744
755
  { name: "session-b", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
745
756
  ]);
757
+ tmuxMocks.multiselect.mockResolvedValue(["session-b"]);
746
758
  const origWrite = process.stdout.write;
747
759
  process.stdout.write = (() => true) as typeof process.stdout.write;
748
760
  try {
749
761
  const code = await sessionKillCommand(undefined, [], "all", makeDeps(), { yes: true });
750
762
  expect(code).toBe(0);
751
763
  expect(tmuxMocks.confirm).not.toHaveBeenCalled();
764
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(1);
765
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-b");
766
+ } finally {
767
+ process.stdout.write = origWrite;
768
+ }
769
+ });
770
+
771
+ test("selecting the all option kills every matching session after confirmation", async () => {
772
+ const now = new Date().toISOString();
773
+ tmuxMocks.listSessions.mockReturnValue([
774
+ { name: "session-a", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
775
+ { name: "session-b", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
776
+ ]);
777
+ tmuxMocks.multiselect.mockResolvedValue(["__atomic_select_all_sessions__"]);
778
+ tmuxMocks.confirm.mockResolvedValue(true);
779
+ const origWrite = process.stdout.write;
780
+ process.stdout.write = (() => true) as typeof process.stdout.write;
781
+ try {
782
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps());
783
+ expect(code).toBe(0);
784
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(2);
785
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-a");
786
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-b");
787
+ } finally {
788
+ process.stdout.write = origWrite;
789
+ }
790
+ });
791
+
792
+ test("all flag skips multiselect and confirms every matching session", async () => {
793
+ const now = new Date().toISOString();
794
+ tmuxMocks.listSessions.mockReturnValue([
795
+ { name: "session-a", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
796
+ { name: "session-b", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
797
+ ]);
798
+ tmuxMocks.confirm.mockResolvedValue(true);
799
+ const origWrite = process.stdout.write;
800
+ process.stdout.write = (() => true) as typeof process.stdout.write;
801
+ try {
802
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps(), { all: true });
803
+ expect(code).toBe(0);
804
+ expect(tmuxMocks.multiselect).not.toHaveBeenCalled();
805
+ expect(tmuxMocks.confirm).toHaveBeenCalledTimes(1);
806
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(2);
807
+ } finally {
808
+ process.stdout.write = origWrite;
809
+ }
810
+ });
811
+
812
+ test("all and yes flags kill every matching session without prompts", async () => {
813
+ const now = new Date().toISOString();
814
+ tmuxMocks.listSessions.mockReturnValue([
815
+ { name: "session-a", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
816
+ { name: "session-b", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
817
+ ]);
818
+ const origWrite = process.stdout.write;
819
+ process.stdout.write = (() => true) as typeof process.stdout.write;
820
+ try {
821
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps(), { all: true, yes: true });
822
+ expect(code).toBe(0);
823
+ expect(tmuxMocks.multiselect).not.toHaveBeenCalled();
824
+ expect(tmuxMocks.confirm).not.toHaveBeenCalled();
752
825
  expect(tmuxMocks.killSession).toHaveBeenCalledTimes(2);
753
826
  } finally {
754
827
  process.stdout.write = origWrite;
@@ -7,7 +7,7 @@
7
7
  * tmux directly.
8
8
  */
9
9
 
10
- import { select, confirm, isCancel, cancel } from "@clack/prompts";
10
+ import { select, multiselect, confirm, isCancel, cancel } from "@clack/prompts";
11
11
  import { createPainter, type PaletteKey } from "../../theme/colors.ts";
12
12
  import {
13
13
  listSessions as _listSessions,
@@ -40,6 +40,8 @@ export interface SessionDeps {
40
40
  killSession: (name: string) => void;
41
41
  /** Prompt function for the session picker — defaults to @clack/prompts select. */
42
42
  select: typeof select;
43
+ /** Prompt function for the session kill picker — defaults to @clack/prompts multiselect. */
44
+ multiselect: typeof multiselect;
43
45
  /** Prompt function for yes/no confirmations — defaults to @clack/prompts confirm. */
44
46
  confirm: typeof confirm;
45
47
  isCancel: typeof isCancel;
@@ -57,6 +59,7 @@ const defaultDeps: SessionDeps = {
57
59
  detachAndAttachAtomic: _detachAndAttachAtomic,
58
60
  killSession: _killSession,
59
61
  select,
62
+ multiselect,
60
63
  confirm,
61
64
  isCancel,
62
65
  };
@@ -273,10 +276,13 @@ export async function sessionPickerCommand(agents: string[] = [], scope: Session
273
276
  // ─── Session kill command ────────────────────────────────────────────────────
274
277
 
275
278
  /**
276
- * Kill a named session or all sessions matching the given scope and agents.
279
+ * Kill a named session or selected sessions matching the given scope and agents.
277
280
  *
278
281
  * - If `sessionId` is provided: confirm and kill that one session.
279
- * - If `sessionId` is omitted: confirm and kill all sessions in scope.
282
+ * - If `sessionId` is omitted: pick sessions with a checkbox multi-select,
283
+ * then confirm and kill the selected sessions.
284
+ * - If `all: true` and `sessionId` is omitted: preselect every matching
285
+ * session and only ask for confirmation unless `yes: true` is also set.
280
286
  *
281
287
  * Pass `yes: true` (the `-y/--yes` flag on the CLI) to skip the
282
288
  * confirmation prompt — useful for orchestrating agents that need to
@@ -287,9 +293,10 @@ export async function sessionKillCommand(
287
293
  agents: string[] = [],
288
294
  scope: SessionScope = "all",
289
295
  deps: SessionDeps = defaultDeps,
290
- options: { yes?: boolean } = {},
296
+ options: { yes?: boolean; all?: boolean } = {},
291
297
  ): Promise<number> {
292
298
  const skipConfirm = options.yes === true;
299
+ const selectAll = options.all === true;
293
300
  const paint = createPainter();
294
301
 
295
302
  if (!deps.isTmuxInstalled()) {
@@ -351,7 +358,7 @@ export async function sessionKillCommand(
351
358
  return 0;
352
359
  }
353
360
 
354
- // ── Kill-all path ─────────────────────────────────────────────────────────
361
+ // ── Multi-kill path ───────────────────────────────────────────────────────
355
362
  const targets = filterByAgent(filterByScope(deps.listSessions(), scope), agents);
356
363
 
357
364
  if (targets.length === 0) {
@@ -359,12 +366,29 @@ export async function sessionKillCommand(
359
366
  return 0;
360
367
  }
361
368
 
362
- const noun = targets.length === 1 ? "session" : "sessions";
369
+ const selectedNames = selectAll
370
+ ? targets.map((t) => t.name)
371
+ : await selectSessionsToKill(targets, deps);
372
+
373
+ if (deps.isCancel(selectedNames)) {
374
+ cancel("Cancelled.");
375
+ return 0;
376
+ }
377
+
378
+ if (selectedNames.length === 0) {
379
+ process.stdout.write(
380
+ "\n " + paint("dim", "No sessions selected.") + "\n\n",
381
+ );
382
+ return 0;
383
+ }
384
+
385
+ const selectedTargets = targets.filter((t) => selectedNames.includes(t.name));
386
+ const noun = selectedTargets.length === 1 ? "session" : "sessions";
363
387
  const scopePrefix = scope === "all" ? "" : `${scope} `;
364
388
  const answer = skipConfirm
365
389
  ? true
366
390
  : await deps.confirm({
367
- message: `Kill all ${targets.length} ${scopePrefix}${noun}?`,
391
+ message: `Kill ${selectedTargets.length} ${scopePrefix}${noun}?`,
368
392
  initialValue: false,
369
393
  });
370
394
 
@@ -374,11 +398,11 @@ export async function sessionKillCommand(
374
398
  }
375
399
 
376
400
  if (answer === true) {
377
- for (const t of targets) {
401
+ for (const t of selectedTargets) {
378
402
  deps.killSession(t.name);
379
403
  }
380
404
  process.stdout.write(
381
- "\n " + paint("success", "✓") + " killed " + paint("text", String(targets.length)) + " " + paint("dim", noun) + "\n\n",
405
+ "\n " + paint("success", "✓") + " killed " + paint("text", String(selectedTargets.length)) + " " + paint("dim", noun) + "\n\n",
382
406
  );
383
407
  return 0;
384
408
  }
@@ -389,3 +413,35 @@ export async function sessionKillCommand(
389
413
  );
390
414
  return 0;
391
415
  }
416
+
417
+ const SELECT_ALL_SESSIONS = "__atomic_select_all_sessions__";
418
+
419
+ async function selectSessionsToKill(
420
+ targets: TmuxSession[],
421
+ deps: SessionDeps,
422
+ ): Promise<string[] | symbol> {
423
+ const selected = await deps.multiselect({
424
+ message: "Select sessions to kill (Space toggles, Enter continues)",
425
+ options: [
426
+ {
427
+ value: SELECT_ALL_SESSIONS,
428
+ label: "All matching sessions",
429
+ hint: `selects ${targets.length}`,
430
+ },
431
+ ...targets.map((s) => {
432
+ const age = formatAge(s.created);
433
+ const tag = s.attached ? "attached" : undefined;
434
+ return {
435
+ value: s.name,
436
+ label: s.name,
437
+ hint: tag ? `${age}, ${tag}` : age,
438
+ };
439
+ }),
440
+ ],
441
+ required: false,
442
+ });
443
+
444
+ if (deps.isCancel(selected)) return selected;
445
+ if (selected.includes(SELECT_ALL_SESSIONS)) return targets.map((t) => t.name);
446
+ return selected;
447
+ }
@@ -86,10 +86,12 @@ complete -c atomic -n '__atomic_using_cmd chat; and not __fish_seen_subcommand_f
86
86
  # chat session
87
87
  complete -c atomic -n '__atomic_using_cmd chat session; and not __fish_seen_subcommand_from list connect kill' -a list -d 'List running sessions'
88
88
  complete -c atomic -n '__atomic_using_cmd chat session; and not __fish_seen_subcommand_from list connect kill' -a connect -d 'Attach to a running session'
89
- complete -c atomic -n '__atomic_using_cmd chat session; and not __fish_seen_subcommand_from list connect kill' -a kill -d 'Kill a running session (omit id to kill all)'
89
+ complete -c atomic -n '__atomic_using_cmd chat session; and not __fish_seen_subcommand_from list connect kill' -a kill -d 'Kill running sessions'
90
90
  complete -c atomic -n '__atomic_using_cmd chat session list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
91
91
  complete -c atomic -n '__atomic_using_cmd chat session connect' -s a -l agent -d 'Filter by agent' -r -a "$agents"
92
92
  complete -c atomic -n '__atomic_using_cmd chat session kill' -s a -l agent -d 'Filter by agent' -r -a "$agents"
93
+ complete -c atomic -n '__atomic_using_cmd chat session kill' -l all -d 'Select all matching sessions'
94
+ complete -c atomic -n '__atomic_using_cmd chat session kill' -s y -l yes -d 'Skip confirmation prompt'
93
95
 
94
96
  # ── workflow ────────────────────────────────────────────────────────────────
95
97
 
@@ -104,19 +106,23 @@ complete -c atomic -n '__atomic_using_cmd workflow list' -s a -l agent -d 'Filte
104
106
  # workflow session
105
107
  complete -c atomic -n '__atomic_using_cmd workflow session; and not __fish_seen_subcommand_from list connect kill' -a list -d 'List running sessions'
106
108
  complete -c atomic -n '__atomic_using_cmd workflow session; and not __fish_seen_subcommand_from list connect kill' -a connect -d 'Attach to a running session'
107
- complete -c atomic -n '__atomic_using_cmd workflow session; and not __fish_seen_subcommand_from list connect kill' -a kill -d 'Kill a running session (omit id to kill all)'
109
+ complete -c atomic -n '__atomic_using_cmd workflow session; and not __fish_seen_subcommand_from list connect kill' -a kill -d 'Kill running sessions'
108
110
  complete -c atomic -n '__atomic_using_cmd workflow session list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
109
111
  complete -c atomic -n '__atomic_using_cmd workflow session connect' -s a -l agent -d 'Filter by agent' -r -a "$agents"
110
112
  complete -c atomic -n '__atomic_using_cmd workflow session kill' -s a -l agent -d 'Filter by agent' -r -a "$agents"
113
+ complete -c atomic -n '__atomic_using_cmd workflow session kill' -l all -d 'Select all matching sessions'
114
+ complete -c atomic -n '__atomic_using_cmd workflow session kill' -s y -l yes -d 'Skip confirmation prompt'
111
115
 
112
116
  # ── session (top-level) ────────────────────────────────────────────────────
113
117
 
114
118
  complete -c atomic -n '__atomic_using_cmd session; and not __fish_seen_subcommand_from list connect kill' -a list -d 'List running sessions'
115
119
  complete -c atomic -n '__atomic_using_cmd session; and not __fish_seen_subcommand_from list connect kill' -a connect -d 'Attach to a running session'
116
- complete -c atomic -n '__atomic_using_cmd session; and not __fish_seen_subcommand_from list connect kill' -a kill -d 'Kill a running session (omit id to kill all)'
120
+ complete -c atomic -n '__atomic_using_cmd session; and not __fish_seen_subcommand_from list connect kill' -a kill -d 'Kill running sessions'
117
121
  complete -c atomic -n '__atomic_using_cmd session list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
118
122
  complete -c atomic -n '__atomic_using_cmd session connect' -s a -l agent -d 'Filter by agent' -r -a "$agents"
119
123
  complete -c atomic -n '__atomic_using_cmd session kill' -s a -l agent -d 'Filter by agent' -r -a "$agents"
124
+ complete -c atomic -n '__atomic_using_cmd session kill' -l all -d 'Select all matching sessions'
125
+ complete -c atomic -n '__atomic_using_cmd session kill' -s y -l yes -d 'Skip confirmation prompt'
120
126
 
121
127
  # ── config ──────────────────────────────────────────────────────────────────
122
128
 
@@ -80,7 +80,15 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
80
80
  $completions = @(
81
81
  @{ text = 'list'; tip = 'List running sessions' }
82
82
  @{ text = 'connect'; tip = 'Attach to a running session' }
83
- @{ text = 'kill'; tip = 'Kill a running session (omit id to kill all)' }
83
+ @{ text = 'kill'; tip = 'Kill running sessions' }
84
+ )
85
+ } elseif ($cmds.Count -ge 3 -and $cmds[2] -eq 'kill') {
86
+ $completions = @(
87
+ @{ text = '-a'; tip = 'Filter by agent' }
88
+ @{ text = '--agent'; tip = 'Filter by agent' }
89
+ @{ text = '--all'; tip = 'Select all matching sessions' }
90
+ @{ text = '-y'; tip = 'Skip confirmation prompt' }
91
+ @{ text = '--yes'; tip = 'Skip confirmation prompt' }
84
92
  )
85
93
  } else {
86
94
  $completions = @(
@@ -110,7 +118,15 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
110
118
  $completions = @(
111
119
  @{ text = 'list'; tip = 'List running sessions' }
112
120
  @{ text = 'connect'; tip = 'Attach to a running session' }
113
- @{ text = 'kill'; tip = 'Kill a running session (omit id to kill all)' }
121
+ @{ text = 'kill'; tip = 'Kill running sessions' }
122
+ )
123
+ } elseif ($cmds.Count -ge 3 -and $cmds[2] -eq 'kill') {
124
+ $completions = @(
125
+ @{ text = '-a'; tip = 'Filter by agent' }
126
+ @{ text = '--agent'; tip = 'Filter by agent' }
127
+ @{ text = '--all'; tip = 'Select all matching sessions' }
128
+ @{ text = '-y'; tip = 'Skip confirmation prompt' }
129
+ @{ text = '--yes'; tip = 'Skip confirmation prompt' }
114
130
  )
115
131
  } else {
116
132
  $completions = @(
@@ -125,7 +141,15 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
125
141
  $completions = @(
126
142
  @{ text = 'list'; tip = 'List running sessions' }
127
143
  @{ text = 'connect'; tip = 'Attach to a running session' }
128
- @{ text = 'kill'; tip = 'Kill a running session (omit id to kill all)' }
144
+ @{ text = 'kill'; tip = 'Kill running sessions' }
145
+ )
146
+ } elseif ($cmds.Count -ge 2 -and $cmds[1] -eq 'kill') {
147
+ $completions = @(
148
+ @{ text = '-a'; tip = 'Filter by agent' }
149
+ @{ text = '--agent'; tip = 'Filter by agent' }
150
+ @{ text = '--all'; tip = 'Select all matching sessions' }
151
+ @{ text = '-y'; tip = 'Skip confirmation prompt' }
152
+ @{ text = '--yes'; tip = 'Skip confirmation prompt' }
129
153
  )
130
154
  } else {
131
155
  $completions = @(
@@ -123,17 +123,24 @@ _atomic_session() {
123
123
  local -a subs=(
124
124
  'list:List running sessions'
125
125
  'connect:Attach to a running session'
126
- 'kill:Kill a running session (omit id to kill all)'
126
+ 'kill:Kill running sessions'
127
127
  )
128
128
  _describe 'subcommand' subs
129
129
  ;;
130
130
  subargs)
131
131
  case "\${words[1]}" in
132
- list|connect|kill)
132
+ list|connect)
133
133
  _arguments \\
134
134
  '*'{-a,--agent}'[Filter by agent]:agent:(claude opencode copilot)' \\
135
135
  '(-h --help)'{-h,--help}'[Show help]'
136
136
  ;;
137
+ kill)
138
+ _arguments \\
139
+ '*'{-a,--agent}'[Filter by agent]:agent:(claude opencode copilot)' \\
140
+ '--all[Select all matching sessions]' \\
141
+ '(-y --yes)'{-y,--yes}'[Skip confirmation prompt]' \\
142
+ '(-h --help)'{-h,--help}'[Show help]'
143
+ ;;
137
144
  esac
138
145
  ;;
139
146
  esac
@@ -44,6 +44,9 @@ export function CompactSwitcher({ selectedIndex }: CompactSwitcherProps) {
44
44
  const isSelected = i === selectedIndex;
45
45
  const icon = statusIcon(agent.status);
46
46
  const iconColor = statusColor(agent.status, theme);
47
+ const rowBackground = isSelected
48
+ ? lerpColor(theme.backgroundElement, theme.primary, 0.12)
49
+ : theme.backgroundElement;
47
50
  const duration =
48
51
  agent.startedAt !== null
49
52
  ? fmtDuration((agent.endedAt ?? Date.now()) - agent.startedAt)
@@ -56,15 +59,17 @@ export function CompactSwitcher({ selectedIndex }: CompactSwitcherProps) {
56
59
  flexDirection="row"
57
60
  paddingLeft={1}
58
61
  paddingRight={1}
59
- backgroundColor={isSelected ? lerpColor(theme.backgroundElement, theme.primary, 0.12) : theme.backgroundElement}
62
+ backgroundColor={rowBackground}
60
63
  >
61
64
  <text>
62
- <span fg={theme.textDim}>{String(i + 1).padStart(2)} </span>
63
- <span fg={iconColor}>{icon} </span>
64
- <span fg={isSelected ? theme.text : theme.textMuted}>{agent.name}</span>
65
+ <span fg={theme.textDim} bg={rowBackground}>{String(i + 1).padStart(2)} </span>
66
+ <span fg={iconColor} bg={rowBackground}>{icon} </span>
67
+ <span fg={isSelected ? theme.text : theme.textMuted} bg={rowBackground}>{agent.name}</span>
65
68
  </text>
66
69
  <box flexGrow={1} />
67
- <text fg={theme.textDim}>{duration}</text>
70
+ <text>
71
+ <span fg={theme.textDim} bg={rowBackground}>{duration}</span>
72
+ </text>
68
73
  </box>
69
74
  );
70
75
  })}
@@ -10,6 +10,7 @@ export interface ConnectorResult {
10
10
  width: number;
11
11
  height: number;
12
12
  color: string;
13
+ backgroundColor: string;
13
14
  }
14
15
 
15
16
  /** Fan-out connector: one parent branching down to one or more tree children. */
@@ -39,6 +40,7 @@ export function buildConnector(
39
40
  width: 1,
40
41
  height: numRows,
41
42
  color: theme.borderActive,
43
+ backgroundColor: theme.background,
42
44
  };
43
45
  }
44
46
 
@@ -85,6 +87,7 @@ export function buildConnector(
85
87
  width,
86
88
  height: numRows,
87
89
  color: theme.borderActive,
90
+ backgroundColor: theme.background,
88
91
  };
89
92
  }
90
93
 
@@ -152,5 +155,6 @@ export function buildMergeConnector(
152
155
  width,
153
156
  height: numRows,
154
157
  color: theme.borderActive,
158
+ backgroundColor: theme.background,
155
159
  };
156
160
  }
@@ -2,10 +2,12 @@
2
2
 
3
3
  import type { ConnectorResult } from "./connectors.ts";
4
4
 
5
- export function Edge({ text, col, row, width, height, color: edgeColor }: ConnectorResult) {
5
+ export function Edge({ text, col, row, width, height, color: edgeColor, backgroundColor }: ConnectorResult) {
6
6
  return (
7
- <box position="absolute" left={col} top={row} width={width} height={height}>
8
- <text fg={edgeColor}>{text}</text>
7
+ <box position="absolute" left={col} top={row} width={width} height={height} backgroundColor={backgroundColor}>
8
+ <text>
9
+ <span fg={edgeColor} bg={backgroundColor}>{text}</span>
10
+ </text>
9
11
  </box>
10
12
  );
11
13
  }
@@ -1,7 +1,6 @@
1
1
  // ─── Graph Theme ──────────────────────────────────
2
2
 
3
3
  import type { TerminalTheme } from "../runtime/theme.ts";
4
- import { lerpColor } from "./color-utils.ts";
5
4
 
6
5
  export interface GraphTheme {
7
6
  background: string;
@@ -24,13 +23,13 @@ export function deriveGraphTheme(t: TerminalTheme): GraphTheme {
24
23
  background: t.bg,
25
24
  backgroundElement: t.surface,
26
25
  text: t.text,
27
- textMuted: lerpColor(t.text, t.bg, 0.3),
26
+ textMuted: t.textMuted,
28
27
  textDim: t.dim,
29
28
  primary: t.accent,
30
29
  success: t.success,
31
30
  error: t.error,
32
31
  warning: t.warning,
33
- info: t.accent,
32
+ info: t.info,
34
33
  mauve: t.mauve,
35
34
  border: t.borderDim,
36
35
  borderActive: t.border,
@@ -9,11 +9,21 @@ import {
9
9
  TmuxSessionContext,
10
10
  } from "./orchestrator-panel-contexts.ts";
11
11
 
12
- function CountBadge({ color, icon, count }: { color: string; icon: string; count: number }) {
12
+ function CountBadge({
13
+ color,
14
+ icon,
15
+ count,
16
+ backgroundColor,
17
+ }: {
18
+ color: string;
19
+ icon: string;
20
+ count: number;
21
+ backgroundColor: string;
22
+ }) {
13
23
  if (count <= 0) return null;
14
24
  return (
15
25
  <text>
16
- <span fg={color}>{icon} {count}</span>
26
+ <span fg={color} bg={backgroundColor}>{icon} {count}</span>
17
27
  </text>
18
28
  );
19
29
  }
@@ -55,18 +65,20 @@ export function Header() {
55
65
 
56
66
  {tmuxSession ? (
57
67
  <box paddingLeft={1} alignItems="center">
58
- <text fg={theme.text}>
59
- <strong>{tmuxSession}</strong>
68
+ <text>
69
+ <span fg={theme.text} bg={theme.backgroundElement}>
70
+ <strong>{tmuxSession}</strong>
71
+ </span>
60
72
  </text>
61
73
  </box>
62
74
  ) : null}
63
75
 
64
76
  <box flexGrow={1} justifyContent="flex-end" flexDirection="row" gap={2}>
65
- <CountBadge color={theme.success} icon={"\u2713"} count={counts.complete} />
66
- <CountBadge color={theme.warning} icon={"\u25CF"} count={counts.running} />
67
- <CountBadge color={theme.info} icon={"?"} count={counts.awaiting_input} />
68
- <CountBadge color={theme.textDim} icon={"\u25CB"} count={counts.pending} />
69
- <CountBadge color={theme.error} icon={"\u2717"} count={counts.error} />
77
+ <CountBadge color={theme.success} backgroundColor={theme.backgroundElement} icon={"\u2713"} count={counts.complete} />
78
+ <CountBadge color={theme.warning} backgroundColor={theme.backgroundElement} icon={"\u25CF"} count={counts.running} />
79
+ <CountBadge color={theme.info} backgroundColor={theme.backgroundElement} icon={"?"} count={counts.awaiting_input} />
80
+ <CountBadge color={theme.textDim} backgroundColor={theme.backgroundElement} icon={"\u25CB"} count={counts.pending} />
81
+ <CountBadge color={theme.error} backgroundColor={theme.backgroundElement} icon={"\u2717"} count={counts.error} />
70
82
  </box>
71
83
  </box>
72
84
  );
@@ -28,12 +28,12 @@ export const NodeCard = React.memo(function NodeCard({
28
28
  if (isRunning) {
29
29
  const t = (Math.sin((pulsePhase / 32) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
30
30
  borderCol = focused
31
- ? lerpColor(theme.warning, "#ffffff", 0.2)
31
+ ? lerpColor(theme.warning, theme.text, 0.2)
32
32
  : lerpColor(theme.border, theme.warning, t);
33
33
  } else if (isAwaitingInput) {
34
34
  const t = (Math.sin((pulsePhase / 32) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
35
35
  borderCol = focused
36
- ? lerpColor(theme.info, "#ffffff", 0.2)
36
+ ? lerpColor(theme.info, theme.text, 0.2)
37
37
  : lerpColor(theme.border, theme.info, t);
38
38
  } else if (isPending) {
39
39
  borderCol = focused ? sc : theme.borderActive;
@@ -41,8 +41,8 @@ export const NodeCard = React.memo(function NodeCard({
41
41
  borderCol = sc;
42
42
  }
43
43
 
44
- // Background: focused nodes get a subtle status-colored tint
45
- const bgCol = focused ? lerpColor(theme.background, sc, 0.12) : "transparent";
44
+ // Keep the card interior aligned with the graph canvas; status color belongs to the border/text.
45
+ const bgCol = theme.background;
46
46
 
47
47
  // Duration computed live from start/end timestamps
48
48
  const durCol = isPending ? theme.textDim : sc;
@@ -68,15 +68,21 @@ export const NodeCard = React.memo(function NodeCard({
68
68
  titleAlignment="center"
69
69
  >
70
70
  <box alignItems="center">
71
- <text fg={durCol}>{duration}</text>
71
+ <text>
72
+ <span fg={durCol} bg={bgCol}>{duration}</span>
73
+ </text>
72
74
  </box>
73
75
  {isAwaitingInput && (
74
76
  <>
75
77
  <box alignItems="center">
76
- <text fg={theme.info}>waiting for response</text>
78
+ <text>
79
+ <span fg={theme.info} bg={bgCol}>waiting for response</span>
80
+ </text>
77
81
  </box>
78
82
  <box alignItems="center">
79
- <text fg={theme.textDim}>↵ enter to respond</text>
83
+ <text>
84
+ <span fg={theme.textDim} bg={bgCol}>↵ enter to respond</span>
85
+ </text>
80
86
  </box>
81
87
  </>
82
88
  )}