style: UI polish and miscellaneous fixes

Fixes link color from hardcoded white to currentColor for proper theme
support. Adds video and audio modality icons to ModelInfo. Registers
anthropic, nvidia, and xiaomi in the provider icon map. Removes unused
pagination validation from agents and topics endpoints. Adds convenience
start script. Updates AGENTS.md.
This commit is contained in:
Zoe
2026-04-27 12:07:01 -05:00
parent 03a0c3587b
commit 7a944afdb5
9 changed files with 54 additions and 91 deletions
+1 -52
View File
@@ -151,7 +151,7 @@ await db.insert(agents).values(data).onConflictDoNothing();
2. **Session**: Better-auth creates session token → stored in cookie 2. **Session**: Better-auth creates session token → stored in cookie
3. **API Keys**: Encrypted client-side with AES-GCM (key derived from password + userId) 3. **API Keys**: Encrypted client-side with AES-GCM (key derived from password + userId)
**Key files**: `lib/auth.ts`, `lib/auth-client.ts`, `app/plugins/auth.*.ts`, `app/middleware/auth.global.ts` **Key files**: `lib/auth.ts`, `app/plugins/auth.*.ts`, `app/middleware/auth.global.ts`, `app/composables/useAuth.ts`
## Styling System ## Styling System
@@ -179,57 +179,6 @@ await db.insert(agents).values(data).onConflictDoNothing();
| `useTheme()` | Accent/neutral theme cookies | | `useTheme()` | Accent/neutral theme cookies |
| `useSettings()` | Settings dialog state | | `useSettings()` | Settings dialog state |
## Key Routes
| Route | File |
|-------|------|
| `/` | `app/pages/index.vue` |
| `/auth/login` | `app/pages/auth/login.vue` |
| `/auth/register` | `app/pages/auth/register.vue` |
| `/agent/:id` | `app/pages/agent/[id]/index.vue` |
| `/agent/:id/topic/:topicId` | `app/pages/agent/[id]/topic/[topicId].vue` |
| `/agent/:id/profile` | `app/pages/agent/[id]/profile.vue` |
## Server API Routes
| Endpoint | File |
|----------|------|
| `POST /api/chat/generate` | `server/api/chat/generate.post.ts` |
| `POST /api/chat/cancel/:id` | `server/api/chat/cancel/[generationId].post.ts` |
| `POST /api/provider/:id/models` | `server/api/provider/[providerId]/models.post.ts` |
| `* /api/auth/*` | `server/api/auth/[...all].ts` |
## Known Issues (from BUGS.md)
1. Sidebar hover animation occasionally glitches on agent routes
2. Theme switcher + sidebar interaction bug
## Critical Architecture Notes
### Chat Endpoint God File
- `server/api/topic/[topicId]/chat/index.post.ts` is 1263 lines handling route validation, tool definitions, streaming orchestration, DB transactions, and event emission
- Files that change together: this file + `server/utils/events.ts` + `server/utils/generations.ts`
- Tool definitions (bash, python, file access) should be extracted to separate module before extending
### Security: Code Execution Tools Have No Sandboxing
- `bashTool` uses `exec()` and `pythonTool` uses `python3 -c` with user-controlled input — full RCE risk
- Do NOT extend these tools without adding: timeouts, path restrictions, resource limits
- Location: `server/api/topic/[topicId]/chat/index.post.ts` ~lines 261-455
### Crash Risk: `todo()` Function
- `todo()` literally throws `new Error('TODO')` on unhandled token types in chat handler
- Handle gracefully before production deployment
- Location: `server/api/topic/[topicId]/chat/index.post.ts` ~line 256
### Module-Scoped Mutable State Limitations
- `useDialog.ts` has module-scoped `actionCallback` — not SSR-safe, overwritten on concurrent dialogs
- `pendingGenerations` in `server/utils/generations.ts` and in-memory event channels in `server/utils/events.ts` won't work in multi-process deployments
- Fine for single-instance dev, but architectural ceiling for scaling
### Pagination Params Validated But Unused
- `agents.get.ts` validates `page`/`limit` query params but never applies them to the query
- All agent queries return everything regardless of pagination params
## Development Notes ## Development Notes
- **Never run dev server as agent** - just complete tasks and signal done - **Never run dev server as agent** - just complete tasks and signal done
+7 -3
View File
@@ -186,6 +186,10 @@ body {
height: 100%; height: 100%;
} }
a {
color: currentColor;
}
button { button {
color: inherit; color: inherit;
display: flex; display: flex;
@@ -273,15 +277,15 @@ button.accent:hover {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='18' height='18'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='18' height='18'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0'/%3E%3C/svg%3E");
} }
.reasoning-contaizner.middle { .reasoning-container.middle {
mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent) mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent)
} }
.reasoning-contaizner.top { .reasoning-container.top {
mask-image: linear-gradient(#000, transparent, #000 0, #000 12%, #000 88%, transparent) mask-image: linear-gradient(#000, transparent, #000 0, #000 12%, #000 88%, transparent)
} }
.reasoning-contaizner.bottom { .reasoning-container.bottom {
mask-image: linear-gradient(transparent, #000, transparent 0, #000 12%, #000 88%, #000) mask-image: linear-gradient(transparent, #000, transparent 0, #000 12%, #000 88%, #000)
} }
-4
View File
@@ -67,10 +67,6 @@ h6 {
text-wrap: balance; text-wrap: balance;
} }
a {
color: #fff;
}
a:hover { a:hover {
text-decoration: none; text-decoration: none;
} }
+8
View File
@@ -110,6 +110,14 @@ const handleEdit = () => {
class="w-4.5 h-4.5 bg-emerald/10 rounded flex items-center justify-center"> class="w-4.5 h-4.5 bg-emerald/10 rounded flex items-center justify-center">
<span class="i-mynaui-image text-2.5 text-emerald"></span> <span class="i-mynaui-image text-2.5 text-emerald"></span>
</div> </div>
<div v-if="model.inputModalities.includes('video')" title="Video Input"
class="w-4.5 h-4.5 bg-pink/10 rounded flex items-center justify-center">
<span class="i-mynaui-video text-2.5 text-pink"></span>
</div>
<div v-if="model.inputModalities.includes('audio')" title="Audio Input"
class="w-4.5 h-4.5 bg-orange/10 rounded flex items-center justify-center">
<span class="i-mynaui-volume-high text-2.5 text-orange"></span>
</div>
<div v-if="model.capabilities.includes('reasoning')" title="Reasoning" <div v-if="model.capabilities.includes('reasoning')" title="Reasoning"
class="w-4.5 h-4.5 bg-[color-mix(in_srgb,_transparent_90%,_var(--reasoning-accent)_10%)] rounded flex items-center justify-center"> class="w-4.5 h-4.5 bg-[color-mix(in_srgb,_transparent_90%,_var(--reasoning-accent)_10%)] rounded flex items-center justify-center">
<span class="i-mynaui-atom text-2.5 text-[var(--reasoning-accent)]"></span> <span class="i-mynaui-atom text-2.5 text-[var(--reasoning-accent)]"></span>
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'; import { ref, computed, watch, nextTick } from 'vue';
import { useFloating, offset, flip, shift, autoUpdate, size, hide } from '@floating-ui/vue'; import { useFloating, offset, flip, shift, autoUpdate, size, hide } from '@floating-ui/vue';
import type { DialogType } from '~/composables/useDialog';
import type { Model, ModelWithProvider, Provider, ProviderWithModels } from '~/composables/useModels'; import type { Model, ModelWithProvider, Provider, ProviderWithModels } from '~/composables/useModels';
import { sortByReleaseDate } from '~/utils/sort'; import { sortByReleaseDate } from '~/utils/sort';
import RowVirtualizerFixed from './RowVirtualizerFixed.vue'; import RowVirtualizerFixed from './RowVirtualizerFixed.vue';
@@ -211,7 +212,7 @@ onUnmounted(() => {
<template> <template>
<div role="button" @click="toggleDropdown()" ref="dropdownButton" <div role="button" @click="toggleDropdown()" ref="dropdownButton"
class="cursor-pointer flex items-center min-w-0 select-none gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-200" class="cursor-pointer flex items-center w-fit min-w-0 select-none gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-200"
:class="[ :class="[
isOpen isOpen
? 'bg-[var(--color-hover)] text-[var(--text-primary)]' ? 'bg-[var(--color-hover)] text-[var(--text-primary)]'
+3 -1
View File
@@ -322,7 +322,7 @@ export const providerIcons: Record<string, any | null> = {
google: markRaw(LogoGoogle), google: markRaw(LogoGoogle),
openai: markRaw(LogoOpenAI), openai: markRaw(LogoOpenAI),
azure: null, azure: null,
anthropic: null, anthropic: markRaw(LogoAnthropic),
cohere: markRaw(LogoCohere), cohere: markRaw(LogoCohere),
huggingface: null, huggingface: null,
openrouter: markRaw(LogoOpenrouter), openrouter: markRaw(LogoOpenrouter),
@@ -332,6 +332,8 @@ export const providerIcons: Record<string, any | null> = {
mistral: markRaw(LogoMistral), mistral: markRaw(LogoMistral),
vllm: markRaw(LogoVllm), vllm: markRaw(LogoVllm),
closedrouter: markRaw(LogoClosedRouter), closedrouter: markRaw(LogoClosedRouter),
nvidia: markRaw(LogoNvidia),
xiaomi: markRaw(LogoXiaomiMiMo),
}; };
export function getModelConfig(modelId: string) { export function getModelConfig(modelId: string) {
-13
View File
@@ -1,22 +1,9 @@
import { db } from "~~/server/lib/db"; import { db } from "~~/server/lib/db";
import * as z from 'zod';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
const userId = event.context.user!.id as string; const userId = event.context.user!.id as string;
const result = await getValidatedQuery(event, z.object({
page: z.number().optional(),
limit: z.number().optional(),
}).safeParse)
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.issues[0]!.message,
});
}
const agents = await db.query.agents.findMany({ const agents = await db.query.agents.findMany({
where: { where: {
userId, userId,
+1 -17
View File
@@ -1,23 +1,9 @@
import { db } from "~~/server/lib/db"; import { db } from "~~/server/lib/db";
import * as z from 'zod';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
const userId = event.context.user!.id as string; const userId = event.context.user!.id as string;
const result = await getValidatedQuery(event, z.object({
page: z.number().optional(),
limit: z.number().optional(),
}).safeParse)
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.issues[0]!.message,
});
}
const { page, limit } = result.data;
const topics = await db.query.topics.findMany({ const topics = await db.query.topics.findMany({
where: { where: {
@@ -25,9 +11,7 @@ export default defineEventHandler(async (event) => {
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: 'desc',
}, }
limit: page && limit ? limit : undefined,
offset: page && limit ? (page - 1) * limit : undefined,
}); });
return { return {
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
SESSION_NAME="veridian"
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
tmux attach-session -t "$SESSION_NAME"
exit 0
fi
tmux new-session -d -s "$SESSION_NAME" -n "dev"
TERMINAL_SIZE=$(stty size 2>/dev/null)
ROWS=$(echo "$TERMINAL_SIZE" | awk '{print $1}')
COLS=$(echo "$TERMINAL_SIZE" | awk '{print $2}')
# Divide columns by 2 because nearly all terminal fonts are 8x16, so to
# *correctly* maximize the space each pane takes up, we need to divide by 2
if [ "$ROWS" -gt $(("$COLS" / 2)) ]; then
tmux split-window -v -t "$SESSION_NAME"
else
tmux split-window -h -t "$SESSION_NAME"
fi
tmux set -t "$SESSION_NAME" mouse on
tmux bind-key -n Pageup copy-mode -u
tmux send-keys -t "$SESSION_NAME:0.0" "frpc -c frpc.toml" C-m
tmux send-keys -t "$SESSION_NAME:0.1" "bun i && bun dev" C-m
tmux select-pane -t "$SESSION_NAME:0.0"
tmux attach-session -t "$SESSION_NAME"