Universal search
Project focused application Queries into the shared Cloud search.
Universal Search is an optional projection of ordinary capability Queries. The application searches its own data and returns only resources the current access subject may read. Cloud discovers live providers, fans out the query, and either merges their results or streams each application's results as soon as that application answers.
Signing and provider requests share one eight-second execution budget. Cold signer preparation uses that same budget; there is no shorter signing timeout. A failed or timed-out authority check returns an unavailable error, never an unauthorized search.
Read App capabilities first for the shared Type, Query, schema, result, registry, and authorization rules.
Add static page links
For utility pages such as a QR generator, add searchLinks to defineApp():
searchLinks: [
{
label: "QR code generator",
description: "Turn links and text into QR codes.",
href: "/tools/qr",
icon: "ti ti-qrcode",
keywords: ["qr", "barcode", "scan"],
},
],These links appear only in the global search dialog, above the resource results: they are matched in the browser and show at once, so application results that arrive later never push them down. Cloud includes them when the app is visible in the user's navigation catalog. The browser matches every search word against the label, description, and keywords, ignoring case, after at least two characters. An explicit application context also shows its static links for an empty query and uses fuzzy matching. Resource contexts and tag searches omit static links.
Use same-origin paths and public page labels. Destination routes still enforce
their own authorization. Localize labels with
presentation.translations.<locale>.searchLinks, keyed by href, and include
alternate search terms in keywords.
An optional plain-text description appears below the result title and in its
preview. Localize it with
presentation.translations.<locale>.searchLinkDescriptions, also keyed by href.
Both labels and descriptions fall back through the requested locale to the base
declaration. Links without descriptions continue to work.
Static links create no agent capabilities or resource readers and do not appear
in /api/search or resource pickers. Use a search Query for dynamic application
data instead.
Add a search Query
An app may expose multiple Queries through Universal Search. Each must use the
exact shared input and data schemas. Add the Queries to the application's
src/capabilities.ts module. The surrounding capability declaration still
owns the resource Type; the excerpt below shows only the search-specific part.
import { defineCapabilities } from "@k2b/cloud";
import {
UniversalSearchDataSchema,
UniversalSearchInputSchema,
} from "@k2b/cloud/contracts";
import { ok } from "@k2b/stdlib";
export const inventoryCapabilities = defineCapabilities({
protocolVersion: 2,
types: {
item: {
title: "Inventory item",
description: "One item in the inventory catalog.",
},
},
queries: {
search: {
title: "Search inventory",
description: "Find visible inventory items by name.",
input: UniversalSearchInputSchema,
data: UniversalSearchDataSchema,
openWorld: false,
universalSearch: {
tags: [
{
tag: "inventory",
title: "Inventory",
description: "Search inventory items.",
aliases: ["stock", "sku"],
},
],
},
run: async ({ query, limit }, context) => {
const items = await inventory.search({
query,
limit,
accessSubject: context.accessSubject,
});
return ok({
data: items.map((item) => ({
ref: { type: "inventory.item", id: item.id },
title: item.name,
preview: `${item.quantity} in stock`,
icon: "ti ti-package",
priority: 7,
metadata: [{ label: "Type", value: "Inventory item" }],
links: [
{ rel: "open", href: `/app/inventory/items/${item.id}` },
],
})),
});
},
},
},
});Add a canonical reader separately when clients must also load a known item by
its ref. Pass the declaration to app.start({ capabilities, fetch }) as
described in App capabilities.
Follow the search contract
UniversalSearchInputSchema provides:
| Field | Meaning |
|---|---|
query |
User-entered text; may be empty when a facet narrows the search |
tags |
Canonical facets supported by this Query |
limit |
Maximum results this provider may return |
scope |
Optional CloudResourceRef identifying the resource whose contents to search |
Each returned resource must:
- use a Type declared by the app;
- have a stable public resource ID;
- include at least one root-relative
openlink; - contain only information the current access subject may read;
- stay within the requested limit.
Cloud preserves each structured CloudResourceRef in the merged result. If
the referenced Type advertises a canonical reader, the search result's
ref.id must work unchanged as that reader's required id. A consumer can
therefore keep the ref and resolve the current reader later without storing a
Capability name.
Searchability does not require a reader. A Type without one may still return a
navigable search result with an open link, but consumers must not present it
as programmatically readable. Search and read remain separate Queries: search
finds bounded resource views; the Type's reader loads one known resource.
Use preview, icon, priority, and metadata only when they help identify
the resource. A preview semantic link lets Cloud load a separate preview
surface.
Tags and aliases help users and agents discover providers. They are not
permissions. Use stable, lower-case values without #, and keep each meaning
unique within the app.
Prefer one focused provider per stable resource kind when that removes app-local routing or result-merging code. Keep one provider when tags are only facets or aliases of the same search. Cloud discovers every opted-in Query directly from the live capability manifest; apps do not register a separate search wrapper. Merged results are capped per app, so multiple focused Queries do not give one app a larger share of the global result set.
Authorize every result
The shared /api/search route requires a user-backed actor. The provider still
authorizes every resource with context.accessSubject; never return a result
and rely on its destination page to hide it later.
The same Query can also be invoked through the generic capability HTTP, CLI, or MCP surface. Those calls follow normal capability authentication and may use a service-account access subject. The application must handle the subject types it supports explicitly.
Resolving a reader is not authorization. Consumers use the current live
manifest, and the owning app checks the current AccessSubject again when the
reader runs.
Keep app-specific search separate
Universal Search is a projection, not the only search operation an app may publish. Add other Queries for app-specific list, filter, lookup, or exhaustive traversal semantics when they have a stable cross-client use.
Cloud ranks results by app-provided priority and title after merging providers.
One provider failure does not fail the complete search: successful providers
still return partial results with HTTP 200 and failedApps identifies unavailable
sources. A shared registry or invocation
signer failure returns HTTP 503, not a successful empty result. Log provider failures
with structured logging; the application's domain
database remains the source of truth.
Stream results as applications answer
By default, GET /api/search answers once with the merged JSON response
above, for the CLI, MCP, and other API clients. A client that sends
Accept: application/x-ndjson receives one JSON object per line instead, in
this order:
| Line | Fields | Meaning |
|---|---|---|
start |
query, apps, providers, optional unsupportedTags |
The app catalog and the IDs of the applications that search |
provider |
provider, status, results, ms |
One application finished; status is ok, empty, timeout, or error |
done |
status (complete or partial), count |
Every application has answered |
Each application named in start gets exactly one provider line, written
when all of its search Queries have finished, so a slow application never holds
back the others. Applications keep searching until the shared eight-second
budget ends; an application still searching then gets a timeout line.
results holds at most that application's provider limit, ranked by priority
and title; a streamed search has no merged top-30 list. An application with
several Queries can report timeout or error together with what its other
Queries found.
Closing the response, for example because the user typed the next character,
cancels the providers that are still running. The response is sent with
Cache-Control: no-store, no-transform and X-Accel-Buffering: no; a reverse
proxy in front of Cloud must pass it through without buffering or compressing
it, or the browser receives every line at the end. Signing and registry
failures still return HTTP 503 before the first line.
Search in the browser
Global search and the resource picker share one search interface. Start typing
or choose All filters to discover tags. Typing # shows matching filters
inside the result area. Suggestions list each canonical tag once and match
its aliases. Enter or a click applies a filter; Tab keeps normal focus navigation.
A typed tag is committed with whitespace. An unfinished tag does not trigger an
unsupported-filter warning.
All filters remains available while tags load and shows their loading state inside the filter view. It uses the same padded hover surface as Actions.
Results are grouped by application, and each application's section appears as soon as that application answers. New sections are appended below the ones already shown, so nothing on screen moves and the keyboard selection stays on its result. Matching pages and actions come first. An action that finishes loading after results are on screen appears with the next keystroke instead of pushing them down. While applications are still searching, a calm line below the results names them, for example “Files and Mail are still searching…”. “No matches” appears only after every application has answered. An application that did not answer in time or failed gets its own line, for example “Mail did not respond in time · Try again”; Try again searches only that application and appends its results. When the connection breaks off, the results already shown stay and every application that had not answered gets that line. Only a search that fails before it names its applications says “Search is currently unavailable”, below anything already on screen. A search narrowed to one application by its chip or a tag has one state at a time: searching, its results, or Mail: no matches for “invoice”. When every application has answered, screen readers hear “Search complete, 3 results”, followed by each application that did not answer, such as “Mail did not respond in time”.
Desktop search shows a preview beside the input and result list. The centered dialog keeps its width, top position, and height from the first search character until the input is cleared, so arriving results never resize it. On small screens, Details opens the preview and Back to results returns to the list. Escape first leaves filter discovery or mobile details, then closes the dialog. Scroll fades indicate more content.
In global search, Cmd+Enter (macOS) or Ctrl+Enter opens the active result in a new tab while preserving the query, filters, and selection. Modified clicks have the same effect. Ordinary Enter and clicks navigate in the current tab. The browser decides whether to focus the new tab.
On desktop screens at least 64rem wide with a mouse and no touch input, drag the outer edge of Spotlight to place it beside the page. Detached search allows background interaction without blur. Its position is stored locally in this browser and kept within the viewport when resized or reopened. Drag near the original centered position and release to snap back and restore the modal backdrop; double-click an edge to reset directly. A translucent, softly blurred preview with a search icon marks the home position while dragging. It matches the current search size. In the snap area it turns blue and shows a checkmark with Release to center. The window only snaps when released. You can place the compact search near the bottom; results grow upward when there is no room below, then return to the preferred position when the search shrinks. Escape during a drag cancels that drag. On smaller or touch screens the search stays modal. Resource pickers always retain their modal selection behavior.
Global search opens a result on click or Enter. The picker selects it first; Add confirms the choice. While a new resource search waits for its first application, earlier results remain visible but cannot be selected for the new query. Results of the current search can be opened while other applications are still searching.
Open search from an application
Use the public browser entry point instead of building a separate navigation Spotlight. The Cloud layout hosts one dialog across independent islands.
import { openGlobalSearch } from "@k2b/cloud/browser/search";
openGlobalSearch({
scope: {
ref: { type: "notebooks.notebook", id: notebook.shortId },
label: notebook.name,
icon: "ti ti-notebook",
},
});
// App-wide search, including all resource kinds and static links of this app:
openGlobalSearch({ scope: { appId: "notebooks", label: "Notebooks" } });
// A list inside an app: one chip, no visible technical hashtag.
openGlobalSearch({
scope: { appId: "assistant", tag: "assistant-project", label: "Projects", icon: "ti ti-folders" },
query: "",
});
// Ordinary cross-app search:
openGlobalSearch();The context is a removable chip. Removing it clears the application restriction
and tag filters while preserving the search text. Labels and icons are display
metadata, never authorization. Concrete scopes are not listed in filter
discovery. Reopening an existing dialog updates its context and focuses the
input. Pass query to replace the text too; omit it to preserve the current text.
Calls made before the layout's islands mount wait for the host; only the latest
request is retained, with a bounded wait and visible failure if it cannot open.
On public pages, signed-out users can search the navigation links visible to
them, such as Tools. The layout does not call the authenticated resource-search
API for these users.
Choose the right context
- Use
scope.reffor the contents of a particular notebook, project, chat or mailbox. - Use
scope.appIdfor everything searchable in an application. - Add
scope.tagto select a list or search provider within that app, such as Assistant projects, Studio apps or all chats. Use a tag declared by that provider; the tag is part of the context, not another visible filter chip.
For example, use tag: "assistant-project" with the label Projects,
tag: "studio-app" with Studio, and tag: "chat" with Chats.
Localize the label and give it the appropriate icon. Removing this single chip
removes both the application and provider restriction while keeping the text.
Do not put a technical #tag into query to replace an app's old Spotlight.
That produces an extra visible tag beside the app chip. query supplies search
text; a search context belongs in scope. Context commands use the same options
and replace the context in place without closing the palette.
The browser sends an app context tag as scope_tag together with app.
It is separate from user-entered tag filters: those cannot add a different
provider outside the context. Unknown or unsupported context tags fail closed
with HTTP 400. A tag context and a resource reference are mutually exclusive.
These are search restrictions, never authorization; providers still check access.
Search providers must return resources that their destination views allow the user to open. Contacts search follows the same address-book membership checks as its regular search endpoint, including for platform administrators.
A search Query declares the local resource types whose contents it accepts:
universalSearch: {
tags: [{ tag: "note", title: "Notes", description: "Find notes." }],
scopeTypes: ["notebook"],
},The Type must exist in the same capability declaration. Cloud selects only
providers that accept the qualified scope type. HTTP requests carry
scope_type and scope_id together; both are forwarded as input.scope.
Opaque IDs keep their casing. The provider resolves the public ID, authorizes
the containing resource against the current access subject and credential
binding, and filters the existing search service. An invalid or inaccessible
scope must never become an unscoped search. Direct capability invocations also
reject scope types that the Query does not declare.
Update all search-provider images with the shared input schema. Live manifests are checked against that exact schema; mixed old and new search contracts are not supported. This change needs no data migration.
Preserve application navigation
Register a handler once in the active view and dispose it on unmount. Registration replaces the previous handler, including across separately bundled islands.
import { registerSearchNavigation } from "@k2b/cloud/browser/search";
import { onCleanup, onMount } from "solid-js";
onMount(() => {
onCleanup(registerSearchNavigation(async ({ href, ref }) => {
if (!canOpenInCurrentView(href)) return false;
await openInCurrentView(href, ref);
return true;
}));
});href is always available; static navigation links have no ref. Return true
after handling the target, or false to use document navigation. A rejected
handler keeps search open with an error; it does not force a reload. Reuse the
application's existing save guards, history, and view-state logic. Being in the
same application alone does not mean a route supports navigation without reload.
The handler also applies when search was opened through the global shortcut.
Modified clicks and Cmd/Ctrl+Enter bypass the handler and open a new tab.
The browser bridge uses document-local events, not shared Solid context or cross-tab channels. Keep navigation and resource selection separate: link insertion, move targets, version comparison, and other pickers still return a selection rather than navigating. Embedded list filters keep filtering their current view.
Let a user choose a Cloud resource
Use openCloudResourcePicker from
@k2b/cloud/browser/resource-picker when an application needs a
stable CloudResourceRef selected from any searchable Cloud application. The
picker groups the existing Universal Search results by their owning app,
supports tag filters, and returns the resource view after the user selects
a result and confirms with Add. Store the
structured ref; treat its title, preview, and links as presentation data.
Set requireReader when the consumer must resolve the selected resource later,
as AI Project references do. Set resourceType to list only resources whose
ref.type matches that type, for example "spaces.space". This filters picker
results in the browser; the consumer still authorizes the chosen resource.
Pass scope: { appId, tag?, label, icon? } to narrow the search on the server
to one app and, optionally, one of its search tags. The removable chip shows
label and icon; scope takes precedence over initialAppId.
Pass query to prefill the search text. An app scope starts searching at once,
even with empty text. Server filtering by app and tag happens before the
per-app result limit, so other resource kinds do not crowd out matching
results as they can with the browser-only resourceType filter.
requireReader still applies within the scope.
import { openCloudResourcePicker } from "@k2b/cloud/browser/resource-picker";
const selected = await openCloudResourcePicker({
title: "Add Cloud reference",
excludeRefs: currentReferences,
requireReader: true,
});
if (selected) await saveReference(selected.ref, selected.title);The shared /api/search route accepts one optional app query parameter to
limit provider fan-out and returns the searchable app catalog with each
response, including canonical tag names, titles, descriptions, and aliases.
Tag presentation follows the request locale. The browser uses this catalog for
both search dialogs; there is no application dropdown. initialAppId still
starts a picker within one application, shown as a removable scope chip.
require_reader=true removes navigation-only resources before
result limits are applied. An empty unscoped request returns only the app
catalog without calling providers. The picker owns this platform-specific
discovery UI; @k2b/ui remains independent of Cloud applications and resource
contracts.
Insert resources from a composer
Every app's search tags appear under Insert in a composer's slash menu
once someone types their word. The translated title names the row; the tag,
aliases, title, and app name are search words. Translate tag titles in
presentation.translations under searchTags. Write aliases in German and
English, for example "tabelle" and "table", because aliases are not translated.
Name a tag that stands for one kind of resource after that Resource Type's
local ID, for example the tag note for notebooks.note and contact for
contacts.contact. A narrowed slash list such as /notiz q3 then also lists
the person's recently sent references of that type above its search row.
References of other types appear only in unscoped search. Assistant maps its
own studio-app tag to assistant.artifact. Tags that filter rather than
name a type, such as urgent, never match. Spaces' task tag finds
spaces.item resources, so recent tasks appear only in unscoped search.
Use cloudInsertMenu from @k2b/cloud/browser/composer-insert to build the
rows, scopes, and fallback for Chat.Composer. Enter opens the resource picker
narrowed on the server to the app and tag, with requireReader: true. Tab
narrows the slash list. A selection returns { mention } for the token's place;
cancelling keeps /…. Supply mention to build the reference your draft stores,
and excludeRefs to omit resources already in it.
createCloudComposerCatalog() gives one composer its tags and Commands from a
catalog load shared by every composer on the page. Call catalog.ready(query, search, locale)
at the start of each searchCommands run. The first run of a menu opening,
identified by search.opening, starts the load, so no keystroke sends a request.
A successful load stays cached for the page; a failed one is retried at the next
opening and keeps the previous tags. Only typed text outside a scope waits for
the load, so a tag word does not settle as Nothing found before its tag
arrives. The overview and scoped searches do not wait, and after a failed load
no search waits again. Each catalog page has a 15-second deadline.
The catalog leaves out the tags of apps that declare no resource reader,
because a picker with requireReader: true can show none of their results.
An app without a reader can still contribute Commands.
catalog.commands() contains localized Commands that accept an empty input,
belong to apps in the person's navigation, and belong to a different app than
the composer. catalog.state() starts as "loading", becomes "ready" after
success or "failed" after failure. A new opening after failure starts one
fresh shared load and returns to "loading".
Use cloudCommandMenu from the same import to build Open in apps rows.
Pass its pending to the last section: three placeholders reserve its end in
the overview, one while searching. The failed catalog offers a persistent
retry row that starts another opening. The row's before(draft, signal, follow) must save
draft.value and draft.mentions, which already leave out /…, together with
the attachments, through your revisioned draft path. Read returnTo() after
saving, so a newly created chat can supply its path. The signal comes from the
composer and aborts when the opening ends without leaving the page: the person
closed the list, or opening failed. The token is then still on screen, so save
the live draft again. If before creates a chat that the composer then shows,
call follow() right before creating it; otherwise any chat change cancels the
opening. Preserve later edits once that signal aborts.
openComposerCommand(command, { before, returnTo, signal, locale }) provides
the same save-then-open behavior for a custom row. A rejected before() reaches
the composer status line and opens nothing. An invalid return path throws. If
Command resolution fails, the row shows the app's unavailability only when the
app cannot be reached; otherwise it shows the resolver's message. It stays open
in either case. If the Command code cannot load, the row keeps the token and
the reload notice offers a reload. signal() is read after saving: once it
aborts, for example because the person left the chat, the row ends without
opening anything. After navigation, the target app owns error feedback and
chooses whether to use returnTo.
import { cloudCommandMenu, cloudInsertMenu, createCloudComposerCatalog } from "@k2b/cloud/browser/composer-insert";
import { Chat, type ChatCommandDraft, type ChatMention, useLocale } from "@k2b/ui";
import { createMemo, createSignal } from "solid-js";
export function ResourceComposer(props: {
saveDraft: (draft: ChatCommandDraft) => Promise<void>;
returnTo: () => string | undefined;
}) {
const locale = useLocale();
const [text, setText] = createSignal("");
const [mentions, setMentions] = createSignal<readonly ChatMention[]>([]);
const catalog = createCloudComposerCatalog();
const menu = createMemo(() =>
cloudInsertMenu({
locale: locale(),
tags: catalog.tags(),
section: "insert",
mention: (item) => ({ id: `resource:${item.ref.type}:${item.ref.id}`, name: item.title, data: item.ref }),
}),
);
const apps = createMemo(() => cloudCommandMenu({
locale: locale(), commands: catalog.commands(), state: catalog.state(), section: "apps",
before: props.saveDraft, returnTo: props.returnTo,
}));
return (
<Chat.Composer
value={text()}
onValueChange={setText}
mentions={mentions()}
onMentionsChange={setMentions}
commandSections={[
{ id: "insert", label: "Insert" },
{ id: "apps", label: "Open in apps", pending: apps().pending },
]}
commands={[menu().insert, ...menu().tags, ...apps().commands]}
commandScopes={menu().scopes}
commandFallback={menu().fallback}
searchCommands={async (query, _signal, search) => {
// Starts this opening's catalog load; typed text outside a scope waits for it.
await catalog.ready(query, search, locale());
return [];
}}
onSubmit={(input) => {
console.log(input.text, input.mentions);
}}
/>
);
}Compose these rows and scopes with your own commands. Pass their scope words as
reservedWords so a catalog tag cannot claim them. reason disables the insert,
tag, and fallback rows with a visible explanation, such as a full message.
Use ownReaders only for a resource type your composer reads itself, for
example { "assistant:studio-app": "assistant.app" }, and add that tag to
tags yourself when its app has no reader. That tag's picker then lists only
this type, also without a Cloud reader, even after the person removes the scope
chip. A word that two tags share narrows to neither, so it
does not become a scope word.
The example stores the reference in attachment.data; adapt that value to your
application's draft format and persist the submitted mentions.
Actions in the global palette
The global search also discovers Commands.
Click Actions or type > to browse them. Ordinary text searches show
matching actions after resource and page results. Only Commands whose input
schema accepts {} and whose app is in the person's navigation appear globally.
The palette keeps the current app's Commands. Navigation is discoverability,
never authorization: the target page still checks access. Other Commands remain
callable with explicit input. Resource pickers never show or execute Commands.
Register useful actions for the currently visible resource from its island:
import { registerContextAwareCommand } from "@k2b/cloud/browser/commands";
import { createEffect, onCleanup } from "solid-js";
createEffect(() => {
const contact = currentContact();
if (!contact?.emails.length) return;
onCleanup(registerContextAwareCommand({
id: `contact.${contact.id}.email`,
title: `Compose email to ${contact.name}`,
description: "Opens Mail with this contact as the recipient.",
icon: "ti ti-mail-plus",
scope: "selection",
action: {
command: "mail.compose",
input: { contact: { type: "contacts.contact", id: contact.id } },
},
}));
});Alternatively, action may be a local function that opens existing UI or calls
the same permission-aware mutation as a visible button. Bind the target in the
registration and remove the entry when the resource changes, disappears, or
becomes unavailable. Avoid ambiguous labels such as “Edit” without a target.
Use the application's normal locale catalog for these labels.
Set scope: "selection" for actions on the currently selected object. Page-level
actions, such as creating a note or searching a notebook, omit it (the default
is "page"). Selection actions appear first in the palette and Layout Help.
Within each scope, registration order is preserved: register the most useful
actions first. This affects presentation only; it never resolves a shortcut
conflict or grants permission. Remove selection actions when the object closes.
Up to three context actions appear before typing, in both the global and
app-scoped search. The Actions view exposes the full list. Actions are never
implicitly selected in an empty search: click them, or select with
an arrow key before pressing Enter. Typing a filter after > selects the first
matching action for Enter. Asynchronous catalog updates preserve the selected
target; removing that target clears the selection. Cmd/Ctrl+Enter opens linkable
Commands in a new tab and leaves the palette open; local functions cannot open in a new tab.
Write distinct action titles and useful descriptions
Apply these rules to both global Commands and browser context commands:
- Make the title understandable on its own. Name the action and its scope; use Search all chats and Search this chat, rather than distinguishing them only by Chats and Chat. Do not rely on icons or shortcuts to tell similar actions apart.
- Use the description to add information: which content is searched, which resource is affected, or what opens next. Never repeat the title or describe the underlying API.
- Include the current resource name when it helps identify the target. Keep titles short and put longer names in the description. Update both with the active context and localize them through the application's message catalog.
| Title | Description |
|---|---|
| Search all chats | Find titles and messages in your Assistant chats. |
| Search this chat | Find messages in “Daily planning”. |
| New chat | Start a conversation with the Assistant. |
| Compose email to Alex | Opens Mail with this contact as the recipient. |
The first two titles become Alle Chats durchsuchen and Diesen Chat durchsuchen in German. Preserve that distinction in every locale.
Search actions
A context command can change the search context without closing the palette:
action: {
search: {
scope: {
ref: { type: "assistant.chat", id: chat.id },
label: chat.title,
icon: "ti ti-message",
},
},
},This reuses GlobalSearchOptions. The palette clears the action query, applies
its removable scope and focuses the existing input. An optional query supplies
initial search text. Triggering the command through its shortcut opens the same
global search host. Search actions do not navigate, open a second dialog or
support opening in a new tab. Resource-result links still support Cmd/Ctrl+Enter.
Describe what the search finds instead of repeating the action title.
Context command shortcuts
Set an optional shortcut on a browser context command, for example "e",
"mod+shift+k", or "mod+alt+n". mod means Command on macOS and Control
elsewhere. Commands in the server capability catalog do not declare shortcuts.
Cloud's layout owns keyboard dispatch; applications must not add a separate
shortcut listener or registry.
The search palette and Layout Help display the same currently registered shortcuts. Duplicate command IDs are unavailable until ownership is unique. Conflicting key combinations remain clickable commands but have no active shortcut or shortcut label until the conflict disappears. Remove registrations with their Solid owner and re-register when the target or permission changes.
Unmodified shortcuts do not run in editable fields. Registered Control/Command combinations take priority over editor bindings; all other editor keys remain local. Events already prevented by another capture listener are ignored. Application shortcuts do not run behind an open dialog. The universal search shortcut remains available. Repeated and IME composition events are ignored. The palette and keyboard share live ownership checks, prevent concurrent execution of one registration, and show an execution error once. Never bind destructive or send actions to casual single-letter shortcuts.
Editor formatting and component navigation, such as arrows within a Kanban board, stay with their component. They are not application commands.
Use consistent meanings across applications:
| Shortcut | Meaning |
|---|---|
mod+k |
Global search, owned by Cloud |
mod+shift+k |
Search the current context |
mod+alt+n |
Create in the current context |
e |
Edit the selected object, when applicable |
r |
Reply to the selected conversation |
d |
Mark the selected object done or reopen it |
m |
Assign the selected item to yourself |
Choose one live owner for each shortcut. For example, Assistant gives
mod+shift+k to Search this chat while a chat is open; Search all chats
remains clickable and only owns the shortcut outside a chat. Do not register
both and rely on mounting order to choose. Spaces uses mod+alt+n for a new
event in calendar view and a new task in other views.
Not every action needs a shortcut. Keep destructive operations and sending behind their existing confirmation or compose flow. Describe the target and outcome, such as “Reply to the sender of ‘Invoice’”, rather than repeating “Reply” as the description. Display only shortcuts that are actually active; do not hard-code an old key in button labels or accessibility attributes.
Built-in application search contexts
Application search buttons reuse the global dialog. Removing the context chip restores the global search without reopening the dialog.
| Entry point | Search context | Results |
|---|---|---|
| Assistant project | assistant.project with the public project ID |
Your chats in the selected project, matched by title and message content |
| Mail mailbox | mail.mailbox with the public mailbox ID |
Messages and attachments in that mailbox |
| Accounts | Accounts app context | Groups; administrators can also find users and service accounts |
| Capabilities | Capabilities app context | Apps, Queries and Actions in the inspector |
Project and mailbox scopes are hidden contexts, not additional discoverable tags. Providers authorize the resource before searching and apply its scope before the result limit. An unavailable context fails instead of searching all resources. Accounts uses Core's existing entity access rules. The Capabilities provider caches public catalog presentation for 30 seconds per supported locale; it never caches execution permissions. Its low-priority results open the inspector and do not invoke an operation or duplicate Commands.
Test application search and context actions
Use @k2b/cloud/browser/testing in browser-environment tests after mounting an
application island:
collectContextAwareCommands()returns the currently registered actions.contextCommandsWithShortcuts()applies the same shortcut conflict rules as the palette and Help.registerGlobalSearchHost(handler)captures search requests; call its returned cleanup function after the test.readWorkspaceNavigation()reads the active application's mobile navigation.
Dispose the island and verify its actions disappear. Use the public
runContextAwareCommand() and openCommand() APIs to exercise execution. These
test probes do not replace application authorization or a real browser check.