Skip to content

feat(slark): channel header tabs + topic detail side panel#37

Open
zoeforfun wants to merge 42 commits intomainfrom
feature/chat-tabs-and-topic-panel
Open

feat(slark): channel header tabs + topic detail side panel#37
zoeforfun wants to merge 42 commits intomainfrom
feature/chat-tabs-and-topic-panel

Conversation

@zoeforfun
Copy link
Copy Markdown
Contributor

Summary

Introduces the two chat-product features that were intentionally deferred from the phase-1 PR #34:

  • Channel header tabs — Messages / Files / Artifacts under every channel header.
  • Topic → right-side detail panel — click a topic card to push open a persistent side panel with a Thread / Files / Members / Pinned experience.

Status / merge sequencing

Channel header tabs (Messages / Files / Artifacts)

  • `ChatView` wraps the channel layout in `` with three surfaces:
    • Messages — the live feed + composer (the existing chat experience).
    • Files — EmptyState placeholder; reserved for shared files in the channel.
    • Artifacts — EmptyState placeholder; reserved for future agent-output browsing.
  • Tab labels are hardcoded English per the "tabs are orientation, not user content" convention that also governs the sidebar section headers.
  • Header chrome is a single unified border-b block — title row and tab row share one surface, no divider between them, so the header reads as one piece of chrome instead of two stacked bars.
  • DMs intentionally keep the flat single-pane layout; tabs only render for `channel`-type channels because DMs don't have a Files / Artifacts story the same way a channel does.

Topic → right-side detail panel

  • Clicking a topic card opens a persistent right-side panel instead of doing nothing. Primary tab is Thread (the reply conversation with inline images + link previews); Files / Members / Pinned are secondary.
  • Layout is push, not overlay: the Messages tab's flex row animates a right column from `width: 0 → 380px` over 200ms (`ease-standard`). No backdrop / scrim — the message list reflows. Closing keeps `activeTopic` until the width transition ends so the inner content doesn't flash empty mid-animation.
  • State lives in `ChatView` and resets on channel switch. The click path threads a dedicated `onTopicOpen` callback through `MessageList` → `ContentBlockRenderer` → `TopicBlock`; it is kept intentionally separate from `onExpand` (which drives the fullscreen modal overlay for code / diff / image) because topics are tracked threads, not transient previews.
  • `TopicDetailPanel` composes existing ui-web primitives (`DetailPanel`, `Tabs`, `EmptyState`) + reuses the `TopicThreadMessage` mock shape already on `types/index.ts` (that type was preserved on the PR fix(slark): neutral selection, compact nav density, white chat panel + codify rules #34 branch specifically so this merge stays clean).

Test plan

  • Open a `channel` route — header shows Messages / Files / Artifacts tabs, Messages is the default, Files / Artifacts render EmptyState.
  • Open a `dm` route — header stays flat, no tabs.
  • In Messages, click a topic card — right panel pushes in from 0 → 380px, message list reflows, Thread tab is active by default.
  • Switch between Thread / Files / Members / Pinned in the panel.
  • Close the panel — it animates out and the message list reflows back to full width.
  • Switch channels while the panel is open — panel resets to closed and `activeTopic` clears.
  • `pnpm format:check` and `pnpm typecheck` pass (verified on this branch).

Made with Cursor

chaoxiaoche added 30 commits April 20, 2026 18:49
The slark app maps --slark-color-nav-active to --color-brand-primary,
which means selected channels, active activity-bar icons, runtime tabs,
agent rows, and more all render with a saturated teal background. This
over-uses brand-primary — AGENTS.md reserves that color for links,
focus rings, accented badges, and brand emphasis, not large surface
fills.

Switch to the brand-subtle wash pattern (same pair used by Plus tier
badges: pale brand background + brand-primary text):

  --slark-color-nav-active      → var(--color-brand-subtle)
  --slark-color-nav-active-fg   → var(--color-brand-primary)
  --slark-color-nav-active-soft / -muted rebased on brand-primary

All 11 call sites across ChatSidebar, ActivityBar, Sidebar,
RuntimesSidebar, and AgentsSidebar pick this up automatically via the
tokens. The ActivityBar's 3px selection indicator (bg-nav-active-fg)
becomes brand-primary on a brand-subtle row — clear brand accent on a
soft wash.

Also refactor the one call site where the nav tokens were being abused
as a primary-button style: the "Add" button for detected runtimes now
uses <Button size="xs"> (the proper default filled variant) instead of
hand-rolling bg-nav-active / text-nav-active-fg.

No new tokens, no changes to ui-web.

Made-with: Cursor
Based on review feedback: the brand-subtle wash still read as "blue
selection" at a glance. Switch to the restrained Slack / Cursor pattern
— neutral surface-3 fill + text-heading (near-black) + font-semibold —
so selection is communicated by weight and text contrast instead of
color. Zero brand tint in the selected row.

  --slark-color-nav-active        → var(--color-surface-3)
  --slark-color-nav-active-fg     → var(--color-text-heading)
  --slark-color-nav-active-soft   → color-mix(text-heading 18%, transparent)
  --slark-color-nav-active-muted  → color-mix(text-heading 78%, transparent)

Also drop the 3px ActivityBarIndicator on active items: with a solid
accent fill gone, the indicator was redundant decoration.

Primary-action buttons (unread badges in ChatSidebar, brand-colored
CTAs like the runtime Add button migrated in the previous commit) are
unaffected — they keep using --color-brand-primary / default Button
variant per AGENTS.md.

Made-with: Cursor
Codifies the lessons from the slark nav color + density fixes into
AGENTS.md so future work (AI agent or human) picks the right tokens
on day one.

Rules added under "Layout conventions":
- Compact nav list density: px-2 container, 0 row gap, rounded-md,
  pl-2 pr-2 py-[3px] rows (~24px height), px-2 pt-3 pb-1 section
  headers — matches Slack / Cursor / Discord.
- Selection row background: neutral surface-3 + text-heading +
  font-semibold; never fill persistent selection with brand/accent/
  semantic tokens. Includes guidance for app-level CSS vars and an
  explicit exception clause for transient "spotlight" states.

Slark fixes applying the new rules:
- ChatSidebar: container px-2, rows rounded-md with no inter-row gap,
  unified "Add channels" affordance with list rows, section header
  aligned with list padding.
- ChatView: use bg-surface-1 (pure white / --card) for the chat
  content panel instead of bg-background (surface-0 = page canvas,
  slight gray). Chat is a content panel, not the page shell.

Made-with: Cursor
…dges

Previous py-[3px] + 16px icons + 18px unread badge was too tight for
a 13px label — rows felt cramped even with no inter-row gap. Back off
to Slack's breathing room while keeping the 0-gap rhythm.

ChatSidebar rows:
- py-[3px] → py-[5px]  (row height ~24px → ~28px)
- icon h-4 w-4 (16px) → h-3.5 w-3.5 (14px)
- unread badge h-[18px] min-w-[18px] px-1.5 → h-4 min-w-4 px-1

AGENTS.md "Compact nav list density" updated to match:
- Row padding pl-2 pr-2 py-[5px], 14px icon, ~28px row height
- New rule for unread/count badge sizing (h-4, compact 16px)
- Keeps container px-2, 0 row gap, rounded-md, section header spec

Made-with: Cursor
The section header already has a "+" icon button that opens the
create-channel dialog. Having a second "+ Add channels" row at the
bottom of the list is redundant affordance and adds visual noise in
the compact nav density.

- Remove the bottom "Add channels" Button row from ChatSidebar
- Drop now-unused `chat.addChannels` i18n key (en-only, no zh parity)
- Section header "+" button remains the single entry point for
  creating a channel, which is the Slack / Cursor convention

Made-with: Cursor
py-[5px] still felt cramped per user feedback. Commit to Discord-
density instead of trying to split the difference.

- py-[5px] → py-2 (8px vertical)  : row height ~28px → ~32px
- gap-2 → gap-2.5                 : 2px more icon-to-label breathing
- AGENTS.md compact nav list spec updated to match (32px row height)

Made-with: Cursor
The UI chrome (搜索 / 频道 / 设置) was already Chinese via i18n, but
the mock channel names, descriptions, and workspace name were still
hardcoded English, creating a jarring mix in the sidebar demo.

Localizes the surfaces that show up in the nav list:
- Workspace: Acme Engineering → 星云工程
- ch-welcome: welcome → 欢迎 (+ Chinese description)
- ch-showcase: design-showcase → 设计展示 (+ Chinese description)
- dm-agent-1: CodeBot → 代码助手
- dm-agent-2: DesignReviewer → 设计评审

Keeps user names (Alice Chen / Bob Kim / Charlie Park / Diana Wu)
and agent/template/skill content + mock message bodies in English —
those are out of scope for this pass and can be done in a follow-up
if a fully localized demo is desired.

Made-with: Cursor
Previous spec said 0 row gap on the assumption that rounded selected
bg already separates rows. But when two adjacent rows are both
filled at the same time (hover + hover, focus-ring + hover,
unread-bg + hover), the fills physically touch and look like one
smudged block — visually reported by user on welcome (focused) and
design-showcase (hovered) row pair.

New rule: space-y-0.5 (2px) is the minimum gap that prevents any
two adjacent filled states from butting into each other, while
staying tight enough to read as one coherent list (not a chain of
disconnected pills).

- ChatSidebar: wrap row lists in <div className="space-y-0.5">
- AGENTS.md "Compact nav list density": change row gap rule from
  0 → space-y-0.5, with rationale about adjacent filled states.

Made-with: Cursor
Two issues reported on the channel sidebar in zh-CN locale:

1. "频道" section header looked awkward because the styling
   (uppercase tracking-wider) is inherently English-first. CJK has
   no case, so text-transform: uppercase is a no-op, and the wide
   letter-spacing looks wrong on CJK glyphs.
2. Only pb-1 (4px) between the section label and the first row,
   which made the label feel like it was touching hover/selected
   backgrounds below it.

Fixes:
- Hardcode "Pinned" / "Channels" as English in ChatSidebar (matches
  Slack / Cursor / Linear convention in CJK locales — decorative
  category labels stay English regardless of app locale).
- Bump section header padding: pb-1 → pb-2 (8px) so the label has
  clear separation from the first filled row below.

AGENTS.md "Compact nav list density" updated with two new rules:
- Section header language convention (decorative uppercase labels
  stay English across all locales, with rationale + escape hatch).
- Section header spacing corrected to pb-2.

Made-with: Cursor
… auto-translated

Reverts commit 058b785. That commit localized channel names, the
workspace name, and agent DM names to Chinese to align with the
zh-CN UI chrome. On user review, this was the wrong call:

These fields are user content (channel names, workspace names, and
DM conversation titles are created by users and persist across
locales), not UI chrome. An IM app should never mutate user-authored
strings based on the viewer's locale — the name you saved is the
name everyone sees.

Reverted:
- Workspace: 星云工程 → Acme Engineering
- ch-welcome: 欢迎 + Chinese description → welcome + EN description
- ch-showcase: 设计展示 + Chinese description → design-showcase + EN
- dm-agent-1: 代码助手 → CodeBot
- dm-agent-2: 设计评审 → DesignReviewer

UI chrome / i18n keys already correctly show Chinese strings in
zh-CN locale; that layer is unchanged.

Made-with: Cursor
… 24px

Section header at 10px was borderline unreadable at sidebar width,
and the "+" button at h-4 w-4 (16px) had a hover fill too small to
read as a real button (below accessible tap-target minimum too).

- Section header: text-[10px] → text-[11px], Pin icon h-3 → h-3.5
- Create-channel "+" button: h-4 w-4 rounded → h-6 w-6 rounded-md,
  plus icon h-3 → h-3.5, giving a proper 24px hit area and a
  visible rounded hover fill matched to the list row radius.

AGENTS.md "Compact nav list density" updated with:
- Section header font size rule (11px, with rationale vs 10px)
- New rule for section header affordance buttons (24px rounded-md
  with 14px icon, not 16px square)

Made-with: Cursor
Two related fixes on the channel sidebar:

1. Search input was at px-3 (12px inset) while the list container
   below was at px-2 (8px inset). The 4px mismatch made the search
   look like a different component from the list it filters — the
   selection fill on "welcome" row visibly extended past the
   search's right edge. Align search wrapper to px-2.

2. Search input had border-transparent at rest, which made it read
   as a passive pill rather than an editable control. Per AGENTS.md
   border tokens, a sidebar search should show --color-border-subtle
   (rgba 0 0 0 / 0.06) — just enough to assert "this is an input"
   without shouting. Focus state unchanged (border-transparent +
   ring-1 ring-nav-ring).

AGENTS.md "Compact nav list density" updated with a new rule for
top-of-list search inputs: match px-2 with the list container, and
always show border-border-subtle at rest, not border-transparent.

Made-with: Cursor
The leftmost ActivityBar was a solid bg-nav-surface with no border,
reading as a flat slab next to the white chat panel. Adopt the same
frosted-glass recipe the rest of the codebase uses for floating
chrome (nav bars, landing headers, floating toasts): translucent
surface + backdrop-blur + subtle border.

ActivityBar class change:
  border-r-0 bg-nav-surface
→ border-r border-border-subtle bg-nav-surface/75 backdrop-blur-md

Keeps the nav-surface gray character (not switching to a new color),
just makes it 75% alpha with medium blur and a hair-thin right
border to separate from the white chat panel.

AGENTS.md new section "Frosted glass (translucent surfaces)" under
Design & styling > Elevation & shadow:
- Canonical recipes for stacked nav vs sidebar/activity bar
- Alpha range (70–92%) and rationale for both bounds
- Always-pair-with-backdrop-blur-md rule + when to deviate
- Always-add-subtle-border rule (prevents bleeding into neighbors)
- Note on Electron vibrancy: classes work with or without, so apply
  them proactively — the effect lights up automatically if vibrancy
  is configured later.
- Explicit anti-pattern: do not use on content panels (chat body,
  page cards) — those must stay fully opaque for readability.

Made-with: Cursor
…ntent canvas

Wire up actual desktop-blur frosted glass on the macOS ActivityBar instead of
the previous CSS-only treatment that had nothing to blur through.

- main/index.ts: enable `vibrancy: "sidebar"` + `visualEffectState: "active"`
  on macOS; drop the opaque `backgroundColor` so vibrancy can show through
  (non-mac still sets `#fafafa` for a no-flash startup)
- renderer chain goes transparent so vibrancy reaches the chrome:
  - index.html body: drop `bg-background`
  - globals.css: `html / body / #root { background: transparent }`
  - AppLayout root: drop `bg-background`
- AppLayout `<main>` gets `bg-surface-1` so every routed view has a solid
  white content canvas (Team / Settings / Runtimes were missing bg and
  silently showed vibrancy through as a "fake frosted" bug)
- ActivityBar: `bg-nav-surface/55` → `bg-nav/80` — the surface-2 token is
  gray, which read as "dim" at low alpha; surface-1 (white) at 80% gives
  the lightly-frosted-white look that matches Slack / Cursor / Finder

Made-with: Cursor
…titles

Redesign the channel header into a single unified chrome block that replaces
the old two-row title-plus-toolbar layout:

- Unified header: title row (`h-9`) + tabs row share one `border-b` block
  with no divider between them, reading as one chrome surface instead of
  two stacked bars
- Members as inline chip: Users icon + count sits immediately right of the
  channel name (not pushed to the far right), and opens AddMembersDialog on
  click. Replaces both the old right-aligned UserPlus button and the
  separate "Members" tab
- Tabs: `Messages / Files / Artifacts` (Members removed since it's now the
  chip). Compact size — `h-6 px-2 text-[12px]` triggers with 12px icons —
  so they sit comfortably under the 15px title. Labels hardcoded English
  across locales, matching the CHANNELS / PINNED convention
- Files + Artifacts tabs render EmptyState placeholders (hardcoded English
  strings — the previously-added chat.tab.* i18n keys were never wired
  after moving to hardcoded labels and are removed)
- Drop channel description rendering entirely from the header — it was
  redundant with the title for most channels and stole horizontal space
  from the members chip
- Hover fill on title + members buttons: `hover:bg-accent` → `hover:bg-surface-2`.
  Tailwind's `bg-accent` resolves to the raw brand teal, not our design-system
  `--color-accent` near-black — the old class flooded the whole row with teal
  on hover
- Rename welcome channel: "welcome" → "Welcome to Nexu!", clear description

Made-with: Cursor
Capture the new rules that came out of the Slark chrome + chat-header pass
so the next contributor doesn't rediscover them:

- Frosted glass: always start from surface-1 (white), never surface-2 (gray);
  tighter 75–85% alpha over native vibrancy; full Electron integration
  recipe (vibrancy config, transparent parent chain, opaque content panels
  as the inverse gotcha)
- Row-level hover fill: ban `hover:bg-accent` (resolves to brand teal),
  prefer `hover:bg-surface-2`; grep new code for this before landing
- Tab labels stay English across locales for page-level navigation tabs
  (chat header, settings, skills) — same convention as decorative uppercase
  category labels
- Compact tab size inside a chat/channel header: `TabsList h-7 p-0.5`,
  `TabsTrigger h-6 px-2 text-[12px]`, `size-3` icons
- Unified chat/channel header: title row + tabs row share one border-b
  container, no divider between them

Made-with: Cursor
Product changes
- Default /chat route now lands on ch-showcase so the demo conversation is
  visible at launch; mock data adds a "Yesterday" system join EventNotice,
  a kickoff message, and an @mention-me message so highlighted rows and
  consecutive-message grouping are exercised out of the box.
- Unify every rich card's width to w-full max-w-[640px] (code, diff,
  tool-result, action, approval, progress, topic) so the feed has one
  card lane and cards are instantly distinguishable from chat bubbles.
- ApprovalBlock, ActionCard, ProgressBlock retuned to design-system
  semantic tokens (info / success / warning / error + their subtle
  variants) and ui-web primitives (Button, Progress replaced with
  checklist) — no more hardcoded blue/green/red/amber.
- Code and diff blocks collapse into a compact single-line pill (Cursor
  style) via a shared CollapsedContentRow; clicking expands via the
  existing ContentDetailOverlay. Keeps the chat feed scannable.
- ProgressBlock rebuilt Cursor-style: drop the bar and the colored step
  dots, switch to Check / Loader2 / Circle icons in monochrome with
  12px step spacing; "3 / 5" counter replaces the big percentage.
  Status color is carried purely by label styling (muted + strikethrough
  for done, heading for active, tertiary for pending).
- TopicCard: brand-tinted border on white (not a brand wash) so the
  status pill owns the semantic color; hover deepens the border and
  adds shadow-sm for affordance. Prevents duplicate "brand flood"
  across four stacked topic cards and unblocks the highlighted-mention
  row styling from fighting the card surface.

Topic → side-panel interaction (new)
- Clicking a topic card now opens a persistent right-side detail panel
  instead of doing nothing. Four tabs: Artifacts / Members / Files /
  Pinned, matching the storybook chat-side-panel prototype (ca6c295).
- Layout is push (not overlay): ChatView wraps the Messages tab in a
  flex row and animates the panel column width 0 → 380px over 200ms
  (ease-standard). No backdrop / scrim — the message list reflows.
- State lives in ChatView and resets on channel switch. A dedicated
  onTopicOpen callback threads through MessageList → ContentBlocks →
  TopicBlock; it is intentionally separate from onExpand (which drives
  the fullscreen ContentDetailOverlay for code / diff / image) because
  topics are tracked threads, not transient previews.
- TopicDetailPanel composes existing ui-web primitives (DetailPanel,
  Tabs, EmptyState) — no new primitives added.

Dev DX
- apps/slark/electron.vite.config.ts aliases @nexu-design/ui-web to
  packages/ui-web/src/index.ts (matching storybook's main.ts) so edits
  to ui-web primitives reflect instantly via HMR. Without this the
  renderer consumed the stale dist bundle from package.json's
  "main": "./dist/index.js", which silently blocked newly-added
  Tailwind utility classes in primitives from reaching the DOM even
  though Tailwind generated CSS for them.

Made-with: Cursor
The right-side TopicDetailPanel's primary tab was a placeholder
"Artifacts" surface with a single summary card. What people expect on
a topic panel is the reply conversation itself — with inline images,
link previews, agent messages, and the originating opening context at
the top. This patch makes that happen.

Data
- Extend the topic content-block type with an optional `thread`
  (`TopicThreadMessage[]`) so a topic can carry canned replies. Each
  reply is { id, author, initials, createdAtLabel, text?, image?,
  link?, isAgent?, accent? } — pre-baked display strings for mock
  data; no timestamps, no driver logic.
- Add canned threads to three topics in ch-showcase:
    * topic-1 (Billing retry storms — NEEDS REVIEW): 4 replies, includes
      a Datadog dashboard link and a GitHub PR link; the fourth reply
      is from the "Coder" agent.
    * topic-2 (Landing redesign — ACTIVE): 3 replies, includes an
      inline hero-exploration image and a Linear issue link.
    * topic-3 (Auth rotation — DONE): 3 replies, includes a Notion
      report link.

Panel UI
- Replace the "Artifacts" tab with a **Thread** tab (new default).
  Renders the reply list with a small local component instead of the
  full ChatMessage primitive — at 380px wide the panel needs tighter
  padding, smaller avatars, and a custom link-card style, and
  overriding ChatMessage for all of that would be more brittle than
  a purpose-built reply row.
- If `topic.preview` exists, render it as an "Opening context" card
  pinned above the replies, mirroring how Slack / Linear surface the
  originating message at the top of a thread view.
- Agents in the thread get a Bot glyph avatar on `bg-brand-subtle`,
  a green "Agent" badge, and the assignee accent color applied to
  their author name — same visual language as the main chat feed.
- Inline images use the shared `ImageAttachment` primitive; links
  render as a compact preview card (icon + title + description +
  host) that opens in a new tab.
- Files tab now aggregates images and links across the thread
  instead of showing EmptyState — images render as a 2-col grid,
  links as a vertical list of the same preview card used inline.
  Falls back to EmptyState when the thread carries nothing.
- Tab order reset to Thread / Files / Members / Pinned; Artifacts
  was dropped because it duplicated Files once threads carry the
  real payload.

Made-with: Cursor
File / image / voice / video attachments now share a single 360px frame
so stacked attachments in a chat feed line up cleanly instead of wobbling
between 320 / 340 / 360. ImageGallery keeps its 480px grid. Adds a
chat-feed width-tier rule to AGENTS.md (card tier 640px, attachment
tier 360px) so future primitives do not drift.

Made-with: Cursor
Voice notes used to always expand their transcript below the waveform,
which made a channel of voice recaps feel heavy. Now the transcript is
hidden by default and a small captions icon fades in on hover (top-right
of the waveform row). Clicking toggles the transcript inline; the button
stays visible + active-styled while expanded, and ARIA aria-expanded /
aria-label describe the state. Callers that want the old behaviour can
pass defaultTranscriptOpen.

Made-with: Cursor
…eight

Three follow-ups from the IM showcase review:

1. Play button hover no longer flips to the brand accent fill (which
   read as a jarring teal disc on a light card). It now matches the
   video attachment's language — stable background, subtle scale-110
   on hover, same transition timing.
2. The hover-only captions icon was ambiguous. Replace it with a
   plain-text toggle rendered BELOW the card ("Show transcript" /
   "Hide transcript" with a rotating chevron). Always visible when a
   transcript is present, so the affordance is obvious without
   hunting.
3. Waveform bars were too tall for a compact feed. Shrink the bar
   container from h-7 (28px) to h-5 (20px), halve the default
   waveform heights, and drop the bar offset from +6 to +3. The
   CustomWaveform story values are rescaled to match the new range.

Made-with: Cursor
The waveform is purely ornamental — it signals "this is audio" and
nothing more. The previous h-5 bar row still read as content. Drop
the bar container to h-4 (16px), narrow each bar from 3px → 2px,
halve the default waveform heights, and reduce the render offset
from +3 to +2 so the tallest bar sits at ~9px. Rescale the
CustomWaveform story values to match the new range.

Made-with: Cursor
Continuing the "waveform is decoration, not data" direction. Shrink
the bar container from h-4 (16px) to h-3 (12px), cap default
waveform values at 4 so the tallest bar renders at ~6px, and
rescale the CustomWaveform story to match.

Made-with: Cursor
Consolidate the Coder agent's five sequential artifacts (retry.ts code,
client.ts diff, pnpm-test action, tool result, staging-rollout progress)
into a single `agent-run` content block so the chat stays quiet. The
renderer shows only the current (last) step expanded; earlier completed
steps live behind a "Show N earlier steps" toggle.

The approval card that follows stays in its own message on purpose —
`AgentRunStep["block"]` narrows the union to code/diff/action/tool-result/
progress, so approval and topic blocks cannot be smuggled inside a run.
Folding an ask into a "quiet" module would hide the thing that actually
needs human attention.

- types: add `agent-run` variant + `AgentRunStep` interface
- mock: merge sc-8..sc-12 into one `sc-agent-run-1` message with 5 steps
- ContentBlocks: add `AgentRunBlock` + `AgentRunStepRenderer`, wire the
  `agent-run` case into the switch

Made-with: Cursor
The outer agent-run container was on surface-1 with a border, and its
nested work cards (code, diff, action, tool-result, progress) were ALSO
surface-1 with borders — so the two concentric rectangles read as a
redundant double frame. Tinting the shell with surface-2 and dropping
its border lets the white inner cards visibly 'sit inside' the run
without competing strokes.

Bumped the 'Show earlier steps' toggle hover to surface-3 so it still
pops against the new surface-2 shell.

Made-with: Cursor
A solid outlined circle for pending/not-yet-started steps can read as
'selectable' or 'actionable'. A dashed ring (as Cursor uses) more
clearly communicates 'placeholder for a future step' and visually pairs
with the strikethrough + muted label already applied to unreached items.

stroke-dasharray is set inline because lucide forwards style to the
root <svg>, and SVG stroke-dasharray inherits down to the child
<circle>.

Made-with: Cursor
The two-step flow (details → add members) always defaulted to "select
everyone" and the member picker screen was redundant with the separate
AddMembersDialog reachable from a channel. Collapsing to one step
removes a click, a progress bar, and a whole screen of UI the user
mostly clicked through.

On create we now seed membership with all workspace users + all agents
(same default as before); owners can prune from the channel members
panel afterwards.

Copy is hardcoded English — this product surface is English-only, and
the tokenised subtitle ("Step 1 of 2 — channel details") was the
noisiest side of the old dialog.

Made-with: Cursor
DialogDescription was rendering at text-base (13px per our token
scale) which reads chunky for a secondary supporting line, especially
with CJK. The spec puts dialog descriptions (and general hint /
supporting text) at text-sm (12px), which also matches shadcn's
upstream default. Applying it at the primitive so every dialog
benefits instead of each caller overriding className.

Made-with: Cursor
Two related fixes surfaced by the delete-channel confirm screenshot:

1. ui-web DialogTitle used leading-none, which on CJK glyphs cropped
   against the description underneath (no natural descender space).
   leading-tight (1.25) gives the title a few px of breathing room so
   the title/description gap reads correctly under all scripts while
   still feeling compact enough for Latin.

2. Slark ChatSidebar's channel-delete ConfirmDialog was still pulling
   copy through i18n. Matches the CreateChannelDialog treatment — this
   product surface is English-only, so hardcode the strings here too
   instead of having English copy bounce through the zh locale.

Made-with: Cursor
…added)

Previous commit 0e7cf0a slipped in four unrelated files that should
have stayed untracked — a .claude skill submodule, two local .cursor
hook artefacts, and a personal analysis markdown. None of them belong
to the slark/ui-web surface. Removing from the index while keeping the
working-tree copies.

Made-with: Cursor
chaoxiaoche added 12 commits April 21, 2026 12:02
Per the design spec, primary/confirm actions trail right and secondary
actions sit to their left. The approval card had Approve on the left
and Reject on the right — mirrored from the convention. Both keep
flex-1 so the row still reads as 'pick one of two equal paths' rather
than a weighted CTA.

Made-with: Cursor
Four polish fixes to the message composer:

1. Send button promoted from a ghost Send icon to a circular accent-
   filled ArrowUp — matches the Cursor composer. Three explicit states:
   disabled (surface-3 fill + muted arrow, no text / nothing to abort),
   ready (accent fill + white arrow, has text), and streaming
   (accent fill + white stop square). One button, one footprint, three
   meanings.

2. Stop works: clicking the stop square flips every in-flight reply
   in the channel to isStreaming=false. simulateAgentReply's token
   loop now re-reads the store each tick and bails out when the flag
   is flipped, so further tokens stop being appended (cooperative
   cancellation, no timer bookkeeping).

3. Placeholder copy is English and hardcoded: 'Message #channel' for
   channels, 'Message {name}' for DMs — drops the useT lookup that
   was routing through the zh locale.

4. Placeholder vertically centered by switching textarea padding from
   py-1.5 (12px) to py-2 (16px) so the 20px line-box fills the 36px
   collapsed height. Composer internal gap bumped from 0.5 (2px) to
   2 (8px) so Attach and Send stop bumping shoulders.

Made-with: Cursor
The 'reacted' state was using brand-subtle + brand-primary text, which
over-weighted emoji reactions in the feed — they read louder than the
actual messages. Switching to surface-2 + text-primary keeps the
reacted pill quiet while still clearly distinct from unreacted pills,
and aligns the hover state with the reacted state so hovering
foreshadows what clicking will do.

Made-with: Cursor
… panel polish

- New `lib/user-presence.ts` centralises presence→dot-class / label
  mapping so every call site agrees on green = online, yellow = away,
  gray = offline. Replaces ad-hoc lookups across the app.
- `globals.css`: add `--slark-color-nexu-away` (warm yellow) and lift
  `--slark-color-nexu-offline` to 0.65 lightness so the gray dot reads
  clearly on the sidebar background.
- `AgentsSidebar`: drop the `useT` indirection and hardcode English
  copy (teammate list is a product surface, not content). Rename the
  section from "People" to "Members"; widen the spacing below the
  search input; add a bottom-right presence dot overlay on each
  avatar; hold the Owner badge in `text-brand-primary bg-brand-subtle`
  regardless of row-selection state; shade member email / agent
  description with `text-text-tertiary` so the name keeps the emphasis.
- `UserDetail`: remove the in-corner avatar status dot and move the
  status affordance below the name (dot + textual label) so the profile
  read order is Name → Status → Email → Runtime. Adopt the canonical
  workspace content-panel wrapper (`max-w-[800px] mx-auto px-4 pt-2
  pb-6 sm:px-6 sm:pb-8`) so this panel shares left/right margins with
  Settings and Agent Detail. Drop Chinese copy in favour of English.

Made-with: Cursor
- `SettingsView` > Workspace tab: remove the card-level "Workspace
  details" title + description. The surrounding `PageHeader` already
  reads "Workspace settings", so repeating a second heading in the
  very next card just pushed the actual inputs down with no added
  information.
- `Sidebar`: suppress the generic uppercase section label on the
  Runtimes route. `RuntimesSidebar` owns its own header row ("Runtimes"
  + "N/M online" on a single line, indented to match the content) and
  rendering both produced two differently-indented headers stacked on
  top of each other.

Made-with: Cursor
… copy

RuntimesSidebar:
- Custom header with "Runtimes" + "N/M online" on a single line,
  left-aligned to content (px-3).
- Add button swapped from filled black to outline (size="xs"
  variant="outline") — it was sitting at the same visual weight as
  primary page CTAs.
- Installed / detected list rows render `RuntimeLogo` instead of
  generic Lucide glyphs, so Claude Code / Codex / Gemini / Pi show
  their actual brand marks.
- "Detected on this device" always renders a single `RefreshCw` icon
  button on the right, so refreshing no longer swaps text ↔ button
  and causes horizontal layout shift.
- Hardcoded English copy (drop `useT`).

RuntimesView:
- Hardcoded English copy (drop `useT`, `TranslationKey`,
  `statusLabelKeys` in favour of a simple inline map) — status pill
  reads Online / Offline / Error.
- Runtime logo now sits inside the canonical logo tile — fixed-size
  `rounded-xl/2xl` surface chip with `border-border-subtle` and
  `bg-surface-1`, matching the `provider-settings` pattern. No more
  blue / teal logo backgrounds.
- Optical-size helper (`logoOpticalScale` + `getLogoSize`) bumps the
  glyphs for Codex / Gemini CLI / Pi that ship with intrinsic padding,
  so every brand mark reads at the same perceived weight.
- Install-guide cards refactored so the entire card navigates to docs
  (absolute-positioned `<a>` overlay) while the install command line
  is independently click-to-copy — new `InstallCommand` component with
  Copy / Check feedback. Jump icon switched to `ArrowUpRight` and
  aligned with the first line of the name.
- Every `hover:bg-accent` / `bg-accent` in this view replaced with
  neutral `hover:bg-surface-2` or surface-2 + ring — `--color-accent`
  is near-black in light mode and turning it on as a hover state made
  the whole row look like the active/primary affordance.

Made-with: Cursor
- Hardcode "Add workspace" and "Sign out of Nexu" (drop the `useT`
  keys for this menu — it's a product surface, not i18n content).
- Remove the `DropdownMenuSeparator` between the existing workspaces
  and the "Add workspace" row. They're sibling affordances (same row
  shape, same tile leading slot), so splitting them with a divider
  implied a semantic break that doesn't exist. The separator before
  "Sign out" stays — that one does fence off a destructive action.
- "Sign out" now follows the destructive-intent hover rule in
  AGENTS.md: neutral `text-text-primary` at rest (so the menu doesn't
  shout red every time it opens) and only swaps to
  `destructive/10` + `text-destructive` on hover / keyboard focus.

Made-with: Cursor
…cent

The circular send affordance was painted with `bg-accent` — that's the
brand teal and it made the composer look like it had two competing
brand moments (teal reaction pills had just been retired for the same
reason). Swap to `bg-primary` / `text-primary-foreground` so the send
button matches Button's canonical `default` variant (near-black fill)
and reserve `--color-accent` for links and focus rings.

Made-with: Cursor
`size="icon"` / `"icon-sm"` inherited the base class's `rounded-lg`
(12px). For `icon-sm` (24px square) that's exactly half the width, so
the hover fill rendered as a perfect circle. Pin both icon sizes to
`rounded-md` (8px) per the design spec — controls live at
`--radius-md`, only cards and panels use the larger `rounded-lg`.

Made-with: Cursor
`--color-accent` is near-black in light mode. Using it as a *hover*
background on outline buttons, ghost buttons, list rows, menu items,
or cards produces a heavy filled affordance that reads as the primary
action — directly conflicting with the "at most one accent-weighted
action per group" rule.

Sweep every slark call site to the neutral gray hover fill:

- `ChatSidebar` pin/unpin context menu row → `hover:bg-surface-2`.
- `MentionPicker` dropdown row → `hover:bg-surface-2`.
- `AgentsView` agent card hover → `hover:bg-surface-2`.
- `DevPanel` 5× state / user / runtime / workspace rows →
  `hover:bg-surface-2` (and dropped the `/50` opacity variant).
- `ConnectRuntimeStep` runtime picker card hover → `hover:bg-surface-2`.

Also expand the Color usage section in AGENTS.md with an explicit
prohibition on `bg-accent` / `hover:bg-accent` / `/30` / `/40` / `/50`
variants for hover states, pointing at `hover:bg-surface-2` (neutral),
`rounded-lg hover:bg-surface-2` (dropdown / menu), and
`hover:bg-destructive/10 hover:text-destructive` (destructive intent)
as the sanctioned alternatives.

Made-with: Cursor
…er release

Phase 1 of PR #34 does not ship either of these two features; both
continue to live on the dedicated feature branch
`feature/chat-tabs-and-topic-panel` and will return in a follow-up
release once the surrounding product scope is agreed.

Surgically removed from this branch:

- Channel header tabs (Messages / Files / Artifacts) — the chat
  header reverts to a single flat bar. The visual improvements that
  landed alongside the tabs work (Globe icon for channels, neutral
  `hover:bg-surface-2` title affordance, inline members chip with
  Users icon + count, no-drag on interactive elements, description
  suppressed from chrome) are *kept* — they were orthogonal wins
  and belong in phase 1.
- Topic → right-side detail panel (Thread / Files / Members /
  Pinned) — `TopicDetailPanel.tsx` deleted; the `onTopicOpen`
  callback prop is unwound from `ChatView` → `MessageList` →
  `ContentBlockRenderer`; `TopicBlock` now renders as a read-only
  display card (still brand-tinted, still composable) with no
  click target. All topic card chrome, status pills, participant
  rows, preview text, etc. stay exactly as before — only the
  panel-opening interaction is deferred.

Kept on purpose (orthogonal improvements that came with the same
commits but are not part of the deferred features):

- `MessageList` still highlights rows where the current user is
  `@mentioned` via `ChatMessage`'s `highlighted` prop.
- `ContentBlocks` card-width unification, Cursor-style code / diff
  collapsing, monochrome progress steps, approval tone tokens —
  all untouched.
- `TopicThreadMessage` type + `thread?` field on the topic block
  are intentionally preserved on the type side so mock data stays
  valid and the feature branch merges back cleanly later. Docstring
  updated to call out that it's dormant for now.

Made-with: Cursor
Reintroduces the two chat-product features that were removed from the
phase-1 PR #34 in 80ffbf4 so they can ship on their own review cycle.

Channel header tabs (Messages / Files / Artifacts)
- `ChatView` wraps the channel layout in `<Tabs>` with three surfaces:
  Messages (the live feed + composer), Files (EmptyState placeholder
  for now), Artifacts (EmptyState placeholder, reserved for future
  agent-output browsing). Tab labels are hardcoded English per the
  "tabs are orientation, not user content" convention that also
  governs the sidebar section headers.
- Header chrome is a single unified border-b block — title row and
  tab row share one surface, no divider between them, so it reads as
  one piece of chrome instead of two stacked bars.
- DMs intentionally keep the flat single-pane layout; tabs only
  render for `channel`-type channels.

Topic → right-side detail panel (Thread / Files / Members / Pinned)
- Clicking a topic card opens a persistent right-side panel instead
  of doing nothing. Primary tab is Thread (reply conversation with
  inline images + link previews); Files / Members / Pinned are
  secondary.
- Layout is *push*, not overlay: the Messages tab's flex row animates
  a right column from width 0 → 380px over 200ms (ease-standard). No
  backdrop / scrim — the message list reflows. Closing keeps
  `activeTopic` until the width transition ends so the inner content
  doesn't flash empty mid-animation.
- State lives in `ChatView` and resets on channel switch. The click
  path threads a dedicated `onTopicOpen` callback through
  `MessageList` → `ContentBlockRenderer` → `TopicBlock`; it is kept
  intentionally separate from `onExpand` (which drives the fullscreen
  modal overlay for code / diff / image) because topics are tracked
  threads, not transient previews.
- `TopicDetailPanel` composes existing ui-web primitives
  (`DetailPanel`, `Tabs`, `EmptyState`) + reuses the
  `TopicThreadMessage` mock shape already on `types/index.ts`.

No other changes vs the PR #34 tip: this branch is exactly
`fix/slark-nav-active-brand-wash` HEAD + this one commit, so once
PR #34 merges, the diff of this PR against `main` collapses to
these two features alone.

Made-with: Cursor
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying nexu-design with  Cloudflare Pages  Cloudflare Pages

Latest commit: d55e078
Status: ✅  Deploy successful!
Preview URL: https://cf2b6e44.nexu-design.pages.dev
Branch Preview URL: https://feature-chat-tabs-and-topic.nexu-design.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant