Surface the Cmd/Ctrl+Enter shortcut as a visible <kbd> next to the
chat and comment send buttons. The tooltip-only hint required hover
discovery, so users pressing bare Enter never learned why nothing
was sent.
Co-authored-by: multica-agent <github@multica.ai>
Two related cleanups to the issue comment/reply/edit composer:
- Drop the trigger-preview "context" copy added in #4147 (chip prefix
`trigger_context_*` and per-context popover titles `trigger_preview_title_*`).
The actual "align context" fix in #4147 was the backend/hook work; the copy
was redundant decoration. Removes the `context` prop, the dead i18n keys
across en/zh/ja/ko, and the corresponding test assertions; the popover title
falls back to the original single `trigger_preview_title`.
- Edit-comment footer: lay the trigger chip on a single row with the action
cluster (📎 Cancel Save) on the right, attachments on their own full-width
row above. The 📎 now sits with the action buttons, matching the new-comment
and reply composers.
- Unify composer buttons on shadcn `Button`: `FileUploadButton` renders a
ghost icon button instead of a hand-rolled circle, and the reply submit
button uses `Button` (icon-xs, ghost-when-empty / primary-when-typed) instead
of a hand-rolled element. Sizes: 📎 and reply submit are both icon-xs (24px).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(issues): render thread replies in chronological order (#3691)
collectThreadReplies walked the parent_id tree depth-first, so an agent
reply forced to nest under its trigger comment rendered before earlier
sibling replies (A-D-B-C instead of A-B-C-D) whenever the agent returned
late. Sort the collected subtree by created_at (id tie-break) so the
thread reads in arrival order — the same order the server already feeds
agents via `comment list --thread` (ListThreadCommentsForIssue).
All other consumers of the array (resolution derivation, fold bars,
counts, deep-link) are order-independent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): rebuild skills list on shared Linear-style list grid
- new ListGrid primitives (subgrid: single source of truth for column tracks)
- skills list: sortable columns, used-by avatar stack, source/creator columns,
row kebab + batch toolbar with add-to-agent and delete
- skill view store in core; addAgentSkills client method; HoverCheck extracted
to views/common (issues header now imports the shared copy)
- locale keys for list actions/filters and the reworked detail page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): rework detail page into overview/files tabs
- tabs directly under the breadcrumb header: overview (default) and files
- overview: identity block + rendered SKILL.md as the main column, right
rail with metadata card (source/creator/updated, inline name+description
edit toggle) and used-by panel with bind/unbind
- files: file tree + viewer/editor unchanged; SKILL.md "edit" jumps here
- header kebab menu (copy skill ID, delete); page-level save bar shared by
both tabs; tab state persisted in ?tab=
- file tree: ARIA tree roles + roving-tabindex keyboard navigation
- drop the old right sidebar (metadata dl, permissions paragraph)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* revert(skills): restore detail page to main, keep branch list-only
Drop the overview/files tabs rework from this branch so the PR scope is
the list rebuild only. skill-detail-page.tsx and file-tree.tsx are back
to the main versions; the locale detail/file_tree sections are restored
to match. The detail rework is preserved on stash/skills-detail-tabs
for a follow-up PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): drop description column from skills list
Description is agent-facing routing metadata, not a scannable list
property — Linear's display options expose no description column for
the same reason. Removes the cell, column key, display toggle, lg grid
track, skeleton cells, and the now-dead table.description /
table.no_description locale keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): drive list column hiding by container width, drop by priority
Replace viewport sm:/lg: breakpoints with Tailwind v4 container query
variants (@2xl/@4xl) on the list wrapper, so an open sidebar or split
pane narrows the column set instead of squashing tracks. Remove the
min-w-fit + overflow-x-auto horizontal-scroll fallback: when space runs
out, low-priority columns (created/source/creator, then updated) drop
and return as the container widens; name and usedBy never drop. ListGrid
conventions comment updated — this is the template for all list pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): virtualize list rows with @tanstack/react-virtual
Linear-style headless virtualization: the virtualizer computes the
visible index range and offsets; offsets land as padding on the
scrolling ListGridBody so mounted rows stay direct subgrid children and
column alignment is untouched. Fixed 48px rows skip per-row measurement.
Hideable column tracks move from max-content to deterministic widths
(CSS vars) — with only the visible slice mounted, content-driven tracks
would resize during scroll. A user-hidden column zeroes its var so the
track still collapses; per-cell max-w caps move into the tracks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills): list tiers must fit their container trigger width
The @4xl tier's track sum (~1080px with gaps) exceeded its 896px
trigger; with the horizontal-scroll fallback gone, the right-side
columns were clipped unreachably between 896-1080px. Move tier 3 to
@5xl (1024px), trim usedBy/source/creator tracks, and document the
fit invariant with its arithmetic next to the template and in the
ListGrid conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): show description as subtext under the skill name
Lives in the name track as a second truncated line (max-w 36rem,
title attr for the full text) — no track, no header, no slot in the
responsive arithmetic. Both lines fit the fixed 48px row, so the
virtualizer contract is untouched; rows without a description center
the name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Revert "feat(skills): show description as subtext under the skill name"
This reverts commit f39721301b.
* fix(skills): anchor batch toolbar to the page, not the viewport
fixed bottom-6 left-1/2 centered the bar on the window; with the
sidebar open the list's visual center sits ~120px right of the window
center, so the bar looked off-center (worse with desktop split panes).
Page root becomes the positioning context (relative) and the bar uses
absolute — same rule applies to future list pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): show matching count next to search while list is narrowed
"n / total" appears right of the search box only when search or
filters are active — idle state would duplicate the total already in
the page header.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(autopilots): derive trigger kinds, next run, last run status in list
The list endpoint only selected the autopilot table, so the list UI
could not answer "is this automation working" without N+1 detail
calls. Each list row now carries trigger_kinds + next_run_at (enabled
triggers only — the columns describe how it fires today) and
last_run_status (most recent run). Fields are omitempty and absent
from detail/create/update responses; clients must treat them as
optional per the API compatibility rules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(autopilots): list schema, parsed client, and view store in core
- listAutopilots now runs through parseWithFallback with a zod schema
(this endpoint was a bare fetch — overdue per the API compatibility
rules); malformed bodies degrade to an empty list, old-server rows
without assignee_type or the new derived fields parse cleanly, and
enum drift passes through as plain strings
- Autopilot type gains the three optional list-only derived fields
- New autopilots view store (scope/sort/columns/filters, persisted per
workspace): status is the promoted scope dimension so it does NOT
appear in filters — one dimension lives in exactly one place
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(autopilots): rebuild list on shared ListGrid with scope buttons
Same skeleton as the skills list (container-query tiers, deterministic
var-width tracks with documented fit arithmetic, virtualized 48px rows,
sortable headers, filter + display toolbar, page-anchored batch
toolbar), plus the autopilots-specific pieces:
- Status is the promoted SCOPE dimension: 全部/运行中/已暂停/已归档
segmented buttons with full-set counts; "all" = active+paused
(archived gets its own visible home, Linear archive semantics);
status is therefore absent from the filter dropdown
- Columns: name (paused marker inline), assignee (agent/squad),
trigger kind badges, last run (outcome dot + time, enum-drift safe
default), next run; mode/creator/created opt-in hidden
- Filters: assignee, trigger kind, mode, creator (composite type:id
values for polymorphic actors); sort name/lastRun/nextRun/created
with lastRun desc default
- Row kebab (pause/resume/archive/unarchive/delete) and batch toolbar
share one delete dialog; status changes ride useUpdateAutopilot's
optimistic cache
- Fix noUncheckedIndexedAccess errors the branch had never typechecked
(skills virtual rows, UsedByCell, added_toast)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(autopilots): scope buttons follow the issues header pattern
Replace the bespoke segmented-pill control with the existing scope
button convention from the issues page: outline buttons with bg-accent
active state on md+, collapsing to a radio dropdown below md. Counts
stay (stage inventories from the full set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills,autopilots): toolbar small-screen treatment follows issues header
Below md: the search box (and its result count) disappear entirely,
and the filter/display controls collapse to square icon-only buttons
(labels and the clear-X are md+), matching the issues header's
responsive pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills,autopilots): two-zone columns — WYSIWYG with scroll escape valve
Static width tiers silently hid user-enabled columns (toggle on,
nothing appears — autopilots' mode/creator/created sat behind a 1280px
container gate no laptop reaches; skills' source/created behind
1024px). Tiers can't know how many columns are enabled, so the
mechanism is replaced, not retuned:
- ≥@2xl container: every enabled column renders; the grid carries
min-width = Σ(enabled tracks + gaps) (pure constants, no
measurement) and the wrapper scrolls horizontally only when the
enabled set outgrows the container
- <@2xl: static core set (skills: name+usedBy; autopilots:
name+assignee), no scroll, toggles don't apply
Per-tier templates and the hand-maintained fit arithmetic retire;
ListGrid conventions updated accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills,autopilots): widen name column minimums (120px base, 200px wide)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(autopilots): drop the archived scope and the list search box
Archiving never existed as a UI flow (the DB status value is only
reachable via direct API; the detail page disables its switch when
archived), so the list stops inventing it: no archived scope, no
archive/unarchive row or batch actions. API-archived rows are excluded
everywhere; a persisted retired scope value falls back to "all".
The search box goes too — scope buttons already partition the small
set, search is redundant (product call). Skills keeps its search (no
scope there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills,autopilots): quiet outline create buttons in page headers
Page-header chrome shouldn't carry the loudest element on the page:
the create button becomes outline with text on md+ and collapses to a
square plus icon below md (same responsive treatment as the toolbar
controls). Primary stays reserved for empty-state CTAs. Agents follows
when its list migrates to ListGrid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(agents): rebuild list on shared ListGrid with identity rows
Same skeleton as the skills/autopilots lists (two-zone container
responsiveness, deterministic var tracks + min-width scroll escape
valve, virtualized fixed-height rows, issues-style scope buttons,
page-anchored batch toolbar, quiet outline create button), plus the
agents-specific decisions:
- Identity rows: the documented exception to the single-line rule —
avatar + name + description two-line cells, 64px rows (agents are
few, identity-rich entities); the italic "no description"
placeholder is gone, empty descriptions just center the name
- Scope: Mine (historical default) | All | Archived with full-set
counts; archived ignores the ownership lens; no search box
- The 7d sparkline column is replaced by a sortable "Last active"
column derived from the same 30-day activity buckets (zero API
change) — per-row-normalized mini bars can't be compared across
rows, and the default sort finally has a visible anchor; the
detailed histogram stays on the hover card / detail page
- Workload folds into the status cell ("Online · 2 tasks") — a 0-2
integer doesn't earn a column
- Columns: status, runtime, last active, runs (30d); model/created
opt-in hidden; filters: availability, runtime
- Operations unchanged: row kebab reuses AgentRowActions
(cancel-tasks/duplicate/archive/restore with permissions); batch
archive (confirmed) + restore; no delete — the API has none
- View store extended (scope incl. archived, sort, columns, filters);
agent-columns.tsx (DataTable columns) deleted
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): trim status track to its real worst case (160 -> 144px)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(runtimes): machine detail's runtime table on the shared ListGrid
The master-detail console keeps its shape (machines are few and
strongly categorized; left list, charts, update section untouched) —
only the right pane's runtimes table moves from TanStack DataTable to
the ListGrid family, taking the paradigm pieces that earn their keep
at 1-5 rows: subgrid template + var tracks, two-zone container
responsiveness (the pane is squeezed by the machine list, so the
core-set collapse below @2xl matters more here than on full-width
pages), min-width scroll escape valve, shared header/row/hover visual
language. Deliberately NOT taken: virtualization, sorting, filters,
column toggles, and batch selection — dead weight at this row count,
and batch-deleting runtimes (a cascade-confirm operation) is unsafe
by design.
Workload folds into the health cell ("Online · Working 2") like the
agents status cell; the owner column keeps its only-when-multiple-
owners rule via a zeroed track var. runtime-columns.tsx is deleted;
the row-menu/CLI tests render the exported cells directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(runtimes): collapse the kebab track when no row has actions
On a healthy local machine every row's only action (delete) is hidden
by the self-healing rule, leaving a permanent ~64px dead zone after
the CLI column. The action track now follows the owner column's
conditional-var mechanism: zeroed unless at least one row will show
the menu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(runtimes): drop doubled header border, align create button with convention
PageHeader already carries border-b; the content wrappers' border-t
stacked a second line right under it (the only list page doing this).
"Add a computer" follows the chrome-button convention: outline with
text on md+, square plus icon below md — primary stays reserved for
the empty state CTA.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(runtimes): health cell load suffix matches the agents status cell
"Healthy · 2 tasks" instead of the old workload vocabulary
("Working 2 +1q") — the count is unit-bearing and both surfaces now
speak one language. The queued-anomaly distinction the old words
hinted at belongs to the health layer if it ever earns surfacing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(lists): pin overflow-y-hidden on the horizontal-scroll wrappers
CSS coerces overflow-x:auto into overflow:auto on both axes, which
silently armed the list wrappers with a vertical scrollbar they were
never meant to have. Combined with the h-full grid's percentage
resolution across scrollbar-induced reflows, the wrapper's vertical
bar and horizontal bar fed each other in a non-converging layout loop
(visible as two stacked, flickering scrollbars on the agents list —
the same latent loop exists in all four wrappers; agents' wider
min-width and 64px rows just hit the trigger zone first). Vertical
scrolling belongs solely to ListGridBody; declare overflow-y-hidden
explicitly to break the loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): single scroll container for the list (trial before rollout)
Both scroll axes move to the outer wrapper; the grid drops h-full and
the rows wrapper drops its own overflow. Kills the percentage-height
bridge between the two scroll elements that fed the flickering double
scrollbars and clipped the last row under the horizontal scrollbar.
Sticky header pins inside the scroller; vertical scrollbar now spans
the full pane (Linear's structure). Skills/autopilots follow after
visual confirmation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(lists): roll single scroll container out to skills/autopilots, add bottom clearance
ListGridBody retires its own scrolling entirely (the agents trial
confirmed the structure): both axes live on the single outer wrapper,
grids drop the h-full percentage bridge, virtualizers point at the
wrapper. The rows wrapper gains LIST_GRID_BOTTOM_CLEARANCE (64px)
appended to the virtualization padding so the last row scrolls clear
of the chat FAB (~48px at bottom-right) and the batch toolbar (~62px).
Runtimes' machine table is untouched: content-height at the top of a
tall pane, no bridge and no practical FAB overlap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(squads): rebuild list on shared ListGrid (identity rows, minimal)
The last list joins the family. Squads are the fewest entity (1-5 rows),
so this is the agents identity-row shell on the runtime-list minimal
skeleton: ListGrid subgrid + var tracks + two-zone responsiveness +
single scroll container, but NO virtualization, checkbox, or batch.
- Identity two-line rows (squad avatar + name + description, 64px) like
agents; columns: name / leader / members (polymorphic ActorAvatar
stack from member_preview), creator + created opt-in hidden
- Scope Mine/All (creator-based, issues-header styling, <md dropdown);
no archived scope (list API hard-filters archived + no restore
endpoint), no search (scope-bearing), no filters (set too small)
- Sort name (default) / members / created
- Row kebab = Archive (= the delete endpoint, which archives + transfers
issues/autopilots to the leader); workspace owner/admin only, so the
kebab track collapses for non-admins. Reuses the existing
archive_dialog copy. No batch.
- View store extended (scope + sort + columns); zero API change — pure
frontend (member_preview/count already in the list payload)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(agents,squads): owner/created-by columns + owner filter
Surface ownership as a real column on both lists, named by what the
field actually means in each permission model:
- Agents: "Owner" — owner_id is the creator (set at creation, never
transferred) and carries management rights. Promoted to a default-
visible column (avatar + name); the half-baked inline owner avatar in
the name cell is removed ("You" badge stays).
- Squads: "Created by" (NOT Owner) — creator_id holds no rights
(archiving is workspace-admin only), so Owner would mislead. Now a
default-visible column with avatar + name.
Agents also gains an Owner filter, kept orthogonal to the Mine scope by
the single-axis rule: "Mine" is the clean no-filter personal view, so
applying any filter (owner or otherwise) leaves Mine for All, and
clicking Mine clears all filters. Owner and Mine therefore never
coexist — no "mine + owner=someone-else = empty" contradiction. Squads
keep the plain Mine/All toggle (too few rows for a creator filter).
Both lists keep a Created (date) column, opt-in hidden.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): backfill new filter dimensions on rehydrate (owners crash)
A view payload persisted before the owners filter existed overwrote the
default filters wholesale on rehydrate, dropping filters.owners to
undefined and crashing the list's filter predicate (.length on
undefined). The store merge now deep-merges filters over
EMPTY_AGENT_FILTERS so newly-added dimensions always get their default.
Regression test added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills,autopilots): deep-merge filters on rehydrate too
Same latent crash the agents store just hit: the copied view-store
merge spread persisted.filters wholesale, so adding a new filter
dimension later would drop it to undefined for users with older
persisted state. Harden skills and autopilots the same way (merge over
their EMPTY_*_FILTERS) before that bug can ship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(projects): rebuild table view on ListGrid + filters + pin/delete kebab
Projects is the dual-view list: the compact table moves onto the shared
ListGrid (subgrid tracks, two-zone responsiveness, single scroll
container, FAB bottom clearance) while the comfortable card grid stays
as the alternate view, toggled by a restyled view switch (Table/Cards
outline buttons, active = bg-accent). Inline editing is preserved —
rows are NOT whole-row links; the name navigates and status/priority/
lead stay click-to-edit (matching prior behaviour, no navigate-vs-edit
conflict).
- View store extended: viewMode + sort (name/priority/status/progress/
created) + hidden columns + filters (status/priority/lead); merge
deep-merges filters (migration-safe). No scope (lead optional/often
an agent; status is a 5-value lifecycle → filter, not scope).
- Toolbar: search (kept — scopeless list) + result count + Filter
(status/priority/lead) + Display (sort+columns, table view only).
- Row kebab: Pin/Unpin (any member, reuses the existing project pin
API — zero new endpoints) + Delete (workspace admin). Pin is the
flexible per-user favourite the list previously lacked.
- Zero API change; status/priority filtering is client-side like the
other lists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(projects): GRID_COLS must be a literal string (Tailwind can't see interpolation)
The table view's grid-cols template interpolated ${STATUS_WIDTH}px, so
Tailwind never generated the arbitrary-value class — the grid collapsed
to one column and every cell stacked vertically. Inline the literal
116px. This is the documented ListGrid rule (keep the class literal so
Tailwind scans it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(projects): single view-toggle button, decouple Display from view mode
Two fixes from the same principle — view mode is pure presentation and
must not couple to anything:
- The view switch is now ONE button that flips table ⇄ cards (shows the
current view's icon+label, tooltip names the target), instead of two
side-by-side buttons.
- The Display (sort/columns) control no longer disappears when you
switch to cards — it was gated on isCompact, so flipping the view
made it vanish (the "filter gone after switching" weirdness). It's
always present now; only the columns *section* inside the popover is
table-only (cards have no columns). Sort applies to both views.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(projects,squads): projects multi-select + squads FAB clearance/toast
Cross-list consistency audit fixes:
- projects: add multi-select (checkbox column + select-all header +
page-anchored batch toolbar) — it's a dozens-scale full-page list
like skills/autopilots/agents but was the only one missing it. Batch
ops: Pin all (any member) + Delete (workspace admin). Table view
only (cards have no checkboxes). GRID template + min-width updated
for the checkbox track.
- squads: add the FAB bottom clearance the other full-page lists have
(last row/kebab was sliding under the chat FAB).
- squads: archive success toast was showing the dialog's question
title ("Archive this squad?"); use a proper "Squad archived" key.
Intentional and left as-is (documented): squads/runtimes have no
multi-select/virtualization (1-5 rows); projects table isn't
virtualized yet (dual-view + card grid; tracked as low-risk debt).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(agents,squads): close the filter/column consistency gaps
Apply the principle "every categorical column is filterable" where it
was missing:
- agents: add a Model filter (model was a categorical column with no
filter). Distinct non-empty models from the in-scope rows.
- squads: add filters entirely (it had leader/creator columns + a
column-toggle panel but no Filter button — the only such outlier).
Leader (agent) + Creator (member) filters, with the result count and
the same Filter dropdown shape as the other lists. Store gains
SquadListFilters + toggleFilter/clearFilters + migration-safe
filters deep-merge.
autopilots creator stays default-hidden per product call (not every
"who made it" must be visible). Filter stores' partialize tests
updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(autopilots): match list-page root to flex-1 convention
skills/agents/projects roots use `relative flex flex-1 min-h-0 flex-col`;
autopilots used `h-full`. Both anchor the batch toolbar correctly, but
align the flex sizing for consistency across the six list surfaces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: flush issue description editor on close
Co-authored-by: multica-agent <github@multica.ai>
* fix: make unmount flush opt-in via flushPendingOnUnmount
The unconditional unmount flush re-emitted discarded content into
composers that clear their draft and then unmount (comment edit cancel,
create-issue / feedback submit), resurrecting the cleared draft.
- Add flushPendingOnUnmount prop (default false); only the issue-detail
description editor opts in.
- Cache the pending markdown in a ref at onUpdate time and emit that
cached copy on unmount, instead of reading the editor instance during
teardown.
- Regression tests: default drops the pending update on unmount, opt-in
flush emits the cached value even when the editor is already
destroyed, no double-emit after the debounce fired, and issue-detail
pins the opt-in wiring.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The comment card exposed two identical add-reaction affordances: a
QuickEmojiPicker in the header's top-right actions and the add button
inside the bottom ReactionBar. Keep only the bottom one.
- Drop QuickEmojiPicker from the root header and reply-row headers
- Always show the ReactionBar add button (it is the only entry point
now), removing the isLongContent gating
- Remove the now-unused hideAddButton prop from ReactionBar
MUL-3262
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
collectThreadReplies walked the parent_id tree depth-first, so an agent
reply forced to nest under its trigger comment rendered before earlier
sibling replies (A-D-B-C instead of A-B-C-D) whenever the agent returned
late. Sort the collected subtree by created_at (id tie-break) so the
thread reads in arrival order — the same order the server already feeds
agents via `comment list --thread` (ListThreadCommentsForIssue).
All other consumers of the array (resolution derivation, fold bars,
counts, deep-link) are order-independent.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The preview answer depends on live queue state (pending-task dedup), not
just the mention set, so three staleness bugs showed up around it:
- staleTime: Infinity pinned a "nobody triggers" snapshot taken while
the mentioned agent was still queued — the chip never appeared even
though sending really did wake the agent (create recomputes).
-> staleTime: 0, cached signatures revalidate in the background.
- The in-flight gap on a signature change rendered as an empty agent
list, flickering the chips and wiping the composer's suppressed-id
set via the pruning effect. -> placeholderData: keepPreviousData.
- Nothing refreshed an open composer when an agent's task finished.
-> the WS task-lifecycle handler now also invalidates the
commentTriggerPreviewAll prefix, so chips appear mid-typing the
moment the agent becomes triggerable again.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Five chip states get distinct copy instead of sharing one sentence and a
vague "not this time":
- single, will trigger: Starts working when sent (unchanged)
- single, skipped: Won't be triggered
- several, k will fire: {{count}} agents start working when sent —
the count covers only non-suppressed agents; skipped ones read as the
dimmed heads in the stack next to the number
- several, all skipped: No agents will be triggered
- popover row state: Skipped
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Copy: one fixed sentence for single and stacked chips — the avatar(s)
carry who and how many, the text carries condition + outcome
("发送后开始工作" / "Starts working when sent"), killing the
"is it already running?" misread. Drops the per-name and count keys.
- Color: sidebar-style resting state — muted-foreground until hover so
the strip reads as metadata, not content.
- Motion: pure fade-in (no slide offset).
- Spacing: reply composer reserves pb-9 so the chip strip reads as a
footer instead of a second content line glued to the text.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The issue description editor bound pending uploads with
`md.includes(a.url)`, but the editor persists the durable markdownLink
(`/api/attachments/<id>/download` / markdown_url), never the raw storage
`a.url`. The filter therefore never matched, so description uploads were
never linked via `attachment_ids`.
After reload the attachment was absent from `issueAttachments`, so the
renderer could not resolve it to a freshly-signed CDN `download_url` and
fell back to the persisted auth-gated download endpoint. That endpoint
loads on web (same-site cookie / proxy) but fails as a native <img> on
Desktop/Electron (cross-origin file:// renderer carries no auth), leaving
the image broken — while comments rendered fine because they already bind
via contentReferencesAttachment.
Switch the description binding to contentReferencesAttachment, matching
the comment/reply/chat composers, so description images resolve to the
signed CDN URL on every client. Add a regression test pinning the
absolute-host markdown_url shape.
* Add comment trigger preview suppression
Co-authored-by: multica-agent <github@multica.ai>
* Use TanStack Query for trigger preview
Co-authored-by: multica-agent <github@multica.ai>
* Test note comments skip create triggers
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): redesign comment trigger chips as avatar chips
Single agent renders as avatar + presence dot + full sentence; several
agents collapse to an overlapping stack + active count, mirroring the
header working chip. Per-agent skip moves into a click-opened popover
(hover layers stay read-only tooltips); suppression reads as brightness,
not a ban glyph. Loading and preview errors render nothing.
Also: share one tooltip body across chip and popover rows, invalidate
cached previews after a comment lands (the enqueued task changes the
dedup answer), move the preview query key into issueKeys, and drop the
now-unconsumed status field from useCommentTriggerPreview.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(server): drop comment trigger wrappers kept only for tests
enqueueMentionedAgentTasks and shouldEnqueueSquadLeaderOnComment had no
production callers after the compute/enqueue split — the comment path
goes through computeCommentAgentTriggers. Tests now exercise the compute
functions directly via package-local helpers, so the legacy adapters
cannot drift from the real path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(skills): sync mentioning/squads source maps with shared trigger computation
The squads source map still pointed the comment-trigger contract at the
pre-refactor call chain (comment.go:940 -> shouldEnqueueSquadLeaderOnComment),
and the mentioning skill referenced the deleted wrapper. Re-anchor both
to computeCommentAgentTriggers / computeAssignedSquadLeaderCommentTrigger
/ computeMentionedAgentCommentTriggers with current line numbers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Comment / issue / chat images uploaded inside the Desktop app rendered
as the broken-image fallback. The editor was persisting a site-relative
`/api/attachments/<id>/download` URL into markdown — that path only
resolves when the document origin proxies /api to the API host (apps/web
via Next.js rewrite). On Electron's file:// origin it never resolved.
Per GPT-Boy's plan, move the durable-URL choice from the client to the
server so the persisted shape is correct regardless of which client
performed the upload.
Server:
- AttachmentResponse gains a markdown_url field, computed by
buildMarkdownURL from the deployment policy:
• storage URL is already absolute + unsigned (public CDN, S3 public
bucket, LocalStorage with MULTICA_LOCAL_UPLOAD_BASE_URL on https) →
use it verbatim;
• CloudFront-signed mode → never expose the raw S3 URL (private
bucket); return cfg.PublicURL + /api/attachments/<id>/download so
the server can re-sign on every request;
• LocalStorage relative + cfg.PublicURL set → same prefixed API
endpoint;
• cfg.PublicURL unset → fall back to site-relative path so web's
Next.js rewrite still works.
- isDurablePublicURL helper rejects URLs carrying CloudFront / S3
signature query params, so a freshly-signed download_url can never
leak into persistence — the original MUL-3130 bug stays closed.
Frontend:
- Attachment type + AttachmentResponseSchema (and apps/mobile mirror)
carry markdown_url. Schema lenient-defaults to '' so a backend old
enough to predate this field doesn't break clients.
- useFileUpload picks markdownLink with three-layer fallback:
(1) att.markdown_url (modern server),
(2) attachmentDownloadPath(att.id) — legacy site-relative shape,
retained for backends old enough to omit markdown_url,
(3) att.url — no-workspace avatar branch with no attachment-row id.
- attachment.tsx keeps the relative→absolute absolutize pass, but
reframed as the legacy-compat fallback for already-persisted
/api/attachments/<id>/download or /uploads/<key> URLs in old
bodies. New content writes absolute URLs and skips this path.
- ContentEditor still tracks freshly-uploaded records into
AttachmentDownloadProvider so Quick Create's editor can swap the URL
via the resolver during the same session even before the server-side
binding lands.
Tests:
- server/internal/handler/file_test.go: 5 new buildMarkdownURL matrix
tests (public CDN passthrough, CloudFront-signed swap, relative
prefixing, PublicURL unset fallback, trailing-slash strip) + 15
table-driven isDurablePublicURL cases.
- packages/core/hooks/use-file-upload.test.ts: new file, 4 cases
covering modern server / legacy server / no-id avatar / oversize.
- packages/views/editor/attachment.test.tsx + content-editor.test.tsx:
10 cases for the absolutize matrix and in-session attachment merge.
- 6 existing test fixtures updated to include markdown_url.
Verification: 1236 @multica/views tests pass; 514 @multica/core tests
pass (4 new); server handler package tests pass for the new matrix
plus all pre-existing TestAttachmentToResponse* and TestDownload*
cases. Typecheck green for views/core/web/desktop. Lint clean on
touched files.
Quick Create attachment_ids binding (orphaned attachment relationship
on the resulting issue) is a follow-up — it requires a new --attachment-id
CLI flag and daemon prompt-template work and is intentionally scoped
out of this PR.
Refs: MUL-3192
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): per-comment thread resolution with sticky collapse
Allow resolving any comment, not just roots. Resolving a root folds the
whole thread into one bar (existing); resolving a reply marks it as the
thread's resolution ("Resolve thread with comment") and folds the other
replies behind a "N comments" bar, with the resolution kept visible and
badged. Which comment is the resolution is a pure frontend derivation
(root wins, else latest resolved reply), so no write-side bookkeeping is
needed and any resolved_at combination renders one resolution.
- backend: drop the "only root comments can be resolved" guard
- views: deriveThreadResolution + reply-resolution rendering, sticky
collapse/fold bars (overflow-clip on the card so sticky resolves to the
timeline scroll parent), scroll the folded thread back into view on
collapse, ListChevronsDownUp icon, locales (en/ja/ko/zh-Hans)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): sticky comment headers for long comments
Pin each comment's header (root + replies) to the timeline's scroll
parent while reading, so a long comment keeps its author + actions
visible instead of scrolling out of reach. Exactly one header is pinned
at a time:
- Reply headers stick within their own CommentRow box (release at the
reply's end).
- The root header is wrapped in a root-section container so its sticky
containing block spans only the header + root body — without it the
containing block is the whole thread and the root header stays stuck
behind every reply. Replies render outside the wrapper, gated on open.
- Skip the root header sticky whenever a resolution collapse bar already
owns the top-0 slot (root resolved+expanded, or reply-resolution
expanded) to avoid two bars stacking at the same offset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening an inbox comment notification on an issue with a running agent
shoved the whole desktop page — header included — off the top, and no
amount of scrolling brought it back; only toggling the right sidebar
(which reflows the panel group) restored it.
Root cause: the deep-link landing uses native
scrollIntoView({block:"center"}) on the target comment. Native
scrollIntoView is spec'd to scroll EVERY scrollable ancestor. On a cold
mount where the timeline is still streaming (is-working) and the sidebar
panel starts collapsed, the inner timeline scroller can't center the
target on its own, so the scroll propagates up and scrolls the desktop
shell's overflow:hidden wrapper (desktop-layout.tsx). That wrapper has no
scrollbar and doesn't auto-clamp, so the page stays shoved up until a
resize reflows it. Desktop-only: web sits in a document-scroll context
with a real scrollbar that self-corrects.
Fix: drive the timeline container's scrollTop directly and re-center
across rAF frames until async heights settle, instead of leaning on the
ancestor scroll. The scroll never touches an ancestor, so the header can
no longer be pushed off-screen.
Tests assert the user-facing contract (lands on + highlights the target
comment) rather than the scroll mechanism, which jsdom can't lay out.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* MUL-3130: persist a stable attachment download URL in comment markdown
Comment image attachments rendered as broken placeholders ~30 minutes
after upload because the editor was persisting a short-lived
HMAC-signed URL into the comment body. After PR #3903 (MUL-3132)
hardened /uploads/* with auth, `attachmentToResponse` started signing
`attachment.url` as `/uploads/<key>?exp=<unix>&sig=<HMAC>` for
LocalStorage so token-auth clients could keep loading inline images.
The signature has a 30-min TTL by design — but `useFileUpload` was
returning that signed value as `link` and the editor was writing
`` straight into the markdown, so the comment
permanently captured a URL that stopped working as soon as the
signature expired.
The fix is to persist a stable per-attachment URL that the server can
re-sign on every request:
* `useFileUpload` now returns `link = /api/attachments/<id>/download`
(avatar uploads without an id still fall back to `att.url` so the
pre-attachment-row code paths keep working).
* `DownloadAttachment` self-resolves the workspace from the attachment
row instead of reading X-Workspace-Slug / X-Workspace-ID headers,
and the route is registered under the auth-only group so a native
browser <img>/<video> resource load (which cannot attach those
headers) succeeds. Membership is checked inside the handler with
a 404 deny shape so the route does not act as an IDOR oracle.
* A new `GetAttachmentByIDOnly` SQL query supports the workspace-
derivation step.
* `AttachmentDownloadProvider` now extracts the attachment id from
the stable URL when matching markdown refs to attachment records,
with a fallback to the existing url-equality check for legacy
comments (and S3/CloudFront markdown that points straight at the
CDN).
* `contentReferencesAttachment` covers both URL shapes for the
composer / standalone-list dedup paths so an attachment uploaded
before the fix and one uploaded after both deduplicate cleanly.
Tests:
- New unit tests for the URL helpers (16 tests, packages/core).
- Backend regression test: bare `<img src>`-style request without
workspace headers now succeeds for a member (200) and 404s for a
non-member, replacing the previous "400 without workspace context"
contract.
- Existing TestDownload*, TestServeLocalUpload*, TestAttachmentTo
Response* and the 1220 frontend views tests all pass.
Refs: MUL-3130, GitHub issue #3891
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3130: address PR review — split markdown link from upload link, swap render src
Two follow-ups from GPT-Boy's review on PR #3937.
(1) Don't reroute every upload consumer through the workspace-gated
download endpoint.
The previous change made `useFileUpload`'s `link` field unconditionally
return `/api/attachments/<id>/download` whenever the upload had an id.
But `useFileUpload` is also used by avatar / logo pickers
(account-tab, workspace-tab, agents/avatar-picker, squads/squad-detail-page)
that persist `result.link` directly into `avatar_url`. Avatars are
referenced cross-workspace (mention chips, member lists, inbox
items), so binding their URL to a workspace-membership-gated
endpoint would silently break cross-workspace avatar visibility.
The fix splits the URL into two semantically distinct fields:
- `link` — same as `att.url` (legacy contract). Avatar /
logo callers continue to use this and remain
on whatever URL semantics the storage backend
dictates.
- `markdownLink` — the stable per-attachment URL
`/api/attachments/<id>/download`. Only the
editor's markdown-persisting flow consumes
this. Falls back to `link` for the
no-workspace upload branch (where there is
no attachment-row id to address).
`editor/extensions/file-upload.ts` switches `image.src` and
`fileCard.href` to `markdownLink ?? link` so comment markdown gets
the stable shape while avatar callers stay on `link` unchanged.
(2) Make the render-time img src loadable for token-mode clients.
Persisting the stable `/api/attachments/<id>/download` URL fixes the
expiry problem but the path itself sits behind `middleware.Auth`,
which expects either a `multica_auth` cookie or a Bearer token in
`Authorization`. Native `<img>`/`<video>` resource loads from
token-mode clients (Electron's default mode, the mobile app,
legacy-token web sessions) cannot attach the Authorization header,
so the bare URL would 401 immediately rather than 30 minutes later.
`Attachment.normalize` now runs the resolved record through a new
`pickInlineMediaURL` helper that returns:
- `record.download_url` when it's an absolute URL with a
recognised CDN signature query (CloudFront-signed
`Signature` / `Expires` / `Key-Pair-Id`, or
`X-Amz-Signature` for raw S3 presigns) — these load as
native resource src in any client.
- else `record.url`, which on the LocalStorage backend carries
a freshly-minted `/uploads/<key>?exp&sig` query whose
signature IS the auth (token-mode-loadable). On non-CF S3
backends this is the raw stored URL — same behaviour as
today.
- else the original input URL (legacy / unresolved markdown
keeps its existing path).
This gives the same effect for both `kind: "record"` and
`kind: "url"` attachment inputs: once a record is in hand, the
rendered media src is whichever URL the current backend exposes
a working signature on.
Tests:
- New `file-upload.test.ts` regression pinning that `markdownLink`
is what lands in the markdown body when the upload result returns
both a short-lived storage URL and a stable download path.
- Updated `attachment.test.tsx` to reflect the new render-time
swap (the rendered img src now follows the freshly signed URL,
not the raw storage URL) and added a record-mode regression
pinning the LocalStorage default — when `download_url` is the
bare /api/attachments/<id>/download path, the renderer must fall
through to the signed `record.url`.
- Updated `chat-input.test.tsx` makeUpload helper for the new
`markdownLink` UploadResult field.
- 1222 frontend views tests + 507 core tests + typecheck across
@multica/{core,ui,views} all pass.
Refs: MUL-3130, GitHub issue #3891. Builds on a740f7a35.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3130: chat upload map keys on persisted markdownLink, not the short-lived link
GPT-Boy's second-round review on PR #3937 caught a chat-only blocker
left over from the previous fix.
After the previous commit split `UploadResult.link` into `link`
(legacy avatar/logo URL) and `markdownLink` (stable per-attachment
URL persisted into markdown), the comment editor's image src + file
card href correctly switched to `markdownLink ?? link`. But chat
input still kept the upload-map key on the old `link`:
uploadMapRef.current.set(result.link, result.id)
…
if (content.includes(url)) activeIds.push(id)
In the LocalStorage backend `link` is the short-lived
`/uploads/<key>?exp=&sig=` URL. The editor persists the stable
`/api/attachments/<id>/download` URL into the message body, so
`content.includes(url)` never matches and the send call drops
`attachment_ids`. The attachment ends up bound only to the chat
session, not to the message — agents reading message-level metadata
see no attachments.
Fix: key the upload map on the same value the editor actually wrote
into the markdown body (`markdownLink || link`). The
`content.includes(url)` check then matches and the attachment id is
correctly forwarded on send.
Tests:
- Updated the chat-input mock editor to insert `markdownLink || link`
into its value, mirroring the real editor's persisted-URL choice
(uploadAndInsertFile in editor/extensions/file-upload.ts). Without
this the mock would silently paper over the bug.
- Added a regression test where the upload result returns a
short-lived `link = /uploads/...?exp&sig` and a stable
`markdownLink = /api/attachments/<id>/download`. Asserts (a) the
message body carries the stable URL and never the signed query,
and (b) the bound `attachment_ids` includes the attachment id.
All 1223 frontend views tests pass (was 1222, +1 new regression).
Typecheck and 507 core tests still green.
Refs: MUL-3130, PR #3937 review by GPT-Boy. Builds on f66a522d0.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): move agent live signal into the issue-detail header
Replace the in-body sticky "agent is working" card (AgentLiveCard) with a
compact chip in the issue-detail header, so the live signal sits in one
fixed place and never competes with sticky banners in the content column.
- New IssueAgentHeaderChip: avatar(s) + live-ticking blue elapsed time;
click opens a popover listing every active task.
- Popover reuses ExecutionLogSection's ActiveTaskRow (now exported) so the
popover and the right panel are literally the same row — no duplication.
- PopoverContent gains an optional keepMounted so the row's confirm dialog
survives the popover closing on Stop.
- Running rows in ExecutionLogSection drop the blue spinner for a
live-ticking blue elapsed timer (panel + popover share this).
- Source the chip from the workspace agent-task snapshot filtered by issue
(same source as board/list indicators, zero extra network); delete the
old AgentLiveCard + its test and its heavy per-issue WS machinery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): live event count on the agent chip + execution-log rows
Show a live "N events (elapsed)" on running agents, consistent across the
header chip, its popover rows, and the right-panel execution log.
- Read the shared per-task message cache (taskMessagesOptions, kept live by
useRealtimeSync's global task:message handler) instead of a bespoke
subscription — one source of truth, deduped across chip / popover / panel /
transcript, no extra WS wiring.
- Extract <RunningStat> (event count in info-blue + elapsed in muted parens)
so all surfaces render the running stat identically.
- ExecutionLogSection running rows now show the same "N events (elapsed)";
the transcript opened from them streams live from the shared cache.
- Chip: single running shows events (elapsed); multiple shows "N working".
- i18n: add agent_live.event_count (4 locales).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(comments): skip agent triggering on /note-prefixed comments
A comment whose first token is the reserved /note prefix (case-insensitive)
is stored like any other comment but never wakes an agent. The guard sits at
the top of triggerTasksForComment, the single chokepoint, so it covers all
three trigger paths — assignee, squad leader, and @mentioned agents. Gating
only shouldEnqueueOnComment (as originally proposed) would still let
"/note @agent ..." through the mention path.
Lets members leave human-only tips/notes on agent-assigned issues without
burning an agent run. MUL-3115, closes#3649.
Co-authored-by: multica-agent <github@multica.ai>
* feat(editor): add /note built-in slash command to comment composer
Enable the `/` menu in the issue comment and reply composers in a new
"command" mode that lists fixed built-in commands instead of the chat
skill picker. Currently one command, /note, which marks a comment as a
human-only note that won't trigger the assigned agent.
Selecting it inserts the plain-text "/note " prefix (not a rich node), so
a menu pick and a hand-typed command are byte-identical and the backend
detects either with a simple prefix match. The command menu renders nothing
on a non-matching `/` (hideOnEmpty) so typing a date like 6/8 isn't noisy.
The chat skill picker is unchanged. MUL-3115.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(editor): match /note by label prefix and localize its description
Address PR review feedback:
- buildBuiltinCommandItems now matches the command label as a prefix only,
dropping the description substring match copied from the skill picker. With
one command this keeps the menu predictable (/no surfaces note; /deploy or a
description word like /agent shows nothing) and avoids Enter selecting note
unexpectedly.
- The command description is now a localized UI string: added
slash_command.commands.note to all four editor locales (en/ja/ko/zh-Hans)
and the menu renders it via the typed translator. The /label itself stays
literal since it's the typed token the backend matches.
MUL-3115.
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): shorten /note command description to avoid truncation
The slash menu item is single-line (truncate, w-72), so the longer copy was
cut off. Shorten to "won't trigger any agents" across all four locales — also
more accurate, since /note skips assignee, squad leader, and @mentioned agents,
not just the assigned one.
MUL-3115.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): remove comment composer expand control
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): auto-grow composers and highlight reply submit when ready
- Drop max-height cap on comment + reply composers so they grow with content
- Reply send button turns primary when there is submittable text
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925)
These fields are calendar days (the pickers offer no time-of-day), but were
stored as TIMESTAMPTZ. A client serializing local midnight via toISOString()
folded its timezone into the instant, so the day shifted by the local offset
(GH #3618). Migrate the columns to DATE and parse/serialize date-only
"YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the
UTC day) so older clients keep working.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925)
Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of
toISOString(), and every read formats via the shared @multica/core/issues/date
helpers with timeZone:"UTC" so the day never shifts with the viewer's offset.
The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers,
quick-set menu, list/board/detail/activity, and the mobile due-date picker.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925)
Review follow-ups on #3692:
- ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339
to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight
instant and rejects ambiguous ones loudly. Adds util unit tests.
- migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via
AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too.
- Convert remaining date-change display sites to formatDateOnly: inbox detail
label (web) and mobile activity + inbox labels (were new Date()+local format).
- CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(chat): rework chat history list
- Drop legacy archived sessions from the history dropdown. The
soft-archive feature was removed, so status='archived' rows are dead
data; exclude them instead of showing a collapsed "archived" group.
Rename the section heading "Active" -> "Chat history".
- Swap hover row actions into the status column's slot instead of an
absolute overlay: status is hidden on hover and actions take its
place inline, while the title keeps flex-1. No mid-row gap, no
overlap, no text bleed-through.
- Remove orphaned i18n keys (active_group, archived_group,
archived_label) across en/zh-Hans/ko.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(issues): align execution log rows with the chat hover-swap pattern
- Drop the fixed w-20 status column that forced premature truncation of
the trigger text and left a mid-row gap; status now sizes to content.
- Running tasks render only the spinner (sr-only label retained for a11y
and tooltip); the redundant "Working" text is removed.
- Hover swaps status for actions in place (RowStatus hidden, RowActions
inline) instead of an absolute gradient overlay. Applies to both
active and past ("show past runs") rows via the shared RowShell /
RowStatus / RowActions.
Known tradeoff: dropping the absolute+opacity slot also drops the
group-focus-within keyboard reveal, so cancel/retry are no longer
Tab-reachable. Matches the chat pattern; revisit if keyboard access for
row actions becomes a requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds avatar_url column to workspace, threads it through the API +
WorkspaceAvatar component, and adds a click-to-upload editor in the
workspace settings tab. Mirrors the squad avatar pattern (migration 086);
UI strings use "logo" while the schema/code uses avatar_url for codebase
consistency with user.avatar_url and squad.avatar_url.
- migration 093: ALTER TABLE workspace ADD COLUMN avatar_url TEXT
- UpdateWorkspace SQL + handler accept avatar_url (auth gated to
owner/admin at the router via RequireWorkspaceRoleFromURL)
- WorkspaceAvatar renders <img> when avatar_url is set, falls back to
the initial-letter span otherwise
- workspace-tab.tsx adds a 16x16 click-to-upload logo editor at the
top of the general settings card, using useFileUpload + accept=
image/png,image/jpeg,image/webp (server stores under workspaces/{id}/)
- en + zh-Hans settings i18n strings added
Co-authored-by: Matt Voska <voska@users.noreply.github.com>
PR #3517 moved the AgentLiveCard out of the activity section to a
sticky bar above the editable title. This restores its original
placement (after LocalDirectoryHint, above the timeline) and reverts
the container margin from mb-4 back to mt-4 that suited the top slot.
Everything else from #3517 is kept: the single-container multi-agent
accordion, the lifted cancel-confirmation dialog, and the dropped
parked/running background variant.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the per-issue "agent is working" bar out of the activity section to a
sticky bar at the top of the main content, above the editable title, and fix
the multi-agent layout.
- Single active task fills one bordered container as a directly-actionable
row (Logs + Stop inline).
- Multiple active tasks collapse into the same container: a summary header
(avatar stack + count) that expands the rows inline as a divided list,
instead of stacking N detached banners.
- Lift cancel confirmation to AgentLiveCard so one dialog serves both the lone
row and the expanded list (avoids a confirm dialog being torn down by an
enclosing popup).
- Drop the redundant parked/running background variant; running vs queued is
signalled by icon + label only.
- Reuse the existing agent_activity.hover_header plural for the summary; no new
i18n keys.
- Update tests for single-inline vs multi-accordion behavior.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(views): unify detail/list headers into shared BreadcrumbHeader
Replace four hand-rolled, divergent header styles (workspace-name root,
"/" separator, back-arrow, raw div) with one shared BreadcrumbHeader
component. The mental model is now identical everywhere: leading crumbs
are the thing's real containers and clicking one navigates up.
- New packages/views/layout/breadcrumb-header.tsx (segments/leaf/actions)
- Detail pages (issue, project, runtime, skill, autopilot, agent, squad)
now render `{Section} › name`; org name removed as a breadcrumb root
- Issue breadcrumb shows the single most-direct container only (parent
wins over project; they are orthogonal columns), never a fabricated
chain; bare issue shows just its title
- Issue leaf (identifier + title) is now a clickable link to the issue
detail page with a subtle hover:opacity-80
- Issues / My Issues list headers drop the workspace prefix, matching the
icon + title style of the other list pages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(views): update breadcrumb tests for unified header behavior
The header unification changed three observable behaviors the tests
asserted against:
- issue detail no longer renders the workspace name as a breadcrumb root
- bare issue shows only its (now clickable) title leaf, no ancestor crumbs
- the project "Unknown project" error placeholder was removed
Rewrite the two affected issue-detail tests to assert the new leaf-link
and no-project-crumb behavior, drop the obsolete Unknown-project test, and
update the issues-page header test to assert the workspace prefix is gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>