add toasts, session usage tracking, and copy controls
This commit is contained in:
59
src/lib/notifications.tsx
Normal file
59
src/lib/notifications.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import toast from "solid-toast"
|
||||
|
||||
export type ToastVariant = "info" | "success" | "warning" | "error"
|
||||
|
||||
export type ToastPayload = {
|
||||
title?: string
|
||||
message: string
|
||||
variant: ToastVariant
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const variantAccent: Record<ToastVariant, { badge: string; border: string; text: string }> = {
|
||||
info: {
|
||||
badge: "bg-blue-500",
|
||||
border: "border-blue-500/40",
|
||||
text: "text-blue-100",
|
||||
},
|
||||
success: {
|
||||
badge: "bg-emerald-500",
|
||||
border: "border-emerald-500/40",
|
||||
text: "text-emerald-100",
|
||||
},
|
||||
warning: {
|
||||
badge: "bg-amber-500",
|
||||
border: "border-amber-500/40",
|
||||
text: "text-amber-100",
|
||||
},
|
||||
error: {
|
||||
badge: "bg-rose-500",
|
||||
border: "border-rose-500/40",
|
||||
text: "text-rose-100",
|
||||
},
|
||||
}
|
||||
|
||||
export function showToastNotification(payload: ToastPayload) {
|
||||
const accent = variantAccent[payload.variant]
|
||||
const duration = payload.duration ?? 5000
|
||||
|
||||
toast.custom(
|
||||
() => (
|
||||
<div class={`min-w-[280px] max-w-[360px] rounded-xl border px-4 py-3 shadow-xl bg-surface-secondary ${accent.border}`}>
|
||||
<div class="flex gap-3">
|
||||
<span class={`mt-1 inline-block h-2.5 w-2.5 rounded-full ${accent.badge}`} />
|
||||
<div class="flex-1 text-sm leading-snug">
|
||||
{payload.title && <p class="font-semibold text-primary">{payload.title}</p>}
|
||||
<p class={`text-primary/90 ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration,
|
||||
ariaProps: {
|
||||
role: "status",
|
||||
"aria-live": "polite",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,16 @@ interface SessionUpdateEvent {
|
||||
session: any
|
||||
}
|
||||
|
||||
interface TuiToastEvent {
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<
|
||||
Map<string, "connecting" | "connected" | "disconnected" | "error">
|
||||
>(new Map())
|
||||
@@ -104,6 +114,9 @@ class SSEManager {
|
||||
case "session.error":
|
||||
this.onSessionError?.(instanceId, event)
|
||||
break
|
||||
case "tui.toast.show":
|
||||
this.onTuiToast?.(instanceId, event as TuiToastEvent)
|
||||
break
|
||||
case "session.idle":
|
||||
console.log("[SSE] Session idle")
|
||||
break
|
||||
@@ -147,6 +160,7 @@ class SSEManager {
|
||||
onSessionUpdate?: (instanceId: string, event: SessionUpdateEvent) => void
|
||||
onSessionCompacted?: (instanceId: string, event: any) => void
|
||||
onSessionError?: (instanceId: string, event: any) => void
|
||||
onTuiToast?: (instanceId: string, event: TuiToastEvent) => void
|
||||
|
||||
getStatus(instanceId: string): "connecting" | "connected" | "disconnected" | "error" | null {
|
||||
return connectionStatus().get(instanceId) ?? null
|
||||
|
||||
@@ -40,12 +40,39 @@ export class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private parseConfig(content: string): ConfigData {
|
||||
const trimmed = content.trim()
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch (error) {
|
||||
const firstBrace = trimmed.indexOf("{")
|
||||
const lastBrace = trimmed.lastIndexOf("}")
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
||||
const sanitized = trimmed.slice(firstBrace, lastBrace + 1)
|
||||
|
||||
if (sanitized.length !== trimmed.length) {
|
||||
console.warn("Config file contained trailing data; attempting recovery")
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(sanitized)
|
||||
} catch {
|
||||
// Fall through to rethrow original error below
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Config operations
|
||||
async loadConfig(): Promise<ConfigData> {
|
||||
await this.ensureInitialized()
|
||||
try {
|
||||
const content = await window.electronAPI.readConfigFile()
|
||||
return JSON.parse(content)
|
||||
return this.parseConfig(content)
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config, using defaults:", error)
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user