feat(app): add chat outline navigation (#2792)
* feat(app): add chat outline navigation Long agent sessions need prompt navigation without coupling viewport movement to disposable server windows. Keep fetched timeline pages in client state so repeated jumps stay local, and settle prompt jumps against virtualized row measurement changes. * fix(app): preserve chat outline timeline coverage * fix(app): reconcile outline window submissions * fix(app): harden outline timeline navigation
This commit is contained in:
@@ -1081,6 +1081,51 @@ test("honors explicit fetchAgentTimeline timeout below the session RPC default",
|
||||
await expect(responsePromise).rejects.toThrow("Timeout waiting for message (2000ms)");
|
||||
});
|
||||
|
||||
test("lists the full agent prompt index", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_unit_test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const responsePromise = client.listAgentTimelinePrompts("agent-1", {
|
||||
requestId: "req-prompts-1",
|
||||
});
|
||||
|
||||
expect(parseSentFrame(mock.sent[0])).toEqual({
|
||||
type: "agent.timeline.list_prompts.request",
|
||||
requestId: "req-prompts-1",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: "agent.timeline.list_prompts.response",
|
||||
payload: {
|
||||
requestId: "req-prompts-1",
|
||||
agentId: "agent-1",
|
||||
epoch: "epoch-1",
|
||||
prompts: [{ seq: 1, timestamp: "2026-01-01T00:00:00.000Z", preview: "First prompt" }],
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(responsePromise).resolves.toMatchObject({
|
||||
epoch: "epoch-1",
|
||||
prompts: [{ seq: 1, preview: "First prompt" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("honors explicit fetchAgents timeout below the session RPC default", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
|
||||
@@ -562,10 +562,16 @@ export interface FetchAgentTimelineOptions {
|
||||
cursor?: FetchAgentTimelineCursor;
|
||||
limit?: number;
|
||||
projection?: FetchAgentTimelineProjection;
|
||||
mergeWindow?: boolean;
|
||||
requestId?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export type AgentTimelinePromptIndexPayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "agent.timeline.list_prompts.response" }
|
||||
>["payload"];
|
||||
|
||||
export type ProviderSubagentListPayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "agent.provider_subagents.list.response" }
|
||||
@@ -2738,6 +2744,7 @@ export class DaemonClient {
|
||||
...(options.cursor ? { cursor: options.cursor } : {}),
|
||||
...(typeof options.limit === "number" ? { limit: options.limit } : {}),
|
||||
...(options.projection ? { projection: options.projection } : {}),
|
||||
...(options.mergeWindow === true ? { mergeWindow: true } : {}),
|
||||
});
|
||||
|
||||
const payload = await this.sendRequest({
|
||||
@@ -2763,6 +2770,33 @@ export class DaemonClient {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async listAgentTimelinePrompts(
|
||||
agentId: string,
|
||||
options: { requestId?: string; timeout?: number } = {},
|
||||
): Promise<AgentTimelinePromptIndexPayload> {
|
||||
const requestId = this.createRequestId(options.requestId);
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "agent.timeline.list_prompts.request",
|
||||
agentId,
|
||||
requestId,
|
||||
});
|
||||
const payload = await this.sendRequest({
|
||||
requestId,
|
||||
message,
|
||||
timeout: options.timeout,
|
||||
options: { skipQueue: true },
|
||||
select: (response) =>
|
||||
response.type === "agent.timeline.list_prompts.response" &&
|
||||
response.payload.requestId === requestId
|
||||
? response.payload
|
||||
: null,
|
||||
});
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async listProviderSubagents(
|
||||
parentAgentId: string,
|
||||
options: { requestId?: string; timeout?: number } = {},
|
||||
|
||||
Reference in New Issue
Block a user