style: formatter pass on bridge + web views
This commit is contained in:
@@ -22,7 +22,9 @@ async function runRepoSetup() {
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
console.error(`[lvmh-bridge] setup timed out after ${SETUP_TIMEOUT_MS}ms, killing`);
|
||||
console.error(
|
||||
`[lvmh-bridge] setup timed out after ${SETUP_TIMEOUT_MS}ms, killing`,
|
||||
);
|
||||
child.kill("SIGKILL");
|
||||
resolve(124);
|
||||
}, SETUP_TIMEOUT_MS);
|
||||
|
||||
+69
-15
@@ -1,5 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import {
|
||||
NavLink,
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import ChatView from "./ChatView";
|
||||
import SessionsView from "./SessionsView";
|
||||
import SpawnView from "./SpawnView";
|
||||
@@ -19,14 +25,20 @@ export default function App() {
|
||||
}, [location.pathname]);
|
||||
|
||||
if (!configured) return <SettingsGate onSaved={() => setConfigured(true)} />;
|
||||
if (store === null) return <SettingsGate onSaved={() => setConfigured(true)} />;
|
||||
if (store === null)
|
||||
return <SettingsGate onSaved={() => setConfigured(true)} />;
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<nav className={classNames("sidebar", sidebarOpen && "open")} aria-label="Sessions">
|
||||
<nav
|
||||
className={classNames("sidebar", sidebarOpen && "open")}
|
||||
aria-label="Sessions"
|
||||
>
|
||||
<div className="sidebar-header">
|
||||
<div className="brand">
|
||||
<span className="logo" aria-hidden="true">L</span>
|
||||
<span className="logo" aria-hidden="true">
|
||||
L
|
||||
</span>
|
||||
lvmh
|
||||
</div>
|
||||
<span
|
||||
@@ -36,22 +48,37 @@ export default function App() {
|
||||
aria-label={`connection ${store.state}`}
|
||||
/>
|
||||
</div>
|
||||
<NavLink to="/new" className={({ isActive }) => classNames("new-chat-btn", isActive && "active")}>
|
||||
<NavLink
|
||||
to="/new"
|
||||
className={({ isActive }) =>
|
||||
classNames("new-chat-btn", isActive && "active")
|
||||
}
|
||||
>
|
||||
+ Spawn session
|
||||
</NavLink>
|
||||
<div className="sidebar-sessions">
|
||||
{[...store.sessions]
|
||||
.filter((s) => s.online)
|
||||
.sort((a, b) => (b.lastEventAt ?? b.startedAt) - (a.lastEventAt ?? a.startedAt))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.lastEventAt ?? b.startedAt) - (a.lastEventAt ?? a.startedAt),
|
||||
)
|
||||
.map((s) => (
|
||||
<NavLink
|
||||
key={s.id}
|
||||
to={`/s/${s.id}`}
|
||||
className={({ isActive }) => classNames("session-link", isActive && "active")}
|
||||
className={({ isActive }) =>
|
||||
classNames("session-link", isActive && "active")
|
||||
}
|
||||
style={{ display: "flex", alignItems: "center", gap: 6 }}
|
||||
>
|
||||
<span className={classNames("online-dot", s.online && "on")} style={{ display: "inline-block" }} />
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{s.name ?? s.repo ?? s.id}</span>
|
||||
<span
|
||||
className={classNames("online-dot", s.online && "on")}
|
||||
style={{ display: "inline-block" }}
|
||||
/>
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{s.name ?? s.repo ?? s.id}
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
@@ -70,27 +97,54 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
{sidebarOpen && <div className="sidebar-backdrop" onClick={() => setSidebarOpen(false)} />}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="sidebar-backdrop"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<main className="main">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn menu-btn"
|
||||
aria-label="Open menu"
|
||||
style={{ display: "none", padding: "8px 12px", alignSelf: "flex-start", borderRadius: 0 }}
|
||||
style={{
|
||||
display: "none",
|
||||
padding: "8px 12px",
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 0,
|
||||
}}
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<Routes>
|
||||
<Route path="/" element={<SessionsView sessions={store.sessions} onChanged={() => void store.refresh()} pushToast={push} />} />
|
||||
<Route path="/s/:id" element={<ChatView store={store} pushToast={push} />} />
|
||||
<Route path="/new" element={<SpawnView store={store} pushToast={push} />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<SessionsView
|
||||
sessions={store.sessions}
|
||||
onChanged={() => void store.refresh()}
|
||||
pushToast={push}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/s/:id"
|
||||
element={<ChatView store={store} pushToast={push} />}
|
||||
/>
|
||||
<Route
|
||||
path="/new"
|
||||
element={<SpawnView store={store} pushToast={push} />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<div className="toasts" role="status">
|
||||
{toasts.map((t) => (
|
||||
<div key={t.id} className="toast">{t.text}</div>
|
||||
<div key={t.id} className="toast">
|
||||
{t.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+36
-9
@@ -3,7 +3,12 @@ import { Link, useParams } from "react-router-dom";
|
||||
import type { EventFrame } from "./protocol";
|
||||
import { Route } from "./protocol";
|
||||
import { ApiError, errMessage, fetchJson } from "./api";
|
||||
import { deriveChat, deriveTasks, lastPersistedSeq, mergeEvents } from "./derive";
|
||||
import {
|
||||
deriveChat,
|
||||
deriveTasks,
|
||||
lastPersistedSeq,
|
||||
mergeEvents,
|
||||
} from "./derive";
|
||||
import type { SessionsStore } from "./store";
|
||||
import { classNames } from "./store";
|
||||
import ChatStream from "./ChatStream";
|
||||
@@ -49,7 +54,10 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
const applyEvents = useCallback((incoming: EventFrame[]): void => {
|
||||
setEvents((prev) => {
|
||||
const merged = mergeEvents(prev, incoming);
|
||||
lastSeqRef.current = Math.max(lastSeqRef.current, lastPersistedSeq(merged));
|
||||
lastSeqRef.current = Math.max(
|
||||
lastSeqRef.current,
|
||||
lastPersistedSeq(merged),
|
||||
);
|
||||
return merged;
|
||||
});
|
||||
}, []);
|
||||
@@ -64,7 +72,9 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
let alive = true;
|
||||
void (async () => {
|
||||
try {
|
||||
const evts = await fetchJson<EventFrame[]>(`${Route.SessionEvents(sessionId)}?after=0&limit=${HISTORY_LIMIT}`);
|
||||
const evts = await fetchJson<EventFrame[]>(
|
||||
`${Route.SessionEvents(sessionId)}?after=0&limit=${HISTORY_LIMIT}`,
|
||||
);
|
||||
if (!alive) return;
|
||||
applyEvents(evts);
|
||||
} catch (err) {
|
||||
@@ -88,7 +98,9 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
useEffect(() => {
|
||||
if (store.state !== "open" || !loadedRef.current) return;
|
||||
const after: number = lastSeqRef.current;
|
||||
void fetchJson<EventFrame[]>(`${Route.SessionEvents(sessionId)}?after=${after}&limit=${HISTORY_LIMIT}`)
|
||||
void fetchJson<EventFrame[]>(
|
||||
`${Route.SessionEvents(sessionId)}?after=${after}&limit=${HISTORY_LIMIT}`,
|
||||
)
|
||||
.then(applyEvents)
|
||||
.catch(() => undefined);
|
||||
}, [store.state, sessionId, applyEvents]);
|
||||
@@ -116,7 +128,8 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 409) pushToast("session offline");
|
||||
if (err instanceof ApiError && err.status === 409)
|
||||
pushToast("session offline");
|
||||
else pushToast(errMessage(err));
|
||||
} finally {
|
||||
setSending(false);
|
||||
@@ -150,10 +163,15 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
<div className="chat-layout">
|
||||
<div className="chat-col">
|
||||
<div className="chat-header">
|
||||
<div className="title">{session?.name ?? session?.repo ?? sessionId}</div>
|
||||
<div className="title">
|
||||
{session?.name ?? session?.repo ?? sessionId}
|
||||
</div>
|
||||
<div className="sub">{session?.model}</div>
|
||||
<span
|
||||
className={classNames("conn-dot", session?.online === true ? "open" : "closed")}
|
||||
className={classNames(
|
||||
"conn-dot",
|
||||
session?.online === true ? "open" : "closed",
|
||||
)}
|
||||
title={session?.online === true ? "online" : "offline"}
|
||||
/>
|
||||
<div className="spacer" />
|
||||
@@ -188,7 +206,11 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
<Link to="/">Back to sessions</Link>
|
||||
</div>
|
||||
) : (
|
||||
<ChatStream messages={chat.messages} tools={chat.tools} busy={chat.busy} />
|
||||
<ChatStream
|
||||
messages={chat.messages}
|
||||
tools={chat.tools}
|
||||
busy={chat.busy}
|
||||
/>
|
||||
)}
|
||||
<div className="composer">
|
||||
<div className="composer-inner">
|
||||
@@ -202,7 +224,12 @@ export default function ChatView({ store, pushToast }: Props) {
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
{chat.busy ? (
|
||||
<button type="button" className="abort-btn" aria-label="Abort current run" onClick={() => void abort()}>
|
||||
<button
|
||||
type="button"
|
||||
className="abort-btn"
|
||||
aria-label="Abort current run"
|
||||
onClick={() => void abort()}
|
||||
>
|
||||
■ stop
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -11,12 +11,19 @@ interface Props {
|
||||
pushToast: (text: string) => void;
|
||||
}
|
||||
|
||||
export default function SessionsView({ sessions, onChanged, pushToast }: Props) {
|
||||
export default function SessionsView({
|
||||
sessions,
|
||||
onChanged,
|
||||
pushToast,
|
||||
}: Props) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const open = useCallback((id: string): void => {
|
||||
const open = useCallback(
|
||||
(id: string): void => {
|
||||
navigate(`/s/${id}`);
|
||||
}, [navigate]);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const stop = async (e: MouseEvent, s: SessionListItem): Promise<void> => {
|
||||
e.preventDefault();
|
||||
@@ -26,7 +33,9 @@ export default function SessionsView({ sessions, onChanged, pushToast }: Props)
|
||||
pushToast(`stopped ${s.name ?? s.id}`);
|
||||
onChanged();
|
||||
} catch (err) {
|
||||
pushToast(`stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
pushToast(
|
||||
`stop failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,12 +43,16 @@ export default function SessionsView({ sessions, onChanged, pushToast }: Props)
|
||||
// reachable by direct URL and are dropped from the default list.
|
||||
const sorted = [...sessions]
|
||||
.filter((s) => s.online)
|
||||
.sort((a, b) => (b.lastEventAt ?? b.startedAt) - (a.lastEventAt ?? a.startedAt));
|
||||
.sort(
|
||||
(a, b) => (b.lastEventAt ?? b.startedAt) - (a.lastEventAt ?? a.startedAt),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Active sessions</h1>
|
||||
{sorted.length === 0 && <p className="empty">No active sessions. Spawn one from the sidebar.</p>}
|
||||
{sorted.length === 0 && (
|
||||
<p className="empty">No active sessions. Spawn one from the sidebar.</p>
|
||||
)}
|
||||
{sorted.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
|
||||
Reference in New Issue
Block a user