@bastani/atomic 0.5.17-0 → 0.5.18-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 (26) hide show
  1. package/README.md +14 -1
  2. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +50 -54
  3. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  4. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +17 -36
  5. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  6. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -1
  7. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +64 -44
  8. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -1
  9. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts +43 -0
  10. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.d.ts.map +1 -0
  11. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +17 -39
  12. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  13. package/package.json +1 -1
  14. package/src/cli.ts +21 -2
  15. package/src/commands/cli/session.test.ts +223 -0
  16. package/src/commands/cli/session.ts +117 -1
  17. package/src/completions/bash.ts +3 -3
  18. package/src/completions/fish.ts +13 -7
  19. package/src/completions/powershell.ts +3 -0
  20. package/src/completions/zsh.ts +2 -1
  21. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +260 -157
  22. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +224 -125
  23. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +2 -2
  24. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +428 -469
  25. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scratch.ts +115 -0
  26. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +249 -137
@@ -6,6 +6,7 @@ import {
6
6
  sessionListCommand,
7
7
  sessionConnectCommand,
8
8
  sessionPickerCommand,
9
+ sessionKillCommand,
9
10
  } from "./session.ts";
10
11
  import type { SessionDeps } from "./session.ts";
11
12
  import type { TmuxSession } from "../../sdk/runtime/tmux.ts";
@@ -287,6 +288,9 @@ const tmuxMocks = {
287
288
  spawnMuxAttach: mock(() => ({ exited: Promise.resolve(0) }) as never),
288
289
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
289
290
  select: mock<(...args: any[]) => Promise<string | symbol>>(() => Promise.resolve("my-session")),
291
+ killSession: mock<(name: string) => void>(() => {}),
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ confirm: mock<(...args: any[]) => Promise<boolean | symbol>>(() => Promise.resolve(true)),
290
294
  isCancel: ((v: unknown) => typeof v === "symbol") as SessionDeps["isCancel"],
291
295
  };
292
296
 
@@ -305,6 +309,8 @@ function resetTmuxMocks(): void {
305
309
  tmuxMocks.detachAndAttachAtomic.mockReset();
306
310
  tmuxMocks.spawnMuxAttach.mockReset().mockReturnValue({ exited: Promise.resolve(0) } as never);
307
311
  tmuxMocks.select.mockReset().mockResolvedValue("my-session");
312
+ tmuxMocks.killSession.mockReset();
313
+ tmuxMocks.confirm.mockReset().mockResolvedValue(true);
308
314
  }
309
315
 
310
316
  // ─── sessionListCommand ─────────────────────────────────────────────────
@@ -489,3 +495,220 @@ describe("sessionPickerCommand", () => {
489
495
  expect(tmuxMocks.spawnMuxAttach).not.toHaveBeenCalled();
490
496
  });
491
497
  });
498
+
499
+ // ─── sessionKillCommand ──────────────────────────────────────────────────
500
+
501
+ describe("sessionKillCommand", () => {
502
+ beforeEach(resetTmuxMocks);
503
+
504
+ // (a) tmux not installed → stdout "no sessions running" + "tmux is not installed", return 0
505
+ test("returns 0 with 'no sessions' when tmux is not installed", async () => {
506
+ tmuxMocks.isTmuxInstalled.mockReturnValue(false);
507
+ const chunks: string[] = [];
508
+ const origWrite = process.stdout.write;
509
+ process.stdout.write = ((c: string) => { chunks.push(c); return true; }) as typeof process.stdout.write;
510
+ try {
511
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps());
512
+ expect(code).toBe(0);
513
+ const output = chunks.join("");
514
+ expect(output).toContain("no sessions running");
515
+ expect(output).toContain("tmux is not installed");
516
+ } finally {
517
+ process.stdout.write = origWrite;
518
+ }
519
+ });
520
+
521
+ // (b) named id, session not in scope → stderr error, return 1
522
+ test("returns 1 with error when named session does not exist", async () => {
523
+ // listSessions returns empty, so the target won't be found
524
+ tmuxMocks.listSessions.mockReturnValue([]);
525
+ const chunks: string[] = [];
526
+ const origWrite = process.stderr.write;
527
+ process.stderr.write = ((c: string) => { chunks.push(c); return true; }) as typeof process.stderr.write;
528
+ try {
529
+ const code = await sessionKillCommand("ghost-session", [], "all", makeDeps());
530
+ expect(code).toBe(1);
531
+ const output = chunks.join("");
532
+ expect(output).toContain("ghost-session");
533
+ } finally {
534
+ process.stderr.write = origWrite;
535
+ }
536
+ });
537
+
538
+ // (c) named id, session exists but is out of scope → return 1
539
+ test("returns 1 when named session exists but is out of scope", async () => {
540
+ const now = new Date().toISOString();
541
+ // listSessions has a chat session, but we request scope=workflow
542
+ tmuxMocks.listSessions.mockReturnValue([
543
+ { name: "atomic-chat-claude-aaa11111", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
544
+ ]);
545
+ const chunks: string[] = [];
546
+ const origWrite = process.stderr.write;
547
+ process.stderr.write = ((c: string) => { chunks.push(c); return true; }) as typeof process.stderr.write;
548
+ try {
549
+ const code = await sessionKillCommand("atomic-chat-claude-aaa11111", [], "workflow", makeDeps());
550
+ expect(code).toBe(1);
551
+ const output = chunks.join("");
552
+ expect(output).toContain("atomic-chat-claude-aaa11111");
553
+ } finally {
554
+ process.stderr.write = origWrite;
555
+ }
556
+ });
557
+
558
+ // (d) named id found, user confirms → killSession called, return 0
559
+ test("prompts and calls killSession on confirm for named session", async () => {
560
+ const now = new Date().toISOString();
561
+ tmuxMocks.listSessions.mockReturnValue([
562
+ { name: "target-session", windows: 1, created: now, attached: false, type: "chat" as const },
563
+ ]);
564
+ tmuxMocks.confirm.mockResolvedValue(true);
565
+ const chunks: string[] = [];
566
+ const origWrite = process.stdout.write;
567
+ process.stdout.write = ((c: string) => { chunks.push(c); return true; }) as typeof process.stdout.write;
568
+ try {
569
+ const code = await sessionKillCommand("target-session", [], "all", makeDeps());
570
+ expect(code).toBe(0);
571
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(1);
572
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("target-session");
573
+ } finally {
574
+ process.stdout.write = origWrite;
575
+ }
576
+ });
577
+
578
+ // (e) named id found, user declines → killSession NOT called, return 0
579
+ test("does NOT call killSession when user declines named kill", async () => {
580
+ const now = new Date().toISOString();
581
+ tmuxMocks.listSessions.mockReturnValue([
582
+ { name: "target-session", windows: 1, created: now, attached: false, type: "chat" as const },
583
+ ]);
584
+ tmuxMocks.confirm.mockResolvedValue(false);
585
+ const origWrite = process.stdout.write;
586
+ process.stdout.write = (() => true) as typeof process.stdout.write;
587
+ try {
588
+ const code = await sessionKillCommand("target-session", [], "all", makeDeps());
589
+ expect(code).toBe(0);
590
+ expect(tmuxMocks.killSession).not.toHaveBeenCalled();
591
+ } finally {
592
+ process.stdout.write = origWrite;
593
+ }
594
+ });
595
+
596
+ // (f) named id found, user cancels (symbol) → killSession NOT called, return 0
597
+ test("does NOT call killSession when user cancels named kill", async () => {
598
+ const now = new Date().toISOString();
599
+ tmuxMocks.listSessions.mockReturnValue([
600
+ { name: "target-session", windows: 1, created: now, attached: false, type: "chat" as const },
601
+ ]);
602
+ tmuxMocks.confirm.mockResolvedValue(Symbol("cancel"));
603
+ const origWrite = process.stdout.write;
604
+ process.stdout.write = (() => true) as typeof process.stdout.write;
605
+ try {
606
+ const code = await sessionKillCommand("target-session", [], "all", makeDeps());
607
+ expect(code).toBe(0);
608
+ expect(tmuxMocks.killSession).not.toHaveBeenCalled();
609
+ } finally {
610
+ process.stdout.write = origWrite;
611
+ }
612
+ });
613
+
614
+ // (g) omitted id, no sessions → empty state on stdout, return 0, confirm NOT called
615
+ test("omitted id with no sessions prints empty state and returns 0 without prompt", async () => {
616
+ tmuxMocks.listSessions.mockReturnValue([]);
617
+ const chunks: string[] = [];
618
+ const origWrite = process.stdout.write;
619
+ process.stdout.write = ((c: string) => { chunks.push(c); return true; }) as typeof process.stdout.write;
620
+ try {
621
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps());
622
+ expect(code).toBe(0);
623
+ const output = chunks.join("");
624
+ expect(output).toContain("no sessions running");
625
+ expect(tmuxMocks.confirm).not.toHaveBeenCalled();
626
+ } finally {
627
+ process.stdout.write = origWrite;
628
+ }
629
+ });
630
+
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 () => {
633
+ const now = new Date().toISOString();
634
+ tmuxMocks.listSessions.mockReturnValue([
635
+ { name: "session-a", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
636
+ { name: "session-b", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
637
+ { name: "session-c", windows: 1, created: now, attached: false, type: "chat" as const, agent: "copilot" },
638
+ ]);
639
+ tmuxMocks.confirm.mockResolvedValue(true);
640
+ const origWrite = process.stdout.write;
641
+ process.stdout.write = (() => true) as typeof process.stdout.write;
642
+ try {
643
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps());
644
+ expect(code).toBe(0);
645
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(3);
646
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-a");
647
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-b");
648
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("session-c");
649
+ } finally {
650
+ process.stdout.write = origWrite;
651
+ }
652
+ });
653
+
654
+ // (i) scope=chat, only chat sessions killed when id omitted
655
+ test("scope=chat only kills chat sessions when id omitted", async () => {
656
+ const now = new Date().toISOString();
657
+ tmuxMocks.listSessions.mockReturnValue([
658
+ { name: "chat-session", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
659
+ { name: "wf-session", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
660
+ ]);
661
+ tmuxMocks.confirm.mockResolvedValue(true);
662
+ const origWrite = process.stdout.write;
663
+ process.stdout.write = (() => true) as typeof process.stdout.write;
664
+ try {
665
+ const code = await sessionKillCommand(undefined, [], "chat", makeDeps());
666
+ expect(code).toBe(0);
667
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(1);
668
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("chat-session");
669
+ expect(tmuxMocks.killSession).not.toHaveBeenCalledWith("wf-session");
670
+ } finally {
671
+ process.stdout.write = origWrite;
672
+ }
673
+ });
674
+
675
+ // (j) scope=workflow, only workflow sessions killed when id omitted
676
+ test("scope=workflow only kills workflow sessions when id omitted", async () => {
677
+ const now = new Date().toISOString();
678
+ tmuxMocks.listSessions.mockReturnValue([
679
+ { name: "chat-session", windows: 1, created: now, attached: false, type: "chat" as const, agent: "claude" },
680
+ { name: "wf-session", windows: 1, created: now, attached: false, type: "workflow" as const, agent: "opencode" },
681
+ ]);
682
+ tmuxMocks.confirm.mockResolvedValue(true);
683
+ const origWrite = process.stdout.write;
684
+ process.stdout.write = (() => true) as typeof process.stdout.write;
685
+ try {
686
+ const code = await sessionKillCommand(undefined, [], "workflow", makeDeps());
687
+ expect(code).toBe(0);
688
+ expect(tmuxMocks.killSession).toHaveBeenCalledTimes(1);
689
+ expect(tmuxMocks.killSession).toHaveBeenCalledWith("wf-session");
690
+ expect(tmuxMocks.killSession).not.toHaveBeenCalledWith("chat-session");
691
+ } finally {
692
+ process.stdout.write = origWrite;
693
+ }
694
+ });
695
+
696
+ // (k) user declines kill-all → killSession NOT called
697
+ test("does NOT kill any sessions when user declines kill-all", async () => {
698
+ const now = new Date().toISOString();
699
+ tmuxMocks.listSessions.mockReturnValue([
700
+ { name: "session-x", windows: 1, created: now, attached: false, type: "chat" as const },
701
+ { name: "session-y", windows: 1, created: now, attached: false, type: "workflow" as const },
702
+ ]);
703
+ tmuxMocks.confirm.mockResolvedValue(false);
704
+ const origWrite = process.stdout.write;
705
+ process.stdout.write = (() => true) as typeof process.stdout.write;
706
+ try {
707
+ const code = await sessionKillCommand(undefined, [], "all", makeDeps());
708
+ expect(code).toBe(0);
709
+ expect(tmuxMocks.killSession).not.toHaveBeenCalled();
710
+ } finally {
711
+ process.stdout.write = origWrite;
712
+ }
713
+ });
714
+ });
@@ -7,7 +7,7 @@
7
7
  * tmux directly.
8
8
  */
9
9
 
10
- import { select, isCancel, cancel } from "@clack/prompts";
10
+ import { select, confirm, isCancel, cancel } from "@clack/prompts";
11
11
  import { createPainter, type PaletteKey } from "../../theme/colors.ts";
12
12
  import {
13
13
  listSessions as _listSessions,
@@ -18,6 +18,7 @@ import {
18
18
  switchClient as _switchClient,
19
19
  spawnMuxAttach as _spawnMuxAttach,
20
20
  detachAndAttachAtomic as _detachAndAttachAtomic,
21
+ killSession as _killSession,
21
22
  SOCKET_NAME,
22
23
  } from "../../sdk/workflows/index.ts";
23
24
  import type { TmuxSession, SessionType } from "../../sdk/runtime/tmux.ts";
@@ -36,8 +37,11 @@ export interface SessionDeps {
36
37
  switchClient: (name: string) => void;
37
38
  spawnMuxAttach: (name: string) => Subprocess;
38
39
  detachAndAttachAtomic: (name: string) => void;
40
+ killSession: (name: string) => void;
39
41
  /** Prompt function for the session picker — defaults to @clack/prompts select. */
40
42
  select: typeof select;
43
+ /** Prompt function for yes/no confirmations — defaults to @clack/prompts confirm. */
44
+ confirm: typeof confirm;
41
45
  isCancel: typeof isCancel;
42
46
  }
43
47
 
@@ -51,7 +55,9 @@ const defaultDeps: SessionDeps = {
51
55
  switchClient: _switchClient,
52
56
  spawnMuxAttach: _spawnMuxAttach,
53
57
  detachAndAttachAtomic: _detachAndAttachAtomic,
58
+ killSession: _killSession,
54
59
  select,
60
+ confirm,
55
61
  isCancel,
56
62
  };
57
63
 
@@ -263,3 +269,113 @@ export async function sessionPickerCommand(agents: string[] = [], scope: Session
263
269
 
264
270
  return sessionConnectCommand(selected as string, deps);
265
271
  }
272
+
273
+ // ─── Session kill command ────────────────────────────────────────────────────
274
+
275
+ /**
276
+ * Kill a named session or all sessions matching the given scope and agents.
277
+ *
278
+ * - If `sessionId` is provided: confirm and kill that one session.
279
+ * - If `sessionId` is omitted: confirm and kill all sessions in scope.
280
+ */
281
+ export async function sessionKillCommand(
282
+ sessionId: string | undefined,
283
+ agents: string[] = [],
284
+ scope: SessionScope = "all",
285
+ deps: SessionDeps = defaultDeps,
286
+ ): Promise<number> {
287
+ const paint = createPainter();
288
+
289
+ if (!deps.isTmuxInstalled()) {
290
+ process.stdout.write(
291
+ "\n " + paint("text", "no sessions running", { bold: true }) +
292
+ "\n\n " + paint("dim", "tmux is not installed") + "\n\n",
293
+ );
294
+ return 0;
295
+ }
296
+
297
+ // ── Named kill path ───────────────────────────────────────────────────────
298
+ if (sessionId !== undefined) {
299
+ const inScope = filterByScope(deps.listSessions(), scope);
300
+ const target = inScope.find((s) => s.name === sessionId);
301
+
302
+ if (!target) {
303
+ const scopeLabel = scope === "all" ? "" : ` in ${scope} scope`;
304
+ process.stderr.write(
305
+ paint("error", `Error: session '${sessionId}' not found${scopeLabel}.`) + "\n",
306
+ );
307
+ if (inScope.length > 0) {
308
+ process.stderr.write(
309
+ "\n" + paint("dim", "Available sessions:") + "\n",
310
+ );
311
+ for (const s of inScope) {
312
+ process.stderr.write(
313
+ " " + paint("dim", "○") + " " + paint("text", s.name) + "\n",
314
+ );
315
+ }
316
+ process.stderr.write("\n");
317
+ }
318
+ return 1;
319
+ }
320
+
321
+ const answer = await deps.confirm({
322
+ message: `Kill session '${sessionId}'?`,
323
+ initialValue: false,
324
+ });
325
+
326
+ if (deps.isCancel(answer)) {
327
+ cancel("Cancelled.");
328
+ return 0;
329
+ }
330
+
331
+ if (answer === true) {
332
+ deps.killSession(sessionId);
333
+ process.stdout.write(
334
+ "\n " + paint("success", "✓") + " killed " + paint("text", sessionId) + "\n\n",
335
+ );
336
+ return 0;
337
+ }
338
+
339
+ // answer === false
340
+ process.stdout.write(
341
+ "\n " + paint("dim", "Cancelled.") + "\n\n",
342
+ );
343
+ return 0;
344
+ }
345
+
346
+ // ── Kill-all path ─────────────────────────────────────────────────────────
347
+ const targets = filterByAgent(filterByScope(deps.listSessions(), scope), agents);
348
+
349
+ if (targets.length === 0) {
350
+ process.stdout.write(renderSessionList([]));
351
+ return 0;
352
+ }
353
+
354
+ const noun = targets.length === 1 ? "session" : "sessions";
355
+ const scopePrefix = scope === "all" ? "" : `${scope} `;
356
+ const answer = await deps.confirm({
357
+ message: `Kill all ${targets.length} ${scopePrefix}${noun}?`,
358
+ initialValue: false,
359
+ });
360
+
361
+ if (deps.isCancel(answer)) {
362
+ cancel("Cancelled.");
363
+ return 0;
364
+ }
365
+
366
+ if (answer === true) {
367
+ for (const t of targets) {
368
+ deps.killSession(t.name);
369
+ }
370
+ process.stdout.write(
371
+ "\n " + paint("success", "✓") + " killed " + paint("text", String(targets.length)) + " " + paint("dim", noun) + "\n\n",
372
+ );
373
+ return 0;
374
+ }
375
+
376
+ // answer === false
377
+ process.stdout.write(
378
+ "\n " + paint("dim", "Cancelled.") + "\n\n",
379
+ );
380
+ return 0;
381
+ }
@@ -58,7 +58,7 @@ _atomic_completions() {
58
58
  COMPREPLY=( $(compgen -W "session -a --agent -h --help" -- "$cur") )
59
59
  elif [[ "$cmd2" == "session" ]]; then
60
60
  if [[ -z "$cmd3" ]]; then
61
- COMPREPLY=( $(compgen -W "list connect -h --help" -- "$cur") )
61
+ COMPREPLY=( $(compgen -W "list connect kill -h --help" -- "$cur") )
62
62
  else
63
63
  COMPREPLY=( $(compgen -W "-a --agent -h --help" -- "$cur") )
64
64
  fi
@@ -71,7 +71,7 @@ _atomic_completions() {
71
71
  COMPREPLY=( $(compgen -W "-a --agent -h --help" -- "$cur") )
72
72
  elif [[ "$cmd2" == "session" ]]; then
73
73
  if [[ -z "$cmd3" ]]; then
74
- COMPREPLY=( $(compgen -W "list connect -h --help" -- "$cur") )
74
+ COMPREPLY=( $(compgen -W "list connect kill -h --help" -- "$cur") )
75
75
  else
76
76
  COMPREPLY=( $(compgen -W "-a --agent -h --help" -- "$cur") )
77
77
  fi
@@ -79,7 +79,7 @@ _atomic_completions() {
79
79
  ;;
80
80
  session)
81
81
  if [[ -z "$cmd2" ]]; then
82
- COMPREPLY=( $(compgen -W "list connect -h --help" -- "$cur") )
82
+ COMPREPLY=( $(compgen -W "list connect kill -h --help" -- "$cur") )
83
83
  else
84
84
  COMPREPLY=( $(compgen -W "-a --agent -h --help" -- "$cur") )
85
85
  fi
@@ -56,7 +56,7 @@ end
56
56
 
57
57
  function __atomic_needs_subcmd_of
58
58
  __atomic_using_cmd $argv
59
- and not __fish_seen_subcommand_from list connect set
59
+ and not __fish_seen_subcommand_from list connect kill set
60
60
  end
61
61
 
62
62
  # ── Global options ──────────────────────────────────────────────────────────
@@ -86,10 +86,12 @@ complete -c atomic -n '__atomic_using_cmd chat; and not __fish_seen_subcommand_f
86
86
  complete -c atomic -n '__atomic_using_cmd chat; and not __fish_seen_subcommand_from session' -a session -d 'Manage running chat sessions'
87
87
 
88
88
  # chat session
89
- complete -c atomic -n '__atomic_using_cmd chat session; and not __fish_seen_subcommand_from list connect' -a list -d 'List running sessions'
90
- complete -c atomic -n '__atomic_using_cmd chat session; and not __fish_seen_subcommand_from list connect' -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 list -d 'List running sessions'
90
+ 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'
91
+ 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)'
91
92
  complete -c atomic -n '__atomic_using_cmd chat session list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
92
93
  complete -c atomic -n '__atomic_using_cmd chat session connect' -s a -l agent -d 'Filter by agent' -r -a "$agents"
94
+ complete -c atomic -n '__atomic_using_cmd chat session kill' -s a -l agent -d 'Filter by agent' -r -a "$agents"
93
95
 
94
96
  # ── workflow ────────────────────────────────────────────────────────────────
95
97
 
@@ -102,17 +104,21 @@ complete -c atomic -n '__atomic_using_cmd workflow; and not __fish_seen_subcomma
102
104
  complete -c atomic -n '__atomic_using_cmd workflow list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
103
105
 
104
106
  # workflow session
105
- complete -c atomic -n '__atomic_using_cmd workflow session; and not __fish_seen_subcommand_from list connect' -a list -d 'List running sessions'
106
- complete -c atomic -n '__atomic_using_cmd workflow session; and not __fish_seen_subcommand_from list connect' -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 list -d 'List running sessions'
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'
109
+ 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)'
107
110
  complete -c atomic -n '__atomic_using_cmd workflow session list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
108
111
  complete -c atomic -n '__atomic_using_cmd workflow session connect' -s a -l agent -d 'Filter by agent' -r -a "$agents"
112
+ complete -c atomic -n '__atomic_using_cmd workflow session kill' -s a -l agent -d 'Filter by agent' -r -a "$agents"
109
113
 
110
114
  # ── session (top-level) ────────────────────────────────────────────────────
111
115
 
112
- complete -c atomic -n '__atomic_using_cmd session; and not __fish_seen_subcommand_from list connect' -a list -d 'List running sessions'
113
- complete -c atomic -n '__atomic_using_cmd session; and not __fish_seen_subcommand_from list connect' -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 list -d 'List running sessions'
117
+ 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'
118
+ 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)'
114
119
  complete -c atomic -n '__atomic_using_cmd session list' -s a -l agent -d 'Filter by agent' -r -a "$agents"
115
120
  complete -c atomic -n '__atomic_using_cmd session connect' -s a -l agent -d 'Filter by agent' -r -a "$agents"
121
+ complete -c atomic -n '__atomic_using_cmd session kill' -s a -l agent -d 'Filter by agent' -r -a "$agents"
116
122
 
117
123
  # ── config ──────────────────────────────────────────────────────────────────
118
124
 
@@ -96,6 +96,7 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
96
96
  $completions = @(
97
97
  @{ text = 'list'; tip = 'List running sessions' }
98
98
  @{ text = 'connect'; tip = 'Attach to a running session' }
99
+ @{ text = 'kill'; tip = 'Kill a running session (omit id to kill all)' }
99
100
  )
100
101
  } else {
101
102
  $completions = @(
@@ -125,6 +126,7 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
125
126
  $completions = @(
126
127
  @{ text = 'list'; tip = 'List running sessions' }
127
128
  @{ text = 'connect'; tip = 'Attach to a running session' }
129
+ @{ text = 'kill'; tip = 'Kill a running session (omit id to kill all)' }
128
130
  )
129
131
  } else {
130
132
  $completions = @(
@@ -139,6 +141,7 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
139
141
  $completions = @(
140
142
  @{ text = 'list'; tip = 'List running sessions' }
141
143
  @{ text = 'connect'; tip = 'Attach to a running session' }
144
+ @{ text = 'kill'; tip = 'Kill a running session (omit id to kill all)' }
142
145
  )
143
146
  } else {
144
147
  $completions = @(
@@ -125,12 +125,13 @@ _atomic_session() {
125
125
  local -a subs=(
126
126
  'list:List running sessions'
127
127
  'connect:Attach to a running session'
128
+ 'kill:Kill a running session (omit id to kill all)'
128
129
  )
129
130
  _describe 'subcommand' subs
130
131
  ;;
131
132
  subargs)
132
133
  case "\${words[1]}" in
133
- list|connect)
134
+ list|connect|kill)
134
135
  _arguments \\
135
136
  '*'{-a,--agent}'[Filter by agent]:agent:(claude opencode copilot)' \\
136
137
  '(-h --help)'{-h,--help}'[Show help]'