Frontend pur, lecture seule, consomme read_conversation_page (LS6) ; aucun changement backend. - adapters : conversation.ts (gateway) + conversationNormalization.ts. - features/conversations : useConversationThread, ConversationViewer, index. - domain (types LS7), ports (port + Gateways.conversation), adapters/index, mock (+ clé inventaire). - features/workstate : ProjectWorkStatePanel prop onOpenConversation. - features/projects : ProjectsView swap viewer. - tests (QA, verts) : conversationNormalization, mock/conversationGateway, useConversationThread, ConversationViewer, ProjectsView.ls7. tsc 0, vitest 443/443 (+36). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
/**
|
|
* Wire → domain normalization for the conversation transcript read (LS7).
|
|
*
|
|
* Mirrors the LS6 backend DTO (`crates/app-tauri/src/dto.rs`): `TurnPageDto`
|
|
* (camelCase `turns` / `hasMore` / `nextAnchor`), `TurnViewDto`
|
|
* (`id` / `atMs` / `role` / `source` / `text` / `textLen`) and the serde-tagged
|
|
* `TurnSourceDto` (`{ kind: "human" }` | `{ kind: "agent", agentId }`). The
|
|
* `source` mapping is calqued on `workStateNormalization.ts`.
|
|
*/
|
|
|
|
import type { TurnPage, TurnRole, TurnSource, TurnView } from "@/domain";
|
|
|
|
type RecordLike = Record<string, unknown>;
|
|
|
|
function isRecord(value: unknown): value is RecordLike {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function stringValue(value: unknown, fallback = ""): string {
|
|
return typeof value === "string" ? value : fallback;
|
|
}
|
|
|
|
function numberValue(value: unknown, fallback = 0): number {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
function arrayValue(value: unknown): unknown[] {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
/** Mirror of LS6 `TurnSourceDto` (serde tag `kind`, camelCase `agentId`). */
|
|
function normalizeSource(value: unknown): TurnSource {
|
|
if (!isRecord(value)) return { kind: "human" };
|
|
if (value.kind === "agent") {
|
|
return { kind: "agent", agentId: stringValue(value.agentId, "unknown") };
|
|
}
|
|
return { kind: "human" };
|
|
}
|
|
|
|
/** Mirror of the camelCase-serialized `TurnRole` (`prompt`/`response`/`toolActivity`). */
|
|
function normalizeRole(value: unknown): TurnRole {
|
|
return value === "response" || value === "toolActivity" ? value : "prompt";
|
|
}
|
|
|
|
function normalizeTurn(value: unknown, index: number): TurnView {
|
|
const turn = isRecord(value) ? value : {};
|
|
const text = stringValue(turn.text);
|
|
return {
|
|
id: stringValue(turn.id, `turn-${index}`),
|
|
atMs: numberValue(turn.atMs, index),
|
|
role: normalizeRole(turn.role),
|
|
source: normalizeSource(turn.source),
|
|
text,
|
|
textLen: numberValue(turn.textLen, text.length),
|
|
};
|
|
}
|
|
|
|
/** Maps a raw `TurnPageDto` payload to the domain {@link TurnPage}. */
|
|
export function normalizeTurnPage(value: unknown): TurnPage {
|
|
const page = isRecord(value) ? value : {};
|
|
const nextAnchor = page.nextAnchor;
|
|
return {
|
|
turns: arrayValue(page.turns).map(normalizeTurn),
|
|
hasMore: page.hasMore === true,
|
|
...(typeof nextAnchor === "string" ? { nextAnchor } : {}),
|
|
};
|
|
}
|