| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| --> |
| |
| # Side Conversation |
| |
| ## Current Product Behavior |
| |
| Maka `v0.1.6` already ships a quote companion that forks the active Session at |
| its latest completed turn, inherits the model, workspace context, and permission |
| profile, hides it from the conversation list, and removes it when the panel |
| closes. |
| |
| The generic side-conversation entry extends that foundation: |
| |
| - the titlebar can open a side conversation without requiring a text selection; |
| - the command palette exposes Side Chat as a first-class action with the same |
| keyboard shortcut shown by the workbar launcher; |
| - `/side` opens an empty side chat, while `/side <prompt>` creates a titled |
| side chat and sends the prompt as its first turn; |
| - the Composer `/` suggestion layer exposes Side with its icon and description; |
| selecting it executes the command and removes the query instead of creating a |
| fake Skill token; |
| - an empty side chat adopts its first accepted prompt as the tab title and |
| keeps that title stable through later follow-ups and panel moves; |
| - selecting transcript text still appends quote context to the same panel; |
| - the Composer is usable the moment the panel opens; the fork is created lazily |
| on the first send — branching from the latest settled turn when one exists, or |
| with an empty context (inheriting the source's model, cwd, and permission but |
| no transcript) when the source has not completed a turn yet, so opening the |
| panel never depends on the main Session's turn state; |
| - the child receives the `mode:side_conversation` label, which adds a system |
| boundary declaring inherited parent history reference-only; |
| - the main Session and its active turn continue independently; |
| - only instructions submitted in the side chat are active; explicit side-chat |
| actions may use the inherited permission profile, and the permission can be |
| changed from the side Composer; |
| - while a side turn is running, Enter queues a follow-up for the next turn and |
| Shift+Enter steers the active turn at its next model step; both appear |
| immediately, while Stop remains available; |
| - the side Composer shares `/` Skill discovery, `@` file references, files, |
| quotes, and draft ownership with the main Composer; |
| - settled side answers expose Copy, Info, and Regenerate without navigating the |
| user out of the temporary side tab; |
| - the side Session is deleted when the panel closes; |
| - every created temporary fork is registered with the main-process cleanup |
| authority, so a process-level interruption removes the orphan on restart. |
| |
| ## Codex Reference |
| |
| Codex implements `/side` as an ephemeral thread fork. |
| |
| The TUI uses `thread/fork` with `excludeTurns`, injects a hidden boundary item, |
| keeps separate parent/side UI state, and interrupts plus unsubscribes the side |
| thread during cleanup. The Desktop app marks the fork as a `sideConversation`, |
| opens it in a `sidechat:<thread-id>` right-panel tab, excludes it from recent |
| conversation surfaces, and confirms before permanently closing a non-empty |
| temporary chat. |
| |
| Primary references: |
| |
| - `openai/codex#18190` introduced `/side`. |
| - `openai/codex#34198` stopped replaying inherited turns in the side UI. |
| - `openai/codex#35011` kept a side conversation open while toggling to its |
| parent. |
| - `openai/codex#35887` moved interrupt/unsubscribe cleanup to the background. |
| |
| ## Desktop Architecture Snapshot |
| |
| The current local Codex Desktop snapshot inspected on 2026-08-06 is app build |
| `26.730.61639` (bundle build `6234`, packaged on 2026-08-05). Its side chat is a |
| tab type inside a generic application-shell panel system, not a dedicated |
| drawer. |
| |
| The shell creates independent `right` and `bottom` tab controllers. Each |
| controller owns: |
| |
| - ordered tab ids, active tab, per-tab state, and activation history; |
| - open, activate, close, close-others, close-to-right, reorder, and pin; |
| - moving a tab between controllers through `moveTabTo` / `receiveMovedTab`; |
| - preview tabs that are replaced by the next preview unless pinned; |
| - tab descriptors carrying component, props, icon, title, close hooks, move |
| hooks, context-menu items, and an optional durable route. |
| |
| Closing an active tab does not merely choose the next array item. The controller |
| tracks opener relationships and recent activation so a child tab can return to |
| its opener before falling back to an adjacent tab. |
| |
| The persisted layout is a topology snapshot containing right and bottom tab |
| order, active ids, open state, focus area, full-width state, and restorable |
| routes. Preview tabs are excluded. Browser tabs additionally persist browser |
| storage ids, restore URLs, device-toolbar state, and target panel. |
| |
| The New Tab page is therefore a registry surface over available tab types |
| (Review, Terminal, Browser, Files, Side chat, and feature-gated additions), not |
| a fixed menu wired to the side-chat feature. |
| |
| ## Desktop Side-Chat Lifecycle |
| |
| Codex installs a non-closable `sidechat-loading:<parent>:<index>` tab, forks the |
| latest parent state, then swaps in `sidechat:<conversation-id>`. |
| |
| Maka diverges from that eager loading contract: opening a side chat installs a |
| normal `sidechat:<conversation-id>` tab whose Composer is usable immediately — |
| there is no loading tab or preparation spinner, and the tab is closeable, |
| draggable, and reorderable from the start. The fork is created lazily on the |
| first send: it branches from the latest settled parent turn when one exists, or |
| forks with an empty context (inheriting the parent's model, cwd, and permission |
| but no transcript, and no in-progress Todo) when the parent has not completed a |
| turn yet. Opening the panel never depends on the parent's turn state, and no |
| mid-flight turn is ever copied. |
| |
| The fork is marked both ephemeral and side-conversation, excluded from recent |
| conversation surfaces, and receives a developer boundary that: |
| |
| - treats inherited history and tools as reference-only; |
| - activates only instructions submitted after the side-chat boundary; |
| - prohibits continuing parent work or using sub-agents; |
| - allows non-mutating inspection by default; |
| - permits a mutation only when the user explicitly requests it in the side |
| chat. |
| |
| A side-chat tab can move between right and bottom panels. Closing a non-empty |
| temporary side chat runs an `onBeforeClose` confirmation (with a persisted |
| "don't ask again" preference), then discards the conversation from cache in its |
| close hook. Existing retained side chats can instead resume and opt into |
| `preserveOnClose`. |
| |
| The panel visibility controller deliberately distinguishes hiding, closing, and |
| moving: |
| |
| - activating or opening a tab reveals its target panel; |
| - opening a side chat reveals its panel immediately, before any fork exists; |
| - manually hiding a panel preserves its tabs, active tab, draft, and stream; |
| - selected-text follow-up searches the right panel first, then the bottom |
| panel, preferring each panel's active side chat before its first side chat; |
| - closing one tab keeps the panel open when another tab remains; |
| - closing the final tab closes the panel; |
| - moving the final tab out does not count as closing it, so the source panel may |
| remain open on its New Tab page while the target panel opens. |
| |
| The inspected right and bottom visibility atoms default to closed and are not |
| backed by a localStorage preference. Tab and route restoration are separate |
| mechanisms from whether the panel is currently visible. |
| |
| The current product still has known recovery and focus gaps. Open upstream |
| reports cover unrecoverable closed chats, expiration after inactivity, focus |
| switching between the main and side composers, and crashes involving multiple |
| side chats plus Browser tabs. Maka should not copy those failure modes merely |
| for visual parity. |
| |
| The inspected Desktop bundle registers a Composer slash command with id |
| `side`. Its parser accepts only `/side` or `/side <prompt>`. A non-empty prompt |
| is sanitized into the tab title and sent as the first side turn. The command is |
| unavailable inside a side chat and before the parent conversation exists. The |
| current Desktop bundle does not register `/btw`; that alias belongs to other |
| Codex surfaces. |
| |
| ## Codex Terminal Architecture |
| |
| The same Codex build uses a real xterm surface rather than a command field over |
| terminal result cards. The renderer loads xterm, `FitAddon`, `WebLinksAddon`, |
| and `ClipboardAddon`. Every terminal owns a stable session id and is represented |
| by a dynamic `terminal:<session-id>` tab with a durable route carrying cwd, |
| host id, and session id. |
| |
| The renderer does not own the process. A singleton terminal controller owns: |
| |
| - conversation-to-session and session-to-conversation mappings; |
| - active session selection and numbered tab titles; |
| - a bounded raw ANSI replay buffer plus live stream listeners; |
| - pending writes until both the host session and renderer listener are ready; |
| - terminal size, workspace binding, title, and user-interaction state; |
| - DEC alternate-screen mode across xterm unmount/remount. |
| |
| The main process owns local `node-pty` and remote process-session backends. It |
| enforces window ownership, creates or attaches sessions, streams raw data, |
| resizes, writes, closes, and retains a bounded replay buffer. Moving a tab |
| between right and bottom panels remounts or relocates the xterm view while the |
| PTY remains under the same main-process session. Closing the tab closes the |
| session. Opening another Terminal creates another PTY and another dynamic tab. |
| |
| Dynamic verification matched the static chain: opening right and bottom |
| Terminals in the inspected build created two direct child login shells on |
| different PTYs. Both surfaces remained live simultaneously. |
| |
| ## Codex Review Architecture |
| |
| Codex uses one fixed `diff` tab whose source changes inside the panel. It is not |
| an aggregation of tool-result messages. The Review model issues Git queries |
| against repository snapshots and supports: |
| |
| - branch, last-turn, uncommitted, staged, unstaged, and commit sources; |
| - target branch selection and multi-repository selection; |
| - batched per-file `review-diff` requests with snapshot generations and stale |
| snapshot rejection; |
| - refresh, jump-to-file, unified/split view, wrapping, whitespace filtering, |
| full-file loading, and rich previews; |
| - stage, unstage, and revert actions at section, file, and hunk scope. |
| |
| Last Turn can aggregate changes across all repositories. Git-backed sources can |
| be disabled independently, while Last Turn remains available from conversation |
| diff state. Maka's existing `ToolResultPreview` remains a good renderer to |
| reuse, but persisted `file_diff` tool results are not an equivalent data |
| authority. |
| |
| ## Deliberate Differences |
| |
| | Boundary | Codex | Maka | |
| | --- | --- | --- | |
| | Entry | `/side`, keyboard shortcut, Desktop actions | `/side`, titlebar, command palette, keyboard shortcut, and selected-text actions | |
| | Initial transcript | parent history hidden | parent history hidden; only side turns render | |
| | Parent history | reference-only developer instruction plus hidden boundary | reference-only system prompt from the side label | |
| | Tool policy | read-mostly guidance; explicit side requests may mutate under the active permission profile | inherited permission profile; only explicit side-chat requests are active | |
| | Lifetime | temporary, with Desktop confirmation and some retained-tab behavior | temporary; close deletes the fork with durable cleanup recovery | |
| | Conversation list | suppressed | suppressed | |
| | Recovery | expired/closed side chats are not reliably recoverable | intentionally not recoverable; cleanup is reliable | |
| |
| ## Maka Parity Status |
| |
| Maka now has the first usable slice of the same architecture: |
| |
| - a dynamic right-side tab state instead of a fixed tab union; |
| - a registry-backed New Tab page rather than a fixed tab switcher; |
| - a Side conversation-first New Tab page, followed by Review, Terminal, |
| Browser, and Files, then Maka-specific Tasks and Trace; |
| - Review now has Branch, Unstaged, Staged, and Last Turn sources inside one |
| fixed tab; |
| - Branch exposes an explicit base-branch selector; |
| - Branch, Unstaged, and Staged use a main-process Git snapshot authority derived |
| from the Session cwd; the renderer cannot provide a cwd or arbitrary Git |
| command; |
| - Branch resolves a different local/default branch when available and includes |
| committed branch changes, staged changes, unstaged changes, and untracked |
| files; |
| - snapshots are bounded by file and diff-byte limits and carry a revision hash; |
| Stage and Unstage re-read that revision and admit only a path present in the |
| authoritative snapshot before mutating the index; |
| - Unstaged files can be reverted after a destructive confirmation; |
| - Last Turn reuses the most recent turn that produced persisted `file_diff` |
| results, through the same highlighted diff renderer; |
| - Terminal is now a dynamic `terminal:<ref>` multi-tab surface backed by xterm |
| and `FitAddon`; |
| - the existing ShellRun process manager remains the sole process, resize, stop, |
| persistence, and Runtime Resource authority; |
| - ShellRun additionally publishes sequenced raw PTY deltas and a bounded replay |
| snapshot to Desktop, while model/tool surfaces keep using the sanitized |
| durable screen snapshot; |
| - moving a Terminal between right and bottom panels preserves the PTY and xterm |
| output; closing its tab stops the PTY; |
| - Terminal tabs are intentionally excluded from static layout persistence |
| because ShellRun processes do not survive an application restart; |
| - integrated login shells suppress oh-my-zsh's automatic update prompt so it |
| cannot consume the first typed command; |
| - titlebar launcher and panel-toggle actions; |
| - command-palette Side Chat action with `side`, `btw`, `侧聊`, and `追问` |
| search terms; |
| - Composer `/side` and `/side <prompt>` handling with automatic first-turn |
| send and a prompt-derived transient tab title; |
| - Composer slash-menu discovery for Side in the same trigger layer as Skills, |
| without changing Skill token serialization or draft ownership; |
| - first-prompt title adoption for side chats opened from the titlebar, |
| launcher, command palette, or selected-text flow; |
| - an immediately usable Composer on open, with the fork created lazily on the |
| first send (no loading tab or preparation spinner); |
| - stable per-kind tab icons, with Side Chat switching from its normal icon to |
| a spinner while a turn is active; |
| - one replaceable preview tab per panel for automatically opened artifacts; |
| opening it manually, double-clicking its tab, using its context-menu Pin |
| action, or interacting with its content converts it to a normal tab; |
| - preview tabs are excluded from layout persistence until pinned; |
| - closeable tabs with adjacent fallback and duplicate-open activation; |
| - most-recent activation history, so closing a tab returns to the tab the user |
| came from before falling back to adjacency; |
| - pointer drag reordering plus context-menu move-left / move-right actions; |
| - standard desktop tab keyboard navigation with ArrowLeft, ArrowRight, Home, |
| and End; |
| - New Tab focus handoff, ArrowUp/ArrowDown/Home/End menu navigation, and Escape |
| return to the previously active tab; |
| - independent right and bottom panel controllers with simultaneous active tabs; |
| - cross-panel tab movement that preserves Side Chat forks/drafts/streams and |
| Browser page state; |
| - persisted right/bottom static-tab topology, panel visibility, and bottom |
| panel height, including v2 right-panel migration; |
| - context-menu close, close-other, and close-to-right actions with batched |
| side-chat confirmation and cleanup; |
| - persisted static tab order and active tab, with side chats excluded; |
| - Browser, Files, and Side conversation shortcuts; |
| - side-chat draft and streaming state preserved while the workbar is |
| collapsed or the New Tab page is active; |
| - multiple numbered side-chat tabs with independent drafts, forks, streams, and |
| quote queues; |
| - the same Composer shell as the main conversation, including a functional |
| attachment menu, Skill and file mentions, inherited permission menu, |
| Enter-to-queue / Shift+Enter-to-Steer routing, and Host-backed queue controls; |
| - the same answer metadata surface for Copy, Info, and Regenerate, with Branch |
| withheld because it would navigate outside the temporary side-tab lifecycle; |
| - no content-area close action: Side Chat lifetime belongs exclusively to tab |
| chrome and its close confirmation; |
| - non-empty close confirmation with a persisted "don't ask again" preference; |
| - fork cleanup when the side-chat tab closes or its owning session changes; |
| - startup cleanup for temporary forks orphaned by an interrupted app process. |
| |
| Maka now also mirrors Codex's panel transition rules: hiding preserves live |
| tabs, closing the final tab closes that panel, moving the final tab leaves the |
| source panel on New Tab, and selected text reuses right-side chats before |
| bottom-side chats. Maka intentionally continues persisting general workbar |
| visibility across restarts; Codex's inspected visibility atoms are in-memory. |
| |
| ### Inserted-message rendering |
| |
| The first Side Chat steering implementation delivered inserted text correctly |
| but projected it incorrectly. A `steering_message` shares the active turn id |
| with the original prompt. The settled turn projection stored only one |
| `turn.user`, so the later steering row replaced the prompt; the live projection |
| also ignored `steering_message`, leaving the inserted text invisible until |
| settlement. |
| |
| Turns now preserve their first user row as the prompt and project later |
| same-turn user rows as `userInterjections`. The live projection adds a steering |
| interjection as soon as Runtime acknowledges it, and the settled overlay |
| deduplicates that optimistic row by message id. The Side Chat regression test |
| requires both user bubbles to appear immediately after insertion and to remain |
| after the assistant response settles. Because the fix lives in the shared turn |
| projection, the main conversation gets the same behavior. |
| |
| ## Validation Note |
| |
| The renderer production build remains split at the workbar boundary. After the |
| panel-lifecycle changes, direct `gzip -9` measurement reported 22,237 bytes for |
| the `session-workbar` chunk and 75,703 bytes for the main `index` chunk, both |
| slightly below the previous 22,260-byte and 75,801-byte snapshots. |
| |
| It does not yet have general durable route restoration or retained side-chat |
| recovery after leaving the owning session. Terminal launch remains local and |
| deliberately narrow: the renderer can request a login shell for the current |
| Session, but cannot yet choose a remote host, arbitrary launch command, or cwd. |
| Review still lacks commit-source browsing, hunk-level actions, filesystem |
| watching, and multi-repository aggregation. |
| |
| The visual shell is low-to-medium difficulty. Full controller parity is a |
| separate medium-to-high effort because it changes ownership, persistence, |
| focus, and cleanup behavior across every workbar surface. |
| |
| ## UI and Performance Verification |
| |
| The panel shell was checked in light and dark themes at 320px, approximately |
| 480px, 600px, and in the bottom placement. Two layout bugs found during that |
| pass were fixed at their ownership boundaries: |
| |
| - Astryx Toolbar's start slot kept its intrinsic tab width, so narrowing the |
| panel pushed the New Tab button outside the panel. The workbar now constrains |
| that slot and scrolls only the tab list, never the outer panel. |
| - The shared Composer footer wrapped its model onto a second row at 320px. The |
| right-side Side Chat now uses a compact single-row footer with an ellipsized |
| inherited model and a responsive 160px maximum; the wider bottom panel keeps |
| the normal Composer geometry. |
| |
| The side Composer receives focus as soon as the panel opens (no preparation |
| status, since there is no eager fork). The empty transcript shows the same blank |
| state as an ordinary chat. An interrupted development process was also |
| restarted against the same workspace to verify that its orphaned fork was |
| removed before the session list rendered. |
| |
| The latest visual pass covered the dark command palette, the dark right-panel |
| and light bottom-panel states. A newly opened side chat shows a normal, |
| closeable tab immediately — there is no separate loading tab or busy footprint. |
| |
| The active-turn pass confirms that Side Chat changes only its tab icon to a |
| spinner while processing. The close affordance remains available during a |
| turn, then the normal Side Chat icon returns without changing the tab title or |
| layout footprint. |
| |
| The `/side <prompt>` visual pass additionally confirms that the command never |
| appears in the parent transcript, the prompt is sent in the side transcript, |
| and the tab uses the prompt as its ellipsized title with the full title |
| available on hover. |
| |
| One real-model failure found during the final dev-app check had two independent |
| causes: |
| |
| - the active tool-result archive placeholder used a URI with `sha256` and |
| `bytes` query parameters; Nemotron copied the artifact id and hash into its |
| `ArchiveRead` call but dropped the second query parameter, so the strict |
| parser returned `invalid_ref`; |
| - the side fork selected the latest non-running parent turn, which included |
| failed and aborted turns. A short side prompt could therefore look like a |
| continuation of unfinished parent work to a weaker model. |
| |
| Archive refs now encode artifact id, hash, and byte length as path segments, |
| while the parser still accepts the old query form for persisted compatibility. |
| Side forks now inherit only through the latest successfully completed parent |
| turn and start empty when no completed turn exists. A live Nemotron validation |
| read one 10,982-character archived result in three successful pages, and a new |
| side chat opened over an aborted parent turn answered `你好` directly without |
| calling Glob, Read, or ArchiveRead. |
| |
| The slash-suggestion pass confirms that `/si` renders a single Side command |
| above the Composer with the normal Side Chat icon and explanatory secondary |
| text. Keyboard selection clears the query and opens an empty side chat. |
| |
| Measured performance checks on the implemented surfaces: |
| |
| - Git Review with 84 changed files improved from 335ms to 90ms median for |
| Branch and from 229ms to 69ms for Unstaged. |
| - A 220-file untracked snapshot stayed bounded to 200 files, completed in |
| 36.4ms, and added about 5.3MB of heap. |
| - Review mounts file diffs in pages of 20. |
| - A 5MB PTY stream dropped from 5,120 renderer IPC events to 80 while preserving |
| every byte. The focused Runtime coalescing test also keeps a 1MB stream within |
| 32 events. |
| - The loading-tab, command-palette, `/side`, slash suggestion, dynamic-title, |
| and tab-icon polish added about 0.70KB gzip to the main renderer chunk and |
| about 0.28KB gzip to the lazy workbar chunk |
| in the measured production build. |
| - Nested lazy boundaries then reduced the `session-workbar` chunk from 120,835 |
| bytes gzip to 21,832 bytes gzip, approximately 82%. Terminal is now an |
| 86,747-byte on-demand chunk, Files is 7,756 bytes, and Review, Browser, and |
| Trace are independent 2.4-4.3KB chunks. The main `index` chunk moved from |
| 75,717 to 75,941 bytes gzip, an increase of 224 bytes. |
| |
| The 19-test workbar interaction suite covers Side Chat, slash and command |
| palette entry, Review, Terminal, tab reordering, panel movement, persistence, |
| and the 320px layout. The shared UI suite passes 432 tests, and the 13-test |
| Skill/mention E2E set confirms that adding Side to the `/` menu did not change |
| Skill token or draft behavior. Full Runtime and Desktop suites can expose |
| timing-sensitive child-process tests under sustained machine load; every such |
| failure observed in this pass succeeded when rerun in isolation. |
| |
| ## Follow-up Work |
| |
| Follow-up work continues to preserve the lifecycle and authority boundaries documented above. Its status and decomposition live in the tracker so this design does not become a second checklist. |
| |
| Tracking: [Side Conversation follow-ups #4331](https://github.com/apache/maka/issues/4331) |