Files
multica/packages/views/common/prop-row.tsx
Naiyuan Qing 281779330e feat(chat): no-agent disabled state with onboarding fix and editor cleanup (#1919)
* fix(onboarding): refresh agent cache after import and agent creation

Two paths could leave the workspace agent-list query cache stale by the
time the dashboard rendered the welcome issue, causing the issue's
agent assignee to resolve to "Unknown Agent":

1. StarterContentPrompt.onImport invalidated pins/projects/issues but
   not agents, and didn't await any of them before navigating — so the
   issue-detail page could mount and read the cache before TanStack
   Query had marked the relevant queries stale.
2. OnboardingFlow.handleAgentCreated created the agent without
   invalidating the agent list, so the dashboard's first mount would
   read whatever was already cached from earlier in onboarding.

Both now invalidate workspaceKeys.agents, and the import flow awaits
all invalidations via Promise.all before pushing the navigation, so
the next page mount always refetches.

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

* refactor(editor): drop editable prop, ContentEditor is editing-only

ContentEditor's `editable` prop had zero true callsites left in the
codebase — every read-only surface had migrated to ReadonlyContent
(react-markdown), and the prop only invited misuse: Tiptap's
`useEditor` reads `editable` at mount, so callers that toggled it
post-mount (like a chat input that needs to disable on no-agent)
silently got stuck in whichever mode the editor first created.

Changes:
- Remove `editable` prop and default; useEditor and createEditorExtensions
  no longer take it.
- Remove the `"readonly"` className branch and the readonly content sync
  useEffect (only the editing path remains).
- Remove the BubbleMenu and mouseDown editable guards.
- Drop LinkReadonly; rename LinkEditable to LinkExtension and use it
  unconditionally.
- Update the docstring to point readers at ReadonlyContent for display
  surfaces.

ReadonlyContent's `.readonly` CSS class stays in content-editor.css —
that file's selectors are still used by react-markdown's wrapper.

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

* feat(chat): empty-state by session history, no-agent disabled state

Three independent improvements to the chat window's pre-conversation
states, sharing a new three-state availability primitive:

1. New `useWorkspaceAgentAvailability()` hook (`"loading" | "none" |
   "available"`) so callers don't have to reinvent the loading-vs-empty
   distinction. Treating loading as "no agent" — the easy mistake —
   caused the chat input to flash a fake disabled state for the few
   hundred ms after mount, even when the workspace had agents.
2. EmptyState now branches on session history, not agent presence:
   never-chatted users get a short pitch ("They know your workspace —
   issues, projects, skills"), returning users get the existing
   starter prompts. Missing-agent feedback moved to the banner above
   the input, keeping this surface focused on "what is chat for".
3. No-agent disabled state: when availability resolves to "none",
   ChatInput dims and stops responding to clicks/keys, with cursor
   `not-allowed` on hover. The disable lives at the wrapper level
   (`pointer-events-none` on the inner card, `cursor-not-allowed` on
   the outer one — splitting layers so hover bubbles to where the
   browser reads cursor) — we no longer reach into the editor's
   editable mode, which never switched cleanly post-mount anyway.
   A `<NoAgentBanner>` (sibling of OfflineBanner, mutually exclusive)
   states the prerequisite without linking out — no one should be
   pulled out of chat mid-thought to a settings page.

Also: default chat width 420 → 380, since the chat docks at the
bottom-right and 420 was crowding everything else.

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

* refactor(views): align PropRow labels using CSS subgrid

The fixed `w-16` (64px) label column on PropRow broke whenever a label
rendered wider than 64px (e.g. "Concurrency" in the agent inspector) —
the label would overflow into the gap and collide with the value.

Switch to subgrid: the parent declares `grid grid-cols-[auto_1fr]` and
each PropRow becomes `col-span-2 grid grid-cols-subgrid`. The `auto`
track sizes to the widest label across all rows in that parent, so
labels always fit and value columns stay aligned across rows without
picking a magic pixel width.

Updated parents:
- agent-detail-inspector Section wrapper
- issue-detail Properties group

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:32:35 +08:00

53 lines
1.8 KiB
TypeScript

import type { ReactNode } from "react";
/**
* Two-column property row used in detail-page sidebars: a muted label on the
* left and a flexible value on the right.
*
* Uses **subgrid**, so the parent must declare the column tracks:
*
* <div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5">
* <PropRow label="…">…</PropRow>
* <PropRow label="…">…</PropRow>
* </div>
*
* The `auto` track sizes to the widest label across all rows in the parent
* grid, so labels always fit and values stay aligned across rows without
* picking a magic pixel width. Earlier versions used a fixed `w-16` label;
* that broke whenever a label (e.g. "Concurrency") rendered wider than 64px
* — the label would overflow into the gap and collide with the value.
*
* `interactive` (default `true`) controls whether the row gets a hover
* highlight. Most rows wrap a Picker/Popover trigger and are clickable
* anywhere across the row, so the highlight tells users "this is one
* target". Read-only rows (Owner / Created / Updated) should pass
* `interactive={false}` so they don't pretend to be clickable when they
* aren't.
*
* Used by:
* - issue detail sidebar (Status / Priority / Assignee / …)
* - agent detail inspector (Runtime / Model / Visibility / …)
*/
export function PropRow({
label,
children,
interactive = true,
}: {
label: string;
children: ReactNode;
interactive?: boolean;
}) {
return (
<div
className={`-mx-2 col-span-2 grid min-h-8 grid-cols-subgrid items-center rounded-md px-2 ${
interactive ? "transition-colors hover:bg-accent/50" : ""
}`}
>
<span className="text-xs text-muted-foreground">{label}</span>
<div className="flex min-w-0 items-center gap-1.5 truncate text-xs">
{children}
</div>
</div>
);
}