refactor(app): introduce attachment storage layer and draft lifecycle management
Replace inline data-URL image handling with a persistent attachment storage service, add draft lifecycle states (active/abandoned/sent) with generation tracking, and update fetchAgent to return project placement alongside agent snapshots.
This commit is contained in:
@@ -77,7 +77,6 @@ import {
|
||||
derivePendingPermissionKey,
|
||||
normalizeAgentSnapshot,
|
||||
} from "@/utils/agent-snapshots";
|
||||
import { resolveProjectPlacement } from "@/utils/project-placement";
|
||||
import { mergePendingCreateImages } from "@/utils/pending-create-images";
|
||||
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
@@ -424,9 +423,13 @@ function AgentScreenContent({
|
||||
const streamItems = streamItemsRaw ?? EMPTY_STREAM_ITEMS;
|
||||
|
||||
const pendingCreate = useCreateFlowStore((state) => state.pending);
|
||||
const markPendingCreateLifecycle = useCreateFlowStore(
|
||||
(state) => state.markLifecycle
|
||||
);
|
||||
const clearPendingCreate = useCreateFlowStore((state) => state.clear);
|
||||
const isPendingCreateForRoute =
|
||||
Boolean(pendingCreate) &&
|
||||
pendingCreate?.lifecycle === "active" &&
|
||||
pendingCreate?.serverId === serverId &&
|
||||
pendingCreate?.agentId === resolvedAgentId;
|
||||
|
||||
@@ -624,7 +627,7 @@ function AgentScreenContent({
|
||||
return [
|
||||
{
|
||||
kind: "user_message",
|
||||
id: pendingCreate.messageId,
|
||||
id: pendingCreate.clientMessageId,
|
||||
text: pendingCreate.text,
|
||||
timestamp: new Date(pendingCreate.timestamp),
|
||||
...(pendingCreate.images && pendingCreate.images.length > 0
|
||||
@@ -725,7 +728,7 @@ function AgentScreenContent({
|
||||
const hasUserMessage = streamItems.some(
|
||||
(item) =>
|
||||
item.kind === "user_message" &&
|
||||
(item.id === pendingCreate.messageId || item.text === pendingCreate.text)
|
||||
item.id === pendingCreate.clientMessageId
|
||||
);
|
||||
if (agent && hasUserMessage) {
|
||||
if (
|
||||
@@ -741,8 +744,7 @@ function AgentScreenContent({
|
||||
|
||||
const merged = mergePendingCreateImages({
|
||||
streamItems: current,
|
||||
messageId: pendingCreate.messageId,
|
||||
text: pendingCreate.text,
|
||||
clientMessageId: pendingCreate.clientMessageId,
|
||||
images: pendingCreate.images,
|
||||
});
|
||||
if (merged === current) {
|
||||
@@ -754,12 +756,14 @@ function AgentScreenContent({
|
||||
return next;
|
||||
});
|
||||
}
|
||||
markPendingCreateLifecycle("sent");
|
||||
clearPendingCreate();
|
||||
}
|
||||
}, [
|
||||
agent,
|
||||
clearPendingCreate,
|
||||
isPendingCreateForRoute,
|
||||
markPendingCreateLifecycle,
|
||||
pendingCreate,
|
||||
resolvedAgentId,
|
||||
serverId,
|
||||
@@ -827,24 +831,21 @@ function AgentScreenContent({
|
||||
.sessions[serverId]
|
||||
?.agents.get(resolvedAgentId);
|
||||
if (!currentAgent && client) {
|
||||
const snapshot = await client.fetchAgent(resolvedAgentId);
|
||||
const result = await client.fetchAgent(resolvedAgentId);
|
||||
if (attemptToken !== initAttemptTokenRef.current) {
|
||||
return;
|
||||
}
|
||||
if (!snapshot) {
|
||||
if (!result) {
|
||||
setMissingAgentState({
|
||||
kind: "not_found",
|
||||
message: `Agent not found: ${resolvedAgentId}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeAgentSnapshot(snapshot, serverId);
|
||||
const normalized = normalizeAgentSnapshot(result.agent, serverId);
|
||||
const hydrated = {
|
||||
...normalized,
|
||||
projectPlacement: resolveProjectPlacement({
|
||||
projectPlacement: null,
|
||||
cwd: normalized.cwd,
|
||||
}),
|
||||
projectPlacement: result.project,
|
||||
};
|
||||
setAgents(serverId, (prev) => {
|
||||
const next = new Map(prev);
|
||||
|
||||
@@ -34,6 +34,7 @@ import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-sugg
|
||||
import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture'
|
||||
import { useSessionStore } from '@/stores/session-store'
|
||||
import { useCreateFlowStore } from '@/stores/create-flow-store'
|
||||
import { generateDraftId } from '@/stores/draft-keys'
|
||||
import { getHostRuntimeStore, useHostRuntimeSession } from '@/runtime/host-runtime'
|
||||
import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context'
|
||||
import { usePanelStore, type ExplorerCheckoutContext } from '@/stores/panel-store'
|
||||
@@ -244,6 +245,7 @@ function DraftAgentScreenContent({
|
||||
)
|
||||
const tauriDragHandlers = useTauriDragHandlers()
|
||||
const isExplorerOpen = isMobile ? mobileView === 'file-explorer' : desktopFileExplorerOpen
|
||||
const draftIdRef = useRef(generateDraftId())
|
||||
|
||||
const [worktreeMode, setWorktreeMode] = useState<'none' | 'create' | 'attach'>('none')
|
||||
const [baseBranch, setBaseBranch] = useState('')
|
||||
@@ -262,6 +264,7 @@ function DraftAgentScreenContent({
|
||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
|
||||
const setPendingCreateAttempt = useCreateFlowStore((state) => state.setPending)
|
||||
const updatePendingAgentId = useCreateFlowStore((state) => state.updateAgentId)
|
||||
const markPendingCreateLifecycle = useCreateFlowStore((state) => state.markLifecycle)
|
||||
const clearPendingCreateAttempt = useCreateFlowStore((state) => state.clear)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -277,7 +280,7 @@ function DraftAgentScreenContent({
|
||||
}, [workingDirSearchQuery])
|
||||
|
||||
type CreateAttempt = {
|
||||
messageId: string
|
||||
clientMessageId: string
|
||||
text: string
|
||||
timestamp: Date
|
||||
images?: UserMessageImageAttachment[]
|
||||
@@ -832,7 +835,7 @@ function DraftAgentScreenContent({
|
||||
return [
|
||||
{
|
||||
kind: 'user_message',
|
||||
id: machine.attempt.messageId,
|
||||
id: machine.attempt.clientMessageId,
|
||||
text: machine.attempt.text,
|
||||
timestamp: machine.attempt.timestamp,
|
||||
...(machine.attempt.images && machine.attempt.images.length > 0
|
||||
@@ -972,15 +975,16 @@ function DraftAgentScreenContent({
|
||||
throw new Error('Host is not connected')
|
||||
}
|
||||
const attempt = {
|
||||
messageId: generateMessageId(),
|
||||
clientMessageId: generateMessageId(),
|
||||
text: trimmedPrompt,
|
||||
timestamp: new Date(),
|
||||
...(images && images.length > 0 ? { images } : {}),
|
||||
}
|
||||
setPendingCreateAttempt({
|
||||
draftId: draftIdRef.current,
|
||||
serverId: selectedServerId,
|
||||
agentId: null,
|
||||
messageId: attempt.messageId,
|
||||
clientMessageId: attempt.clientMessageId,
|
||||
text: attempt.text,
|
||||
timestamp: attempt.timestamp.getTime(),
|
||||
...(attempt.images && attempt.images.length > 0 ? { images: attempt.images } : {}),
|
||||
@@ -1026,6 +1030,7 @@ function DraftAgentScreenContent({
|
||||
config,
|
||||
labels: { ui: 'true' },
|
||||
initialPrompt: trimmedPrompt,
|
||||
clientMessageId: attempt.clientMessageId,
|
||||
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
||||
git: gitOptions,
|
||||
})
|
||||
@@ -1042,12 +1047,14 @@ function DraftAgentScreenContent({
|
||||
message: 'Failed to create agent',
|
||||
})
|
||||
onCreateFlowActiveChange?.(false)
|
||||
markPendingCreateLifecycle('abandoned')
|
||||
clearPendingCreateAttempt()
|
||||
throw new Error('Failed to create agent')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create agent'
|
||||
dispatch({ type: 'CREATE_FAILED', message })
|
||||
onCreateFlowActiveChange?.(false)
|
||||
markPendingCreateLifecycle('abandoned')
|
||||
clearPendingCreateAttempt()
|
||||
throw error // Re-throw so AgentInputArea knows it failed
|
||||
}
|
||||
@@ -1073,6 +1080,7 @@ function DraftAgentScreenContent({
|
||||
createAgentClient,
|
||||
setPendingCreateAttempt,
|
||||
updatePendingAgentId,
|
||||
markPendingCreateLifecycle,
|
||||
clearPendingCreateAttempt,
|
||||
workingDir,
|
||||
isAttachWorktree,
|
||||
@@ -1324,6 +1332,7 @@ function DraftAgentScreenContent({
|
||||
autoFocus={machine.tag === 'draft'}
|
||||
onAddImages={handleAddImagesCallback}
|
||||
commandDraftConfig={draftCommandConfig}
|
||||
draftId={draftIdRef.current}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user