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
11 changes: 11 additions & 0 deletions internal/dashboard/webui/app-src/src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function ChatPanel({
persistKey,
canAct = false,
onConvMeta,
onBusyChange,
}: {
wsPath: string;
getCtx?: () => string;
Expand All @@ -70,6 +71,10 @@ export function ChatPanel({
// Fires once per conversation with its captured id + stable UUID (from the
// server's conv_meta frame). The host chat header shows the UUID.
onConvMeta?: (meta: { convId: number; convUuid: string }) => void;
// Fires whenever the turn state flips: true while a turn is streaming
// ("working"), false when idle ("waiting"). Lets a parent (e.g. the conductor
// rail) show a live working/waiting dot per chat, like the Work tab does.
onBusyChange?: (busy: boolean) => void;
}) {
const msgsKey = persistKey ? `corral.chat.msgs.${persistKey}` : "";
const sidKey = persistKey ? `corral.chat.sid.${persistKey}` : "";
Expand Down Expand Up @@ -103,6 +108,12 @@ export function ChatPanel({
// it to the effect deps (which would needlessly reconnect the socket).
const onConvMetaRef = useRef(onConvMeta);
onConvMetaRef.current = onConvMeta;
// Report busy transitions (working ↔ waiting) to the parent for a live dot.
const onBusyChangeRef = useRef(onBusyChange);
onBusyChangeRef.current = onBusyChange;
useEffect(() => {
onBusyChangeRef.current?.(busy);
}, [busy]);
// The Claude session id (persisted), passed as ?resume= on (re)connect.
const sessionIdRef = useRef<string>(persistKey ? localStorage.getItem(sidKey) || "" : "");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ type GlobalResp = { api_writes_enabled: boolean; api_writes_configured: boolean
export function FirstRunChat({
onConvMeta,
persistKey = "global",
onBusyChange,
}: {
onConvMeta?: (meta: { convId: number; convUuid: string }) => void;
// localStorage key for this conductor's transcript + session, so multiple
// global conductors each keep their own conversation. Defaults to "global".
persistKey?: string;
// Bubbled up from ChatPanel so a conductor rail can show a working/waiting dot.
onBusyChange?: (busy: boolean) => void;
} = {}) {
const [cap, setCap] = useState<CapResp | null>(null);
const [writes, setWrites] = useState<GlobalResp | null>(null);
Expand Down Expand Up @@ -49,6 +52,7 @@ export function FirstRunChat({
persistKey={persistKey}
canAct={cap.capability === "act"}
onConvMeta={onConvMeta}
onBusyChange={onBusyChange}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export function GlobalConductors({ onConvMeta }: { onConvMeta?: (meta: { convId:
// existing single-conductor transcript from before this multi-conductor UI.
const [list, setList] = usePersistentState<Conductor[]>(LIST_KEY, [{ id: "c1", label: "Conductor 1" }]);
const [active, setActive] = useState<string>(() => list[0]?.id || "c1");
// Live per-conductor turn state (working ↔ waiting), reported by each mounted
// ChatPanel. All conductors stay mounted, so a background one that's mid-turn
// still shows a working dot.
const [busyById, setBusyById] = useState<Record<string, boolean>>({});
const setBusy = (id: string, busy: boolean) => setBusyById((m) => (m[id] === busy ? m : { ...m, [id]: busy }));

const [railWidth, setRailWidth] = usePersistentState<number>(RAIL_W_KEY, RAIL_W_DEFAULT);
const railResizeRef = useDragResize({
Expand Down Expand Up @@ -89,7 +94,7 @@ export function GlobalConductors({ onConvMeta }: { onConvMeta?: (meta: { convId:
</button>
</div>
<div className="conductors-view">
<FirstRunChat persistKey={persistKeyFor(only.id)} onConvMeta={onConvMeta} />
<FirstRunChat persistKey={persistKeyFor(only.id)} onConvMeta={onConvMeta} onBusyChange={(b) => setBusy(only.id, b)} />
</div>
</div>
);
Expand All @@ -104,11 +109,16 @@ export function GlobalConductors({ onConvMeta }: { onConvMeta?: (meta: { convId:
+ New
</button>
</div>
{list.map((c, i) => (
{list.map((c, i) => {
const working = !!busyById[c.id];
return (
<div key={c.id} className={`work-rail-item${active === c.id ? " active" : ""}`}>
<button type="button" className="work-rail-btn" onClick={() => setActive(c.id)}>
<span className="work-rail-label">{c.label || `Conductor ${i + 1}`}</span>
<span className="work-rail-status">global · host</span>
<span className="work-rail-label">
<i className={`work-dot ${working ? "running" : "idle"}`} />
{c.label || `Conductor ${i + 1}`}
</span>
<span className="work-rail-status">{working ? "working" : "waiting"}</span>
</button>
<button
type="button"
Expand All @@ -119,7 +129,8 @@ export function GlobalConductors({ onConvMeta }: { onConvMeta?: (meta: { convId:
</button>
</div>
))}
);
})}
</div>

<div className="work-rail-resize" ref={railResizeRef} title="Drag to resize the list" />
Expand All @@ -132,7 +143,11 @@ export function GlobalConductors({ onConvMeta }: { onConvMeta?: (meta: { convId:
key={c.id}
style={{ display: active === c.id ? "flex" : "none", flex: 1, minHeight: 0, flexDirection: "column" }}
>
<FirstRunChat persistKey={persistKeyFor(c.id)} onConvMeta={active === c.id ? onConvMeta : undefined} />
<FirstRunChat
persistKey={persistKeyFor(c.id)}
onConvMeta={active === c.id ? onConvMeta : undefined}
onBusyChange={(b) => setBusy(c.id, b)}
/>
</div>
))}
</div>
Expand Down
Loading
Loading