anthropics / anthropics/claude-code
Slash menu: the top-5 most-used hoist never fires in the desktop app (frecency bucket returns empty)
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Summary
The slash-command menu already implements a "top 5 most-used commands first" hoist when the filter is empty (i.e. you just typed `/`). In the Claude desktop app on Windows that bucket comes back empty, so the menu opens straight into built-ins followed by a long alphabetical run. All three preconditions the hoist checks are satisfied in my session, which is why I think this is a bug rather than a missing feature.
With a plugin installed that contributes 73 menu entries, this is the difference between a usable menu and one where every invocation requires typing a namespaced prefix.
### Environment
- Claude desktop app 1.24012.9 (x64, MSIX), Windows 11 Home 26200
- Bundled engine: `%APPDATA%\Claude\claude-code\2.1.219\claude.exe` (ProductVersion 2.1.219.0)
- Plugin: `agent-orchestra@agent-orchestra` v3.4.9 (18 commands + 55 skills = 73 namespaced entries)
### The ordering the engine implements
In the empty-filter branch of the menu builder (symbol names below are from the minified bundle, so they will differ between builds):
```js
if (r === "") {
let d = t.filter(T => !T.isHidden && !Hz(T)),
p = [],
f = d.filter(T => T.type === "prompt")
.map(T => ({cmd: T, score: lNt(T.name)}))
.filter(T => T.score > 0)
.sort((T, C) => C.score - T.score);
for (let T of f.slice(0, 5)) p.push(T.cmd);
let m = new Set(p.map(T => Dli(T)));
d.forEach(T => {
if (m.has(Dli(T))) return; // hoisted entries are removed from later buckets
...
});
return [...p, ...g, ...y, ..._, ...E, ...A].map(...)
}
```
So the intended order is: top-5 by frecency, then built-ins, then `~/.claude/commands`, then `/.claude/commands`, then policy commands, then everything else alphabetically.
The scorer is a standard frecency function - lifetime `usageCount` decayed with a 7-day half-life, floored at 0.1:
```js
function lNt(e) {
let r = bt().skillUsage?.[e];
if (!r) return 0;
let n = (Date.now() - r.lastUsedAt) / 86400000,
o = Math.pow(0.5, n / 7);
return r.usageCount * Math.max(o, 0.1);
}
```
### The three preconditions are all satisfied
1. **`type === "prompt"`** - the plugin command/skill loader returns `{type: "prompt", name: e, ..., isHidden: !G}`, so plugin-contributed commands qualify.
2. **Key match** - the loader builds the name as `` `${plugin.name}:${basename(file).replace(/\.md$/,"")}` ``, e.g. `agent-orchestra:experience`. The counter is incremented with that same value (`if (f.type === "prompt" && f.userInvocable !== false) GJr(f.name)`), and `lNt` reads `skillUsage[name]`. Write key and read key are the same by construction.
3. **`score > 0`** - computed live from my `~/.claude.json` (last written the same day):
| name | usageCount | days since use | score |
| --- | --- | --- | --- |
| `agent-orchestra:experience` | 275 | 0.0 | 273.96 |
| `agent-orchestra:design` | 250 | 0.0 | 249.86 |
| `agent-orchestra:plan` | 227 | 3.2 | 164.70 |
| `agent-orchestra:orchestrate` | 190 | 2.8 | 144.48 |
| `agent-orchestra:review-github` | 135 | 0.1 | 133.84 |
Next highest is 14.94, so the top 5 is unambiguous and stable.
### Observed behavior
Opening the menu with `/` and no filter, the first entries are built-ins (`schedule`, `model`, `workflows`, `usage`, `config`, `__remote-workflow`), then straight into the alphabetical run of plugin entries: `agent-orchestra:adversarial-review`, `agent-orchestra:ai-first-documentation`, `agent-orchestra:audit-docs`, ...
`agent-orchestra:experience` appears in that run at its plain alphabetical position, between `agent-orchestra:engagement-record-emission` and `agent-orchestra:frame-credit-emission`.
That placement is the tell. Hoisted commands are removed from the later buckets via the `m` set, so a command sitting in its alphabetical slot cannot also have been hoisted. Bucket `p` was empty.
### Expected
`agent-orchestra:experience`, `:design`, `:plan`, `:orchestrate` and `:review-github` appear as the first five entries, above the built-ins, and are absent from the alphabetical run.
### Likely locus
Since the engine-side preconditions all hold, my guess is that the desktop surface builds its own list rather than consuming this ordering function - but I can only see the bundled engine, not the app shell, so I can't confirm that half. If the ordering is applied identically in the terminal TUI, that would localize it quickly.
### Why this is worth fixing rather than routing around
There is no user-side lever for it. `~/.claude/commands/*.md` sorts above plugin entries, so short personal wrappers help, but commands that pin `model:` in frontmatter cannot be usefully wrapped. And there is no setting to trim the menu - I checked for `disabledSkills` / `hiddenCommands` / `disabledCommands` and equivalents; plugin enable/disable is whole-plugin only. The hoist is the mechanism that makes a large menu usable, and it already exists.
### Related
- #80125 - a desktop user reporting this exact symptom ("arbitrary order - not alphabetical") and asking for alphabetical sorting as a feature. Same underlying cause, I think.
- #75044, #79157 - favorites/pinned-bar requests, which overlap in motivation.
- #59023 - "weighted command history based on execution frequency", closed not-planned as stale, though the mechanism shipped.
- #32859 - "Sort slash commands by usage frequency", auto-closed as a duplicate of #23668, which was itself closed as a duplicate.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the bundled engine's empty-filter menu-builder branch and compare its ordering with the desktop surface and terminal TUI. Reproduce the slash menu with the listed plugin usage data; done means the five highest-scoring prompt commands appear first and are removed from the alphabetical run.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100