feat(quick-pane): promote active runtime
This commit is contained in:
@@ -389,7 +389,8 @@ describe("QuickPane", () => {
|
||||
provider: "provider",
|
||||
id: "quick/model",
|
||||
name: "Quick Model",
|
||||
thinkingLevelMap: { off: "off", high: "high" },
|
||||
reasoning: true,
|
||||
thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high" },
|
||||
},
|
||||
];
|
||||
bridgeMock([thinkingModels], () =>
|
||||
@@ -663,23 +664,13 @@ describe("QuickPane", () => {
|
||||
expect(response.scrollTop).toBe(100);
|
||||
});
|
||||
|
||||
it("shows manual continuation after a completed normal answer and hands off transcript", async () => {
|
||||
it("promotes completed quick runtime without copying a handoff", async () => {
|
||||
bridgeMock([defaultModels], () =>
|
||||
snapshotWithTranscript([
|
||||
{ role: "user", content: "Inspect" },
|
||||
{ role: "assistant", content: "Completed answer" },
|
||||
]),
|
||||
);
|
||||
const baseImplementation = mocks.invoke.getMockImplementation();
|
||||
mocks.invoke.mockImplementation(
|
||||
(command: string, payload?: Record<string, unknown>) => {
|
||||
if (command === "create_session_runtime")
|
||||
return Promise.resolve({
|
||||
runtime: { agentId: "agent-full", runtimeId: "runtime-full" },
|
||||
});
|
||||
return baseImplementation?.(command, payload);
|
||||
},
|
||||
);
|
||||
|
||||
render(<QuickPane />);
|
||||
await preparedTrigger();
|
||||
@@ -690,19 +681,26 @@ describe("QuickPane", () => {
|
||||
expect(
|
||||
continueButton.closest(".quick-pane-assistant-footer"),
|
||||
).not.toBeNull();
|
||||
|
||||
const submittedPromptCount = mocks.invoke.mock.calls.filter(
|
||||
([command]) => command === "submit_prompt",
|
||||
).length;
|
||||
await userEvent.click(continueButton);
|
||||
await waitFor(() =>
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("promote_quick_runtime", {
|
||||
runtimeId: "runtime-1",
|
||||
}),
|
||||
);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane");
|
||||
expect(mocks.invoke).not.toHaveBeenCalledWith(
|
||||
"create_session_runtime",
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
await userEvent.click(continueButton);
|
||||
await waitFor(() =>
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
|
||||
agentId: "agent-full",
|
||||
message:
|
||||
"Inspect\n\nQuick Pane handoff: Continue this chat in a full session.\n\nQuick Pane transcript:\nUser: Inspect\n\nAssistant: Completed answer",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
mocks.invoke.mock.calls.filter(
|
||||
([command]) => command === "submit_prompt",
|
||||
),
|
||||
).toHaveLength(submittedPromptCount);
|
||||
});
|
||||
|
||||
it("keeps unlimited follow-ups on one runtime and renders snapshot messages in order", async () => {
|
||||
@@ -988,10 +986,6 @@ describe("QuickPane", () => {
|
||||
const baseImplementation = mocks.invoke.getMockImplementation();
|
||||
mocks.invoke.mockImplementation(
|
||||
(command: string, payload?: Record<string, unknown>) => {
|
||||
if (command === "create_session_runtime")
|
||||
return Promise.resolve({
|
||||
runtime: { agentId: "agent-full", runtimeId: "runtime-full" },
|
||||
});
|
||||
if (command === "submit_prompt" && payload?.agentId === "agent-1")
|
||||
submittedPrompts.push(String(payload.message));
|
||||
return baseImplementation?.(command, payload);
|
||||
@@ -1011,18 +1005,18 @@ describe("QuickPane", () => {
|
||||
await screen.findByText("Second runtime failed."),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("First answer")).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("create_session_runtime", {
|
||||
worktreePath: "/repo",
|
||||
await userEvent.click(
|
||||
within(screen.getByRole("status")).getByRole("button", {
|
||||
name: "Continue",
|
||||
}),
|
||||
);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
|
||||
agentId: "agent-full",
|
||||
message:
|
||||
"Second\n\nQuick Pane handoff: Quick runtime failed. Continue in a full session if needed.\n\nQuick Pane transcript:\nUser: First\n\nAssistant: First answer\n\nUser: Second",
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("promote_quick_runtime", {
|
||||
runtimeId: "runtime-1",
|
||||
}),
|
||||
);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane");
|
||||
});
|
||||
|
||||
it("maps the exact submitted payload to the entered multiline request despite delimiter collisions", async () => {
|
||||
|
||||
@@ -213,6 +213,7 @@ export default function QuickPane() {
|
||||
const [submissionStarting, setSubmissionStarting] = useState(false);
|
||||
const [requestActive, setRequestActive] = useState(false);
|
||||
const [chatReplacing, setChatReplacing] = useState(false);
|
||||
const [promoting, setPromoting] = useState(false);
|
||||
const [thinkingLevel, setThinkingLevel] = useState("medium");
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
const [catalogStatus, setCatalogStatus] = useState<
|
||||
@@ -229,7 +230,6 @@ export default function QuickPane() {
|
||||
const [catalogNotice, setCatalogNotice] = useState<string>();
|
||||
const [runtime, setRuntime] = useState<Runtime>();
|
||||
const [messages, setMessages] = useState<TranscriptMessage[]>([]);
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [responseComplete, setResponseComplete] = useState(false);
|
||||
const [escalation, setEscalation] = useState<string>();
|
||||
const [extension, setExtension] = useState<Extension>();
|
||||
@@ -634,7 +634,6 @@ export default function QuickPane() {
|
||||
setRequestActive(false);
|
||||
setSubmissionStarting(false);
|
||||
setMessages([]);
|
||||
setAnswer("");
|
||||
setResponseComplete(false);
|
||||
setEscalation(undefined);
|
||||
setExtension(undefined);
|
||||
@@ -894,8 +893,6 @@ export default function QuickPane() {
|
||||
activeTool: snapshot.runtime.activeTool,
|
||||
hasAssistantText: !!text,
|
||||
});
|
||||
if (promptSubmittedRuntimeRef.current === runtime.runtimeId)
|
||||
setAnswer(result);
|
||||
setExtension(snapshot.extensions[0]);
|
||||
if (
|
||||
complete &&
|
||||
@@ -983,7 +980,6 @@ export default function QuickPane() {
|
||||
};
|
||||
promptSubmittedRuntimeRef.current = undefined;
|
||||
completionHandledRef.current = false;
|
||||
setAnswer("");
|
||||
setResponseComplete(false);
|
||||
setError(undefined);
|
||||
setEscalation(undefined);
|
||||
@@ -1050,9 +1046,7 @@ export default function QuickPane() {
|
||||
}
|
||||
}
|
||||
|
||||
const handoffRequest =
|
||||
request.trim() || submittedRequestRef.current?.request || "";
|
||||
const canContinue = !invalid && !!handoffRequest;
|
||||
const canContinue = !invalid && !!runtime && !promoting;
|
||||
const completedAssistantAnswer =
|
||||
responseComplete &&
|
||||
messages.some((message) => message.role === "assistant");
|
||||
@@ -1069,35 +1063,22 @@ export default function QuickPane() {
|
||||
responseComplete ||
|
||||
submittedRequestRef.current !== undefined;
|
||||
const hasTranscript = messages.length > 0 || runActive || responseComplete;
|
||||
const transcriptHandoff = messages
|
||||
.map(
|
||||
(message) =>
|
||||
`${message.role === "user" ? "User" : "Assistant"}: ${message.markdown}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
async function continueFull() {
|
||||
if (invalid || !handoffRequest)
|
||||
return setError(invalid ?? "Enter a request.");
|
||||
const current = runtimeRef.current;
|
||||
if (invalid || !current) return setError(invalid ?? "Quick runtime is unavailable.");
|
||||
setPromoting(true);
|
||||
try {
|
||||
const result = await invoke<{ runtime: Runtime }>(
|
||||
"create_session_runtime",
|
||||
{
|
||||
worktreePath: settings.workspace,
|
||||
},
|
||||
);
|
||||
await invoke("set_model", {
|
||||
agentId: result.runtime.agentId,
|
||||
provider: settings.followUpProvider,
|
||||
modelId: settings.followUpModel,
|
||||
await invoke<{ runtime: Runtime }>("promote_quick_runtime", {
|
||||
runtimeId: current.runtimeId,
|
||||
});
|
||||
await invoke("submit_prompt", {
|
||||
agentId: result.runtime.agentId,
|
||||
message: `${handoffRequest}\n\nQuick Pane handoff: ${escalation ?? "Continue this chat in a full session."}\n\nQuick Pane transcript:\n${transcriptHandoff || answer}`,
|
||||
});
|
||||
setNotice("Full session started with follow-up model.");
|
||||
cleanupQueueRef.current.delete(current.runtimeId);
|
||||
clearRuntimeIfCurrent(current.runtimeId);
|
||||
await dismissPane();
|
||||
} catch (reason) {
|
||||
setError(`Could not start full session: ${String(reason)}`);
|
||||
setError(`Could not continue in full session: ${String(reason)}`);
|
||||
} finally {
|
||||
setPromoting(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1187,6 +1168,16 @@ export default function QuickPane() {
|
||||
/>
|
||||
</div>
|
||||
<div className="quick-pane-header-actions">
|
||||
{runtime && (
|
||||
<button
|
||||
className="quick-pane-action"
|
||||
type="button"
|
||||
disabled={catalogOverlayActive || promoting}
|
||||
onClick={continueFull}
|
||||
>
|
||||
{promoting ? "Continuing…" : "Continue"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="quick-pane-close"
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user