fix(domain,infrastructure,web-server,app-tauri,frontend): TUI native noire — route PTY explicite et replay du scrollback pré-attach sans perte — #360 (QA verte)

This commit is contained in:
2026-09-16 19:08:01 +02:00
parent a833af0fb5
commit 355a482fee
8 changed files with 220 additions and 13 deletions

View File

@ -400,6 +400,69 @@ describe("TauriAgentGateway invoke payloads", () => {
});
});
it("launchAgent requests and accepts only the PTY route for a native TUI", async () => {
invoke.mockResolvedValueOnce({
sessionId: "pty-session-1",
cwd: "/repo",
rows: 24,
cols: 80,
cellKind: "pty",
assignedConversationId: "conversation-1",
});
const out = await new TauriAgentGateway().launchAgent(
"proj-1",
"agent-2",
{
cwd: "/repo",
rows: 24,
cols: 80,
conversationId: "conversation-0",
nodeId: "node-3",
},
vi.fn(),
);
expect(invoke).toHaveBeenCalledWith("launch_agent", {
request: {
projectId: "proj-1",
agentId: "agent-2",
rows: 24,
cols: 80,
cellKind: "pty",
conversationId: "conversation-0",
freshStart: false,
nodeId: "node-3",
},
onOutput: expect.anything(),
});
expect(out.sessionId).toBe("pty-session-1");
expect(out.assignedConversationId).toBe("conversation-1");
});
it("launchAgent rejects a chat-routed launch instead of returning a silent PTY handle", async () => {
invoke.mockResolvedValueOnce({
sessionId: "chat-session-1",
cwd: "/repo",
rows: 24,
cols: 80,
cellKind: "chat",
});
await expect(
new TauriAgentGateway().launchAgent(
"proj-1",
"agent-2",
{ cwd: "/repo", rows: 24, cols: 80, nodeId: "node-3" },
vi.fn(),
),
).rejects.toEqual({
code: "NATIVE_TUI_ROUTED_TO_NON_PTY",
message:
"native TUI launch for agent agent-2 in project proj-1 returned cellKind=chat; expected pty. sessionId=chat-session-1; nodeId=node-3",
});
});
it("launchAgentChat rejects a PTY-routed launch before returning a fake structured session", async () => {
const log = vi.spyOn(console, "error").mockImplementation(() => {});
invoke.mockResolvedValueOnce({

View File

@ -74,6 +74,22 @@ function structuredLaunchRoutedToPtyError(
};
}
function nativeLaunchRoutedToNonPtyError(
projectId: string,
agentId: string,
options: OpenTerminalOptions,
response: LaunchAgentResponse,
): GatewayError {
const actual = response.cellKind ?? "missing";
return {
code: "NATIVE_TUI_ROUTED_TO_NON_PTY",
message:
`native TUI launch for agent ${agentId} in project ${projectId} ` +
`returned cellKind=${actual}; expected pty. ` +
`sessionId=${response.sessionId}; nodeId=${options.nodeId ?? "none"}`,
};
}
export class TauriAgentGateway implements AgentGateway {
listAgents(projectId: string): Promise<Agent[]> {
return invoke<Agent[]>("list_agents", { projectId });
@ -214,6 +230,11 @@ export class TauriAgentGateway implements AgentGateway {
agentId,
rows: options.rows,
cols: options.cols,
// Native TUI is always the raw PTY route, including for profiles that
// also support the structured/custom CLI. Omitting this hint lets the
// backend select a chat session, whose reply stream can never feed the
// xterm output channel and leaves the cell looking blank.
cellKind: "pty",
// Resume id: the leaf's persisted conversation id, when any (T4b). The
// backend resumes it; absent ⇒ a fresh cell that may get a new id.
conversationId: options.conversationId ?? null,
@ -224,6 +245,10 @@ export class TauriAgentGateway implements AgentGateway {
onOutput: channel,
});
if (res.cellKind !== "pty") {
throw nativeLaunchRoutedToNonPtyError(projectId, agentId, options, res);
}
const base = makeTerminalHandle(res.sessionId, channel);
// Surface the id assigned by this launch (so the caller persists it on the
// leaf and resumes next time).

View File

@ -220,6 +220,7 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
nodeId: "node-1",
rows: 24,
cols: 80,
cellKind: "pty",
conversationId: "resume-9",
freshStart: false,
});

View File

@ -416,6 +416,10 @@ export class WsLiveClient {
nodeId: params.nodeId ?? null,
rows: params.rows,
cols: params.cols,
// The native cell must select the raw PTY route explicitly. Profiles
// may also expose a structured chat route; relying on their default can
// produce a chat session behind an xterm subscription with no output.
cellKind: "pty",
conversationId: params.conversationId ?? null,
freshStart: params.freshStart ?? false,
},