Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions apps/microbridge-ui/src/components/IntegrationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ReactNode } from "react";
import type { ThemeTokens } from "../lib/theme";
import {
TRAFFIC_COLORS,
type TrafficLight,
} from "../lib/hosts";

export function IntegrationCard({
name,
badge,
diagnostic,
light,
label,
theme,
children,
}: {
name: string;
badge?: string;
diagnostic: string;
light: TrafficLight;
label: string;
theme: ThemeTokens;
children?: ReactNode;
}) {
const colors = TRAFFIC_COLORS[light];
return (
<li
className="rounded-xl px-3 py-3"
style={{
backgroundColor: theme.panel,
border: `1px solid ${theme.hairline}`,
}}
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2 text-[12.5px] font-medium">
<span
className="inline-block h-2 w-2 shrink-0 rounded-full"
style={{ backgroundColor: colors.dot }}
aria-hidden
/>
{name}
{badge ? (
<span
className="rounded-full px-2 py-0.5 text-[9.5px] capitalize"
style={{
backgroundColor: theme.hoverBg,
color: theme.textSecondary,
}}
>
{badge}
</span>
) : null}
</div>
<div
className="mt-1 text-[11px]"
style={{ color: theme.textSecondary }}
>
{diagnostic}
</div>
</div>
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium"
style={{ backgroundColor: colors.bg, color: colors.fg }}
>
{label}
</span>
</div>
{children}
</li>
);
}
105 changes: 105 additions & 0 deletions apps/microbridge-ui/src/lib/hosts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it } from "vitest";

import type { AdapterStatus, SessionStatus } from "./types";
import { hostPresence, integrationView } from "./hosts";

function session(app: string, id = "1"): SessionStatus {
return {
id,
app,
title: "Thread",
state: "working",
updated_at_ms: 100,
};
}

function adapter(
partial: Partial<AdapterStatus> & Pick<AdapterStatus, "id" | "display_name" | "state">,
): AdapterStatus {
return {
kind: "native",
capabilities: {
lifecycle_observation: true,
approval_acceptance: false,
approval_rejection: false,
interrupt: false,
new_session: false,
focus_open: false,
reasoning_effort: false,
},
diagnostic: "Built-in lifecycle watcher is active.",
...partial,
};
}

describe("hostPresence", () => {
it("counts sessions for one app", () => {
const presence = hostPresence(
[session("Synara", "a"), session("Codex CLI", "b"), session("Synara", "c")],
"Synara",
);
expect(presence.count).toBe(2);
});
});

describe("integrationView", () => {
it("marks Synara green when sessions are live", () => {
const view = integrationView(
adapter({ id: "synara", display_name: "Synara", state: "connected" }),
[session("Synara")],
);
expect(view.light).toBe("green");
expect(view.label).toContain("Active");
expect(view.connectedGroup).toBe(true);
expect(view.diagnostic).toContain("no separate adapter");
});

it("marks Synara yellow while waiting for sessions", () => {
const view = integrationView(
adapter({ id: "synara", display_name: "Synara", state: "connected" }),
[],
);
expect(view.light).toBe("yellow");
expect(view.label).toBe("Waiting");
expect(view.connectedGroup).toBe(false);
});

it("flags disabled Cursor when journal sessions already exist", () => {
const view = integrationView(
adapter({
id: "cursor",
display_name: "Cursor",
kind: "community",
state: "disabled",
diagnostic: "Disabled until you explicitly enable this integration.",
}),
[session("Cursor"), session("Cursor", "2")],
);
expect(view.light).toBe("yellow");
expect(view.diagnostic).toContain("2 threads auto-detected");
});

it("keeps Claude Code green when the watcher is connected", () => {
const view = integrationView(
adapter({ id: "claude", display_name: "Claude Code", state: "connected" }),
[],
);
expect(view.light).toBe("green");
expect(view.label).toBe("Connected");
});

it("maps adapter errors to red", () => {
const view = integrationView(
adapter({
id: "t3code",
display_name: "T3 Code",
kind: "community",
state: "error",
diagnostic: "Pairing failed.",
}),
[],
);
expect(view.light).toBe("red");
expect(view.label).toBe("Error");
});
});
171 changes: 171 additions & 0 deletions apps/microbridge-ui/src/lib/hosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import type {
AdapterConnectionState,
AdapterStatus,
SessionStatus,
} from "./types";

/** Traffic-light glance for Integrations cards. */
export type TrafficLight = "green" | "yellow" | "red";

/**
* Hosts that ride Claude/Codex journals — always listed as Integrations cards,
* but status is session-derived (no separate pairable adapter).
*/
export const HOST_ATTRIBUTED: ReadonlyArray<{
id: string;
app: string;
}> = [
{ id: "synara", app: "Synara" },
{ id: "chatgpt", app: "ChatGPT" },
{ id: "claude_desktop", app: "Claude Desktop" },
{ id: "conductor", app: "Conductor" },
];

/** Opt-in sources that can still receive journal-attributed sessions before enable. */
const JOURNAL_APP_BY_ADAPTER: Record<string, string> = {
cursor: "Cursor",
t3code: "T3 Code",
factory: "Factory",
};

export interface HostPresence {
count: number;
lastActivityMs: number | null;
}

export function hostPresence(
sessions: SessionStatus[],
appName: string,
): HostPresence {
let count = 0;
let lastActivityMs: number | null = null;
for (const session of sessions) {
if (session.app !== appName) continue;
count += 1;
if (lastActivityMs === null || session.updated_at_ms > lastActivityMs) {
lastActivityMs = session.updated_at_ms;
}
}
return { count, lastActivityMs };
}

export function isHostAttributed(adapterId: string): boolean {
return HOST_ATTRIBUTED.some((host) => host.id === adapterId);
}

function journalAppFor(adapterId: string): string | undefined {
return (
HOST_ATTRIBUTED.find((host) => host.id === adapterId)?.app ??
JOURNAL_APP_BY_ADAPTER[adapterId]
);
}

export interface IntegrationView {
light: TrafficLight;
label: string;
diagnostic: string;
/** True when the card should sit in the Connected group. */
connectedGroup: boolean;
}

const STATE_LABELS: Record<AdapterConnectionState, string> = {
disabled: "Not connected",
needs_setup: "Waiting",
connecting: "Connecting",
connected: "Connected",
limited: "Limited",
incompatible: "Incompatible",
error: "Error",
};

function lightForState(state: AdapterConnectionState): TrafficLight {
if (state === "connected") return "green";
if (state === "error" || state === "incompatible") return "red";
return "yellow";
}

/**
* Derive the card's traffic light, label, and diagnostic from daemon adapter
* state plus live session attribution.
*/
export function integrationView(
adapter: AdapterStatus,
sessions: SessionStatus[],
): IntegrationView {
const journalApp = journalAppFor(adapter.id);
const presence = journalApp
? hostPresence(sessions, journalApp)
: { count: 0, lastActivityMs: null };

if (isHostAttributed(adapter.id)) {
if (adapter.state === "disabled") {
return {
light: "yellow",
label: "Not connected",
diagnostic: "Disabled in Microbridge configuration.",
connectedGroup: false,
};
}
if (adapter.state === "error" || adapter.state === "incompatible") {
return {
light: "red",
label: STATE_LABELS[adapter.state],
diagnostic: adapter.diagnostic,
connectedGroup: false,
};
}
if (presence.count > 0) {
return {
light: "green",
label:
presence.count === 1
? "Active · 1 thread"
: `Active · ${presence.count} threads`,
diagnostic:
"via Claude & Codex journals — no separate adapter needed.",
connectedGroup: true,
};
}
return {
light: "yellow",
label: "Waiting",
diagnostic:
"via Claude & Codex journals — no separate adapter needed. Waiting for sessions.",
connectedGroup: false,
};
}

// Opt-in sources: journal sessions already flowing while the card is disabled.
if (
adapter.state === "disabled" &&
presence.count > 0 &&
JOURNAL_APP_BY_ADAPTER[adapter.id]
) {
return {
light: "yellow",
label: "Not connected",
diagnostic:
presence.count === 1
? "1 thread auto-detected — enable for controls."
: `${presence.count} threads auto-detected — enable for controls.`,
connectedGroup: false,
};
}

const light = lightForState(adapter.state);
return {
light,
label: STATE_LABELS[adapter.state],
diagnostic: adapter.diagnostic,
connectedGroup: light === "green",
};
}

export const TRAFFIC_COLORS: Record<
TrafficLight,
{ bg: string; fg: string; dot: string }
> = {
green: { bg: "#30C4631F", fg: "#30A653", dot: "#30C463" },
yellow: { bg: "#FFB0001F", fg: "#C48400", dot: "#FFB000" },
red: { bg: "#FF453A1F", fg: "#D93A32", dot: "#FF453A" },
};
Loading
Loading