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:
@@ -151,7 +151,7 @@ await db.insert(agents).values(data).onConflictDoNothing();
|
||||
2. **Session**: Better-auth creates session token → stored in cookie
|
||||
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
|
||||
|
||||
@@ -179,57 +179,6 @@ await db.insert(agents).values(data).onConflictDoNothing();
|
||||
| `useTheme()` | Accent/neutral theme cookies |
|
||||
| `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
|
||||
|
||||
- **Never run dev server as agent** - just complete tasks and signal done
|
||||
|
||||
@@ -186,6 +186,10 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
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");
|
||||
}
|
||||
|
||||
.reasoning-contaizner.middle {
|
||||
.reasoning-container.middle {
|
||||
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)
|
||||
}
|
||||
|
||||
.reasoning-contaizner.bottom {
|
||||
.reasoning-container.bottom {
|
||||
mask-image: linear-gradient(transparent, #000, transparent 0, #000 12%, #000 88%, #000)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,10 +67,6 @@ h6 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,14 @@ const handleEdit = () => {
|
||||
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>
|
||||
</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"
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from '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 { sortByReleaseDate } from '~/utils/sort';
|
||||
import RowVirtualizerFixed from './RowVirtualizerFixed.vue';
|
||||
@@ -211,7 +212,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<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="[
|
||||
isOpen
|
||||
? 'bg-[var(--color-hover)] text-[var(--text-primary)]'
|
||||
|
||||
@@ -322,7 +322,7 @@ export const providerIcons: Record<string, any | null> = {
|
||||
google: markRaw(LogoGoogle),
|
||||
openai: markRaw(LogoOpenAI),
|
||||
azure: null,
|
||||
anthropic: null,
|
||||
anthropic: markRaw(LogoAnthropic),
|
||||
cohere: markRaw(LogoCohere),
|
||||
huggingface: null,
|
||||
openrouter: markRaw(LogoOpenrouter),
|
||||
@@ -332,6 +332,8 @@ export const providerIcons: Record<string, any | null> = {
|
||||
mistral: markRaw(LogoMistral),
|
||||
vllm: markRaw(LogoVllm),
|
||||
closedrouter: markRaw(LogoClosedRouter),
|
||||
nvidia: markRaw(LogoNvidia),
|
||||
xiaomi: markRaw(LogoXiaomiMiMo),
|
||||
};
|
||||
|
||||
export function getModelConfig(modelId: string) {
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import { db } from "~~/server/lib/db";
|
||||
import * as z from 'zod';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
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({
|
||||
where: {
|
||||
userId,
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import { db } from "~~/server/lib/db";
|
||||
import * as z from 'zod';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
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({
|
||||
where: {
|
||||
@@ -25,9 +11,7 @@ export default defineEventHandler(async (event) => {
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
limit: page && limit ? limit : undefined,
|
||||
offset: page && limit ? (page - 1) * limit : undefined,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Executable
+32
@@ -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"
|
||||
Reference in New Issue
Block a user