Keep conversation turns distinct and history stable (#7990)

* fix(app): keep autonomous turns distinct and readable

Hidden system prompts merged autonomous work into the previous response and reopened completed turns. Preserve canonical turn boundaries and expose their triggers, while making received messages attributable and expanded turn details readable in one step.

* fix(timeline): mount history at actual run boundaries

* fix(timeline): load and retain complete available turns

* fix(app): summarize actions and preserve conversation events

* test(timeline): verify whole-turn paging stays responsive

* fix(app): group reasoning according to thinking preference

* test(app): align history journeys with whole-turn loading
This commit is contained in:
Mohamed Boudra
2026-10-27 13:57:17 +07:00
committed by GitHub
parent 3a1b146690
commit b12eefb28f
72 changed files with 1690 additions and 409 deletions
+3
View File
@@ -1910,6 +1910,8 @@ export const AgentTimelineCursorSchema = z.object({
});
export const FetchAgentTimelineRequestMessageSchema = z.object({
// Keep available turns whole for tail/before pages; after catch-up stays incremental.
wholeTurns: z.boolean().optional(),
type: z.literal("fetch_agent_timeline_request"),
agentId: z.string(),
requestId: z.string(),
@@ -3765,6 +3767,7 @@ export const ServerInfoStatusPayloadSchema = z
providerSubagents: z.boolean().optional(),
// COMPAT(projectedSubagentTimeline): added after v0.8.0, remove gates after 2027-03-14; retain wire field.
projectedSubagentTimeline: z.boolean().optional(),
wholeTurnTimeline: z.boolean().optional(),
// COMPAT(providerSubagentNesting): added in v0.7, remove gate after 2027-03-04.
providerSubagentNesting: z.boolean().optional(),
// COMPAT(workspacePinning): added in v0.1.107, remove gate after 2027-01-12.
@@ -19,6 +19,7 @@ export interface PaseoToolDetailSpec {
export interface PaseoToolEntry {
label: string;
completedLabel: string;
context?: PaseoToolContext;
detail?: PaseoToolDetailSpec;
}
@@ -123,14 +124,24 @@ function baseName(value: unknown): string | undefined {
return value.split(/[\\/]/).findLast((segment) => segment.length > 0);
}
function browser(label: string, context?: PaseoToolContext): PaseoToolEntry {
return { label, ...(context ? { context } : {}), detail: { inputOrder: BROWSER_FIELDS } };
function browser(
label: string,
completedLabel: string,
context?: PaseoToolContext,
): PaseoToolEntry {
return {
label,
completedLabel,
...(context ? { context } : {}),
detail: { inputOrder: BROWSER_FIELDS },
};
}
export const PASEO_TOOL_CATALOG = {
// Agents
create_agent: {
label: "Create agent",
completedLabel: "Created agent",
context: field("title", "initialPrompt"),
detail: {
promptField: "initialPrompt",
@@ -140,6 +151,7 @@ export const PASEO_TOOL_CATALOG = {
},
send_agent_prompt: {
label: "Send message",
completedLabel: "Sent message",
context: field("prompt"),
detail: {
promptField: "prompt",
@@ -149,42 +161,55 @@ export const PASEO_TOOL_CATALOG = {
},
get_agent_status: {
label: "Check agent",
completedLabel: "Checked agent",
context: targetAgent(),
detail: { inputOrder: ["agentId"] },
},
get_agent_activity: {
label: "Read agent activity",
completedLabel: "Read agent activity",
context: targetAgent(),
detail: { inputOrder: ["agentId", "limit"] },
},
list_agents: {
label: "List agents",
completedLabel: "Listed agents",
detail: { inputOrder: ["cwd", "statuses", "sinceHours", "limit", "includeArchived"] },
},
update_agent: {
label: "Update agent",
completedLabel: "Updated agent",
context: fieldOrTargetAgent("name"),
detail: { inputOrder: AGENT_FIELDS },
},
set_agent_mode: {
label: "Set agent mode",
completedLabel: "Set agent mode",
context: field("modeId"),
detail: { inputOrder: ["agentId", "modeId"] },
},
cancel_agent: {
label: "Cancel agent",
completedLabel: "Canceled agent",
context: targetAgent(),
detail: { inputOrder: ["agentId"] },
},
archive_agent: {
label: "Archive agent",
completedLabel: "Archived agent",
context: targetAgent(),
detail: { inputOrder: ["agentId"] },
},
kill_agent: { label: "Kill agent", context: targetAgent(), detail: { inputOrder: ["agentId"] } },
list_pending_permissions: { label: "List permissions" },
kill_agent: {
label: "Kill agent",
completedLabel: "Killed agent",
context: targetAgent(),
detail: { inputOrder: ["agentId"] },
},
list_pending_permissions: { label: "List permissions", completedLabel: "Listed permissions" },
respond_to_permission: {
label: "Respond to permission",
completedLabel: "Responded to permission",
context: targetAgent(),
detail: { inputOrder: ["agentId", "requestId", "response"] },
},
@@ -192,32 +217,41 @@ export const PASEO_TOOL_CATALOG = {
// Workspaces and scripts
create_workspace: {
label: "Create workspace",
completedLabel: "Created workspace",
context: workspaceContext(),
detail: { inputOrder: WORKSPACE_FIELDS, outputFields: ["workspaceId", "projectId"] },
},
list_hosts: { label: "List hosts" },
list_workspaces: { label: "List workspaces" },
list_hosts: { label: "List hosts", completedLabel: "Listed hosts" },
list_workspaces: { label: "List workspaces", completedLabel: "Listed workspaces" },
rename_workspace: {
label: "Rename workspace",
completedLabel: "Renamed workspace",
context: field("title"),
detail: { inputOrder: ["title", "workspaceId"] },
},
archive_workspace: {
label: "Archive workspace",
completedLabel: "Archived workspace",
context: field("workspaceId"),
detail: {
inputOrder: ["workspaceId"],
outputFields: ["workspaceId", "archivedAgentIds", "removedDirectory"],
},
},
list_workspace_scripts: { label: "List scripts", detail: { inputOrder: ["workspaceId"] } },
list_workspace_scripts: {
label: "List scripts",
completedLabel: "Listed scripts",
detail: { inputOrder: ["workspaceId"] },
},
start_workspace_script: {
label: "Start script",
completedLabel: "Started script",
context: field("scriptName"),
detail: { inputOrder: ["workspaceId", "scriptName"] },
},
stop_workspace_script: {
label: "Stop script",
completedLabel: "Stopped script",
context: field("scriptName"),
detail: { inputOrder: ["workspaceId", "scriptName"] },
},
@@ -225,22 +259,30 @@ export const PASEO_TOOL_CATALOG = {
// Terminals
create_terminal: {
label: "Create terminal",
completedLabel: "Created terminal",
context: field("name", "command"),
detail: { inputOrder: ["cwd", "workspaceId", "name", "command"] },
},
list_terminals: { label: "List terminals", detail: { inputOrder: ["cwd", "all"] } },
list_terminals: {
label: "List terminals",
completedLabel: "Listed terminals",
detail: { inputOrder: ["cwd", "all"] },
},
capture_terminal: {
label: "Read terminal",
completedLabel: "Read terminal",
context: field("terminalId"),
detail: { inputOrder: ["terminalId", "lines"] },
},
send_terminal_keys: {
label: "Send keys",
completedLabel: "Sent keys",
context: field("keys"),
detail: { inputOrder: ["terminalId", "keys", "literal"] },
},
kill_terminal: {
label: "Kill terminal",
completedLabel: "Killed terminal",
context: field("terminalId"),
detail: { inputOrder: ["terminalId"] },
},
@@ -248,6 +290,7 @@ export const PASEO_TOOL_CATALOG = {
// Schedules and heartbeats
create_schedule: {
label: "Create schedule",
completedLabel: "Created schedule",
context: field("name", "cron"),
detail: {
promptField: "prompt",
@@ -257,6 +300,7 @@ export const PASEO_TOOL_CATALOG = {
},
create_heartbeat: {
label: "Create heartbeat",
completedLabel: "Created heartbeat",
context: field("name", "cron"),
detail: {
promptField: "prompt",
@@ -266,79 +310,93 @@ export const PASEO_TOOL_CATALOG = {
},
update_schedule: {
label: "Update schedule",
completedLabel: "Updated schedule",
context: field("name", "id"),
detail: { promptField: "prompt", inputOrder: AUTOMATION_FIELDS },
},
list_schedules: { label: "List schedules" },
list_schedules: { label: "List schedules", completedLabel: "Listed schedules" },
inspect_schedule: {
label: "Inspect schedule",
completedLabel: "Inspected schedule",
context: field("id"),
detail: { inputOrder: ["id"] },
},
schedule_logs: {
label: "Read schedule logs",
completedLabel: "Read schedule logs",
context: field("id"),
detail: { inputOrder: ["id"] },
},
run_schedule_once: {
label: "Run schedule",
completedLabel: "Ran schedule",
context: field("id"),
detail: { inputOrder: ["id"] },
},
pause_schedule: {
label: "Pause schedule",
completedLabel: "Paused schedule",
context: field("id"),
detail: { inputOrder: ["id"] },
},
pause_schedule: { label: "Pause schedule", context: field("id"), detail: { inputOrder: ["id"] } },
resume_schedule: {
label: "Resume schedule",
completedLabel: "Resumed schedule",
context: field("id"),
detail: { inputOrder: ["id"] },
},
delete_schedule: {
label: "Delete schedule",
completedLabel: "Deleted schedule",
context: field("id"),
detail: { inputOrder: ["id"] },
},
delete_heartbeat: {
label: "Delete heartbeat",
completedLabel: "Deleted heartbeat",
context: field("id"),
detail: { inputOrder: ["id"] },
},
// Providers
list_providers: { label: "List providers" },
list_providers: { label: "List providers", completedLabel: "Listed providers" },
list_models: {
label: "List models",
completedLabel: "Listed models",
context: field("provider"),
detail: { inputOrder: ["provider"] },
},
list_profiles: { label: "List profiles" },
list_profiles: { label: "List profiles", completedLabel: "Listed profiles" },
inspect_provider: {
label: "Inspect provider",
completedLabel: "Inspected provider",
context: field("provider"),
detail: { inputOrder: ["provider", "cwd", "settings"] },
},
// Browser
browser_list_tabs: browser("List browser tabs"),
browser_new_tab: browser("Open browser tab", field("url")),
browser_close_tab: browser("Close browser tab", field("browserId")),
browser_navigate: browser("Navigate", field("url")),
browser_back: browser("Go back"),
browser_forward: browser("Go forward"),
browser_reload: browser("Reload page"),
browser_snapshot: browser("Snapshot page"),
browser_screenshot: browser("Take screenshot"),
browser_click: browser("Click element", field("ref")),
browser_hover: browser("Hover element", field("ref")),
browser_drag: browser("Drag element", field("sourceRef")),
browser_fill: browser("Fill field", field("ref")),
browser_select: browser("Select option", field("value", "ref")),
browser_type: browser("Type text", field("text")),
browser_keypress: browser("Press key", field("key")),
browser_upload: browser("Upload files", field("filePaths")),
browser_wait: browser("Wait for page", field("text", "url")),
browser_scroll: browser("Scroll page"),
browser_resize: browser("Resize browser", viewportContext()),
browser_logs: browser("Read browser logs"),
browser_evaluate: browser("Run JavaScript", field("function")),
browser_list_tabs: browser("List browser tabs", "Listed browser tabs"),
browser_new_tab: browser("Open browser tab", "Opened browser tab", field("url")),
browser_close_tab: browser("Close browser tab", "Closed browser tab", field("browserId")),
browser_navigate: browser("Navigate", "Navigated", field("url")),
browser_back: browser("Go back", "Went back"),
browser_forward: browser("Go forward", "Went forward"),
browser_reload: browser("Reload page", "Reloaded page"),
browser_snapshot: browser("Snapshot page", "Snapshotted page"),
browser_screenshot: browser("Take screenshot", "Took screenshot"),
browser_click: browser("Click element", "Clicked element", field("ref")),
browser_hover: browser("Hover element", "Hovered element", field("ref")),
browser_drag: browser("Drag element", "Dragged element", field("sourceRef")),
browser_fill: browser("Fill field", "Filled field", field("ref")),
browser_select: browser("Select option", "Selected option", field("value", "ref")),
browser_type: browser("Type text", "Typed text", field("text")),
browser_keypress: browser("Press key", "Pressed key", field("key")),
browser_upload: browser("Upload files", "Uploaded files", field("filePaths")),
browser_wait: browser("Wait for page", "Waited for page", field("text", "url")),
browser_scroll: browser("Scroll page", "Scrolled page"),
browser_resize: browser("Resize browser", "Resized browser", viewportContext()),
browser_logs: browser("Read browser logs", "Read browser logs"),
browser_evaluate: browser("Run JavaScript", "Ran JavaScript", field("function")),
} satisfies Record<string, PaseoToolEntry>;
export type PaseoToolName = keyof typeof PASEO_TOOL_CATALOG;
@@ -28,6 +28,8 @@ export const PASEO_TOOL_NAMES = Object.keys(PASEO_TOOL_CATALOG) as readonly Pase
/** Returns null for tools outside the Paseo namespace. */
export function buildPaseoToolDisplay(input: PaseoToolDisplayInput): PaseoToolDisplay | null {
// Canonical details own internal lifecycle rows such as worktree setup.
if (input.detail.type !== "unknown") return null;
const leafName = paseoLeafName(input.toolName);
if (!leafName) return null;
const entry = getPaseoToolEntry(leafName);
@@ -39,6 +41,13 @@ export function buildPaseoToolDisplay(input: PaseoToolDisplayInput): PaseoToolDi
return { displayName: entry.label, ...(summary ? { summary } : {}) };
}
/** A semantic action label for summaries, sharing the tool presentation catalog. */
export function getPaseoToolActionLabel(toolName: string): string | null {
const leafName = paseoLeafName(toolName);
if (!leafName) return null;
return getPaseoToolEntry(leafName)?.completedLabel ?? humanizeLeafName(leafName);
}
/** The agent a Paseo call acts on, so callers can resolve its title. */
export function getPaseoToolTargetAgentId(
toolName: string,
@@ -49,7 +58,11 @@ export function getPaseoToolTargetAgentId(
}
function paseoLeafName(toolName: string): string | null {
return isPaseoToolName(toolName) ? getPaseoToolLeafName(toolName) : null;
if (isPaseoToolName(toolName)) return getPaseoToolLeafName(toolName);
const normalized = toolName.trim().toLowerCase();
return normalized.startsWith("paseo_") && !normalized.startsWith("paseo__")
? normalized.slice("paseo_".length)
: null;
}
function readToolInput(detail: ToolCallDetail): Readonly<Record<string, unknown>> {
+24
View File
@@ -0,0 +1,24 @@
/** Canonical run identity wins over trigger-shaped rows steered into that run. */
export function continuesTimelineTurn(
previousTurnId: string | undefined,
nextTurnId: string | undefined,
nextIsTrigger: boolean,
): boolean {
if (previousTurnId !== undefined && nextTurnId !== undefined) {
return previousTurnId === nextTurnId;
}
// Missing IDs cannot recover historical batching or steering. Each visible
// trigger deterministically starts a turn in both paging and presentation.
return !nextIsTrigger;
}
export function isTimelineTurnTrigger(item: {
type: string;
name?: string;
agentMessage?: unknown;
}): boolean {
return (
item.type === "user_message" ||
(item.type === "tool_call" && (item.agentMessage !== undefined || item.name === "heartbeat"))
);
}