返回 DeepSeek-Reasonix
shell_completion.go
根目录 / internal / cli / shell_completion.go
1 package cli
2
3 import (
4 "fmt"
5 "os"
6 "slices"
7 "strconv"
8 "strings"
9
10 "reasonix/internal/agent"
11 )
12
13 type cliCompletionValueKind uint8
14
15 const (
16 cliCompletionNoValue cliCompletionValueKind = iota
17 cliCompletionStaticValue
18 cliCompletionOptionalValue // next token may be a value or another flag (--resume [QUERY])
19 cliCompletionPathValue // path/file; backend returns empty so shells use default file completion
20 cliCompletionModelValue
21 cliCompletionSessionValue
22 )
23
24 type cliCompletionFlag struct {
25 names []string
26 kind cliCompletionValueKind
27 values []string
28 }
29
30 type cliCompletionSpec struct {
31 name string
32 aliases []string
33 flags []cliCompletionFlag
34 subcommands []cliCompletionSpec
35 }
36
37 func completionFlag(names string, kind cliCompletionValueKind, values ...string) cliCompletionFlag {
38 return cliCompletionFlag{names: strings.Fields(names), kind: kind, values: values}
39 }
40
41 func completionSpec(name string, flags []cliCompletionFlag, subcommands ...cliCompletionSpec) cliCompletionSpec {
42 return cliCompletionSpec{name: name, flags: flags, subcommands: subcommands}
43 }
44
45 func completionSpecWithAliases(name string, aliases []string, flags []cliCompletionFlag, subcommands ...cliCompletionSpec) cliCompletionSpec {
46 return cliCompletionSpec{name: name, aliases: aliases, flags: flags, subcommands: subcommands}
47 }
48
49 func catalogCompletionSpec(help cliCompletionFlag) cliCompletionSpec {
50 reindex := make([]cliCompletionSpec, 0, len(catalogCommands))
51 for _, command := range catalogCommands {
52 flags := append(slices.Clone(command.completionFlags), help)
53 reindex = append(reindex, completionSpec(command.name, flags))
54 }
55 return completionSpec("catalogs", []cliCompletionFlag{help}, completionSpec("reindex", []cliCompletionFlag{help}, reindex...))
56 }
57
58 func cliCompletionRootSpec() cliCompletionSpec {
59 model := completionFlag("--model", cliCompletionModelValue)
60 resume := completionFlag("--resume -r", cliCompletionOptionalValue) // optional QUERY
61 effort := completionFlag("--effort", cliCompletionStaticValue, "auto", "low", "medium", "high", "max")
62 permissionMode := completionFlag("--permission-mode", cliCompletionStaticValue,
63 "read-only", "workspace-write", "danger-full-access", "plan")
64 help := completionFlag("--help -h", cliCompletionNoValue)
65
66 interactiveFlags := []cliCompletionFlag{
67 model,
68 completionFlag("--max-steps", cliCompletionStaticValue),
69 completionFlag("--continue -c", cliCompletionNoValue),
70 resume,
71 completionFlag("--copy", cliCompletionNoValue),
72 completionFlag("--dir", cliCompletionPathValue),
73 effort, permissionMode,
74 completionFlag("--add-dir", cliCompletionPathValue),
75 completionFlag("--allowed-tools --allowedTools", cliCompletionStaticValue),
76 help,
77 }
78 // run/serve --resume require a value; only interactive root --resume [QUERY] is optional.
79 runResume := completionFlag("--resume", cliCompletionSessionValue)
80 runFlags := []cliCompletionFlag{
81 model,
82 completionFlag("--max-steps", cliCompletionStaticValue),
83 completionFlag("--show-thinking", cliCompletionNoValue),
84 completionFlag("--metrics", cliCompletionPathValue),
85 completionFlag("--dir", cliCompletionPathValue),
86 completionFlag("--continue -c", cliCompletionNoValue),
87 runResume,
88 completionFlag("--copy", cliCompletionNoValue),
89 effort, permissionMode,
90 completionFlag("--print -p", cliCompletionNoValue),
91 completionFlag("--events-jsonl", cliCompletionNoValue),
92 completionFlag("--output-format", cliCompletionStaticValue, "text", "json", "stream-json"),
93 completionFlag("--add-dir", cliCompletionPathValue),
94 completionFlag("--allowed-tools --allowedTools", cliCompletionStaticValue),
95 completionFlag("--ablate", cliCompletionStaticValue, "none", "all", "evidence", "planner", "subagent", "retrieval", "compaction"),
96 help,
97 }
98 root := cliCompletionSpec{name: "reasonix", flags: append([]cliCompletionFlag{
99 model,
100 completionFlag("--max-steps", cliCompletionStaticValue),
101 completionFlag("--print -p", cliCompletionNoValue),
102 completionFlag("--continue -c", cliCompletionNoValue),
103 resume,
104 completionFlag("--copy", cliCompletionNoValue),
105 permissionMode,
106 effort,
107 completionFlag("--dir", cliCompletionPathValue),
108 completionFlag("--add-dir", cliCompletionPathValue),
109 completionFlag("--allowed-tools --allowedTools", cliCompletionStaticValue),
110 completionFlag("--acp", cliCompletionNoValue),
111 completionFlag("--version -v", cliCompletionNoValue),
112 }, help)}
113
114 serveFlags := cliServeCompletionFlags(model, help)
115
116 root.subcommands = []cliCompletionSpec{
117 completionSpec("run", runFlags),
118 completionSpecWithAliases("chat", []string{"code"}, interactiveFlags),
119 completionSpec("serve", serveFlags),
120 completionSpec("web", serveFlags),
121 completionSpec("setup", []cliCompletionFlag{completionFlag("--local -l", cliCompletionNoValue), help}),
122 completionSpec("config", []cliCompletionFlag{help},
123 completionSpec("auto-plan", []cliCompletionFlag{completionFlag("--local", cliCompletionNoValue), help}),
124 completionSpec("reasoning-language", []cliCompletionFlag{completionFlag("--local", cliCompletionNoValue), help}),
125 completionSpec("compact-ratio", []cliCompletionFlag{completionFlag("--local", cliCompletionNoValue), help}),
126 completionSpec("currency", []cliCompletionFlag{completionFlag("--local", cliCompletionNoValue), help}),
127 completionSpec("telemetry", []cliCompletionFlag{help}),
128 ),
129 completionSpec("init", []cliCompletionFlag{help}),
130 completionSpec("acp", []cliCompletionFlag{
131 model,
132 completionFlag("--planner", cliCompletionStaticValue, "auto", "off"),
133 completionFlag("--sandbox-network", cliCompletionStaticValue, "auto", "on", "off"),
134 completionFlag("--sandbox-bash", cliCompletionStaticValue, "auto", "enforce"),
135 completionFlag("--workspace-only", cliCompletionNoValue), help,
136 }),
137 completionSpec("mcp", []cliCompletionFlag{help},
138 completionSpecWithAliases("list", []string{"ls"}, []cliCompletionFlag{help}),
139 completionSpec("get", []cliCompletionFlag{help}),
140 completionSpec("add", []cliCompletionFlag{
141 completionFlag("--http --streamable-http --sse --env --header", cliCompletionStaticValue), help,
142 }),
143 completionSpec("enable", []cliCompletionFlag{help}),
144 completionSpec("disable", []cliCompletionFlag{help}),
145 completionSpecWithAliases("retry", []string{"connect"}, []cliCompletionFlag{help}),
146 completionSpec("update", []cliCompletionFlag{help}),
147 completionSpec("import", []cliCompletionFlag{help}),
148 completionSpecWithAliases("browse", []string{"search"}, []cliCompletionFlag{
149 completionFlag("--limit", cliCompletionStaticValue), completionFlag("--json", cliCompletionNoValue), help,
150 }),
151 completionSpec("install", []cliCompletionFlag{completionFlag("--as", cliCompletionStaticValue), help}),
152 completionSpecWithAliases("remove", []string{"rm"}, []cliCompletionFlag{help}),
153 ),
154 completionSpec("remote", []cliCompletionFlag{help},
155 completionSpec("add", []cliCompletionFlag{
156 completionFlag("--identity --jump --workspace --serve-install --passphrase-env --password-env", cliCompletionStaticValue),
157 completionFlag("--use-ssh-config", cliCompletionNoValue), help,
158 }),
159 completionSpecWithAliases("list", []string{"ls"}, []cliCompletionFlag{help}),
160 completionSpecWithAliases("remove", []string{"rm"}, []cliCompletionFlag{help}),
161 completionSpec("import", []cliCompletionFlag{completionFlag("--all", cliCompletionNoValue), help}),
162 completionSpec("test", []cliCompletionFlag{help}),
163 completionSpecWithAliases("connect", []string{"open"}, []cliCompletionFlag{
164 completionFlag("--workspace --local-port", cliCompletionStaticValue),
165 completionFlag("--no-serve --open --forward-only", cliCompletionNoValue), help,
166 }),
167 completionSpec("status", []cliCompletionFlag{help}),
168 completionSpec("forward", []cliCompletionFlag{help},
169 completionSpec("add", []cliCompletionFlag{help}),
170 completionSpecWithAliases("remove", []string{"rm"}, []cliCompletionFlag{help}),
171 completionSpecWithAliases("list", []string{"ls"}, []cliCompletionFlag{help}),
172 ),
173 completionSpec("serve", []cliCompletionFlag{help},
174 completionSpec("start", []cliCompletionFlag{completionFlag("--workspace -n", cliCompletionStaticValue), help}),
175 completionSpec("stop", []cliCompletionFlag{completionFlag("--workspace -n", cliCompletionStaticValue), help}),
176 completionSpec("status", []cliCompletionFlag{completionFlag("--workspace -n", cliCompletionStaticValue), help}),
177 completionSpec("logs", []cliCompletionFlag{completionFlag("--workspace -n", cliCompletionStaticValue), help}),
178 ),
179 completionSpec("fs", []cliCompletionFlag{help},
180 completionSpecWithAliases("list", []string{"ls"}, []cliCompletionFlag{help}),
181 completionSpec("get", []cliCompletionFlag{help}),
182 completionSpec("put", []cliCompletionFlag{help}),
183 ),
184 completionSpec("attach-workspace", []cliCompletionFlag{completionFlag("--stdio", cliCompletionNoValue), completionFlag("--workspace", cliCompletionStaticValue), help}),
185 ),
186 completionSpec("plugin", []cliCompletionFlag{help},
187 completionSpec("install", []cliCompletionFlag{
188 completionFlag("--yes --dry-run --link --replace", cliCompletionNoValue),
189 completionFlag("--name", cliCompletionStaticValue), help,
190 }),
191 completionSpec("list", []cliCompletionFlag{help}),
192 completionSpec("show", []cliCompletionFlag{help}),
193 completionSpecWithAliases("remove", []string{"uninstall"}, []cliCompletionFlag{completionFlag("--yes", cliCompletionNoValue), help}),
194 completionSpec("enable", []cliCompletionFlag{help}),
195 completionSpec("disable", []cliCompletionFlag{help}),
196 completionSpec("doctor", []cliCompletionFlag{help}),
197 ),
198 completionSpec("subagent", []cliCompletionFlag{help},
199 completionSpecWithAliases("list", []string{"ls"}, []cliCompletionFlag{completionFlag("--dir", cliCompletionPathValue), help}),
200 completionSpecWithAliases("create", []string{"new"}, append(subagentCompletionFlags(model, effort, help), completionFlag("--scope", cliCompletionStaticValue, "project", "global"))),
201 completionSpecWithAliases("edit", []string{"update"}, subagentCompletionFlags(model, effort, help)),
202 completionSpecWithAliases("delete", []string{"remove", "rm"}, []cliCompletionFlag{
203 completionFlag("--yes", cliCompletionNoValue), completionFlag("--dir", cliCompletionPathValue), help,
204 }),
205 completionSpec("try", []cliCompletionFlag{model, completionFlag("--max-steps --dir", cliCompletionStaticValue), help}),
206 completionSpec("run", []cliCompletionFlag{model, completionFlag("--max-steps --dir", cliCompletionStaticValue), help}),
207 ),
208 doctorCompletionSpec(help),
209 completionSpec("sessions", []cliCompletionFlag{help},
210 completionSpec("reindex", []cliCompletionFlag{
211 completionFlag("--json", cliCompletionNoValue), completionFlag("--dir", cliCompletionPathValue), help,
212 }),
213 completionSpec("diagnose", []cliCompletionFlag{
214 completionFlag("--json", cliCompletionNoValue), completionFlag("--dir", cliCompletionPathValue), help,
215 }),
216 completionSpec("cleanup", []cliCompletionFlag{
217 completionFlag("--json --apply", cliCompletionNoValue), completionFlag("--dir", cliCompletionPathValue), help,
218 }),
219 ),
220 catalogCompletionSpec(help),
221 completionSpec("report", []cliCompletionFlag{help},
222 completionSpec("list", []cliCompletionFlag{help}),
223 completionSpec("show", []cliCompletionFlag{help}),
224 completionSpec("send", []cliCompletionFlag{help}),
225 completionSpec("delete", []cliCompletionFlag{help}),
226 ),
227 completionSpec("session", []cliCompletionFlag{help},
228 completionSpec("list", machineSessionCompletionFlags(help)),
229 completionSpec("show", machineSessionCompletionFlags(help)),
230 completionSpec("status", machineSessionCompletionFlags(help)),
231 completionSpec("recovery", machineSessionCompletionFlags(help)),
232 ),
233 completionSpecWithAliases("hook", []string{"hooks"}, []cliCompletionFlag{help},
234 completionSpec("list", hookCompletionFlags(help)),
235 completionSpec("status", hookCompletionFlags(help)),
236 ),
237 completionSpec("task", []cliCompletionFlag{help},
238 // Machine list/show: --json --dir --project-root --session (task_machine.go).
239 completionSpec("list", completionTaskMachineListFlags(help)),
240 completionSpec("show", completionTaskMachineShowFlags(help)),
241 // Live monitor surface (task.go FlagSets).
242 completionSpec("monitor", []cliCompletionFlag{help},
243 completionSpec("list", completionTaskListFlags(help)),
244 completionSpec("status", completionTaskStatusFlags(help)),
245 completionSpec("events", completionTaskEventsFlags(help)),
246 completionSpec("stop", completionTaskControlFlags(help)),
247 completionSpec("cancel", completionTaskControlFlags(help)),
248 completionSpec("requeue", completionTaskRequeueFlags(help)),
249 completionSpec("open-session", completionTaskOpenSessionFlags(help)),
250 ),
251 completionSpec("status", completionTaskStatusFlags(help)),
252 completionSpec("events", completionTaskEventsFlags(help)),
253 completionSpec("stop", completionTaskControlFlags(help)),
254 completionSpec("cancel", completionTaskControlFlags(help)),
255 completionSpec("requeue", completionTaskRequeueFlags(help)),
256 completionSpec("open-session", completionTaskOpenSessionFlags(help)),
257 completionSpec("tmux", []cliCompletionFlag{help},
258 completionSpec("attach", completionTaskTmuxAttachFlags(help)),
259 completionSpec("status", completionTaskTmuxFlags(help)),
260 completionSpec("open", completionTaskTmuxFlags(help)),
261 completionSpec("detach", completionTaskTmuxFlags(help)),
262 ),
263 ),
264 completionSpec("review", []cliCompletionFlag{
265 completionFlag("--base --commit --instructions", cliCompletionStaticValue), model, help,
266 }),
267 completionSpec("bot", []cliCompletionFlag{help},
268 completionSpec("start", []cliCompletionFlag{completionFlag("--channels --dir", cliCompletionStaticValue), model, help}),
269 completionSpec("doctor", []cliCompletionFlag{completionFlag("--json --deep", cliCompletionNoValue), help}),
270 completionSpec("pairing", []cliCompletionFlag{help},
271 completionSpec("list", []cliCompletionFlag{help}),
272 completionSpec("approve", []cliCompletionFlag{help}),
273 completionSpecWithAliases("reject", []string{"deny"}, []cliCompletionFlag{help}),
274 ),
275 completionSpec("weixin-login", []cliCompletionFlag{completionFlag("--timeout", cliCompletionStaticValue), help}),
276 ),
277 completionSpecWithAliases("upgrade", []string{"update"}, []cliCompletionFlag{
278 completionFlag("--check --force", cliCompletionNoValue), completionFlag("--channel", cliCompletionStaticValue), help,
279 }),
280 completionSpec("completion", []cliCompletionFlag{help},
281 completionSpec("bash", nil),
282 completionSpec("zsh", nil),
283 completionSpec("fish", nil),
284 ),
285 completionSpec("version", []cliCompletionFlag{
286 completionFlag("--verbose --json", cliCompletionNoValue), help,
287 }),
288 completionSpec("docs-manifest", []cliCompletionFlag{help}),
289 completionSpec("help", []cliCompletionFlag{help}),
290 }
291 return root
292 }
293
294 func doctorCompletionSpec(help cliCompletionFlag) cliCompletionSpec {
295 return completionSpec("doctor", []cliCompletionFlag{completionFlag("--json", cliCompletionNoValue), help},
296 completionSpec("sessions", []cliCompletionFlag{completionFlag("--json", cliCompletionNoValue), help}),
297 completionSpec("repair", []cliCompletionFlag{
298 completionFlag("--root", cliCompletionStaticValue), completionFlag("--apply --project --json", cliCompletionNoValue), help,
299 }),
300 completionSpec("quality", []cliCompletionFlag{completionFlag("--json", cliCompletionNoValue), help}),
301 completionSpec("session", []cliCompletionFlag{completionFlag("--zip", cliCompletionNoValue), completionFlag("--out", cliCompletionStaticValue), help}),
302 completionSpec("redact-sessions", []cliCompletionFlag{completionFlag("--dry-run --json", cliCompletionNoValue), completionFlag("--dir", cliCompletionPathValue), help}),
303 completionSpec("capabilities", []cliCompletionFlag{
304 completionFlag("--root --timeout", cliCompletionStaticValue), completionFlag("--json --live", cliCompletionNoValue), help,
305 }),
306 )
307 }
308
309 func cliServeCompletionFlags(model, help cliCompletionFlag) []cliCompletionFlag {
310 return []cliCompletionFlag{
311 model,
312 completionFlag("--max-steps --addr", cliCompletionStaticValue),
313 // Serve/Web resume accepts file paths, not branch IDs.
314 completionFlag("--resume", cliCompletionPathValue),
315 completionFlag("--auth", cliCompletionStaticValue, "none", "token", "password"),
316 completionFlag("--token --password --port-file --token-file --pid-file", cliCompletionStaticValue),
317 completionFlag("--hash-password --behind-proxy --open --no-open", cliCompletionNoValue), help,
318 }
319 }
320
321 func subagentCompletionFlags(model, effort, help cliCompletionFlag) []cliCompletionFlag {
322 return []cliCompletionFlag{
323 completionFlag("--description --prompt --prompt-file --tools --color --dir", cliCompletionStaticValue),
324 model, effort, help,
325 }
326 }
327
328 func machineSessionCompletionFlags(help cliCompletionFlag) []cliCompletionFlag {
329 return []cliCompletionFlag{
330 completionFlag("--json", cliCompletionNoValue),
331 completionFlag("--dir --project-root", cliCompletionStaticValue), help,
332 }
333 }
334
335 func hookCompletionFlags(help cliCompletionFlag) []cliCompletionFlag {
336 return []cliCompletionFlag{
337 completionFlag("--json", cliCompletionNoValue),
338 completionFlag("--dir --project-root --home-dir", cliCompletionStaticValue), help,
339 }
340 }
341
342 // completionTask*Flags mirror the real FlagSets in task.go / task_machine.go.
343 // Names are prefixed with completion to avoid clashing with task.go helpers.
344
345 func completionTaskMachineListFlags(help cliCompletionFlag) []cliCompletionFlag {
346 return []cliCompletionFlag{
347 completionFlag("--json", cliCompletionNoValue),
348 completionFlag("--dir --project-root", cliCompletionPathValue),
349 completionFlag("--session", cliCompletionStaticValue), help,
350 }
351 }
352
353 func completionTaskMachineShowFlags(help cliCompletionFlag) []cliCompletionFlag {
354 return completionTaskMachineListFlags(help)
355 }
356
357 func completionTaskListFlags(help cliCompletionFlag) []cliCompletionFlag {
358 return []cliCompletionFlag{
359 completionFlag("--json", cliCompletionNoValue),
360 completionFlag("--dir", cliCompletionPathValue), help,
361 }
362 }
363
364 func completionTaskStatusFlags(help cliCompletionFlag) []cliCompletionFlag {
365 return completionTaskListFlags(help)
366 }
367
368 func completionTaskEventsFlags(help cliCompletionFlag) []cliCompletionFlag {
369 return []cliCompletionFlag{
370 completionFlag("--json --jsonl --follow", cliCompletionNoValue),
371 completionFlag("--dir", cliCompletionPathValue),
372 completionFlag("--after", cliCompletionStaticValue), help,
373 }
374 }
375
376 func completionTaskControlFlags(help cliCompletionFlag) []cliCompletionFlag {
377 return []cliCompletionFlag{
378 completionFlag("--json", cliCompletionNoValue),
379 completionFlag("--dir", cliCompletionPathValue),
380 completionFlag("--expected-version --reason --idempotency-key", cliCompletionStaticValue), help,
381 }
382 }
383
384 func completionTaskRequeueFlags(help cliCompletionFlag) []cliCompletionFlag {
385 return []cliCompletionFlag{
386 completionFlag("--json", cliCompletionNoValue),
387 completionFlag("--dir", cliCompletionPathValue),
388 completionFlag("--expected-version --idempotency-key", cliCompletionStaticValue), help,
389 }
390 }
391
392 func completionTaskOpenSessionFlags(help cliCompletionFlag) []cliCompletionFlag {
393 return completionTaskListFlags(help)
394 }
395
396 func completionTaskTmuxFlags(help cliCompletionFlag) []cliCompletionFlag {
397 return completionTaskListFlags(help)
398 }
399
400 func completionTaskTmuxAttachFlags(help cliCompletionFlag) []cliCompletionFlag {
401 return []cliCompletionFlag{
402 completionFlag("--json", cliCompletionNoValue),
403 completionFlag("--dir", cliCompletionPathValue),
404 completionFlag("--session", cliCompletionStaticValue), help,
405 }
406 }
407
408 func completionCommand(args []string) int {
409 if len(args) == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "help" {
410 completionUsage(os.Stdout)
411 if len(args) == 0 {
412 return 2
413 }
414 return 0
415 }
416 if args[0] == "__complete" {
417 return completionCandidateCommand(args[1:])
418 }
419 if len(args) != 1 {
420 completionUsage(os.Stderr)
421 return 2
422 }
423 var script string
424 switch args[0] {
425 case "bash":
426 script = bashCompletionScript
427 case "zsh":
428 script = zshCompletionScript
429 case "fish":
430 script = fishCompletionScript
431 default:
432 fmt.Fprintf(os.Stderr, "unknown shell %q (want bash, zsh, or fish)\n", args[0])
433 return 2
434 }
435 fmt.Print(script)
436 return 0
437 }
438
439 func completionUsage(w *os.File) {
440 fmt.Fprintln(w, `Usage:
441 reasonix completion bash
442 reasonix completion zsh
443 reasonix completion fish
444
445 The command prints a completion script to stdout. Source it directly or save it
446 in your shell's completion directory.`)
447 }
448
449 func completionCandidateCommand(args []string) int {
450 if len(args) < 1 {
451 return 2
452 }
453 cword, err := strconv.Atoi(args[0])
454 if err != nil || cword < 0 {
455 return 2
456 }
457 for _, candidate := range cliCompletionCandidates(cword, args[1:]) {
458 if candidate == "" || strings.ContainsAny(candidate, "\r\n") {
459 continue
460 }
461 fmt.Println(candidate)
462 }
463 return 0
464 }
465
466 func cliCompletionCandidates(cword int, words []string) []string {
467 return cliCompletionCandidatesWithValues(cliCompletionRootSpec(), cword, words, runtimeCompletionValues)
468 }
469
470 func cliCompletionCandidatesWithValues(root cliCompletionSpec, cword int, words []string, dynamic func(cliCompletionValueKind) []string) []string {
471 if cword < 0 {
472 return nil
473 }
474 current := ""
475 if cword < len(words) {
476 current = words[cword]
477 }
478 limit := min(cword, len(words))
479
480 ctx := &root
481 positionalSeen := false
482 var awaiting *cliCompletionFlag
483 for i := 1; i < limit; i++ {
484 token := words[i]
485 if awaiting != nil {
486 // Optional values may be omitted: a following flag-looking token is
487 // treated as the next flag rather than the optional value.
488 if awaiting.kind == cliCompletionOptionalValue && strings.HasPrefix(token, "-") && !strings.HasPrefix(token, "---") {
489 awaiting = nil
490 // fall through and parse token as a flag/subcommand
491 } else {
492 awaiting = nil
493 continue
494 }
495 }
496 if flag, inline := cliCompletionLookupFlag(ctx, token); flag != nil {
497 if flag.kind != cliCompletionNoValue && !inline {
498 awaiting = flag
499 }
500 continue
501 }
502 if strings.HasPrefix(token, "-") {
503 continue
504 }
505 if !positionalSeen {
506 if child := cliCompletionLookupSubcommand(ctx, token); child != nil {
507 ctx = child
508 continue
509 }
510 }
511 positionalSeen = true
512 }
513
514 if awaiting != nil {
515 if awaiting.kind == cliCompletionPathValue {
516 // Empty candidates → shell default file completion (bash -o default, zsh _default, fish without -f).
517 return nil
518 }
519 if awaiting.kind == cliCompletionOptionalValue && strings.HasPrefix(current, "-") {
520 // Completing another flag after optional-value flag with no value typed.
521 var out []string
522 for _, flag := range ctx.flags {
523 out = append(out, flag.names...)
524 }
525 return filterCompletionPrefix(stableUniqueCompletionValues(out), current)
526 }
527 return filterCompletionPrefix(stableUniqueCompletionValues(completionFlagValues(*awaiting, dynamic)), current)
528 }
529 if name, value, ok := strings.Cut(current, "="); ok {
530 if flag, _ := cliCompletionLookupFlag(ctx, name); flag != nil && flag.kind != cliCompletionNoValue {
531 values := completionFlagValues(*flag, dynamic)
532 out := make([]string, 0, len(values))
533 for _, candidate := range filterCompletionPrefix(values, value) {
534 out = append(out, name+"="+candidate)
535 }
536 return out
537 }
538 }
539 if strings.HasPrefix(current, "-") {
540 var out []string
541 for _, flag := range ctx.flags {
542 out = append(out, flag.names...)
543 }
544 return filterCompletionPrefix(stableUniqueCompletionValues(out), current)
545 }
546 if positionalSeen {
547 return nil
548 }
549 var out []string
550 for i := range ctx.subcommands {
551 out = append(out, ctx.subcommands[i].name)
552 }
553 return filterCompletionPrefix(stableUniqueCompletionValues(out), current)
554 }
555
556 func cliCompletionLookupFlag(spec *cliCompletionSpec, token string) (*cliCompletionFlag, bool) {
557 name := token
558 inline := false
559 if before, _, ok := strings.Cut(token, "="); ok {
560 name = before
561 inline = true
562 }
563 for i := range spec.flags {
564 if slices.Contains(spec.flags[i].names, name) {
565 return &spec.flags[i], inline
566 }
567 }
568 return nil, inline
569 }
570
571 func cliCompletionLookupSubcommand(spec *cliCompletionSpec, token string) *cliCompletionSpec {
572 for i := range spec.subcommands {
573 child := &spec.subcommands[i]
574 if child.name == token {
575 return child
576 }
577 if slices.Contains(child.aliases, token) {
578 return child
579 }
580 }
581 return nil
582 }
583
584 func completionFlagValues(flag cliCompletionFlag, dynamic func(cliCompletionValueKind) []string) []string {
585 switch flag.kind {
586 case cliCompletionStaticValue:
587 return stableUniqueCompletionValues(flag.values)
588 case cliCompletionOptionalValue:
589 // Interactive --resume [QUERY]: static values (if any) plus dynamic session IDs.
590 // Used for both separated and --resume=QUERY forms.
591 out := append([]string{}, flag.values...)
592 out = append(out, dynamic(cliCompletionSessionValue)...)
593 return stableUniqueCompletionValues(out)
594 case cliCompletionPathValue:
595 return nil
596 case cliCompletionModelValue, cliCompletionSessionValue:
597 return stableUniqueCompletionValues(dynamic(flag.kind))
598 default:
599 return nil
600 }
601 }
602
603 func runtimeCompletionValues(kind cliCompletionValueKind) []string {
604 switch kind {
605 case cliCompletionModelValue:
606 return modelRefs()
607 case cliCompletionSessionValue:
608 return completionSessionIDs()
609 default:
610 return nil
611 }
612 }
613
614 func completionSessionIDs() []string {
615 ordered, err := agent.ListSessionOrder(resolveCLISessionDir())
616 if err != nil {
617 return nil
618 }
619 out := make([]string, 0, len(ordered))
620 for _, session := range ordered {
621 out = append(out, agent.BranchID(session.Path))
622 }
623 return stableUniqueCompletionValues(out)
624 }
625
626 func filterCompletionPrefix(values []string, prefix string) []string {
627 out := make([]string, 0, len(values))
628 for _, value := range values {
629 if strings.HasPrefix(value, prefix) {
630 out = append(out, value)
631 }
632 }
633 return out
634 }
635
636 func stableUniqueCompletionValues(values []string) []string {
637 seen := make(map[string]bool, len(values))
638 out := make([]string, 0, len(values))
639 for _, value := range values {
640 value = strings.TrimSpace(value)
641 if value == "" || seen[value] {
642 continue
643 }
644 seen[value] = true
645 out = append(out, value)
646 }
647 return out
648 }
649
650 const bashCompletionScript = `# bash completion for reasonix
651 _reasonix_completion() {
652 local line
653 COMPREPLY=()
654 while IFS= read -r line; do
655 COMPREPLY+=("$line")
656 done < <(command reasonix completion __complete "$COMP_CWORD" "${COMP_WORDS[@]}" 2>/dev/null)
657 }
658 complete -o default -F _reasonix_completion reasonix
659 `
660
661 const zshCompletionScript = `#compdef reasonix
662 _reasonix_completion() {
663 local -a candidates
664 candidates=("${(@f)$(command reasonix completion __complete "$((CURRENT - 1))" "${words[@]}" 2>/dev/null)}")
665 if (( ${#candidates[@]} )); then
666 compadd -- "${candidates[@]}"
667 else
668 _default
669 fi
670 }
671 compdef _reasonix_completion reasonix
672 `
673
674 const fishCompletionScript = `function __reasonix_completion
675 set -l tokens (commandline -opc)
676 set -a tokens (commandline -ct)
677 set -l current_index (math (count $tokens) - 1)
678 command reasonix completion __complete $current_index $tokens 2>/dev/null
679 end
680 complete -c reasonix -a '(__reasonix_completion)'
681 `
682
682 lines GO