Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/components/convert-chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ export function ConvertChip() {
const { run, cancel } = useConvert();
const t = useT();

// Only show in split mode — when only one pane is visible there's no
// divider to hang off, and the toolbar button is already obvious.
if (layoutMode !== "split") return null;

const agentInfo = agents.find((a) => a.id === agent);
const model = agent ? agentModels[agent] ?? "default" : "default";
const canConvert =
Expand Down Expand Up @@ -55,6 +51,7 @@ export function ConvertChip() {
// Lives here (not in Toolbar) because the chip is the single source of
// Convert truth after the toolbar button was removed.
useEffect(() => {
if (layoutMode !== "split") return;
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
if (isRunning || !canConvert) return;
Expand All @@ -64,7 +61,11 @@ export function ConvertChip() {
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClick, isRunning, canConvert]);
}, [layoutMode, onClick, isRunning, canConvert]);

// Only show in split mode — when only one pane is visible there's no
// divider to hang off, and the toolbar button is already obvious.
if (layoutMode !== "split") return null;

return (
<div
Expand Down
44 changes: 41 additions & 3 deletions src/lib/agents/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,11 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age

if (agent === "codex") {
if (obj.type === "item.completed" && obj.item && typeof obj.item === "object") {
const item = obj.item as { item_type?: string; text?: string };
if (item.item_type === "assistant_message" && typeof item.text === "string") {
const item = obj.item as { item_type?: string; type?: string; text?: string };
if (
(item.item_type === "assistant_message" || item.type === "agent_message") &&
typeof item.text === "string"
) {
out.push({ kind: "delta", text: item.text });
}
}
Expand All @@ -313,12 +316,16 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age
out.push({ kind: "delta", text: msg.message });
}
}
if (obj.type === "task_complete" && obj.usage) {
if ((obj.type === "task_complete" || obj.type === "turn.completed") && obj.usage) {
out.push({ kind: "meta", key: "usage", value: obj.usage });
}
}

if (agent === "cursor-agent" || agent === "gemini") {
if (obj.type === "init") {
if (obj.model) out.push({ kind: "meta", key: "model", value: obj.model });
if (obj.session_id) out.push({ kind: "meta", key: "session", value: obj.session_id });
}
if (obj.type === "stream_event" && obj.event && typeof obj.event === "object") {
const ev = obj.event as { type?: string; delta?: { type?: string; text?: string } };
if (ev.delta?.type === "text_delta" && typeof ev.delta.text === "string") {
Expand All @@ -341,6 +348,15 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age
if (text) out.push({ kind: "delta", text });
}
}
if (obj.type === "message" && obj.role === "assistant" && typeof obj.content === "string") {
if (obj.delta === true || !state.sawStreamEventText) {
out.push({ kind: "delta", text: obj.content });
}
if (obj.delta === true) state.sawStreamEventText = true;
}
if (obj.type === "result" && obj.stats) {
out.push({ kind: "meta", key: "usage", value: obj.stats });
}
// Bare `text` field — only honor it when we haven't already emitted a
// streamed delta or an assistant body, otherwise it duplicates the same
// payload (cursor-agent / gemini both ship this redundancy on some
Expand All @@ -351,6 +367,28 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age
}

if (agent === "copilot") {
if (obj.type === "session.tools_updated" && obj.data && typeof obj.data === "object") {
const data = obj.data as { model?: string };
if (data.model) out.push({ kind: "meta", key: "model", value: data.model });
}
if (obj.type === "assistant.message_delta" && obj.data && typeof obj.data === "object") {
const data = obj.data as { deltaContent?: string };
if (typeof data.deltaContent === "string") {
state.sawStreamEventText = true;
out.push({ kind: "delta", text: data.deltaContent });
}
}
if (obj.type === "assistant.message" && obj.data && typeof obj.data === "object") {
const data = obj.data as { content?: string; model?: string };
if (data.model) out.push({ kind: "meta", key: "model", value: data.model });
if (typeof data.content === "string" && !state.sawStreamEventText) {
out.push({ kind: "delta", text: data.content });
}
}
if (obj.type === "result" && obj.data && typeof obj.data === "object") {
const data = obj.data as { usage?: unknown };
if (data.usage) out.push({ kind: "meta", key: "usage", value: data.usage });
}
if (typeof obj.response === "string") out.push({ kind: "delta", text: obj.response });
if (typeof obj.text === "string") out.push({ kind: "delta", text: obj.text });
}
Expand Down
14 changes: 14 additions & 0 deletions src/lib/use-convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ export function useConvert() {
: `准备调用 ${req.agent}${useModel ? ` · 模型 ${useModel}` : ""} · 模板 ${req.templateId} · ${sizeNote}`,
});

const stillWaitingTimers = [30_000, 75_000].map((ms) =>
window.setTimeout(() => {
const current = useStore.getState().tasks.find((t) => t.id === taskId);
if (current?.status !== "running" || current.stats.deltaCount > 0) return;
const waited = Math.round((Date.now() - startedAt) / 1000);
useStore.getState().pushLogFor(taskId, {
kind: "info",
elapsed: Date.now() - startedAt,
text: `仍在等待 ${req.agent} 的首段输出 (${waited}s)。Codex / Copilot 这类 CLI 有时会先完整生成,再一次性吐出 HTML;请先不要刷新页面或再次点击 Convert。`,
});
}, ms),
);

try {
const res = await fetch("/api/convert", {
method: "POST",
Expand Down Expand Up @@ -160,6 +173,7 @@ export function useConvert() {
});
useStore.getState().setStatusFor(taskId, "error");
} finally {
stillWaitingTimers.forEach((id) => window.clearTimeout(id));
if (controllers.get(taskId) === ctl) controllers.delete(taskId);
}
},
Expand Down