feat(web): parité layout AppImage desktop + switch TUI naïf/CLI custom — #208
This commit is contained in:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user