fix(app): source composer cwd from agent state, not draft hook

The GitHub picker showed "No results found." for any running agent after
the first message was sent. useAgentInputDraft owned a local cwd state
that clear("sent") wiped to "", so the composer read cwd: "" even though
the agent's real cwd was correct on the wire.

Drop cwd ownership from the hook. Composer takes cwd as a prop sourced
by the parent — agentState.cwd for running agents, composerState.workingDir
for drafts, sourceDirectory for setup flows. The hook now persists only
{text, attachments}. Legacy persisted drafts with a cwd field still
hydrate; tagged COMPAT(draft-cwd) for removal after 2026-11-09.

Also moves attachment pills back inside the composer's bordered surface
via a new attachmentSlot prop on MessageInput, and shrinks pill height
from 48 to 32 — the subagents-track tuck previously had attachments
floating between it and the input.

Adds one Playwright spec asserting the draft-create-agent path submits
the workspace cwd.
This commit is contained in:
Mohamed Boudra
2026-05-09 18:32:43 +07:00
parent d241c24776
commit d1bda25a0b
14 changed files with 145 additions and 310 deletions
@@ -4,54 +4,6 @@ import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
import type { UseAgentFormStateResult } from "@/hooks/use-agent-form-state";
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types";
export interface DraftStorage {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
}
export interface StoredDraft {
text: string;
attachments: UserComposerAttachment[];
cwd: string;
}
export interface AgentInputDraftCore {
load(initialCwd?: string): Promise<StoredDraft | null>;
save(draft: StoredDraft): Promise<void>;
clear(): Promise<void>;
}
export function createAgentInputDraftCore(input: {
storage: DraftStorage;
storageKey: string;
}): AgentInputDraftCore {
return {
async load(initialCwd?: string) {
const json = await input.storage.getItem(input.storageKey);
if (!json) return null;
try {
const parsed = JSON.parse(json) as Record<string, unknown>;
return {
text: typeof parsed.text === "string" ? parsed.text : "",
attachments: Array.isArray(parsed.attachments)
? (parsed.attachments as UserComposerAttachment[])
: [],
cwd: typeof parsed.cwd === "string" ? parsed.cwd : (initialCwd ?? ""),
};
} catch {
return null;
}
},
async save(draft) {
await input.storage.setItem(input.storageKey, JSON.stringify(draft));
},
async clear() {
await input.storage.removeItem(input.storageKey);
},
};
}
export interface DraftKeyContext {
selectedServerId: string | null;
}
@@ -152,11 +104,8 @@ export function buildDraftStatusControls(input: {
export function hasDraftContent(input: {
text: string;
attachments: UserComposerAttachment[];
cwd: string;
}): boolean {
return (
input.text.trim().length > 0 || input.attachments.length > 0 || input.cwd.trim().length > 0
);
return input.text.trim().length > 0 || input.attachments.length > 0;
}
export function areAttachmentsEqual(input: {