mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
* refactor(app): stop resetting the chat timeline when its agent id changes A mounted timeline only ever shows one agent: every host keys its panel by tab id, and a tab keeps its id across a target change only when a draft becomes the agent it created. The per-agent resets inside the timeline therefore never had a different agent to reset for. They only fired at that create handoff, where they hid and re-placed a timeline that was already on screen. Delete them: LegendList's dataKey and the placed agent id, the web viewport key, the bottom anchor's resetForAgent, the history window's agent id, and the expansion and path-opener resets in the stream view. * fix(app): keep one chat timeline across the draft-to-agent handoff A draft tab and the agent its first prompt creates rendered two different panels: DraftPanel mounted its own dock and list, and the retarget swapped it for AgentPanel's. On Android, with the LegendList timeline, the swap blanked the first turn and re-placed it. Render both from the agent panel. A draft supplies its timeline from the pending create in the create-flow store (composer/draft/timeline.ts) and its composer in the composer slot. The retarget then re-renders the same stream view with the agent's timeline, so the list instance and its measured rows stay mounted. Agent gating is unchanged. Float the compact header over draft tabs as it already floats over agent tabs. The header sat in flow above a draft and over an agent, so the list viewport grew 92 dp at the retarget and the turn moved. The handoff E2E now fails if the first row disappears, hides, or shifts down while the draft becomes the agent. * fix(app): load subagents and tasks only for an agent, not a draft tab
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import { useMemo } from "react";
|
|
import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types";
|
|
import type { PendingMessageSubmission } from "@/composer/submission/model";
|
|
import type { AgentScreenAgent } from "@/hooks/use-agent-screen-state-machine";
|
|
import { type PendingCreateAttempt, useCreateFlowStore } from "@/stores/create-flow-store";
|
|
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
|
import {
|
|
resolveTurnPresentation,
|
|
TURN_LIVENESS_IDLE,
|
|
type TurnPresentation,
|
|
} from "@/timeline/turn-liveness";
|
|
import { createUserMessage, type StreamItem } from "@/types/stream";
|
|
import type { WorkspaceDraftTabSetup } from "@/workspace-tabs/model";
|
|
import { resolveDraftWorkingDirectory } from "@/composer/draft/workspace-tab-core";
|
|
|
|
export const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: false,
|
|
supportsDynamicModes: false,
|
|
supportsMcpServers: false,
|
|
supportsReasoningStream: false,
|
|
supportsToolInvocations: false,
|
|
};
|
|
|
|
export interface DraftConversation {
|
|
tabId: string;
|
|
workspaceId: string;
|
|
draftId: string;
|
|
setup?: WorkspaceDraftTabSetup;
|
|
}
|
|
|
|
/** The timeline a draft shows while its first prompt creates the agent. */
|
|
export interface DraftTimeline {
|
|
agent: AgentScreenAgent;
|
|
streamItems: StreamItem[];
|
|
pendingMessageSubmissions: readonly PendingMessageSubmission[];
|
|
turnPresentation: TurnPresentation;
|
|
}
|
|
|
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
|
|
|
/**
|
|
* The sent prompt, pending, with the turn working. The created agent's timeline carries the same
|
|
* row (same client message id), so the handoff only changes who owns it.
|
|
*/
|
|
export function resolveDraftTimeline(input: {
|
|
pending: PendingCreateAttempt | undefined;
|
|
serverId: string;
|
|
tabId: string;
|
|
cwd: string | null;
|
|
}): DraftTimeline | null {
|
|
const { pending, serverId, tabId, cwd } = input;
|
|
if (!pending || pending.serverId !== serverId || pending.lifecycle === "abandoned" || !cwd) {
|
|
return null;
|
|
}
|
|
const hasContent =
|
|
pending.text.length > 0 ||
|
|
(pending.images?.length ?? 0) > 0 ||
|
|
(pending.attachments?.length ?? 0) > 0;
|
|
return {
|
|
agent: { serverId, id: tabId, status: "running", cwd, capabilities: DRAFT_CAPABILITIES },
|
|
streamItems: hasContent
|
|
? [
|
|
createUserMessage({
|
|
clientMessageId: pending.clientMessageId,
|
|
text: pending.text,
|
|
timestamp: new Date(pending.timestamp),
|
|
images: pending.images,
|
|
attachments: pending.attachments,
|
|
}),
|
|
]
|
|
: EMPTY_STREAM_ITEMS,
|
|
pendingMessageSubmissions: [{ clientMessageId: pending.clientMessageId }],
|
|
turnPresentation: resolveTurnPresentation(TURN_LIVENESS_IDLE, true),
|
|
};
|
|
}
|
|
|
|
export function useDraftTimeline(input: {
|
|
serverId: string;
|
|
draft: DraftConversation | null;
|
|
}): DraftTimeline | null {
|
|
const { serverId, draft } = input;
|
|
const pending = useCreateFlowStore((state) =>
|
|
draft ? state.pendingByDraftId[draft.draftId] : undefined,
|
|
);
|
|
const workspaceDirectory = useWorkspaceFields(
|
|
serverId,
|
|
draft?.workspaceId ?? null,
|
|
(workspace) => workspace.workspaceDirectory,
|
|
);
|
|
const cwd = draft
|
|
? resolveDraftWorkingDirectory({
|
|
workspaceDirectory: workspaceDirectory || null,
|
|
initialSetup: draft.setup ?? null,
|
|
})
|
|
: null;
|
|
const tabId = draft?.tabId ?? "";
|
|
return useMemo(
|
|
() => resolveDraftTimeline({ pending, serverId, tabId, cwd }),
|
|
[cwd, pending, serverId, tabId],
|
|
);
|
|
}
|