Parsing Commands in the Omnibox

Turn an extension's omnibox keyword into a small command line: parsing subcommands and arguments, suggesting commands as the user types, showing usage hints, quoting, and dispatching to handlers safely.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

A tab-manager extension registers the keyword tm. Typing tm save reading-list should save the current window as a session, tm open reading-list should restore it, tm close dupes should close duplicate tabs, and plain tm github should just search tabs. That is a command line living in the address bar — fast for power users and invisible to everyone else. The omnibox API gives you raw text on each keystroke and on Enter; turning that into commands with arguments, live usage hints and safe dispatch is up to you. This guide builds a small parser and suggestion layer. It belongs to omnibox and address bar integration.

A command grammar for the omnibox

Keep the grammar simple enough to type without thinking: the first word is a command if it matches a known name or alias; the rest are arguments, split on spaces with quotes for arguments containing spaces. If the first word is not a command, the whole input is a default action (usually search). Each command declares its arguments, so the suggestion layer can show usage (“save ‹name›”) while the user types, and can complete argument values (existing session names, open tab titles). Enter dispatches the parsed command to a handler; unknown or incomplete commands fall back to showing help rather than doing something surprising.

From typed text to a dispatched commandTyped text is tokenised with quote support, the first token is matched against commands and aliases, arguments are validated against the command's spec, and Enter dispatches to the handler; non-commands fall through to search."open 'work tabs'"raw inputtokenize[open, work tabs]match commandname or aliasEntervalidate argsrequired, choiceshandler(args)open sessionfallbacksearch / help
Tokenise, match, validate, dispatch — or search.

Step-by-step: an omnibox command line

1. Declare commands as data

 1// commands.js
 2export const COMMANDS = [
 3  { name: "save",  aliases: ["s"],  args: [{ name: "name", required: true }],                 help: "Save this window as a session",
 4    run: ({ name }) => saveSession(name) },
 5  { name: "open",  aliases: ["o"],  args: [{ name: "name", required: true, complete: sessionNames }], help: "Restore a saved session",
 6    run: ({ name }, disposition) => openSession(name, disposition) },
 7  { name: "close", aliases: [],     args: [{ name: "what", required: true, choices: ["dupes", "others", "right"] }], help: "Close tabs",
 8    run: ({ what }) => closeTabs(what) },
 9  { name: "help",  aliases: ["?"],  args: [],                                                 help: "List commands",
10    run: () => chrome.tabs.create({ url: chrome.runtime.getURL("help.html#omnibox") }) },
11];
12export const byName = new Map(COMMANDS.flatMap((c) => [[c.name, c], ...c.aliases.map((a) => [a, c])]));

Execution context: the service worker. A declarative table gives the parser, the suggestions and the help page one source of truth. complete functions supply argument values; choices constrain them.

2. Tokenise with quotes

1export function tokenize(input) {
2  const tokens = [];
3  const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
4  for (const m of input.matchAll(re)) tokens.push(m[1] ?? m[2] ?? m[3]);
5  const trailingSpace = /\s$/.test(input);
6  return { tokens, trailingSpace };
7}
8// tokenize("open 'work tabs'") → { tokens: ["open", "work tabs"], trailingSpace: false }

Execution context: the service worker. Quoted arguments allow spaces in names. trailingSpace tells the suggestion layer whether the user is still typing the current token or has moved on to the next, which changes what to suggest.

What to suggest at each point in the inputThe suggestion shown for an empty input, a partial command name, a complete command awaiting an argument, a partial argument, and a non-command query.TypedDefault rowSuggestions(empty)Type a command or searchAll commands with help"op"open ‹name›Commands starting with op"open "open ‹name› — Restore a saved session…Session names"open wo"Open session: work tabsSessions matching wo"github"Search tabs for githubMatching tabs
Suggestions follow the cursor through the grammar.

3. Parse into a command and arguments

 1export function parse(input) {
 2  const { tokens, trailingSpace } = tokenize(input.trim() ? input : "");
 3  const cmd = byName.get(tokens[0]?.toLowerCase());
 4  if (!cmd) return { kind: "search", query: input.trim() };
 5  const values = tokens.slice(1);
 6  const args = {}; const errors = [];
 7  cmd.args.forEach((spec, i) => {
 8    const v = i === cmd.args.length - 1 ? values.slice(i).join(" ") : values[i];   // last arg takes the rest
 9    if (!v && spec.required) errors.push(`missing ‹${spec.name}›`);
10    else if (v && spec.choices && !spec.choices.includes(v)) errors.push(`‹${spec.name}› must be ${spec.choices.join(", ")}`);
11    else if (v) args[spec.name] = v;
12  });
13  return { kind: "command", cmd, args, errors, trailingSpace, argIndex: Math.max(0, values.length - (trailingSpace ? 0 : 1)) };
14}

Execution context: the service worker. The last argument swallows remaining words, so save my reading list works without quotes. Errors are collected rather than thrown, so the suggestion layer can show them as hints.

4. Suggest commands and arguments as the user types

 1chrome.omnibox.onInputChanged.addListener(async (text, suggest) => {
 2  const p = parse(text);
 3  if (p.kind === "search") {
 4    const partial = COMMANDS.filter((c) => c.name.startsWith(text.trim().toLowerCase()) && text.trim());
 5    chrome.omnibox.setDefaultSuggestion({ description: partial[0] ? usage(partial[0]) : `Search tabs for <match>${escapeXml(text)}</match>` });
 6    return suggest([...partial.slice(1).map(cmdSuggestion), ...(await tabMatches(text))].slice(0, 6));
 7  }
 8  const spec = p.cmd.args[p.argIndex];
 9  chrome.omnibox.setDefaultSuggestion({ description: p.errors.length ? `${usage(p.cmd)} <dim>— ${escapeXml(p.errors[0])}</dim>` : `${escapeXml(p.cmd.help)}: <match>${escapeXml(Object.values(p.args).join(" "))}</match>` });
10  if (spec?.complete || spec?.choices) {
11    const prefix = p.trailingSpace ? "" : (Object.values(p.args).at(-1) ?? "");
12    const values = spec.choices ?? (await spec.complete());
13    return suggest(values.filter((v) => v.toLowerCase().startsWith(prefix.toLowerCase())).slice(0, 6)
14      .map((v) => ({ content: `${p.cmd.name} ${quote(v)}`, description: `${p.cmd.name} <match>${escapeXml(v)}</match>` })));
15  }
16  suggest([]);
17});
18
19const usage = (c) => `<match>${c.name}</match> ${c.args.map((a) => `‹${a.name}›`).join(" ")} <dim>— ${escapeXml(c.help)}</dim>`;
20const quote = (v) => (/\s/.test(v) ? `"${v}"` : v);

Execution context: the service worker. The default row always shows either usage for the command being typed, an error hint, or what Enter will do. Argument completions put the full command line in content, so selecting one and pressing Enter dispatches the complete command. See escaping XML in omnibox descriptions.

Typing a command with completionThe user types tm then op; the default row shows open usage; after a space, session names are suggested; the user picks work tabs and presses Enter; the worker parses the content and restores the session.UserOmniboxService workertm opdefault: open ‹name›"open "suggest session namesEnter on "open 'work tabs'"parse → openSes…
Usage hints, then argument completion, then dispatch.

5. Dispatch on Enter

 1chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
 2  const p = parse(text);
 3  if (p.kind === "search") return searchTabs(p.query, disposition);
 4  if (p.errors.length) return chrome.tabs.create({ url: chrome.runtime.getURL(`help.html#${p.cmd.name}`) });
 5  try {
 6    await p.cmd.run(p.args, disposition);
 7  } catch (e) {
 8    notifyError(chrome.i18n.getMessage("commandFailed", [p.cmd.name, e.message]));
 9  }
10});

Execution context: the service worker. Incomplete commands open help for that command rather than guessing. Errors from handlers are reported, since the omnibox closes on Enter and offers no other feedback. Commands that open pages should respect disposition, as in handling omnibox input entered navigation.

6. Guard destructive commands

close others closes many tabs on a single Enter. For destructive commands, either make them undoable (save the closed tabs as a session and show a notification with “Undo”), or require an explicit confirmation word (close others!). Never let a fuzzy match on the command name trigger a destructive action — match command names exactly or by declared alias only.

7. Generate help from the table

1export const helpHtml = () => COMMANDS.map((c) =>
2  `<dt><kbd>tm ${c.name}</kbd> ${c.args.map((a) => `‹${a.name}›`).join(" ")}</dt><dd>${c.help}</dd>`).join("");

Execution context: the help page. Generating help from the same table guarantees it is never out of date.

Common mistakes

  • Fuzzy-matching command names. Typos trigger the wrong command.
  • No usage hints. Users cannot discover arguments.
  • content without the command. Picking a completion then dispatches only the argument.
  • Silent failures. The omnibox closes; show a notification on error.
  • Unescaped user text in descriptions. Breaks the XML.

Cross-browser variation

  • Chrome / Edge: XML descriptions with <match>, <dim>, <url> support usage hints.
  • Firefox: same events; descriptions are plain text, so usage hints must be written without markup.
  • Safari: no omnibox API; put the command line in the popup’s search field.

Verification

  1. Type each command partially and confirm usage appears in the default row.
  2. Type open and confirm session names are suggested and selecting one opens it.
  3. Type close maybe and confirm the choices hint appears and Enter opens help.
  4. Type a non-command and confirm search runs.

FAQ

Can the keyword itself vary per command?

No. An extension registers one keyword; subcommands live inside it.

How do I let users discover the commands?

Show all commands when the input is empty, and add a help command and a help page.

Should arguments be case-sensitive?

Usually not for matching; preserve case when saving names.

Can commands take flags like --all?

Yes — treat tokens beginning with -- as flags before assigning positional arguments, and suggest them in completions. Keep flags rare; most omnibox commands are clearer with a subcommand instead.

How do I add a new command later?

Add one entry to the table with its arguments, help text and handler. Suggestions, parsing and the generated help page pick it up automatically, which is the main reason to keep commands declarative.

Other UI/UX Patterns & Interactive Components Resources