diff --git a/apps/microbridge-ui/src/components/IntegrationCard.tsx b/apps/microbridge-ui/src/components/IntegrationCard.tsx index 3b92751..fe928f3 100644 --- a/apps/microbridge-ui/src/components/IntegrationCard.tsx +++ b/apps/microbridge-ui/src/components/IntegrationCard.tsx @@ -5,68 +5,142 @@ import { type TrafficLight, } from "../lib/hosts"; +function TileFace({ + name, + diagnostic, + light, + label, + theme, +}: { + name: string; + diagnostic: string; + light: TrafficLight; + label: string; + theme: ThemeTokens; +}) { + const colors = TRAFFIC_COLORS[light]; + return ( + <> + +
+
+ {name} +
+
+ {label} +
+
+
+ {diagnostic} +
+ + ); +} + export function IntegrationCard({ name, - badge, diagnostic, light, label, theme, - children, + expandable = false, + expanded = false, + onSelect, }: { name: string; - badge?: string; diagnostic: string; light: TrafficLight; label: string; theme: ThemeTokens; - children?: ReactNode; + expandable?: boolean; + expanded?: boolean; + onSelect?: () => void; }) { const colors = TRAFFIC_COLORS[light]; + const shellStyle = { + backgroundColor: theme.panel, + border: `1px solid ${expanded ? colors.dot : theme.hairline}`, + boxShadow: expanded ? `0 0 0 1px ${colors.dot}33` : undefined, + color: theme.text, + } as const; + + const face = ( + + ); + return ( -
  • + {onSelect ? ( + + ) : ( +
    + {face} +
    + )} +
  • + ); +} + +export function IntegrationDetail({ + name, + diagnostic, + theme, + children, +}: { + name: string; + diagnostic: string; + theme: ThemeTokens; + children?: ReactNode; +}) { + return ( +
    -
    -
    -
    - - {name} - {badge ? ( - - {badge} - - ) : null} -
    -
    - {diagnostic} -
    -
    - - {label} - +
    {name}
    +
    + {diagnostic}
    {children} - +
    ); } diff --git a/apps/microbridge-ui/src/surfaces/Settings.tsx b/apps/microbridge-ui/src/surfaces/Settings.tsx index cb768ae..6ca5feb 100644 --- a/apps/microbridge-ui/src/surfaces/Settings.tsx +++ b/apps/microbridge-ui/src/surfaces/Settings.tsx @@ -26,8 +26,15 @@ import { launchAtLoginEnabled, setLaunchAtLogin, } from "../lib/autostart"; -import { IntegrationCard } from "../components/IntegrationCard"; -import { TRAFFIC_COLORS, integrationView } from "../lib/hosts"; +import { + IntegrationCard, + IntegrationDetail, +} from "../components/IntegrationCard"; +import { + TRAFFIC_COLORS, + integrationView, + isHostAttributed, +} from "../lib/hosts"; const LIGHTING_STATES: { id: keyof StateColors; label: string }[] = [ { id: "idle", label: "Idle" }, @@ -126,6 +133,9 @@ export function Settings({ const [pairingUrl, setPairingUrl] = useState(""); const [adapterMessage, setAdapterMessage] = useState(null); const [adapterBusy, setAdapterBusy] = useState>(() => new Set()); + const [selectedIntegration, setSelectedIntegration] = useState( + null, + ); // null until the login item has been read, and permanently null where a login // item is meaningless: outside Tauri, or in a dev build whose executable path // points into `target/debug`. @@ -602,6 +612,8 @@ export function Settings({ .map((adapter) => ({ adapter, view: integrationView(adapter, snapshot.sessions), + actionable: + adapter.kind === "community" && !isHostAttributed(adapter.id), })); const groups = [ { @@ -615,22 +627,28 @@ export function Settings({ items: views.filter((item) => !item.view.connectedGroup), }, ]; + const selectable = views.filter((item) => item.actionable); + const activeId = + selectedIntegration && + selectable.some((item) => item.adapter.id === selectedIntegration) + ? selectedIntegration + : (selectable.find((item) => !item.view.connectedGroup)?.adapter + .id ?? + selectable[0]?.adapter.id ?? + null); + const selected = views.find((item) => item.adapter.id === activeId); return (

    Integrations

    - Every supported app is its own card. Green means live threads or a - healthy watcher; yellow means waiting or setup; red means error. - ChatGPT, Claude Desktop, Synara, Conductor, and CNVS are built in. - Cursor, Factory, T3 Code, and OpenCode connect after you enable them. + Each app is a tile. Green / yellow / red is always visible; hover + for detail. Click Cursor, Factory, T3 Code, or OpenCode to enable + or repair.

    - Synara and the desktop apps share Claude/Codex journals — they do - not need a separate adapter. CNVS-hosted terminals replace matching - raw journal cards while CNVS owns them. OpenCode uses its official - global plugin API. For T3 controls, enable Network access in T3 Code - Settings → Connections, create a link under Authorized clients, then - paste it below. Factory hooks are merged without replacing yours. + Synara and the desktop apps share Claude/Codex journals — no + separate adapter. For T3 controls, enable Network access in T3 Code + Settings → Connections, then paste a pairing link below.

    {adapterMessage && (

    @@ -643,32 +661,51 @@ export function Settings({ {group.label} · {group.items.length}

    - - - ))} + + )} ); })()} diff --git a/apps/microbridge-ui/src/surfaces/surfaces.test.tsx b/apps/microbridge-ui/src/surfaces/surfaces.test.tsx index 9e6564c..e07f51a 100644 --- a/apps/microbridge-ui/src/surfaces/surfaces.test.tsx +++ b/apps/microbridge-ui/src/surfaces/surfaces.test.tsx @@ -118,7 +118,7 @@ describe("Settings", () => { expect(html).toContain("Reset to Codex Defaults"); }); - it("renders unified integration state and capability limits", () => { + it("renders integration tiles with status and a setup detail panel", () => { const html = renderToStaticMarkup( { expect(html).toContain("Lifecycle is connected"); expect(html).toContain("Live state"); expect(html).toContain("Integrations"); + expect(html).toContain("grid-cols-2"); // Synara waits (yellow) with no sessions; CNVS stays connected. expect(html).toContain("Connected · 1"); expect(html).toContain("Not connected · 2"); @@ -140,11 +141,12 @@ describe("Settings", () => { expect(html).toContain("Synara"); expect(html).toContain("no separate adapter needed"); expect(html).toContain("Connected across 3 exact canvas terminal targets"); - expect(html).toContain("✓ Open"); + expect(html).toContain("✓ Live state"); + expect(html).toContain("— Open"); expect(html).toContain("Interrupt"); - expect(html).toContain("OpenCode uses its official"); + // Cursor is auto-selected as the first actionable not-connected tile. expect(html).toContain("Repair bundled integration"); - expect(html).toContain("Green means live"); + expect(html).toContain("hover for detail"); expect(html).not.toContain("Install managed plugin"); expect(html).not.toContain("scaffold only"); expect(html).not.toContain("not production"); diff --git a/docs/design/README.md b/docs/design/README.md index 03a4ad8..fb73810 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -131,10 +131,11 @@ Keyboard setup. Four sections in a left rail: - **Agent Keys** — live "six keys, six threads" view, key source, deck focus mode (auto/pinned), app priority order, approvals-interrupt toggle (policy only — no live approve UI) -- **Integrations** — one card per supported app, grouped by connected / not - connected, with green / yellow / red status. Host-attributed apps (Synara, - ChatGPT, Claude Desktop, Conductor) are status-only; enable / repair / - disconnect / remove apply only to session sources that need them +- **Integrations** — compact square tile grid (2–3 columns), grouped by + connected / not connected. Each tile always shows name + traffic-light status; + hover reveals the diagnostic. Click Cursor / Factory / T3 / OpenCode to open a + detail strip for enable / repair / pair. Host-attributed apps (Synara, + ChatGPT, Claude Desktop, Conductor) are status + hover only - **Device** — Appearance (System/Light/Dark), Lighting (Codex defaults + Phosphor preset + reset), brightness, LED test, sleep timer (default 3 min), firmware, zero-network note