Tab IDs in KISS Sorcar: How Workflows Route Around a Pane Identifier
A line-verified tour of every tab_id / tabId use in
src/kiss/core/, src/kiss/agents/sorcar/, and
src/kiss/server/ — branch bigrefactor, after the
mirror-clients refactor and the removal of the interactive diff/merge review.
All line numbers cite the current working tree.
1. What a tab id is — the four identifier forms
A tab id is the routing key of one pane in the frontend: a chat tab in the
VS Code webview or in the kiss-web browser app. Under the mirror-clients model every
connected client shows the same set of tabs, so a tab id names a globally shared
pane, not a per-client one. The backend uses tab ids in two dominant roles: to
stamp outbound events so clients know which pane renders them, and to look up the
per-tab server state (the launched task, its model pick, its viewed chat) when a command
arrives carrying a tab id. A few narrower roles also appear and are covered where they arise:
the headless API client filters its shared input stream by tab id
(sorcar.py:1187), the server parses the synthetic
__sub_ id convention to find a sub-agent's parent tab
(server.py:1141–1145), _fanout_talk tests target ids
against the local-UDS tab set for talk-playback arbitration
(web_server.py:1977–1989), and snake_case tab ids ride some
payloads as plain data (the sub-agent tab tree, subagentDone).
Four identifier forms appear on the wire, distinguished only by convention:
| Form | Example | Minted by | Purpose |
|---|---|---|---|
| Frontend UUID | 3f2a…c9 |
The webview's createNewTab flow; the backend first sees it on a
newChat / run command. |
An ordinary interactive chat tab. server.py:875–879 documents
that newChat always carries a fresh, never-seen id. |
| Synthetic API tab | api-{uuid.hex} |
The headless Python client kiss.server.sorcar.run(), one per call.
sorcar.py:1120 |
Makes a headless UDS client indistinguishable from a frontend tab: the daemon fans events out stamped with this id and the client filters its own stream by it (sorcar.py:1136, 1187). |
| Live sub-agent tab | task-{parent_task_id}__sub_{idx} |
The parent agent, in ChatSorcarAgent._run_tasks_parallel.
chat_sorcar_agent.py:613 |
Gives each parallel child its own child pane; injected as the child's
agent._tab_id (:614). |
| Replay sub-agent tab | {parent_tab_id}__sub_{sub_task_id} |
The server, when re-opening persisted sub-agent runs on session resume. server.py:1188 | Deterministic, so clicking the same parent task twice updates sub-tabs in place instead of stacking duplicates (server.py:1165–1171). |
The __sub_ substring in the last two forms is a cross-layer contract:
server.py's _resolve_parent_tab_id_for_sub parses it with
rsplit("__sub_", 1) as its last-resort tier when inferring which tab a
sub-agent pane should nest under (server.py:1141–1145) — the
right-split keeps nested chains (A__sub_B__sub_C → parent prefix
A__sub_B) correct. Section 6 covers the whole mechanism.
tabId on an event is a routing stamp — it tells the printer
"this copy is pre-addressed; deliver it to clients (which filter panes by it) and do not
record or persist it". Snake_case tab_id / parent_tab_id fields are
plain payload data the webview stores (e.g. the tab-tree structure in
openSubagentTab). An event can carry both at once — subagentDone ships
{"tab_id": vid, "tabId": ""}: an all-clients broadcast (empty routing stamp)
whose payload names the pane whose spinner should stop
(sorcar_agent.py:256, server.py:1222–1228).
2. The layer map: who touches tab ids, and how much
A case-insensitive grep for tab_id/tabId over the three directories
gives a strictly stratified picture: src/kiss/core/ has zero
occurrences; the three agent files hold 22 lines of narrow UI hooks; and the
nine server files own everything else — 507 lines. Commands flow
down from clients carrying tabId; task events flow up from agents
completely tab-free and are stamped per-tab only at the fan-out edge inside
WebPrinter.
cmd["tabId"] and are sanitized in
server.py:418–421; agents emit task events with no tab id at all; the per-tab
tabId stamp is spliced onto each subscriber's copy only at the fan-out edge,
WebPrinter._fanout_stamped (web_server.py:1890–1922). The single downward injection
into the agent layer is agent._tab_id = tab_id (task_runner.py:671; the only
other production assignment is the sub-agent synthesis at chat_sorcar_agent.py:614).
The stratification is deliberate. The printer base class is task-centric: "every piece
of per-stream state … is keyed by task_id rather than by the frontend tab id"
(json_printer.py:7–9); tab ids appear only at the edges —
subscription (_subscribers[task_id] → {tab_id, …}) and transient targeting. Agents
receive a tab id only where the UI structurally needs one (sub-agent panes, best-effort
toasts); everything else — steering, persistence, recording — reaches them keyed by task id.
3. Launching a task from a tab
The run command is where a tab id does the most work. Between the frontend's
click and the agent's first event, the launching tab's id is copied into four places:
the tab-keyed guards in commands.py, the new AgentState.tab_id
attribute (in a registry keyed by a uuid state key, not by tab id), the injected
agent._tab_id, and the printer's _task_ui /
_subscribers tables — wired up the moment the run's task_history
row id exists.
T. Line numbers
without a file refer to the lifeline's own file (commands.py for _cmd_run,
task_runner.py for the run lifecycle, json_printer.py for the printer boxes). The launching tab
id ends up in four places: the tab-keyed guards, AgentState.tab_id (in a
uuid-keyed registry entry), agent._tab_id (:671), and the printer's
_task_ui/_subscribers tables — the latter wired by
_on_run_task_id_allocated before any agent event is broadcast, so neither the
launcher nor pre-existing chat viewers can miss a live event.Three details worth calling out:
- The registry is not tab-keyed.
_cmd_runregisters the newAgentStateunder a freshuuid4().hexstate key (commands.py:356–378); the printer bridge later re-keys it to the real task id. The tab id is a searchable attribute, resolved byagent_state.find_by_tab(tab_id)— a two-pass linear scan that prefers server-owned states (agent_state.py:202–221). If the tab had a previous idle state (e.g. holding a pending worktree), its agent andfrontend_closedflag carry over and the old state is unregistered (commands.py:368–373). - Failures before a task id exists are tab-addressed. Setup failures,
the no-model guard, and the merge-in-progress guards broadcast
result/errorevents stamped with the launcher'stabId(task_runner.py:387–410, 682–703, 718–729) because no task-keyed fan-out is possible yet. Once atask_historyrow exists, failureresults prefer ataskIdstamp and only fall back totabId(task_runner.py:890–893, 939–942). - History resume is consumed at launch. If the user had opened a historical
task in this tab,
_tab_opened_task_ids.pop(tab_id)hands the popped id toagent.resume_from_task_id— consumed exactly once per run (task_runner.py:740–743).
4. Event routing: WebPrinter.broadcast
Events sent through the printer's broadcast API funnel through one method, and the
presence of the tabId key (even with an empty value) is the routing
discriminator. There is no per-connection tab filtering in the server:
ordinary tab-addressed and global broadcasts are sent to every connected WSS and UDS endpoint
(_send_to_ws_clients, web_server.py:2056–2071), and the frontend filters
by the tabId stamp. The narrower channels are keyed by connection, never by tab:
a connId-stamped broadcast goes to that single endpoint via
_send_to_conn and returns before any tab routing
(web_server.py:1857–1859), and some replies bypass
broadcast entirely with direct _endpoint_send calls — the ready
flow's tasks_updated / focusInput / openRunningTasks,
plus fileContent and pathsExist (§10.5).
WebPrinter.broadcast
(web_server.py:1806–1889; the recording rules mirror the base class
JsonPrinter.broadcast, json_printer.py:984–1015). The tabId key is the
discriminator between pre-addressed transients (delivered verbatim, unrecorded) and task
events (recorded once tab-free, then stamped per subscriber tab by
_fanout_stamped). Ordinary tab-addressed and global sends go to every WSS + UDS
endpoint — the mirror model — and the frontend filters panes by the stamp;
connId-stamped sends reach only their one endpoint
(_send_to_conn, :1857–1859), and some direct replies bypass this method entirely
via _endpoint_send (§10.5).
The transient primitives layered on top of this contract live in the base printer:
broadcast_transient(event, task_id=None, tab_id="") resolves all watching tabs via
_transient_targets (thread-local task id first, explicit argument as a
near-teardown fallback, _subscribers as the target source — _task_ui is
deliberately not consulted because it is dropped at task teardown) and broadcasts one
stamped copy per tab (json_printer.py:483–553). Even when no
watcher resolves, one copy stamped with the fallback tab_id — possibly
"" — still goes out: the mere key presence preserves the no-record semantics
(json_printer.py:552–553).
5. Steering: follow-up messages into a running task
Follow-ups typed while a task runs are addressed by tab but consumed by task.
The server enqueues them on the owning AgentState found via the tab id (with a
viewer-tab fallback through the printer's subscriber map); the agent drains them through a
duck-typed printer bridge that resolves the state by the calling thread's task id —
the agent never sees a tab id on the consumption side.
AgentState — directly via find_by_tab (two-pass, server-owned
first) or through the printer's subscriber map for viewer tabs — and appends to
task-owned queues. Right: the agent's self-guarding hooks drain those queues via the printer
bridge keyed by the calling thread's task id, and refuse to finish while a
follow-up is undrained. The tab id never crosses to the consumption side.6. Parallel sub-agent tabs
When a task fans out into parallel sub-agents, each child gets its own pane. Live children
are announced by the parent agent, which mints synthetic tab ids; persisted children
are re-opened by the server on session resume with a different, deterministic id form.
Both forms embed __sub_, the contract that lets the server re-derive the parent
pane when it must.
agent._tab_id (the second of the only two production assignment sites); replay ids
are minted by the server from the parent tab id and the child task id, making
re-resume idempotent. In both, the spinner-stopping subagentDone carries the pane
in the snake_case payload and an empty camelCase routing stamp.7. Post-task flows: autocommit and worktree presentation
With the interactive diff/merge review removed, what happens after a task is a short
decision tree, and every branch is tab-addressed: the tab id stamps the progress and
terminal events, keys the find_by_tab lookups that reach the tab's worktree
agent, and anchors the per-tab is_merging mutual-exclusion claim.
worktree_done. Per-tab mutual exclusion is the is_merging flag on the
tab's AgentState, always claimed and released under the daemon-wide
_state_lock, with _dispose_if_closed(tab_id) after every release so a
tab closed mid-flight is finally torn down.8. The tab lifecycle: birth, life, disconnects, death
Since the mirror-clients refactor, tabs are global server state: no connection
owns them, and a disconnect tears down nothing tab-related. The only death is an explicit
closeTab — which is also the only command whose catalog entry requires a
tabId (sorcar.py:322).
AgentState, and the
talk-muting refcount), and exactly one death path: explicit closeTab, deferred via
frontend_closed + _dispose_if_closed when the tab is busy. Disconnects
— WSS or UDS — never touch tab state; the headless client compensates by sending its own
closeTab in a finally.9. Session resume, replay, and the ready flow
9.1 resumeSession → _replay_session (server.py:907–1078)
Clicking a history entry (or restoring a session) sends resumeSession {chatId, tabId
[, taskId]}. The engine's tab-id behavior, in order:
- Empty tab id is a hard no-op (:938–940). The previous behavior of synthesizing a phantom tab keyed by chat id violated per-tab state isolation and was removed (the C2/C3 fix). The tab id — the frontend routing key — never changes during a resume; only the tab's chat association does.
- Record what the tab views:
_tab_opened_task_ids[tab_id] = task_idwhen a specific task row was clicked, popped otherwise (:941–945); consumed once by the next run on this tab (task_runner.py:740–743). - Purge, then rebind: both replay branches call
_printer_cleanup_tab(tab_id)before resubscribing (:954, :1005), so a tab re-viewing a different chat never keeps receiving the old stream. Then_reattach_running_chat(chat_id, tab_id, task_id=…, is_subagent=…)(:1267–1354) subscribes the tab to a still-live source state with two-pass matching: pass 1 requires an exact task-id match (mandatory for sub-agent multi-view); pass 2 falls back to a chat-id match but is skipped entirely whenis_subagent=Trueand always excludes sub-agent states — so a sub-agent view can never subscribe to its parent's stream, and a regular chat can never land inside a sub-agent stream. The single rebinding call isprinter.subscribe_tab(source_task_id, new_tab_id)(:1353) — duplicating the stream to the new pane, never stealing it from existing viewers. - Replayed events are stamped once, at the envelope. Persisted per-event rows
carry no tab id; they are coalesced (consecutive
thinking_delta/text_delta/system_outputmerged, :181–206) and shipped as onetask_eventsevent whose top-leveltabIdis the viewing tab (:1061–1071), withextrafiltered by_extra_for_replayso a replay cannot flip global toolbar toggles (:84–126). A still-running chat with no persisted events yet gets an empty envelope plusstatus {running:true, tabId, startTs}with the true start time from_live_task_start_ms(:962–981, 1230–1265). - Resume clears
frontend_closed(:986, :1015) and ends by re-presenting any pending worktree (_emit_pending_worktree, :1072) and re-opening persisted sub-agent tabs (:1074–1078, Fig 5B).
9.2 ready / restoredTabs (sorcar.py:821–844, web_server.py:4256–4400)
A (re)loading webview announces the tabs it restored. _sanitized_restored_tabs
caps the list, skips non-dict entries, and blanks non-string ids
(web_server.py:4256–4315); each restored id then passes through
_record_tab (talk-muting bookkeeping, sorcar.py:836–840). The web layer's
_handle_ready replays each sanitized {tabId, chatId} pair with one
backend resumeSession (:4369–4376) — the client
supplies the tab ids, the server replays into them. For remote (non-UDS) clients only, it also
pushes openRunningTasks built from _snapshot_running_task_rows() —
rows carry chatId/taskId and no tab ids at all
(:1499–1563, 4377–4399): a reconnecting browser mints fresh panes
and issues its own resumeSessions. Init fan-out (getModels,
getInputHistory, getConfig) is connId-scoped, and a
focusInput {tabId} echo lands on the sender's active pane
(:4345–4366).
10. Smaller flows
10.1 Ask-user: question out task-keyed, answer back tab-resolved
_ask_user_question broadcasts askUser {question} with no
tabId (task_runner.py:1636–1641) — a
task event that fans out to every subscribed pane, so any viewer can answer. The reply,
userAnswer {tabId, answer}, is resolved in two tiers
(commands.py:579–626): ① the state launched from the answering
tab itself, when it has a live user_answer_queue; ② otherwise any task the tab is
subscribed to, via the printer's _subscribers map — resolving through the
task id makes a cross-task answer hijack structurally impossible. The queue is
drain-then-put under _state_lock (a maxsize=1 queue cannot wedge on a
double answer), and askUserDone {tabId} is broadcast once per subscriber tab of the
answered task — exactly the panes showing that modal, not every tab the answerer ever
touched (:536–577). The waiting side resolves its queue by the
calling thread's task id, deliberately tab-free
(task_runner.py:1607–1625).
10.2 Stop
_stop_task(tab_id) refuses an empty tab id with a warning — a
missing tabId at this layer indicates a frontend bug that must not silently stop
every tab's task (task_runner.py:1390–1397). Resolution:
find_by_tab first; when the tab owns no live task, the viewer fallback
_find_viewer_task_states scans _subscribers for tasks the tab views
(:1398–1413, 1470–1506) — so a second client viewing a running
task via history-click can stop it. stop_ack {accepted, tabId} is broadcast to the
clicking pane before the stop event is set, so the click visibly lands even if the task
dies on the next bytecode (:1431–1437, 1452–1468).
10.3 Model picker
The picker is per-tab: _tab_models[tab_id] is written by
selectModel (empty tab id degrades to updating only the daemon default,
commands.py:407–426) and read when a run omits a model (task_runner.py:665, server.py:394–404).
When a running agent calls set_model, the display-only override reaches every
watching pane via broadcast_agent_model_pick, which remembers the targets in
_model_override_tabs and the model in _task_model_override[task]
(json_printer.py:587–626); a pane joining mid-task catches up in
subscribe_tab (:272–279). At task end,
restore_model_pick hands each overridden pane its user's own pick back — a no-op
for panes that never showed an override (:628–642;
task_runner.py:448–468, 523). The agent side supplies only best-effort hints:
_show_model_in_picker passes _tab_id/_last_task_id so the
fan-out still works near teardown, after the printer's thread-local task id is cleared
(sorcar_agent.py:1064–1096).
10.4 Autocomplete: the tab id as inert reply cargo
autocomplete.py holds zero tab-keyed state: staleness sequences and
request tokens are keyed by connection id, the file cache by work_dir
(:193–195). The request's tabId rides the asynchronous
autocomplete work as inert cargo — through the _complete_queue worker tuple for
ghost and completions
(commands.py:801–805; :395–454), and as ordinary
_get_files / _emit_files / refresh-closure arguments for
files (:567–572, 627–670) — and is echoed on all
three replies
because one window shows several chat tabs over one connection but has a single ghost overlay
and a single @-picker element: "the tab is what actually identifies the owner of
the reply" (:656–659). The request side's only semantic use is in
commands.py:_cmd_complete, which resolves the tab's chat context
(find_by_tab, then _tab_chat_views)
(commands.py:780–806). The post-task cache rescan deliberately
emits no files event at all — an unsolicited reply with blank
conn/tab/prefix would be accepted by every client showing a bare @
(autocomplete.py:576–625).
10.5 Direct tab-stamped utility replies
| Event | Trigger | Delivery | Where |
|---|---|---|---|
files | getFiles (@-mention) |
connId narrows to the window, tabId to the pane |
commands.py:463–488; autocomplete.py:627–670 |
commitMessage | generateCommitMessage |
all four exit paths (not-a-repo, nothing staged, success, failure) stamp the requesting tabId |
server.py:1454–1520 |
fileContent | openFile (remote web) |
direct _endpoint_send reply; tabId is a pure request echo |
web_server.py:3943–4020 |
pathsExist | checkPaths (linkification probe) |
direct reply, tabId echo |
web_server.py:4019–4082 |
adjacent_task_events | getAdjacentTask (history arrows) |
coalesced replay envelope stamped with the requesting pane; chat resolved from the tab's own chat only — no global-latest fallback | commands.py:812–843; server.py:1425–1452 |
error (unknown/invalid command) | dispatcher | echoes the command's tabId, sent to the issuing connection only |
server.py:427–431; sorcar.py:628–634 |
10.6 Talk muting: the one remaining per-connection tab structure
Every command's tabId is recorded in the connection's tabs_seen;
UDS (local VS Code) connections additionally refcount it in
WebPrinter._local_uds_tab_counts via register_local_uds_tab
(sorcar.py:649–666; web_server.py:1939–1952). The sole consumer is
_fanout_talk: Chromium autoplay policy keeps webviews from playing audio, so when a
talk clip targets a locally-shown tab, the daemon plays it natively and stamps those
tabs' copies muted; remote browser tabs get the playable copy
(web_server.py:1954–1990). Since the mirror-clients refactor this
bookkeeping drives nothing else — no filtering, no ownership, no lifecycle.
11. Inventory: every tab-keyed data structure
| Structure | Type | Purpose | Created / written | Cleaned |
|---|---|---|---|---|
_tab_chat_views |
dict[tab_id → chat_id] |
which chat each pane is viewing; viewer discovery at task start | server.py:313; commands.py:355; server.py:988/1017 (replay) | _teardown_tab_resources :868; _new_chat :895; sub-agent views popped :1019 |
_tab_opened_task_ids |
dict[tab_id → task_id] |
history click to resume; consumed once by the next run | server.py:314, 943 | popped at launch task_runner.py:741; teardown :869; new chat :896; no-task resume :945 |
_tab_models |
dict[tab_id → model] |
per-pane model pick | server.py:315, 894 (seed); commands.py:422 | _teardown_tab_resources :870 |
JsonPrinter._subscribers |
dict[task_id → set[tab_id]] |
the single fan-out truth: which panes receive a task's events | subscribe_tab json_printer.py:266–271; register_task_ui :306 | cleanup_tab :741–746; cleanup_task :809–814 (300 s linger via _subscriber_expiry, swept lazily :817–831) |
JsonPrinter._task_ui |
dict[task_id → (tab_id, conn_id)] |
launch-time metadata only; never consulted for fan-out or transients | register_task_ui :305 | cleanup_task :808 (immediate) |
JsonPrinter._model_override_tabs |
set[tab_id] |
panes currently showing a running agent's model instead of the user's pick | broadcast_agent_model_pick :622; subscribe_tab catch-up :277 | restore_model_pick :641; cleanup_tab :739 |
AgentState.tab_id |
attribute on task-keyed states | the launching pane; searched by find_by_tab (two-pass, server-owned first) |
commands.py:361; task_runner.py:606; json_printer.py:355 (from agent._tab_id) | dies with the state: closeTab unregister server.py:803–804; _dispose_if_closed :827; agent_task_finished for non-server-owned |
agent._tab_id |
attribute on the agent object | steering-registry bridge + sub-pane identity + toast/model-pick fallback hints | task_runner.py:671 (per run); chat_sorcar_agent.py:614 (sub-agents); initialized "" worktree_sorcar_agent.py:136 | overwritten on the tab's next run; never explicitly cleared |
WebPrinter._local_uds_tab_counts |
dict[tab_id → refcount] |
talk-playback arbitration only (mute local webview copies) | register_local_uds_tab web_server.py:1939–1946, driven by sorcar.py:662–666 | unregister_local_uds_tabs :1948–1952 from _uds_handler finally :3539–3541; key deleted at zero |
tabs_seen / conn_state["local_tabs"] |
set[tab_id] per connection |
two independent sets: only conn_state["local_tabs"] (UDS peers)
deduplicates registration and later decrements the refcount above via
unregister_local_uds_tabs; tabs_seen is connection-local
observation bookkeeping with no lifecycle effect |
web_server.py:3444/3509 (empty per connection); sorcar.py:649–666 | local_tabs unregistered in the UDS handler's finally (web_server.py:3539–3541); both garbage-collected with the connection |
For contrast, everything else is keyed by task id (_recordings, offsets, bash
buffers, persistence, the agent-state registry itself), by connection id
(_conn_endpoints, autocomplete staleness, _complete_seq_latest), by
endpoint object (_send_locks, _pending_sends), or by work_dir
(_file_cache).
12. The agent-side surface: all 22 occurrences
The three agent files touch tab ids on exactly 22 lines, all via duck-typed printer
attributes (getattr with safe defaults) — the agent layer never imports server
modules, and a printer-less console run degrades every site to a no-op or an empty stamp.
| File · lines | What | Why |
|---|---|---|
| chat_sorcar_agent.py:582, 590 | parent_tab_id = own _tab_id, or the first sorted
_fanout_targets viewer for nested sub-agents |
a renderable anchor for the frontend's tab tree (Fig 5A) |
| chat_sorcar_agent.py:613–614, 623 | mint task-{parent_task_id}__sub_{idx}; inject agent._tab_id;
store parent_tab_id in _subagent_info |
each parallel child streams into its own pane; the injected id is what the printer
bridge copies into the child's AgentState (json_printer.py:355) |
| chat_sorcar_agent.py:662–663 | dedupe sub_tab_id into the viewer list before subagentDone |
the synthetic pane may or may not already be a subscriber |
| chat_sorcar_agent.py:794–795, 800 | new_tab broadcast carrying parent_tab_id (payload) and
taskId: "" |
tells the frontend where to nest the child pane, at run start |
| sorcar_agent.py:234, 246, 254, 256 | _broadcast_subagent_done(printer, tab_ids, model):
{tab_id: vid, tabId: ""} + restore_model_pick per id |
stop each child pane's spinner and hand back its picker; best-effort, errors swallowed |
| sorcar_agent.py:1094 | the file's only _tab_id read: passed with
_last_task_id to broadcast_agent_model_pick |
fallback hints so the model-pick fan-out works off-thread / near teardown; the printer resolves watching panes itself |
| sorcar_agent.py:1511–1513 | non-UI parallel path synthesizes task-{parent_key}__sub_{idx} from the
parent task id alone |
closes out any child pane the frontend opened; no _tab_id read or
assigned on this path |
| worktree_sorcar_agent.py:136 | self._tab_id: str = "" field initializer |
safe default; set by task_runner.py:671 per run (or chat_sorcar_agent.py:614 for sub-agents), left empty for console runs |
| worktree_sorcar_agent.py:278, 283–284 | commit toast via broadcast_transient(event, task_id=…, tab_id=self._tab_id),
falling back to one broadcast({**event, "tabId": self._tab_id}) |
task id is the preferred routing key; the explicit stamp keeps the toast out of recording/persistence on the fallback path (Fig 6) |
Notably absent since the recent refactors: the steering hooks read no tab id
and touch no registry (they are installed unconditionally and drain via the printer bridge,
§5); the old running_agent_state.py per-tab registry module is gone (its role is
served by src/kiss/server/agent_state.py); and the old _subscribe_tab_id
run-kwarg no longer exists in any agent file — launcher-tab subscription is entirely
server-side.
13. Why core/ is tab-free, and what changed recently
src/kiss/core/ has zero tab-id occurrences (a case-insensitive
grep over the tree returns nothing). This is by design, and it is the same design that keeps
the agent surface at 22 lines: the core Printer interface and agent framework
speak only in tasks, steps, and events. The tab — a purely presentational concept, "which
pane of which frontend renders this" — is confined to the server layer, where the
task-centric printer translates at exactly two edges: subscription
(subscribe_tab) and stamping (_fanout_stamped / the transient
primitives). Anything below those edges can run headless, in tests, or embedded without a
frontend existing at all.
What changed recently (context for readers of older docs)
- UI-mirror machinery is gone. Under the mirror-clients assumption (all
clients show the same tabs), the per-viewer
mirrorOfstamping, owner/viewer resolution, andbroadcast_tab_uiwere removed; a single owner-tab-stamped event reaches every client. - Deferred close and per-connection tab ownership are gone. Disconnects no
longer arm teardown timers; a tab dies only by explicit
closeTab(Fig 7). The headless client compensates with its owncloseTabin afinally. - The interactive diff/merge review is gone.
mergeAction,autocommitAction,merge_data/merge_started/merge_navand the per-tab merge-data machinery no longer exist; dirty non-worktree trees always auto-commit, and worktrees present plain Merge/Discard buttons (Fig 6).
web_server.py:_stop_active_agent_tasks the shutdown sweep collects tuples of
(task_id, stop_event, thread) (:5628–5632) but the
unpacking variables in the log line and the join loops are named tab_id /
_tab_id (:5654, 5657, 5662, and the timeout warning near
:5677). The values logged are task ids. Behavior is correct — the values are
only formatted into log messages — but readers (and diagrams) should label them as task ids.
The signal-handler path is fine: it uses _snapshot_active_tabs(), which genuinely
prints tabId(task=task_id) pairs (:1461–1497).
Summary
Tab ids in this codebase form a disciplined, single-purpose system: commands carry them down, events carry them up, and nothing in between thinks in tabs. The 507 server lines implement the two translation edges plus per-tab UI state (chat view, model pick, opened-task resume, steering queues, worktree claims); the 22 agent lines exist only where the UI structurally needs an agent's help (sub-agent panes, best-effort toasts and picker hints); and the core layer proves the abstraction holds by never mentioning tabs at all.
Generated from six line-verified audit notes (printer, task runner, server, commands,
web server, agents/merge-flow) cross-checked against the working tree at branch
bigrefactor. Line numbers reflect that tree; counts are from a case-insensitive
grep for tab_id/tabId.