Commit Graph

10 Commits

Author SHA1 Message Date
Jiayuan Zhang
7803a5b9ea feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6136)
tokens.css defined colours, radii and font families but not a single --text-*
step, so font sizes had no baseline to align to and grew wherever they were
needed: 51 distinct sizes across web + desktop, 370 written as arbitrary
values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px).
text-xs and text-sm carried nearly all UI text while the range between them —
11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not
come from having more sizes; past a handful, each extra size makes the
hierarchy blurrier.

Add ten role-named steps, each with its own line-height so leading cannot
fragment the way size did, and move every product-UI call site onto them.
Steps are named for what the text is for, not for a t-shirt size, because
that is what keeps the scale from drifting again.

Six steps deliberately keep the exact size/line-height pairs of the Tailwind
defaults they replace, so the ~1,900-call-site rename moves nothing on screen.
The visible changes are confined to former arbitrary values snapping to a step:
8/9/10px -> micro (11px) on badges and overlines; 17 -> 18; 22 -> 24; 30
(text-3xl) -> 36 on headings and stat numbers; 12.8px -> label (13px) on small
buttons and toggles. Half-pixel sizes are gone.

This supersedes #6108, which was reverted by #6116 because the sidebar group
labels rendered at the inherited 16px. The cause was not the scale but cn():
`text-<x>` is ambiguous in Tailwind, and tailwind-merge resolves it against a
table listing only the default sizes, so it filed every role step under
text-colour and dropped whichever of `text-caption` /
`text-sidebar-foreground/70` came first. Registering the steps as a font-size
class group restores the real conflict groups — size beats size, colour beats
colour, the two coexist — and a test pins the list against the scale, since
the failure is silent in source.

Hand-written CSS is covered too. The transcript kept a 12.5px body long after
every Tailwind call site was on the scale, so the "no half-pixel sizes" claim
was true of the classes and false of the product; the editor's prose, code and
mermaid ramps had the same blind spot, and seven of their eight values already
equalled a step exactly. All now reference var(--text-*). The guard test reads
raw `font-size:` declarations as well as class names, exempting only the 16px
iOS input-zoom workaround in base.css and the landing pages' marketing ramp.

apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system)
keep Tailwind's default scale and are untouched. Landing display type
(rem/clamp, 2.2-6.4rem) stays on its separate ramp, as do four decorative
emoji / serif-hero sizes.

Verified on a running local stack: pinned sidebar rows and group labels
measure 12px/16px, nav items 14px/20px — identical to pre-migration. An audit
of every rendered font size across the product surfaces finds nothing off the
scale; the only exceptions are avatar initials and emoji, which
actor-avatar.tsx sizes proportionally to the avatar diameter by design.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-30 13:42:33 +08:00
Naiyuan Qing
e70e71fea1 fix(issues): repair the issue Table's column interactions, loading states and scroll rendering (#6036)
* fix(ui): make table column resizing land where the pointer is (MUL-5166)

Pressing the resize handle committed a width before any drag: it wrote the
rendered width, which fixed table-layout had stretched past the configured
one, so a stray click on a column edge silently rewrote and persisted that
column and stopped it adapting to the window. Dragging then ran ahead of the
pointer, because committing one width changes how much leftover space the
layout has to share out and rescales every other column mid-gesture. And the
gesture never ended if the pointer came up outside the window or the user
switched apps — the column kept tracking the pointer on return, with the page
stuck unselectable under a col-resize cursor.

- Require 4px of travel before anything is committed, matching the
  column-reorder sensor's activation distance on the same header.
- Pin every column to its rendered width on the frame the drag starts, so
  there is no leftover left to redistribute and the drag maps 1:1.
- Capture the pointer and end on blur / pointercancel / lostpointercapture,
  the same four-part contract the sidebar rail already uses.
- Own the cursor from a portaled full-viewport layer for the duration of the
  drag. document.body.style.cursor loses to any descendant that declares its
  own, which is why the cursor flickered over rows and text.

Resizing also re-rendered every cell each frame, and each cell carries a
popover, so a frame cost ~143ms. Widths now travel as custom properties
published once on the <table>, and the body is swapped for a memoized copy
while a drag is live — the browser applies each new width with no React work.

Visually the table had no column rules at all, which left the pinned columns'
trailing shadow as the only vertical line and made it read as a stray border
on one column. Every column now carries a rule, the pinned shadow appears
only once the surface is actually scrolled sideways, and the resize handle
brightens its rule to brand rather than matching the faint resting colour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(issues): drag a table column by its header, not by a hidden grip

Reordering columns was advertised by a grip that only appeared once the
pointer was already on the header, and dragging it moved a wrapper the height
of its own line of text while the <th> around it clipped anything that
travelled outside the cell — so the column being moved looked like it had
vanished rather than slid.

The header is now the handle. There is no grip: the cursor carries the
affordance (grab, then grabbing), the whole cell responds, and only the
sort/hide button opts out, since a press there is always the menu. The
wrapper spans the cell's box at all times — the negative margins undo the
<th>'s padding and put it back inside — so what travels is a header-sized
block and going translucent is the entire drag state, matching how a desktop
tab behaves. Overflow is lifted for the length of a reorder and the column in
hand is raised over its neighbours.

Columns are restricted to the horizontal axis, the same constraint the
desktop tab bar puts on tab reordering, which is why packages/views now
declares @dnd-kit/modifiers directly.

Transforming the <th> itself would carry the header's height along for free,
but `transform` on a table cell is a corner of the spec browsers take
liberties with — Chromium lifts the cell out of the table's box model and its
geometry stops matching the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(issues): stop a dragged table column stretching into its neighbour's width

Dragging a column header applied dnd-kit's full transform, which carries
scaleX/scaleY alongside the translation. Those come from its layout animation:
old rect over new rect, tweening an item into the shape of the slot it lands
in. Between two tabs of equal width the ratio is 1 and never shows. Between
two columns it is not — swapping a 174px column with a 96px one stretched the
header to 1.8x on the way across. Only the horizontal translation is applied
now; reordering changes no column's width, so there is no shape for a tween to
describe. The travel and the settle stay animated through `transition`.

The travelling wrapper was also fixed at h-8 while the header strip measures
39px once row borders are counted, leaving the block short at both edges and
misaligned by 3px — which read as the same flattening. Its height is derived
from the cell now.

The reorder grip returns as the drag handle, appearing on hover as before and
staying visible for the length of a drag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): mark the frozen column boundary while the table is scrolled sideways

The edge where the frozen columns end had a shadow drawn inside the pinned
cell with `--border`. That token is tuned for hairlines between adjacent
surfaces — oklch .945 in light mode — and all but disappears once spread into
a shadow, so the boundary read as unmarked while content slid underneath it.
Darkening it in place only made the frozen column look outlined: an inset
shadow can shade that cell's own edge and nothing else, while the depth being
described belongs to the content passing beneath.

Split into the two things MUI X's data grid separates, a permanent border for
where the boundary is and a shadow for something crossing it right now:

- the rule between the frozen columns and the rest stays put, as before;
- a 12px gradient is cast past the frozen block, mounted outside the scroll
  container and shown only while scrolled sideways.

Its position is measured off the DOM — the trailing frozen header carries a
`data-pinned-edge` marker — rather than summed from column sizes. Fixed
table-layout stretches columns past their configured widths whenever the
container is wider than the table's min-width, so a sum lands the marker in
the middle of visible content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(ui): size a table column to its content on double-click

Double-clicking a column's resize handle called column.resetSize, which
cleared the stored width and let TanStack fall back to its generic default of
150 — a number unrelated to any width this table was designed with. "Reset"
therefore widened priority from 130, collapsed labels from 220, and clamped
title to its 260 minimum. It restored nothing.

It now sizes the column to its widest rendered cell, the convention Excel,
Sheets, AG Grid and Notion all share for that gesture.

Fixed table-layout ignores content and the cells truncate their own text, so
nothing on screen reports the width the content wants. The measurement lifts
both constraints across the column's cells, reads them, and restores
everything within the same task, so the browser paints once — after the
restore — and the intermediate layout is never seen. Only the rows inside the
virtual window are measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): stop the table header flickering black during scroll

The sticky header carried backdrop-blur over a translucent fill. A
backdrop-filter on a sticky element with content scrolling beneath it is a
known Chromium compositing fault — the blur layer recomputes its backdrop
every frame, and virtualisation is adding and removing the very rows it reads
from, so the strip flickers black under fast scrolling. Chromium's fix for the
same symptom was reverted, so upgrading does not carry it (electron#12906,
electron#45854, chromium#339841685).

The fill is now the opaque colour that bg-muted/30 was compositing to, which
is the mix the pinned header cells already use. A header the content scrolls
behind has no reason to show it through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(issues): give the table's title column back the width its hover actions held

The sub-issue and rename buttons sat inline in the title cell at opacity 0.
Hidden is not absent: their boxes reserved around 40px of the column at all
times, in the one column with the least room to spare, for controls that only
appear on hover. They are positioned over the cell's trailing edge now, the
way SidebarMenuAction is, with a gradient fading the title running underneath
rather than icons sitting on top of it — the sidebar needs no gradient because
its labels are short, a title runs to the cell's edge. focus-within keeps them
reachable from the keyboard, where hover never fires.

Reordering a column also scrolled the table vertically. Modifiers constrain a
drag's movement but not its auto-scrolling, which reads raw pointer
coordinates, so a few pixels of vertical drift sent the rows moving under a
gesture that cannot act on them. Auto-scroll's y threshold is zero now; x
keeps it, since a table wider than its viewport needs it to reach a distant
slot, but at 0.05 rather than the default 0.2, which arms it a fifth of the
way in from either edge — most of a wide header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(issues): show the table's own grid while its first page loads, and end its columns like every other surface

Two loading-state gaps, both in how the table's non-issue rows are modelled.

A cold load rendered a single "Loading…" line under a full header, which reads
as an empty table rather than a loading one. The surface-level skeleton meant
for it was unreachable: use-issue-surface-data hardcodes isLoading to false for
table, so the `mode === "table"` branch never ran — and it would not have
fitted, drawing rounded bars at p-2 where the table draws an edge-to-edge grid,
so switching it on would have traded one wrong state for a layout jump.

Placeholders are rows now, rendered through the ordinary cell renderer so they
inherit the real column widths, pinning and borders. Everything the table knows
before its data — header, toolbar, column layout — is up immediately, and the
rows swap in without moving anything. The unreachable surface branch is gone.

The end of a column was hand-rolled too, leaving the table the one surface
where a failed page read as muted body text rather than an error, where the
prompt sat left-aligned against three centred ones, and where reaching the end
of a paginated branch said nothing at all. The row carries its state rather
than a finished label and renders through ListLoadMoreFooter, the footer Board,
List and Swimlane already share — which exists, per its own comment, so those
states and their wording stay consistent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): measure table rows instead of assuming they are all one height

Virtualisation sized every row at the same 41px estimate, but the table does
not have one kind of row: group headers are 37px, an end-of-column footer
shorter still, and placeholders different again. Each one put the estimate a
few pixels out, and the error accumulates — with grouping on, the rendered
window drifts off the rows it should be showing and the scrollbar overshoots
the bottom.

Rows report their own height through the virtualizer's measureElement now, and
the estimate only covers rows that have never been mounted. Rows built by
renderRow are cloned to carry data-index and the measuring ref, so callers keep
returning a plain <tr> and still take part.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(issues): scope the title cell's actions to the title cell

The sub-issue and rename buttons keyed off the row's hover state, so pointing
anywhere along a row — a status chip, a date, empty space — put controls that
act on the title under a pointer that was somewhere else. They follow the
title cell's own hover now. The gradient behind them still tracks the row
colour, since that is what the cell is painted with while they are showing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): let group rows report their height, and re-measure the frozen edge on resize

Self-review of the branch turned up two places where a change did not reach as
far as it was supposed to.

Group rows never took part in the row measurement. DataTable clones the
virtualizer's ref and data-index onto whatever renderRow returns, but
IssueTableGroupRow declared only its own three props and absorbed them — and a
group header, being shorter than a data row, is exactly the row the
measurement was added for.

The frozen-column shadow measured its boundary from the scroll handler alone.
Resizing a frozen column while the surface is scrolled sideways moves that
boundary with no scroll event to follow, leaving the shadow behind at the old
position.

Also drops a dependency left behind when the load-more rows stopped building
their own labels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:13:20 +08:00
Jiayuan Zhang
e0e9383efe Improve UI animation transitions (MUL-5172) (#5718)
* Improve UI animation transitions

* Remove implementation plan documents

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 01:38:16 +08:00
Jiayuan Zhang
c6fb2c5c50 feat(agents): assign emoji avatars by default (#5764)
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-22 16:38:50 +08:00
Naiyuan Qing
f4de0948a2 refactor(ui): unify ActorAvatar size tiers + round all avatars & cropper (MUL-4277, MUL-4184) (#5133)
* refactor(ui): converge ActorAvatar size to semantic tiers (MUL-4277)

Replace the free-form numeric `size` on ActorAvatar with a constrained
`AvatarSize` union (xs/sm/md/lg/xl/2xl) so avatar dimensions are chosen by
role instead of ad-hoc pixels. This eliminates the magic-number drift where
the same role rendered at different sizes across pages.

- Add `@multica/ui/lib/avatar-size` (AvatarSize union + AVATAR_SIZE_PX map +
  default tier).
- Base `ActorAvatar` (packages/ui) and business `ActorAvatar`/`AgentStatusDot`
  (packages/views) now take `AvatarSize`; internal font/icon math and the
  presence-dot threshold read px from the map.
- Migrate all web/desktop call sites (packages/ui + packages/views) from
  numeric sizes to tiers using the role table
  (12,14->xs 16,18,20->sm 22,24,28->md 30,32,34->lg 40,44->xl 56,64->2xl).
- Token-ise the derived consumers `AgentAvatarStack` and
  `IssueAgentActivityIndicator` (px looked up internally for overlap/+N math).

Out of scope (per plan): ui/avatar.tsx primitive, account-tab/AvatarPicker,
mobile, and component-name disambiguation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(ui): unify all avatars and the upload cropper to round (MUL-4277, MUL-4184)

main's avatar-shape decision rendered non-human actors (agent, squad,
system) and the workspace logo as rounded squares, and the upload cropper
mirrored that with a square crop window. Per the updated decision
(avatars_and_cropper_round_required), every avatar and the crop UI are now
circular; the square path is removed rather than left as dead config.

- Base ActorAvatar: always rounded-full (drop the isHuman/rounded-md split).
- avatar-crop-dialog: remove the AvatarCropShape/square path; crop window is
  always cropShape="round".
- avatar-upload-control: drop VARIANT_SHAPE; the control is always round and
  no longer threads a shape to the dialog (variant still drives the fallback).
- Strip rounded-md/rounded-none square overrides from agent/squad/member
  ActorAvatar call sites; round the read-only agent/squad static wrappers.
- WorkspaceAvatar: round the org logo so it matches the (now round) workspace
  upload/crop and the shared avatar shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(ui): make round avatar shape a hard invariant (MUL-4277)

Close the two remaining square squad-avatar paths flagged in review and
prevent call sites from re-squaring the avatar:

- base ActorAvatar: keep `rounded-full` as the last class in cn() so a
  call-site `className` can no longer override the circle.
- SquadHeaderAvatar: drop `className="rounded"` (was overriding the base
  circle into a small rounded square).
- SquadsPage no-avatar fallback: route through the shared ActorAvatarBase
  (`isSquad size="lg"`) instead of a hand-written rounded-md tile, so the
  fallback matches the image path — one shape source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(views): round the agent/squad avatar loading skeletons (MUL-4277)

The avatar placeholder skeletons on the agent/squad list, detail, and
profile-card loading states were still rounded squares (rounded-md/lg) from
the pre-round era, so the avatar visibly popped from square to circle on
load — inconsistent with the round avatars and with the member/inbox/issue
skeletons that already use rounded-full.

Round all five: agents-page, agent-detail-page, squads-page,
squad-detail-page, squad-profile-card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-10 08:31:40 +08:00
Bohan Jiang
d6540a1869 fix(clipboard): support copy over http:// via execCommand fallback (#3810)
navigator.clipboard is only exposed in a secure context (https or
localhost). On self-hosted instances served over plain http:// it is
undefined, so every copy / "copy all" / export button silently failed and
left the clipboard empty (GitHub #3781).

Add a shared copyText(text): Promise<boolean> helper in
@multica/ui/lib/clipboard that prefers the async Clipboard API and falls
back to a hidden <textarea> + document.execCommand('copy') for non-secure
contexts. Migrate all direct navigator.clipboard.writeText call sites
(code blocks, agent transcript copy-all, token / webhook / issue-link
copy, etc.) to it, gating success side-effects on the returned boolean,
and remove the now-redundant copyMarkdown wrapper. Secure-context users
keep the native path unchanged.

MUL-3068

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 14:55:23 +08:00
Bohan Jiang
27b473151b refactor(ui): move CODE_LIGATURE_CLASS to zero-dep code-style module (#3463)
Extracts CODE_LIGATURE_CLASS and CODE_LIGATURE_DESCENDANT_CLASS into
packages/ui/lib/code-style.ts. Non-markdown CLI command surfaces
(onboarding/cli-install-instructions, runtimes/connect-remote-dialog)
can now import the class strings without pulling in the shiki +
react-markdown + katex dependency graph via the markdown barrel.

CodeBlock and Markdown continue to consume the constants from the
new module; the markdown barrel no longer re-exports CODE_LIGATURE_CLASS.

MUL-2793

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-28 18:52:09 +08:00
Multica Eve
06880d6ba2 fix: make workspace table columns resizable (#1881)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Jiayuan Zhang <forrestchang7@gmail.com>
2026-04-29 15:23:12 +02:00
Naiyuan Qing
f745a3bbbe feat(agent): presence v3 + execution log + trigger summary (#1823)
* refactor(views): migrate agent/runtime/skill lists to TanStack DataTable

Replace the per-page CSS Grid + minmax(min, fr) + sticky-first-col + truncate
implementation with a TanStack Table backend rendered through a Dice UI-style
DataTable shell. Column widths are now px-based via column.size, so cells
no longer shrink or auto-truncate as the viewport narrows; when the sum of
columns exceeds the viewport, the container scrolls horizontally instead.

- Add @tanstack/react-table to the catalog (8.21.3) and wire it into
  packages/ui (dep) and packages/views (peerDep).
- packages/ui: new DataTable + DataTableColumnHeader + lib/data-table.ts
  (getColumnPinningStyle), adapted from Dice UI's registry. The shell
  renders <table> directly (skipping shadcn's <Table> wrapper) so its own
  outer overflow controls both axes — no nested overflow conflicts.
- packages/views: each list now declares ColumnDef[] with explicit
  cell renderers. Row click navigates to detail via onRowClick (instead of
  wrapping <tr> in <a>, which is invalid HTML); kebab dropdowns
  stopPropagation so they don't trigger the row navigation.
- Drop the previous AGENT_LIST_GRID / GRID_WITH_OWNER / ROW_GRID
  templates and the sticky-first-col / subgrid mechanics that came with
  them. agent-list-item.tsx is removed; runtime-list.tsx and
  skills-page.tsx are trimmed to thin wrappers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent): cap description at 255 chars (db + api + ui)

Symmetric enforcement across DB, server, and UI:

- Migration 060: pre-flight truncate of any oversize rows, then ADD
  CONSTRAINT NOT VALID + VALIDATE CONSTRAINT so the new check doesn't
  block writes during validation.
- Server handler validates utf8.RuneCountInString on Create/Update and
  rejects over-limit input with 400.
- Front-end gets AGENT_DESCRIPTION_MAX_LENGTH in core/agents/constants
  (single source of truth shared by the create dialog + edit modal +
  test suite) and a CharCounter component that warns at 90% and errors
  past the cap.
- Description editor moves from a 288px popover to a roomy modal.
  Editor body is mounted only while the dialog is open, so the local
  draft state is locked in at mount time and never reset by an external
  WS update — the React-recommended replacement for the
  useEffect(reset, [value]) anti-pattern.

Counted in code points everywhere (rune count / spread length /
char_length) so multibyte input agrees across all three layers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(views): data-table polish across runtime + skill lists

Builds on the DataTable migration in 2be0f287:

- Add ColumnMeta.grow flag — declared via TanStack module augmentation
  in ui/lib/data-table.ts. Columns marked meta.grow skip their inline
  width so fixed table-layout assigns them the leftover container space
  (no spacer column). The Title-grows / others-fixed pattern from
  Linear / GitHub PR rows.
- Authoritative table min-width = sum of column.size, applied to the
  <table> itself (fixed-layout ignores cell-level min-width per spec,
  so the floor has to live on the table).
- Header tightens to h-8 + uppercase + tracking-wider; pinned cells
  switch to opaque bg + group-hover so they cover content scrolling
  beneath them and follow row hover state.
- Toolbar slot removed from DataTable (callers wrap the toolbar
  themselves now — keeps DataTable single-purpose).

Also: hover-card popup stops contextmenu / auxclick / dblclick from
bubbling out (in addition to click). Stops the popup from triggering
ancestor handlers (e.g. issue list rows) on right-click / middle-click
without breaking Base UI's outside-click dismiss, which listens to
pointerdown — pointerdown is deliberately NOT stopped.

Runtime + skill list pages updated to use the new sizing model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(agent): drop LastTaskState, introduce 3-state Workload

Continues the presence-model rework started in #1794 / #1798.

The previous LastTaskState union (running / completed / failed /
cancelled / idle) carried historical outcome at the list level — a
runtime-healthy agent whose last task failed showed a sticky red dot
indistinguishable from a daemon-dead agent.

New model: presence is two orthogonal "right-now" dimensions:

  AgentAvailability — runtime reachability only (online / unstable /
                      offline). Drives the dot colour everywhere.
  Workload          — current load (working / queued / idle). Three
                      states, never historical. Failure / completion /
                      cancellation are surfaced via Recent Work + Inbox,
                      not list-level state.

`queued` (= nothing running, ≥1 queued) is an honest "stuck on offline
runtime" signal. To avoid amber flashes during the brief enqueue→claim
race on healthy runtimes, the queued chip composes with availability:
muted on online, warning amber otherwise.

Activity tab cleanup that follows from the new model:
  - failureReasonLabel relocated from agents/presence.ts to
    tabs/task-failure.ts (presence no longer owns historical state).
  - Recent Work paginates (5 initial, +20 per "Show more"); chat-session
    tasks are filtered out of every Agent-scoped surface to keep
    "team work" separate from private chat.
  - Agents page drops the lastTaskFilter chip group; users find broken
    agents via Inbox / Recent Work, not a list-level filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(task): trigger summary snapshot + task:queued lifecycle event

Two task-lifecycle improvements that ship together because they share
the same enqueue/retry hot paths and changes interleave inside task.go:

1. trigger_summary snapshot (migration 061)

   New nullable column on agent_task_queue. Comment-triggered tasks
   snapshot the comment content; autopilot tasks snapshot the run title.
   Truncated to 200 runes via strings.Builder so multibyte input counts
   correctly without O(N²) concatenation. Snapshot survives source
   edits/deletes — every task row self-describes across surfaces (issue
   detail Execution log, agent activity tooltip, inbox) without joining
   back to the originating row.

   Retry rows inherit the parent's snapshot (CreateRetryTask SELECT) so
   the description stays meaningful across attempts. The UI is
   responsible for stacking "Retry #N" context on top.

2. task:queued WS event

   New protocol event covering the ∅ → queued transition. Front-end
   types/events.ts registers it; use-realtime-sync's task: prefix path
   already invalidates task caches via onAny, so old clients without
   this exact-match subscription still refresh correctly. Specific
   subscribers (sticky banner) get sub-second updates instead of
   waiting for daemon claim.

   Retry path now broadcasts task:queued (not task:dispatch) — same
   status transition shape as enqueue, so all "new task created" paths
   agree on one event type.

   Ordering: broadcastTaskEvent runs *before* notifyTaskAvailable so
   the queued event is published into the WS bus before the daemon is
   poked. Without this, a fast daemon could claim and emit task:dispatch
   over the wire before the in-process queued broadcast fan-out reached
   clients — race window is tiny but unsafe-by-construction.

   Per-agent task list (agentTasksKeys.all) and per-issue task list
   (["issues","tasks"]) added to the task: invalidation set so Activity
   tab Recent Work and the Execution log section stay fresh.

Type contracts: AgentTask gains parent_task_id / attempt /
trigger_comment_id (already returned by the API, just missing from TS)
plus the new trigger_summary field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(issue): ExecutionLogSection — unified active+past runs panel

Replaces two pieces:
  - the click-to-expand timeline that lived inside AgentLiveCard
  - the standalone TaskRunHistory below the main content

with a single right-panel section that lists every agent run for the
issue. Active runs sit at the top (always visible when present); past
runs collapse behind a "Show past runs (N)" toggle, sorted failed →
cancelled → completed within group.

Active rows show the trigger summary, status + relative time, and
Cancel / Transcript actions on hover (gradient backdrop fades the
status text rather than hard-clipping). Past rows show the same
shape minus Cancel.

Retry tasks prepend "Retry #N · " to the inherited summary so they're
distinguishable from their parent (which would otherwise share the
exact same trigger text).

Cache key registered as issueKeys.tasks(issueId); the global
useRealtimeSync task: prefix path already invalidates ["issues","tasks"]
on every task lifecycle event, so the section stays fresh without
local WS subscriptions.

AgentLiveCard slims down to a header-only "agent is working" sticky
banner — keeps the at-a-glance "is anyone working on this right now"
signal and the Stop / Transcript actions, drops the inline timeline
that ExecutionLogSection now owns. Subscribes to both task:queued and
task:dispatch so retries (which only emit queued) land in the banner
without waiting for daemon claim.

issue-detail mounts ExecutionLogSection in the right panel and removes
the now-defunct TaskRunHistory call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:50:58 +08:00
Naiyuan Qing
35828492d5 feat(ui): extract packages/ui — shared atomic UI layer
- Move 55 shadcn components → packages/ui/components/ui/
- Move lib/utils.ts (cn function) → packages/ui/lib/
- Move 3 DOM hooks (auto-scroll, mobile, scroll-fade) → packages/ui/hooks/
- Extract CSS design tokens (@theme + :root + .dark) → packages/ui/styles/tokens.css
- Refactor 3 common components to pure-props (actor-avatar, mention-hover-card, reaction-bar)
- Move 6 markdown components with renderMention slot for IssueMentionCard decoupling
- Create wrapper components in apps/web/ for data-aware ActorAvatar and Markdown
- Update 116 import paths across apps/web/
- Add @source directives for Tailwind to scan packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:44:31 +08:00