The sidebar metadata trigger now reads "Metadata · N" (#3010), so the
exact-name button query stopped matching and 2 tests went red on main.
Relax the assertion to `/^Metadata\b/` — still anchors on the label but
tolerates the count suffix.
Reshape the sidebar metadata trigger so it visually matches the Pull
requests / Details / Parent issue headers (muted "Metadata · N" row
instead of an icon+label button). Clicking still opens the existing
JSON dialog — folding the bag inline pushed the rest of the sidebar
down too much when the payload was large.
* feat: add delete button to fleet nodes list
- Add deleteCloudRuntimeNode method to API client (DELETE /api/cloud-runtime/nodes/:nodeId)
- Add useDeleteCloudRuntimeNode mutation hook in cloud-runtime.ts
- Add delete button with Trash2 icon to CloudRuntimeNodeRow component
- Include confirmation dialog, loading state, and toast notifications
- Add i18n keys for en and zh-Hans locales
Co-authored-by: multica-agent <github@multica.ai>
* fix(api): correct deleteCloudRuntimeNode contract to match server
- Change from DELETE /api/cloud-runtime/nodes/:nodeId (no body) to
DELETE /api/cloud-runtime/nodes with JSON body { id: nodeId }
- Use fetchRaw + Content-Type header to match server's withBody proxy
- Add contract test verifying URL, method, body, and Content-Type
Fixes review feedback on MUL-2510
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): collapse long metadata bags in sidebar (MUL-2503)
The metadata KV strip rendered every key inline, so issues with many
pinned keys pushed the rest of the sidebar far down. Keep the first
four rows visible and tuck the remainder behind a Show N more / Show
less toggle once the bag reaches five keys, mirroring the PR list
collapse rule.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issues): hide metadata behind a JSON dialog (MUL-2503)
Metadata is an agent-facing free-form KV bag — the values almost never
mean anything to a human reader, and every property humans actually care
about already has a dedicated sidebar field (status, priority, assignee,
etc.). Rendering the first four keys inline still pushed real signal
down and added visual noise for no benefit, so drop the inline strip
entirely.
Replace the section with a small `{ }` Metadata button at the bottom of
the sidebar that opens a Dialog showing the formatted JSON. The button
hides itself when the bag is empty, so the common case stays completely
quiet. Removes the prior collapse threshold (and its `Show N more` /
`Show less` strings) since there is nothing to collapse anymore.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
When bootstrap is enabled and no PAT is available from the request
header or Authorization bearer token, the server now generates a new
PAT automatically and forwards it to the cloud service.
This removes the need for the frontend to pass X-User-PAT — the
server handles it entirely.
* feat(issues): per-issue metadata KV (MUL-2017)
Adds a small JSONB KV map to every issue for agent pipeline state (attempts,
PR number, pipeline status, ...). Keys match a narrow regex, values are
primitives (string / number / bool), capped at 50 keys per issue and 8KB
per blob. Defense-in-depth via two CHECK constraints (object shape + size).
All mutations are single-key atomic (jsonb_set / `- key`). `UpdateIssue`
intentionally does NOT touch metadata: a whole-blob overwrite would race
with concurrent agent writes.
GET /api/issues/:id/metadata
PUT /api/issues/:id/metadata/:key body: { "value": <primitive> }
DELETE /api/issues/:id/metadata/:key
Containment filter on list: GET /api/issues?metadata=<json-object> uses
PG `@>` against a `jsonb_path_ops` GIN index. Mirrored across ListIssues,
CountIssues, ListOpenIssues, and the hand-rolled ListGroupedIssues SQL so
CLI/API and UI grouped views stay consistent.
CLI: multica issue metadata {list,get,set,delete}
multica issue list --metadata key=value (repeatable, AND)
set has --type to override the default value-sniffing
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): metadata test bugs + wire realtime + read-only display (MUL-2017)
- Fix two failing handler tests blocking backend CI:
- reset decode target after delete so map merge does not mask removal
- url.PathEscape the key segment so spaces no longer panic NewRequest
- Wire issue_metadata:changed end to end so the detail / list / my-issues
caches stay in sync with set/delete events (other tabs, CLI writes).
- Add a read-only Metadata strip to the issue detail sidebar; hidden when
the issue has no keys so it stays quiet in the common case.
Co-authored-by: multica-agent <github@multica.ai>
* feat(runtime): teach agents to read/write issue metadata (MUL-2017)
Add an `## Issue Metadata` section to the runtime brief plus a
`metadata list` step on entry and a `metadata set`/`delete` step on
exit. Section only emits when the task carries an issue id (comment- or
assignment-triggered); chat / quick-create / run-only autopilot stay
clean so they don't fire failing CLI calls.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): bump metadata migration to 105 and drop attempts as example (MUL-2017)
main is now at 104_drop_runtime_timezone; the migrator picks
LatestVersion() by sorted filename, so a slot before the tail would
let DBs that have already run 099–104 think they're up-to-date while
the issue.metadata column is missing — runtime would then fail with
column does not exist. Renumbering to 105 puts the migration at the
tail and forces it to run.
Also drop attempts as a positive example across docs/code comments and
test fixtures — the runtime instruction prompt already lists it under
"What NOT to pin" (runtime bookkeeping). Replace with pr_number, which
is in the recommended-keys set, so docs/tests speak the same language
as the prompt.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* docs(timezone): add scheduling/viewing timezone architecture RFC
* feat(db): replace daily rollups with task_usage_hourly, add user.timezone
Migrations 100-104: add "user".timezone (Viewing tz), build the UTC
hourly task_usage_hourly rollup with its pipeline, drop the legacy
task_usage_daily / task_usage_dashboard_daily pipelines, and drop the
agent_runtime.timezone column. Report queries now slice day boundaries
at read time by the caller-supplied @tz instead of materialising in a
fixed tz. Regenerate sqlc.
* feat(server): add task_usage_hourly backfill command
Replace the two legacy backfill commands (daily / dashboard_daily) with
a single backfill_task_usage_hourly that loads historical task_usage
into the new UTC hourly rollup, sliced per workspace.
* refactor(server): resolve viewing timezone in report handlers
Report handlers resolve the Viewing tz per request (?tz query param,
then user.timezone, then UTC) and pass it to the hourly-rollup queries.
Drop the UseDailyRollup feature flags and the old raw-scan/daily-rollup
dual paths, remove the /api/usage endpoints, and stop the daemon from
reporting and the runtime handler from accepting host timezone.
* refactor(core): switch report queries to viewing timezone
API client and dashboard/runtime queries send ?tz with each report
request, the user schema/types carry the new timezone field, and the
runtime timezone field/mutation is removed.
* feat(views): add viewing timezone preference and UI
Add the useViewingTimezone hook and a Timezone setting in Preferences;
report charts and the dashboard week boundary follow the viewer tz.
Remove the runtime detail timezone editor and its locale strings.
* fix(test): update fixtures and stabilize tests for timezone refactor
The timezone architecture refactor changed several types without
updating dependent test code:
- RuntimeDevice no longer has a timezone field — drop it from the
create-agent-dialog runtime fixture.
- User now requires a timezone field — add it to the apps/web mockUser
fixture.
- The PreferencesTab timezone tests asserted on the async save handler
(PATCH then store update) with a bare expect, racing the mutation's
settle callback, and timed out querying the Select's ~600-option IANA
list on a loaded CI runner. Wrap the assertions in waitFor and extend
the timeout for those three tests.
* docs(timezone): document self-host migration order and trigger invariant
Add a SELF-HOST UPGRADE ORDER runbook to the backfill command's package
comment: applying migrations 100-104 in a single migrate-up drops the
legacy daily rollups before the hourly backfill runs, leaving dashboards
empty until cron catches up.
Add an INVARIANT comment on trg_atq_dirty_hourly noting that agent_id
must be added to the trigger's OF list if it ever becomes mutable,
otherwise dirty buckets for the old agent_id are silently missed.
* style(runtimes): drop trailing blank line in runtime-detail
The compact view was the original list layout and is what users expect
on this page; the post-#2840 default of comfortable changed long-standing
behavior. Reset the unpersisted default (and the cross-workspace fallback
in `merge`) back to compact. Updates the view-store tests accordingly.
MUL-2464
Co-authored-by: multica-agent <github@multica.ai>
The post-#2946 onmessage guard logs the raw event.data alongside the
warning. A malformed or rogue server can stream arbitrarily large
garbage and bloat the renderer / desktop main-process log buffers, so
cap the logged payload to the first 200 chars and append a
"(truncated, N chars total)" suffix when truncation occurs.
MUL-2490
Co-authored-by: multica-agent <github@multica.ai>
The model dropdown already exposes a "Default (provider)" option meaning
"follow the CLI's current selection". Tagging the runtime's preferred
model with a small "default" chip created two competing notions of
"default" in the same UI and confused users. Remove the chip from both
the create-agent ModelDropdown and the inspector ModelPicker; keep the
underlying RuntimeModel.default flag intact since thinking-prop-row
still uses it as a fallback heuristic.
Co-authored-by: multica-agent <github@multica.ai>
Replaces the plain "Loading..." text fallback in SquadDetailPage with a
skeleton that mirrors the loaded page's two-column layout (left inspector
+ right tabs panel), matching the SquadsListSkeleton work shipped in #2890.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(squads): skeleton loader + AlertDialog archive confirm (MUL-2437)
- Replace `Loading...` text on the squads list with a Skeleton placeholder
matching the SquadCard shape (avatar + title + subtitle), aligning with
the Agents / Dashboard pattern.
- Replace the native `confirm()` on the squad detail Archive button with
the project's AlertDialog (destructive variant, pending-disabled, i18n
copy interpolating the squad name).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): drop misleading restore copy from archive confirm (MUL-2437)
Archive is irreversible — there is no unarchive command (see
apps/docs/content/docs/squads.mdx:113). Aligns dialog copy with
docs: tells the user the action can't be undone and to create a
new squad if they need the routing back.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): surface "agent working" on board + add Working filter (MUL-2452)
Adds a brand-color "agent working" badge to board cards / list rows so
users can see at a glance which issues have an active agent task, plus a
new "Working" toggle on the `/issues` and `/my-issues` headers (next to
the existing scope segmented control) that filters to those issues. The
toggle shows an avatar stack of the agents currently active on the
current surface + scope. Pure frontend: re-shapes the existing
workspace-wide `agentTaskSnapshot` cache via two new selectors
(`activeTasksByIssueOptions` / `workingIssueIdsOptions`), no new SQL,
endpoint, or DB field; WS `task:*` events already invalidate the
snapshot so the badge / filter update in realtime.
Project detail page keeps the per-card badge but intentionally omits the
header toggle (`showWorkingToggle={false}`) to leave the project
surface's filter dimensions unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): working filter column header reflects filtered count (MUL-2452)
Assignee-grouped board column headers kept showing the unfiltered cache
total when Working was on, because `PaginatedAssigneeBoardColumn` passed
`useLoadMoreByAssigneeGroup`'s cache-derived `total` straight to
`BoardColumn`. The hook still needs the cache total for hasMore, but the
displayed count must follow the visible-after-filter set.
Split the two: when Working is active the column header now uses
`group.totalCount` (set by applyWorkingFilterToGroups) for the assignee
path, and `issueIds.length` for the status path. Load-more keeps reading
from cache so paginated columns still see the full server total.
Regression tests cover applyWorkingFilterToGroups (total rewrite +
empty-group preservation), filterIssues workingOnly combinations, and an
end-to-end assertion via IssuesPage that proves the column header equals
the filtered count, not the cached value.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Follow-up to #2919 review nits — comments still described the empty
thinking_level as "use runtime default" and claimed ThinkingPicker callers
guaranteed non-empty levels. Both were stale after the semantics changed:
- packages/core/types/agent.ts: clarify that "" clears the override and
the local CLI config / built-in default decides at runtime.
- thinking-picker.tsx: document that the stale-orphan clear path in
ThinkingPropRow mounts the picker with an empty levels list plus a
persisted value, so callers do not guarantee non-empty levels.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): retry button targets the row's agent, not the assignee (MUL-2457)
The execution log retry button used to re-fire the issue's current
assignee instead of the agent that actually ran the clicked row. After
a reassignment, or for squad workers / @-mention agents, the rerun
landed on the wrong agent.
POST /api/issues/{id}/rerun now accepts an optional task_id: when set,
the rerun targets that task's agent (and reuses its leader/worker
role). An empty body keeps the assignee-driven CLI/API contract.
The execution-log retry button passes task.id, so per-row retry always
fires the correct agent. enqueueMentionTask gained a forceFreshSession
parameter so the new mention-path rerun keeps the same fresh-session
contract as the assignee path.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): inherit trigger provenance + fix cross-issue test (MUL-2457)
Address review feedback on PR #2921:
1. RerunIssue now inherits TriggerCommentID from the source task when
sourceTaskID is valid. Without this, a per-row rerun of a comment-
or mention-triggered task degrades into a generic issue run because
the daemon's buildCommentPrompt path keys on TriggerCommentID. The
inherited summary is rebuilt naturally inside the enqueue helpers
(buildCommentTriggerSummary derives it from the comment ID).
2. The new cross-issue rejection test inserted a second issue without
`number`, hitting uq_issue_workspace_number on a same-workspace
collision with the fixture's issue. Both inserts now claim the next
available per-workspace number (MAX(number)+1) — matching the
pattern used by notification_listeners_test.
Added TestRerunIssueInheritsTriggerCommentFromSourceTask to lock the
trigger provenance contract.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* polish(agent-inspector): optimistic updates + picker layout + thinking-default semantics
Round of cleanup on the agent inspector pickers after using them end-to-end:
1. **Optimistic updates** (`agent-detail-page.tsx`)
The `handleUpdate` callback that backs every inspector picker
(thinking / model / visibility / concurrency / runtime / name /
description / avatar) was strictly sequential:
`await api.updateAgent → invalidateQueries → toast.success`. Each pick
waited 0.5-2s for the network round trip before the trigger chip
updated, which read as visible UI lag.
Snapshot the cached agent list, patch the matching agent
synchronously via `setQueryData`, then run the network request in
the background. On error roll back to the snapshot before the toast
surfaces the cause. All inspector pickers now respond instantly.
2. **Block-in-inline fix in Model + Thinking pickers**
`PickerItem` wraps its children in a flex `<span>`. The picker
bodies had `<div>` children, which is block-in-inline (invalid
HTML5) and triggers a browser layout quirk that off-aligns
descendants — model IDs floated to the center under their labels
in ModelPicker, descriptions indented unevenly under levels in
ThinkingPicker. Replace the inner `<div>`s with `<span block
text-left>` so the layout is deterministic across rows.
3. **Visual polish in Thinking picker**
Label was `font-medium` at the parent's default `text-sm` (14px),
chunky next to the 10px description. Drop to `text-[13px]`, bump
description to `text-[11px] leading-snug` with `mt-0.5` so the
contrast between rows feels less jarring.
4. **Match Model picker's row typography to Thinking's**
Same `text-[13px]` for label + `text-[10px] mt-0.5` for the model
ID. Both pickers now read as the same component family.
5. **"Default" semantics: follow CLI config, not model factory default**
The chip displayed "Default" / "default" badge when no
`thinking_level` was set, alongside a `[default]` chip on the
model's factory-advertised default option in the menu. That was
misleading: when Multica omits `--effort` (because picker is
unset), it's the user's *local CLI config* (claude/codex) that
decides the reasoning level — not the model's factory default.
Showing "medium [default]" while the user has xhigh in their CLI
config lies about what actually fires at the API.
- Trigger label: "Default" → "Follow CLI config" (zh: "跟随 CLI 配置")
- Footer clear button: "Use model default" → "Follow CLI config"
- Footer tooltip: explicitly mentions claude/codex CLI config
- Inline `[default]` badge on the factory-default option: removed
- `defaultLevel` prop chain (picker + prop-row + test): cleaned up
as now-dead code
6. **Stop hiding the Thinking row while discovery loads**
`if (levels.length === 0 && !value) return null` hid the row
while the runtime-models query was still in flight, which
subscribed-then-unsubscribed from useQuery in such a way that
the discovery only fired when the user manually opened the Model
picker. Gate the early return on `!isLoading && !isFetching` so
ThinkingPropRow stays mounted (and thus its useQuery keeps
subscribed) until discovery returns; row appears as soon as
data arrives, no Model-picker tap required.
7. **Drop the inline tooltip on Thinking picker items**
The same description was rendered both inline under the label
(always visible) and as a hover tooltip (overlapping the next
row). The hover bubble was redundant — removed.
Tests
- `pnpm --filter @multica/views test thinking-picker` → 7/7 pass after
renaming the "Default" assertion + clearing the unused defaultLevel
test prop.
- `pnpm --filter @multica/views typecheck` clean.
* fix(test): align thinking-prop-row tests with renamed copy + loading-aware row gate
CI surfaced 3 broken assertions in `thinking-prop-row.test.tsx` —
all consequences of the polish PR's behaviour changes that the test
file hadn't tracked:
- "hides the row when ... no thinking levels and nothing is persisted"
The row now stays mounted while runtime-models discovery is in
flight (so the useQuery subscription actually survives long enough
to issue the request — fixes the bug where Thinking only appeared
after manually opening the Model picker). The assertion asserted
absence only after `initiate` was called, but loading is still in
progress at that point. Wrap the absence assertion in `waitFor`
so it waits for the row to disappear after the query settles.
- "clears the orphan value via the picker footer"
Tooltip copy changed from "Clear and fall back to this model's
default reasoning level" → "Clear the override and let the local
CLI config decide the reasoning level". Update the regex.
- "renders the row with \"Default\" when value is empty"
Trigger label changed from "Default" → "Follow CLI config" to
reflect that Multica omits --effort and the local CLI config
decides. Update the assertion + test name.
`pnpm --filter @multica/views test` → 701/701 pass.
* fix(agent-inspector): drop loading-row gate + per-field optimistic rollback (MUL-2339)
Addressing review feedback on #2919:
- ThinkingPropRow no longer keeps the row visible during discovery.
The previous explanation ("early return null aborts the useQuery
subscription") was wrong — React doesn't unmount a component that
returns null, so hooks (and their subscriptions) stay live. The
loading-aware gate only succeeded in showing an empty "Follow CLI
config" row that opened to an empty menu before discovery settled.
Restore the simple `levels empty && !value -> null` behavior; the
sibling ModelPicker mounts unconditionally and keeps the shared
runtime-models query active regardless.
- AgentDetailPage.handleUpdate now rolls back only the fields the
failing PATCH wrote, instead of restoring a whole-list snapshot.
A whole-list snapshot rollback discards any concurrent successful
inspector mutation that landed between snapshot and rollback. Per-
field rollback + a final invalidate converges the cache on server
truth without clobbering unrelated optimistic writes.
- Sync the now-stale "use model/runtime default" wording in the
thinking-related JSDoc and type comments: empty thinking_level is a
"no override" sentinel — the backend omits --effort and the upstream
CLI config decides — not a Multica-known default level.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(agent): inspector picker for thinking_level (MUL-2339)
PR1 (#2865) shipped the backend — column, daemon-side discovery,
Claude/Codex injection, API validation — but the agent detail inspector
had no UI to set the value. Users could only configure thinking_level
via custom_env / API. This wires up the picker so it lives next to
Runtime and Model where everything else editable already lives.
Picker is per-(runtime, model): it reuses the same `runtimeModelsOptions`
query the Model picker already runs (60s cache, no extra round-trip)
and reads the active model's `thinking.supported_levels`. When the list
is empty — every provider except Claude/Codex today, or a Claude model
that doesn't expose `--effort` — the entire PropRow is hidden, not just
rendered inert. The picker never gets to invent value/label pairs
itself; they come verbatim from each CLI's own catalog (`Low`,
`Extra high`, …) so the user sees exactly what `claude --effort` /
`/effort` and Codex's TUI show.
The `default_level` from the catalog is badged inside the popover so
the user knows which value `""` (the persisted "use model default"
sentinel) maps to. The clear footer sends `""` explicitly, which the
backend already understands as the tri-state "explicit clear" branch
of UpdateAgent. Invalid combinations (e.g. picking a value not in the
target provider's enum after a runtime swap in the same PATCH) hit
the existing 400 path on the server and surface as a toast via the
inspector's standard `onUpdate` error handler — no extra client-side
guard needed.
Exports `RuntimeModelThinking` and `RuntimeModelThinkingLevel` from
`@multica/core/types` so views consumers can refer to them by name.
i18n keys added in EN and zh-Hans (parity test green).
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): preserve unknown thinking_level in picker label
Stale persisted values (model swap, CLI catalog shrink) used to render
as 'Default' even though the backend would still ship the orphaned
token. Fall back to the raw value when no entry matches so the user
sees what's actually saved and can clear it.
Co-authored-by: multica-agent <github@multica.ai>
* test(agent): unit tests for thinking-picker label + clear flow
Covers the default-vs-set trigger label, the unknown-token preservation
path added in 3452fae3f, the read-only display, picking and re-picking
into onChange, and the clear footer's empty-string emission.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): keep Thinking row visible when value is stale (MUL-2339)
Inspector was hiding the row whenever the active model had no
supported_levels, which also hid persisted orphan tokens (model swap
into a non-thinking runtime, or a CLI catalog that shrank). PR1's
per-model invalid behavior is daemon-side warn/drop, not a synchronous
DB clear, so the frontend has to surface the raw value and let the
user explicit-clear it via the picker footer.
Render the row when levels are empty AND value is empty; otherwise
keep it. Extract ThinkingPropRow into its own file so the row-level
logic is unit-testable.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(agent): persist thinking_level per agent (MUL-2339)
Adds a nullable `thinking_level` column to the `agent` table so the
backend can route a runtime-native reasoning/effort token (e.g. Claude's
`xhigh`, Codex's `minimal`) through to the agent CLI on every dispatch.
The column is intentionally TEXT rather than an enum — Claude and Codex
publish overlapping but distinct vocabularies and we want the persisted
value to round-trip exactly through whichever CLI receives it. NULL is
the "use runtime default" sentinel that every downstream consumer reads
as "do not inject --effort / reasoning_effort".
This commit is just the storage layer (migration + sqlc); subsequent
commits wire it through the API, daemon, and agent backends.
Co-authored-by: multica-agent <github@multica.ai>
* feat(agent-backend): inject reasoning effort for claude + codex (MUL-2339)
Extends ExecOptions with a runtime-native ThinkingLevel string and wires
it into the Claude and Codex backends. Discovery is driven by the local
CLI so the daemon advertises whatever the host install supports rather
than a hand-maintained list that goes stale.
Per Elon's PR1 review:
- Claude: parses `claude --help` to learn the `--effort` superset and
projects through a per-model allow-list (xhigh is Opus-only; max is
session-only on the smaller models). Falls back to a conservative
static list when the binary is missing or help drift hides the line.
- Codex: drives `codex debug models --output json` so per-model
reasoning subsets and the documented default come directly from the
CLI. The older config-error probe trick is gone — the JSON path is
stable and doesn't pollute stderr with an intentional misconfig.
- Cache key includes (provider, executablePath, cliVersion) so a CLI
upgrade invalidates entries that referenced the older help / catalog.
Per Trump's PR1 constraint, all three Codex injection points
(thread/start.config, thread/resume.config, turn/start.effort) flow
through one helper (`applyCodexReasoningEffort`) so they cannot drift
independently. The shared `codexReasoningCases` fixture in
`thinking_test.go` asserts the same value→{shape, key} contract at
each site for every level the runtimes know about.
Claude's `--effort` is also added to `claudeBlockedArgs` so a user
custom_args entry can't silently outvote the daemon-injected value.
Co-authored-by: multica-agent <github@multica.ai>
* feat(api): wire thinking_level through API + daemon contract (MUL-2339)
End-to-end plumbing for the per-agent reasoning/effort setting:
- AgentResponse / TaskAgentData now carry `thinking_level`; the daemon's
claim response includes it and the daemon's executor passes it through
to agent.ExecOptions, where the Claude and Codex backends already know
what to do with it.
- ModelEntry on the runtime-models wire format gains a `thinking` block
carrying `supported_levels` + `default_level` per model so the UI can
render a runtime-aware picker without the server having to know about
the local CLI install. `handleModelList` projects the agent-package
catalog (including the new Thinking field) into the wire shape.
- CreateAgent / UpdateAgent gate the field with a synchronous provider
enum check (claude / codex only today). UpdateAgent is tri-state:
field omitted = no change, "" = explicit clear (new
`ClearAgentThinkingLevel` query, mirrors the existing mcp_config null
pattern), non-empty = validate then set.
Per Trump's PR1 review, the API NEVER auto-clears on a runtime/model
swap and ALWAYS returns 400 on an unknown literal value — same shape
across CreateAgent, UpdateAgent, and combined patches that move
runtime + level in one request. Per-model combination failures (e.g.
`xhigh` against a model that only supports up to `high`) surface as a
daemon-side task error, not a silent server-side rewrite.
TS types follow the same shape: `Agent.thinking_level`,
`CreateAgentRequest`/`UpdateAgentRequest` add the field, `RuntimeModel`
grows a `thinking` block. Older backends omit the field, which the
front-end treats as "no picker for this model" — installed desktop
builds keep working.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): correct codex debug models argv + pin via runner test (MUL-2339)
`codex debug models --output json` is rejected by codex-cli 0.131.0 —
the subcommand emits JSON on stdout by default and has no `--output`
flag. Drop the flag and add `--bundled` to skip the network refresh
discovery doesn't need. Move the argv to a package-level var and add
a test that runs a fake `codex` to assert the binary actually
receives exactly `debug models --bundled`, so the contract can't
silently drift on the next refactor.
Also teach ValidateThinkingLevel to resolve an empty model to the
provider's default model entry. Without this, every default-model
task with a persisted thinking_level would be misjudged "unknown
model" by the daemon guard.
Co-authored-by: multica-agent <github@multica.ai>
* fix(api): reject runtime switch that would leave invalid thinking_level (MUL-2339)
A PATCH that changed `runtime_id` without touching `thinking_level`
used to silently keep the existing value, so a Claude agent storing
`max` could land on a Codex runtime where `max` is not a recognised
token at all, and the daemon would receive a literal-invalid level.
Hold the same "always 400 on literal-invalid, never silent coerce"
rule on this implicit path. When runtime_id changes and the existing
value is not in the new provider's enum, return 400 with the
recovery options (clear via `thinking_level=""` or re-set in the
same PATCH).
Add coverage for both the kept-when-still-valid and the rejected
cases, plus the two recovery paths (clear and replace).
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): guard runTask with per-model thinking_level validator (MUL-2339)
ValidateThinkingLevel existed but had no call site — `task.Agent.
ThinkingLevel` flowed straight into ExecOptions, so `xhigh` configured
on a non-Opus Claude model, or API-side stale values that escaped the
provider enum gate, would be injected anyway.
Run the validator before building ExecOptions. Invalid combinations
log a warning and drop the level instead of failing the task: the
agent still runs, just at the runtime's default reasoning effort.
Discovery errors fail open (keep the level, let the CLI surface any
objection) so a transient `claude --help` failure can't strand work.
Empty model is forwarded as-is; the validator resolves it to the
provider's default model internally per the cross-package contract.
Co-authored-by: multica-agent <github@multica.ai>
* chore(agent): drop stale `--output json` comments + unused scanner (MUL-2339)
Codex CLI's `debug models` subcommand emits JSON without an `--output`
flag, and `parseCodexDebugModels` never read from the bufio.Scanner.
Sync the comments with the actual invocation and remove the dead init.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): use official Gemini spark icon (MUL-2447)
Gemini provider was falling through to the default Monitor icon in the
runtime list. Add the official 4-point spark mark with Google's
blue → purple → pink gradient, matching the SVG style/sizing of the
other provider icons.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): use current Gemini multicolor spark gradient (MUL-2447)
Per review on PR #2904: the previous 3-stop blue/purple/pink gradient
was the legacy Bard-era Gemini spark. Update to the 5-stop cyan → blue
→ purple → pink → orange gradient used by the current Gemini app/web
multicolor mark.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): switch Gemini icon to aurora multicolor treatment (MUL-2447)
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): align Gemini aurora color positions and smooth spark path
Swap yellow/green radial gradient anchors so colors land at the official
positions: top red / right blue / left yellow / bottom green, matching
gemini.google.com's current aurora spark. Replace the arc-based 4-point
spark outline with a cubic-bezier version normalized to the 24-viewBox
so the inset between tips is smoother and closer to the gstatic source.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): use Simple Icons Google Gemini mark (MUL-2447)
Drop the hand-crafted aurora gradient approximation and inline the
canonical "Google Gemini" path from Simple Icons (CC0 1.0), rendered
in the Simple Icons brand color (#8E75B2). This matches the pattern
used by the other provider marks in this file (Claude/Codex from
Bootstrap Icons, etc.) instead of trying to manually approximate the
official multicolor wash from gemini.google.com (which paints via a
clipPath over an embedded raster).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The Start button lives in `DaemonRuntimeActions`, which is rendered in
the per-machine detail pane and only when the selected machine is
flagged `isCurrent`. After the user manually stopped the daemon,
`status.daemonId` went back to undefined, so no machine could be
matched as `isCurrent` — the local row either disappeared (when the
server-side runtime had been GC'd) or moved into the "remote" section
(when it was still present but unmatched). Either way the Start button
was unreachable until the app was restarted.
Two-part fix:
- `DesktopRuntimesPage` now caches the last-known daemonId/deviceName
so the local match keeps working while the runtime is still on the
server (recently_lost / offline window).
- `buildRuntimeMachines` accepts an `ensureLocalMachine` flag; when no
real runtime matches, a placeholder local row is synthesized so the
Start button still has a home. Desktop opts in via a new
`hasLocalMachine` prop on `RuntimesPage`. The empty state is also
suppressed when this prop is set so the placeholder row isn't hidden
behind the "register a runtime" hint on first launch.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(autopilot): support assigning autopilot to a squad (MUL-2429)
Path A (Squad-as-Leader) from the RFC: when an autopilot's assignee is a
squad, dispatch resolves to squad.leader_id and executes against the
leader's runtime — semantics match a human manually assigning the issue
to that squad, no fan-out.
Backend scope only; frontend picker change is a follow-up PR.
Changes:
- 096_autopilot_squad_assignee migration: drop agent FK on
autopilot.assignee_id, add assignee_type column (default 'agent'),
add autopilot_run.squad_id attribution column.
- service.AgentReadiness: single source of truth for archived /
runtime-bound / runtime-online checks. Shared by autopilot
admission gate, run_only dispatch, and isSquadLeaderReady.
- service.resolveAutopilotLeader: translates assignee_type/id to the
agent that actually runs the work.
- dispatchCreateIssue: stamps issue with assignee_type='squad' for
squad autopilots and enqueues via EnqueueTaskForSquadLeader.
- dispatchRunOnly: belt-and-braces readiness re-check after resolving
squad → leader so a leader that went offline between admission and
dispatch produces a clean failure instead of a doomed task.
- handler.CreateAutopilot / UpdateAutopilot: accept assignee_type with
squad/agent existence + leader-archived validation. Backward-compatible
default of "agent" preserves the contract for older clients.
- Analytics: AutopilotRunStarted/Completed/Failed events carry
assignee_type and squad_id; PostHog can now group autopilot runs by
squad without joining back to the autopilot row.
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilot): reject archived squads, route post-admission skips, cleanup dangling-agent autopilots (MUL-2429)
Addresses three review findings on PR #2888:
1. Archived squad handling: validateAutopilotAssignee now rejects squads
with archived_at set; resolveAutopilotLeader returns errSquadArchived
so the admission gate fails closed; DeleteSquad now mirrors the issue
transfer for autopilot rows (TransferSquadAutopilotsToLeader) so
surviving autopilots flip to assignee_type='agent' (leader) instead
of dangling at the archived squad.
2. dispatchRunOnly post-admission readiness: introduces errDispatchSkipped
sentinel, recognised by DispatchAutopilot via handleDispatchSkip so
the run is recorded as `skipped` (not `failed`). Manual triggers no
longer 500 when the leader's runtime goes offline between admission
and task creation. New TestManualTriggerDoesNotErrorOnPostAdmissionSkip
locks the behaviour in.
3. Dangling agent assignee after migration 096 dropped the FK:
shouldSkipDispatch now distinguishes pgx.ErrNoRows / errSquadArchived
(hard skip — retrying won't help) from transient DB errors
(fail-open). DeleteAgentRuntime pauses autopilots that target agents
about to be hard-deleted (ListArchivedAgentIDsByRuntime +
PauseAutopilotsByAgentAssignees) so the breakage surfaces as a paused
row in the UI instead of a quiet skip-burning loop.
Unit tests cover the sentinel unwrap contract and errSquadArchived
errors.Is behaviour. Integration test
TestAutopilotDispatchSkipsWhenRuntimeOffline re-verified against a fresh
DB with migration 096 applied.
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilot): bump last_run_at on post-admission skip (MUL-2429)
Match recordSkippedRun (pre-flight skip) and the success path so the
scheduler / "last seen" UI both reflect that this tick evaluated the
trigger, even when the post-admission readiness gate caught a late
regression.
Addresses Emacs review caveat #1 on PR #2888.
Co-authored-by: multica-agent <github@multica.ai>
* feat(autopilot): mixed agent/squad assignee picker in dialog (MUL-2429)
End-to-end UI for assigning an autopilot to a squad. Closes the PR #2888
backend gap: the squad-as-assignee feature was already wired in Go (Path A,
RFC §4) but the desktop dialog never offered the choice.
- core/types/autopilot: add `AutopilotAssigneeType`, surface
`assignee_type` on `Autopilot` + Create/Update request payloads.
- views/autopilots/pickers/agent-picker: switch to a polymorphic
AssigneeSelection (`{type, id}`); render agents and squads as two
grouped sections with shared pinyin search.
- views/autopilots/autopilot-dialog: maintain `assigneeType` state, send
it on create/update, render the trigger avatar / hover dot with
`assignee.type`.
- views/autopilots/autopilots-page + autopilot-detail-page: render the
assignee row using `autopilot.assignee_type` so squad-typed autopilots
show the squad avatar + name, not a broken agent lookup.
- locales: add `agents_group` / `squads_group` / `select_assignee` keys
(en + zh-Hans), keep legacy `select_agent` for callers that still
reference it.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Wires the frontend half of the read-only RFC. The Settings → GitHub tab
now always issues the installation list query for any workspace member
(the backend gates it via `RequireWorkspaceMember` after PR #2886) and
gets `can_manage` straight from the API response. The render matrix
covers the six cases the RFC calls out:
- configured + connected + admin → Disconnect + (optional) Connected by
- configured + connected + member → read-only "Connected to" + read_only_hint
- configured + not connected + admin → Connect button + dev description
- configured + not connected + member → contact_admin_to_connect hint
- not configured + admin → operator banner + disabled Connect
- not configured + member → contact_admin_to_connect hint
New i18n keys (en + zh-Hans): read_only_hint, connected_by, contact_admin_to_connect.
The unused github.manage_hint string is removed (its non-admin branch
now resolves to one of the two new hints depending on connection state).
GitHubInstallation gains an optional `connected_by` display name so the
UI can render the "Connected by {name}" line without further changes
once the backend exposes the field.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(github): expose read-only installation list to workspace members (MUL-2413)
Relax `GET /api/workspaces/{id}/github/installations` from owner/admin-only
to any workspace member so the Settings → Integrations tab no longer renders
blank for non-admins (the original symptom of MUL-2413).
The handler now reads the caller's role from the workspace middleware:
- owner / admin keep the full row including the numeric `installation_id`
(the connect / disconnect handle) and receive `can_manage: true`.
- every other role (member / guest) receives rows with `installation_id`
omitted and `can_manage: false`, giving them visibility into "is GitHub
wired up?" without the management handle.
`GET /github/connect` and `DELETE /github/installations/{id}` stay under
the admin/owner middleware group — this PR only relaxes the read path.
Tests: `TestListGitHubInstallations_RoleGating` exercises admin, owner,
member, and guest paths against the real DB-backed handler fixture and
asserts the field stripping + `can_manage` contract.
Refs: MUL-2413
Co-authored-by: multica-agent <github@multica.ai>
* fix(github): redact installation_id from realtime broadcasts (MUL-2413)
GET /github/installations strips the numeric installation_id for non-admin
members, but the github_installation:created / uninstall / suspend WS
events were still publishing it, so the same handle was reachable from
any workspace client subscribed to the workspace scope. Broadcast both
payload variants without it — the frontend uses these events only to
invalidate the installations query, so admins re-query the list endpoint
to recover the management handle.
Also adds a router-level test that mounts the production middleware split
(member-visible list vs. owner/admin connect+delete) so a future routing
change can't silently widen the write surface.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
In the attachment preview modal, image and video previews used
`max-h-full max-w-full`, which let small assets render at their
natural size and leave the modal mostly empty. Switch to
`h-full w-full` so the preview always occupies the modal viewport,
relying on `object-contain` to preserve aspect ratio without
upscaling beyond the intrinsic bounds.
Only touches `packages/views/editor/attachment-preview-modal.tsx`
for the image (line 355) and video (line 373) branches; pdf, audio,
markdown, html, and text branches keep their existing layout.
Co-authored-by: multica-agent <github@multica.ai>
Tiptap's stock ListItem keymap binds Enter only to splitListItem. When the
cursor sits in an empty top-level list item, splitListItem returns false
(without dispatching) with a code comment saying "let next command handle
lifting" — but no next command is chained. Enter then falls through to
ProseMirror's baseKeymap which inserts another empty paragraph inside the
list item, trapping the user.
Replace StarterKit's ListItem with PatchedListItem whose Enter binding
chains splitListItem → liftListItem via commands.first. The lift fallback
only runs when splitListItem returns false (top-level empty case),
restoring the standard "double-Enter exits the list" behaviour seen in
every other rich-text editor. Non-empty and nested-empty items are
unaffected because splitListItem already handles them correctly.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
## Summary
Adds per-user `profile_description` so coding agents have cheap, durable context about who is asking. v1 per the brief Xeon locked in on [MUL-2406](mention://issue/63a7247c-4f6a-42cf-90d1-7c746e77158a):
- **DB** — `user.profile_description TEXT NOT NULL DEFAULT ''` (migration 096). 2000-rune cap enforced server-side. No nullable / privacy state to manage.
- **API** — `PATCH /api/me` accepts the field; `UserResponse` always emits it. Client wraps `updateMe` in a lenient `UserSchema` + `EMPTY_USER` fallback per CLAUDE.md API Response Compatibility.
- **UI** — Settings → Account gains an "About you" textarea with live `n/2000` counter, `maxLength` guard, and a localized too-long error (EN + zh-Hans).
- **CLI** — `multica user profile get` / `multica user profile update` with `--description / --description-stdin / --description-file / --clear`, mirroring the existing `issue comment add` input-mode menu.
- **Daemon injection** — claim handler resolves the runtime owner and stamps `requesting_user_name` + `requesting_user_profile_description` on the task. `buildMetaSkillContent` emits `## Requesting User` between `## Agent Identity` and `## Available Commands`, blockquoted and framed as background context. The block is omitted entirely when the description is empty (no token cost when unused).
Brief is written **once per task** via `CLAUDE.md` / `AGENTS.md`, not the per-turn prompt — same path the agent already reads for identity, so no extra per-turn cost.
## Test plan
- [x] `go build ./...`, `go vet ./...`, `go test ./internal/cli/ ./internal/daemon/ ./internal/daemon/execenv/ ./cmd/multica/`
- [x] New brief tests: `TestBuildMetaSkillContentEmitsRequestingUser`, `TestBuildMetaSkillContentOmitsRequestingUserWhenEmpty`
- [x] `pnpm typecheck`, `pnpm lint`, `pnpm test` (74 files, 644 tests pass)
- [ ] Handler DB tests (`TestUpdateMe*`) require a migrated test DB — not runnable in this sandbox
- [ ] Manual: open Settings → Account, set a description, confirm the next daemon-run agent's `CLAUDE.md` shows `## Requesting User`
* feat(issues): show project segment in issue breadcrumb (MUL-2422)
Render the issue's project (when present) between the workspace and any
parent-issue segment. Segment reflects the issue's own `project_id` so
the same URL produces the same breadcrumb from every entry point.
Failed/missing project queries fall back to an "Unknown project"
placeholder; loading shows a skeleton to avoid layout shift.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): cap project breadcrumb width to preserve title precedence
Constrain Project crumb to max-w-72 (matching ProjectChip) and add
min-w-0 to the title span so the flex compression order matches RFC
§5/§9: Project/Parent shrink before the current Issue title.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(runtimes): declutter the runtimes page (MUL-2407)
Cuts visual noise on the Runtimes detail view without removing real
information:
- MachineDetail: drop the 4-card metric grid (RUNTIMES / HEALTH /
WORKLOAD / CLI) and replace it with a single inline meta strip. The
cards repeated what the title chip and runtime rows already show.
- PageHeaderBar: remove the inline tagline + "Learn more" link. The
header is now icon + title + count + connect button.
- VisibilityBadge: only render the Public chip. Private is the default,
so a row of `🔒 Private` badges was pure noise.
- CliCell: drop the per-row "Desktop" managed badge — the same string on
every desktop row carried near-zero information.
- MachineSidebar row: hide the truncated daemon-id subtitle. The id is
still available on hover via `title` and remains visible in the
detail header.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): address review feedback on inline meta and hover title
- Inline meta now reads "6 runtimes · 5 online" instead of "6 6 online"
by using runtime_count for the total label.
- Sidebar machine title hover now shows full daemon id (with subtitle
fallback) so the daemon id is recoverable after the sub-row was hidden.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
320px was too cramped for typical rendered HTML (charts, dashboards,
formatted documents). Matches the existing HTML attachment preview
height for visual consistency across both iframe surfaces.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438)
Drops the post-onboarding ImportStarterContent / DismissStarterContent
flow (handler + routes + StarterContentPrompt + templates + locale
strings + analytics event). The bug — web onboarding seeding 6+ starter
issues without a runtime — only existed through that path; with it gone
the source disappears.
The "install a runtime" issue from BootstrapOnboardingNoRuntime is now
the canonical no-runtime onboarding seed. The title/description and a
LockAndFindActiveDuplicate-deduped seeder move to
handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace /
AcceptInvitation seed it whenever the workspace has no runtime yet, so
every mark-onboarded entry point lands the user on a concrete next
step.
starter_content_state column is kept and continues to be claimed as
'imported' in all five entry points so older desktop builds (which
still render the legacy dialog on NULL) don't surface it to accounts
created after this change.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438)
054 only covered pre-feature users. Anyone onboarded between then and the
starter-content kit removal could still sit at NULL, and old desktop
clients gate the legacy StarterContentPrompt on `starter_content_state
IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL
would surface a dialog whose buttons 404. Mark them 'imported' to match
the new helper's claim semantics.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Add optional `opts.activate` to NavigationAdapter.openInNewTab. Default
stays `false` so cmd/ctrl+click on links/mentions keeps browser-style
background semantics. The two explicit toolbar entry points
(attachment-preview-modal, html-attachment-preview) opt in with
`{ activate: true }` so the new tab gains focus after the modal closes.
Both desktop providers (root + per-tab) now use the tab id returned by
`store.openTab` to call `setActiveTab` only when `activate` is true.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881)
Project Gantt now fetches its own scheduled-only data instead of riding the
Board/List pagination cache. The Unscheduled drawer and pagination warning
banner are gone, and any WS-driven issue change (create / update / delete)
invalidates the new cache so the timeline stays live.
- Backend: `GET /api/issues?scheduled=true` adds an
`(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both
ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler.
- Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single
fetch and lives under its own cache key. WS handlers and mutations
invalidate the prefix on create/update/delete so the bar reacts to
start_date / due_date changes from other tabs and from this tab without
waiting on the WS round-trip.
- GanttView: drops the Unscheduled section, the pagination warning banner,
and the load-all button; renders only scheduled rows.
- Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`,
`summarizeIssueListPagination`, and the gantt locale strings that
supported the old plumbing.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): page through Gantt fetch and isolate per-view data sources
- Walk paginated `scheduled=true` issues until total is reached so projects
with more than 500 scheduled bars no longer silently truncate.
- Gantt mode disables the bucketed Board/List query and reads its own
scheduled cache for the project empty-state check, so the page never
short-circuits Gantt with a Board-derived "no issues" CTA.
- `onIssueLabelsChanged` patches matching rows in the Project Gantt cache
in-place, keeping label filters consistent after attach/detach from
other tabs or agents.
MUL-1881
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Earlier the unification commit dragged in a Tailwind override stack
(ring, rounded-md, transition-shadow, bg-background/95, button hover
classes) "to make standalone surfaces work without .rich-text-editor
scope". Because the legacy CSS rules were not removed, both layers
applied in the editor, producing a visible double-stroke selection
ring and a light-theme hover on top of the dark-glass toolbar.
This commit reverts the styling churn:
- ImageAttachmentView now emits the same span-only DOM as the original
ReadonlyImage: <span.image-node> > <span.image-figure> > <img.image-content>
+ <span.image-toolbar> with naked <button> children. No Tailwind tax.
- The `.image-*` rules in content-editor.css are de-scoped from
`.rich-text-editor` so the single set of styles also drives chat /
AttachmentList renders. Editor-only behavior (640px cap, NodeView
centering) stays under the `.rich-text-editor` scope.
- A `data-clickable` attribute carries the "this image is clickable
to preview" hint that the readonly cursor rule used to key off the
`.rich-text-editor.readonly` scope.
- ImageView NodeViewWrapper no longer adds its own `image-node` class
because `<Attachment>` already emits one; the duplicate was harmless
but redundant.
Visual: editor + readonly comments render identical to before. Chat /
AttachmentList previously rendered a gray file card for images (the
P0 fix in the parent commit) and now match the editor visual without
the heavy-handed Tailwind detour.
Tests: 98 attachment-related tests pass; full `pnpm typecheck` + `pnpm
test` (652 tests) green.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent root causes made "Open in new tab" on a desktop
attachment-preview modal feel like "the popup is still there and the
current tab got replaced":
1. `AttachmentPreviewModal.handleOpenInNewTab` never called `onClose()`,
so the modal stayed mounted over the new tab.
2. Both `DesktopNavigationProvider.openInNewTab` and
`TabNavigationProvider.openInNewTab` called
`store.setActiveTab(tabId)` after `store.openTab(...)`, which stole
focus to the new tab — violating the type contract
("Desktop only: open a path in a new background tab") and matching
neither Chrome's cmd+click default nor the user's expectation.
Fixes:
- Modal: always call `onClose()` after dispatching the navigation
(desktop adapter path and web `window.open` fallback path).
- Desktop navigation: drop the post-`openTab` `setActiveTab` call in both
providers. `openTab` already preserves `activeTabId` for new paths and
switches to the existing tab when the path is already open, which is
exactly the background-tab semantics the type contract advertises.
Tests:
- `attachment-preview-modal.test.tsx`: assert `onClose` is invoked on
both the desktop and web fallback branches.
- `pageview-tracker.test.tsx`: rename the "openInNewTab / addTab" case
so the comment no longer claims `openInNewTab` activates the new tab.
- New `apps/desktop/.../platform/navigation.test.tsx`: assert that
`openInNewTab` on both providers calls `openTab` and never
`setActiveTab` for same-workspace paths, and routes cross-workspace
paths through `switchWorkspace`.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
HTML attachment previews mount the document inside a sandboxed
`<iframe srcdoc>` deliberately WITHOUT `allow-same-origin` — uploads are
untrusted user content. Chromium treats fragment-link clicks inside such an
opaque-origin srcdoc iframe as cross-origin frame navigation and silently
rejects them, so clicking a TOC entry never scrolls.
Append a tiny shim script to the srcdoc that intercepts `<a href="#...">`
clicks inside the iframe and calls `scrollIntoView` directly. The shim runs
in the iframe's own opaque origin under `allow-scripts` — no new
capabilities, no sandbox token changes; it cannot reach parent / cookies /
localStorage.
All three HTML attachment surfaces share the same helper:
- inline 480px card (html-attachment-preview.tsx)
- full-screen modal (attachment-preview-modal.tsx)
- full-page route (attachment-preview-page.tsx)
References: whatwg/html#3537, crbug 40191760.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Collapse the five separate attachment render paths (file-card NodeView,
image NodeView, readonly markdown img/fileCard renderers, AttachmentList
standalone fallback, and the parallel packages/ui/markdown renderer) into
one <Attachment attachment={a} /> dispatcher.
Fixes a P0 visual regression: a PNG attached to a message but not inlined
in the markdown body used to render as a gray "file card" because
getPreviewKind() lacked an "image" branch and image rendering bypassed
the dispatcher entirely. Now every surface routes through <Attachment>,
so the same PNG renders as a real <img> with hover toolbar and
preview-modal everywhere.
Key changes:
- PreviewKind gains "image"; getPreviewKind() detects image/* + common
extensions before the html/text branches (so svg stays image, not text).
- AttachmentPreviewModal gains case "image" (replaces the standalone
ImageLightbox, which is deleted).
- New packages/views/editor/attachment.tsx owns all kind-aware routing
(image | html | file) and dispatches preview modal + download via the
existing useAttachmentPreview / useDownloadAttachment hooks. Subsumes
the deleted AttachmentBlock.
- AttachmentInput.url accepts a forceKind hint so callers that *know*
the structural kind (markdown , Tiptap image node) skip the
filename-based autodetect — fixes a regression where empty or
descriptive alt text would route an image to the file-card chrome.
- Tiptap NodeViews (file-card.tsx, image-view.tsx) shrink to thin
wrappers that forward editor hints (selected, deleteNode, uploading)
to <Attachment>.
- ReadonlyContent and AttachmentList each mount their own
AttachmentDownloadProvider so url → record resolution works outside
ContentEditor's provider.
- packages/ui/markdown gains optional renderImage / renderFileCard slot
props; packages/views/common/markdown.tsx injects <Attachment> into
those slots and threads message attachments through to chat /
skill-file viewers.
- chat-message-list passes message.attachments to every <Markdown> call
site and renders a standalone AttachmentList under each bubble for
attachments not referenced in the body.
Tests: attachment.test.tsx covers 9 scenarios (record image / pdf / html;
url-only image with resolver hit and miss; uploading state; editable
delete; forceKind regression). attachment-preview-modal.test.tsx gains
image-dispatch cases. 652/652 unit tests pass.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a header toggle that lets users flip the agent transcript between
chronological (oldest first, current behavior) and newest-first. The
preference is persisted via a small Zustand store. Default stays
chronological so existing readers see no behavior change.
Sort is a pure presentation concern — the underlying timeline (seq
numbers, filter keys, segment navigation) is untouched. Toggling resets
the scroll container to the top so the user lands on the newest end of
the chosen direction. Copy-all respects the displayed order so the
exported text matches what's on screen.
Scope is limited to the task transcript dialog per the MVP plan; the
issue execution log and agent activity tab are out of scope and may be
revisited once this interaction validates.
Closes GH #2736.
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): add Project Gantt view (MUL-1881)
Adds Gantt as a third option in the Project page's view toggle (Board /
List / Gantt). Bars span start_date → due_date; issues with only one
date render as markers, issues with neither are collapsed into an
Unscheduled section. Toolbar exposes day/week/month zoom and a
show-completed toggle. The Gantt view shares the existing IssuesHeader
filters/sort.
Implementation is self-rendered SVG/HTML — no new dependencies. UTC
day-aligned date math keeps bars on the right columns regardless of
viewer timezone.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): scope Gantt to project surface + warn on hidden pages
- IssuesHeader / IssueDisplayControls now take `allowGantt` (default false);
only Project Detail opts in. /issues, /my-issues and the actor panel no
longer expose a Gantt option that silently fell through to List, and the
toggle icon falls back to List when a stored `viewMode === "gantt"` lands
on a surface that doesn't render it.
- Project Gantt now surfaces a banner with hidden-issue count plus a
Load-all action that drains every remaining paginated page into the
cache via the new `useLoadAllRemaining` helper. Pagination summary comes
from `myIssueListPaginationOptions`, which shares the existing cache key
with `myIssueListOptions` so totals stay in sync with Board/List.
- ScheduledRow normalizes a `start_date > due_date` anomaly to min/max and
outlines the bar with a destructive ring + tooltip note, instead of
silently dropping the row.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): simplify runtime bootstrap
* fix(onboarding): close private-helper reuse hole and guide-issue nav race
- server: when bootstrap looks for an existing Multica Helper, require
Visibility="workspace" so a private helper owned by another member
can't be auto-assigned to the onboarding issue (and trigger a task as
that private agent), which would have bypassed canAccessPrivateAgent.
- web onboarding page: refreshMe() inside bootstrap flips hasOnboarded
before onComplete fires, letting the guard's router.replace overtake
onComplete's router.push to the new guide issue. Mark the page as
"completing" right before navigating so the guard stays silent during
the in-flight transition.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): escape daemon command literals to satisfy i18next/no-literal-string
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Lambda <lambda@multica.ai>