From c575415bcb107603caad78516406221894e86a71 Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 13 May 2026 16:01:16 +0800 Subject: [PATCH 1/8] feat(workspaces/ui): readable tab titles + sidebar truncate + SDK icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small frontend polish items on the Workspaces surface: **1. Tab titles use tag + session name instead of UUID slices.** Before: tab strip showed `40216df4 · bd4ef4` — opaque hex prefixes from the workspace UUID and session UUID. Useless. Now: `chat-with-alice · c1` — same shape every other workspace tool uses. Threads `workspaces` into `TitleCtx` (was channels-only) so `workspaceModule.title` can look up the tag/sessionName by ID. Falls back to UUID prefix when the workspace hasn't loaded yet (first paint). **2. Sidebar workspace row tag truncates instead of wrapping.** Before: long workspace tags like `chat-with-alice` wrapped to 3 lines inside the narrow sidebar column (`chat-/with-/alice`). Wasted vertical space + ugly. Now: ellipsis truncation + hover tooltip with the full tag. `min-width: 0` on `.sidebar-row-main` lets the `.sidebar-tag` child shrink below its intrinsic width (the standard flex-overflow gotcha). **3. Session badges show SDK icons instead of `c` / `x` / `sh` letters.** Before: every session row had a colored badge with a single letter (`c` claude, `x` codex, `sh` shell). At-a-glance recognition was bad — `c` and `x` collide visually, `sh` doesn't fit the one-char convention, and the letters don't communicate "this is the agent SDK". Now: Lucide icons inside the badge — `Sparkles` (claude), `Cpu` (codex), `Terminal` (shell). Unknown adapter ids still fall back to the first letter so future adapters don't render blank. Background colour-coding kept (blue / green / orange). Verified live via DOM probe — all `.sidebar-agent-badge` instances now have `` children (was text nodes), badge size 16×14 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/components/TabStrip.tsx | 4 +++- ui/src/components/workspace/Sidebar.tsx | 23 +++++++++++++++++++++- ui/src/components/workspace/workspaces.css | 10 ++++++++++ ui/src/tabs/registry.tsx | 14 ++++++++++--- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/ui/src/components/TabStrip.tsx b/ui/src/components/TabStrip.tsx index c28433d65..727b89c4a 100644 --- a/ui/src/components/TabStrip.tsx +++ b/ui/src/components/TabStrip.tsx @@ -1,6 +1,7 @@ import { useState, type MouseEvent, type WheelEvent } from 'react' import { X } from 'lucide-react' import { useChannels } from '../contexts/ChannelsContext' +import { useWorkspaces } from '../contexts/WorkspacesContext' import { useWorkspace } from '../tabs/store' import { getView } from '../tabs/registry' import { ContextMenu, type ContextMenuItem } from './ContextMenu' @@ -22,6 +23,7 @@ import { ContextMenu, type ContextMenuItem } from './ContextMenu' */ export function TabStrip() { const { channels } = useChannels() + const { workspaces } = useWorkspaces() const tabIds = useWorkspace((state) => state.tree.kind === 'leaf' ? state.tree.group.tabIds : [], ) @@ -101,7 +103,7 @@ export function TabStrip() { const tab = tabsMap[id] if (!tab) return null const view = getView(tab.spec.kind) - const title = view.title(tab.spec as never, { channels }) + const title = view.title(tab.spec as never, { channels, workspaces }) const isActive = id === activeTabId return ( = { + claude: Sparkles, + codex: Cpu, + shell: Terminal, +}; + +function AgentBadgeGlyph({ agentId }: { agentId: string }): ReactElement { + const Icon = AGENT_ICONS[agentId]; + if (Icon) return ; + return ; +} + function WorkspaceRow(props: WorkspaceRowProps): ReactElement { const w = props.workspace; const isSelected = props.selection?.wsId === w.id && props.selection.sessionId === null; @@ -281,6 +301,7 @@ function WorkspaceRow(props: WorkspaceRowProps): ReactElement { type="button" className="sidebar-row-main" onClick={() => props.onSelectWorkspace(w.id)} + title={w.tag} > + )} + {overrideAgents.length > 0 && !onConfigure && ( +
+ + Workspace override · {overrideAgents.join(', ')} +
+ )} + {lastCommit && ( +
+ + + {lastCommit.subject} + +
+ )} + + )} + + ) +} diff --git a/ui/src/components/workspace/Sidebar.tsx b/ui/src/components/workspace/Sidebar.tsx index ece4b88df..ba68b2790 100644 --- a/ui/src/components/workspace/Sidebar.tsx +++ b/ui/src/components/workspace/Sidebar.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import type { FormEvent, ReactElement } from 'react'; -import { Cpu, Sparkles, Terminal, type LucideIcon } from 'lucide-react'; +import { Cpu, LayoutGrid, Sparkles, Terminal, type LucideIcon } from 'lucide-react'; import { createWorkspace, @@ -39,6 +39,10 @@ export interface SidebarProps { readonly onChanged: () => void; /** Optional: open the per-workspace AI-provider config modal. */ readonly onConfigureWorkspace?: (wsId: string) => void; + /** Open the Workspaces Overview dashboard tab (card view of all workspaces). */ + readonly onOpenOverview?: () => void; + /** True when the Workspaces Overview tab is currently focused — highlights the pinned row. */ + readonly overviewActive?: boolean; } export function Sidebar(props: SidebarProps): ReactElement { @@ -177,6 +181,19 @@ export function Sidebar(props: SidebarProps): ReactElement { {createError &&
{createError}
}