All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m2s
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>
151 lines
4.0 KiB
TypeScript
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 };
|
|
}
|