wraith/frontend/src/components/copilot/CopilotSettings.vue
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

150 lines
5.1 KiB
Vue

<template>
<!-- Backdrop -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
@click.self="emit('close')"
>
<!-- Modal -->
<div class="w-80 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
<h3 class="text-sm font-semibold text-[#e0e0e0]">XO Settings</h3>
<button
class="text-[#8b949e] hover:text-[#e0e0e0] transition-colors cursor-pointer"
@click="emit('close')"
>
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
</svg>
</button>
</div>
<!-- Body -->
<div class="px-4 py-4 space-y-4">
<!-- Auth section -->
<div v-if="!store.isAuthenticated">
<button
class="w-full px-3 py-2 text-sm font-medium text-white bg-[#1f6feb] hover:bg-[#388bfd] rounded transition-colors cursor-pointer"
@click="handleLogin"
>
Connect to Claude
</button>
<div class="mt-3">
<label class="block text-xs text-[#8b949e] mb-1">Or enter API Key</label>
<input
v-model="apiKey"
type="password"
placeholder="sk-ant-..."
class="w-full px-2.5 py-1.5 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[#e0e0e0] placeholder-[#484f58] outline-none focus:border-[#58a6ff] transition-colors"
/>
<button
class="mt-2 w-full px-3 py-1.5 text-xs font-medium text-[#e0e0e0] bg-[#21262d] hover:bg-[#30363d] border border-[#30363d] rounded transition-colors cursor-pointer"
:disabled="!apiKey.trim()"
@click="handleApiKey"
>
Authenticate
</button>
</div>
</div>
<!-- Model selector -->
<div>
<label class="block text-xs text-[#8b949e] mb-1">Model</label>
<select
v-model="store.model"
class="w-full px-2.5 py-1.5 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] transition-colors cursor-pointer appearance-none"
>
<option value="claude-sonnet-4-5-20250514">claude-sonnet-4-5-20250514</option>
<option value="claude-opus-4-5-20250414">claude-opus-4-5-20250414</option>
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
</select>
</div>
<!-- Token usage -->
<div>
<label class="block text-xs text-[#8b949e] mb-1">Token Usage</label>
<div class="flex items-center gap-3 text-xs text-[#e0e0e0]">
<span>
<span class="text-[#8b949e]">In:</span> {{ formatTokens(store.tokenUsage.input) }}
</span>
<span>
<span class="text-[#8b949e]">Out:</span> {{ formatTokens(store.tokenUsage.output) }}
</span>
</div>
</div>
<!-- Actions -->
<div class="space-y-2 pt-2 border-t border-[#30363d]">
<button
class="w-full px-3 py-1.5 text-xs font-medium text-[#e0e0e0] bg-[#21262d] hover:bg-[#30363d] border border-[#30363d] rounded transition-colors cursor-pointer"
@click="handleClearHistory"
>
Clear History
</button>
<button
v-if="store.isAuthenticated"
class="w-full px-3 py-1.5 text-xs font-medium text-[#f85149] bg-[#21262d] hover:bg-[#30363d] border border-[#30363d] rounded transition-colors cursor-pointer"
@click="handleDisconnect"
>
Disconnect
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useCopilotStore } from "@/stores/copilot.store";
import { useCopilot } from "@/composables/useCopilot";
const store = useCopilotStore();
const { startLogin, logout, setModel } = useCopilot();
const apiKey = ref("");
const emit = defineEmits<{
(e: "close"): void;
}>();
async function handleLogin(): Promise<void> {
await startLogin();
// Auth state will be updated when the OAuth callback completes
// and the panel re-checks on next interaction.
store.isAuthenticated = true;
emit("close");
}
function handleApiKey(): void {
if (!apiKey.value.trim()) return;
// TODO: Wails AIService.SetApiKey(apiKey)
store.isAuthenticated = true;
apiKey.value = "";
emit("close");
}
function handleClearHistory(): void {
store.clearHistory();
emit("close");
}
async function handleDisconnect(): Promise<void> {
await logout();
emit("close");
}
function formatTokens(n: number): string {
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
return String(n);
}
// Sync model changes to the Go backend
watch(
() => store.model,
(newModel) => {
setModel(newModel);
},
);
</script>