# Extending Flowbar

This guide explains how Flowbar is structured and how to add commands, search
sources, keyboard triggers, pickers, clipboard actions, and destructive
operations without breaking the command palette.

## Architecture

Flowbar is a Manifest V3 extension with three UI/runtime contexts:

```text
":" or Option+Space
        |
        v
content.js (bottom palette)  <---->  background.js (Chrome APIs)
                                             ^
                                             |
popup.js (restricted-page fallback) ---------+
```

- `manifest.json` declares permissions, the service worker, content-script
  injection, the toolbar popup, and the Chrome-managed fallback shortcut.
- `background.js` owns privileged Chrome API calls, the command registry,
  uFuzzy ranking, and command execution.
- `content.js` renders the bottom palette, handles keyboard navigation, and
  sends requests to the service worker.
- `content.css` isolates the palette's appearance from host-page styles.
- `popup.html` and `popup.js` provide a fallback on pages where Chrome blocks
  content scripts.
- `vendor/uFuzzy.iife.min.js` is the locally vendored fuzzy matcher. Flowbar
  does not load executable code from the network.

The service worker is the source of truth. Both UI surfaces should send it the
same messages rather than implement separate Chrome API behavior.

## How search works

`searchAll()` in `background.js` collects:

1. Registered commands.
2. Tabs across all windows.
3. All bookmarks.
4. The most recent history candidates.

Each list is independently passed through `fuzzyRank()`. Results remain
grouped by category while receiving uFuzzy relevance ordering within that
category. Adding an entry to `COMMANDS` automatically makes it searchable.

Bookmarks and history use a short-lived cache. Bookmark/history events
invalidate the cache so typing does not repeatedly query large Chrome data
sets.

## Adding a simple command

Add searchable metadata to `COMMANDS` in `background.js`:

```js
{
  commandId: "reload-tab",
  title: "Reload Tab",
  url: "Reload the current tab"
}
```

Then add its behavior to `executePaletteCommand()`:

```js
if (commandId === "reload-tab") {
  await chrome.tabs.reload(activeTab.id);
  return {};
}
```

Command IDs are the stable API between the UI and service worker. Do not use
the display title as an identifier. Keep titles short and put search aliases or
extra context in the description (`url`) field because uFuzzy searches both.

`getCommandTab(sender)` prefers the tab that opened the bottom palette. It
falls back to the active tab for commands invoked from the toolbar popup.

## Returning results from a command

The service worker uses a small response protocol:

| Result | Response | UI behavior |
| --- | --- | --- |
| Completed action | `{}` | Close the palette |
| Copy text | `{ copyText: "value" }` | Copy, confirm, then close |
| Choose a destination | `{ picker: { title, items } }` | Render a nested picker |
| Failure | Throw `Error` | Keep the palette open and show the message |

Always return serializable data. DOM nodes, functions, `URL` objects, and Chrome
API objects with non-serializable fields must not cross a runtime message.

## URL transformation commands

Parse URLs with the `URL` class instead of string replacement. Validate the
source origin or path before changing the tab.

The ZoomDev toggle is the reference pattern:

```js
if (url.origin === "http://localhost:3000") {
  url.protocol = "https:";
  url.hostname = "zoomdev.us";
  url.port = "";
  return url.href;
}
```

Changing only `protocol`, `hostname`, and `port` preserves the path, query,
and hash. Reject unrelated origins so a command cannot unexpectedly rewrite an
arbitrary website.

For path extraction, validate the path structure before reading a segment. The
Doc ID command expects `/wb/<type>/<docid>/...` and returns the third non-empty
path segment.

## Clipboard commands

Service workers do not have a normal page DOM, so command handlers return
`{ copyText }` instead of writing to the clipboard directly. The UI performs
the copy while it still owns the user gesture.

When adding another copy command:

1. Add `clipboardWrite` to `manifest.json` if it is not already present.
2. Validate and extract the value in `background.js`.
3. Return `{ copyText: extractedValue }`.
4. Reuse `copyTextToClipboard()` in `content.js`.

Do not place secrets or full page content in status messages after copying.

## Commands that need a picker

For multi-step commands, return a picker from the first invocation:

```js
return {
  picker: {
    title: "Move Tab To Window",
    items: [
      {
        title: "Window title",
        url: "4 tabs",
        targetWindowId: 123
      }
    ]
  }
};
```

`content.js` renders these items with type `window`. Selecting one sends the
same command again with `targetWindowId`. `Esc` returns to the main palette.

Use opaque Chrome IDs for execution and human-readable titles for display.
Revalidate the target in the service worker because a window or tab may close
while the picker is open.

## Destructive actions

`Ctrl+X` operates on the highlighted result and routes through
`flowbar:remove-result`:

- Tab: `chrome.tabs.remove()`
- Bookmark: `chrome.bookmarks.remove()`
- History: `chrome.history.deleteUrl()`

Commands and picker entries must never be removable by `Ctrl+X`. If a new
search category supports deletion, add its type to the allowlist in
`removeSelectedResult()` and add explicit validation in `removeResult()`.

Remember that deleting a history URL removes its visits, not merely the one
visible search row.

## Adding another search category

To add a category such as recently closed tabs:

1. Request only the required manifest permission.
2. Load and normalize items in `searchAll()` or a cached catalog helper.
3. Give every item `title` and `url` strings for fuzzy matching and display.
4. Return the list from `searchAll()` under a stable property name.
5. Add the category to both `content.js` and `popup.js`.
6. Add open/remove routing only if the category supports it.

Apply a candidate limit before fuzzy matching if the Chrome API can return an
unbounded data set. Keep `MAX_RESULTS_PER_CATEGORY` small enough for fast DOM
rendering.

## Keyboard triggers

Flowbar has two entry paths:

- `:` is captured by `content.js` on normal web pages. It is intentionally
  ignored inside inputs, textareas, selects, and contenteditable elements.
- `Option+Space` on macOS (`Ctrl+Shift+Space` elsewhere) is registered through
  the Chrome Commands API. Websites cannot consume this shortcut. The service
  worker sends `flowbar:open` to the current content script and falls back to
  `chrome.action.openPopup()` on restricted pages.

Do not add a global browser shortcut for every action. Add actions to the
palette and keep a single reliable shortcut for opening it. Users can remap the
opening shortcut at `chrome://extensions/shortcuts`.

## Permissions

Before adding a permission, confirm that the command cannot be implemented
with an existing one. Permission changes can disable an installed extension
until the user approves them.

Current permissions are used as follows:

| Permission | Purpose |
| --- | --- |
| `tabs` | Search, focus, duplicate, move, close, and navigate tabs |
| `history` | Search and delete history URLs |
| `bookmarks` | Search and delete bookmarks |
| `windows` | Focus, merge, create, and enumerate windows |
| `sessions` | Restore recently closed tabs |
| `clipboardWrite` | Copy command output |

Keep content-script match patterns limited to the pages that need the bottom
palette. Chrome-owned pages cannot host content scripts and must use the popup
fallback.

## Validation checklist

Run static checks after every change:

```sh
node --check background.js
node --check content.js
node --check popup.js
node -e 'JSON.parse(require("fs").readFileSync("manifest.json", "utf8"))'
```

Then reload the unpacked extension at `chrome://extensions` and test:

1. `:` opens the palette on a normal page when focus is not editable.
2. `Option+Space` opens it even when a Google search/editor field has focus.
3. The toolbar popup works on a restricted page.
4. The new command can be found using abbreviated fuzzy input.
5. Enter and mouse click produce the same result.
6. Errors leave the palette open with a useful message.
7. Paths, queries, and hashes survive URL transformations.
8. `Ctrl+X` affects only removable search results.
9. Incognito and normal windows are not mixed.

Test both directions for every toggle and include malformed or unrelated URLs
in helper-level tests.

## When the command list grows

`executePaletteCommand()` is intentionally direct while Flowbar is small. Once
the registry grows beyond roughly a dozen commands, move command handlers into
a map keyed by `commandId`:

```js
const COMMAND_HANDLERS = {
  "reload-tab": async ({ activeTab }) => {
    await chrome.tabs.reload(activeTab.id);
    return {};
  }
};
```

Keep metadata and handlers keyed by the same ID, but do not expose privileged
handler functions to content scripts. The service worker must remain the only
place that validates and executes commands.
