diff --git a/apps/desktop/src/app/chat/pane-mirror.ts b/apps/desktop/src/app/chat/pane-mirror.ts index 8e8a9683670..b7818959369 100644 --- a/apps/desktop/src/app/chat/pane-mirror.ts +++ b/apps/desktop/src/app/chat/pane-mirror.ts @@ -31,6 +31,9 @@ export interface PaneMirror { before?: (tile: T) => null | string | undefined minWidth: string title: (key: string) => string + /** Lead-dot color for the tile's tab (e.g. a session's project color). Re-read + * on every `also` change, so pass the color source in `also` to keep it live. */ + accent?: (key: string) => string | undefined render: (key: string) => ReactNode /** Wrap the tile's TAB (domain context menu — session verbs). */ tabWrap?: (key: string, tab: ReactElement) => ReactNode @@ -49,7 +52,7 @@ export interface PaneMirror { /** Build a `watch*` fn: syncs once, then re-syncs on every source/also change. * Module-level state lives in the returned closure, so call it once per app. */ export function paneMirror(cfg: PaneMirror): () => void { - const registered = new Map void; title: string }>() + const registered = new Map void; title: string; accent?: string }>() const paneId = (key: string) => `${cfg.prefix}:${key}` const sync = () => { @@ -59,10 +62,11 @@ export function paneMirror(cfg: PaneMirror): () => void { for (const tile of tiles) { const key = cfg.key(tile) const title = cfg.title(key) + const accent = cfg.accent?.(key) const current = registered.get(key) - // register() replaces same-id in place — safe for live title refreshes. - if (current && current.title === title) { + // register() replaces same-id in place — safe for live title/accent refreshes. + if (current && current.title === title && current.accent === accent) { continue } @@ -71,6 +75,7 @@ export function paneMirror(cfg: PaneMirror): () => void { area: 'panes', title, data: { + accent, dock: { before: cfg.before?.(tile), pane: cfg.anchor?.(tile) ?? 'workspace', @@ -87,7 +92,7 @@ export function paneMirror(cfg: PaneMirror): () => void { render: () => cfg.render(key) }) - registered.set(key, { dispose, title }) + registered.set(key, { dispose, title, accent }) if (!current) { registerPaneCloser(paneId(key), () => cfg.close(key)) diff --git a/apps/desktop/src/app/chat/session-tile.tsx b/apps/desktop/src/app/chat/session-tile.tsx index 04ddaf1cc63..1a9ee67ac0c 100644 --- a/apps/desktop/src/app/chat/session-tile.tsx +++ b/apps/desktop/src/app/chat/session-tile.tsx @@ -40,6 +40,7 @@ import { sessionMatchesStoredId, sessionPinId } from '@/store/session' +import { $sessionColorById, sessionColorFor } from '@/store/session-color' import { $sessionStates, $sessionTiles, @@ -257,6 +258,12 @@ function tileTitle(storedSessionId: string): string { return stored ? sessionTitle(stored) : 'Session' } +/** The tab's lead-dot color — the tile's session resolved through the SAME + * shared map the sidebar reads, so a row and its tab always agree. */ +function tileAccent(storedSessionId: string): string | undefined { + return sessionColorFor($sessions.get().find(s => sessionMatchesStoredId(s, storedSessionId))) +} + /** The `@session` link payload for a tile tab drag — id + owning profile + title. */ function tileDragPayload(storedSessionId: string): SessionDragPayload { const stored = $sessions.get().find(s => sessionMatchesStoredId(s, storedSessionId)) @@ -407,7 +414,7 @@ export function WorkspaceTabMenu({ children }: { children: React.ReactElement }) * `$sessions`). Tiles dock against main on the chosen edge, flex width. */ export const watchSessionTiles = paneMirror({ source: $sessionTiles, - also: [$sessions], + also: [$sessions, $sessionColorById], key: t => t.storedSessionId, prefix: 'session-tile', dir: t => t.dir, @@ -415,6 +422,7 @@ export const watchSessionTiles = paneMirror({ before: t => t.before, minWidth: '20rem', title: tileTitle, + accent: tileAccent, render: storedSessionId => , tabWrap: (storedSessionId, tab) => ( { area: 'panes', title: stored ? storedSessionTitle(stored) : 'New session', data: { + // The tab's lead dot — same shared map the sidebar row reads, so the + // main tab and its sidebar row always show the same color. + accent: sessionColorFor(stored), // Pages aren't tab-able: the main zone's bar stands down while one shows. headerVeto: $workspaceIsPage.get(), placement: 'main', @@ -417,6 +421,7 @@ const syncWorkspaceTitle = () => { $selectedStoredSessionId.listen(syncWorkspaceTitle) $sessions.listen(syncWorkspaceTitle) +$sessionColorById.listen(syncWorkspaceTitle) $workspaceIsPage.listen(syncWorkspaceTitle) // Layout reset collapses every session tile into main as a tab (after the diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts b/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts index d5e5b9afe43..0fdc2aae28b 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts +++ b/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts @@ -58,6 +58,11 @@ interface PaneChrome { * (artifacts/skills/plugin pages) are not tab-able surfaces. The flag is * live: the workspace contribution re-registers it on route changes. */ headerVeto?: boolean + /** A lead-dot color for this pane's TAB (a session tab inheriting its + * project color). Generic — any pane may contribute one; the strip just + * renders a tinted dot before the label. Live: the owning contribution + * re-registers it when the resolved color changes. */ + accent?: string } export const paneChrome = (c: Contribution | undefined) => (c?.data ?? {}) as PaneChrome diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx index 8988e374a75..ff15a790bc8 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx @@ -449,6 +449,13 @@ export function TreeGroup({ role="tab" style={{ cursor: 'grab' }} > + {chrome.accent ? ( +