feat(frontend): viewer humain fil-par-paire en lecture seule (LS7)
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>
This commit is contained in:
@ -38,10 +38,14 @@ import type {
|
||||
SkillScope,
|
||||
Template,
|
||||
TerminalSession,
|
||||
TurnPage,
|
||||
TurnView,
|
||||
Unsubscribe,
|
||||
} from "@/domain";
|
||||
import type {
|
||||
AgentGateway,
|
||||
ConversationGateway,
|
||||
ConversationPageRequest,
|
||||
ConversationDetails,
|
||||
CreateAgentInput,
|
||||
CreateMemoryInput,
|
||||
@ -1693,6 +1697,86 @@ export class MockWorkStateGateway implements WorkStateGateway {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory {@link ConversationGateway} with **real** pagination, mirroring the
|
||||
* LS6 backend slicing (`crates/infrastructure/.../conversation_log/mod.rs::page`
|
||||
* + `domain::clamp_page_limit`): turns stored oldest→newest; anchor pagination
|
||||
* is **strict** (turns strictly before/after the anchor); `limit` defaults to 50
|
||||
* and is clamped to `[1, 200]`; `hasMore` is true when more turns exist in the
|
||||
* travel direction; `nextAnchor` is the **last** (most recent) turn of the page.
|
||||
* Full turn text is preserved (never truncated).
|
||||
*/
|
||||
export class MockConversationGateway implements ConversationGateway {
|
||||
private threads = new Map<string, TurnView[]>();
|
||||
|
||||
private key(projectId: string, conversationId: string): string {
|
||||
return `${projectId}::${conversationId}`;
|
||||
}
|
||||
|
||||
/** Seeds the turns of a conversation (oldest→newest) for deterministic tests. */
|
||||
_setTurns(
|
||||
projectId: string,
|
||||
conversationId: string,
|
||||
turns: TurnView[],
|
||||
): void {
|
||||
this.threads.set(
|
||||
this.key(projectId, conversationId),
|
||||
turns.map((t) => structuredClone(t)),
|
||||
);
|
||||
}
|
||||
|
||||
async readPage(
|
||||
projectId: string,
|
||||
conversationId: string,
|
||||
request?: ConversationPageRequest,
|
||||
): Promise<TurnPage> {
|
||||
const all = this.threads.get(this.key(projectId, conversationId)) ?? [];
|
||||
const len = all.length;
|
||||
const raw = request?.limit;
|
||||
const limit = Math.min(Math.max(raw && raw > 0 ? raw : 50, 1), 200);
|
||||
const direction = request?.direction ?? "backward";
|
||||
const anchor = request?.anchor;
|
||||
|
||||
let start: number;
|
||||
let end: number;
|
||||
let hasMore: boolean;
|
||||
if (anchor === undefined) {
|
||||
if (direction === "forward") {
|
||||
start = 0;
|
||||
end = Math.min(limit, len);
|
||||
hasMore = end < len;
|
||||
} else {
|
||||
start = Math.max(len - limit, 0);
|
||||
end = len;
|
||||
hasMore = start > 0;
|
||||
}
|
||||
} else {
|
||||
const pos = all.findIndex((t) => t.id === anchor);
|
||||
if (pos === -1) {
|
||||
start = 0;
|
||||
end = 0;
|
||||
hasMore = false;
|
||||
} else if (direction === "forward") {
|
||||
start = pos + 1;
|
||||
end = Math.min(start + limit, len);
|
||||
hasMore = end < len;
|
||||
} else {
|
||||
end = pos;
|
||||
start = Math.max(end - limit, 0);
|
||||
hasMore = start > 0;
|
||||
}
|
||||
}
|
||||
|
||||
const turns = all.slice(start, end).map((t) => structuredClone(t));
|
||||
const last = turns.at(-1);
|
||||
return {
|
||||
turns,
|
||||
hasMore,
|
||||
...(last ? { nextAnchor: last.id } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function mostRestrictive(
|
||||
project?: PermissionSet["fallback"],
|
||||
agent?: PermissionSet["fallback"],
|
||||
@ -1722,6 +1806,7 @@ export function createMockGateways(): Gateways {
|
||||
embedder: new MockEmbedderGateway(),
|
||||
permission: new MockPermissionGateway(),
|
||||
workState: new MockWorkStateGateway(),
|
||||
conversation: new MockConversationGateway(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user