feat(slash): /init command with SlashOutcome trait refactor#59
Merged
Conversation
Adds the third command kind — prompt-submit — backing the new `/init` command. `SlashCommand::execute` now returns `Result<SlashOutcome, String>` where `SlashOutcome` carries either `Local` (the existing client-side semantics) or `PromptSubmit(String)` (`/init`'s synthesized body). The dispatcher forwards the synthesized body via its return; the App-side wiring lands in a follow-up commit. `/init` adapts Claude Code's `OLD_INIT_PROMPT` to the AGENTS.md / CLAUDE.md convention oxide-code's instruction loader walks. Aliases none; `is_read_only=false` so the busy-turn dispatcher refuses with the standard system message instead of starting a parallel turn. Existing impls (`/help`, `/clear`, `/status`, `/config`, `/diff`) swap `Ok(())` for `Ok(SlashOutcome::Local)`; popup snapshots regenerated to include the new `/init` row.
`apply_action_locally`'s slash branch now reads `slash::dispatch`'s return: on `Some(prompt)` it flips input + status to `Streaming` and forwards `UserAction::SubmitPrompt(prompt)` — turn-start UI side effects mirror a typed prompt, so no race window where the input could accept another submit before the synthesized prompt hits the agent loop. Extracts `forward_to_agent` from `dispatch_user_action` so the new synthesized-prompt path reuses the same channel-error handling.
New per-command doc at `docs/research/design/slash-commands/init.md` covering the prompt-body decision (OLD_INIT_PROMPT adapted for AGENTS.md), the `SlashOutcome::PromptSubmit` send-after-dispatch ordering, and what's deferred (the multi-phase interactive flow, `progressMessage`, onboarding-state recording, harness-side instruction-file detection). Surface-level updates: slash-commands/README.md gains decision 12 (three command kinds via `SlashOutcome`), and refreshes the oxide-code comparison row + Today prose to reflect the actual shipped surface (six built-ins, not none). Roadmap moves `/init` to Working Today and folds its multi-phase flow into Deferred. CLAUDE.md crate tree picks up `slash/init.rs` and the updated `registry.rs` description.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The file lives only under `.claude/plans/` (gitignored), so readers fetching the repo can't follow the links. Replace each citation with the gating subsystem name (`AgentEvent::PromptRequest`) inline.
The /clear and /init Working-Today bullets and the Deferred bullet each unspooled into 2-3 sentences with stacked parentheticals. Compress to one short sentence per bullet; drop SlashOutcome implementation details that belong in the design doc.
Restore three load-bearing rules dropped from the OLD_INIT_PROMPT adaptation: forbid fabricated sections (no `Common Tasks` / `Tips for Development` headers), forbid restating the same fact across sections, and replace "prefer AGENTS.md when both exist" with the three-case rule (neither / one / both) — the previous default would push migration on a repo that already standardized on CLAUDE.md. Tighten rule 4 from "non-obvious gotchas" (overlapped rule 3) to "external constraints the code can't reveal". Drop the oxide-code-first parenthetical from the opener; alphabetize the prefix-block AI assistant list. Hard-wrap to ~74 chars matching prompt/sections.rs. Drop "instruction file" trailing noun from the description (singular read off when naming two files). Trim the module docstring to scope + design-doc pointer; let the registry comment own the trait contract. Replace the PROMPT doc comment's `OLD_INIT_PROMPT` reference with the gating subsystem (`AgentEvent::PromptRequest`).
Add docs/guide/slash-commands.md as the canonical user-facing reference: built-in command table, autocomplete popup mechanics, the `//foo` literal-escape, mid-turn refusal policy, and the "no silent config writes" stance. Place it after Configuration in the guide index — slash commands are everyday-use surface, more frequently needed than instruction-file setup. Roadmap's slash-command Working-Today bullets unspooled the user guide's contents into seven dense paragraphs about UI mechanics, parser policy, and alias display. Compress to three signal-only bullets (commands shipped, autocomplete exists, design stance); the user guide owns the rest. Add the new doc to README.md and docs/guide/README.md.
The Documentation table mirrored docs/guide/README.md row-for-row. Two indexes in two places drift apart — replace the README table with a single link to docs/guide/, which is the canonical index.
Drop the subsumed `execute_returns_prompt_submit_with_non_empty_body`
in init.rs — `execute_prompt_targets_agents_md_and_claude_md`
already implies a non-empty body. Drop the description-non-emptiness
check in `metadata_matches_built_ins_contract` for the same reason
(covered by the registry's built-ins contract test).
Strengthen `execute_prompt_says_do_not_overwrite_existing_file` to
`contains("already exists") && contains("not overwrite")`. The
previous `||` over two phrasings would pass a prompt that says
"please overwrite all existing files" since "overwrite" is retained.
Replace `let-else { panic! }` destructure patterns with
`assert!(matches!(...))` — drops dead-by-design sentinel arms that
codecov flags and tightens the diagnostic to one line.
Add `dispatch_prompt_submit_command_returns_synthesized_body` to
slash.rs — the public `dispatch` wrapper had no direct test for the
`Some(body)` return path; only `dispatch_with` was exercised via
synthetic registries. Add `"init"` to
`classify_built_in_state_mutating_command_is_mutating` — both
override `is_read_only=false` for different reasons but classify
the same.
Add a chat-block-count assertion to the busy-refuses init test:
the typed `/init` row should still land even when the synthesized
body is suppressed (3 entries: active prompt, typed `/init`,
refusal).
Rename `is_read_only_is_false_so_busy_dispatch_refuses_init` (and
the parallel `_clear`) to `is_read_only_is_false`. The original
names described a downstream consequence tested elsewhere; project
convention prefers scenario-side phrasing.
…ction Replaces SlashOutcome::PromptSubmit(String) with Action(UserAction), so /clear and /init route through the same trait return rather than the prior asymmetry where /clear reached into user_tx itself while /init returned a body string. SlashContext loses its user_tx field — slash impls never reach into the agent channel; the App-side dispatcher owns forwarding the synthesized UserAction. The TUI dispatcher inspects the returned Action and flips input-disabled + Streaming status before forwarding SubmitPrompt, so a typed prompt cannot squeeze in between dispatch and forward. /clear forwards as-is. Collateral cleanup: - UserAction derives PartialEq for SlashOutcome equality in tests. - Drop Eq from SlashOutcome (UserAction holds a String body). - Drop test_user_tx fixture and the channel-closed /clear test (the side-channel path is gone). - Update slash-commands design docs and the CLAUDE.md crate tree to reflect the new shape.
BUILT_INS reorders to clear, config, diff, help, init, status. The
matcher already sorts alphabetically within each tier when filtering,
so aligning the slice removes the special case where the empty-query
popup rendered in a different order from filtered queries. Codex and
Claude Code's `/help` both sort alphabetically — this matches the
precedent and removes the curation cost as the registry grows.
Cascade through every site that listed commands in order: README
status bullet, roadmap working-today and current-focus mentions,
user-guide table and mid-turn list, design-doc README built-in
listing and decision-12 read-only enumeration, popup snapshots, and
the popup-tab / popup-enter app tests (now filter to /help with
`Char('h')` rather than relying on the first-row landing on /help).
The `is_read_only` and `execute` rustdocs still referenced `SlashOutcome::PromptSubmit`, the variant name from before the unification. `cargo doc` warns on the dead intra-doc link.
The slash branch reuses `forward_to_agent` for any `Action(_)`, but the doc-comment only mentioned the `PromptSubmit` arm — `Action(Clear)` flows through the same call too.
The test name and comment described the pre-refactor world where `/clear` wrote to `user_tx` from `SlashContext`. After the unification that bypass is gone — `/clear` returns `Action(Clear)` and the slash branch forwards. The arm-level `false` return now guards future callers that route `Clear` straight through `dispatch_user_action`.
The comment claimed "BUILT_INS today has 4 commands" — wrong since the registry grew to six. The test still pins the right invariant (`height == matches.len()` while below the cap); only the comment needed updating to reference `MAX_VISIBLE_ROWS` instead of a count that drifts every time a command lands.
The Workflow Skills bullet listed `/init`, `/review`, `/commit` as "user-extensible commands backed by prompt templates" — but `/init` already ships as a built-in under Working Today, contradicting the section three screens above. Rewrote to describe the template override surface as the deliverable; the existing built-ins are called out by reference.
`dispatch_prompt_submit_command_returns_synthesized_body` still
destructured `UserAction::SubmitPrompt(body)` via let-else with a
`panic!` on the negative arm — the same dead-by-design pattern the
earlier coverage pass converted in four other sites. Folds the
destructure and the body-contains check into one `matches!` guard;
the failure message keeps the `{action:?}` debug print since `action`
is bound in the surrounding scope.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
/init— the first prompt-submit slash command. Typing/initshows the typed line, then forwards a synthesized prompt asking the model to author or update the project'sAGENTS.md(orCLAUDE.mdwhen one exists). The body adapts Claude Code'sOLD_INIT_PROMPTfor oxide-code's AGENTS.md-first instruction loader; the multi-phaseNEW_INIT_PROMPTdefers untilAgentEvent::PromptRequestplumbing lands.To carry prompt-submit cleanly,
SlashCommand::executenow returnsResult<SlashOutcome, String>withSlashOutcome::{ Local, Action(UserAction) }./initreturnsAction(SubmitPrompt(body));/clear(previously reaching intouser_txfrom insideexecute) now returnsAction(Clear). Slash impls never touch the agent channel — the trait return is the only seam.Design decisions
Action(UserAction)replaces the prior asymmetry between/clear's side-channel send and/init's string-body return.SlashContextloses itsuser_txfield; the App-side dispatcher owns forwarding.SubmitPrompt, the dispatcher flips input-disabled +Streamingbefore forwarding, so a typed prompt cannot squeeze in between dispatch and forward./clearforwards as-is.is_read_only=falseso/initrefuses mid-turn. Same gate/clearuses — running mid-turn would race the in-flight one./initlands as a chat block; resumed transcripts show the full body via JSONLMessage::user. Accepted trade-off.Changes
slash/init.rsInitCmdreturningAction(SubmitPrompt(PROMPT));is_read_only=false; AGENTS.md-first prompt body.slash/registry.rsSlashOutcome { Local, Action(UserAction) };execute → Result<SlashOutcome, String>; registerInitCmd.slash/clear.rsAction(Clear)instead of writing touser_tx.slash/context.rsuser_txfield — slash impls never reach into the agent channel.slash.rsdispatchreturnsOption<UserAction>for the App-side branch to forward.slash/{config,diff,help,status,matcher}.rsOk(())→Ok(SlashOutcome::Local).agent/event.rsUserActionderivesPartialEqforSlashOutcomeequality.tui/app.rsforward_to_agentforSubmitPrompt; forwards other actions as-is.docs/guide/slash-commands.mddocs/research/design/slash-commands/{init,README}.mddocs/roadmap.md,README.md,CLAUDE.mdTest plan
cargo fmt --all --checkcargo build/cargo clippy --all-targets -- -D warnings— zero warningscargo test— 1510 passcargo llvm-cov --ignore-filename-regex 'main\.rs'pnpm lint/pnpm spellcheck/initidle (typed line lands, agent receives body, status flips to Streaming, model authors the file); mid-turn (refuses with system message); popup row visible at/i.Out of scope
NEW_INIT_PROMPT'sAskUserQuestionclarifications, hook wiring, skill suggestions). Gates onAgentEvent::PromptRequestplumbing — seeinit.md§ Deferred.progressMessage— needs a status-bar variant; trivial to add when a second prompt-submit command warrants it.maybeMarkProjectOnboardingCompletewrites per-project state to~/.claude.json; oxide-code's stance against silent mega-file writes rules it out by default.