chore: update Cargo dependencies, configs and API clients

This commit is contained in:
ZhenYi 2026-05-17 16:38:28 +08:00
parent 88a51f45cb
commit 1e94c280e9
10 changed files with 897 additions and 186 deletions

View File

@ -107,3 +107,30 @@ APP_DOMAIN_URL=http://127.0.0.1
# HOOK_POOL_REDIS_BLOCK_TIMEOUT=5
# HOOK_POOL_REDIS_MAX_RETRIES=3
# HOOK_POOL_WORKER_ID=(随机 UUID
# =============================================================================
# Frontend (Vite) — 前端运行环境变量
# =============================================================================
# API 基础 URL为空时使用 Vite dev 代理 /api -> localhost:8080
# VITE_API_BASE_URL=http://localhost:8080
# 前端 WebSocket 连接地址(开发模式通过 Vite 代理)
VITE_WS_URL=ws://localhost:5080
# API URL前端 API 调用,通过 Vite 代理时可为空)
VITE_API_URL=
# WebSocket 连接模式: "raw-ws" | "socketio"
VITE_WS_MODE=raw-ws
# =============================================================================
# Frontend: Grafana Faro (RUM) — 前端性能监控(可选)
# =============================================================================
# VITE_FARO_ENABLED=false
# VITE_FARO_URL=https://faro.example.com/collect
# VITE_FARO_API_KEY=
# VITE_FARO_APP_NAME=GitDataAIWeb
# VITE_FARO_APP_ENV=production
# VITE_FARO_APP_VERSION=0.0.1

2
Cargo.lock generated
View File

@ -3678,6 +3678,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"serde_yaml",
"sha1 0.11.0",
"sha2 0.11.0",
"ssh-key",
@ -9036,6 +9037,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"serde_yaml",
"session",
"sha1 0.11.0",
"sha2 0.11.0",

View File

@ -133,7 +133,7 @@ fs2 = "0.4.3"
image = "0.25.10"
tokio = "1.50.0"
tokio-util = "0.7.18"
tokio-stream = "0.1.18"
tokio-stream = { version = "0.1.18", features = ["sync"] }
url = "2.5.8"
tower = "0.5"
num_cpus = "1.17.0"

View File

@ -7359,6 +7359,158 @@
}
}
},
"/api/projects/{project_name}/message-favorites": {
"get": {
"tags": [
"Project"
],
"operationId": "project_message_favorites",
"parameters": [
{
"name": "project_name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
},
{
"name": "per_page",
"in": "query",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "List current user's project message favorites",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponse_ProjectMessageFavoriteResponse"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not found"
}
}
}
},
"/api/projects/{project_name}/messages/{message_id}/favorite": {
"post": {
"tags": [
"Project"
],
"operationId": "project_message_favorite_add",
"parameters": [
{
"name": "project_name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "message_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Favorite a project message",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponse_ProjectMessageFavoriteItem"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not found"
}
}
},
"delete": {
"tags": [
"Project"
],
"operationId": "project_message_favorite_remove",
"parameters": [
{
"name": "project_name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "message_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Remove a project message favorite"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not found"
}
}
}
},
"/api/projects/{project_name}/repos": {
"get": {
"tags": [
@ -24956,7 +25108,9 @@
"required": [
"uid",
"username",
"has_unread_notifications"
"has_unread_notifications",
"language",
"timezone"
],
"properties": {
"uid": {
@ -24982,6 +25136,12 @@
"type": "integer",
"format": "int64",
"minimum": 0
},
"language": {
"type": "string"
},
"timezone": {
"type": "string"
}
}
}
@ -27442,6 +27602,136 @@
}
}
},
"ApiResponse_ProjectMessageFavoriteItem": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"data": {
"type": "object",
"required": [
"uid",
"project_uid",
"room_id",
"room_name",
"message_id",
"sender_type",
"content",
"content_type",
"send_at",
"favorited_at"
],
"properties": {
"uid": {
"type": "string",
"format": "uuid"
},
"project_uid": {
"type": "string",
"format": "uuid"
},
"room_id": {
"type": "string",
"format": "uuid"
},
"room_name": {
"type": "string"
},
"message_id": {
"type": "string",
"format": "uuid"
},
"sender_id": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"sender_type": {
"type": "string"
},
"display_name": {
"type": [
"string",
"null"
]
},
"content": {
"type": "string"
},
"content_type": {
"type": "string"
},
"send_at": {
"type": "string",
"format": "date-time"
},
"favorited_at": {
"type": "string",
"format": "date-time"
}
}
}
}
},
"ApiResponse_ProjectMessageFavoriteResponse": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"data": {
"type": "object",
"required": [
"page",
"per_page",
"total",
"list"
],
"properties": {
"page": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"per_page": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"total": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"list": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProjectMessageFavoriteItem"
}
}
}
}
}
},
"ApiResponse_ProjectRepoCreateResponse": {
"type": "object",
"required": [
@ -34908,7 +35198,9 @@
"required": [
"uid",
"username",
"has_unread_notifications"
"has_unread_notifications",
"language",
"timezone"
],
"properties": {
"uid": {
@ -34934,6 +35226,12 @@
"type": "integer",
"format": "int64",
"minimum": 0
},
"language": {
"type": "string"
},
"timezone": {
"type": "string"
}
}
},
@ -39247,6 +39545,125 @@
}
}
},
"ProjectMessageFavoriteItem": {
"type": "object",
"required": [
"uid",
"project_uid",
"room_id",
"room_name",
"message_id",
"sender_type",
"content",
"content_type",
"send_at",
"favorited_at"
],
"properties": {
"uid": {
"type": "string",
"format": "uuid"
},
"project_uid": {
"type": "string",
"format": "uuid"
},
"room_id": {
"type": "string",
"format": "uuid"
},
"room_name": {
"type": "string"
},
"message_id": {
"type": "string",
"format": "uuid"
},
"sender_id": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"sender_type": {
"type": "string"
},
"display_name": {
"type": [
"string",
"null"
]
},
"content": {
"type": "string"
},
"content_type": {
"type": "string"
},
"send_at": {
"type": "string",
"format": "date-time"
},
"favorited_at": {
"type": "string",
"format": "date-time"
}
}
},
"ProjectMessageFavoriteQuery": {
"type": "object",
"properties": {
"page": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
},
"per_page": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
},
"ProjectMessageFavoriteResponse": {
"type": "object",
"required": [
"page",
"per_page",
"total",
"list"
],
"properties": {
"page": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"per_page": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"total": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"list": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProjectMessageFavoriteItem"
}
}
}
},
"ProjectModel": {
"type": "object",
"required": [
@ -43614,7 +44031,7 @@
"type": "object",
"required": [
"uid",
"project_uid",
"user_uid",
"amount",
"currency",
"reason",
@ -43626,16 +44043,16 @@
"format": "uuid"
},
"project_uid": {
"type": "string",
"format": "uuid"
},
"user_uid": {
"type": [
"string",
"null"
],
"format": "uuid"
},
"user_uid": {
"type": "string",
"format": "uuid"
},
"amount": {
"type": "number",
"format": "double"
@ -44410,4 +44827,4 @@
"description": "AI conversation and messaging"
}
]
}
}

View File

@ -157,9 +157,59 @@ export async function shareConversation(conversationId: string): Promise<{ share
}
export interface StreamChunk {
type: "token" | "thinking" | "tool_call" | "tool_result" | "done" | "error" | "title" | "billing_error";
type: "token" | "thinking" | "tool_call" | "tool_result" | "done" | "stopped" | "error" | "title" | "billing_error";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
children_id?: string;
}
async function* streamConversationWatch(conversationId: string): AsyncGenerator<StreamChunk> {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || ""}/api/ai/conversations/${conversationId}/watch`, {
method: "GET",
credentials: "include",
});
if (!response.ok) throw new Error(`Watch stream request failed: ${response.status}`);
if (!response.body) throw new Error("No response body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data: ")) continue;
try {
const parsed = JSON.parse(trimmed.slice(6));
const eventType = parsed.type || parsed.event;
const payload = parsed.data;
if (eventType === "message" && payload?.role === "assistant" && payload?.content) {
yield { type: "done", data: "ok" };
return;
}
const chunkType = payload?.chunk_type || eventType;
if (chunkType === "token" || chunkType === "thinking" || chunkType === "tool_call" || chunkType === "tool_result" || chunkType === "error") {
yield {
type: chunkType,
data: payload?.content ?? payload?.error ?? payload,
};
}
} catch {
// Ignore comments and malformed events.
}
}
}
}
export async function* streamChat(conversationId: string, messageId: string): AsyncGenerator<StreamChunk> {
@ -197,8 +247,12 @@ export async function* streamChat(conversationId: string, messageId: string): As
const parsed = JSON.parse(jsonStr);
// Normalize backend SSE format: {event: "token", data: "..."} → {type: "token", data: "..."}
const eventType = parsed.type || parsed.event;
if (eventType === "recovery") {
yield* streamConversationWatch(conversationId);
return;
}
if (eventType === "token" || eventType === "thinking" || eventType === "tool_call" || eventType === "tool_result" || eventType === "done" || eventType === "error" || eventType === "title" || eventType === "billing_error") {
yield { type: eventType, data: parsed.data };
yield { type: eventType, data: parsed.data, children_id: parsed.children_id };
}
} catch {
// Ignore unparseable lines
@ -206,3 +260,64 @@ export async function* streamChat(conversationId: string, messageId: string): As
}
}
}
/** Stream sub-agent output via SSE.
* Connects to `GET /api/ai/subagent/{conversationId}/{childrenId}/stream`
* and yields StreamChunk events (token, thinking, done, error).
*/
export async function* streamSubAgent(conversationId: string, childrenId: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || ""}/api/ai/subagent/${conversationId}/${childrenId}/stream`, {
method: "GET",
credentials: "include",
signal,
});
if (!response.ok) throw new Error(`Sub-agent stream request failed: ${response.status}`);
if (!response.body) throw new Error("No response body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data: ")) continue;
const jsonStr = trimmed.slice(6);
try {
const parsed = JSON.parse(jsonStr);
const eventType = parsed.type || parsed.event;
if (eventType === "token" || eventType === "thinking" || eventType === "done" || eventType === "stopped" || eventType === "error") {
const data = parsed.data && typeof parsed.data === "object" && "content" in parsed.data
? parsed.data.content
: parsed.data;
yield { type: eventType, data };
} else if (eventType === "tool_call" || eventType === "tool_result") {
yield {
type: eventType,
data: parsed.data?.metadata || parsed.data,
children_id: parsed.data?.children_id || parsed.children_id
};
}
} catch {
// Ignore unparseable lines
}
}
}
}
export async function stopSubAgent(conversationId: string, childrenId: string): Promise<void> {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || ""}/api/ai/subagent/${conversationId}/${childrenId}/stop`, {
method: "POST",
credentials: "include",
});
if (!response.ok) throw new Error(`Failed to stop sub-agent: ${response.status}`);
}

View File

@ -15,6 +15,7 @@ const axiosInstance = axios.create({
const api = getApi(axiosInstance);
// Export all API functions
export const {
// Auth
api2faDisable,
@ -67,6 +68,9 @@ export const {
projectBilling,
projectBillingHistory,
projectBillingErrors,
projectMessageFavorites,
projectMessageFavoriteAdd,
projectMessageFavoriteRemove,
projectCreateLabel,
projectUpdateLabel,
projectDeleteLabel,
@ -163,6 +167,8 @@ export const {
modelList,
categoryList,
categoryCreate,
categoryDelete,
categoryUpdate,
participantList,
pinAdd,
pinRemove,
@ -248,6 +254,7 @@ export const {
aiMessageCreate,
aiMessageGet,
aiMessageChildren,
// @ts-ignore
aiMessageFork,
aiMessageResend,
aiMessageStop,
@ -300,6 +307,7 @@ export const {
// Search
search,
searchMessages,
} = api;
// Manual avatar upload (not in generated API)

View File

@ -0,0 +1,111 @@
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Thin.woff2") format("woff2");
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
font-weight: 100;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
font-weight: 200;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Light.woff2") format("woff2");
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-LightItalic.woff2") format("woff2");
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
font-weight: 800;
font-style: italic;
}

View File

@ -8,6 +8,12 @@
@plugin "@tailwindcss/typography";
body {
font-family: Inter,
system-ui,
sans-serif;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
@ -129,192 +135,210 @@
*/
:root {
/* Surfaces (layer hierarchy, 3 layers only) */
--surface-rail: oklch(0.98 0 0);
--surface-sidebar: oklch(0.97 0 0);
--surface-ground: oklch(1 0 0);
--surface-rail: oklch(0.98 0 0);
--surface-sidebar: oklch(0.97 0 0);
--surface-ground: oklch(1 0 0);
--surface-elevated: oklch(1 0 0);
--surface-overlay: oklch(1 0 0 / 90%);
--surface-overlay: oklch(1 0 0 / 90%);
/* Borders */
--border-subtle: oklch(0.90 0 0 / 40%);
--border-default: oklch(0.88 0 0);
--border-strong: oklch(0.80 0 0);
--border-subtle: oklch(0.90 0 0 / 40%);
--border-default: oklch(0.88 0 0);
--border-strong: oklch(0.80 0 0);
/* Text */
--text-primary: oklch(0.13 0 0);
--text-secondary: oklch(0.40 0 0);
--text-muted: oklch(0.55 0 0);
--text-inverse: oklch(0.985 0 0);
--text-tertiary: oklch(0.70 0 0);
--text-primary: oklch(0.13 0 0);
--text-secondary: oklch(0.40 0 0);
--text-muted: oklch(0.55 0 0);
--text-inverse: oklch(0.985 0 0);
--text-tertiary: oklch(0.70 0 0);
/* Brand accent — Discord blurple as reference */
--accent: oklch(0.25 0 0);
--accent-hover: oklch(0.15 0 0);
--accent-fg: oklch(0.985 0 0);
--accent-muted: oklch(0.25 0 0 / 15%);
--accent-bg: oklch(0.25 0 0 / 8%);
--accent-rgb: 88, 101, 242;
--accent: oklch(0.25 0 0);
--accent-hover: oklch(0.15 0 0);
--accent-fg: oklch(0.985 0 0);
--accent-muted: oklch(0.25 0 0 / 15%);
--accent-bg: oklch(0.25 0 0 / 8%);
--accent-rgb: 88, 101, 242;
/* Status */
--status-online: oklch(0.55 0 0);
--status-idle: oklch(0.60 0 0);
--status-dnd: oklch(0.50 0 0);
--status-offline: oklch(0.60 0 0);
--status-online: oklch(0.55 0 0);
--status-idle: oklch(0.60 0 0);
--status-dnd: oklch(0.50 0 0);
--status-offline: oklch(0.60 0 0);
/* Semantic */
--success: oklch(0.50 0 0);
--success-alpha10: oklch(0.50 0 0 / 10%);
--warning: oklch(0.60 0 0);
--warning-alpha10: oklch(0.60 0 0 / 10%);
--destructive: oklch(0.45 0 0);
--success: oklch(0.50 0 0);
--success-alpha10: oklch(0.50 0 0 / 10%);
--warning: oklch(0.60 0 0);
--warning-alpha10: oklch(0.60 0 0 / 10%);
--destructive: oklch(0.45 0 0);
--destructive-alpha10: oklch(0.45 0 0 / 10%);
--info: oklch(0.50 0 0);
--info: oklch(0.50 0 0);
/* Role colors (from Discord) */
--role-red: oklch(0.50 0 0);
--role-orange: oklch(0.55 0 0);
--role-yellow: oklch(0.60 0 0);
--role-green: oklch(0.50 0 0);
--role-blue: oklch(0.55 0 0);
--role-purple: oklch(0.55 0 0);
--role-pink: oklch(0.55 0 0);
--role-gray: oklch(0.50 0 0);
--role-red: oklch(0.50 0 0);
--role-orange: oklch(0.55 0 0);
--role-yellow: oklch(0.60 0 0);
--role-green: oklch(0.50 0 0);
--role-blue: oklch(0.55 0 0);
--role-purple: oklch(0.55 0 0);
--role-pink: oklch(0.55 0 0);
--role-gray: oklch(0.50 0 0);
/* Interactive */
--interactive: oklch(0.97 0 0);
--interactive: oklch(0.97 0 0);
--interactive-hover: oklch(0.92 0 0);
--interactive-active: oklch(0.88 0 0);
/* Hover */
--hover-bg: oklch(0.95 0 0 / 70%);
--hover-bg-strong: oklch(0.90 0 0);
--hover-bg: oklch(0.95 0 0 / 70%);
--hover-bg-strong: oklch(0.90 0 0);
/* Input */
--input-bg: oklch(0.98 0 0);
--input-bg: oklch(0.98 0 0);
--input-placeholder: oklch(0.50 0 0);
--input-ring: oklch(0.30 0 0);
--input-ring: oklch(0.30 0 0);
/* Heatmap (contribution graph) */
--heatmap-0: oklch(0.93 0 0);
--heatmap-1: oklch(0.75 0.10 155);
--heatmap-2: oklch(0.62 0.15 155);
--heatmap-3: oklch(0.50 0.15 155);
--heatmap-4: oklch(0.38 0.15 155);
--heatmap-0: oklch(0.93 0 0);
--heatmap-1: oklch(0.75 0.10 155);
--heatmap-2: oklch(0.62 0.15 155);
--heatmap-3: oklch(0.50 0.15 155);
--heatmap-4: oklch(0.38 0.15 155);
}
.dark {
--surface-rail: oklch(0.12 0 0);
--surface-sidebar: oklch(0.15 0 0);
--surface-ground: oklch(0.13 0 0);
--surface-rail: oklch(0.12 0 0);
--surface-sidebar: oklch(0.15 0 0);
--surface-ground: oklch(0.13 0 0);
--surface-elevated: oklch(0.18 0 0);
--surface-overlay: oklch(0.10 0 0 / 95%);
--surface-overlay: oklch(0.10 0 0 / 95%);
--border-subtle: oklch(0.30 0 0 / 30%);
--border-default: oklch(0.35 0 0);
--border-strong: oklch(0.50 0 0);
--border-subtle: oklch(0.30 0 0 / 30%);
--border-default: oklch(0.35 0 0);
--border-strong: oklch(0.50 0 0);
--text-primary: oklch(0.97 0 0);
--text-secondary: oklch(0.80 0 0);
--text-muted: oklch(0.65 0 0);
--text-inverse: oklch(0.13 0 0);
--text-tertiary: oklch(0.55 0 0);
--text-primary: oklch(0.97 0 0);
--text-secondary: oklch(0.80 0 0);
--text-muted: oklch(0.65 0 0);
--text-inverse: oklch(0.13 0 0);
--text-tertiary: oklch(0.55 0 0);
--accent: oklch(0.70 0.15 264);
--accent-hover: oklch(0.78 0.15 264);
--accent-fg: oklch(0.10 0 0);
--accent-muted: oklch(0.70 0.15 264 / 20%);
--accent-bg: oklch(0.70 0.15 264 / 12%);
--accent-rgb: 88, 101, 242;
--accent: oklch(0.70 0.15 264);
--accent-hover: oklch(0.78 0.15 264);
--accent-fg: oklch(0.10 0 0);
--accent-muted: oklch(0.70 0.15 264 / 20%);
--accent-bg: oklch(0.70 0.15 264 / 12%);
--accent-rgb: 88, 101, 242;
--status-online: oklch(0.72 0.17 155);
--status-idle: oklch(0.78 0.15 80);
--status-dnd: oklch(0.65 0.20 25);
--status-offline: oklch(0.55 0 0);
--status-online: oklch(0.72 0.17 155);
--status-idle: oklch(0.78 0.15 80);
--status-dnd: oklch(0.65 0.20 25);
--status-offline: oklch(0.55 0 0);
--success: oklch(0.72 0.17 155);
--success-alpha10: oklch(0.72 0.17 155 / 10%);
--warning: oklch(0.78 0.15 90);
--warning-alpha10: oklch(0.78 0.15 90 / 10%);
--destructive: oklch(0.70 0.20 25);
--success: oklch(0.72 0.17 155);
--success-alpha10: oklch(0.72 0.17 155 / 10%);
--warning: oklch(0.78 0.15 90);
--warning-alpha10: oklch(0.78 0.15 90 / 10%);
--destructive: oklch(0.70 0.20 25);
--destructive-alpha10: oklch(0.70 0.20 25 / 10%);
--info: oklch(0.70 0.15 250);
--info: oklch(0.70 0.15 250);
--role-red: oklch(0.65 0.20 20);
--role-orange: oklch(0.72 0.18 50);
--role-yellow: oklch(0.78 0.16 85);
--role-green: oklch(0.70 0.17 155);
--role-blue: oklch(0.70 0.20 250);
--role-purple: oklch(0.65 0.20 290);
--role-pink: oklch(0.65 0.20 340);
--role-gray: oklch(0.58 0 0);
--role-red: oklch(0.65 0.20 20);
--role-orange: oklch(0.72 0.18 50);
--role-yellow: oklch(0.78 0.16 85);
--role-green: oklch(0.70 0.17 155);
--role-blue: oklch(0.70 0.20 250);
--role-purple: oklch(0.65 0.20 290);
--role-pink: oklch(0.65 0.20 340);
--role-gray: oklch(0.58 0 0);
--interactive: oklch(0.18 0 0);
--interactive: oklch(0.18 0 0);
--interactive-hover: oklch(0.25 0 0);
--interactive-active: oklch(0.32 0 0);
--hover-bg: oklch(0.22 0 0 / 60%);
--hover-bg-strong: oklch(0.28 0 0);
--hover-bg: oklch(0.22 0 0 / 60%);
--hover-bg-strong: oklch(0.28 0 0);
--input-bg: oklch(0.15 0 0);
--input-bg: oklch(0.15 0 0);
--input-placeholder: oklch(0.55 0 0);
--input-ring: oklch(0.70 0.15 264);
--input-ring: oklch(0.70 0.15 264);
/* Heatmap (contribution graph) */
--heatmap-0: oklch(0.20 0 0);
--heatmap-1: oklch(0.30 0.08 155);
--heatmap-2: oklch(0.45 0.12 155);
--heatmap-3: oklch(0.60 0.15 155);
--heatmap-4: oklch(0.75 0.15 155);
--heatmap-0: oklch(0.20 0 0);
--heatmap-1: oklch(0.30 0.08 155);
--heatmap-2: oklch(0.45 0.12 155);
--heatmap-3: oklch(0.60 0.15 155);
--heatmap-4: oklch(0.75 0.15 155);
}
@layer base {
* {
@apply border-border outline-ring/50;
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
html {
@apply font-sans;
}
}
/* ─── Settings Modal open/close animation ─── */
.settings-dialog[data-state="open"] {
animation: settings-modal-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
animation: settings-modal-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
}
.settings-dialog[data-state="closed"] {
animation: settings-modal-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
animation: settings-modal-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes settings-modal-open {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes settings-modal-close {
from { opacity: 1; }
to { opacity: 0; }
from {
opacity: 1;
}
to {
opacity: 0;
}
}
[data-slot="dialog-overlay"][data-state="open"] {
animation: settings-overlay-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
animation: settings-overlay-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
}
[data-slot="dialog-overlay"][data-state="closed"] {
animation: settings-overlay-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
animation: settings-overlay-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes settings-overlay-open {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes settings-overlay-close {
from { opacity: 1; }
to { opacity: 0; }
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/*
@ -323,75 +347,81 @@
dark mode text never falls back to defaults
*/
:root .prose {
--tw-prose-body: var(--text-primary);
--tw-prose-headings: var(--text-primary);
--tw-prose-lead: var(--text-secondary);
--tw-prose-links: var(--accent);
--tw-prose-bold: var(--text-primary);
--tw-prose-counters: var(--text-secondary);
--tw-prose-bullets: var(--text-muted);
--tw-prose-hr: var(--border-default);
--tw-prose-quotes: var(--text-secondary);
--tw-prose-quote-borders: var(--border-default);
--tw-prose-captions: var(--text-muted);
--tw-prose-kbd: var(--text-primary);
--tw-prose-kbd-shadows: oklch(0.13 0 0 / 10%);
--tw-prose-code: var(--accent);
--tw-prose-pre-code: var(--text-primary);
--tw-prose-pre-bg: var(--surface-elevated);
--tw-prose-th-borders: var(--border-default);
--tw-prose-td-borders: var(--border-subtle);
--tw-prose-body: var(--text-primary);
--tw-prose-headings: var(--text-primary);
--tw-prose-lead: var(--text-secondary);
--tw-prose-links: var(--accent);
--tw-prose-bold: var(--text-primary);
--tw-prose-counters: var(--text-secondary);
--tw-prose-bullets: var(--text-muted);
--tw-prose-hr: var(--border-default);
--tw-prose-quotes: var(--text-secondary);
--tw-prose-quote-borders: var(--border-default);
--tw-prose-captions: var(--text-muted);
--tw-prose-kbd: var(--text-primary);
--tw-prose-kbd-shadows: oklch(0.13 0 0 / 10%);
--tw-prose-code: var(--accent);
--tw-prose-pre-code: var(--text-primary);
--tw-prose-pre-bg: var(--surface-elevated);
--tw-prose-th-borders: var(--border-default);
--tw-prose-td-borders: var(--border-subtle);
}
.dark .prose {
--tw-prose-body: var(--text-primary);
--tw-prose-headings: var(--text-primary);
--tw-prose-lead: var(--text-secondary);
--tw-prose-links: var(--accent);
--tw-prose-bold: var(--text-primary);
--tw-prose-counters: var(--text-secondary);
--tw-prose-bullets: var(--text-muted);
--tw-prose-hr: var(--border-default);
--tw-prose-quotes: var(--text-secondary);
--tw-prose-quote-borders: var(--border-default);
--tw-prose-captions: var(--text-muted);
--tw-prose-kbd: var(--text-primary);
--tw-prose-kbd-shadows: oklch(0.97 0 0 / 10%);
--tw-prose-code: var(--accent);
--tw-prose-pre-code: var(--text-primary);
--tw-prose-pre-bg: var(--surface-elevated);
--tw-prose-th-borders: var(--border-default);
--tw-prose-td-borders: var(--border-subtle);
--tw-prose-body: var(--text-primary);
--tw-prose-headings: var(--text-primary);
--tw-prose-lead: var(--text-secondary);
--tw-prose-links: var(--accent);
--tw-prose-bold: var(--text-primary);
--tw-prose-counters: var(--text-secondary);
--tw-prose-bullets: var(--text-muted);
--tw-prose-hr: var(--border-default);
--tw-prose-quotes: var(--text-secondary);
--tw-prose-quote-borders: var(--border-default);
--tw-prose-captions: var(--text-muted);
--tw-prose-kbd: var(--text-primary);
--tw-prose-kbd-shadows: oklch(0.97 0 0 / 10%);
--tw-prose-code: var(--accent);
--tw-prose-pre-code: var(--text-primary);
--tw-prose-pre-bg: var(--surface-elevated);
--tw-prose-th-borders: var(--border-default);
--tw-prose-td-borders: var(--border-subtle);
}
.app-scrollbar {
scrollbar-width: thin;
scrollbar-color: color-mix(in oklch, var(--text-muted) 34%, transparent) transparent;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: color-mix(in oklch, var(--text-muted) 34%, transparent) transparent;
scrollbar-gutter: stable;
}
.app-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
width: 10px;
height: 10px;
}
.app-scrollbar::-webkit-scrollbar-track {
background: transparent;
background: transparent;
}
.app-scrollbar::-webkit-scrollbar-thumb {
background: color-mix(in oklch, var(--text-muted) 28%, transparent);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
background: color-mix(in oklch, var(--text-muted) 28%, transparent);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
.app-scrollbar:hover::-webkit-scrollbar-thumb {
background: color-mix(in oklch, var(--text-secondary) 42%, transparent);
border: 2px solid transparent;
background-clip: padding-box;
background: color-mix(in oklch, var(--text-secondary) 42%, transparent);
border: 2px solid transparent;
background-clip: padding-box;
}
.app-scrollbar[data-scrollbar="room"]::-webkit-scrollbar {
width: 12px;
width: 12px;
}
.pre_nobackground {
pre {
background-color: transparent;
}
}

View File

@ -1,41 +1,41 @@
import {StrictMode} from "react"
import {createRoot} from "react-dom/client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import {QueryClient, QueryClientProvider} from "@tanstack/react-query"
import "@/fonts.css"
import "@/index.css"
import {ThemeProvider} from "@/components/theme-provider.tsx"
import { performMaintenance } from "@/lib/db/maintenance";
import { applyThemePreset } from "@/components/theme/ThemePresetSelector";
import {Toaster} from "@/components/ui/sonner"
import {performMaintenance} from "@/lib/db/maintenance";
import {applyThemePreset} from "@/components/theme/ThemePresetSelector";
import App from "@/App.tsx";
import { initRum } from "@/rum-core";
import { RumUserContext } from "@/rum";
import {initRum} from "@/rum-core";
import {RumUserContext} from "@/rum";
initRum();
// Apply saved theme preset on startup
const PRESET_KEY = "app-theme-preset";
const savedPreset = localStorage.getItem(PRESET_KEY) || "soft-mono";
applyThemePreset(savedPreset);
// Trigger background maintenance on startup
performMaintenance().catch(console.error);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 1,
},
},
},
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RumUserContext />
<RumUserContext/>
<ThemeProvider>
<App/>
<Toaster/>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>
)
)

View File

@ -15,6 +15,7 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"resolveJsonModule": true,
/* Linting */
"strict": true,