@bastani/atomic 0.5.5 → 0.5.6-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 +60 -34
- package/dist/sdk/components/compact-switcher.d.ts +10 -0
- package/dist/sdk/components/compact-switcher.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.d.ts +21 -1
- package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -1
- package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -1
- 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/runtime/executor.d.ts +3 -2
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/tmux.d.ts +82 -2
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/workflows/index.d.ts +2 -2
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +150 -27
- package/src/commands/cli/chat/index.ts +25 -14
- package/src/commands/cli/completions.ts +24 -0
- package/src/commands/cli/session.test.ts +491 -0
- package/src/commands/cli/session.ts +265 -0
- package/src/commands/cli/workflow.ts +1 -1
- package/src/completions/bash.ts +107 -0
- package/src/completions/fish.ts +126 -0
- package/src/completions/index.ts +7 -0
- package/src/completions/powershell.ts +184 -0
- package/src/completions/zsh.ts +144 -0
- package/src/sdk/components/compact-switcher.tsx +73 -0
- package/src/sdk/components/orchestrator-panel-store.test.ts +124 -0
- package/src/sdk/components/orchestrator-panel-store.ts +36 -1
- package/src/sdk/components/orchestrator-panel-types.ts +2 -0
- package/src/sdk/components/session-graph-panel.tsx +138 -10
- package/src/sdk/components/statusline.tsx +13 -8
- package/src/sdk/runtime/executor.ts +18 -27
- package/src/sdk/runtime/tmux.conf +18 -0
- package/src/sdk/runtime/tmux.ts +198 -24
- package/src/sdk/workflows/index.ts +7 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell completion script for the atomic CLI (Windows / cross-platform).
|
|
3
|
+
*
|
|
4
|
+
* Install: atomic completions powershell | Invoke-Expression
|
|
5
|
+
* or add to $PROFILE for persistence.
|
|
6
|
+
*/
|
|
7
|
+
export const powershellCompletionScript = `
|
|
8
|
+
Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
|
|
9
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
10
|
+
|
|
11
|
+
$tokens = $commandAst.ToString().Substring(0, $cursorPosition) -split '\\s+' |
|
|
12
|
+
Where-Object { $_ -ne '' }
|
|
13
|
+
|
|
14
|
+
$agents = @('claude', 'opencode', 'copilot')
|
|
15
|
+
$scms = @('github', 'sapling')
|
|
16
|
+
$shells = @('bash', 'zsh', 'fish', 'powershell')
|
|
17
|
+
|
|
18
|
+
# Parse command chain, skipping flags and their values
|
|
19
|
+
$cmds = @()
|
|
20
|
+
$skipNext = $false
|
|
21
|
+
$prevToken = ''
|
|
22
|
+
for ($i = 1; $i -lt $tokens.Count; $i++) {
|
|
23
|
+
$t = $tokens[$i]
|
|
24
|
+
if ($skipNext) { $skipNext = $false; continue }
|
|
25
|
+
if ($t -match '^-') {
|
|
26
|
+
if ($t -match '^(-a|--agent|-s|--scm|-n|--name)$') { $skipNext = $true }
|
|
27
|
+
$prevToken = $t
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
$cmds += $t
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Check if the previous non-word token is a value-expecting flag
|
|
34
|
+
$lastToken = if ($tokens.Count -gt 1) { $tokens[-1] } else { '' }
|
|
35
|
+
$prevFullToken = if ($tokens.Count -gt 2) { $tokens[-2] } else { '' }
|
|
36
|
+
|
|
37
|
+
# Complete flag values
|
|
38
|
+
if ($prevFullToken -match '^(-a|--agent)$' -or $lastToken -match '^(-a|--agent)$') {
|
|
39
|
+
if ($lastToken -match '^(-a|--agent)$') {
|
|
40
|
+
$agents | ForEach-Object {
|
|
41
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
42
|
+
}
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
$agents | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
46
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
47
|
+
}
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if ($prevFullToken -match '^(-s|--scm)$' -or $lastToken -match '^(-s|--scm)$') {
|
|
51
|
+
if ($lastToken -match '^(-s|--scm)$') {
|
|
52
|
+
$scms | ForEach-Object {
|
|
53
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
54
|
+
}
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
$scms | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
58
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
59
|
+
}
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$completions = @()
|
|
64
|
+
|
|
65
|
+
switch ($cmds.Count) {
|
|
66
|
+
0 {
|
|
67
|
+
# Top-level commands
|
|
68
|
+
$completions = @(
|
|
69
|
+
@{ text = 'init'; tip = 'Interactive setup with agent selection' }
|
|
70
|
+
@{ text = 'chat'; tip = 'Start an interactive chat session' }
|
|
71
|
+
@{ text = 'workflow'; tip = 'Run a multi-session agent workflow' }
|
|
72
|
+
@{ text = 'session'; tip = 'Manage running tmux sessions' }
|
|
73
|
+
@{ text = 'config'; tip = 'Manage atomic configuration' }
|
|
74
|
+
@{ text = 'completions'; tip = 'Output shell completion script' }
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
default {
|
|
78
|
+
switch ($cmds[0]) {
|
|
79
|
+
'init' {
|
|
80
|
+
$completions = @(
|
|
81
|
+
@{ text = '-a'; tip = 'Agent to configure' }
|
|
82
|
+
@{ text = '--agent'; tip = 'Agent to configure' }
|
|
83
|
+
@{ text = '-s'; tip = 'Source control system' }
|
|
84
|
+
@{ text = '--scm'; tip = 'Source control system' }
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
'chat' {
|
|
88
|
+
if ($cmds.Count -eq 1) {
|
|
89
|
+
$completions = @(
|
|
90
|
+
@{ text = 'session'; tip = 'Manage running chat sessions' }
|
|
91
|
+
@{ text = '-a'; tip = 'Agent to chat with' }
|
|
92
|
+
@{ text = '--agent'; tip = 'Agent to chat with' }
|
|
93
|
+
)
|
|
94
|
+
} elseif ($cmds[1] -eq 'session') {
|
|
95
|
+
if ($cmds.Count -eq 2) {
|
|
96
|
+
$completions = @(
|
|
97
|
+
@{ text = 'list'; tip = 'List running sessions' }
|
|
98
|
+
@{ text = 'connect'; tip = 'Attach to a running session' }
|
|
99
|
+
)
|
|
100
|
+
} else {
|
|
101
|
+
$completions = @(
|
|
102
|
+
@{ text = '-a'; tip = 'Filter by agent' }
|
|
103
|
+
@{ text = '--agent'; tip = 'Filter by agent' }
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
'workflow' {
|
|
109
|
+
if ($cmds.Count -eq 1) {
|
|
110
|
+
$completions = @(
|
|
111
|
+
@{ text = 'list'; tip = 'List available workflows' }
|
|
112
|
+
@{ text = 'session'; tip = 'Manage running workflow sessions' }
|
|
113
|
+
@{ text = '-n'; tip = 'Workflow name' }
|
|
114
|
+
@{ text = '--name'; tip = 'Workflow name' }
|
|
115
|
+
@{ text = '-a'; tip = 'Agent to use' }
|
|
116
|
+
@{ text = '--agent'; tip = 'Agent to use' }
|
|
117
|
+
)
|
|
118
|
+
} elseif ($cmds[1] -eq 'list') {
|
|
119
|
+
$completions = @(
|
|
120
|
+
@{ text = '-a'; tip = 'Filter by agent' }
|
|
121
|
+
@{ text = '--agent'; tip = 'Filter by agent' }
|
|
122
|
+
)
|
|
123
|
+
} elseif ($cmds[1] -eq 'session') {
|
|
124
|
+
if ($cmds.Count -eq 2) {
|
|
125
|
+
$completions = @(
|
|
126
|
+
@{ text = 'list'; tip = 'List running sessions' }
|
|
127
|
+
@{ text = 'connect'; tip = 'Attach to a running session' }
|
|
128
|
+
)
|
|
129
|
+
} else {
|
|
130
|
+
$completions = @(
|
|
131
|
+
@{ text = '-a'; tip = 'Filter by agent' }
|
|
132
|
+
@{ text = '--agent'; tip = 'Filter by agent' }
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
'session' {
|
|
138
|
+
if ($cmds.Count -eq 1) {
|
|
139
|
+
$completions = @(
|
|
140
|
+
@{ text = 'list'; tip = 'List running sessions' }
|
|
141
|
+
@{ text = 'connect'; tip = 'Attach to a running session' }
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
$completions = @(
|
|
145
|
+
@{ text = '-a'; tip = 'Filter by agent' }
|
|
146
|
+
@{ text = '--agent'; tip = 'Filter by agent' }
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
'config' {
|
|
151
|
+
if ($cmds.Count -eq 1) {
|
|
152
|
+
$completions = @(
|
|
153
|
+
@{ text = 'set'; tip = 'Set a configuration value' }
|
|
154
|
+
)
|
|
155
|
+
} elseif ($cmds[1] -eq 'set') {
|
|
156
|
+
if ($cmds.Count -eq 2) {
|
|
157
|
+
$completions = @(
|
|
158
|
+
@{ text = 'telemetry'; tip = 'Telemetry setting' }
|
|
159
|
+
)
|
|
160
|
+
} elseif ($cmds[2] -eq 'telemetry') {
|
|
161
|
+
$completions = @(
|
|
162
|
+
@{ text = 'true'; tip = 'Enable telemetry' }
|
|
163
|
+
@{ text = 'false'; tip = 'Disable telemetry' }
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
'completions' {
|
|
169
|
+
$completions = @(
|
|
170
|
+
@{ text = 'bash'; tip = 'Bash completion script' }
|
|
171
|
+
@{ text = 'zsh'; tip = 'Zsh completion script' }
|
|
172
|
+
@{ text = 'fish'; tip = 'Fish completion script' }
|
|
173
|
+
@{ text = 'powershell'; tip = 'PowerShell completion script' }
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$completions | Where-Object { $_.text -like "$wordToComplete*" } | ForEach-Object {
|
|
181
|
+
[System.Management.Automation.CompletionResult]::new($_.text, $_.text, 'ParameterValue', $_.tip)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zsh completion script for the atomic CLI.
|
|
3
|
+
*
|
|
4
|
+
* Install: eval "$(atomic completions zsh)"
|
|
5
|
+
*/
|
|
6
|
+
export const zshCompletionScript = `
|
|
7
|
+
#compdef atomic
|
|
8
|
+
|
|
9
|
+
_atomic() {
|
|
10
|
+
local -a agents=('claude' 'opencode' 'copilot')
|
|
11
|
+
local -a scms=('github' 'sapling')
|
|
12
|
+
|
|
13
|
+
_arguments -C \\
|
|
14
|
+
'(-y --yes)'{-y,--yes}'[Auto-confirm all prompts]' \\
|
|
15
|
+
'--no-banner[Skip ASCII banner display]' \\
|
|
16
|
+
'(-v --version)'{-v,--version}'[Show version number]' \\
|
|
17
|
+
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
18
|
+
'1:command:->cmds' \\
|
|
19
|
+
'*::arg:->args'
|
|
20
|
+
|
|
21
|
+
case "$state" in
|
|
22
|
+
cmds)
|
|
23
|
+
local -a commands=(
|
|
24
|
+
'init:Interactive setup with agent selection'
|
|
25
|
+
'chat:Start an interactive chat session'
|
|
26
|
+
'workflow:Run a multi-session agent workflow'
|
|
27
|
+
'session:Manage running tmux sessions'
|
|
28
|
+
'config:Manage atomic configuration'
|
|
29
|
+
'completions:Output shell completion script'
|
|
30
|
+
)
|
|
31
|
+
_describe 'command' commands
|
|
32
|
+
;;
|
|
33
|
+
args)
|
|
34
|
+
case "\${words[1]}" in
|
|
35
|
+
init)
|
|
36
|
+
_arguments \\
|
|
37
|
+
'(-a --agent)'{-a,--agent}'[Agent to configure]:agent:(claude opencode copilot)' \\
|
|
38
|
+
'(-s --scm)'{-s,--scm}'[Source control system]:scm:(github sapling)' \\
|
|
39
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
40
|
+
;;
|
|
41
|
+
chat)
|
|
42
|
+
_arguments -C \\
|
|
43
|
+
'(-a --agent)'{-a,--agent}'[Agent to chat with]:agent:(claude opencode copilot)' \\
|
|
44
|
+
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
45
|
+
'1:subcommand:->sub' \\
|
|
46
|
+
'*::subarg:->subargs'
|
|
47
|
+
case "$state" in
|
|
48
|
+
sub)
|
|
49
|
+
local -a subs=('session:Manage running chat sessions')
|
|
50
|
+
_describe 'subcommand' subs
|
|
51
|
+
;;
|
|
52
|
+
subargs)
|
|
53
|
+
case "\${words[1]}" in
|
|
54
|
+
session) _atomic_session ;;
|
|
55
|
+
esac
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
;;
|
|
59
|
+
workflow)
|
|
60
|
+
_arguments -C \\
|
|
61
|
+
'(-n --name)'{-n,--name}'[Workflow name]:name:' \\
|
|
62
|
+
'(-a --agent)'{-a,--agent}'[Agent to use]:agent:(claude opencode copilot)' \\
|
|
63
|
+
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
64
|
+
'1:subcommand:->sub' \\
|
|
65
|
+
'*::subarg:->subargs'
|
|
66
|
+
case "$state" in
|
|
67
|
+
sub)
|
|
68
|
+
local -a subs=(
|
|
69
|
+
'list:List available workflows'
|
|
70
|
+
'session:Manage running workflow sessions'
|
|
71
|
+
)
|
|
72
|
+
_describe 'subcommand' subs
|
|
73
|
+
;;
|
|
74
|
+
subargs)
|
|
75
|
+
case "\${words[1]}" in
|
|
76
|
+
list)
|
|
77
|
+
_arguments \\
|
|
78
|
+
'(-a --agent)'{-a,--agent}'[Filter by agent]:agent:(claude opencode copilot)' \\
|
|
79
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
80
|
+
;;
|
|
81
|
+
session) _atomic_session ;;
|
|
82
|
+
esac
|
|
83
|
+
;;
|
|
84
|
+
esac
|
|
85
|
+
;;
|
|
86
|
+
session)
|
|
87
|
+
_atomic_session
|
|
88
|
+
;;
|
|
89
|
+
config)
|
|
90
|
+
_arguments -C \\
|
|
91
|
+
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
92
|
+
'1:subcommand:->sub' \\
|
|
93
|
+
'*::subarg:->subargs'
|
|
94
|
+
case "$state" in
|
|
95
|
+
sub)
|
|
96
|
+
local -a subs=('set:Set a configuration value')
|
|
97
|
+
_describe 'subcommand' subs
|
|
98
|
+
;;
|
|
99
|
+
subargs)
|
|
100
|
+
case "\${words[1]}" in
|
|
101
|
+
set)
|
|
102
|
+
_arguments \\
|
|
103
|
+
'1:key:(telemetry)' \\
|
|
104
|
+
'2:value:(true false)'
|
|
105
|
+
;;
|
|
106
|
+
esac
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
;;
|
|
110
|
+
completions)
|
|
111
|
+
_arguments '1:shell:(bash zsh fish powershell)'
|
|
112
|
+
;;
|
|
113
|
+
esac
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_atomic_session() {
|
|
119
|
+
_arguments -C \\
|
|
120
|
+
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
121
|
+
'1:subcommand:->sub' \\
|
|
122
|
+
'*::subarg:->subargs'
|
|
123
|
+
case "$state" in
|
|
124
|
+
sub)
|
|
125
|
+
local -a subs=(
|
|
126
|
+
'list:List running sessions'
|
|
127
|
+
'connect:Attach to a running session'
|
|
128
|
+
)
|
|
129
|
+
_describe 'subcommand' subs
|
|
130
|
+
;;
|
|
131
|
+
subargs)
|
|
132
|
+
case "\${words[1]}" in
|
|
133
|
+
list|connect)
|
|
134
|
+
_arguments \\
|
|
135
|
+
'*'{-a,--agent}'[Filter by agent]:agent:(claude opencode copilot)' \\
|
|
136
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
137
|
+
;;
|
|
138
|
+
esac
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_atomic "$@"
|
|
144
|
+
`;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
/**
|
|
3
|
+
* CompactSwitcher — a lightweight popup that lists all agents for quick
|
|
4
|
+
* direct-jump navigation. Opened with "/" from any view mode.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useStore, useGraphTheme, useStoreVersion } from "./orchestrator-panel-contexts.ts";
|
|
8
|
+
import { statusIcon, statusColor, fmtDuration } from "./status-helpers.ts";
|
|
9
|
+
import { lerpColor } from "./color-utils.ts";
|
|
10
|
+
|
|
11
|
+
export interface CompactSwitcherProps {
|
|
12
|
+
selectedIndex: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CompactSwitcher({ selectedIndex }: CompactSwitcherProps) {
|
|
16
|
+
const store = useStore();
|
|
17
|
+
const theme = useGraphTheme();
|
|
18
|
+
useStoreVersion(store);
|
|
19
|
+
|
|
20
|
+
const agents = store.sessions;
|
|
21
|
+
const headerHint = "\u2191\u2193 select \u00B7 \u21B5 jump \u00B7 Esc close";
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<box
|
|
25
|
+
position="absolute"
|
|
26
|
+
bottom={1}
|
|
27
|
+
left={0}
|
|
28
|
+
width={44}
|
|
29
|
+
border
|
|
30
|
+
borderStyle="rounded"
|
|
31
|
+
borderColor={theme.borderActive}
|
|
32
|
+
backgroundColor={theme.backgroundElement}
|
|
33
|
+
flexDirection="column"
|
|
34
|
+
>
|
|
35
|
+
{/* Header */}
|
|
36
|
+
<box height={1} flexDirection="row" paddingLeft={1} paddingRight={1}>
|
|
37
|
+
<text fg={theme.textDim}>agents</text>
|
|
38
|
+
<box flexGrow={1} />
|
|
39
|
+
<text fg={theme.textDim}>{headerHint}</text>
|
|
40
|
+
</box>
|
|
41
|
+
|
|
42
|
+
{/* Agent list */}
|
|
43
|
+
{agents.map((agent, i) => {
|
|
44
|
+
const isSelected = i === selectedIndex;
|
|
45
|
+
const icon = statusIcon(agent.status);
|
|
46
|
+
const iconColor = statusColor(agent.status, theme);
|
|
47
|
+
const duration =
|
|
48
|
+
agent.startedAt !== null
|
|
49
|
+
? fmtDuration((agent.endedAt ?? Date.now()) - agent.startedAt)
|
|
50
|
+
: "\u2014";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<box
|
|
54
|
+
key={agent.name}
|
|
55
|
+
height={1}
|
|
56
|
+
flexDirection="row"
|
|
57
|
+
paddingLeft={1}
|
|
58
|
+
paddingRight={1}
|
|
59
|
+
backgroundColor={isSelected ? lerpColor(theme.backgroundElement, theme.primary, 0.12) : theme.backgroundElement}
|
|
60
|
+
>
|
|
61
|
+
<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
|
+
</text>
|
|
66
|
+
<box flexGrow={1} />
|
|
67
|
+
<text fg={theme.textDim}>{duration}</text>
|
|
68
|
+
</box>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -714,4 +714,128 @@ describe("PanelStore", () => {
|
|
|
714
714
|
expect(store.sessions.find((s) => s.name === "s3")!.parents).toEqual(["s2"]);
|
|
715
715
|
});
|
|
716
716
|
});
|
|
717
|
+
|
|
718
|
+
// ── setViewMode ────────────────────────────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
describe("setViewMode", () => {
|
|
721
|
+
test("defaults to graph mode with empty active agent", () => {
|
|
722
|
+
expect(store.viewMode).toBe("graph");
|
|
723
|
+
expect(store.activeAgentId).toBe("");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("switches to attached mode with agent ID", () => {
|
|
727
|
+
store.setViewMode("attached", "worker-1");
|
|
728
|
+
expect(store.viewMode).toBe("attached");
|
|
729
|
+
expect(store.activeAgentId).toBe("worker-1");
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("switches back to graph mode and clears active agent", () => {
|
|
733
|
+
store.setViewMode("attached", "worker-1");
|
|
734
|
+
store.setViewMode("graph");
|
|
735
|
+
expect(store.viewMode).toBe("graph");
|
|
736
|
+
expect(store.activeAgentId).toBe("");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("increments version by exactly 1", () => {
|
|
740
|
+
const before = store.version;
|
|
741
|
+
store.setViewMode("attached", "worker-1");
|
|
742
|
+
expect(store.version).toBe(before + 1);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("notifies subscribers", () => {
|
|
746
|
+
const listener = mock(() => {});
|
|
747
|
+
store.subscribe(listener);
|
|
748
|
+
store.setViewMode("attached", "worker-1");
|
|
749
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("attached without agent ID clears active agent", () => {
|
|
753
|
+
store.setViewMode("attached");
|
|
754
|
+
expect(store.viewMode).toBe("attached");
|
|
755
|
+
expect(store.activeAgentId).toBe("");
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// ── getSubagents ───────────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
describe("getSubagents", () => {
|
|
762
|
+
beforeEach(() => {
|
|
763
|
+
store.setWorkflowInfo("wf", "claude", [
|
|
764
|
+
{ name: "planner", parents: [] },
|
|
765
|
+
{ name: "writer", parents: ["planner"] },
|
|
766
|
+
{ name: "reviewer", parents: ["writer"] },
|
|
767
|
+
], "prompt");
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("returns empty when all non-orchestrator sessions are pending", () => {
|
|
771
|
+
expect(store.getSubagents()).toEqual([]);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("excludes orchestrator from subagent list", () => {
|
|
775
|
+
store.startSession("planner");
|
|
776
|
+
const subs = store.getSubagents();
|
|
777
|
+
expect(subs.every((s) => s.name !== "orchestrator")).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("includes running and completed sessions", () => {
|
|
781
|
+
store.startSession("planner");
|
|
782
|
+
store.completeSession("planner");
|
|
783
|
+
store.startSession("writer");
|
|
784
|
+
const subs = store.getSubagents();
|
|
785
|
+
expect(subs.map((s) => s.name)).toEqual(["planner", "writer"]);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("includes errored sessions", () => {
|
|
789
|
+
store.startSession("planner");
|
|
790
|
+
store.failSession("planner", "timeout");
|
|
791
|
+
const subs = store.getSubagents();
|
|
792
|
+
expect(subs.map((s) => s.name)).toEqual(["planner"]);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("excludes pending sessions", () => {
|
|
796
|
+
store.startSession("planner");
|
|
797
|
+
const subs = store.getSubagents();
|
|
798
|
+
expect(subs.map((s) => s.name)).toEqual(["planner"]);
|
|
799
|
+
expect(subs.some((s) => s.name === "writer")).toBe(false);
|
|
800
|
+
expect(subs.some((s) => s.name === "reviewer")).toBe(false);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// ── getActiveAgentIndex ────────────────────────────────────────────────────
|
|
805
|
+
|
|
806
|
+
describe("getActiveAgentIndex", () => {
|
|
807
|
+
beforeEach(() => {
|
|
808
|
+
store.setWorkflowInfo("wf", "claude", [
|
|
809
|
+
{ name: "planner", parents: [] },
|
|
810
|
+
{ name: "writer", parents: ["planner"] },
|
|
811
|
+
{ name: "reviewer", parents: ["writer"] },
|
|
812
|
+
], "prompt");
|
|
813
|
+
store.startSession("planner");
|
|
814
|
+
store.startSession("writer");
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("returns -1 when no agent is active", () => {
|
|
818
|
+
expect(store.getActiveAgentIndex()).toBe(-1);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("returns correct index for first subagent", () => {
|
|
822
|
+
store.setViewMode("attached", "planner");
|
|
823
|
+
expect(store.getActiveAgentIndex()).toBe(0);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test("returns correct index for second subagent", () => {
|
|
827
|
+
store.setViewMode("attached", "writer");
|
|
828
|
+
expect(store.getActiveAgentIndex()).toBe(1);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("returns -1 for orchestrator (not a subagent)", () => {
|
|
832
|
+
store.setViewMode("attached", "orchestrator");
|
|
833
|
+
expect(store.getActiveAgentIndex()).toBe(-1);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("returns -1 for non-existent agent", () => {
|
|
837
|
+
store.setViewMode("attached", "nonexistent");
|
|
838
|
+
expect(store.getActiveAgentIndex()).toBe(-1);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
717
841
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─── State Store ──────────────────────────────────
|
|
2
2
|
// Bridges the imperative OrchestratorPanel API with the React component tree.
|
|
3
3
|
|
|
4
|
-
import type { SessionData, SessionStatus, PanelSession } from "./orchestrator-panel-types.ts";
|
|
4
|
+
import type { SessionData, SessionStatus, PanelSession, ViewMode } from "./orchestrator-panel-types.ts";
|
|
5
5
|
|
|
6
6
|
type Listener = () => void;
|
|
7
7
|
|
|
@@ -17,6 +17,11 @@ export class PanelStore {
|
|
|
17
17
|
exitResolve: (() => void) | null = null;
|
|
18
18
|
abortResolve: (() => void) | null = null;
|
|
19
19
|
|
|
20
|
+
/** Current view mode — graph overview or attached to a specific agent. */
|
|
21
|
+
viewMode: ViewMode = "graph";
|
|
22
|
+
/** ID of the agent currently attached to (only meaningful when viewMode === "attached"). */
|
|
23
|
+
activeAgentId = "";
|
|
24
|
+
|
|
20
25
|
private listeners = new Set<Listener>();
|
|
21
26
|
|
|
22
27
|
subscribe = (fn: Listener): (() => void) => {
|
|
@@ -108,6 +113,36 @@ export class PanelStore {
|
|
|
108
113
|
this.emit();
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Switch between graph and attached view modes.
|
|
118
|
+
* When switching to "attached", provide the agent ID to attach to.
|
|
119
|
+
* Switching to "graph" clears the active agent.
|
|
120
|
+
*/
|
|
121
|
+
setViewMode(mode: ViewMode, agentId?: string): void {
|
|
122
|
+
this.viewMode = mode;
|
|
123
|
+
this.activeAgentId = mode === "attached" && agentId ? agentId : "";
|
|
124
|
+
this.emit();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Return non-orchestrator agents that have started (not pending).
|
|
129
|
+
* Used for the tmux status bar agent count and active-agent index.
|
|
130
|
+
*/
|
|
131
|
+
getSubagents(): SessionData[] {
|
|
132
|
+
return this.sessions.filter(
|
|
133
|
+
(s) => s.name !== "orchestrator" && s.status !== "pending",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Return the 0-based index of the active agent within the subagent list,
|
|
139
|
+
* or -1 if not found.
|
|
140
|
+
*/
|
|
141
|
+
getActiveAgentIndex(): number {
|
|
142
|
+
const subs = this.getSubagents();
|
|
143
|
+
return subs.findIndex((s) => s.name === this.activeAgentId);
|
|
144
|
+
}
|
|
145
|
+
|
|
111
146
|
/** Safely invoke exitResolve at most once, guarding against rapid repeated calls. */
|
|
112
147
|
resolveExit(): void {
|
|
113
148
|
if (this.exitResolve) {
|