---
title: Tag a session
---

# Tag a session

## What it is

**Session tags** are short colored labels you attach to a session for quick categorisation — things like `urgent`, `bug`, `client-acme`, or `followup`. This page covers the **free-form** flavor — short strings you type into the picker on the spot, stored as a JSON array directly on the session row. A second flavor — **library tags**, curated entries managed in **Settings → Tags** — coexists with free-form on every session; see [tag-manager.md](tag-manager.md) for that system. Free-form tag names are up to **30 characters** each; the **10-tag cap is combined** across library applications and free-form strings (a session with 5 library applications and 5 free-form tags is full). The picker is a centered modal palette — open it with the **T** hotkey when a session is selected (composer not focused), or click the **Tags…** row in the session overflow menu's "More" subsection.

The feature is gated by a setting: **Settings → Sessions → Session Tags** ("Add short labels to sessions for quick categorisation. Tags appear as colored chips on the session header and can be managed from the session overflow menu."). When the toggle is off, the Tags subsection is hidden from the overflow menu and the chip strip disappears from the SessionPanel header — existing tags on a session remain in storage and reappear when you toggle it back on.

## Where to find it

Press **T** with a session selected and the composer not focused, and a centred tag picker opens — or use the **Tags…** row in the session’s **⋯** overflow menu, which opens the same palette. The chips then render in the strip under the title of an open session, and on a phone they collapse to the first tag plus a **+N** chip.

## How it behaves

### How to use it

Press **T** with a session selected (composer not focused) to open a centered Tag picker palette. The input is auto-focused — type a tag name, press **Enter** to add it, keep typing to add more, **Esc** to close. The session overflow menu (`⋯`) also has a **Tags…** row that opens the same palette.

1. **Add a tag — type and press Enter.** As you type, a popover lists matching existing tags ordered by usage count (most-used first when the input is empty). Pick one with the mouse, or arrow-key to it and press Enter. If your text doesn't match any existing tag exactly, a "Create _yourtext_" row appears at the bottom of the list — Enter creates that tag on the spot. Matching is **case-insensitive**: typing `Bug` when a tag `bug` already exists picks up the existing one rather than creating a duplicate.
2. **Browse all existing tags.** Click **Browse all (N)…** at the bottom of the suggestions popover to swap to a checkbox-grid view of every tag in the database, with its usage count. Type in the search box at the top to filter the grid. Check a box to add the tag, uncheck to remove. Click the back arrow to return to the type-and-create view.
3. **Remove a tag.** Click the **×** on any tag chip. Or, with the input empty, press **Backspace** — that removes the last chip in the list. (Tag chips are always visible above the input; you don't need to open any popover to remove them.)
4. **Hit the limits.** The tag-text input has a hard **30-character** cap (the browser silently stops accepting input past that). Trying to add an **11th** tag silently no-ops — the Create row stays visible but appears disabled, with a tooltip "Maximum 10 tags" explaining why. The cap is combined with library applications, so a session already carrying 10 library tags will refuse a free-form add the same way.

**On a phone, multiple tags collapse to keep the header tidy.** When a session has more than one tag, the mobile session header shows only the **first tag plus a "+N"** chip (N = how many are hidden) so the tags can't wrap and shove the title around. Tap **+N** to reveal every tag in place; a **Show less** chip collapses them again. On desktop — and for a session carrying a single tag — every tag shows at once. Removal (the **×**), colors, and the combined 10-tag cap are unchanged. The behavior is locked by [session-tag-strip-contract.md](../../.claude/memory/contracts/session-tag-strip-contract.md).

## For agents

### How it works

The picker is `TagPicker` at [src/renderer/src/components/ui/TagPicker.tsx](../../src/renderer/src/components/ui/TagPicker.tsx) — a small ARIA combobox mounted inside a centered modal portal whenever the global tag-palette store is opened (via the **T** keybinding dispatcher or the overflow-menu "Tags…" row); the dispatcher and the menu row both gate on `sessionTagsEnabled === true`. It calls a hook, `useAllTags`, which invokes the `SESSION_LIST_ALL_TAGS` IPC channel; the backing query is `getAllTagsWithCounts()` in [src/main/db/queries-sessions/index.ts](../../src/main/db/queries-sessions/index.ts), which scans every non-deleted session's `tags` JSON array using SQLite's `json_each` and groups by tag value. That's why "frequent tags first" works without a separate tag table — the count comes from the live data. Tag colors are deterministic: `getTagColor()` at [src/renderer/src/lib/tag-color.ts](../../src/renderer/src/lib/tag-color.ts) hashes the tag string into one of 8 Tailwind colors (`bg-sky-400`, `bg-emerald-400`, `bg-amber-400`, `bg-rose-400`, `bg-violet-400`, `bg-teal-400`, `bg-orange-400`, `bg-pink-400`), so the same tag always renders the same color across the picker chips, the suggestion list, and the SessionPanel header chip strip.

Storage is a JSON-array column directly on the `sessions` row (`sessions.tags`) — there is no separate `tags` table for free-form, no join table, no soft-delete on individual tag strings. The renderer sends `onChange` updates through the session store, which writes the new array via the session-update IPC handler. The setting itself, `sessionTagsEnabled`, is a boolean on `AppSettings` (defined in [src/shared/types.ts](../../src/shared/types.ts) and wired through `updateSettingsSchema` in [src/shared/ipc-schemas.ts](../../src/shared/ipc-schemas.ts)); the toggle in [src/renderer/src/features/settings/sections/session/SessionSettings.tsx](../../src/renderer/src/features/settings/sections/session/SessionSettings.tsx) flips it. Chips render in **one place only** — the SessionPanel header chip strip immediately under the title row of an open session (see [tag-manager.md](tag-manager.md) "Where chips actually appear in v1" for the merged library + free-form render path). Sidebar session rows do **not** show tag chips or color dots.

### Library vs free-form

Free-form tags (this page) and library tags ([tag-manager.md](tag-manager.md)) are two coexisting flavors that render side by side on every session. Free-form is the **type-and-Enter path** — strings you invent on the fly, persisted as JSON on `sessions.tags`. Library tags are the **curated path** — pre-defined entries created once in **Settings → Tags**, optionally scoped to a subset of projects, and applied via a backend join (no string lands in `sessions.tags`). Both flavors share the same picker surface (the **T**-hotkey palette and the overflow-menu **Tags…** row), the same chip render in the SessionPanel header strip, and the same combined 10-tag cap. Use free-form when the label is one-off or session-specific; promote to library when you want consistent color, project scope, or a description across many sessions.

### Renderer dedupe rule (library wins)

When a session carries both a library application and a free-form string with the same trimmed-lowercase name (e.g. a library tag `feature` plus a free-form `'Feature'` on `sessions.tags`), the SessionPanel header chip strip renders only the library chip — the free-form duplicate is silently filtered for that render. The merge logic lives in [src/renderer/src/features/sessions/SessionPanel.tsx](../../src/renderer/src/features/sessions/SessionPanel.tsx) (the `mergedChips` memo builds the merged list; the header chip strip renders it): library applications are iterated first in `applied_at` ascending order, populating a `taken` Set keyed by trimmed-lowercase name; free-form strings are iterated second and skipped if their key is already taken. Library always wins because the library row carries authored color, description, and scope metadata that a free-form duplicate can't.

### Tag-name validation (permissive on both flavors)

Library tag names and free-form tag names share the same permissive rule — any non-empty string up to 30 characters, including mixed case, spaces, punctuation, and the `__` prefix the Omniscio plugin system uses as a sentinel on `sessions.tags`. The shared regex `TAG_NAME_REGEX` from [src/shared/tag-types.ts](../../src/shared/tag-types.ts) is `/^.+$/` (empty-rejection only); length is capped at the Zod boundary. The dialog's inline error message (`TAG_NAME_VALIDATION_MESSAGE`) reads `Tag name cannot be empty.` and only fires when the trimmed name is empty. The historical `(?!__)` lookahead that banned library names from starting with `__` has been removed — it was scaffolding to prevent a library tag from displacing the `__plugin__` sentinel via the renderer dedupe, but that concern doesn't materialise (library tags never write into `sessions.tags` and plugin-detection code reads the JSON column directly). A user-created library tag named `__plugin__` is a cosmetic edge case, not a data-corruption risk.

### Picker grouping (From library / Recent · free-form)

With the library populated, the suggestions popover opened by the picker gains a **From library** group above the free-form group. Library matches visible to the current session's project (per the scope rule in [tag-manager.md](tag-manager.md) "Scope and visibility") render first, ordered by name; free-form matches render second, ordered by usage count (most-used first when the input is empty, falling back to "frequent" / "recent" within the input-filtered set). The familiar **Create _yourtext_** row still appears at the bottom for inputs that match neither group — Enter creates a free-form tag exactly as before, even if a library tag with the same name exists in a project scope that excludes the current session. The grouping is wired through [src/renderer/src/components/ui/TagPicker.tsx](../../src/renderer/src/components/ui/TagPicker.tsx) (the **From library** label is at line 293) and seeded by [src/renderer/src/components/ui/TagPaletteGate.tsx](../../src/renderer/src/components/ui/TagPaletteGate.tsx), which derives the visible-library subset before opening the picker.

### Deletion behaviour (free-form unaffected by library deletion)

Three deletion paths exist and they are mutually independent — deleting one kind never cascades into the other:

- **Remove a free-form chip via × in the picker.** Mutates `sessions.tags` for that one session — the string vanishes from this session's array; every other session that holds the same string is untouched.
- **Delete a free-form tag globally** via `session:delete-tag-globally` (`SESSION_DELETE_TAG_GLOBALLY`). Scrubs the string from every non-deleted session's `tags` array. Does **not** touch the library — a library tag of the same name keeps its row, its scope, and its existing applications.
- **Soft-delete a library tag** (Settings → Tags → trash icon → confirm dialog). [`softDeleteTag`](../../src/main/db/queries-tags.ts) explicitly `DELETE`s every `session_tags` row for that `tag_id` and then `UPDATE`s the library row to `is_deleted = 1`, both inside one transaction — so every application across every session is removed. (`session_tags.tag_id` does also have `ON DELETE CASCADE`, which acts as belt-and-suspenders if the row were ever hard-deleted; soft-delete is an `UPDATE`, so the explicit `DELETE` is what actually scrubs the applications.) Free-form strings of the same name on `sessions.tags` are **not** scrubbed. The confirm dialog spells this out: "This removes the tag from N session(s). Free-form tag chips with the same name remain unaffected."

The independence is deliberate. A free-form `feature` and a library `feature` are two different storage paths that happen to share a name; lifecycle operations on one never silently delete the other.

## Related

- [tag-manager.md](tag-manager.md) — the curated library system that coexists with free-form: scope, sticky applications, IPC contract, deletion semantics
- [snooze-a-session.md](snooze-a-session.md) — another overflow-menu action for organising the session list
- [bulk-select-sidebar.md](bulk-select-sidebar.md) — Shift+J/K multi-select for batch operations on tagged sessions
