@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.
- package/README.md +22 -16
- package/dist/sdk/components/compact-switcher.d.ts.map +1 -1
- package/dist/sdk/components/connectors.d.ts +1 -0
- package/dist/sdk/components/connectors.d.ts.map +1 -1
- package/dist/sdk/components/edge.d.ts +1 -1
- package/dist/sdk/components/edge.d.ts.map +1 -1
- package/dist/sdk/components/graph-theme.d.ts.map +1 -1
- package/dist/sdk/components/header.d.ts.map +1 -1
- package/dist/sdk/components/node-card.d.ts.map +1 -1
- package/dist/sdk/components/orchestrator-panel.d.ts +7 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -1
- package/dist/sdk/components/renderer-background.d.ts +9 -0
- package/dist/sdk/components/renderer-background.d.ts.map +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
- package/dist/sdk/components/statusline.d.ts.map +1 -1
- package/dist/sdk/components/tui-diagnostics.d.ts +56 -0
- package/dist/sdk/components/tui-diagnostics.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +2 -1
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +3 -2
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/theme.d.ts +4 -0
- package/dist/sdk/runtime/theme.d.ts.map +1 -1
- package/dist/theme/colors.d.ts +2 -0
- package/dist/theme/colors.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +3 -3
- package/src/commands/cli/chat/index.ts +10 -4
- package/src/commands/cli/management-commands.ts +4 -3
- package/src/commands/cli/session.test.ts +79 -6
- package/src/commands/cli/session.ts +65 -9
- package/src/completions/fish.ts +9 -3
- package/src/completions/powershell.ts +27 -3
- package/src/completions/zsh.ts +9 -2
- package/src/sdk/components/compact-switcher.tsx +10 -5
- package/src/sdk/components/connectors.ts +4 -0
- package/src/sdk/components/edge.tsx +5 -3
- package/src/sdk/components/graph-theme.ts +2 -3
- package/src/sdk/components/header.tsx +21 -9
- package/src/sdk/components/node-card.tsx +13 -7
- package/src/sdk/components/orchestrator-panel.tsx +47 -2
- package/src/sdk/components/renderer-background.ts +49 -0
- package/src/sdk/components/session-graph-panel.tsx +9 -2
- package/src/sdk/components/statusline.tsx +26 -22
- package/src/sdk/components/tui-diagnostics.ts +273 -0
- package/src/sdk/components/workflow-picker-panel.tsx +33 -22
- package/src/sdk/providers/copilot.ts +10 -4
- package/src/sdk/runtime/executor.ts +28 -1
- package/src/sdk/runtime/theme.ts +28 -36
- package/src/services/system/install-ui.ts +16 -17
- package/src/theme/colors.ts +14 -9
- 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
|
|
632
|
-
test("omitted id prompts and kills
|
|
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.
|
|
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
|
|
740
|
-
test("yes flag skips the prompt
|
|
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
|
|
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:
|
|
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
|
-
// ──
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/src/completions/fish.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = @(
|
package/src/completions/zsh.ts
CHANGED
|
@@ -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
|
|
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
|
|
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={
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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({
|
|
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
|
|
59
|
-
<
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
45
|
-
const bgCol =
|
|
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
|
|
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
|
|
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
|
|
83
|
+
<text>
|
|
84
|
+
<span fg={theme.textDim} bg={bgCol}>↵ enter to respond</span>
|
|
85
|
+
</text>
|
|
80
86
|
</box>
|
|
81
87
|
</>
|
|
82
88
|
)}
|