Skip to content

Commit cb2a2ac

Browse files
committed
upon provider error, reset to easily resubmit last user message
1 parent 265ae5e commit cb2a2ac

File tree

4 files changed

+114
-25
lines changed

4 files changed

+114
-25
lines changed

node/chat/chat.spec.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,8 @@ describe("tea/chat.spec.ts", () => {
364364
await withDriver(async (driver) => {
365365
await driver.showSidebar();
366366

367-
await driver.inputMagentaText("Generate some text for me");
367+
const userPrompt = "Generate some text for me";
368+
await driver.inputMagentaText(userPrompt);
368369
await driver.send();
369370

370371
const pendingRequest = await driver.mockAnthropic.awaitPendingRequest();
@@ -383,11 +384,8 @@ describe("tea/chat.spec.ts", () => {
383384
"Error Connection error during response",
384385
);
385386

386-
const displayText = await driver.getDisplayBufferText();
387-
expect(displayText).toContain("# user:");
388-
expect(displayText).toContain("Generate some text for me");
389-
390-
expect(displayText).toContain("Error Connection error");
387+
// Check that the input buffer is pre-populated with the last user message
388+
await driver.assertInputBufferContains(userPrompt);
391389
});
392390
});
393391
});

node/chat/chat.ts

+72-18
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type ConversationState =
4040
| {
4141
state: "error";
4242
error: Error;
43+
lastAssistantMessage?: Message.Model;
4344
};
4445

4546
export type Model = {
@@ -98,6 +99,10 @@ export type Msg =
9899
}
99100
| {
100101
type: "show-message-debug-info";
102+
}
103+
| {
104+
type: "sidebar-setup-resubmit";
105+
lastUserMessage: string;
101106
};
102107

103108
export function init({ nvim, lsp }: { nvim: Nvim; lsp: Lsp }) {
@@ -173,28 +178,67 @@ export function init({ nvim, lsp }: { nvim: Nvim; lsp: Lsp }) {
173178

174179
case "conversation-state": {
175180
model.conversation = msg.conversation;
176-
if (msg.conversation.state == "stopped") {
177-
const lastMessage = model.messages[model.messages.length - 1];
178-
if (lastMessage?.role === "assistant") {
179-
lastMessage.parts.push({
180-
type: "stop-msg",
181-
stopReason: msg.conversation.stopReason,
182-
usage: msg.conversation.usage,
183-
});
181+
182+
switch (msg.conversation.state) {
183+
case "stopped": {
184+
const lastMessage = model.messages[model.messages.length - 1];
185+
if (lastMessage?.role === "assistant") {
186+
lastMessage.parts.push({
187+
type: "stop-msg",
188+
stopReason: msg.conversation.stopReason,
189+
usage: msg.conversation.usage,
190+
});
191+
}
192+
return [model, maybeAutorespond(model)];
184193
}
185-
}
186194

187-
if (msg.conversation.state == "error") {
188-
const lastMessage = model.messages[model.messages.length - 1];
189-
if (lastMessage?.role == "assistant") {
190-
// get rid of the latest assistant message so we can re-submit.
191-
model.messages.pop();
195+
case "error": {
196+
const lastAssistantMessage =
197+
model.messages[model.messages.length - 1];
198+
if (lastAssistantMessage?.role == "assistant") {
199+
model.messages.pop();
200+
201+
// save the last message so we can show a nicer error message.
202+
(
203+
model.conversation as Extract<
204+
ConversationState,
205+
{ state: "error" }
206+
>
207+
).lastAssistantMessage = lastAssistantMessage;
208+
}
209+
210+
const lastUserMessage = model.messages[model.messages.length - 1];
211+
if (lastUserMessage?.role == "user") {
212+
model.messages.pop();
213+
const sidebarResubmitSetupThunk: Thunk<Msg> = (dispatch) =>
214+
new Promise((resolve) => {
215+
dispatch({
216+
type: "sidebar-setup-resubmit",
217+
lastUserMessage: lastUserMessage.parts
218+
.map((p) => (p.type == "text" ? p.text : ""))
219+
.join(""),
220+
});
221+
resolve();
222+
});
223+
224+
return [model, sidebarResubmitSetupThunk];
225+
}
226+
return [model];
192227
}
193-
}
194228

195-
return [model, maybeAutorespond(model)];
229+
case "message-in-flight":
230+
return [model];
231+
232+
default:
233+
return assertUnreachable(msg.conversation);
234+
}
196235
}
197236

237+
case "sidebar-setup-resubmit":
238+
// this action is really just there so the parent (magenta app) can observe it and manipulate the sidebar
239+
// accordingly
240+
return [model];
241+
198242
case "send-message": {
199243
const lastMessage = model.messages[model.messages.length - 1];
200244
if (lastMessage && lastMessage.role == "user") {
@@ -504,7 +548,8 @@ export function init({ nvim, lsp }: { nvim: Nvim; lsp: Lsp }) {
504548
}) => {
505549
if (
506550
model.messages.length == 0 &&
507-
Object.keys(model.contextManager.files).length == 0
551+
Object.keys(model.contextManager.files).length == 0 &&
552+
model.conversation.state == "stopped"
508553
) {
509554
return d`${LOGO}`;
510555
}
@@ -532,7 +577,16 @@ export function init({ nvim, lsp }: { nvim: Nvim; lsp: Lsp }) {
532577
? withBindings(d`Stopped (${model.conversation.stopReason})`, {
533578
"<CR>": () => dispatch({ type: "show-message-debug-info" }),
534579
})
535-
: d`Error ${model.conversation.error.message}${model.conversation.error.stack ? "\n" + model.conversation.error.stack : ""}`
580+
: d`Error ${model.conversation.error.message}${model.conversation.error.stack ? "\n" + model.conversation.error.stack : ""}${
581+
model.conversation.lastAssistantMessage
582+
? "\n\nLast assistant message:\n" +
583+
JSON.stringify(
584+
model.conversation.lastAssistantMessage,
585+
null,
586+
2,
587+
)
588+
: ""
589+
}`
536590
}${
537591
model.conversation.state != "message-in-flight" &&
538592
!contextManagerModel.isContextEmpty(model.contextManager)

node/magenta.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,29 @@ export class Magenta {
6464
this.chatApp = TEA.createApp({
6565
nvim: this.nvim,
6666
initialModel: this.chatModel.initModel(),
67-
update: (msg, model) => this.chatModel.update(msg, model, { nvim }),
67+
update: (msg, model) => {
68+
if (msg.type == "sidebar-setup-resubmit") {
69+
if (
70+
this.sidebar &&
71+
this.sidebar.state &&
72+
this.sidebar.state.inputBuffer
73+
) {
74+
this.sidebar.state.inputBuffer
75+
.setLines({
76+
start: 0,
77+
end: -1,
78+
lines: msg.lastUserMessage.split("\n") as Line[],
79+
})
80+
.catch((error) => {
81+
this.nvim.logger?.error(
82+
`Error updating sidebar input: ${error}`,
83+
);
84+
});
85+
}
86+
}
87+
88+
return this.chatModel.update(msg, model, { nvim });
89+
},
6890
View: this.chatModel.view,
6991
});
7092

node/providers/anthropic.ts

+15
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ export class AnthropicProvider implements Provider {
458458
}
459459
};
460460

461+
let requestActive = true;
461462
try {
462463
this.request = this.client.messages
463464
.stream(
@@ -466,10 +467,16 @@ export class AnthropicProvider implements Provider {
466467
) as Anthropic.Messages.MessageStreamParams,
467468
)
468469
.on("text", (text: string) => {
470+
if (!requestActive) {
471+
return;
472+
}
469473
buf.push(text);
470474
flushBuffer();
471475
})
472476
.on("inputJson", (_delta, snapshot) => {
477+
if (!requestActive) {
478+
return;
479+
}
473480
this.nvim.logger?.debug(
474481
`anthropic stream inputJson: ${JSON.stringify(snapshot)}`,
475482
);
@@ -559,12 +566,20 @@ export class AnthropicProvider implements Provider {
559566
usage.cacheMisses = response.usage.cache_creation_input_tokens;
560567
}
561568

569+
if (!requestActive) {
570+
throw new Error(`request no longer active`);
571+
}
572+
562573
return {
563574
toolRequests,
564575
stopReason: response.stop_reason || "end_turn",
565576
usage,
566577
};
567578
} finally {
579+
requestActive = false;
580+
if (this.request) {
581+
this.request.abort();
582+
}
568583
this.request = undefined;
569584
}
570585
}

0 commit comments

Comments
 (0)