generated from nhcarrigan/template
feat: handle StopFailure hook event for API error turns (#224)
Parses [StopFailure Hook] from stderr, emits claude:stop-failure, and transitions the character to error state with a toast notification.
This commit is contained in:
@@ -187,6 +187,33 @@ describe("toastStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("addError", () => {
|
||||
it("adds an error toast with the warning icon", () => {
|
||||
toastStore.addError("Something went wrong");
|
||||
const toasts = get(toastStore);
|
||||
expect(toasts).toHaveLength(1);
|
||||
const toast = toasts[0];
|
||||
expect(toast.kind).toBe("info");
|
||||
if (toast.kind === "info") {
|
||||
expect(toast.message).toBe("Something went wrong");
|
||||
expect(toast.icon).toBe("⚠️");
|
||||
expect(typeof toast.id).toBe("string");
|
||||
expect(toast.id.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-dismisses after 6000ms", () => {
|
||||
toastStore.addError("Rate limit reached");
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(5999);
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(get(toastStore)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addUpdate", () => {
|
||||
it("adds a persistent update toast with the correct fields", () => {
|
||||
toastStore.addUpdate("2.0.0", "1.9.0", "https://example.com/release");
|
||||
|
||||
@@ -68,6 +68,13 @@ function createToastStore() {
|
||||
setTimeout(() => remove(id), 4000);
|
||||
}
|
||||
|
||||
function addError(message: string) {
|
||||
const id = crypto.randomUUID();
|
||||
const toast: InfoToast = { id, kind: "info", message, icon: "⚠️" };
|
||||
update((toasts) => [...toasts, toast]);
|
||||
setTimeout(() => remove(id), 6000);
|
||||
}
|
||||
|
||||
function addAchievement(achievement: AchievementUnlockedEvent["achievement"]) {
|
||||
const id = crypto.randomUUID();
|
||||
const toast: AchievementToast = { id, kind: "achievement", achievement };
|
||||
@@ -82,7 +89,7 @@ function createToastStore() {
|
||||
// Update toasts are persistent — no auto-dismiss
|
||||
}
|
||||
|
||||
return { subscribe, addInfo, addAchievement, addUpdate, remove };
|
||||
return { subscribe, addInfo, addError, addAchievement, addUpdate, remove };
|
||||
}
|
||||
|
||||
export const toastStore = createToastStore();
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ConnectionStatus,
|
||||
ElicitationEvent,
|
||||
PermissionPromptEvent,
|
||||
StopFailureEvent,
|
||||
UserQuestionEvent,
|
||||
} from "$lib/types/messages";
|
||||
import type { CharacterState } from "$lib/types/states";
|
||||
@@ -640,6 +641,24 @@ export async function initializeTauriListeners() {
|
||||
}
|
||||
);
|
||||
unlisteners.push(elicitationResultUnlisten);
|
||||
|
||||
const stopFailureUnlisten = await listen<StopFailureEvent>("claude:stop-failure", (event) => {
|
||||
const { stop_reason, error_type } = event.payload;
|
||||
|
||||
characterState.setTemporaryState("error", 3000);
|
||||
|
||||
let message: string;
|
||||
if (stop_reason === "rate_limit") {
|
||||
message = "Rate limit reached";
|
||||
} else if (stop_reason === "auth_failure" || stop_reason === "authentication") {
|
||||
message = "Authentication failed";
|
||||
} else {
|
||||
message = `API error: ${stop_reason ?? error_type ?? "unknown"}`;
|
||||
}
|
||||
|
||||
toastStore.addError(message);
|
||||
});
|
||||
unlisteners.push(stopFailureUnlisten);
|
||||
}
|
||||
|
||||
export function cleanupTauriListeners() {
|
||||
|
||||
@@ -176,6 +176,12 @@ export interface ElicitationResultEvent {
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
export interface StopFailureEvent {
|
||||
stop_reason?: string;
|
||||
error_type?: string;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
||||
|
||||
export interface Attachment {
|
||||
|
||||
Reference in New Issue
Block a user