返回 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 hookCompletionSpec(help),
234 completionSpec("trust", []cliCompletionFlag{completionFlag("--dir", cliCompletionPathValue), completionFlag("--yes -y --revoke", cliCompletionNoValue), help}),
235 completionSpec("task", []cliCompletionFlag{help},
236 // Machine list/show: --json --dir --project-root --session (task_machine.go).
237 completionSpec("list", completionTaskMachineListFlags(help)),
238 completionSpec("show", completionTaskMachineShowFlags(help)),
239 // Live monitor surface (task.go FlagSets).
240 completionSpec("monitor", []cliCompletionFlag{help},
241 completionSpec("list", completionTaskListFlags(help)),
242 completionSpec("status", completionTaskStatusFlags(help)),
243 completionSpec("events", completionTaskEventsFlags(help)),
244 completionSpec("stop", completionTaskControlFlags(help)),
245 completionSpec("cancel", completionTaskControlFlags(help)),
246 completionSpec("requeue", completionTaskRequeueFlags(help)),
247 completionSpec("open-session", completionTaskOpenSessionFlags(help)),
248 ),
249 completionSpec("status", completionTaskStatusFlags(help)),
250 completionSpec("events", completionTaskEventsFlags(help)),
251 completionSpec("stop", completionTaskControlFlags(help)),
252 completionSpec("cancel", completionTaskControlFlags(help)),
253 completionSpec("requeue", completionTaskRequeueFlags(help)),
254 completionSpec("open-session", completionTaskOpenSessionFlags(help)),
255 completionSpec("tmux", []cliCompletionFlag{help},
256 completionSpec("attach", completionTaskTmuxAttachFlags(help)),
257 completionSpec("status", completionTaskTmuxFlags(help)),
258 completionSpec("open", completionTaskTmuxFlags(help)),
259 completionSpec("detach", completionTaskTmuxFlags(help)),
260 ),
261 ),
262 completionSpec("review", []cliCompletionFlag{
263 completionFlag("--base --commit --instructions", cliCompletionStaticValue), model, help,
264 }),
265 completionSpec("bot", []cliCompletionFlag{help},
266 completionSpec("start", []cliCompletionFlag{completionFlag("--channels --dir", cliCompletionStaticValue), model, help}),
267 completionSpec("doctor", []cliCompletionFlag{completionFlag("--json --deep", cliCompletionNoValue), help}),
268 completionSpec("pairing", []cliCompletionFlag{help},
269 completionSpec("list", []cliCompletionFlag{help}),
270 completionSpec("approve", []cliCompletionFlag{help}),
271 completionSpecWithAliases("reject", []string{"deny"}, []cliCompletionFlag{help}),
272 ),
273 completionSpec("weixin-login", []cliCompletionFlag{completionFlag("--timeout", cliCompletionStaticValue), help}),
274 ),
275 completionSpecWithAliases("upgrade", []string{"update"}, []cliCompletionFlag{
276 completionFlag("--check --force", cliCompletionNoValue), completionFlag("--channel", cliCompletionStaticValue), help,
277 }),
278 completionSpec("completion", []cliCompletionFlag{help},
279 completionSpec("bash", nil),
280 completionSpec("zsh", nil),
281 completionSpec("fish", nil),
282 ),
283 completionSpec("version", []cliCompletionFlag{
284 completionFlag("--verbose --json", cliCompletionNoValue), help,
285 }),
286 completionSpec("docs-manifest", []cliCompletionFlag{help}),
287 completionSpec("help", []cliCompletionFlag{help}),
288 }
289 return root
290 }
291
292 func doctorCompletionSpec(help cliCompletionFlag) cliCompletionSpec {
293 return completionSpec("doctor", []cliCompletionFlag{completionFlag("--json", cliCompletionNoValue), help},
294 completionSpec("sessions", []cliCompletionFlag{completionFlag("--json", cliCompletionNoValue), help}),
295 completionSpec("repair", []cliCompletionFlag{
296 completionFlag("--root", cliCompletionStaticValue), completionFlag("--apply --project --json", cliCompletionNoValue), help,
297 }),
298 completionSpec("quality", []cliCompletionFlag{completionFlag("--json", cliCompletionNoValue), help}),
299 completionSpec("session", []cliCompletionFlag{completionFlag("--zip", cliCompletionNoValue), completionFlag("--out", cliCompletionStaticValue), help}),
300 completionSpec("redact-sessions", []cliCompletionFlag{completionFlag("--dry-run --json", cliCompletionNoValue), completionFlag("--dir", cliCompletionPathValue), help}),
301 completionSpec("capabilities", []cliCompletionFlag{
302 completionFlag("--root --timeout", cliCompletionStaticValue), completionFlag("--json --live", cliCompletionNoValue), help,
303 }),
304 )
305 }
306
307 func cliServeCompletionFlags(model, help cliCompletionFlag) []cliCompletionFlag {
308 return []cliCompletionFlag{
309 model,
310 completionFlag("--max-steps --addr", cliCompletionStaticValue),
311 // Serve/Web resume accepts file paths, not branch IDs.
312 completionFlag("--resume", cliCompletionPathValue),
313 completionFlag("--auth", cliCompletionStaticValue, "none", "token", "password"),
314 completionFlag("--token --password --port-file --token-file --pid-file", cliCompletionStaticValue),
315 completionFlag("--hash-password --behind-proxy --open --no-open", cliCompletionNoValue), help,
316 }
317 }
318
319 func subagentCompletionFlags(model, effort, help cliCompletionFlag) []cliCompletionFlag {
320 return []cliCompletionFlag{
321 completionFlag("--description --prompt --prompt-file --tools --color --dir", cliCompletionStaticValue),
322 model, effort, help,
323 }
324 }
325
326 func machineSessionCompletionFlags(help cliCompletionFlag) []cliCompletionFlag {
327 return []cliCompletionFlag{
328 completionFlag("--json", cliCompletionNoValue),
329 completionFlag("--dir --project-root", cliCompletionStaticValue), help,
330 }
331 }
332
333 func hookCompletionSpec(help cliCompletionFlag) cliCompletionSpec {
334 return completionSpecWithAliases("hook", []string{"hooks"}, []cliCompletionFlag{help},
335 completionSpec("list", hookCompletionFlags(help)),
336 completionSpec("status", hookCompletionFlags(help)),
337 )
338 }
339
340 func hookCompletionFlags(help cliCompletionFlag) []cliCompletionFlag {
341 return []cliCompletionFlag{
342 completionFlag("--json", cliCompletionNoValue),
343 completionFlag("--dir --project-root --home-dir", cliCompletionStaticValue), help,
344 }
345 }
346
347 // completionTask*Flags mirror the real FlagSets in task.go / task_machine.go.
348 // Names are prefixed with completion to avoid clashing with task.go helpers.
349
350 func completionTaskMachineListFlags(help cliCompletionFlag) []cliCompletionFlag {
351 return []cliCompletionFlag{
352 completionFlag("--json", cliCompletionNoValue),
353 completionFlag("--dir --project-root", cliCompletionPathValue),
354 completionFlag("--session", cliCompletionStaticValue), help,
355 }
356 }
357
358 func completionTaskMachineShowFlags(help cliCompletionFlag) []cliCompletionFlag {
359 return completionTaskMachineListFlags(help)
360 }
361
362 func completionTaskListFlags(help cliCompletionFlag) []cliCompletionFlag {
363 return []cliCompletionFlag{
364 completionFlag("--json", cliCompletionNoValue),
365 completionFlag("--dir", cliCompletionPathValue), help,
366 }
367 }
368
369 func completionTaskStatusFlags(help cliCompletionFlag) []cliCompletionFlag {
370 return completionTaskListFlags(help)
371 }
372
373 func completionTaskEventsFlags(help cliCompletionFlag) []cliCompletionFlag {
374 return []cliCompletionFlag{
375 completionFlag("--json --jsonl --follow", cliCompletionNoValue),
376 completionFlag("--dir", cliCompletionPathValue),
377 completionFlag("--after", cliCompletionStaticValue), help,
378 }
379 }
380
381 func completionTaskControlFlags(help cliCompletionFlag) []cliCompletionFlag {
382 return []cliCompletionFlag{
383 completionFlag("--json", cliCompletionNoValue),
384 completionFlag("--dir", cliCompletionPathValue),
385 completionFlag("--expected-version --reason --idempotency-key", cliCompletionStaticValue), help,
386 }
387 }
388
389 func completionTaskRequeueFlags(help cliCompletionFlag) []cliCompletionFlag {
390 return []cliCompletionFlag{
391 completionFlag("--json", cliCompletionNoValue),
392 completionFlag("--dir", cliCompletionPathValue),
393 completionFlag("--expected-version --idempotency-key", cliCompletionStaticValue), help,
394 }
395 }
396
397 func completionTaskOpenSessionFlags(help cliCompletionFlag) []cliCompletionFlag {
398 return completionTaskListFlags(help)
399 }
400
401 func completionTaskTmuxFlags(help cliCompletionFlag) []cliCompletionFlag {
402 return completionTaskListFlags(help)
403 }
404
405 func completionTaskTmuxAttachFlags(help cliCompletionFlag) []cliCompletionFlag {
406 return []cliCompletionFlag{
407 completionFlag("--json", cliCompletionNoValue),
408 completionFlag("--dir", cliCompletionPathValue),
409 completionFlag("--session", cliCompletionStaticValue), help,
410 }
411 }
412
413 func completionCommand(args []string) int {
414 if len(args) == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "help" {
415 completionUsage(os.Stdout)
416 if len(args) == 0 {
417 return 2
418 }
419 return 0
420 }
421 if args[0] == "__complete" {
422 return completionCandidateCommand(args[1:])
423 }
424 if len(args) != 1 {
425 completionUsage(os.Stderr)
426 return 2
427 }
428 var script string
429 switch args[0] {
430 case "bash":
431 script = bashCompletionScript
432 case "zsh":
433 script = zshCompletionScript
434 case "fish":
435 script = fishCompletionScript
436 default:
437 fmt.Fprintf(os.Stderr, "unknown shell %q (want bash, zsh, or fish)\n", args[0])
438 return 2
439 }
440 fmt.Print(script)
441 return 0
442 }
443
444 func completionUsage(w *os.File) {
445 fmt.Fprintln(w, `Usage:
446 reasonix completion bash
447 reasonix completion zsh
448 reasonix completion fish
449
450 The command prints a completion script to stdout. Source it directly or save it
451 in your shell's completion directory.`)
452 }
453
454 func completionCandidateCommand(args []string) int {
455 if len(args) < 1 {
456 return 2
457 }
458 cword, err := strconv.Atoi(args[0])
459 if err != nil || cword < 0 {
460 return 2
461 }
462 for _, candidate := range cliCompletionCandidates(cword, args[1:]) {
463 if candidate == "" || strings.ContainsAny(candidate, "\r\n") {
464 continue
465 }
466 fmt.Println(candidate)
467 }
468 return 0
469 }
470
471 func cliCompletionCandidates(cword int, words []string) []string {
472 return cliCompletionCandidatesWithValues(cliCompletionRootSpec(), cword, words, runtimeCompletionValues)
473 }
474
475 func cliCompletionCandidatesWithValues(root cliCompletionSpec, cword int, words []string, dynamic func(cliCompletionValueKind) []string) []string {
476 if cword < 0 {
477 return nil
478 }
479 current := ""
480 if cword < len(words) {
481 current = words[cword]
482 }
483 limit := min(cword, len(words))
484
485 ctx := &root
486 positionalSeen := false
487 var awaiting *cliCompletionFlag
488 for i := 1; i < limit; i++ {
489 token := words[i]
490 if awaiting != nil {
491 // Optional values may be omitted: a following flag-looking token is
492 // treated as the next flag rather than the optional value.
493 if awaiting.kind == cliCompletionOptionalValue && strings.HasPrefix(token, "-") && !strings.HasPrefix(token, "---") {
494 awaiting = nil
495 // fall through and parse token as a flag/subcommand
496 } else {
497 awaiting = nil
498 continue
499 }
500 }
501 if flag, inline := cliCompletionLookupFlag(ctx, token); flag != nil {
502 if flag.kind != cliCompletionNoValue && !inline {
503 awaiting = flag
504 }
505 continue
506 }
507 if strings.HasPrefix(token, "-") {
508 continue
509 }
510 if !positionalSeen {
511 if child := cliCompletionLookupSubcommand(ctx, token); child != nil {
512 ctx = child
513 continue
514 }
515 }
516 positionalSeen = true
517 }
518
519 if awaiting != nil {
520 if awaiting.kind == cliCompletionPathValue {
521 // Empty candidates → shell default file completion (bash -o default, zsh _default, fish without -f).
522 return nil
523 }
524 if awaiting.kind == cliCompletionOptionalValue && strings.HasPrefix(current, "-") {
525 // Completing another flag after optional-value flag with no value typed.
526 var out []string
527 for _, flag := range ctx.flags {
528 out = append(out, flag.names...)
529 }
530 return filterCompletionPrefix(stableUniqueCompletionValues(out), current)
531 }
532 return filterCompletionPrefix(stableUniqueCompletionValues(completionFlagValues(*awaiting, dynamic)), current)
533 }
534 if name, value, ok := strings.Cut(current, "="); ok {
535 if flag, _ := cliCompletionLookupFlag(ctx, name); flag != nil && flag.kind != cliCompletionNoValue {
536 values := completionFlagValues(*flag, dynamic)
537 out := make([]string, 0, len(values))
538 for _, candidate := range filterCompletionPrefix(values, value) {
539 out = append(out, name+"="+candidate)
540 }
541 return out
542 }
543 }
544 if strings.HasPrefix(current, "-") {
545 var out []string
546 for _, flag := range ctx.flags {
547 out = append(out, flag.names...)
548 }
549 return filterCompletionPrefix(stableUniqueCompletionValues(out), current)
550 }
551 if positionalSeen {
552 return nil
553 }
554 var out []string
555 for i := range ctx.subcommands {
556 out = append(out, ctx.subcommands[i].name)
557 }
558 return filterCompletionPrefix(stableUniqueCompletionValues(out), current)
559 }
560
561 func cliCompletionLookupFlag(spec *cliCompletionSpec, token string) (*cliCompletionFlag, bool) {
562 name := token
563 inline := false
564 if before, _, ok := strings.Cut(token, "="); ok {
565 name = before
566 inline = true
567 }
568 for i := range spec.flags {
569 if slices.Contains(spec.flags[i].names, name) {
570 return &spec.flags[i], inline
571 }
572 }
573 return nil, inline
574 }
575
576 func cliCompletionLookupSubcommand(spec *cliCompletionSpec, token string) *cliCompletionSpec {
577 for i := range spec.subcommands {
578 child := &spec.subcommands[i]
579 if child.name == token {
580 return child
581 }
582 if slices.Contains(child.aliases, token) {
583 return child
584 }
585 }
586 return nil
587 }
588
589 func completionFlagValues(flag cliCompletionFlag, dynamic func(cliCompletionValueKind) []string) []string {
590 switch flag.kind {
591 case cliCompletionStaticValue:
592 return stableUniqueCompletionValues(flag.values)
593 case cliCompletionOptionalValue:
594 // Interactive --resume [QUERY]: static values (if any) plus dynamic session IDs.
595 // Used for both separated and --resume=QUERY forms.
596 out := append([]string{}, flag.values...)
597 out = append(out, dynamic(cliCompletionSessionValue)...)
598 return stableUniqueCompletionValues(out)
599 case cliCompletionPathValue:
600 return nil
601 case cliCompletionModelValue, cliCompletionSessionValue:
602 return stableUniqueCompletionValues(dynamic(flag.kind))
603 default:
604 return nil
605 }
606 }
607
608 func runtimeCompletionValues(kind cliCompletionValueKind) []string {
609 switch kind {
610 case cliCompletionModelValue:
611 return modelRefs()
612 case cliCompletionSessionValue:
613 return completionSessionIDs()
614 default:
615 return nil
616 }
617 }
618
619 func completionSessionIDs() []string {
620 ordered, err := agent.ListSessionOrder(resolveCLISessionDir())
621 if err != nil {
622 return nil
623 }
624 out := make([]string, 0, len(ordered))
625 for _, session := range ordered {
626 out = append(out, agent.BranchID(session.Path))
627 }
628 return stableUniqueCompletionValues(out)
629 }
630
631 func filterCompletionPrefix(values []string, prefix string) []string {
632 out := make([]string, 0, len(values))
633 for _, value := range values {
634 if strings.HasPrefix(value, prefix) {
635 out = append(out, value)
636 }
637 }
638 return out
639 }
640
641 func stableUniqueCompletionValues(values []string) []string {
642 seen := make(map[string]bool, len(values))
643 out := make([]string, 0, len(values))
644 for _, value := range values {
645 value = strings.TrimSpace(value)
646 if value == "" || seen[value] {
647 continue
648 }
649 seen[value] = true
650 out = append(out, value)
651 }
652 return out
653 }
654
655 const bashCompletionScript = `# bash completion for reasonix
656 _reasonix_completion() {
657 local line
658 COMPREPLY=()
659 while IFS= read -r line; do
660 COMPREPLY+=("$line")
661 done < <(command reasonix completion __complete "$COMP_CWORD" "${COMP_WORDS[@]}" 2>/dev/null)
662 }
663 complete -o default -F _reasonix_completion reasonix
664 `
665
666 const zshCompletionScript = `#compdef reasonix
667 _reasonix_completion() {
668 local -a candidates
669 candidates=("${(@f)$(command reasonix completion __complete "$((CURRENT - 1))" "${words[@]}" 2>/dev/null)}")
670 if (( ${#candidates[@]} )); then
671 compadd -- "${candidates[@]}"
672 else
673 _default
674 fi
675 }
676 compdef _reasonix_completion reasonix
677 `
678
679 const fishCompletionScript = `function __reasonix_completion
680 set -l tokens (commandline -opc)
681 set -a tokens (commandline -ct)
682 set -l current_index (math (count $tokens) - 1)
683 command reasonix completion __complete $current_index $tokens 2>/dev/null
684 end
685 complete -c reasonix -a '(__reasonix_completion)'
686 `
687
687 lines GO