feat: show source inventory and rpc evidence

This commit is contained in:
lda
2026-07-02 12:34:20 +07:00 Verified
parent 4d95b22e0b
commit 6c7522123d
11 changed files with 1041 additions and 114 deletions
+140 -7
View File
@@ -1,10 +1,143 @@
import { useReducer, useEffect, useCallback } from "react";
import {
connectionReducer,
initialState,
type SourceRecord,
} from "./state.js";
import { connectToServer, callOperation } from "../connection/api.js";
import { ConnectionHeader } from "../components/ConnectionHeader.js";
import { SourceInventory } from "../components/SourceInventory.js";
import { ProtocolEvidence } from "../components/ProtocolEvidence.js";
export function App() {
return (
<main>
<h1>lda.chat Workflow Console</h1>
<ConnectionHeader />
</main>
const parseSources = (
data: unknown,
): SourceRecord[] => {
if (!data || typeof data !== "object") return [];
const obj = data as Record<string, unknown>;
if (!Array.isArray(obj.sources)) return [];
return obj.sources.map((entry: unknown, i: number) => {
const s = entry as Record<string, unknown>;
const id = typeof s.id === "string" ? s.id : `source-${i}`;
const kind = typeof s.kind === "string" ? s.kind : "unknown";
const enabled = s.enabled !== false;
const description =
typeof s.description === "string" ? s.description : null;
const counts = (s.counts ?? {}) as Record<string, number>;
return {
id,
kind,
enabled,
description,
toolCount: typeof counts.tools === "number" ? counts.tools : 0,
nodeSpecCount: typeof counts.nodeSpecs === "number" ? counts.nodeSpecs : 0,
reducerCount: typeof counts.reducers === "number" ? counts.reducers : 0,
promptCount: typeof counts.prompts === "number" ? counts.prompts : 0,
resourceCount:
typeof counts.resources === "number" ? counts.resources : 0,
};
});
};
export const App = () => {
const [state, dispatch] = useReducer(connectionReducer, null, initialState);
const loadSources = useCallback(
async (target: string) => {
const result = await callOperation(
"workflow.sources.list",
target,
{ limit: 50 },
);
if (result.ok) {
const sources = parseSources(result.interpreted);
dispatch({
type: "sources_loaded",
sources,
evidence: {
id: `sources-${Date.now()}`,
operation: "workflow.sources.list",
label: "Source inventory",
equivalentCli: "uv run wf sources list --limit 50",
request: result.exchange.request,
response: result.exchange.response,
durationMs: result.durationMs,
},
});
} else {
dispatch({
type: "sources_error",
message: result.error.message,
evidence: {
id: `sources-${Date.now()}`,
operation: "workflow.sources.list",
label: "Source inventory",
equivalentCli: "uv run wf sources list --limit 50",
request: result.exchange.request,
response: result.exchange.response,
durationMs: 0,
},
});
}
},
[],
);
}
useEffect(() => {
if (state.phase === "connected" && state.connectedTarget) {
void loadSources(state.connectedTarget);
}
}, [state.phase, state.connectedTarget, loadSources]);
const onSubmit = (target: string) => {
dispatch({ type: "submit", target });
void connectToServer(target).then(
(response) => {
if (response.ok) {
dispatch({ type: "success", data: response });
dispatch({
type: "evidence_recorded",
record: {
id: `health-${Date.now()}`,
operation: "workflow.health",
label: "Health check",
equivalentCli: "uv run wf status",
request: response.exchange.request,
response: response.exchange.response,
durationMs: response.connection.durationMs,
},
});
} else {
dispatch({
type: "failure",
code: response.error.code,
message: response.error.message,
});
}
},
(e: unknown) => {
dispatch({
type: "failure",
code: "rpc_protocol_error",
message: e instanceof Error ? e.message : "unknown error",
});
},
);
};
return (
<div className="app-layout">
<ConnectionHeader
state={state}
onSubmit={onSubmit}
onDraftChange={(value) => dispatch({ type: "draft_changed", value })}
/>
<SourceInventory
sources={state.sources}
loading={state.sourcesLoading}
error={state.sourceError}
/>
<ProtocolEvidence evidence={state.evidence} />
</div>
);
};
+84 -1
View File
@@ -9,6 +9,28 @@ export type ConnectionPhase =
| "rpc_error"
| "malformed_response";
export type EvidenceRecord = {
readonly id: string;
readonly operation: string;
readonly label: string;
readonly equivalentCli: string;
readonly request: unknown;
readonly response: unknown;
readonly durationMs: number;
};
export type SourceRecord = {
readonly id: string;
readonly kind: string;
readonly enabled: boolean;
readonly description: string | null;
readonly toolCount: number;
readonly nodeSpecCount: number;
readonly reducerCount: number;
readonly promptCount: number;
readonly resourceCount: number;
};
export type ConnectionState = {
readonly phase: ConnectionPhase;
readonly draftTarget: string;
@@ -17,6 +39,10 @@ export type ConnectionState = {
readonly storeRoot: string | null;
readonly durationMs: number | null;
readonly message: string | null;
readonly evidence: ReadonlyArray<EvidenceRecord>;
readonly sources: ReadonlyArray<SourceRecord>;
readonly sourcesLoading: boolean;
readonly sourceError: string | null;
};
export const STORAGE_KEY = "lda.workflowConsole.target";
@@ -50,13 +76,29 @@ export const initialState = (): ConnectionState => ({
storeRoot: null,
durationMs: null,
message: null,
evidence: [],
sources: [],
sourcesLoading: false,
sourceError: null,
});
export type ConnectionAction =
| { readonly type: "submit"; readonly target: string }
| { readonly type: "success"; readonly data: ConnectionSuccess }
| { readonly type: "failure"; readonly code: string; readonly message: string }
| { readonly type: "draft_changed"; readonly value: string };
| { readonly type: "draft_changed"; readonly value: string }
| { readonly type: "sources_loading" }
| {
readonly type: "sources_loaded";
readonly sources: ReadonlyArray<SourceRecord>;
readonly evidence: EvidenceRecord;
}
| {
readonly type: "sources_error";
readonly message: string;
readonly evidence?: EvidenceRecord;
}
| { readonly type: "evidence_recorded"; readonly record: EvidenceRecord };
export const connectionReducer = (
state: ConnectionState,
@@ -69,6 +111,9 @@ export const connectionReducer = (
phase: "connecting",
draftTarget: action.target,
message: null,
sources: [],
sourceError: null,
sourcesLoading: false,
};
case "success": {
@@ -84,6 +129,7 @@ export const connectionReducer = (
storeRoot: action.data.connection.storeRoot,
durationMs: action.data.connection.durationMs,
message: null,
sourcesLoading: true,
};
}
@@ -101,9 +147,46 @@ export const connectionReducer = (
...state,
draftTarget: action.value,
};
case "sources_loading":
return {
...state,
sourcesLoading: true,
sourceError: null,
};
case "sources_loaded":
return {
...state,
sources: action.sources,
sourcesLoading: false,
sourceError: null,
evidence: appendEvidence(state.evidence, action.evidence),
};
case "sources_error":
return {
...state,
sourcesLoading: false,
sourceError: action.message,
evidence: action.evidence
? appendEvidence(state.evidence, action.evidence)
: state.evidence,
};
case "evidence_recorded":
return {
...state,
evidence: appendEvidence(state.evidence, action.record),
};
}
};
const appendEvidence = (
existing: ReadonlyArray<EvidenceRecord>,
record: EvidenceRecord,
): ReadonlyArray<EvidenceRecord> => [...existing, record];
const mapCodeToPhase = (code: string): ConnectionPhase => {
switch (code) {
case "invalid_target":