wraith/frontend/src/components/copilot/CopilotSettings.vue
Vantz Stockwell e916d5942b
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m5s
feat: "Use Claude Code Token" button — imports credentials.json as OAuth fallback
Reads %USERPROFILE%\.claude\.credentials.json (or ~/.claude/.credentials.json),
extracts the access and refresh tokens, stores them encrypted in Wraith's vault.
Works when Wraith's own OAuth exchange fails. If Claude Code is authenticated
on the same machine, Wraith piggybacks on the existing token.

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

171 lines
5.8 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 (OAuth)
</button>
<button
class="w-full mt-2 px-3 py-2 text-sm font-medium text-[#58a6ff] border border-[#30363d] hover:bg-[#1c2128] rounded transition-colors cursor-pointer"
@click="handleImportClaudeCode"
>
Use Claude Code Token
</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 { Call } from "@wailsio/runtime";
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;
}>();
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
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");
}
async function handleImportClaudeCode(): Promise<void> {
try {
await Call.ByName(`${APP}.ImportClaudeCodeToken`);
store.isAuthenticated = true;
alert("Claude Code token imported successfully.");
emit("close");
} catch (err: any) {
alert(`Failed to import token: ${err?.message ?? err}`);
}
}
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>