Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **A flag before the workspace spec no longer stops tab completion.**
`aid --codex owner/repo<TAB>` offered nothing at all, and neither did
`aid --claude`, `aid --gemini` or `dl --devcontainer robot owner/repo<TAB>`.
The completion script found the spec by counting words from the command, word
two for the spec and word three for a verb, and neither grammar works that
way: `parse_aid_args` reads aid's leading flags and calls the first word that
is not one the spec, and dl is clap, which puts options anywhere among the
positional words.

The script now scans the words before the cursor and counts the positional
ones, stepping over a value option with its value and stopping at `--`, so
the spec is wherever it actually lands. `dl --rm my-ws<TAB>` completes a
workspace, `dl --rm my-ws <TAB>` completes a verb, and `aid --codex my-ws`
completes exactly what `aid my-ws` does, trailing space included.

That needs a distinction the old guard could not make, since it ended
completion on any leading `--`: a flag a spec may follow against one that
ends the line. The table lists the first, three flags for dl and nine for
aid, and every other flag ends the line. That direction is deliberate and it
is where the first attempt went wrong: listing the endings instead put ten
flags in the wrong arm, because the ones nobody thinks to list are all on
that side. `dl --repos my-workspace` answers "--repos takes no workspace",
`dl --json my-workspace` is a clap error about the missing `--ls`, and
`dl --force my-workspace` answers "Unknown workspace '--force'" because a
leading `--force` is the workspace slot itself. Completing a name onto any of
those is worse than completing nothing, so the default arm is the refusal.

Both tables are derived rather than hand-judged, and
`rust/dl/tests/completion_tables.rs` diffs them: dl's is every flag the
grammar declares minus clap's `what` group, minus the hidden ones, minus the
five that need something already on the line, and aid's is the three tables
`parse_aid_args` reads past.

- **Editing prose under `.devcontainer/` no longer throws away the prebuilt
container.** Opening this repository with `dl` pulls a published image instead
of building one, and it had gone back to building. The prebuild tag is a hash
Expand Down
44 changes: 44 additions & 0 deletions docs/workspace-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,50 @@ After running `dl --install`, tab completion offers:
- File/directory paths when starting with `./`, `/`, or `~`
- All global flags (`--ls`, `--install`, etc.) and workspace commands

### A flag before the spec does not move it

Both command lines take flags ahead of the workspace, and the completion reads
them rather than counting words:

```
$ aid --codex kin<TAB>
kinisi-robotics/
$ dl --devcontainer robot kin<TAB>
kinisi-robotics/
```

Counting is what this used to do, and it put the spec at the second word alone,
so every one of those lines completed nothing. The rule each command actually
follows is different. aid reads its leading flags and calls the first word that
is not one the spec, which is `parse_aid_args`, and dl is a clap grammar, which
lets an option sit anywhere among the positional words.

The flags a spec may follow are the ones that modify a launch: `--rm`,
`--devcontainer` and `--claude-profile` for `dl`, and for `aid` those two plus
the agent flags and either polarity of `--remote-control`. Every other flag ends
the line, and nothing is offered after one:

```
$ dl --ls <TAB>
$ dl --json <TAB>
$ dl --repos <TAB>
```

Listing the flags a spec may follow, rather than the flags that end the line, is
the load-bearing choice. The flags nobody thinks to list are all on the ending
side, and each of them refuses a workspace for a different reason: `--repos`
answers "--repos takes no workspace", `--json` is an error about the missing
`--ls` it requires, `--yes` is refused as meaningless for a workspace command,
and a leading `--force` is not a modifier at all but the workspace slot itself,
so `dl --force my-ws` answers "Unknown workspace '--force'". Tabbing to a name
and then being refused for a word you never typed is worse than no completion,
which is the same bar the profile names are held to under
[Naming a profile](#naming-a-profile).

Once one modifier is on the line the only flags still offered are the other
modifiers, since `dl --rm --ls` is refused and `aid --codex --help` is not aid's
help but an unknown option handed to dl.

### Owners come before workspace ids, and why

The first word of a `dl` line can be two different things, a spec or the id of a
Expand Down
127 changes: 105 additions & 22 deletions rust/devlaunch-core/completions/dl.bash
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,17 @@ _dl_completion() {
cmd="${words[0]##*/}"
fi

# Global command options (only valid as first arg).
# The options offered where a workspace spec goes.
#
# Every user-facing flag dl's argument grammar declares, and a test diffs the
# two: `dl/tests/completion_tables.rs`. Four are deliberately absent —
# --json, --size, --yes and --force modify a line that already named a
# command, so none of them is ever the first word — and that test names them
# with the reason. Anything added below has to be added there too.
# two: `dl/tests/completion_tables.rs`. Five are deliberately absent —
# --json, --size, --yes, --force and --force-worktrees each need something
# already on the line, so none is ever the first word, and that test names
# them with the reason. Anything added below has to be added there too.
#
# "Where the spec goes" is not "the second word": a modifying flag can come
# first, so these are offered after one too (`dl --rm --<tab>`). Which word
# that is, is the scan further down.
#
# The retired spellings (--stop, --autorm) are absent by rule rather than by
# hand: the grammar marks them `hide = true`, and the test drops every hidden
Expand All @@ -80,6 +84,40 @@ _dl_completion() {
# Options that take a value; a variant name, a profile name or a path follows.
local value_opts="--devcontainer --claude-profile"

# The flags a workspace spec may still follow: they modify a launch instead
# of being one. Every other flag ends the line, and that direction is the
# load-bearing half -- listing the flags that *end* it instead put ten flags
# in the wrong arm at once, because the ones nobody thinks to list are all on
# that side: the command group's hidden members (--repos, --update-cache,
# --completion-data), the five that modify a command already on the line
# (--json, --size, --yes, --force, --force-worktrees), and the two retired
# spellings. `dl --json my-workspace` is a clap error and `dl --repos
# my-workspace` answers "--repos takes no workspace", so completing a name
# after either is completing onto a refusal.
#
# Derived rather than judged, from three tables that are each already pinned:
# the grammar's flags, minus clap's `what` group, minus the hidden ones,
# minus `NOT_OFFERED_FIRST`. `dl/tests/completion_tables.rs` does that
# subtraction and diffs the answer against this line.
#
# "Ends the line" means no *workspace* follows, which is not quite the same
# as no word: `dl --install [<rc-file>]` takes an optional path. Nothing
# completes that path, here or before this scan existed, and offering it
# would mean a second exception rather than a wider `spec_follows` -- the
# thing that follows is not a spec, and the branch below that handles `./`
# is inside the spec position.
local spec_follows="--rm --devcontainer --claude-profile"
if [[ "$cmd" == aid ]]; then
# aid's own, from `parse_aid_args`: it reads an agent flag, a remote
# control flag or a dl value option and keeps looking for the spec. The
# three it answers itself (--help, -h, --version) are absent because they
# end the line, and so is an unknown flag -- aid cannot tell whether one
# takes a value, so on `aid --unknown-taking-a-value foo owner/repo` it
# calls `foo` the spec, and completing a slot aid itself cannot place is
# worse than completing nothing.
spec_follows="--claude --codex --gemini --remote-control --remote --no-remote-control --no-remote --devcontainer --claude-profile"
fi

# After --claude-profile, offer the profile directories that exist. Read off the
# disk rather than out of the completion cache, deliberately: profiles are
# created by hand and rarely, the cache is rebuilt by commands that change
Expand Down Expand Up @@ -157,11 +195,66 @@ _dl_completion() {
source "$cache_file"
fi

# First argument: global flags, workspaces, repos, owners, or paths
if [[ ${word_count} -eq 2 ]]; then
# Global flags
# Which positional slot the word being completed sits in: the spec is the
# first, a verb the second.
#
# Read off the words before it rather than counted from the command, because
# a flag can precede the spec in both grammars and counting cannot see that.
# `aid --codex owner/repo` is the line that reported this: the spec was
# offered at word two alone, so every agent-flag line completed nothing, and
# so did `dl --devcontainer robot owner/repo`. dl's rule is clap's -- options
# sit anywhere among the positional words -- and aid's is `parse_aid_args`,
# which reads the leading flags and calls the first word that is not one the
# spec.
#
# The words strictly before `cur` are indices 1 through word_count-2, which
# holds whether or not the line ends in a space: the trailing-space branch
# above incremented word_count without appending to `words`.
local position=0 ends_here=0 modified=0 scan=1 scanned
while (( scan <= word_count - 2 )); do
scanned="${words[scan]}"
if [[ "$scanned" == "--" ]]; then
# Everything past it is the command run inside the workspace, which
# is the user's shell to complete and not ours.
return 0
fi
if [[ "$scanned" == -* ]]; then
if [[ " ${spec_follows} " == *" ${scanned} "* ]]; then
modified=1
else
ends_here=1
fi
if [[ " ${value_opts} " == *" ${scanned} "* ]]; then
# Its value is not a positional word, so step over the pair.
(( scan += 2 ))
else
(( scan++ ))
fi
continue
fi
(( position++ ))
(( scan++ ))
done

# A line carrying a flag no spec follows takes neither a workspace nor a
# verb. This is what used to be a guard on the first word starting with
# `--`, which could not tell `dl --ls` from `dl --rm`.
if (( ends_here )); then
return 0
fi

# The spec's position: flags, workspaces, repos, owners, or paths.
if (( position == 0 )); then
# Flags. Once one modifier is on the line the line is a launch, so the
# only flags that can still precede the spec are the other modifiers:
# `dl --rm --ls` is refused, and `aid --codex --help` is not aid's help
# (that is `argv[0]`) but an unknown option handed to dl.
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${global_opts}" -- ${cur}) )
if (( modified )); then
COMPREPLY=( $(compgen -W "${spec_follows}" -- ${cur}) )
else
COMPREPLY=( $(compgen -W "${global_opts}" -- ${cur}) )
fi
return 0
fi

Expand Down Expand Up @@ -247,19 +340,9 @@ _dl_completion() {
return 0
fi

# Second argument (after workspace): subcommands. Everything after an aid
# workspace is the prompt, so there is nothing to offer there.
if [[ ${word_count} -eq 3 && "$cmd" != aid ]]; then
# Don't complete after global flags
# Extract the first argument (word after "dl") from the words array
local first=""
if (( ${#words[@]} > 1 )); then
first="${words[1]}"
fi
if [[ "$first" == --* ]]; then
return 0
fi

# The verb's position, after the spec. Everything after an aid workspace is
# the prompt, so there is nothing to offer there.
if (( position == 1 )) && [[ "$cmd" != aid ]]; then
COMPREPLY=( $(compgen -W "${ws_cmds}" -- ${cur}) )
return 0
fi
Expand Down
Loading