wraith/frontend/src/composables/useCopilot.ts
Vantz Stockwell fbd2fd4f80
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m2s
feat: wire real Claude API — OAuth login + live chat via Wails bindings
Replace mock responses in the XO copilot panel with real Wails binding
calls to the Go AIService backend:

- StartLogin now opens the browser via pkg/browser.OpenURL
- SendMessage returns ChatResponse (text + tool call results) instead of
  bare error, fixing the tool-call accumulation bug in messageLoop
- Add GetModel/SetModel methods for frontend model switching
- Frontend useCopilot composable calls Go via Call.ByName from
  @wailsio/runtime, with conversation auto-creation, auth checks, and
  error display in the chat panel
- Store defaults to isAuthenticated=false; panel checks auth on mount
- CopilotSettings syncs model changes and logout to the backend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:22:07 -04:00

151 lines
4.0 KiB
TypeScript

import { useCopilotStore } from "@/stores/copilot.store";
import type { ToolCall } from "@/stores/copilot.store";
import { Call } from "@wailsio/runtime";
/**
* Fully qualified Go method name prefix for AIService bindings.
* Wails v3 ByName format: 'package.struct.method'
*/
const AI = "github.com/vstockwell/wraith/internal/ai.AIService";
/** Call a bound Go method on AIService by name. */
async function callAI<T = unknown>(method: string, ...args: unknown[]): Promise<T> {
return Call.ByName(`${AI}.${method}`, ...args) as Promise<T>;
}
/** Shape returned by Go AIService.SendMessage. */
interface ChatResponse {
text: string;
toolCalls?: {
name: string;
input: unknown;
result: unknown;
error?: string;
}[];
}
/**
* Composable providing real Wails binding wrappers for the AI copilot.
*
* Calls the Go AIService via Wails v3 Call.ByName. SendMessage blocks
* until the full response (including tool-use loops) is complete.
*/
export function useCopilot() {
const store = useCopilotStore();
/**
* Process a user message by calling the real Go backend.
* The backend blocks until the full response is ready (no streaming yet).
*/
async function processMessage(text: string): Promise<void> {
store.isStreaming = true;
// Ensure we have a conversation
if (!store.activeConversationId) {
try {
const convId = await callAI<string>("NewConversation");
store.activeConversationId = convId;
} catch (err) {
store.messages.push({
id: `msg-${Date.now()}`,
role: "assistant",
content: `Error creating conversation: ${err}`,
timestamp: Date.now(),
});
store.isStreaming = false;
return;
}
}
try {
const response = await callAI<ChatResponse>(
"SendMessage",
store.activeConversationId,
text,
);
// Build the assistant message from the response
const toolCalls: ToolCall[] | undefined = response.toolCalls?.map(
(tc) => ({
id: `tc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: tc.name,
input: (tc.input ?? {}) as Record<string, unknown>,
result: tc.result,
status: (tc.error ? "error" : "done") as "done" | "error",
}),
);
store.messages.push({
id: `msg-${Date.now()}`,
role: "assistant",
content: response.text || "",
toolCalls: toolCalls,
timestamp: Date.now(),
});
} catch (err) {
store.messages.push({
id: `msg-${Date.now()}`,
role: "assistant",
content: `Error: ${err}`,
timestamp: Date.now(),
});
} finally {
store.isStreaming = false;
}
}
/** Begin the OAuth login flow (opens browser). */
async function startLogin(): Promise<void> {
try {
await callAI("StartLogin");
} catch (err) {
console.error("StartLogin failed:", err);
}
}
/** Check whether the user is authenticated. */
async function checkAuth(): Promise<boolean> {
try {
const authed = await callAI<boolean>("IsAuthenticated");
store.isAuthenticated = authed;
return authed;
} catch {
return false;
}
}
/** Log out and clear tokens. */
async function logout(): Promise<void> {
try {
await callAI("Logout");
store.isAuthenticated = false;
store.clearHistory();
} catch (err) {
console.error("Logout failed:", err);
}
}
/** Sync the model setting to the Go backend. */
async function setModel(model: string): Promise<void> {
try {
await callAI("SetModel", model);
store.model = model;
} catch (err) {
console.error("SetModel failed:", err);
}
}
/** Load the current model from the Go backend. */
async function getModel(): Promise<string> {
try {
const m = await callAI<string>("GetModel");
store.model = m;
return m;
} catch {
return store.model;
}
}
return { processMessage, startLogin, checkAuth, logout, setModel, getModel };
}