spotlight: ctrl-space, type a few letters, go
The dock is full and cannot grow. Once navigation lives here it is free to be a short list of what you actually use rather than a complete one — which is the other half of this and is not done yet. It INDEXES THE DOCK rather than keeping a list of its own. The dock is already the composed, permission-filtered answer to "what can this account open here": the shell's screens, plus whatever installed plugins contribute, minus anything canVisit refuses. A second list would be a second answer to one question, and it would be wrong the day somebody installs a plugin. So a new plugin appears in Spotlight for the reason it appears in the dock — because it is installed — and nothing here is edited. Settings sections are added, because they are what people hunt for and the dock deliberately does not carry them. Those ARE hand-listed, which is a real cost written down in the file: a section added to IntegrationsSettings will not appear until someone adds it here. The alternative couples five settings screens to the shell to save five lines. Matching is subsequence, not substring, and lives in its own file so it can be tested with strings: "chq" finds Command HQ, "apik" finds API keys, "fi" gives Files rather than Profile. The weights encode what "the right result" means — exact label, then prefix, then word boundaries, then adjacency, with shortness breaking ties so "Chat" beats "Chat history". Labels are worth about three times hints, and paths almost nothing, so a path fragment finds an item without ever outranking a real label. Nine tests cover the ORDER rather than the matching, because returning the right set is easy and returning it with the right thing first is the product. ctrl-space, not alt-space: the more macOS-shaped chord is already load-bearing for the owner. Not cmd-k either — that belongs to the browser on some platforms and to text fields on others. Mounted beside the header and the dock rather than inside the content region, so it still gets you off a screen that is maximised, erroring or mid-navigation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Settings, User, Puzzle, Server, Cpu, Users } from 'lucide-react';
|
||||
import { usePermissions } from 'hooks/usePermissions';
|
||||
import { CORE_DOCK_ITEMS, dockItemsFromPlugins } from '../Dock';
|
||||
|
||||
// What Spotlight can take you to.
|
||||
//
|
||||
// ── It indexes the DOCK, rather than a list of its own ──
|
||||
//
|
||||
// The dock is already the composed, permission-filtered answer to "what can this account open here":
|
||||
// the shell's own screens, plus whatever installed plugins contribute, minus anything `canVisit` refuses.
|
||||
// Building a second list would mean two answers to one question, and the second would be wrong the day
|
||||
// somebody installs a plugin — which is the whole failure the dock's own comment warns about, arrived at
|
||||
// from a third direction.
|
||||
//
|
||||
// So a new plugin appears in Spotlight for the same reason it appears in the dock: because it is
|
||||
// installed. Nothing here is edited.
|
||||
//
|
||||
// Settings sections are added because they are the things people hunt for and the dock deliberately does
|
||||
// not carry. They are static: a settings screen is shell, not a plugin.
|
||||
|
||||
export type SpotlightItem = {
|
||||
/** Stable across renders — used as the React key and for the recency store. */
|
||||
id: string;
|
||||
label: string;
|
||||
/** Where it goes. Matched against `canVisit` for anything not already filtered. */
|
||||
to: string;
|
||||
/** Shown small beside the label, so two "Profile"s are distinguishable. */
|
||||
hint?: string;
|
||||
icon?: LucideIcon;
|
||||
/** Plugin artwork, when the tile has some. */
|
||||
image?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The settings sections, which the dock does not carry.
|
||||
*
|
||||
* Hand-listed rather than derived, and that is a real cost: a section added to `IntegrationsSettings`
|
||||
* will not appear here until someone adds it. The alternative is exporting every settings screen's
|
||||
* section array up into the shell, which couples five screens to this one to save five lines.
|
||||
*
|
||||
* Worth revisiting if it drifts. `hint` is what makes them findable — typing "keys" should reach the API
|
||||
* keys screen, and the label alone does not say where it lives.
|
||||
*/
|
||||
const SETTINGS_ITEMS: SpotlightItem[] = [
|
||||
{ id: 'set:profile', label: 'Profile', to: '/settings/profile', hint: 'avatar, name, email', icon: User },
|
||||
{ id: 'set:password', label: 'Change password', to: '/settings/profile/change-password', icon: User },
|
||||
{ id: 'set:api-keys', label: 'API keys', to: '/settings/profile/api-keys', hint: 'per-app keys', icon: User },
|
||||
{ id: 'set:dock', label: 'Dock', to: '/settings/profile/dock', hint: 'choose and reorder', icon: User },
|
||||
{ id: 'set:integrations', label: 'Integrations', to: '/settings/integrations', icon: Puzzle },
|
||||
{ id: 'set:server', label: 'Server settings', to: '/settings/server', icon: Server },
|
||||
{ id: 'set:system', label: 'System', to: '/settings/system', icon: Cpu },
|
||||
{ id: 'set:users', label: 'Users', to: '/settings/users', hint: 'accounts and permissions', icon: Users },
|
||||
{ id: 'set:ai', label: 'AI', to: '/settings/ai', hint: 'models and harnesses', icon: Settings },
|
||||
];
|
||||
|
||||
/** Everything Spotlight can offer this account, in a stable order. */
|
||||
export function useSpotlightItems(): SpotlightItem[] {
|
||||
const { canVisit, plugins } = usePermissions();
|
||||
|
||||
return useMemo(() => {
|
||||
const fromDock: SpotlightItem[] = [...CORE_DOCK_ITEMS, ...dockItemsFromPlugins(plugins)]
|
||||
.filter((item) => canVisit(item.to))
|
||||
.map((item) => ({
|
||||
id: `nav:${item.to}`,
|
||||
label: item.label,
|
||||
to: item.to,
|
||||
icon: item.icon,
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
}));
|
||||
|
||||
// Settings go through `canVisit` too. `usePermissions` fails OPEN by design — hiding a door is a
|
||||
// courtesy and the 403 is the lock — so this cannot lock anyone out of their own settings.
|
||||
const settings = SETTINGS_ITEMS.filter((item) => canVisit(item.to));
|
||||
|
||||
return [...fromDock, ...settings];
|
||||
}, [canVisit, plugins]);
|
||||
}
|
||||
Reference in New Issue
Block a user