Cloud|

Universal search

Project focused application Queries into the shared Cloud search.

22 min read Updated 2026-10-19 #search#capabilities#authorization

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.

For utility pages such as a QR generator, add searchLinks to defineApp():

ts
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.

ts
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 open link;
  • 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.

See Resource authorization.

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.

ts
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.ref for the contents of a particular notebook, project, chat or mailbox.
  • Use scope.appId for everything searchable in an application.
  • Add scope.tag to 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:

ts
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.

ts
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.

ts
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.

tsx
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:

ts
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:

ts
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.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.