@bastani/atomic 0.6.6 → 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/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/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/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
|
@@ -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
|
)}
|
|
@@ -14,20 +14,39 @@ import { StoreContext, ThemeContext, TmuxSessionContext } from "./orchestrator-p
|
|
|
14
14
|
import type { PanelSession, PanelOptions, SessionData } from "./orchestrator-panel-types.ts";
|
|
15
15
|
import { SessionGraphPanel } from "./session-graph-panel.tsx";
|
|
16
16
|
import { ErrorBoundary } from "./error-boundary.tsx";
|
|
17
|
+
import {
|
|
18
|
+
requestRendererBackgroundRepaint,
|
|
19
|
+
resetRendererTerminalBackground,
|
|
20
|
+
setRendererBackground,
|
|
21
|
+
} from "./renderer-background.ts";
|
|
22
|
+
import { createTuiDiagnostics, type TuiDiagnostics } from "./tui-diagnostics.ts";
|
|
17
23
|
|
|
18
24
|
export class OrchestratorPanel {
|
|
19
25
|
private store: PanelStore;
|
|
20
26
|
private renderer: CliRenderer;
|
|
21
27
|
private destroyed = false;
|
|
28
|
+
private terminalBackgroundSynced: boolean;
|
|
29
|
+
private diagnostics: TuiDiagnostics | null = null;
|
|
30
|
+
private unsubscribeDiagnostics: (() => void) | null = null;
|
|
22
31
|
|
|
23
32
|
private constructor(
|
|
24
33
|
renderer: CliRenderer,
|
|
25
34
|
store: PanelStore,
|
|
26
35
|
graphTheme: GraphTheme,
|
|
27
36
|
tmuxSession: string,
|
|
37
|
+
terminalBackgroundSynced: boolean,
|
|
28
38
|
) {
|
|
29
39
|
this.renderer = renderer;
|
|
30
40
|
this.store = store;
|
|
41
|
+
this.terminalBackgroundSynced = terminalBackgroundSynced;
|
|
42
|
+
this.diagnostics = createTuiDiagnostics({
|
|
43
|
+
renderer,
|
|
44
|
+
graphTheme,
|
|
45
|
+
getSnapshot: () => this.getDiagnosticSnapshot(),
|
|
46
|
+
});
|
|
47
|
+
this.unsubscribeDiagnostics = this.diagnostics
|
|
48
|
+
? store.subscribe(() => this.diagnostics?.capture("store-update"))
|
|
49
|
+
: null;
|
|
31
50
|
|
|
32
51
|
createRoot(renderer).render(
|
|
33
52
|
<StoreContext.Provider value={store}>
|
|
@@ -56,6 +75,8 @@ export class OrchestratorPanel {
|
|
|
56
75
|
</ThemeContext.Provider>
|
|
57
76
|
</StoreContext.Provider>,
|
|
58
77
|
);
|
|
78
|
+
requestRendererBackgroundRepaint(this.renderer);
|
|
79
|
+
this.diagnostics?.capture("post-mount");
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
/**
|
|
@@ -69,18 +90,20 @@ export class OrchestratorPanel {
|
|
|
69
90
|
exitOnCtrlC: false,
|
|
70
91
|
exitSignals: ["SIGTERM", "SIGQUIT", "SIGABRT", "SIGHUP", "SIGPIPE", "SIGBUS", "SIGFPE"],
|
|
71
92
|
});
|
|
72
|
-
return OrchestratorPanel.createWithRenderer(renderer, options);
|
|
93
|
+
return OrchestratorPanel.createWithRenderer(renderer, options, { syncTerminalBackground: true });
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
/** Create with an externally-provided renderer (e.g. a test renderer). */
|
|
76
97
|
static createWithRenderer(
|
|
77
98
|
renderer: CliRenderer,
|
|
78
99
|
options: PanelOptions,
|
|
100
|
+
{ syncTerminalBackground = false }: { syncTerminalBackground?: boolean } = {},
|
|
79
101
|
): OrchestratorPanel {
|
|
80
102
|
const termTheme = resolveTheme(renderer.themeMode);
|
|
103
|
+
setRendererBackground(renderer, termTheme.bg, { syncTerminalDefault: syncTerminalBackground });
|
|
81
104
|
const graphTheme = deriveGraphTheme(termTheme);
|
|
82
105
|
const store = new PanelStore();
|
|
83
|
-
return new OrchestratorPanel(renderer, store, graphTheme, options.tmuxSession);
|
|
106
|
+
return new OrchestratorPanel(renderer, store, graphTheme, options.tmuxSession, syncTerminalBackground);
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
/**
|
|
@@ -175,7 +198,15 @@ export class OrchestratorPanel {
|
|
|
175
198
|
destroy(): void {
|
|
176
199
|
if (this.destroyed) return;
|
|
177
200
|
this.destroyed = true;
|
|
201
|
+
this.unsubscribeDiagnostics?.();
|
|
202
|
+
this.unsubscribeDiagnostics = null;
|
|
203
|
+
this.diagnostics?.capture("destroy");
|
|
204
|
+
this.diagnostics?.dispose();
|
|
205
|
+
this.diagnostics = null;
|
|
178
206
|
try {
|
|
207
|
+
if (this.terminalBackgroundSynced) {
|
|
208
|
+
resetRendererTerminalBackground(this.renderer);
|
|
209
|
+
}
|
|
179
210
|
this.renderer.destroy();
|
|
180
211
|
} catch {}
|
|
181
212
|
}
|
|
@@ -214,4 +245,18 @@ export class OrchestratorPanel {
|
|
|
214
245
|
sessions: this.store.sessions,
|
|
215
246
|
};
|
|
216
247
|
}
|
|
248
|
+
|
|
249
|
+
private getDiagnosticSnapshot() {
|
|
250
|
+
return {
|
|
251
|
+
workflowName: this.store.workflowName,
|
|
252
|
+
agent: this.store.agent,
|
|
253
|
+
prompt: this.store.prompt,
|
|
254
|
+
fatalError: this.store.fatalError,
|
|
255
|
+
completionReached: this.store.completionReached,
|
|
256
|
+
sessions: this.store.sessions,
|
|
257
|
+
backgroundTaskCount: this.store.backgroundTaskCount,
|
|
258
|
+
viewMode: this.store.viewMode,
|
|
259
|
+
activeAgentId: this.store.activeAgentId,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
217
262
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
export function setRendererBackground(
|
|
4
|
+
renderer: CliRenderer,
|
|
5
|
+
color: string,
|
|
6
|
+
{ syncTerminalDefault = false }: { syncTerminalDefault?: boolean } = {},
|
|
7
|
+
): void {
|
|
8
|
+
renderer.setBackgroundColor(color);
|
|
9
|
+
if (syncTerminalDefault) {
|
|
10
|
+
process.stdout.write(wrapForTmuxIfNeeded(terminalBackgroundColorSequence(color)));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function requestRendererBackgroundRepaint(renderer: CliRenderer): void {
|
|
15
|
+
// OpenTUI 0.1.103+ no longer syncs the renderer background to the terminal
|
|
16
|
+
// default via OSC 11. Force the next frame so blank cells with this background
|
|
17
|
+
// are emitted instead of being skipped as unchanged initial buffer contents.
|
|
18
|
+
Object.assign(renderer, { forceFullRepaintRequested: true });
|
|
19
|
+
renderer.requestRender();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resetRendererTerminalBackground(renderer: CliRenderer): void {
|
|
23
|
+
if (process.env.TMUX) {
|
|
24
|
+
process.stdout.write(wrapForTmuxIfNeeded("\x1b]111\x07"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
renderer.resetTerminalBgColor();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function terminalBackgroundColorSequence(color: string): string {
|
|
32
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(color);
|
|
33
|
+
if (!match) {
|
|
34
|
+
throw new Error(`Cannot sync terminal background for non-hex color: ${color}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const hex = match[1]!;
|
|
38
|
+
return `\x1b]11;rgb:${hex.slice(0, 2)}/${hex.slice(2, 4)}/${hex.slice(4, 6)}\x07`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function wrapForTmuxIfNeeded(sequence: string): string {
|
|
42
|
+
if (!process.env.TMUX) return sequence;
|
|
43
|
+
|
|
44
|
+
let escaped = "";
|
|
45
|
+
for (const char of sequence) {
|
|
46
|
+
escaped += char === "\x1b" ? "\x1b\x1b" : char;
|
|
47
|
+
}
|
|
48
|
+
return `\x1bPtmux;${escaped}\x1b\\`;
|
|
49
|
+
}
|
|
@@ -433,9 +433,16 @@ export function SessionGraphPanel() {
|
|
|
433
433
|
},
|
|
434
434
|
}}
|
|
435
435
|
>
|
|
436
|
-
<box width={canvasW} height={canvasH} position="relative">
|
|
436
|
+
<box width={canvasW} height={canvasH} position="relative" backgroundColor={theme.background}>
|
|
437
437
|
{/* Offset all content by padding to center the graph */}
|
|
438
|
-
<box
|
|
438
|
+
<box
|
|
439
|
+
position="absolute"
|
|
440
|
+
left={padX}
|
|
441
|
+
top={padY}
|
|
442
|
+
width={layout.width}
|
|
443
|
+
height={layout.height}
|
|
444
|
+
backgroundColor={theme.background}
|
|
445
|
+
>
|
|
439
446
|
{/* Connectors (rendered behind nodes) */}
|
|
440
447
|
{connectors.map((conn, i) => (
|
|
441
448
|
<Edge key={`e${i}`} {...conn} />
|