diff --git a/css/views/dev.css b/css/views/dev.css deleted file mode 100644 index f491649..0000000 --- a/css/views/dev.css +++ /dev/null @@ -1,275 +0,0 @@ -.dev-view { - padding: var(--space-inset) 0; -} - -.dev-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-page); -} -.dev-header h2 { margin-bottom: 0; } - -.dev-view h2 { - font-size: 1.25rem; - font-weight: 700; - margin-bottom: var(--space-page); - color: var(--text); -} - -.dev-section { margin-bottom: var(--space-page); } - -.dev-section h3 { - color: var(--text-dim); - margin-bottom: var(--space-gap); -} - -.dev-actions { display: flex; gap: var(--space-gap); flex-wrap: wrap; } - -/* Credits widget */ -.credits-widget { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius, 6px); - padding: 16px 20px; -} -.credits-bar-track { - height: 6px; - background: var(--border); - border-radius: 3px; - overflow: hidden; - margin-bottom: 14px; -} -.credits-bar-fill { - height: 100%; - background: var(--accent); - border-radius: 3px; - transition: width 0.4s ease; -} -.credits-row { - display: flex; - gap: 32px; -} -.credits-stat { - display: flex; - flex-direction: column; - gap: 2px; -} -.credits-label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted, var(--text-dim)); -} -.credits-amount { - font-size: 1.1rem; - font-weight: 600; - color: var(--text); - font-variant-numeric: tabular-nums; -} -.credits-used { color: var(--error); } -.credits-remaining { color: var(--success-dim, var(--accent)); } - -/* Table */ -.dev-table { - width: 100%; - border-collapse: collapse; -} -.dev-table th { - text-align: left; - padding: 8px 12px; - color: var(--text-dim); - font-weight: 500; - border-bottom: 1px solid var(--border); -} -.dev-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); color: var(--text); } -.dev-table tr:last-child td { border-bottom: none; } -.dev-table .agent-id { font-weight: 600; } -.dev-table code { - background: var(--border); - padding: 2px 6px; - border-radius: 3px; - font-family: var(--font-mono); -} - -/* Dev flags */ -.dev-flags { display: flex; gap: 16px; flex-wrap: wrap; } -.dev-flag { - display: flex; - align-items: center; - gap: 6px; - color: var(--text); - cursor: pointer; - user-select: none; -} -.dev-flag input[type="checkbox"] { - accent-color: var(--accent); - width: 16px; - height: 16px; - cursor: pointer; -} -.dev-flag span { font-family: var(--font-mono); } - -.takeover-token { - font-family: var(--font-mono); - background: var(--surface); - border: 1px solid var(--border); - padding: 4px 10px; - border-radius: var(--radius-sm); - color: var(--accent); - user-select: all; -} - -.dev-loading { color: var(--text-dim); } -.dev-error { color: var(--error); } - -.dev-refresh-btn { - background: none; - border: 1px solid var(--border); - color: var(--text-dim); - padding: 6px 14px; - border-radius: 4px; - cursor: pointer; - transition: color 0.15s, border-color 0.15s; -} -.dev-refresh-btn:hover { color: var(--text); border-color: var(--text-dim); } -.dev-refresh-btn:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; } - -.dev-disco-btn { - background: none; - border: 1px solid var(--error); - color: var(--error); - padding: 6px 14px; - border-radius: 4px; - cursor: pointer; - transition: color 0.15s, border-color 0.15s, background 0.15s; -} -.dev-disco-btn:hover { background: var(--error)22; } -.dev-disco-btn:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; } - -/* Theme buttons */ -.dev-theme-btn { - background: var(--surface); - border: 1px solid var(--border); - color: var(--text-dim); - padding: 6px 18px; - border-radius: 4px; - cursor: pointer; - transition: color 0.15s, border-color 0.15s, background 0.15s; -} -.dev-theme-btn:hover { color: var(--text); border-color: var(--text-dim); } -.dev-theme-btn.active { - border-color: var(--accent); - color: var(--bg); - background: var(--accent); -} - -/* Table horizontal scroll wrapper */ -.dev-table-wrap { - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -/* Breakout confirmation modal */ -.breakout-modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; -} -.breakout-modal { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 24px 32px; - min-width: 300px; - text-align: center; -} -.breakout-modal h3 { color: var(--text); margin-bottom: 12px; } -.breakout-modal p { color: var(--text-dim); margin: 4px 0; } -.breakout-nonce { - font-family: var(--font-mono); - font-size: 2rem; - font-weight: 700; - color: var(--accent); - letter-spacing: 0.15em; - margin: 16px 0; -} -.breakout-modal-actions { - display: flex; - gap: 12px; - justify-content: center; - margin-top: 16px; -} - -/* ── MCP Counter ── */ -.counter-widget { transition: opacity 0.3s; } -.counter-widget.muted { opacity: 0.35; } -.counter-controls { display: flex; align-items: center; gap: 16px; } -.counter-btn { - width: 48px; height: 48px; border-radius: 50%; - border: 1px solid var(--text-dim); background: transparent; - color: var(--text); font-size: 1.5rem; cursor: pointer; - transition: all 0.2s; -} -.counter-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); } -.counter-btn:disabled { cursor: not-allowed; opacity: 0.3; } -.counter-value { - font-size: 2.5rem; font-variant-numeric: tabular-nums; - min-width: 3ch; text-align: center; color: var(--text); -} -.counter-challenge { - margin-top: 12px; display: flex; align-items: center; gap: 12px; - animation: counter-pulse 1s ease-in-out infinite alternate; -} -.counter-message { color: var(--accent); font-weight: 600; font-size: 0.9rem; } -.counter-timer { - font-variant-numeric: tabular-nums; color: var(--text-dim); - font-size: 0.85rem; min-width: 3ch; -} -.counter-hint { margin-top: 8px; font-size: 0.8rem; opacity: 0.4; } -@keyframes counter-pulse { - from { opacity: 0.7; } - to { opacity: 1; } -} -.counter-widget.flash { - animation: counter-flash 0.5s ease-out 3; -} -@keyframes counter-flash { - 0% { box-shadow: 0 0 0 0 var(--accent); } - 50% { box-shadow: 0 0 20px 4px var(--accent); } - 100% { box-shadow: 0 0 0 0 var(--accent); } -} - -/* ── Confetti ── */ -.confetti-container { - position: fixed; inset: 0; pointer-events: none; z-index: 9999; overflow: hidden; -} -.confetti-piece { - position: absolute; width: 10px; height: 10px; top: -20px; - animation: confetti-fall 3s ease-in forwards; -} -@keyframes confetti-fall { - 0% { transform: translateY(0) rotate(0deg); opacity: 1; } - 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; } -} - -/* ── Action Picker ── */ -.action-picker { display: flex; flex-wrap: wrap; gap: 8px; } -.action-pick-btn { - padding: 8px 16px; border-radius: 6px; - border: 1px solid var(--accent); background: transparent; - color: var(--accent); font-size: 0.85rem; cursor: pointer; - transition: all 0.2s; -} -.action-pick-btn:hover:not(:disabled) { background: var(--accent); color: var(--bg); } -.action-pick-btn:disabled { opacity: 0.3; cursor: not-allowed; } - -/* ── Mobile ── */ -@media (max-width: 639px) { - .dev-theme-btn, .dev-disco-btn { min-height: 44px; } - .dev-flag { min-height: 44px; } -} diff --git a/src/composables/ui.ts b/src/composables/ui.ts index a1e7e79..17025d5 100644 --- a/src/composables/ui.ts +++ b/src/composables/ui.ts @@ -1,51 +1,4 @@ import { computed, type Ref } from 'vue'; - -import type { Agent } from './agents'; // Import Agent interface - -export function formatUsage(u: any, agentId: string | null = null, allAgents: Agent[] | null = null): string { - if (!u) return ''; - const inn = u.input_tokens ?? u.in ?? null; - const out = u.output_tokens ?? u.out ?? null; - - let inCost = 0, outCost = 0, totalCost = 0; - if (agentId && allAgents && inn !== null && out !== null) { - const agent = allAgents.find(a => a.id === agentId); - if (agent && agent.promptPrice !== null && agent.completionPrice !== null) { - inCost = (inn / 1_000_000) * agent.promptPrice; - outCost = (out / 1_000_000) * agent.completionPrice; - totalCost = inCost + outCost; - } - } - - // Fallback to existing cost from backend if no agent pricing - if (totalCost === 0) { - totalCost = typeof u.cost === 'object' ? (u.cost?.total ?? 0) : Number(u.cost || 0); - } - - const showCosts = totalCost > 0.0000001; - const inCostStr = showCosts && inCost > 0 ? `$${inCost.toFixed(4)}` : ''; - const outCostStr = showCosts && outCost > 0 ? `$${outCost.toFixed(4)}` : ''; - - if (inn !== null && out !== null) { - const inFmt = inn >= 1000 ? (inn / 1000).toFixed(1) + 'k' : String(inn); - const outFmt = out >= 1000 ? (out / 1000).toFixed(1) + 'k' : String(out); - - let pricingStr = ''; - if (agentId && allAgents) { - const agent = allAgents.find(a => a.id === agentId); - if (agent && agent.promptPrice !== null && agent.completionPrice !== null) { - pricingStr = ` (${agent.promptPrice.toFixed(2)}/${agent.completionPrice.toFixed(2)})`; - } - } - - const parts = [`${inFmt} in${inCostStr ? ` (${inCostStr})` : ''}`, `${outFmt} out${outCostStr ? ` (${outCostStr})` : ''}`]; - if (showCosts) parts.push(`$${totalCost.toFixed(4)}${pricingStr}`); - return parts.join(' · '); - } - const total = u.total_tokens ?? u.total ?? 0; - return `${total} tokens${showCosts ? ` · $${totalCost.toFixed(4)}` : ''}`; -} - import pkg from '../../package.json'; declare const __BUILD__: string; diff --git a/src/store.ts b/src/store.ts index f1f0cbf..8a4357b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -10,6 +10,7 @@ import { useWebSocket } from './composables/ws'; import { useAgents } from './composables/agents'; import { useAuth } from './composables/auth'; import { useViewerStore } from './store/viewer'; +import tenant from './tenant'; // Viewer last-selected path — reactive singleton so App.vue nav link stays current export const lastViewerPath = ref(typeof localStorage !== 'undefined' ? localStorage.getItem('viewer_last_path') || '' : ''); @@ -31,8 +32,9 @@ agents.setCurrentUser(ws.currentUser); // Auth — on login/auto-connect, start HTTP transport export const auth = useAuth(() => { ws.connect(agents.selectedAgent, auth.isLoggedIn, auth.loginError, agents.selectedMode); - // Pre-warm viewer token in background so agent→viewer is instant - useViewerStore().acquire(); + if (tenant.features.viewer) { + useViewerStore().acquire(); + } }); // Auto-connect if valid token exists from previous session (deferred so Pinia is ready) setTimeout(() => auth.tryAutoConnect(), 0); diff --git a/src/views/DevView.vue b/src/views/DevView.vue deleted file mode 100644 index ebf842d..0000000 --- a/src/views/DevView.vue +++ /dev/null @@ -1,508 +0,0 @@ - - - - -