* desktop: surface /tools, /save, /personality and fix /help skill count
Move /tools and /save out of TERMINAL_ONLY_COMMANDS and /personality out of
ADVANCED_COMMANDS so they appear in the desktop slash palette and execute via
the existing slash.exec → command.dispatch fallback. The backend gateway already
accepts these through slash.exec (none are in _PENDING_INPUT_COMMANDS or the
skill list), so no backend change is required.
Recompute skill_count in filterDesktopCommandsCatalog from the filtered pairs.
Previously the /help footer echoed the unfiltered backend total — e.g. "60
skill commands available" while only ~29 actually appeared in the rendered
list, because the desktop hides terminal-only, picker-owned, and advanced
commands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* desktop: keep slash popover live while typing args
The trigger regex `(?:^|[\s])([@/])([^\s@/]*)$` stopped matching the moment
the user typed a space after a slash command, so the popover never showed arg
completions for `/personality`, `/tools`, etc. — even though the backend's
`complete.slash` already returns them with a `replace_from` indicator.
Split the trigger detection so `/` allows args (`/cmd arg1 arg2`) while `@`
keeps the strict no-space behavior. Restrict the slash command name to
`[a-zA-Z][\w-]*` so file paths like `src/foo/bar` don't accidentally trigger
the popover.
Rewrite arg-completion items in useSlashCompletions to insert the full
`/personality alice` token instead of stranding `/alice`: when `replace_from`
is past the command base, prepend the existing prefix to each item's text so
the chip serializer produces a coherent replacement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* cli: complete toolset names after /tools enable|disable
SlashCommandCompleter previously only auto-derived the first subcommand level
from args_hint, so `/tools enable <tab>` yielded nothing — the user had to
remember every toolset key (web, file, spotify, …) and every MCP server prefix.
Add `_tools_completions` that handles both stages: subcommand (list|disable|enable)
and tool name. Filter by current enable state so `/tools enable <tab>` only
offers disabled toolsets and `/tools disable <tab>` only offers enabled ones —
no point suggesting a no-op. MCP server prefixes (server:) come from the
saved mcp_servers config; per-tool completion under a server would require
runtime MCP introspection and is left as follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* desktop: registry-driven slash commands with first-class pickers
Collapse the if/else slash dispatch into one DESKTOP_COMMAND_SPECS table
that drives popover suggestions, per-type composer pills, and execution.
- /resume, /sessions, /switch: inline session completions (like /skin) plus
a "Browse all sessions…" entry that opens a dedicated session picker overlay
- /handoff: inline platform completion + handoff.request/handoff.state
gateway bridge so desktop reaches CLI parity
- colored per-type pills (command/skill/theme) in the composer
- strip ANSI and fix width/alignment of slash output in the chat panel
* desktop: fold repeated slash session/output boilerplate into one helper
runExec, /title, /help and the unavailable case each re-derived the same
ensure-session → bail-with-notify → build-renderSlashOutput dance.
withSlashOutput() returns {sessionId, render} or null, so each handler is
a two-line resolve instead of an eight-line preamble.
* desktop: keep backend meta on slash arg completions
Arg suggestions (/personality <name>, /tools enable <toolset>, /handoff
<platform>) were having their meta overwritten with the parent command's
registry description: desktopSlashDescription("/personality none") canonicalizes
back to /personality and returns its blurb. Skip the lookup for arg rows so the
backend's own display_meta ("clear personality overlay", etc.) survives.
* cli: list real personalities in /personality completion
_personality_completions resolved load_config().agent.personalities — but that
schema has no agent.personalities key, so completion always returned just
`none` even though the runtime (load_cli_config().agent.personalities) ships a
dozen built-ins (helpful, kawaii, pirate, …). Read from the same source the
command actually applies, so `/personality ` surfaces the real options.
* desktop: expand bare arg-commands to their options on pick
Picking a command like /personality from the slash popover committed it
immediately instead of advancing to its argument list. Mark arg-taking
commands (/skin, /resume, /handoff, /personality, /tools) in the registry
and, when one is picked bare, insert "/cmd " as plain text and re-open the
popover on its inline options — mirroring typing "/cmd " by hand. Arg picks
(serialized text already contains a space) still commit a single pill.
Also realign trigger-popover loading test with the redesigned popover (the
/help empty-state hint shows when resolved, not while the spinner is up);
the merge from main reintroduced the pre-redesign expectation.
* tui_gateway: fold session-db close into a context manager
Both handoff RPCs repeated the same `db, close_db = _session_db_handle()`
+ `finally: if close_db: db.close()` dance. Turn the helper into a
`_session_db` contextmanager that owns the close, so callers just
`with _session_db(session) as db:`.
* desktop: unblock handoff retries and exact resume ids
Clear timed-out desktop handoffs through the gateway so retries are not stuck behind a pending row, and let typed /resume session ids bypass the loaded sidebar cache.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
7.1 KiB
TypeScript
181 lines
7.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
desktopSkinSlashCompletions,
|
|
desktopSlashDescription,
|
|
desktopSlashUnavailableMessage,
|
|
filterDesktopCommandsCatalog,
|
|
isDesktopSlashCommand,
|
|
isDesktopSlashSuggestion,
|
|
isModelPickerCommand,
|
|
isPickerCommand,
|
|
resolveDesktopCommand
|
|
} from './desktop-slash-commands'
|
|
|
|
describe('desktop slash command curation', () => {
|
|
it('keeps core desktop chat commands in suggestions', () => {
|
|
expect(isDesktopSlashSuggestion('/new')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/branch')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/skin')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/usage')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/version')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/yolo')).toBe(true)
|
|
expect(isDesktopSlashCommand('/yolo')).toBe(true)
|
|
})
|
|
|
|
it('surfaces skill and quick commands (extensions) in suggestions and lets them run', () => {
|
|
expect(isDesktopSlashSuggestion('/my-skill')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/gif-search')).toBe(true)
|
|
expect(isDesktopSlashCommand('/my-skill')).toBe(true)
|
|
})
|
|
|
|
it('hides terminal, messaging, and dedicated-UI commands from suggestions', () => {
|
|
expect(isDesktopSlashSuggestion('/clear')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/compact')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/redraw')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/approve')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/model')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/skills')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/voice')).toBe(false)
|
|
expect(isDesktopSlashSuggestion('/curator')).toBe(false)
|
|
})
|
|
|
|
it('surfaces /tools, /save, and /personality on the desktop', () => {
|
|
expect(isDesktopSlashSuggestion('/tools')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/save')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/personality')).toBe(true)
|
|
expect(isDesktopSlashCommand('/tools')).toBe(true)
|
|
expect(isDesktopSlashCommand('/save')).toBe(true)
|
|
expect(isDesktopSlashCommand('/personality')).toBe(true)
|
|
expect(desktopSlashUnavailableMessage('/tools')).toBeNull()
|
|
expect(desktopSlashUnavailableMessage('/save')).toBeNull()
|
|
expect(desktopSlashUnavailableMessage('/personality')).toBeNull()
|
|
})
|
|
|
|
it('allows aliases to execute without cluttering the popover', () => {
|
|
expect(isDesktopSlashSuggestion('/reset')).toBe(false)
|
|
expect(isDesktopSlashCommand('/reset')).toBe(true)
|
|
})
|
|
|
|
it('filters built-in catalog noise but keeps skill / quick-command extensions', () => {
|
|
const filtered = filterDesktopCommandsCatalog({
|
|
categories: [
|
|
{
|
|
name: 'Session',
|
|
pairs: [
|
|
['/new', 'Start a new session'],
|
|
['/clear', 'Clear terminal screen']
|
|
]
|
|
},
|
|
{
|
|
name: 'User commands',
|
|
pairs: [['/ship-it', 'Run release checklist']]
|
|
}
|
|
],
|
|
pairs: [
|
|
['/new', 'Start a new session'],
|
|
['/model', 'Switch model'],
|
|
['/ship-it', 'Run release checklist']
|
|
],
|
|
skill_count: 2
|
|
})
|
|
|
|
expect(filtered.categories).toEqual([
|
|
{ name: 'Session', pairs: [['/new', 'Start a new desktop chat']] },
|
|
{ name: 'User commands', pairs: [['/ship-it', 'Run release checklist']] }
|
|
])
|
|
expect(filtered.pairs).toEqual([
|
|
['/new', 'Start a new desktop chat'],
|
|
['/ship-it', 'Run release checklist']
|
|
])
|
|
// skill_count is recomputed from the filtered output (only /ship-it is an
|
|
// extension command — /new is a built-in) so the /help footer matches what
|
|
// the user actually sees rather than echoing the unfiltered backend total.
|
|
expect(filtered.skill_count).toBe(1)
|
|
})
|
|
|
|
it('recomputes skill_count to reflect only extensions surfaced on desktop', () => {
|
|
const filtered = filterDesktopCommandsCatalog({
|
|
pairs: [
|
|
['/new', 'Start a new session'],
|
|
['/clear', 'Clear terminal screen'],
|
|
['/gif-search', 'Search for a gif'],
|
|
['/ship-it', 'Run release checklist']
|
|
],
|
|
skill_count: 12
|
|
})
|
|
|
|
expect(filtered.pairs?.map(([cmd]) => cmd)).toEqual(['/new', '/gif-search', '/ship-it'])
|
|
expect(filtered.skill_count).toBe(2)
|
|
})
|
|
|
|
it('uses desktop-specific labels for commands with different UI behavior', () => {
|
|
expect(desktopSlashDescription('/branch', 'Branch the current session')).toBe(
|
|
'Branch the latest message into a new chat'
|
|
)
|
|
expect(desktopSlashDescription('/skin', 'Show or change the display skin/theme')).toBe(
|
|
'Switch desktop theme or cycle to the next one'
|
|
)
|
|
})
|
|
|
|
it('builds /skin completions from desktop themes', () => {
|
|
const completions = desktopSkinSlashCompletions(
|
|
[
|
|
{ name: 'mono', label: 'Mono', description: 'Clean grayscale' },
|
|
{ name: 'midnight', label: 'Midnight', description: 'Deep blue' },
|
|
{ name: 'slate', label: 'Slate', description: 'Cool slate blue' }
|
|
],
|
|
'mono',
|
|
'm'
|
|
)
|
|
|
|
expect(completions).toEqual([
|
|
{
|
|
text: '/skin mono',
|
|
display: '/skin mono',
|
|
meta: 'Mono (current) - Clean grayscale'
|
|
},
|
|
{
|
|
text: '/skin midnight',
|
|
display: '/skin midnight',
|
|
meta: 'Midnight - Deep blue'
|
|
}
|
|
])
|
|
})
|
|
|
|
it('explains known commands that desktop owns elsewhere', () => {
|
|
expect(desktopSlashUnavailableMessage('/model sonnet')).toContain('model picker')
|
|
expect(desktopSlashUnavailableMessage('/skills')).toContain('desktop sidebar')
|
|
expect(desktopSlashUnavailableMessage('/clear')).toContain('terminal interface')
|
|
})
|
|
|
|
it('flags /model as a picker-owned command so the desktop opens the overlay', () => {
|
|
expect(isModelPickerCommand('/model')).toBe(true)
|
|
expect(isModelPickerCommand('/model sonnet')).toBe(true)
|
|
expect(isModelPickerCommand('/new')).toBe(false)
|
|
expect(isModelPickerCommand('/skills')).toBe(false)
|
|
})
|
|
|
|
it('gives /resume (and its aliases) a first-class session picker surface', () => {
|
|
expect(isPickerCommand('/resume', 'session')).toBe(true)
|
|
expect(isPickerCommand('/sessions', 'session')).toBe(true)
|
|
expect(isPickerCommand('/switch', 'session')).toBe(true)
|
|
// Unlike /model, /resume shows in the popover; its aliases stay hidden.
|
|
expect(isDesktopSlashSuggestion('/resume')).toBe(true)
|
|
expect(isDesktopSlashSuggestion('/sessions')).toBe(false)
|
|
expect(isDesktopSlashCommand('/switch')).toBe(true)
|
|
// The session picker is distinct from the model picker.
|
|
expect(isModelPickerCommand('/resume')).toBe(false)
|
|
})
|
|
|
|
it('resolves commands and aliases to their declared surface', () => {
|
|
expect(resolveDesktopCommand('/new')?.surface).toEqual({ kind: 'action', action: 'new' })
|
|
expect(resolveDesktopCommand('/reset')?.surface).toEqual({ kind: 'action', action: 'new' })
|
|
expect(resolveDesktopCommand('/resume')?.surface).toEqual({ kind: 'picker', picker: 'session' })
|
|
expect(resolveDesktopCommand('/usage')?.surface).toEqual({ kind: 'exec' })
|
|
expect(resolveDesktopCommand('/clear')?.surface).toEqual({ kind: 'unavailable', reason: 'terminal' })
|
|
// Skill / quick commands aren't in the registry.
|
|
expect(resolveDesktopCommand('/gif-search')).toBeNull()
|
|
})
|
|
})
|