Skip to content

CCC

v4.9.0 Feature

This release adds 2 notable features for engineering teams evaluating rollout.

✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

agents claude claude-code web developer-tools headless
+4 more
kanban llm-tools local-first python

Summary

AI summary

Flow board gains live Codex session state badges and first‑class edge/flow node manipulation.

Changes in this release

Feature Low

Adds a quick‑close × button in the status rail that collapses and persists across reloads.

Adds a quick‑close × button in the status rail that collapses and persists across reloads.

Source: llm_adapter@2026-06-07

Confidence: high

Feature Low

Introduces a live rate knob for text‑to‑speech playback, tunable between 0.5× and 2.5×.

Introduces a live rate knob for text‑to‑speech playback, tunable between 0.5× and 2.5×.

Source: llm_adapter@2026-06-07

Confidence: high

Feature Low

Renders ```mermaid``` fenced code blocks as SVG diagrams via lazy‑loaded Mermaid v10.

Renders ```mermaid``` fenced code blocks as SVG diagrams via lazy‑loaded Mermaid v10.

Source: llm_adapter@2026-06-07

Confidence: high

Feature Low

Makes Flow board background have extra padded pan space for edge items.

Makes Flow board background have extra padded pan space for edge items.

Source: llm_adapter@2026-06-07

Confidence: high

Feature Low

Adds Cmd+` (and Cmd+Shift+`) shortcuts to cycle through CCC windows on macOS.

Adds Cmd+` (and Cmd+Shift+`) shortcuts to cycle through CCC windows on macOS.

Source: llm_adapter@2026-06-07

Confidence: high

Feature Low

Makes Flow “Organize” incremental, preserving existing placements and reporting total pixel displacement.

Makes Flow “Organize” incremental, preserving existing placements and reporting total pixel displacement.

Source: llm_adapter@2026-06-07

Confidence: high

Bugfix Medium

Strips unpaired UTF-16 surrogate code points before Anthropic API calls, preventing 400 errors.

Strips unpaired UTF-16 surrogate code points before Anthropic API calls, preventing 400 errors.

Source: llm_adapter@2026-06-07

Confidence: high

Bugfix Medium

Prevents stale "▶ Bash /bin/zsh -c …" in‑flight pill on idle Cursor sessions.

Prevents stale "▶ Bash /bin/zsh -c …" in‑flight pill on idle Cursor sessions.

Source: llm_adapter@2026-06-07

Confidence: high

Bugfix Medium

Ensures "Launch in Terminal" uses an existing cwd, avoiding fallback to home directory.

Ensures "Launch in Terminal" uses an existing cwd, avoiding fallback to home directory.

Source: llm_adapter@2026-06-07

Confidence: high

Bugfix Medium

Prevents user‑typed messages from disappearing after cleanIssuePrompt over‑stripping.

Prevents user‑typed messages from disappearing after cleanIssuePrompt over‑stripping.

Source: llm_adapter@2026-06-07

Confidence: high

Full changelog

Added

  • Codex sessions now show a live state badge (Working / Idle / Stuck / Offline) on the conversation row and in the conversation pane, derived from the rollout log — fixing pool-model codex sessions that previously showed no activity indicator.

  • Flow edges (the curved lines connecting child nodes to their parent object/repo) are now first-class objects you can manipulate:

  • Click an edge to select it. Selected edges thicken and turn orange so they stand out from the rest of the board.

  • Backspace / Delete with an edge selected removes the parent assignment; the child falls back to its default repo group (or no parent). Skipped automatically when focus is in a text field so the shortcut doesn't hijack typing.

  • Drag any edge to reconnect it. Pointer-down on the line starts a reparent drag — a dashed ghost line follows the cursor, candidate parent nodes light up orange, and dropping on one re-links the child to that new parent. Drop outside any node to cancel. Cycle-prevention: a node can't become its own ancestor.

  • Click background or hit Escape to clear the edge selection.

Edges now render as <g class="flow-edge"> carrying a wide invisible hit path on top of the thin visible line — clicking the visible 1.6px stroke is unreasonably hard, so the hit-target widens to 14px while staying invisible.

  • Group chats are now first-class nodes on the Flow workspace alongside repos and objects:

  • Render: every entry in the existing _gcActiveChats cache shows up as a cyan-accented flow-node-group-chat card with the chat's topic, participant count, status, and last-activity timestamp.

  • "+ Group chat" toolbar button sits next to "+ Object". Click it and the existing new-group-chat dialog (window-prompt for the name, /api/coordinate POST) runs; once pollGcActive refreshes, the new node appears on the board automatically.

  • Drag a session node onto a group-chat node to add the session as a participant — same outcome as dragging a conv-list row onto a chat row in the sidebar. The session card snaps back to its repo cluster (sessions stay under their repo for layout purposes; the chat just registers the participation via /api/group-chats/add-participant).

  • Click a group-chat node to open the chat reader through the existing openGroupChatReader entry point.

All three node kinds (repo / object / group-chat) participate in the Organize layout the same way — they anchor at their current position and the overlap resolver minimises movement.

  • Added a button to the Flow popout's toolbar (the small split-rectangle icon) that toggles a conversation reader pane on the right side of the popout window. With the reader on, clicking a node in the flow board mounts that conversation into the right pane through the normal selectConversation path — same conv reader, same input bar, same TTS / Esc / Send buttons. With the reader off (the default), the flow board fills the whole popout. The toggle is persisted across popout reloads in localStorage (ccc-flow-popout-reader). No new conv-rendering code — just un-hides the existing main pane and splits the viewport via CSS.
  • The Flow view now has its own pop-out button in the flow toolbar (next to the Expand toggle). Click it and the whole Flow board opens in its own window — a native CCC window when running inside the macOS app, a browser popup otherwise — reusing the same window.open + cccNative.openPopout + /api/open-browser fallback chain the conversation pop-out already uses. Boot-time detection of ?ccc_popout=flow adds a body.flow-popout class, forces ccc-session-view=flow in localStorage so the popped-out tab lands on the board immediately, sets the window title to "Flow", and CSS hides every other surface (main pane, topbar, attention panel, conv list, kanban) so the flow board fills the viewport. The button is hidden inside the popout itself (no point popping a popout).
  • Flow popout's conv reader (right pane) is now draggable: a 6px column between the flow board and the reader can be dragged left to widen the reader or right to narrow it. Width persists across reloads in ccc-flow-reader-width. Bounds: min 280px, max viewport width − 320px so the flow board stays usable. The CSS custom prop --flow-reader-width drives the .main flex-basis so the change is instant and animation-free.
  • Flow adds Record mode and Organize+ for replaying recorded manual layout preferences.
  • Flow repo/object nodes now open editable Markdown-backed work-item status pages in the conversation pane, with refreshable auto sections and deterministic per-work-item accent colors on the board.
  • New "Group chats" modal lists every group chat with a per-row pause / unpause button. Opens from the small ⚙ button next to "+ New Group chat" in the sidebar. Each row shows the topic, current status (active / paused / closed) in colour, the participant count, and time since last activity. Pausing or resuming routes through the existing setGroupChatPaused API (and benefits from the optimistic local update so the row's status flips immediately). Sort: newest activity first. Esc or backdrop click closes the modal.
  • Cmd+ cycles through CCC's open windows (main ↔ flow popout ↔ conversation popout), and Cmd+Shift+ cycles in reverse. Both surface as explicit "Cycle Through Windows" / "Cycle Through Windows (Reverse)" items in the Window menu, so the keystroke is bound at the menu-bar level — macOS' default Cmd+` works for AppKit apps with multiple windows, but WKWebView often swallows the keystroke before AppKit sees it, which is why pop-outs felt like dead ends. DMG users get this only via a Sparkle release (scripts/macapp/ change).
  • mermaid ``` fenced code blocks in assistant messages now render as actual SVG diagrams instead of showing raw flowchart TD … source. renderCodeBlock emits a .mermaid-block carrier whose .mermaid-source pre is the offline fallback; a lazy loader fetches mermaid@10 from cdn.jsdelivr.net on first appearance and converts every pending block into an SVG. Hooked into the existing conv-view MutationObserver (the same one that tags blocks for RTL), so every render path — assistant text, stream bubbles, group-chat messages, issue bodies — picks up the rendering for free. Loader is cached after first call; if the CDN is unreachable, the fallback source pre stays visible with a data-mermaid-error="load-failed" marker. Diagram theme follows the dashboard theme (dark by default, default when [data-theme=light]).
  • The status rail (right-side panel with Original ask / Activity / Files) now has a quick-close × button in its top-right corner. Click it and the rail collapses immediately and stays collapsed across reloads (writes ccc-status-rail-collapsed=1, same persistence as the existing topbar toggle). Previously the only way to close the rail was to drag the resizer to the edge or find the topbar toggle button — neither obvious. The × only shows when the rail is open in right-position mode, so it doesn't appear when the rail is already collapsed (the topbar restore button handles the un-collapse).
  • Text-to-speech playback now has a live rate knob next to the TTS button — a thin range slider that appears while playback is active or paused, defaulting to 1.25× and tunable between 0.5× and 2.5×. Dragging it cancels the in-flight utterance and re-speaks from the most recent word boundary so the new rate kicks in within ~180ms (debounced so per-pixel drags don't stutter), instead of having to wait for the next message to hear the change. The rate is persisted to localStorage (ccc-tts-rate) so it sticks across sessions — set it to 1.2× once and it stays there.

Changed

  • Conversation row's live-tool pill now shows just the tool label (e.g. "Reading file" — glowing when in-flight) instead of "Reading file ...s/claude-command-center/static/app.js". The path detail was ellipsizing into unreadable suffixes and pushing the rest of the row meta (size, branch, age, action buttons) off-screen on narrower sidebars. The full file/command still appears in the hover title, so users who want to confirm exactly what's being touched can read it there.
  • Shifted Cursor IDE integration from a planned full two-way chat sync to a metadata bookmark sync. Cursor's Desktop IDE compiles chat UI state into an undocumented Protobuf Merkle tree in store.db rather than simple JSON strings. Injecting full chat history natively carries a severe risk of corrupting user workspaces when Cursor pushes minor internal updates. CCC now safely injects only the session metadata into store.db so you can see your CLI sessions listed in the IDE sidebar, but the full interactive chat history remains safely decoupled in the CCC dashboard.
  • Flow board background now has extra padded pan space around every edge so top-left items can be dragged toward the center of the viewport.
  • The Flow toggle button (☷ icon in the sidebar header) now pops the Flow board into its own window instead of swapping the sidebar contents in-place. Reuses the existing openFlowPopout helper — native CCC window inside the macOS app, browser popup otherwise. When clicked from INSIDE the flow popout itself, falls back to the legacy in-sidebar swap (no point popping a popout). The "+ New session" / "+ New Group chat" panel and the conv list stay visible in the main window so the user can keep working without flipping sidebar modes.
  • Flow "Organize" is now incremental — it keeps repos and objects exactly where you put them and only moves them when it absolutely has to. Per user request: "move repos and objects as least as possible. The only case we're OK moving them is if we cannot form a rectangle that includes the sessions beneath them and the object."

Previously every run bin-packed every chain from the top-left, which scrambled a board the moment you ran it. Now each chain anchors at its root's current position; if two chain bounding boxes overlap, a greedy resolver picks the worst-overlapping pair, pushes the chain that has moved less so far by the minimum right/down amount, and repeats until clean. Re-running Organize on an already-tidy board is a no-op (zero pixels moved). The toast at the end reports the total pixel displacement so you can see how much it had to nudge.

Untouched chains (first-ever Organize, root still at 0,0) seed from the legacy bin-pack cursor so a fresh board still produces a tidy initial layout. The minimum-displacement rule is now R10 in the in-source algorithm doc block.

Fixed

  • "Active Group chat" pill no longer lingers after the user stops orchestration. Two fixes in gcShouldShowActivePill:
  1. Hard short-circuit on paused / closed / orchestrator-off. A chat with status === 'paused', paused === true, or orchestrator_timer_active === false returns false from the show-gate immediately — the pill claims "active right now"; the moment the user clicks Stop, the pill must respect that, not coast on the trigger-freshness window.

  2. Dropped last_mtime from the freshness calc. It's the chat file's stat mtime which the server bumps on metadata writes (name_map updates, polled sidecar writes), not real message arrivals. The label-side code already filtered it out for the same reason; the show/hide gate now matches.

Plus an optimistic local patch in setGroupChatPaused so the pill drops within one render tick of the Stop click instead of waiting for the next 15s gcActive poll to land.

  • Annotations and any text routed through _inject_text_into_session are now stripped of unpaired UTF-16 surrogate code points (U+D800..U+DFFF) before they can reach an Anthropic API call. Symptom: when a pasted annotation or selected DOM text carried a lone surrogate (the browser's clipboard / selection APIs can split a surrogate pair, especially when a selection ends mid-emoji), the downstream Claude session POSTed a request body containing that surrogate and Anthropic rejected it with API Error: 400 The request body is not valid JSON: no low surrogate in string: line 1 column N (char N) — same root as anthropics/claude-code#16294. Fix: new _strip_lone_surrogates helper at the server boundary, called from _annotation_text (covers /api/annotations and enqueue_annotation_ux_fixes_queue payloads) and from _inject_text_into_session (covers every other inject path as belt-and-suspenders). Paired surrogates — real astral-plane characters like 😀 (U+1F600), which Python stores as a single code point — pass through unchanged; only LONE surrogates are dropped.
  • Fixed Codex slash commands so CCC offers the Codex command catalog and routes /... commands through a live Codex terminal instead of sending them as headless prompts.
  • Context percentage compact now uses a dedicated compact API instead of injecting /compact as ordinary text. Live Claude terminals receive the slash command directly; busy terminals queue it; dormant Claude sessions open an interactive claude --resume terminal and run /compact there, avoiding the broken headless-resume fallback.
  • Cursor backfill now correctly sets the lastUpdatedAt field to match the most recent transcript activity, ensuring fresh sessions appear at the top of the Cursor IDE history.
  • Cursor sessions that have gone idle no longer show a stale "▶ Bash /bin/zsh -c …" in-flight pill on their sidebar row. Root: cursor JSONLs don't carry per-event timestamps, so pending_tool_ts falls back to the JSONL file's mtime, which keeps refreshing as the file appends metadata-only lines. The codex stale-tool check compares now - pending_tool_ts against a 15-minute threshold and never tripped because the ts kept looking fresh. A finished cursor turn whose last event was a tool_use would therefore display the in-flight pill indefinitely.

Fix: _cursor_activity_fields_from_tail now checks file idleness directly — if the JSONL hasn't been written to in the configured window (default 60s, env CCC_CURSOR_IDLE_SEC), it returns empty activity fields regardless of the dangling pending_tool. The pill drops as soon as the session stops emitting events.

  • Flow button opens or focuses the Flow popout without embedding Flow in the main sidebar.
  • When creating a new flow object via the "+ Object" toolbar button, the new node now lands where the input modal was instead of stacking into a fixed top-left grid. The modal's center is captured before awaiting the user's OK (capturing it after would read a zeroed rect since the modal cleans up to display:none first), then translated into flow-canvas coordinates accounting for the current flowZoom and the canvas's bounding rect. The node is centered on that point. Window-prompt fallback still uses the old grid layout when the modal element isn't available.
  • Flow "Organize" no longer lets one repo cluster overlay a child object that sits nested under the previous repo. Root: the placement loop advanced the row cursor by the top-level cluster's own width only — nested clusters were placed to the right of their ancestor but didn't extend the row's right edge, so the next top-level cluster slid right over them. Refactor: group clusters into chains (top-level root + every cluster transitively nested under it), simulate each chain at origin to learn its combined bounding box, then bin-pack chains as single units. The row-budget wrap check now sees the full chain width, so wide chains wrap to a new row instead of bleeding under the next one.
  • Two flow-board fixes:
  1. Background pan no longer hitches every 90s. The archiveTimes poller fired its refreshArchiveData fetch unconditionally; even though the resulting renderArchiveList correctly deferred itself when a sidebar drag was in progress, the queued render kicked in right after the drag ended and could clip the pan. Wrap the poller body with deferSidebarRenderIfDragging() so the whole tick skips while panning — the flush-after-drag hook still replays the deferred render the moment the user releases.

  2. Nested objects / repos now stack BELOW their parent, not to the right when first added. The unplaced-nested seed used to default to ancestor.right + NESTED_GAP_X which placed a child repo next to its parent object; the user wanted the layout to match the "Small Projects → video-claw / usage_on_mac" stack-below shape. Seed is now (ancestor.x, ancestor.y + ancestor.h + CLUSTER_MARGIN). Multiple unplaced siblings start at the same slot and the overlap resolver stacks them further down.

  • Releasing a flow-board pan no longer snaps the view back to where it started before the drag. Root: renderFlowSidebar rewrites $flow.innerHTML, which wipes the element's scrollLeft/scrollTop. While the user was actively panning, the in-progress drag suppressed renders correctly; the moment they released, the deferred-after-drag flush ran renderFlowSidebar and the innerHTML write reset the scroll. Fix: capture the previous scroll position before the rewrite and restore it immediately after applyFlowZoom settles the canvas size. The pan now sticks where the user released.
  • Flow board pan (click-and-drag the background) no longer jumps every few seconds while you hold the drag. Root: isSidebarDragInProgress self-heals by checking the DOM for a .dragging-class element — if the boolean flag is true but no node carries that class, it clears the flag (defense against stuck-true after a cancelled drag). The flow pan calls beginSidebarDrag but doesn't add .dragging to any node (it's dragging the BACKGROUND, not a node), so the self-heal cleared the flag within one render tick. Once cleared, the next periodic liveStatus or liveSessionsActivity tick passed through _scheduleSidebarRenderrenderSidebar unimpeded, swapped the flow board DOM mid-pan, and the user saw nodes jump. Fix: the pan handler now sets .is-panning on the flow board element while the drag is active, and the self-heal selector includes .flow-board.is-panning — so the flag stays honestly true for the full pan duration and pollers' renders correctly defer until release.
  • The "+ New session / + New Group chat" panel no longer shows up in the Flow pop-out window. It's an entry point into the main dashboard's session-creation flow and has no business in a dedicated flow board view. Added .new-session-panel to the body.flow-popout hide list alongside the conv list, search bar, topbar, etc.
  • Flow wheel/trackpad zoom now defers sidebar refresh renders while the zoom gesture is active, matching the existing pan guard so periodic refreshes cannot interrupt the gesture or snap the board mid-zoom.
  • "Launch in Terminal" no longer hallucinates a deleted-worktree cwd and drops the user in their home dir. Two-layer fix:
  1. Server (find_codex_conversations)effective_cwd used to be tail_worktree_path or cwd, surfacing whatever path the JSONL tail extracted from an old cd <…> Bash command. If that worktree was since deleted, the row carried a non-existent path. Now picks the first cwd candidate that still exists on disk via the new _first_existing_dir helper (tail → cwd → pinned), falling back to the literal worktree path only when nothing exists.

  2. Client (buildResumeCommand) — for missing cwds that don't match the .claude/worktrees/<branch> recreation pattern (e.g. ad-hoc BYM-Finie-push-reschedule-sGH1nB), cd '/...' && resume would fail (no such dir) and the && would block the resume. Now falls back to currentSession.repoPath when known; drops the cd entirely (runs resume from the user's terminal pwd) when no repo path is available.

  • Mobile single-column layout (conv list full-width → tap a row → conv pane slides in → back button returns to list) now triggers on every phone, including iPhone landscape. Root: the breakpoint was 768px (JS _mobileMQ) / 720px (back button CSS) / 768px (main-overlay CSS). iPhone Pro Max landscape is 932px and even baseline iPhone landscape is 844px — both exceeded all three thresholds, so isMobile() returned false and mobileShowForCurrentMode no-op'd. The user saw the desktop dual-pane layout cramped onto a phone with no back button. Aligned all three to 950px (covers up to iPhone Pro Max landscape with a small safety margin). The wiring (selectConversation → mobileShowForCurrentMode → mobile-show-main → translateX(0), back button → mobileShowMain(false)) was already in place; this just opens it to the right viewports.
  • Mobile conversations keep a visible back-to-list button inside the conversation pane.
  • Mobile conversations keep one header while preserving the back-to-list button.
  • On mobile, the page now lands on the conv list instead of the auto-restored conv pane. Root: restoreLastConversation runs at boot and calls selectConversation for whichever conv was open last, which in turn triggers mobileShowForCurrentMode and slides the conv pane over the sidebar. The user therefore landed on a conv overlay every page load and never saw the list — opposite of the standard phone pattern. Fix: when the restore loop completes on a mobile viewport, call mobileShowMain(false) to slide the conv pane back off-screen. The conv stays loaded so tapping a row brings it back instantly with no fetch latency.
  • Three mobile toolbar fixes in one ship:
  1. Reverted the temporary blue Annotate button — it had served its purpose as a cache-bust probe and the user confirmed new CSS reaches the browser.

  2. Topbar now fits one row on phones instead of wrapping into a second row that pushed the back button off-screen. Hidden at max-width: 950px: Update pill, Report-a-bug, Annotate / Screen / Notes, Worktrees, Stats, Terminal, Vercel / localhost deploy pills, and the status-rail position toggle. None of these are phone-friendly anyway. The breadcrumb now flexes to fill remaining space; its category chip caps at 96px so the conv title gets visible room.

  3. Right status rail (Original ask / Activity / Files) defaults to collapsed on mobile. The mobile viewport doesn't have the spare width to host the rail, and surfacing it pinches the conv reader into a narrow column. Boot-time check in index.html adds status-rail-collapsed when the viewport is ≤950px UNLESS the user has explicitly opened it (localStorage = '0'); the desktop default behavior is unchanged.

  • Organize now respects hand-placed nested objects. Previous R10 implementation only anchored the chain ROOT at its current position — every nested cluster was placed at a chain-derived offset (ancestor.right + NESTED_GAP_X, ancestor.top), so a nested object you'd dragged anywhere different would snap back to "right of ancestor" on every Organize run. New behavior: every cluster (root AND nested) anchors at its own parent's current offsetLeft/offsetTop. The overlap resolver runs over all clusters as independent units instead of as chain bounding boxes, so a nested object can stay where you put it while its repo and sibling repos stay where THEY were too. Unplaced clusters still fall back to sensible seeds — bin-pack cursor for top-level, "right of ancestor" for nested — so first-ever Organize still tidies a fresh board.
  • Inline session rename no longer gets stuck in edit mode when the sidebar search box is open (or focused). Root: the rename input is itself a text input, and after Enter/blur focus is either still on it or has moved to the search box (also text) — shouldPauseSidebarRender returns true for either, so the post-commit renderSidebar call early-returned and the rename input was never swapped back for the rendered title. Same class of bug as the "Sending… pill" fix shipped earlier. Fix: the rename commit's renderSidebar call now passes { force: true } to bypass the periodic-pause guard for user-initiated paints. The save still happens (the API call ran), it just wasn't visible.
  • Sidebar search now hides active and archived group-chat rows so search results stay focused on matching sessions/issues instead of showing group-chat navigation rows in In progress or Archived.
  • Command tool results now attach to the matching command and show a visible result/error label.
  • User-typed messages no longer disappear from the conv view when cleanIssuePrompt over-strips them. Root: the conv reader runs the JSONL user_text through cleanIssuePrompt (which removes spawn-prompt boilerplate, session-state instructions, slash-command markup, etc.) before rendering. If the cleanup eats the entire body — possible when a regex matches too broadly or the user's prose happens to look like template plumbing — the user_text div rendered with just a "User" label and no content. Combined with the pending-echo dedupe (which removes the optimistic stub the moment a matching JSONL event arrives), the user saw their sent message silently disappear. Safety net: when cleanIssuePrompt returns empty but the raw ev.text had content, fall back to the raw text. The user's typed words never disappear from their own conv view.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track CCC

Get notified when new releases ship.

Sign up free

Related context

Earlier breaking changes

  • v5.0.1 Removes horizontal-drag gesture that collapsed conversation pane.

Beta — feedback welcome: [email protected]