feat(web): parité layout AppImage desktop + switch TUI naïf/CLI custom — #208

This commit is contained in:
2026-08-15 09:08:42 +02:00
parent 780f91a211
commit 7cdb97ae89
7 changed files with 914 additions and 661 deletions

View File

@ -25,9 +25,11 @@ import type {
AppExitWorkGuardState,
DomainEvent,
EffortSelection,
ExecuteSlashCommandResult,
HealthReport,
ReplyChunk,
ResumableAgent,
SlashCommand,
Sprint,
TerminalSession,
Ticket,
@ -40,6 +42,7 @@ import type {
Unsubscribe,
} from "@/domain";
import type {
AgentChatHandle,
AgentGateway,
ChatDraftScope,
ChatDraftSnapshot,
@ -48,7 +51,9 @@ import type {
CreateTicketInput,
LiveAgent,
OpenTerminalOptions,
ReattachAgentChatResult,
ReattachResult,
SendAgentChatOptions,
StoppedLiveAgent,
SystemGateway,
TerminalGateway,
@ -59,7 +64,7 @@ import type {
} from "@/ports";
import type { HttpInvoker } from "./httpInvoker";
import { WsLiveClient } from "./wsLiveClient";
import { type ChatOutputPayload } from "./frames";
import { type ChatAttachedPayload, type ChatOutputPayload } from "./frames";
import { unsupportedOnWeb } from "./unsupported";
/**
@ -96,6 +101,14 @@ export function makeWsTerminalHandle(
};
}
function chatSessionId(payload: ChatAttachedPayload): string {
const sessionId = payload.sessionId ?? payload.session?.sessionId;
if (!sessionId) {
throw { code: "INVALID_CHAT_ACK", message: "chat acknowledgement omitted sessionId" };
}
return sessionId;
}
export class HttpSystemGateway implements SystemGateway {
constructor(
private readonly http: HttpInvoker,
@ -306,6 +319,114 @@ export class HttpAgentGateway implements AgentGateway {
scrollback: res.scrollback,
};
}
async launchAgentChat(
projectId: string,
agentId: string,
options: OpenTerminalOptions,
): Promise<AgentChatHandle> {
const ack = await this.ws.send("agent.launch", {
projectId,
agentId,
nodeId: options.nodeId ?? null,
rows: options.rows,
cols: options.cols,
cellKind: "chat",
conversationId: options.conversationId ?? null,
freshStart: options.freshStart ?? false,
});
if (ack.kind !== "chat.attached") {
throw {
code: "STRUCTURED_ROUTED_TO_NON_CHAT",
message: `custom CLI launch returned ${ack.kind}; expected chat.attached`,
};
}
const payload = ack.payload as unknown as ChatAttachedPayload;
if (payload.cellKind && payload.cellKind !== "chat") {
throw {
code: "STRUCTURED_ROUTED_TO_NON_CHAT",
message: `custom CLI launch returned cellKind=${payload.cellKind}; expected chat`,
};
}
const sessionId = chatSessionId(payload);
return {
sessionId,
cellKind: "chat",
...(payload.assignedConversationId
? { assignedConversationId: payload.assignedConversationId }
: {}),
};
}
async reattachAgentChat(
sessionId: string,
onChunk: (chunk: ReplyChunk) => void,
): Promise<ReattachAgentChatResult> {
const ack = await this.ws.send("chat.attach", { sessionId });
if (ack.kind !== "chat.attached") {
throw {
code: "CHAT_ATTACH_FAILED",
message: `chat attach returned ${ack.kind}; expected chat.attached`,
};
}
const payload = ack.payload as unknown as ChatAttachedPayload;
const attachedSessionId = chatSessionId(payload);
this.ws.setChatSink(attachedSessionId, onChunk);
return {
sessionId: attachedSessionId,
scrollback: payload.scrollback ?? [],
};
}
async detachAgentChat(sessionId: string): Promise<void> {
this.ws.removeChatSink(sessionId);
await this.ws.sendFireAndForget("chat.detach", { sessionId });
}
async sendAgentChat(
sessionId: string,
prompt: string,
onChunk: (chunk: ReplyChunk) => void,
options: SendAgentChatOptions = {},
): Promise<void> {
this.ws.setChatSink(sessionId, onChunk);
const ack = await this.ws.send("chat.send", {
sessionId,
prompt,
attachments: options.attachments ?? [],
});
const payload = ack.payload as unknown as ChatOutputPayload | undefined;
if (payload?.chunk) onChunk(payload.chunk);
}
listSlashCommands(prefix?: string): Promise<SlashCommand[]> {
const request = prefix === undefined ? {} : { prefix };
return this.http
.invoke<{ commands: SlashCommand[] }>("list_slash_commands", { request })
.then((res) => res.commands);
}
executeSlashCommand(
name: string,
options: { sessionId?: string | null; arguments?: unknown[] } = {},
): Promise<ExecuteSlashCommandResult> {
return this.http.invoke<ExecuteSlashCommandResult>("execute_slash_command", {
request: {
name,
sessionId: options.sessionId ?? null,
arguments: options.arguments ?? [],
},
});
}
async closeAgentChat(sessionId: string): Promise<void> {
this.ws.removeChatSink(sessionId);
await this.http.invoke("close_agent_session", { sessionId });
}
async cancelAgentChat(sessionId: string): Promise<void> {
await this.http.invoke("cancel_agent_chat", { sessionId });
}
}
export class HttpTicketGateway implements TicketGateway {