mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 01:06:16 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad295d58fa |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
"@dnd-kit/dom": "0.5.0",
|
||||
@@ -93,7 +93,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -155,7 +155,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -191,7 +191,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -218,7 +218,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -240,7 +240,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -264,7 +264,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -284,7 +284,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -378,7 +378,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -432,7 +432,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -446,7 +446,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -458,7 +458,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -490,7 +490,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -506,7 +506,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -537,7 +537,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -556,7 +556,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -687,9 +687,10 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
@@ -752,6 +753,7 @@
|
||||
"dependencies": {
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -763,7 +765,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -778,7 +780,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -793,7 +795,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -837,7 +839,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -850,7 +852,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
@@ -883,7 +885,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -902,7 +904,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -943,7 +945,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -970,7 +972,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1021,7 +1023,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
+16
-61
@@ -9,7 +9,7 @@ import { Font } from "@opencode-ai/ui/font"
|
||||
import { Splash } from "@opencode-ai/ui/logo"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { type BaseRouterProps, Navigate, Route, Router, useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
@@ -32,7 +32,7 @@ import { CommandProvider, useCommand, type CommandOption } from "@/context/comma
|
||||
import { CommentsProvider } from "@/context/comments"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||
import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
|
||||
import { ServerSyncProvider } from "@/context/server-sync"
|
||||
import { GlobalProvider, useGlobal } from "@/context/global"
|
||||
import { HighlightsProvider } from "@/context/highlights"
|
||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||
@@ -52,10 +52,9 @@ import LegacyLayout from "@/pages/layout"
|
||||
import NewLayout from "@/pages/layout-new"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { useCheckServerHealth } from "./utils/server-health"
|
||||
import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||
import { createSessionLineage } from "@/pages/session/session-lineage"
|
||||
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||
|
||||
import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
|
||||
import { SessionPage, TargetSessionRouteContent } from "@/pages/session"
|
||||
import { NewHome, LegacyHome } from "@/pages/home"
|
||||
|
||||
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||
@@ -89,14 +88,10 @@ const SessionRoute = () => {
|
||||
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
||||
})
|
||||
|
||||
return (
|
||||
<SessionRouteErrorBoundary sessionID={params.id}>
|
||||
<SessionPage />
|
||||
</SessionRouteErrorBoundary>
|
||||
)
|
||||
return <SessionPage />
|
||||
}
|
||||
|
||||
function TargetServerRoute(props: ParentProps) {
|
||||
const TargetSessionRoute = () => {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const global = useGlobal()
|
||||
const conn = createMemo(() => {
|
||||
@@ -110,47 +105,14 @@ function TargetServerRoute(props: ParentProps) {
|
||||
// re-resolves reactively instead); both rely on this key for server changes.
|
||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||
<ServerSDKProvider server={conn}>
|
||||
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||
<ServerSyncProvider server={conn}>
|
||||
<TargetSessionRouteContent />
|
||||
</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetSessionRoute = () => (
|
||||
<TargetServerRoute>
|
||||
<TargetSessionRouteContent />
|
||||
</TargetServerRoute>
|
||||
)
|
||||
|
||||
function LegacyTargetSessionRoute() {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
return (
|
||||
<TargetServerRoute>
|
||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
|
||||
<LegacyTargetSessionRedirect />
|
||||
</SessionRouteErrorBoundary>
|
||||
</TargetServerRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyTargetSessionRedirect() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const sync = useServerSync()
|
||||
const current = createSessionLineage(
|
||||
() => params.id,
|
||||
() => sync().session.lineage,
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const directory = current()?.session.directory
|
||||
if (!directory) return
|
||||
navigate(legacySessionHref(directory, params.id), { replace: true })
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
||||
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
|
||||
// Notification/Models + the visual Layout) for that server.
|
||||
@@ -224,7 +186,7 @@ declare global {
|
||||
deepLinks?: string[]
|
||||
}
|
||||
api?: {
|
||||
setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise<void>
|
||||
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
|
||||
exportDebugLogs?: () => Promise<string>
|
||||
}
|
||||
}
|
||||
@@ -358,8 +320,8 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<ThemeProvider
|
||||
onThemeApplied={(_, mode, scheme) => {
|
||||
void window.api?.setTitlebar?.({ mode, scheme })
|
||||
onThemeApplied={(_, mode) => {
|
||||
void window.api?.setTitlebar?.({ mode })
|
||||
}}
|
||||
>
|
||||
<LanguageProvider locale={props.locale}>
|
||||
@@ -580,14 +542,7 @@ function Routes(props: { serverScoped?: JSX.Element }) {
|
||||
<LegacyServerLayout serverScoped={props.serverScoped}>{routeProps.children}</LegacyServerLayout>
|
||||
)}
|
||||
>
|
||||
<Show when={!settings.general.newLayoutDesigns()}>
|
||||
{
|
||||
<>
|
||||
<Route path="/" component={LegacyHome} />
|
||||
<Route path="/server/:serverKey/session/:id" component={LegacyTargetSessionRoute} />
|
||||
</>
|
||||
}
|
||||
</Show>
|
||||
<Show when={!settings.general.newLayoutDesigns()}>{<Route path="/" component={LegacyHome} />}</Show>
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
@@ -595,15 +550,15 @@ function Routes(props: { serverScoped?: JSX.Element }) {
|
||||
</Route>
|
||||
<Show when={settings.general.newLayoutDesigns()}>
|
||||
<Route path="/" component={NewHome} />
|
||||
<Route path="/:dir/session/:id" component={NewLayoutLegacySessionRedirect} />
|
||||
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
||||
<Route path="/:dir/session/:id" component={LegacyTargetSessionRoute} />
|
||||
</Show>
|
||||
<Route path="/new-session" component={DraftRoute} />
|
||||
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function NewLayoutLegacySessionRedirect() {
|
||||
function LegacyTargetSessionRoute() {
|
||||
const server = useServer()
|
||||
const tabs = useTabs()
|
||||
const params = useParams<{ id: string }>()
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createMemo, onCleanup } from "solid-js"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
|
||||
export type CommandPaletteEntry = {
|
||||
id: string
|
||||
type: "command" | "file" | "session"
|
||||
title: string
|
||||
description?: string
|
||||
keybind?: string
|
||||
category: string
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
directory?: string
|
||||
sessionID?: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}
|
||||
|
||||
const ENTRY_LIMIT = 5
|
||||
const COMMON_COMMAND_IDS = [
|
||||
"session.new",
|
||||
"workspace.new",
|
||||
"session.previous",
|
||||
"session.next",
|
||||
"terminal.toggle",
|
||||
"review.toggle",
|
||||
] as const
|
||||
|
||||
export function uniqueCommandPaletteEntries(items: CommandPaletteEntry[]) {
|
||||
const seen = new Set<string>()
|
||||
return items.filter((item) => {
|
||||
if (seen.has(item.id)) return false
|
||||
seen.add(item.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function createCommandPaletteFileEntry(path: string, category: string): CommandPaletteEntry {
|
||||
return {
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => void) {
|
||||
const file = useFile()
|
||||
const layout = useLayout()
|
||||
const { tabs, view } = useSessionLayout()
|
||||
|
||||
return (path: string) => {
|
||||
const value = file.tab(path)
|
||||
void tabs().open(value)
|
||||
void file.load(path)
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("all")
|
||||
onOpenFile?.(path)
|
||||
tabs().setActive(value)
|
||||
}
|
||||
}
|
||||
|
||||
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const serverSDK = useServerSDK()()
|
||||
const serverSync = useServerSync()
|
||||
const { params, tabs } = useSessionLayout()
|
||||
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const filesOnly = () => props.filesOnly?.() ?? false
|
||||
|
||||
const allowedCommands = createMemo(() => {
|
||||
if (filesOnly()) return []
|
||||
return command.options.filter(
|
||||
(option) =>
|
||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
)
|
||||
})
|
||||
const commandEntries = createMemo(() => {
|
||||
const category = language.t("palette.group.commands")
|
||||
return allowedCommands().map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
const preferredCommandEntries = createMemo(() => {
|
||||
const all = allowedCommands()
|
||||
const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
const category = language.t("palette.group.commands")
|
||||
return sorted.map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
|
||||
const tabState = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||
})
|
||||
const recentFileEntries = createMemo(() => {
|
||||
const all = tabState.openedTabs()
|
||||
const active = tabState.activeFileTab()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const category = language.t("palette.group.files")
|
||||
return order
|
||||
.map((item) => file.pathFromTab(item))
|
||||
.filter((path): path is string => {
|
||||
if (!path || seen.has(path)) return false
|
||||
seen.add(path)
|
||||
return true
|
||||
})
|
||||
.slice(0, ENTRY_LIMIT)
|
||||
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||
})
|
||||
const rootFileEntries = createMemo(() => {
|
||||
const category = language.t("palette.group.files")
|
||||
return file.tree
|
||||
.children("")
|
||||
.filter((node) => node.type === "file")
|
||||
.map((node) => node.path)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.slice(0, ENTRY_LIMIT)
|
||||
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||
})
|
||||
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return undefined
|
||||
return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
||||
})
|
||||
const workspaces = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
const current = project()
|
||||
if (!current) return directory ? [directory] : []
|
||||
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||
return dirs
|
||||
})
|
||||
const homedir = createMemo(() => serverSync().data.path.home)
|
||||
const sessions = createSessionEntries({
|
||||
workspaces,
|
||||
label: (directory) => {
|
||||
const current = project()
|
||||
const kind =
|
||||
current && directory === current.worktree
|
||||
? language.t("workspace.type.local")
|
||||
: language.t("workspace.type.sandbox")
|
||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
||||
const home = homedir()
|
||||
const path = home ? directory.replace(home, "~") : directory
|
||||
const name = store.vcs?.branch ?? getFilename(directory)
|
||||
return `${kind} : ${name || path}`
|
||||
},
|
||||
load: (directory) => serverSDK.client.session.list({ directory, roots: true }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
|
||||
const highlight = (item: CommandPaletteEntry | undefined) => {
|
||||
state.cleanup?.()
|
||||
state.cleanup = undefined
|
||||
if (item?.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const select = (item: CommandPaletteEntry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
if (item.type === "session") {
|
||||
if (!item.directory || !item.sessionID) return
|
||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||
return
|
||||
}
|
||||
if (!item.path) return
|
||||
openFile(item.path)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
return {
|
||||
language,
|
||||
file,
|
||||
commandEntries,
|
||||
preferredCommandEntries,
|
||||
recentFileEntries,
|
||||
rootFileEntries,
|
||||
sessions,
|
||||
highlight,
|
||||
select,
|
||||
close: () => dialog.close(),
|
||||
}
|
||||
}
|
||||
|
||||
function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
||||
return {
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category,
|
||||
option,
|
||||
}
|
||||
}
|
||||
|
||||
function createSessionEntries(props: {
|
||||
workspaces: () => string[]
|
||||
label: (directory: string) => string
|
||||
load: (directory: string) => ReturnType<ServerSDK["client"]["session"]["list"]>
|
||||
untitled: () => string
|
||||
category: () => string
|
||||
}) {
|
||||
const state: {
|
||||
token: number
|
||||
inflight: Promise<CommandPaletteEntry[]> | undefined
|
||||
cached: CommandPaletteEntry[] | undefined
|
||||
} = { token: 0, inflight: undefined, cached: undefined }
|
||||
|
||||
return (text: string) => {
|
||||
if (!text.trim()) {
|
||||
state.token += 1
|
||||
state.inflight = undefined
|
||||
state.cached = undefined
|
||||
return [] as CommandPaletteEntry[]
|
||||
}
|
||||
if (state.cached) return state.cached
|
||||
if (state.inflight) return state.inflight
|
||||
|
||||
const current = state.token
|
||||
const dirs = props.workspaces()
|
||||
if (dirs.length === 0) return [] as CommandPaletteEntry[]
|
||||
|
||||
state.inflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = props.label(directory)
|
||||
return props
|
||||
.load(directory)
|
||||
.then((result) =>
|
||||
(result.data ?? [])
|
||||
.filter((session) => !!session?.id)
|
||||
.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title ?? props.untitled(),
|
||||
description,
|
||||
directory,
|
||||
archived: session.time?.archived,
|
||||
updated: session.time?.updated,
|
||||
})),
|
||||
)
|
||||
.catch(() => [] as SessionEntryInput[])
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (state.token !== current) return [] as CommandPaletteEntry[]
|
||||
const seen = new Set<string>()
|
||||
const next = results
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const key = `${item.directory}:${item.id}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map((item) => createSessionEntry(item, props.category()))
|
||||
state.cached = next
|
||||
return next
|
||||
})
|
||||
.catch(() => [] as CommandPaletteEntry[])
|
||||
.finally(() => {
|
||||
state.inflight = undefined
|
||||
})
|
||||
|
||||
return state.inflight
|
||||
}
|
||||
}
|
||||
|
||||
type SessionEntryInput = {
|
||||
directory: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}
|
||||
|
||||
function createSessionEntry(input: SessionEntryInput, category: string): CommandPaletteEntry {
|
||||
return {
|
||||
id: `session:${input.directory}:${input.id}`,
|
||||
type: "session",
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category,
|
||||
directory: input.directory,
|
||||
sessionID: input.id,
|
||||
archived: input.archived,
|
||||
updated: input.updated,
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
.command-palette-v2 {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Anchor to the top edge of where a centered 480px-tall dialog would sit, so the
|
||||
top stays put while the content-driven height grows and shrinks. */
|
||||
[data-component="dialog-v2"]:has(.command-palette-v2) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
[data-component="dialog-v2"]:has(.command-palette-v2) [data-slot="dialog-container"] {
|
||||
width: min(calc(100vw - 24px), 640px);
|
||||
height: auto;
|
||||
min-height: 280px;
|
||||
max-height: min(calc(100vh - 96px), 480px);
|
||||
margin-top: max(48px, calc((100vh - 480px) / 2));
|
||||
border-radius: 12px;
|
||||
background: var(--v2-background-bg-base);
|
||||
box-shadow: var(--v2-elevation-floating);
|
||||
}
|
||||
|
||||
.command-palette-v2-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.command-palette-v2-search {
|
||||
flex-shrink: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.command-palette-v2-search [data-component="text-input-v2"] {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
outline: 0;
|
||||
background: color-mix(in srgb, var(--v2-background-bg-layer-02) 60%, transparent);
|
||||
box-shadow: none;
|
||||
transition:
|
||||
background-color 120ms ease-in-out,
|
||||
box-shadow 120ms ease-in-out;
|
||||
}
|
||||
|
||||
.command-palette-v2-search [data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]),
|
||||
.command-palette-v2-search [data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-value"] {
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-input"] {
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.command-palette-v2-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.command-palette-v2-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 6px 6px 8px;
|
||||
}
|
||||
|
||||
.command-palette-v2-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.command-palette-v2-group-title {
|
||||
margin: 6px 0;
|
||||
padding: 0 12px;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.command-palette-v2-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--v2-text-text-base);
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
scroll-margin: 6px 0;
|
||||
}
|
||||
|
||||
.command-palette-v2-row[data-active] {
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
.command-palette-v2-row:focus-visible {
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.command-palette-v2-row-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.command-palette-v2-row-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--v2-icon-icon-muted);
|
||||
}
|
||||
|
||||
.command-palette-v2-row-text {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.command-palette-v2-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--v2-text-text-base);
|
||||
font-size: 13px;
|
||||
font-weight: 530;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-palette-v2-description,
|
||||
.command-palette-v2-meta {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-palette-v2-meta {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.command-palette-v2-file-path {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
}
|
||||
|
||||
.command-palette-v2-file-dir {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-weight: 440;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-palette-v2-file-name {
|
||||
flex-shrink: 0;
|
||||
color: var(--v2-text-text-base);
|
||||
font-weight: 530;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-palette-v2-state {
|
||||
display: grid;
|
||||
min-height: 120px;
|
||||
place-items: center;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.command-palette-v2-row-text {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.command-palette-v2-description {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
|
||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||
import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||
import { formatKeybindParts } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getRelativeTime } from "@/utils/time"
|
||||
import {
|
||||
createCommandPaletteFileEntry,
|
||||
createCommandPaletteModel,
|
||||
uniqueCommandPaletteEntries,
|
||||
type CommandPaletteEntry,
|
||||
} from "./command-palette"
|
||||
import "./dialog-command-palette-v2.css"
|
||||
|
||||
function groups(entries: CommandPaletteEntry[]) {
|
||||
const map = new Map<string, CommandPaletteEntry[]>()
|
||||
for (const entry of entries) map.set(entry.category, [...(map.get(entry.category) ?? []), entry])
|
||||
return Array.from(map.entries()).map(([category, entries]) => ({ category, entries }))
|
||||
}
|
||||
|
||||
function matchesEntry(entry: CommandPaletteEntry, query: string) {
|
||||
const value = query.toLowerCase()
|
||||
return [entry.title, entry.description, entry.category].some((text) => text?.toLowerCase().includes(value))
|
||||
}
|
||||
|
||||
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
||||
const palette = createCommandPaletteModel(props)
|
||||
const [query, setQuery] = createSignal("")
|
||||
const [active, setActive] = createSignal(0)
|
||||
|
||||
const loadItems = async (text: string) => {
|
||||
const q = text.trim()
|
||||
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||
|
||||
const [files, nextSessions] = await Promise.all([palette.file.searchFiles(q), Promise.resolve(palette.sessions(q))])
|
||||
const category = palette.language.t("palette.group.files")
|
||||
return [
|
||||
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
||||
...nextSessions.filter((entry) => matchesEntry(entry, q)),
|
||||
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
||||
]
|
||||
}
|
||||
|
||||
const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
||||
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
||||
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
||||
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
||||
const activeEntry = createMemo(() => visibleEntries()[active()])
|
||||
|
||||
createEffect(() => {
|
||||
query()
|
||||
visibleEntries()
|
||||
setActive(0)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
palette.highlight(activeEntry())
|
||||
})
|
||||
|
||||
let resultsRef: HTMLDivElement | undefined
|
||||
|
||||
const move = (delta: -1 | 1) => {
|
||||
const count = visibleEntries().length
|
||||
if (count === 0) return
|
||||
setActive((index) => (index + delta + count) % count)
|
||||
requestAnimationFrame(() => {
|
||||
resultsRef?.querySelector("[data-active]")?.scrollIntoView({ block: "nearest" })
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
move(1)
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
move(-1)
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
palette.select(activeEntry())
|
||||
return
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
palette.close()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog class="command-palette-v2" size="large">
|
||||
<DialogBody class="command-palette-v2-body">
|
||||
<div class="command-palette-v2-search">
|
||||
<TextInputV2
|
||||
value={query()}
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
spellcheck={false}
|
||||
appearance="large"
|
||||
placeholder={palette.language.t("palette.search.placeholder")}
|
||||
leadingIcon={<Icon name="magnifying-glass" />}
|
||||
onInput={(event) => setQuery(event.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<ScrollView class="command-palette-v2-scroll" viewportRef={(el) => (resultsRef = el)}>
|
||||
<div class="command-palette-v2-results" role="listbox">
|
||||
<Show
|
||||
when={visibleEntries().length > 0}
|
||||
fallback={
|
||||
<div class="command-palette-v2-state">
|
||||
{entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={groupedEntries()}>
|
||||
{(group) => (
|
||||
<div class="command-palette-v2-group">
|
||||
<Show when={group.category}>
|
||||
<div class="command-palette-v2-group-title">{group.category}</div>
|
||||
</Show>
|
||||
<For each={group.entries}>
|
||||
{(item) => (
|
||||
<PaletteRow
|
||||
item={item}
|
||||
active={activeEntry()?.id === item.id}
|
||||
language={palette.language}
|
||||
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
||||
onSelect={() => palette.select(item)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</ScrollView>
|
||||
</DialogBody>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function PaletteRow(props: {
|
||||
item: CommandPaletteEntry
|
||||
active: boolean
|
||||
language: ReturnType<typeof useLanguage>
|
||||
onActive: () => void
|
||||
onSelect: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="command-palette-v2-row"
|
||||
role="option"
|
||||
aria-selected={props.active}
|
||||
data-active={props.active ? "" : undefined}
|
||||
onMouseMove={(event) => {
|
||||
// Ignore hover from a static cursor when keyboard scrolling moves rows underneath it.
|
||||
if (event.movementX === 0 && event.movementY === 0) return
|
||||
props.onActive()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={props.onSelect}
|
||||
>
|
||||
<Switch
|
||||
fallback={
|
||||
<div class="command-palette-v2-row-main">
|
||||
<FileIcon node={{ path: props.item.path ?? "", type: "file" }} class="command-palette-v2-row-icon size-4" />
|
||||
<div class="command-palette-v2-file-path">
|
||||
<span class="command-palette-v2-file-dir">{getDirectory(props.item.path ?? "")}</span>
|
||||
<span class="command-palette-v2-file-name">{getFilename(props.item.path ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Match when={props.item.type === "command"}>
|
||||
<div class="command-palette-v2-row-main">
|
||||
<div class="command-palette-v2-row-text">
|
||||
<span class="command-palette-v2-title">{props.item.title}</span>
|
||||
<Show when={props.item.description}>
|
||||
<span class="command-palette-v2-description">{props.item.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.item.keybind}>
|
||||
<KeybindV2 keys={formatKeybindParts(props.item.keybind ?? "", props.language.t)} variant="neutral" />
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={props.item.type === "session"}>
|
||||
<div class="command-palette-v2-row-main">
|
||||
<Icon name="status" class="command-palette-v2-row-icon" />
|
||||
<div class="command-palette-v2-row-text">
|
||||
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
||||
{props.item.title}
|
||||
</span>
|
||||
<Show when={props.item.description}>
|
||||
<span class="command-palette-v2-description" classList={{ "opacity-70": !!props.item.archived }}>
|
||||
{props.item.description}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.item.updated}>
|
||||
<span class="command-palette-v2-meta">
|
||||
{getRelativeTime(new Date(props.item.updated!).toISOString(), props.language.t)}
|
||||
</span>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,84 +1,329 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { createMemo, createSignal, lazy, Match, Show, Switch } from "solid-js"
|
||||
import { formatKeybind } from "@/context/command"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createMemo, createSignal, lazy, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { getRelativeTime } from "@/utils/time"
|
||||
import {
|
||||
createCommandPaletteFileEntry,
|
||||
createCommandPaletteFileOpener,
|
||||
createCommandPaletteModel,
|
||||
uniqueCommandPaletteEntries,
|
||||
type CommandPaletteEntry,
|
||||
} from "./command-palette"
|
||||
|
||||
const DialogSelectFileV2 = lazy(() =>
|
||||
import("./dialog-select-directory-v2").then((module) => ({ default: module.DialogSelectDirectoryV2 })),
|
||||
)
|
||||
const DialogCommandPaletteV2 = lazy(() =>
|
||||
import("./dialog-command-palette-v2").then((module) => ({ default: module.DialogCommandPaletteV2 })),
|
||||
)
|
||||
|
||||
type EntryType = "command" | "file" | "session"
|
||||
|
||||
type Entry = {
|
||||
id: string
|
||||
type: EntryType
|
||||
title: string
|
||||
description?: string
|
||||
keybind?: string
|
||||
category: string
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
directory?: string
|
||||
sessionID?: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}
|
||||
|
||||
type DialogSelectFileMode = "all" | "files"
|
||||
|
||||
const ENTRY_LIMIT = 5
|
||||
const COMMON_COMMAND_IDS = [
|
||||
"session.new",
|
||||
"workspace.new",
|
||||
"session.previous",
|
||||
"session.next",
|
||||
"terminal.toggle",
|
||||
"review.toggle",
|
||||
] as const
|
||||
|
||||
const uniqueEntries = (items: Entry[]) => {
|
||||
const seen = new Set<string>()
|
||||
const out: Entry[] = []
|
||||
for (const item of items) {
|
||||
if (seen.has(item.id)) continue
|
||||
seen.add(item.id)
|
||||
out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const createCommandEntry = (option: CommandOption, category: string): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category,
|
||||
option,
|
||||
})
|
||||
|
||||
const createFileEntry = (path: string, category: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category,
|
||||
path,
|
||||
})
|
||||
|
||||
const createSessionEntry = (
|
||||
input: {
|
||||
directory: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
},
|
||||
category: string,
|
||||
): Entry => ({
|
||||
id: `session:${input.directory}:${input.id}`,
|
||||
type: "session",
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category,
|
||||
directory: input.directory,
|
||||
sessionID: input.id,
|
||||
archived: input.archived,
|
||||
updated: input.updated,
|
||||
})
|
||||
|
||||
function createCommandEntries(props: {
|
||||
filesOnly: () => boolean
|
||||
command: ReturnType<typeof useCommand>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const allowed = createMemo(() => {
|
||||
if (props.filesOnly()) return []
|
||||
return props.command.options.filter(
|
||||
(option) =>
|
||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
)
|
||||
})
|
||||
|
||||
const list = createMemo(() => {
|
||||
const category = props.language.t("palette.group.commands")
|
||||
return allowed().map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
const category = props.language.t("palette.group.commands")
|
||||
return sorted.map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
|
||||
return { allowed, list, picks }
|
||||
}
|
||||
|
||||
function createFileEntries(props: {
|
||||
file: ReturnType<typeof useFile>
|
||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const tabState = createSessionTabs({
|
||||
tabs: props.tabs,
|
||||
pathFromTab: props.file.pathFromTab,
|
||||
normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab),
|
||||
})
|
||||
const recent = createMemo(() => {
|
||||
const all = tabState.openedTabs()
|
||||
const active = tabState.activeFileTab()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const category = props.language.t("palette.group.files")
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = props.file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(createFileEntry(path, category))
|
||||
}
|
||||
|
||||
return items.slice(0, ENTRY_LIMIT)
|
||||
})
|
||||
|
||||
const root = createMemo(() => {
|
||||
const category = props.language.t("palette.group.files")
|
||||
const nodes = props.file.tree.children("")
|
||||
const paths = nodes
|
||||
.filter((node) => node.type === "file")
|
||||
.map((node) => node.path)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category))
|
||||
})
|
||||
|
||||
return { recent, root }
|
||||
}
|
||||
|
||||
function createSessionEntries(props: {
|
||||
workspaces: () => string[]
|
||||
label: (directory: string) => string
|
||||
serverSDK: ServerSDK
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const state: {
|
||||
token: number
|
||||
inflight: Promise<Entry[]> | undefined
|
||||
cached: Entry[] | undefined
|
||||
} = {
|
||||
token: 0,
|
||||
inflight: undefined,
|
||||
cached: undefined,
|
||||
}
|
||||
|
||||
const sessions = (text: string) => {
|
||||
const query = text.trim()
|
||||
if (!query) {
|
||||
state.token += 1
|
||||
state.inflight = undefined
|
||||
state.cached = undefined
|
||||
return [] as Entry[]
|
||||
}
|
||||
|
||||
if (state.cached) return state.cached
|
||||
if (state.inflight) return state.inflight
|
||||
|
||||
const current = state.token
|
||||
const dirs = props.workspaces()
|
||||
if (dirs.length === 0) return [] as Entry[]
|
||||
|
||||
state.inflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = props.label(directory)
|
||||
return props.serverSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) =>
|
||||
(x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.title ?? props.language.t("command.session.new"),
|
||||
description,
|
||||
directory,
|
||||
archived: s.time?.archived,
|
||||
updated: s.time?.updated,
|
||||
})),
|
||||
)
|
||||
.catch(
|
||||
() =>
|
||||
[] as {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
directory: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}[],
|
||||
)
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (state.token !== current) return [] as Entry[]
|
||||
const seen = new Set<string>()
|
||||
const category = props.language.t("command.category.session")
|
||||
const next = results
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const key = `${item.directory}:${item.id}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map((item) => createSessionEntry(item, category))
|
||||
state.cached = next
|
||||
return next
|
||||
})
|
||||
.catch(() => [] as Entry[])
|
||||
.finally(() => {
|
||||
state.inflight = undefined
|
||||
})
|
||||
|
||||
return state.inflight
|
||||
}
|
||||
|
||||
return { sessions }
|
||||
}
|
||||
|
||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const settings = useSettings()
|
||||
const filesOnly = () => props.mode === "files"
|
||||
|
||||
if (!filesOnly() && settings.general.newLayoutDesigns()) {
|
||||
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
|
||||
}
|
||||
|
||||
if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
|
||||
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
|
||||
}
|
||||
|
||||
return <DialogSelectFileLegacy filesOnly={filesOnly} onOpenFile={props.onOpenFile} />
|
||||
}
|
||||
|
||||
function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void }) {
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const serverSDK = useServerSDK()
|
||||
const { params } = useSessionLayout()
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||
|
||||
return (
|
||||
<DialogSelectFileV2
|
||||
server={serverSDK().server}
|
||||
mode="file"
|
||||
start={projectDirectory()}
|
||||
title={language.t("session.header.searchFiles")}
|
||||
onSelect={(result) => {
|
||||
if (typeof result !== "string") return
|
||||
openFile(result)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||
const palette = createCommandPaletteModel(props)
|
||||
const serverSync = useServerSync()
|
||||
const { params, tabs, view } = useSessionLayout()
|
||||
const filesOnly = () => props.mode === "files"
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const commandEntries = createCommandEntries({ filesOnly, command, language })
|
||||
const fileEntries = createFileEntries({ file, tabs, language })
|
||||
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||
})
|
||||
const workspaces = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
const current = project()
|
||||
if (!current) return directory ? [directory] : []
|
||||
|
||||
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||
return dirs
|
||||
})
|
||||
const homedir = createMemo(() => serverSync().data.path.home)
|
||||
const label = (directory: string) => {
|
||||
const current = project()
|
||||
const kind =
|
||||
current && directory === current.worktree
|
||||
? language.t("workspace.type.local")
|
||||
: language.t("workspace.type.sandbox")
|
||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
||||
const home = homedir()
|
||||
const path = home ? directory.replace(home, "~") : directory
|
||||
const name = store.vcs?.branch ?? getFilename(directory)
|
||||
return `${kind} : ${name || path}`
|
||||
}
|
||||
|
||||
const { sessions } = createSessionEntries({ workspaces, label, serverSDK: serverSDK(), language })
|
||||
|
||||
const items = async (text: string) => {
|
||||
const query = text.trim()
|
||||
setGrouped(query.length > 0)
|
||||
|
||||
if (!query && props.filesOnly()) {
|
||||
const loaded = palette.file.tree.state("")?.loaded
|
||||
const pending = loaded ? Promise.resolve() : palette.file.tree.list("")
|
||||
const next = uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
||||
if (!query && filesOnly()) {
|
||||
const loaded = file.tree.state("")?.loaded
|
||||
const pending = loaded ? Promise.resolve() : file.tree.list("")
|
||||
const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
||||
|
||||
if (loaded || next.length > 0) {
|
||||
void pending
|
||||
@@ -86,24 +331,79 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
||||
}
|
||||
|
||||
await pending
|
||||
return uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
||||
return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
||||
}
|
||||
|
||||
if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||
if (!query) return [...commandEntries.picks(), ...fileEntries.recent()]
|
||||
|
||||
if (props.filesOnly()) {
|
||||
const files = await palette.file.searchFiles(query)
|
||||
const category = palette.language.t("palette.group.files")
|
||||
return files.map((path) => createCommandPaletteFileEntry(path, category))
|
||||
if (filesOnly()) {
|
||||
const files = await file.searchFiles(query)
|
||||
const category = language.t("palette.group.files")
|
||||
return files.map((path) => createFileEntry(path, category))
|
||||
}
|
||||
|
||||
const [files, nextSessions] = await Promise.all([
|
||||
palette.file.searchFiles(query),
|
||||
Promise.resolve(palette.sessions(query)),
|
||||
])
|
||||
const category = palette.language.t("palette.group.files")
|
||||
const entries = files.map((path) => createCommandPaletteFileEntry(path, category))
|
||||
return [...palette.commandEntries(), ...nextSessions, ...entries]
|
||||
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
|
||||
const category = language.t("palette.group.files")
|
||||
const entries = files.map((path) => createFileEntry(path, category))
|
||||
return [...commandEntries.list(), ...nextSessions, ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
state.cleanup?.()
|
||||
if (!item) return
|
||||
if (item.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
void tabs().open(value)
|
||||
void file.load(path)
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("all")
|
||||
props.onOpenFile?.(path)
|
||||
tabs().setActive(value)
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
|
||||
if (item.type === "session") {
|
||||
if (!item.directory || !item.sessionID) return
|
||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.path) return
|
||||
open(item.path)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
|
||||
return (
|
||||
<DialogSelectFileV2
|
||||
server={serverSDK().server}
|
||||
mode="file"
|
||||
start={projectDirectory()}
|
||||
title={language.t("session.header.searchFiles")}
|
||||
onSelect={(result) => {
|
||||
if (typeof result !== "string") return
|
||||
open(result)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -111,21 +411,21 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
||||
<List
|
||||
class="px-3"
|
||||
search={{
|
||||
placeholder: props.filesOnly()
|
||||
? palette.language.t("session.header.searchFiles")
|
||||
: palette.language.t("palette.search.placeholder"),
|
||||
placeholder: filesOnly()
|
||||
? language.t("session.header.searchFiles")
|
||||
: language.t("palette.search.placeholder"),
|
||||
autofocus: true,
|
||||
hideIcon: true,
|
||||
}}
|
||||
emptyMessage={palette.language.t("palette.empty")}
|
||||
loadingMessage={palette.language.t("common.loading")}
|
||||
emptyMessage={language.t("palette.empty")}
|
||||
loadingMessage={language.t("common.loading")}
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
skipFilter={(item) => item.type === "file"}
|
||||
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||
onMove={(item: CommandPaletteEntry | undefined) => palette.highlight(item)}
|
||||
onSelect={(item: CommandPaletteEntry | undefined) => palette.select(item)}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<Switch
|
||||
@@ -152,7 +452,7 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind}>
|
||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", palette.language.t)}</Keybind>
|
||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
@@ -179,7 +479,7 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
|
||||
</div>
|
||||
<Show when={item.updated}>
|
||||
<span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
|
||||
{getRelativeTime(new Date(item.updated!).toISOString(), palette.language.t)}
|
||||
{getRelativeTime(new Date(item.updated!).toISOString(), language.t)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||
import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2"
|
||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import { ModelTooltip } from "./model-tooltip"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
@@ -93,7 +92,6 @@ const ModelList: Component<{
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
openDelay={0}
|
||||
value={<ModelTooltip model={item} latest={item.latest} free={isFree(item.provider.id, item.cost)} />}
|
||||
>
|
||||
{node}
|
||||
@@ -448,41 +446,26 @@ export function ModelSelectorPopoverV2(props: {
|
||||
<MenuV2.RadioGroup value={current()}>
|
||||
<For each={group.items}>
|
||||
{(item) => (
|
||||
<TooltipV2
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
openDelay={0}
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
latest={item.latest}
|
||||
free={isFree(item.provider.id, item.cost)}
|
||||
v2
|
||||
/>
|
||||
}
|
||||
<MenuV2.RadioItem
|
||||
value={modelKey(item)}
|
||||
data-option-key={modelKey(item)}
|
||||
data-selected-model={current() === modelKey(item) ? true : undefined}
|
||||
class="scroll-my-6"
|
||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||
onMouseEnter={() => {
|
||||
setStore("active", modelKey(item))
|
||||
setTimeout(() => searchRef?.focus())
|
||||
}}
|
||||
onSelect={() => selectModel(item)}
|
||||
>
|
||||
<MenuV2.RadioItem
|
||||
value={modelKey(item)}
|
||||
data-option-key={modelKey(item)}
|
||||
data-selected-model={current() === modelKey(item) ? true : undefined}
|
||||
class="scroll-my-6 w-full"
|
||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||
onMouseEnter={() => {
|
||||
setStore("active", modelKey(item))
|
||||
setTimeout(() => searchRef?.focus())
|
||||
}}
|
||||
onSelect={() => selectModel(item)}
|
||||
>
|
||||
<span class="min-w-0 truncate">{item.name}</span>
|
||||
<Show when={isFree(item.provider.id, item.cost)}>
|
||||
<TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2>
|
||||
</Show>
|
||||
<Show when={item.latest}>
|
||||
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
||||
</Show>
|
||||
</MenuV2.RadioItem>
|
||||
</TooltipV2>
|
||||
<span class="min-w-0 truncate">{item.name}</span>
|
||||
<Show when={isFree(item.provider.id, item.cost)}>
|
||||
<TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2>
|
||||
</Show>
|
||||
<Show when={item.latest}>
|
||||
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
||||
</Show>
|
||||
</MenuV2.RadioItem>
|
||||
)}
|
||||
</For>
|
||||
</MenuV2.RadioGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, type Component, type JSX } from "solid-js"
|
||||
import { Show, type Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
type InputKey = "text" | "image" | "audio" | "video" | "pdf"
|
||||
@@ -23,18 +23,7 @@ type ModelInfo = {
|
||||
}
|
||||
}
|
||||
|
||||
function ModelTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
||||
return (
|
||||
<div class="flex min-w-0 items-center gap-4">
|
||||
<span class="shrink-0 text-v2-text-text-muted">{props.name}</span>
|
||||
<span class="ml-auto min-w-0 truncate text-right text-v2-text-text-base">{props.value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean; v2?: boolean }> = (
|
||||
props,
|
||||
) => {
|
||||
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
|
||||
const language = useLanguage()
|
||||
const sourceName = (model: ModelInfo) => {
|
||||
const value = `${model.id} ${model.name}`.toLowerCase()
|
||||
@@ -62,13 +51,6 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
|
||||
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
||||
return `${sourceName(props.model)} ${props.model.name}${suffix}`
|
||||
}
|
||||
const name = () => {
|
||||
const tags: Array<string> = []
|
||||
if (props.latest) tags.push(language.t("model.tag.latest"))
|
||||
if (props.free) tags.push(language.t("model.tag.free"))
|
||||
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
||||
return `${props.model.name}${suffix}`
|
||||
}
|
||||
const inputs = () => {
|
||||
if (props.model.capabilities) {
|
||||
const input = props.model.capabilities.input
|
||||
@@ -91,21 +73,6 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
|
||||
: language.t("model.tooltip.reasoning.none")
|
||||
}
|
||||
const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
|
||||
const contextLimit = () => props.model.limit.context.toLocaleString(language.intl())
|
||||
|
||||
if (props.v2) {
|
||||
return (
|
||||
<div class="flex w-[180px] flex-col gap-2">
|
||||
<ModelTooltipRow name={language.t("model.tooltip.model")} value={name()} />
|
||||
<ModelTooltipRow name={language.t("model.tooltip.provider")} value={props.model.provider.name} />
|
||||
<Show when={inputs()}>
|
||||
{(value) => <ModelTooltipRow name={language.t("model.tooltip.inputs")} value={value()} />}
|
||||
</Show>
|
||||
<ModelTooltipRow name={language.t("model.tooltip.reasoning")} value={reasoning()} />
|
||||
<ModelTooltipRow name={language.t("model.tooltip.context.label")} value={contextLimit()} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-1 py-1">
|
||||
|
||||
@@ -440,6 +440,11 @@ export const SettingsGeneral: Component = () => {
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewColorScheme(option.value)
|
||||
return () => theme.cancelPreview()
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
@@ -466,6 +471,11 @@ export const SettingsGeneral: Component = () => {
|
||||
if (!option) return
|
||||
theme.setTheme(option.id)
|
||||
}}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewTheme(option.id)
|
||||
return () => theme.cancelPreview()
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
triggerVariant="settings"
|
||||
|
||||
@@ -422,6 +422,11 @@ export const SettingsGeneralV2: Component<{
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewColorScheme(option.value)
|
||||
return () => theme.cancelPreview()
|
||||
}}
|
||||
/>
|
||||
</SettingsRowV2>
|
||||
|
||||
@@ -449,6 +454,11 @@ export const SettingsGeneralV2: Component<{
|
||||
if (!option) return
|
||||
theme.setTheme(option.id)
|
||||
}}
|
||||
onHighlight={(option) => {
|
||||
if (!option) return
|
||||
theme.previewTheme(option.id)
|
||||
return () => theme.cancelPreview()
|
||||
}}
|
||||
/>
|
||||
</SettingsRowV2>
|
||||
|
||||
|
||||
@@ -246,23 +246,18 @@ export function TabNavItem(props: {
|
||||
}}
|
||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
||||
>
|
||||
<span data-slot="project-avatar-slot" class="flex size-4 shrink-0 items-center justify-center">
|
||||
<Show
|
||||
when={props.session()}
|
||||
fallback={
|
||||
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
||||
}
|
||||
>
|
||||
{(session) => (
|
||||
<Show when={props.session()}>
|
||||
{(session) => (
|
||||
<span data-slot="project-avatar-slot">
|
||||
<SessionTabAvatar
|
||||
project={project()}
|
||||
directory={session().directory}
|
||||
sessionId={session().id}
|
||||
server={props.server}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<span
|
||||
ref={(el) => {
|
||||
titleEl = el
|
||||
|
||||
@@ -40,7 +40,6 @@ function SessionTabSlot(props: {
|
||||
let ref!: HTMLDivElement
|
||||
const sdk = createMemo(() => props.serverCtx()?.sdk ?? null)
|
||||
const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId))
|
||||
const persisted = createMemo(() => tabs.info[props.id])
|
||||
const [loadedSession] = createResource(
|
||||
() => {
|
||||
const ctx = props.serverCtx()
|
||||
@@ -72,10 +71,8 @@ function SessionTabSlot(props: {
|
||||
|
||||
createEffect(() => {
|
||||
const value = session()
|
||||
if (!value) return
|
||||
tabs.rememberSessionInfo(props.tab, value)
|
||||
const current = sdk()
|
||||
if (!current) return
|
||||
if (!value || !current) return
|
||||
createTabPromptState(tabs, props.tab, current.scope, {
|
||||
dir: base64Encode(value.directory),
|
||||
id: value.id,
|
||||
@@ -89,7 +86,7 @@ function SessionTabSlot(props: {
|
||||
data-tab-key={props.id}
|
||||
data-active={props.active()}
|
||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||
classList={{ hidden: !session() && !missingSession() && !persisted()?.title }}
|
||||
classList={{ hidden: !session() && !missingSession() }}
|
||||
>
|
||||
<TabNavItem
|
||||
ref={(el) => {
|
||||
@@ -98,7 +95,7 @@ function SessionTabSlot(props: {
|
||||
href={tabHref(props.tab)}
|
||||
server={props.tab.server}
|
||||
session={session}
|
||||
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
||||
fallbackTitle={missingSession() ? language.t("session.tab.unknown") : undefined}
|
||||
onTitleChange={(title) => {
|
||||
const value = session()
|
||||
const ctx = props.serverCtx()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
||||
import { upsertCommandRegistration } from "./command"
|
||||
|
||||
describe("upsertCommandRegistration", () => {
|
||||
test("replaces keyed registrations", () => {
|
||||
@@ -23,19 +23,3 @@ describe("upsertCommandRegistration", () => {
|
||||
expect(next[1]?.options).toBe(one)
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveKeybindOption", () => {
|
||||
test("prefers a matching contextual command over the global fallback", () => {
|
||||
const fallback = { id: "tab.close", title: "Close tab" }
|
||||
const contextual = { id: "terminal.close", title: "Close terminal", when: () => true }
|
||||
|
||||
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(contextual)
|
||||
})
|
||||
|
||||
test("uses the global fallback outside the command context", () => {
|
||||
const fallback = { id: "tab.close", title: "Close tab" }
|
||||
const contextual = { id: "terminal.close", title: "Close terminal", when: () => false }
|
||||
|
||||
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,15 +82,10 @@ export interface CommandOption {
|
||||
suggested?: boolean
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
when?: (event: KeyboardEvent) => boolean
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
|
||||
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
||||
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
||||
}
|
||||
|
||||
type CommandSource = "palette" | "keybind" | "slash"
|
||||
|
||||
export type CommandCatalogItem = {
|
||||
@@ -339,7 +334,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
})
|
||||
|
||||
const keymap = createMemo(() => {
|
||||
const map = new Map<string, CommandOption[]>()
|
||||
const map = new Map<string, CommandOption>()
|
||||
for (const option of options()) {
|
||||
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
||||
if (option.disabled) continue
|
||||
@@ -349,12 +344,8 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
for (const kb of keybinds) {
|
||||
if (!kb.key) continue
|
||||
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
||||
const existing = map.get(sig)
|
||||
if (existing) {
|
||||
existing.push(option)
|
||||
continue
|
||||
}
|
||||
map.set(sig, [option])
|
||||
if (map.has(sig)) continue
|
||||
map.set(sig, option)
|
||||
}
|
||||
}
|
||||
return map
|
||||
@@ -383,7 +374,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
|
||||
const sig = signatureFromEvent(event)
|
||||
const isPalette = palette().has(sig)
|
||||
const option = resolveKeybindOption(keymap().get(sig), event)
|
||||
const option = keymap().get(sig)
|
||||
const modified = event.ctrlKey || event.metaKey || event.altKey
|
||||
const isTab = event.key === "Tab"
|
||||
|
||||
@@ -392,19 +383,17 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
|
||||
if (isPalette) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
showPalette()
|
||||
return
|
||||
}
|
||||
|
||||
if (!option) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
option.onSelect?.("keybind")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
makeEventListener(document, "keydown", handleKeyDown, { capture: true })
|
||||
makeEventListener(document, "keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
function register(cb: () => CommandOption[]): void
|
||||
|
||||
@@ -174,7 +174,7 @@ describe("server session", () => {
|
||||
await ctx.store.sync("root")
|
||||
|
||||
expect(ctx.get).toEqual([{ sessionID: "root" }])
|
||||
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, before: undefined }])
|
||||
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 2, before: undefined }])
|
||||
expect(ctx.store.data.message.root).toEqual([])
|
||||
})
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("server session", () => {
|
||||
|
||||
await store.sync("child")
|
||||
|
||||
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, before: undefined }])
|
||||
expect(client.requests).toEqual([{ sessionID: "child", limit: 2, before: undefined }])
|
||||
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
|
||||
expect(store.data.message.child).toEqual([user, ...assistants])
|
||||
expect(store.history.more("child")).toBe(true)
|
||||
|
||||
@@ -21,7 +21,7 @@ import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } fro
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const initialMessagePageSize = 20
|
||||
const initialMessagePageSize = 2
|
||||
const historyMessagePageSize = 200
|
||||
const sessionInfoLimit = 2_048
|
||||
const emptyIDs: ReadonlySet<string> = new Set()
|
||||
|
||||
@@ -28,11 +28,6 @@ export type DraftTab = {
|
||||
|
||||
export type Tab = SessionTab | DraftTab
|
||||
|
||||
export type TabInfo = {
|
||||
title?: string
|
||||
directory?: string
|
||||
}
|
||||
|
||||
type RecentTab = {
|
||||
key?: string
|
||||
}
|
||||
@@ -69,7 +64,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
createStore<Tab[]>([]),
|
||||
)
|
||||
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
|
||||
const [info, setInfo] = persisted(Persist.window("tabs.info"), createStore<Record<string, TabInfo>>({}))
|
||||
const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore<ClosedTab[]>([]))
|
||||
|
||||
const params = useParams()
|
||||
@@ -108,15 +102,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
|
||||
}
|
||||
|
||||
const removeInfo = (key: string) => {
|
||||
if (!info[key]) return
|
||||
setInfo(
|
||||
produce((draft) => {
|
||||
delete draft[key]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
onCleanup(memory.dispose)
|
||||
|
||||
createEffect(() => {
|
||||
@@ -125,19 +110,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
const next = store.filter((tab) => servers.has(tab.server))
|
||||
if (next.length !== store.length) {
|
||||
for (const tab of store) {
|
||||
if (!servers.has(tab.server)) {
|
||||
const key = tabKey(tab)
|
||||
memory.remove(key)
|
||||
removeInfo(key)
|
||||
}
|
||||
if (!servers.has(tab.server)) memory.remove(tabKey(tab))
|
||||
}
|
||||
setStore(() => next)
|
||||
}
|
||||
if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined)
|
||||
const keys = new Set(next.map(tabKey))
|
||||
for (const key of Object.keys(info)) {
|
||||
if (!keys.has(key)) removeInfo(key)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -173,7 +150,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
if (nextTab) navigateTab(nextTab)
|
||||
}).finally(() => closing.delete(key))
|
||||
memory.remove(key)
|
||||
removeInfo(key)
|
||||
if (draftID) removeDraftPersisted(draftID)
|
||||
}
|
||||
|
||||
@@ -288,7 +264,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
const removed = store.filter((tab) => tab.server === key).map(tabKey)
|
||||
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
||||
for (const key of removed) memory.remove(key)
|
||||
for (const key of removed) removeInfo(key)
|
||||
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
|
||||
for (const draftID of drafts) removeDraftPersisted(draftID)
|
||||
if (server.key === key) navigate("/")
|
||||
@@ -343,14 +318,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
|
||||
})
|
||||
for (const key of removed) memory.remove(key)
|
||||
for (const key of removed) removeInfo(key)
|
||||
},
|
||||
rememberSessionInfo(tab: SessionTab, session: Session) {
|
||||
const key = tabKey(tab)
|
||||
const next = { title: session.title, directory: session.directory }
|
||||
const current = info[key]
|
||||
if (current?.title === next.title && current.directory === next.directory) return
|
||||
setInfo(key, next)
|
||||
},
|
||||
select: navigateTab,
|
||||
remember(tab: Tab) {
|
||||
@@ -375,6 +342,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
},
|
||||
}
|
||||
|
||||
return { ...actions, store, info, ready, recentReady }
|
||||
return { ...actions, store, ready, recentReady }
|
||||
},
|
||||
})
|
||||
|
||||
@@ -212,11 +212,6 @@ export const dict = {
|
||||
"model.input.video": "video",
|
||||
"model.input.pdf": "pdf",
|
||||
"model.tooltip.allows": "Allows: {{inputs}}",
|
||||
"model.tooltip.context.label": "Context",
|
||||
"model.tooltip.inputs": "Inputs",
|
||||
"model.tooltip.model": "Model",
|
||||
"model.tooltip.provider": "Provider",
|
||||
"model.tooltip.reasoning": "Reasoning",
|
||||
"model.tooltip.reasoning.allowed": "Allows reasoning",
|
||||
"model.tooltip.reasoning.none": "No reasoning",
|
||||
"model.tooltip.context": "Context limit {{limit}}",
|
||||
|
||||
@@ -224,26 +224,7 @@
|
||||
|
||||
[data-slot="model-selector-scroll"]::before {
|
||||
top: 0;
|
||||
height: 12px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 78.3336%, transparent) 4.7%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 76.4824%, transparent) 8.9%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 74.368%, transparent) 12.8%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 71.9123%, transparent) 16.56%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 69.037%, transparent) 20.37%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 65.664%, transparent) 24.4%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 61.715%, transparent) 28.83%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 57.1117%, transparent) 33.84%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 51.776%, transparent) 39.6%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 45.6296%, transparent) 46.3%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 38.5944%, transparent) 54.1%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 30.592%, transparent) 63.2%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 21.5443%, transparent) 73.76%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 11.373%, transparent) 85.97%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 0%, transparent) 100%
|
||||
);
|
||||
background: linear-gradient(to bottom, var(--v2-background-bg-layer-01), transparent);
|
||||
}
|
||||
|
||||
[data-slot="manage-models-scroll"]::before {
|
||||
@@ -258,26 +239,7 @@
|
||||
|
||||
[data-slot="model-selector-scroll"]::after {
|
||||
bottom: 0;
|
||||
height: 12px;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 78.3336%, transparent) 4.7%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 76.4824%, transparent) 8.9%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 74.368%, transparent) 12.8%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 71.9123%, transparent) 16.56%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 69.037%, transparent) 20.37%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 65.664%, transparent) 24.4%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 61.715%, transparent) 28.83%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 57.1117%, transparent) 33.84%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 51.776%, transparent) 39.6%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 45.6296%, transparent) 46.3%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 38.5944%, transparent) 54.1%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 30.592%, transparent) 63.2%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 21.5443%, transparent) 73.76%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 11.373%, transparent) 85.97%,
|
||||
color-mix(in srgb, var(--v2-background-bg-layer-01) 0%, transparent) 100%
|
||||
);
|
||||
background: linear-gradient(to top, var(--v2-background-bg-layer-01), transparent);
|
||||
}
|
||||
|
||||
@supports (animation-timeline: --home-projects-scroll) and (timeline-scope: --home-projects-scroll) {
|
||||
|
||||
@@ -35,7 +35,7 @@ import { usePlatform } from "@/context/platform"
|
||||
import { DateTime } from "luxon"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||
import { useSettingsDialog } from "@/components/settings-dialog"
|
||||
import { useSettingsCommand } from "@/components/settings-dialog"
|
||||
import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server"
|
||||
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
|
||||
import { ServerConnection, serverName, useServer } from "@/context/server"
|
||||
@@ -267,7 +267,7 @@ export function NewHome() {
|
||||
const command = useCommand()
|
||||
const notification = useNotification()
|
||||
const marked = useMarked()
|
||||
const openSettings = useSettingsDialog()
|
||||
const openSettings = useSettingsCommand()
|
||||
let focusSessionSearch: (() => void) | undefined
|
||||
const [state, setState] = createStore({
|
||||
search: "",
|
||||
@@ -1278,7 +1278,7 @@ function HomeSessionSearch(props: {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02">
|
||||
<label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02">
|
||||
<IconV2 name="magnifying-glass" />
|
||||
<input
|
||||
ref={input}
|
||||
|
||||
@@ -6,13 +6,11 @@ import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { setNavigate } from "@/utils/notification-click"
|
||||
import { setV2Toast, ToastRegion } from "@/utils/toast"
|
||||
import { useSettingsCommand } from "@/components/settings-dialog"
|
||||
|
||||
export default function NewLayout(props: ParentProps) {
|
||||
const platform = usePlatform()
|
||||
const navigate = useNavigate()
|
||||
setNavigate(navigate)
|
||||
useSettingsCommand()
|
||||
|
||||
createEffect(() => setV2Toast(true))
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createMemo, type Accessor } from "solid-js"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { sessionPermissionRequest, sessionQuestionRequest } from "@/pages/session/composer/session-request-tree"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
||||
|
||||
export function useSessionTabAvatarState(
|
||||
server: Accessor<ServerConnection.Key>,
|
||||
@@ -26,20 +26,15 @@ export function useSessionTabAvatarState(
|
||||
return !permission.autoResponds(item, directory())
|
||||
})
|
||||
})
|
||||
const hasQuestions = createMemo(() => {
|
||||
const serverSync = sync()
|
||||
if (!serverSync) return false
|
||||
const [store] = serverSync.child(directory(), { bootstrap: false })
|
||||
return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId())
|
||||
const unread = createMemo(() => {
|
||||
if (hasPermissions()) return true
|
||||
if (!connection()) return false
|
||||
return notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0
|
||||
})
|
||||
const needsAttention = createMemo(() => hasPermissions() || hasQuestions())
|
||||
const unread = createMemo(
|
||||
() => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0,
|
||||
)
|
||||
const loading = createMemo(() => {
|
||||
const serverSync = sync()
|
||||
if (!serverSync) return false
|
||||
if (needsAttention()) return false
|
||||
if (hasPermissions()) return false
|
||||
return serverSync.session.data.session_working(sessionId())
|
||||
})
|
||||
return { unread, loading }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { NewSessionDesignView } from "@/components/session"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { StatusPopoverV2 } from "@/components/status-popover"
|
||||
import { useSettingsCommand } from "@/components/settings-dialog"
|
||||
import {
|
||||
PromptProjectAddButton,
|
||||
PromptProjectSelector,
|
||||
@@ -44,6 +45,7 @@ export default function NewSessionPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
||||
|
||||
useComposerCommands()
|
||||
useSettingsCommand()
|
||||
|
||||
let inputRef: HTMLDivElement | undefined
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-b
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
|
||||
import { useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||
import { Navigate, useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { ErrorPage } from "@/pages/error"
|
||||
import { CommentsProvider, useComments } from "@/context/comments"
|
||||
@@ -56,6 +56,7 @@ import { useSync } from "@/context/sync"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { TerminalProvider, useTerminal } from "@/context/terminal"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { useSettingsCommand } from "@/components/settings-dialog"
|
||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||
import {
|
||||
createPromptInputController,
|
||||
@@ -152,7 +153,7 @@ export function TargetSessionRouteContent() {
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionRouteErrorBoundary(
|
||||
function SessionRouteErrorBoundary(
|
||||
props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key; padded?: boolean }>,
|
||||
) {
|
||||
const settings = useSettings()
|
||||
@@ -220,6 +221,7 @@ function SessionErrorFallback(props: { error: unknown; sessionID?: string; serve
|
||||
|
||||
function ResolvedTargetSessionRoute() {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const settings = useSettings()
|
||||
const tabs = useTabs()
|
||||
const sync = useServerSync()
|
||||
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
||||
@@ -246,11 +248,16 @@ function ResolvedTargetSessionRoute() {
|
||||
the terminal. Same-workspace tab switches keep it open because warm
|
||||
targets resolve synchronously from the sync cache. */}
|
||||
<Show when={directory()}>
|
||||
<SDKProvider directory={targetDirectory}>
|
||||
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
||||
<TargetSessionPage />
|
||||
</DirectoryDataProvider>
|
||||
</SDKProvider>
|
||||
<Show
|
||||
when={settings.general.newLayoutDesigns()}
|
||||
fallback={<Navigate href={legacySessionHref(directory()!, params.id)} />}
|
||||
>
|
||||
<SDKProvider directory={targetDirectory}>
|
||||
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
||||
<TargetSessionPage />
|
||||
</DirectoryDataProvider>
|
||||
</SDKProvider>
|
||||
</Show>
|
||||
</Show>
|
||||
</TargetServerScopedProviders>
|
||||
)
|
||||
@@ -1014,6 +1021,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
useComposerCommands()
|
||||
useSettingsCommand()
|
||||
useSessionCommands({
|
||||
navigateMessageByOffset,
|
||||
setActiveMessage,
|
||||
|
||||
@@ -18,6 +18,8 @@ import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { requireServerKey } from "@/utils/session-route"
|
||||
import { createSessionOwnership } from "./session-ownership"
|
||||
|
||||
export type SessionCommandContext = {
|
||||
@@ -46,6 +48,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const settings = useSettings()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const sessionTabs = useTabs()
|
||||
const layout = useLayout()
|
||||
const navigate = useNavigate()
|
||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||
@@ -266,14 +269,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
view().terminal.open()
|
||||
}
|
||||
|
||||
const closeTerminal = () => {
|
||||
const id = terminal.active()
|
||||
if (!id) return
|
||||
const last = terminal.all().length === 1
|
||||
void terminal.close(id)
|
||||
if (last) view().terminal.close()
|
||||
}
|
||||
|
||||
const chooseMcp = () => {
|
||||
void openDialog(
|
||||
() => import("@/components/dialog-select-mcp"),
|
||||
@@ -416,9 +411,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
title: language.t("command.session.new"),
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
onSelect: (source) => {
|
||||
if (settings.general.newLayoutDesigns()) {
|
||||
command.trigger("tab.new", source)
|
||||
onSelect: () => {
|
||||
if (params.serverKey) {
|
||||
sessionTabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory })
|
||||
return
|
||||
}
|
||||
navigate(`/${params.dir}/session`)
|
||||
@@ -523,14 +518,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
]
|
||||
|
||||
const terminalCmds = () => [
|
||||
terminalCommand({
|
||||
id: "terminal.close",
|
||||
title: language.t("terminal.close"),
|
||||
keybind: "mod+w",
|
||||
hidden: true,
|
||||
when: (event) => event.target instanceof Element && !!event.target.closest('[data-component="terminal"]'),
|
||||
onSelect: closeTerminal,
|
||||
}),
|
||||
terminalCommand({
|
||||
id: "terminal.new",
|
||||
title: language.t("command.terminal.new"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -21,6 +21,14 @@ import { Api } from "@opencode-ai/server/api"
|
||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
|
||||
|
||||
const Client = await import("../src/effect")
|
||||
|
||||
test("effect entrypoint exposes canonical Schema contracts", () => {
|
||||
expect(Client.Agent).toBe(Agent)
|
||||
expect(Client.Model).toBe(Model)
|
||||
expect(Client.Session).toBe(Session)
|
||||
})
|
||||
|
||||
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
||||
expect(AgentV2.ID).toBe(Agent.ID)
|
||||
expect(CoreLocation.Ref).toBe(Location.Ref)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -11,11 +11,6 @@ import { FSUtil } from "../../fs-util"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ConfigAgentV1 } from "../../v1/config/agent"
|
||||
import { ConfigMigrateV1 } from "../../v1/config/migrate"
|
||||
import { Global } from "../../global"
|
||||
import { PermissionV2 } from "../../permission"
|
||||
import type { LocationMutation } from "../../location-mutation"
|
||||
import type { ReadTool } from "../../tool/read"
|
||||
import type { EditTool } from "../../tool/edit"
|
||||
|
||||
const legacySources = [
|
||||
{ pattern: "{agent,agents}/**/*.md", primary: false },
|
||||
@@ -24,11 +19,6 @@ const legacySources = [
|
||||
const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
|
||||
const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
|
||||
const decodeConfig = Schema.decodeUnknownOption(Config.Info)
|
||||
type PathAction =
|
||||
| LocationMutation.ExternalDirectoryAuthorization["action"]
|
||||
| typeof ReadTool.name
|
||||
| typeof EditTool.name
|
||||
const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[]
|
||||
const agentKeys = new Set([
|
||||
"model",
|
||||
"variant",
|
||||
@@ -48,7 +38,6 @@ export const Plugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (draft) {
|
||||
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||
@@ -67,14 +56,11 @@ export const Plugin = define({
|
||||
)
|
||||
})
|
||||
}).pipe(Effect.map((documents) => documents.flat()))
|
||||
const permissions = expandPermissions(
|
||||
documents.flatMap((document) => document.info.permissions ?? []),
|
||||
global.home,
|
||||
)
|
||||
const global = documents.flatMap((document) => document.info.permissions ?? [])
|
||||
const configuredDefault = Config.latest(documents, "default_agent")
|
||||
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
||||
for (const current of draft.list()) {
|
||||
draft.update(current.id, (agent) => agent.permissions.push(...permissions))
|
||||
draft.update(current.id, (agent) => agent.permissions.push(...global))
|
||||
}
|
||||
|
||||
for (const document of documents) {
|
||||
@@ -87,7 +73,7 @@ export const Plugin = define({
|
||||
|
||||
const exists = draft.get(agentID) !== undefined
|
||||
draft.update(agentID, (agent) => {
|
||||
if (!exists) agent.permissions.push(...permissions)
|
||||
if (!exists) agent.permissions.push(...global)
|
||||
if (item.model !== undefined) {
|
||||
const model = ModelV2.parse(item.model)
|
||||
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
|
||||
@@ -105,9 +91,7 @@ export const Plugin = define({
|
||||
if (item.hidden !== undefined) agent.hidden = item.hidden
|
||||
if (item.color !== undefined) agent.color = item.color
|
||||
if (item.steps !== undefined) agent.steps = item.steps
|
||||
if (item.permissions !== undefined) {
|
||||
agent.permissions.push(...expandPermissions(item.permissions, global.home))
|
||||
}
|
||||
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -116,27 +100,6 @@ export const Plugin = define({
|
||||
}),
|
||||
})
|
||||
|
||||
function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset {
|
||||
// Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text:
|
||||
// rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing.
|
||||
return rules.map((rule) =>
|
||||
isPathAction(rule.action) ? { ...rule, resource: expandHome(rule.resource, home) } : rule,
|
||||
)
|
||||
}
|
||||
|
||||
function isPathAction(action: string): action is PathAction {
|
||||
return pathActions.some((item) => item === action)
|
||||
}
|
||||
|
||||
function expandHome(resource: string, home: string) {
|
||||
if (resource.startsWith("~/")) return home + resource.slice(1)
|
||||
if (resource === "~") return home
|
||||
if (resource === "$HOME") return home
|
||||
if (resource.startsWith("$HOME/")) return home + resource.slice(5)
|
||||
if (resource.startsWith("$HOME\\")) return home + resource.slice(5)
|
||||
return resource
|
||||
}
|
||||
|
||||
function discover(fs: FSUtil.Interface, directory: string) {
|
||||
return Effect.forEach(legacySources, (source) =>
|
||||
fs
|
||||
|
||||
@@ -60,10 +60,9 @@ export namespace FSUtil {
|
||||
})
|
||||
|
||||
const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) {
|
||||
return yield* fs.readFileString(path).pipe(
|
||||
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)),
|
||||
Effect.catchReason("PlatformError", "PermissionDenied", () => Effect.succeed(undefined)),
|
||||
)
|
||||
return yield* fs
|
||||
.readFileString(path)
|
||||
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
|
||||
})
|
||||
|
||||
const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) {
|
||||
|
||||
@@ -22,27 +22,20 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
|
||||
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
|
||||
|
||||
## Work State
|
||||
### Completed
|
||||
- [finished work, verified facts, or changes made; otherwise "(none)"]
|
||||
|
||||
### Active
|
||||
- [current work, partial changes, or investigation state; otherwise "(none)"]
|
||||
|
||||
### Blocked
|
||||
- [blockers, failing commands, or unknowns; otherwise "(none)"]
|
||||
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"]
|
||||
- Active: [current work, partial changes, or investigation state; otherwise "(none)"]
|
||||
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
|
||||
|
||||
## Next Move
|
||||
1. [immediate concrete action, or "(none)"]
|
||||
2. [next action if known, or "(none)"]
|
||||
|
||||
## Relevant Files
|
||||
- [file or directory path: why it matters, or "(none)"]
|
||||
</template>
|
||||
|
||||
Rules:
|
||||
- Keep every section, even when empty.
|
||||
- Use terse bullets, not prose paragraphs.
|
||||
- Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known.
|
||||
- Put relevant files and symbols inside the section where they matter; do not add extra sections.
|
||||
- Do not mention the summary process or that context was compacted.`
|
||||
|
||||
type Entry = {
|
||||
|
||||
@@ -7,20 +7,20 @@ import { makeGlobalNode } from "../effect/app-node"
|
||||
import { SessionHistory } from "./history"
|
||||
import { MessageDecodeError } from "./error"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionMessageTable, SessionTable } from "./sql"
|
||||
import { fromRow } from "./info"
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info | undefined>
|
||||
readonly context: (sessionID: SessionSchema.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||
readonly get: (sessionID: Session.ID) => Effect.Effect<Session.Info | undefined>
|
||||
readonly context: (sessionID: Session.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||
readonly runnerContext: (
|
||||
sessionID: SessionSchema.ID,
|
||||
sessionID: Session.ID,
|
||||
baselineSeq: number,
|
||||
) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||
readonly message: (
|
||||
messageID: SessionMessage.ID,
|
||||
) => Effect.Effect<{ readonly sessionID: SessionSchema.ID; readonly message: SessionMessage.Message } | undefined>
|
||||
) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Message } | undefined>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionStore") {}
|
||||
@@ -51,7 +51,7 @@ const layer = Layer.effect(
|
||||
.pipe(Effect.orDie)
|
||||
return row
|
||||
? {
|
||||
sessionID: SessionSchema.ID.make(row.session_id),
|
||||
sessionID: Session.ID.make(row.session_id),
|
||||
message: yield* decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie),
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -8,43 +8,16 @@ import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { agentHost, host } from "../plugin/host"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node, Global.node])))
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node])))
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
|
||||
describe("ConfigAgentPlugin.Plugin", () => {
|
||||
it.effect("matches POSIX paths against home-relative permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const permissions = yield* loadHomePermissions("/home/test")
|
||||
expect(PermissionV2.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe(
|
||||
"allow",
|
||||
)
|
||||
expect(PermissionV2.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny")
|
||||
expect(PermissionV2.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny")
|
||||
expect(PermissionV2.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny")
|
||||
expect(PermissionV2.evaluate("bash", "$HOME/private/key", permissions).effect).toBe("deny")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("matches Windows paths against home-relative permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const permissions = yield* loadHomePermissions("C:\\Users\\test")
|
||||
expect(
|
||||
PermissionV2.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect,
|
||||
).toBe("allow")
|
||||
expect(PermissionV2.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe(
|
||||
"deny",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies all global permissions before agent-specific permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
@@ -299,51 +272,3 @@ Use native v2 fields.`,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
function loadHomePermissions(home: string) {
|
||||
return Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const build = AgentV2.ID.make("build")
|
||||
yield* agents.transform((editor) => editor.update(build, () => {}))
|
||||
const config = Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
permission: {
|
||||
external_directory: {
|
||||
"~/p/**": "allow",
|
||||
"/some/~/path": "deny",
|
||||
"$HOMELESS/**": "deny",
|
||||
},
|
||||
bash: {
|
||||
"$HOME/private/**": "deny",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
build: {
|
||||
permission: {
|
||||
external_directory: {
|
||||
"$HOME/cache/**": "deny",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home })),
|
||||
)
|
||||
|
||||
const agent = yield* agents.get(build)
|
||||
if (!agent) throw new Error("expected configured build agent")
|
||||
return agent.permissions
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||
|
||||
test("compaction prompt preserves detailed work state and relevant files", () => {
|
||||
const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] })
|
||||
|
||||
expect(prompt).toContain("## Work State\n### Completed")
|
||||
expect(prompt).toContain("### Active")
|
||||
expect(prompt).toContain("### Blocked")
|
||||
expect(prompt).toContain("## Relevant Files")
|
||||
})
|
||||
|
||||
test("compaction describes tool media without embedding base64", () => {
|
||||
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
||||
const serialized = SessionCompaction.serializeToolContent([
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -71,10 +71,7 @@ export function setAppQuitting(quitting = true) {
|
||||
|
||||
export function setBackgroundColor(color: string) {
|
||||
backgroundColor = color
|
||||
BrowserWindow.getAllWindows().forEach((win) => {
|
||||
win.setBackgroundColor(color)
|
||||
if (process.platform === "darwin") win.invalidateShadow()
|
||||
})
|
||||
BrowserWindow.getAllWindows().forEach((win) => win.setBackgroundColor(color))
|
||||
}
|
||||
|
||||
export function getBackgroundColor(): string | undefined {
|
||||
@@ -109,13 +106,6 @@ function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
|
||||
|
||||
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
|
||||
titlebarThemes.set(win, theme)
|
||||
// macOS draws the window frame hairline and shadow using the NSWindow
|
||||
// appearance, which follows nativeTheme rather than the rendered content.
|
||||
// Align it with the app theme so a light app on a dark system does not get
|
||||
// the dark-appearance border and shadow. A "system" scheme must map to
|
||||
// "system" (not the resolved mode) or prefers-color-scheme stops tracking
|
||||
// OS appearance changes in the renderer.
|
||||
if (process.platform === "darwin") nativeTheme.themeSource = theme.scheme ?? theme.mode ?? "system"
|
||||
updateTitlebar(win)
|
||||
}
|
||||
|
||||
@@ -182,7 +172,7 @@ export function createMainWindow(id: string = randomUUID()) {
|
||||
...(process.platform === "darwin"
|
||||
? {
|
||||
titleBarStyle: "hidden" as const,
|
||||
trafficLightPosition: { x: 14, y: 14 },
|
||||
trafficLightPosition: { x: 12, y: 14 },
|
||||
}
|
||||
: {}),
|
||||
...(process.platform === "win32"
|
||||
|
||||
@@ -31,7 +31,6 @@ export type UpdaterAPI = {
|
||||
export type LinuxDisplayBackend = "wayland" | "auto"
|
||||
export type TitlebarTheme = {
|
||||
mode: "light" | "dark"
|
||||
scheme?: "system" | "light" | "dark"
|
||||
}
|
||||
export type FatalRendererError = {
|
||||
error: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,7 +6,6 @@ const patterns = [
|
||||
/input is too long for requested model/i,
|
||||
/exceeds the context window/i,
|
||||
/input token count.*exceeds the maximum/i,
|
||||
/tokens in request more than max tokens allowed/i,
|
||||
/maximum prompt length is \d+/i,
|
||||
/reduce the length of the messages/i,
|
||||
/maximum context length is \d+ tokens/i,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { isContextOverflow } from "../src"
|
||||
|
||||
describe("provider error classification", () => {
|
||||
test("classifies Z.AI GLM token limit messages as context overflow", () => {
|
||||
expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -293,7 +293,6 @@ const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const ensureGitignore = Effect.fn("Config.ensureGitignore")(function* (dir: string) {
|
||||
yield* fs.ensureDir(dir)
|
||||
const gitignore = path.join(dir, ".gitignore")
|
||||
const hasIgnore = yield* fs.existsSafe(gitignore)
|
||||
if (!hasIgnore) {
|
||||
|
||||
@@ -923,31 +923,6 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR
|
||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
|
||||
)
|
||||
|
||||
it.effect("ignores an inaccessible OPENCODE_CONFIG_DIR", () =>
|
||||
Effect.gen(function* () {
|
||||
if (process.platform === "win32") return
|
||||
|
||||
const dir = yield* tmpdirScoped()
|
||||
const configDir = path.join(dir, "inaccessible")
|
||||
yield* FSUtil.use.ensureDir(configDir)
|
||||
yield* FSUtil.use.chmod(configDir, 0o000)
|
||||
yield* Effect.addFinalizer(() => FSUtil.use.chmod(configDir, 0o755).pipe(Effect.ignore))
|
||||
|
||||
yield* withProcessEnv("OPENCODE_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir)))
|
||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
|
||||
)
|
||||
|
||||
it.effect("creates a missing OPENCODE_CONFIG_DIR", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const configDir = path.join(dir, "configdir")
|
||||
|
||||
yield* withProcessEnv("OPENCODE_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir)))
|
||||
|
||||
expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("node_modules")
|
||||
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
|
||||
)
|
||||
|
||||
it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
|
||||
@@ -1448,7 +1448,6 @@ describe("session.message-v2.fromError", () => {
|
||||
"prompt is too long: 213462 tokens > 200000 maximum",
|
||||
"Your input exceeds the context window of this model",
|
||||
"The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
|
||||
"tokens in request more than max tokens allowed",
|
||||
"Please reduce the length of the messages or completion",
|
||||
"400 status code (no body)",
|
||||
"413 status code (no body)",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "bun test --timeout 5000",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
@@ -22,6 +23,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Agent } from "@opencode-ai/schema/agent"
|
||||
import type { Types } from "effect"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface AgentDraft {
|
||||
list(): readonly AgentV2Info[]
|
||||
get(id: string): AgentV2Info | undefined
|
||||
list(): readonly Types.DeepMutable<Agent.Info>[]
|
||||
get(id: string): Types.DeepMutable<Agent.Info> | undefined
|
||||
default(id: string | undefined): void
|
||||
update(id: string, update: (agent: AgentV2Info) => void): void
|
||||
update(id: string, update: (agent: Types.DeepMutable<Agent.Info>) => void): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export type AISDKHooks = Hooks<{
|
||||
sdk: {
|
||||
readonly model: ModelV2Info
|
||||
readonly model: Model.Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}
|
||||
language: {
|
||||
readonly model: ModelV2Info
|
||||
readonly model: Model.Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
export type { PluginContext } from "./context.js"
|
||||
export { define } from "./plugin.js"
|
||||
export type { Plugin } from "./plugin.js"
|
||||
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Connection } from "@opencode-ai/schema/connection"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Provider } from "@opencode-ai/schema/provider"
|
||||
export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export type AISDKHooks = Hooks<{
|
||||
sdk: {
|
||||
readonly model: ModelV2Info
|
||||
readonly model: Model.Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}
|
||||
language: {
|
||||
readonly model: ModelV2Info
|
||||
readonly model: Model.Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
|
||||
@@ -10,3 +10,13 @@ export type { CommandDraft, CommandHooks } from "./command.js"
|
||||
export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration } from "./integration.js"
|
||||
export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
|
||||
export type { SkillDraft, SkillHooks } from "./skill.js"
|
||||
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Connection } from "@opencode-ai/schema/connection"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Provider } from "@opencode-ai/schema/provider"
|
||||
export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
import { Connection } from "@opencode-ai/schema/connection"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Reference } from "@opencode-ai/schema/reference"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
|
||||
const Plugin = await import("../src/v2/effect/index")
|
||||
const PromisePlugin = await import("../src/v2/promise/index")
|
||||
|
||||
test.each([
|
||||
["effect", Plugin],
|
||||
["promise", PromisePlugin],
|
||||
])("%s entrypoint exposes its canonical Schema contracts", (_, entrypoint) => {
|
||||
expect(entrypoint.Agent).toBe(Agent)
|
||||
expect(entrypoint.Command).toBe(Command)
|
||||
expect(entrypoint.Connection).toBe(Connection)
|
||||
expect(entrypoint.Credential).toBe(Credential)
|
||||
expect(entrypoint.Integration).toBe(Integration)
|
||||
expect(entrypoint.Model).toBe(Model)
|
||||
expect(entrypoint.Provider).toBe(Provider)
|
||||
expect(entrypoint.Reference).toBe(Reference)
|
||||
expect(entrypoint.Skill).toBe(Skill)
|
||||
expect(Object.keys(entrypoint).sort()).toEqual([
|
||||
"Agent",
|
||||
"Command",
|
||||
"Connection",
|
||||
"Credential",
|
||||
"Integration",
|
||||
"Model",
|
||||
"Provider",
|
||||
"Reference",
|
||||
"Skill",
|
||||
"define",
|
||||
])
|
||||
})
|
||||
@@ -4,9 +4,9 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"module": "nodenext",
|
||||
"module": "esnext",
|
||||
"declaration": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["es2022", "dom", "dom.iterable"]
|
||||
},
|
||||
"include": ["src"]
|
||||
|
||||
@@ -34,6 +34,13 @@
|
||||
- Use flat top-level exports plus the package's existing namespace projection pattern, for example `export * as SessionMessage from "./session-message"`.
|
||||
- Keep standalone ID modules only when they prevent real cycles or heavy dependency edges. Inline one-off IDs into their owning contract module when no cycle exists.
|
||||
|
||||
## Public Re-exports
|
||||
|
||||
- Public leaf packages re-export the canonical domain namespaces used by their APIs so consumers install and version one package. Keep the list curated by public API reachability rather than mirroring the Schema root.
|
||||
- Re-export directly from `@opencode-ai/schema/<domain>` and preserve the exact canonical values and types. Do not wrap schemas, duplicate definitions, or route exports through another facade package.
|
||||
- Internal packages import contracts directly from their canonical Schema owner. Import a Core domain facade only when runtime behavior from that facade is required.
|
||||
- Resolve name collisions by retaining the domain namespace and qualifying its members, such as `Session.Info` and `Agent.Info`. Do not introduce aliases such as `SessionSchema`, `AgentSchema`, or `ModelSchema`.
|
||||
|
||||
## Naming
|
||||
|
||||
- Exported schema values and namespace objects use `PascalCase`.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
|
||||
@@ -2,16 +2,25 @@ export * as OpenCode from "./opencode"
|
||||
export * as Tool from "./tool"
|
||||
|
||||
export { ClientError } from "@opencode-ai/client/effect"
|
||||
export {
|
||||
AbsolutePath,
|
||||
Agent,
|
||||
Location,
|
||||
Model,
|
||||
Prompt,
|
||||
Provider,
|
||||
RelativePath,
|
||||
Session,
|
||||
SessionInput,
|
||||
SessionMessage,
|
||||
} from "@opencode-ai/client/effect"
|
||||
export type { OpenCodeEvent } from "@opencode-ai/client/effect"
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Location } from "@opencode-ai/schema/location"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Permission } from "@opencode-ai/schema/permission"
|
||||
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||
export { Project } from "@opencode-ai/schema/project"
|
||||
export { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
||||
export { Prompt } from "@opencode-ai/schema/prompt"
|
||||
export { Provider } from "@opencode-ai/schema/provider"
|
||||
export { Pty } from "@opencode-ai/schema/pty"
|
||||
export { Question } from "@opencode-ai/schema/question"
|
||||
export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
||||
export { Session } from "@opencode-ai/schema/session"
|
||||
export { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
|
||||
const SDK = await import("../src/index")
|
||||
|
||||
test("re-exports canonical contracts directly from Schema", () => {
|
||||
expect(SDK.Agent).toBe(Agent)
|
||||
expect(SDK.Model).toBe(Model)
|
||||
expect(SDK.Session).toBe(Session)
|
||||
expect(Object.keys(SDK).sort()).toEqual([
|
||||
"AbsolutePath",
|
||||
"Agent",
|
||||
"ClientError",
|
||||
"Command",
|
||||
"Credential",
|
||||
"FileSystem",
|
||||
"Integration",
|
||||
"Location",
|
||||
"Model",
|
||||
"OpenCode",
|
||||
"Permission",
|
||||
"PermissionSaved",
|
||||
"Project",
|
||||
"ProjectCopy",
|
||||
"Prompt",
|
||||
"Provider",
|
||||
"Pty",
|
||||
"Question",
|
||||
"Reference",
|
||||
"RelativePath",
|
||||
"Session",
|
||||
"SessionInput",
|
||||
"SessionMessage",
|
||||
"Skill",
|
||||
"Tool",
|
||||
])
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -121,20 +121,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li:has(> input[type="checkbox"]),
|
||||
li:has(> p > input[type="checkbox"]) {
|
||||
position: relative;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li > input[type="checkbox"],
|
||||
li > p > input[type="checkbox"] {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: -20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li > p + p {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -210,30 +210,25 @@ const en = {
|
||||
"model.totalModels": "{{count}} models",
|
||||
"model.momentum": "Momentum",
|
||||
"model.vsPreviousWindow": "vs previous window",
|
||||
"model.usageDescription": "Daily token volume.",
|
||||
"model.usageDescription": "Daily OpenCode Go token volume over the recent two-month window.",
|
||||
"model.noUsageTitle": "No usage",
|
||||
"model.noUsageDescription": "No usage landed in the current window.",
|
||||
"model.dailyTokenChart": "Daily token usage chart",
|
||||
"model.usersDescription": "Daily unique users.",
|
||||
"model.usersDescription": "Daily unique OpenCode Go users over the recent two-month window.",
|
||||
"model.noUsersTitle": "No user data",
|
||||
"model.noUsersDescription": "No user-bearing rows landed in the current window.",
|
||||
"model.dailyUserChart": "Daily unique user chart",
|
||||
"model.efficiencyDescription": "Cost, cache behavior, and average session shape.",
|
||||
"model.efficiencyDescription": "Cost, cache behavior, and average OpenCode Go session shape.",
|
||||
"model.noEfficiencyTitle": "No efficiency data",
|
||||
"model.noEfficiencyDescription": "Efficiency data appears after usage lands.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.cost": "Cost",
|
||||
"model.totalSpend": "total spend",
|
||||
"model.costPerMillion": "Cost / 1M",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.inputOutput": "input / output",
|
||||
"model.observedTokens": "observed all tokens",
|
||||
"model.costSession": "Cost / Session",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.average": "average",
|
||||
"model.tokensSession": "Tokens / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cacheRatio": "Cache Ratio",
|
||||
"model.inputTokens": "input tokens",
|
||||
"model.geoDescription": "OpenCode Go model tokens used by country.",
|
||||
|
||||
@@ -202,11 +202,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "التكلفة، وسلوك التخزين المؤقت، ومتوسط شكل جلسة OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "لا توجد بيانات كفاءة",
|
||||
"model.noEfficiencyDescription": "تظهر بيانات الكفاءة بعد وصول الاستخدام.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "التكلفة",
|
||||
"model.totalSpend": "إجمالي الإنفاق",
|
||||
"model.costPerMillion": "التكلفة / 1M",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Custo, comportamento de cache e formato médio da sessão do OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Sem dados de eficiência",
|
||||
"model.noEfficiencyDescription": "Os dados de eficiência aparecem depois que o uso chega.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Custo",
|
||||
"model.totalSpend": "gasto total",
|
||||
"model.costPerMillion": "Custo / 1M",
|
||||
|
||||
@@ -203,11 +203,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Omkostning, cacheadfærd og gennemsnitlig OpenCode Go-sessionsform.",
|
||||
"model.noEfficiencyTitle": "Ingen effektivitetsdata",
|
||||
"model.noEfficiencyDescription": "Effektivitetsdata vises, når brug lander.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Omkostning",
|
||||
"model.totalSpend": "samlet forbrug",
|
||||
"model.costPerMillion": "Omkostning / 1 mio.",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Kosten, Cache-Verhalten und durchschnittliche OpenCode Go-Sitzungsform.",
|
||||
"model.noEfficiencyTitle": "Keine Effizienzdaten",
|
||||
"model.noEfficiencyDescription": "Effizienzdaten erscheinen, nachdem Nutzung eingegangen ist.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Kosten",
|
||||
"model.totalSpend": "Gesamtausgaben",
|
||||
"model.costPerMillion": "Kosten / 1 Mio.",
|
||||
|
||||
@@ -203,11 +203,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Coste, comportamiento de caché y forma media de sesión de OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Sin datos de eficiencia",
|
||||
"model.noEfficiencyDescription": "Los datos de eficiencia aparecen después de que llegue uso.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Coste",
|
||||
"model.totalSpend": "gasto total",
|
||||
"model.costPerMillion": "Coste / 1 M",
|
||||
|
||||
@@ -205,11 +205,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Coût, comportement du cache et forme moyenne des sessions OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Aucune donnée d'efficacité",
|
||||
"model.noEfficiencyDescription": "Les données d'efficacité apparaissent après l'arrivée de l'utilisation.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Coût",
|
||||
"model.totalSpend": "dépense totale",
|
||||
"model.costPerMillion": "Coût / 1 M",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Costi, comportamento della cache e forma media della sessione OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Nessun dato di efficienza",
|
||||
"model.noEfficiencyDescription": "I dati di efficienza appaiono dopo l'arrivo dell'utilizzo.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Costo",
|
||||
"model.totalSpend": "spesa totale",
|
||||
"model.costPerMillion": "Costo / 1 M",
|
||||
|
||||
@@ -205,11 +205,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "コスト、キャッシュ動作、平均的なOpenCode Goセッションの形。",
|
||||
"model.noEfficiencyTitle": "効率データがありません",
|
||||
"model.noEfficiencyDescription": "使用量が届くと効率データが表示されます。",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "コスト",
|
||||
"model.totalSpend": "総支出",
|
||||
"model.costPerMillion": "100万あたりのコスト",
|
||||
|
||||
@@ -205,11 +205,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "비용, 캐시 동작, 평균 OpenCode Go 세션 형태입니다.",
|
||||
"model.noEfficiencyTitle": "효율 데이터 없음",
|
||||
"model.noEfficiencyDescription": "사용량이 들어오면 효율 데이터가 표시됩니다.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "비용",
|
||||
"model.totalSpend": "총 지출",
|
||||
"model.costPerMillion": "100만당 비용",
|
||||
|
||||
@@ -203,11 +203,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Kostnad, cacheatferd og gjennomsnittlig OpenCode Go-øktform.",
|
||||
"model.noEfficiencyTitle": "Ingen effektivitetsdata",
|
||||
"model.noEfficiencyDescription": "Effektivitetsdata vises etter at bruk lander.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Kostnad",
|
||||
"model.totalSpend": "totalt forbruk",
|
||||
"model.costPerMillion": "Kostnad / 1 mill.",
|
||||
|
||||
@@ -202,11 +202,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Koszt, zachowanie cache i średni kształt sesji OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Brak danych wydajności",
|
||||
"model.noEfficiencyDescription": "Dane wydajności pojawią się po nadejściu użycia.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Koszt",
|
||||
"model.totalSpend": "łączne wydatki",
|
||||
"model.costPerMillion": "Koszt / 1 mln",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Стоимость, поведение кэша и средняя форма сеанса OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Нет данных эффективности",
|
||||
"model.noEfficiencyDescription": "Данные эффективности появятся после использования.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Стоимость",
|
||||
"model.totalSpend": "общие расходы",
|
||||
"model.costPerMillion": "Стоимость / 1 млн",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "ต้นทุน พฤติกรรมแคช และรูปแบบเซสชัน OpenCode Go โดยเฉลี่ย",
|
||||
"model.noEfficiencyTitle": "ไม่มีข้อมูลประสิทธิภาพ",
|
||||
"model.noEfficiencyDescription": "ข้อมูลประสิทธิภาพจะแสดงหลังจากมีการใช้งานเข้ามา",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "ต้นทุน",
|
||||
"model.totalSpend": "ยอดใช้จ่ายรวม",
|
||||
"model.costPerMillion": "ต้นทุน / 1 ล้าน",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Maliyet, önbellek davranışı ve ortalama OpenCode Go oturum yapısı.",
|
||||
"model.noEfficiencyTitle": "Verimlilik verisi yok",
|
||||
"model.noEfficiencyDescription": "Verimlilik verileri kullanım geldikten sonra görünür.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Maliyet",
|
||||
"model.totalSpend": "toplam harcama",
|
||||
"model.costPerMillion": "Maliyet / 1M",
|
||||
|
||||
@@ -204,11 +204,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "Вартість, поведінка кешу та середня форма сеансу OpenCode Go.",
|
||||
"model.noEfficiencyTitle": "Немає даних ефективності",
|
||||
"model.noEfficiencyDescription": "Дані ефективності з'являться після використання.",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "Вартість",
|
||||
"model.totalSpend": "загальні витрати",
|
||||
"model.costPerMillion": "Вартість / 1 млн",
|
||||
|
||||
@@ -203,11 +203,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "成本、缓存行为和平均 OpenCode Go 会话形态。",
|
||||
"model.noEfficiencyTitle": "无效率数据",
|
||||
"model.noEfficiencyDescription": "使用量到达后会显示效率数据。",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "成本",
|
||||
"model.totalSpend": "总支出",
|
||||
"model.costPerMillion": "每 100 万成本",
|
||||
|
||||
@@ -203,11 +203,6 @@ export const dict = {
|
||||
"model.efficiencyDescription": "成本、快取行為和平均 OpenCode Go 工作階段形態。",
|
||||
"model.noEfficiencyTitle": "無效率數據",
|
||||
"model.noEfficiencyDescription": "使用量到達後會顯示效率數據。",
|
||||
"model.totalSpendLabel": "Total spend",
|
||||
"model.costInput": "Cost input",
|
||||
"model.costOutput": "Cost output",
|
||||
"model.averageCostSession": "Average cost / Session",
|
||||
"model.averageTokensSession": "Average tokens / session",
|
||||
"model.cost": "成本",
|
||||
"model.totalSpend": "總支出",
|
||||
"model.costPerMillion": "每 100 萬成本",
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type UsageRange,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
|
||||
import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
formatCatalogLabName,
|
||||
getModelCatalog,
|
||||
type ModelCatalog,
|
||||
type ModelCatalogCost,
|
||||
type ModelCatalogEntry,
|
||||
} from "../model-catalog"
|
||||
import { SectionHeading } from "../section-heading"
|
||||
@@ -120,8 +121,6 @@ export default function StatsModel() {
|
||||
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
||||
{ href: "#overview", label: i18n.t("nav.overview") },
|
||||
{ href: "#momentum", label: i18n.t("model.momentum") },
|
||||
{ href: "#usage", label: i18n.t("nav.usage") },
|
||||
{ href: "#unique-users", label: i18n.t("model.uniqueUsers") },
|
||||
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
|
||||
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
|
||||
{ href: "#peers", label: i18n.t("nav.peers") },
|
||||
@@ -184,8 +183,6 @@ export default function StatsModel() {
|
||||
/>
|
||||
<ModelOverview catalog={catalogEntry() ?? null} />
|
||||
<ModelMomentumSection data={stats() ?? null} />
|
||||
<ModelUsageSection data={stats() ?? null} />
|
||||
<ModelUniqueUsersSection data={stats() ?? null} />
|
||||
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||
<ModelPeersSection data={stats() ?? null} />
|
||||
@@ -564,303 +561,6 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUsageSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<ModelTrendSection
|
||||
data={props.data}
|
||||
id="usage"
|
||||
title={i18n.t("nav.usage")}
|
||||
description={i18n.t("model.usageDescription")}
|
||||
ariaLabel={i18n.t("model.dailyTokenChart")}
|
||||
emptyTitle={i18n.t("model.noUsageTitle")}
|
||||
emptyDescription={i18n.t("model.noUsageDescription")}
|
||||
value={(point) => point.tokens}
|
||||
formatValue={formatTokens}
|
||||
valueUnit={i18n.t("lab.tokens")}
|
||||
rowLabel={i18n.t("lab.dailyTokens")}
|
||||
lineTone="muted"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<ModelTrendSection
|
||||
data={props.data}
|
||||
id="unique-users"
|
||||
title={i18n.t("model.uniqueUsers")}
|
||||
description={i18n.t("model.usersDescription")}
|
||||
ariaLabel={i18n.t("model.dailyUserChart")}
|
||||
emptyTitle={i18n.t("model.noUsersTitle")}
|
||||
emptyDescription={i18n.t("model.noUsersDescription")}
|
||||
value={(point) => point.users}
|
||||
formatValue={formatUsers}
|
||||
valueUnit={i18n.t("format.users")}
|
||||
rowLabel={i18n.t("model.uniqueUsers")}
|
||||
lineTone="active"
|
||||
activeLineBaseTone="muted"
|
||||
highlightBars={false}
|
||||
area
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelTrendSection(props: {
|
||||
data: StatsModelData | null
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
ariaLabel: string
|
||||
emptyTitle: string
|
||||
emptyDescription: string
|
||||
value: (point: ModelUsagePoint) => number
|
||||
formatValue: (value: number) => string
|
||||
valueUnit: string
|
||||
rowLabel: string
|
||||
lineTone: "muted" | "active"
|
||||
activeLineBaseTone?: "muted" | "active"
|
||||
highlightBars?: boolean
|
||||
area?: boolean
|
||||
}) {
|
||||
const activeLineClipId = createUniqueId()
|
||||
const activeLineMaskId = createUniqueId()
|
||||
const areaGradientId = createUniqueId()
|
||||
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||
const highlightBars = createMemo(() => props.highlightBars ?? true)
|
||||
const lineScale = 326 / 450
|
||||
const areaBottom = 100 / lineScale
|
||||
const usage = createMemo(() => props.data?.usage ?? [])
|
||||
const valueMax = createMemo(() => Math.max(0, ...usage().map((item) => props.value(item))) || 1)
|
||||
const linePoints = createMemo(() =>
|
||||
usage().map((point, index) => ({
|
||||
point,
|
||||
x: modelUsagePointX(index, usage().length),
|
||||
y: modelUsageLineY(props.value(point), valueMax()),
|
||||
})),
|
||||
)
|
||||
const linePath = createMemo(() => modelUsageLinePath(linePoints()))
|
||||
const areaPath = createMemo(() => modelUsageAreaPath(linePoints(), areaBottom))
|
||||
const activeLineBreak = createMemo(() => {
|
||||
const index = activeIndex()
|
||||
if (index === undefined || linePoints().length < 2) return undefined
|
||||
return modelUsageColumnBounds(index, linePoints().length)
|
||||
})
|
||||
const activeLineClip = createMemo(() => {
|
||||
const index = activeIndex()
|
||||
if (index === undefined || linePoints().length < 2) return undefined
|
||||
return modelUsageColumnInnerBounds(index, linePoints().length)
|
||||
})
|
||||
const activeTooltip = createMemo(() => {
|
||||
const index = activeIndex()
|
||||
const point = index === undefined ? undefined : usage()[index]
|
||||
if (index === undefined || !point) return undefined
|
||||
const bounds = modelUsageColumnBounds(index, usage().length)
|
||||
return {
|
||||
bounds,
|
||||
index,
|
||||
point,
|
||||
tooltipY: (linePoints()[index]?.y ?? 100) * lineScale,
|
||||
}
|
||||
})
|
||||
const monthTicks = createMemo(() => modelUsageMonthTicks(usage(), props.data?.updatedAt ?? null))
|
||||
|
||||
return (
|
||||
<section id={props.id} data-section="model-panel">
|
||||
<SectionTitle href={`#${props.id}`} title={props.title} description={props.description} />
|
||||
<Show
|
||||
when={usage().some((item) => props.value(item) > 0)}
|
||||
fallback={<ModelEmptyState title={props.emptyTitle} description={props.emptyDescription} />}
|
||||
>
|
||||
<div
|
||||
data-component="model-usage-chart"
|
||||
data-variant="model-trend"
|
||||
data-highlight-bars={highlightBars() ? "true" : undefined}
|
||||
role="img"
|
||||
aria-label={props.ariaLabel}
|
||||
style={{ "--model-usage-count": usage().length } as JSX.CSSProperties}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
setActiveIndex(undefined)
|
||||
}}
|
||||
>
|
||||
<div data-slot="model-trend-plot">
|
||||
<Show when={linePath()}>
|
||||
{(path) => (
|
||||
<>
|
||||
<Show when={props.area && areaPath()}>
|
||||
{(area) => (
|
||||
<svg
|
||||
data-slot="model-trend-area-layer"
|
||||
viewBox={`0 0 100 ${formatUsagePathNumber(areaBottom)}`}
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={areaGradientId} x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="var(--model-trend-active)" stop-opacity="0.1" />
|
||||
<stop offset="66%" stop-color="var(--model-trend-active)" stop-opacity="0.045" />
|
||||
<stop offset="100%" stop-color="var(--model-trend-active)" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path data-slot="model-trend-area" d={area()} fill={`url(#${areaGradientId})`} />
|
||||
</svg>
|
||||
)}
|
||||
</Show>
|
||||
<svg
|
||||
data-slot="model-trend-line"
|
||||
data-layer="base"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Show
|
||||
when={activeLineBreak()}
|
||||
fallback={<path data-slot="model-trend-line-base" data-tone={props.lineTone} d={path()} />}
|
||||
>
|
||||
{(lineBreak) => (
|
||||
<>
|
||||
<defs>
|
||||
<mask id={activeLineMaskId} maskUnits="userSpaceOnUse">
|
||||
<rect x="0" y="-2" width="100" height="104" fill="white" />
|
||||
<rect x={lineBreak().x} y="-2" width={lineBreak().width} height="104" fill="black" />
|
||||
</mask>
|
||||
</defs>
|
||||
<path
|
||||
data-slot="model-trend-line-base"
|
||||
data-tone={props.activeLineBaseTone ?? props.lineTone}
|
||||
d={path()}
|
||||
mask={`url(#${activeLineMaskId})`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</svg>
|
||||
<Show when={activeLineClip()}>
|
||||
{(clip) => (
|
||||
<svg
|
||||
data-slot="model-trend-line"
|
||||
data-layer="active"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id={activeLineClipId} clipPathUnits="userSpaceOnUse">
|
||||
<rect x={clip().x} y="-2" width={clip().width} height="104" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path data-slot="model-trend-line-active" d={path()} clip-path={`url(#${activeLineClipId})`} />
|
||||
</svg>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={linePoints().at(-1)}>
|
||||
{(point) => (
|
||||
<span
|
||||
data-slot="model-trend-end-marker"
|
||||
data-tone={props.lineTone}
|
||||
aria-hidden="true"
|
||||
style={
|
||||
{
|
||||
"--model-trend-end-x": `${point().x}%`,
|
||||
"--model-trend-end-top": `${point().y * lineScale}%`,
|
||||
} as JSX.CSSProperties
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<div data-slot="model-trend-bars">
|
||||
<For each={usage()}>
|
||||
{(point, index) => (
|
||||
<div
|
||||
data-slot="model-trend-column"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${point.date} ${props.formatValue(props.value(point))} ${props.valueUnit}`}
|
||||
data-active={activeIndex() === index() ? "true" : undefined}
|
||||
data-muted={
|
||||
highlightBars() && activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined
|
||||
}
|
||||
style={
|
||||
{
|
||||
"--model-trend-token-height": modelUsageStripHeight(props.value(point), valueMax()),
|
||||
} as JSX.CSSProperties
|
||||
}
|
||||
onPointerDown={(event) => {
|
||||
if (event.pointerType !== "touch") return
|
||||
setActiveIndex(index())
|
||||
}}
|
||||
onPointerEnter={() => setActiveIndex(index())}
|
||||
onPointerMove={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
setActiveIndex(index())
|
||||
}}
|
||||
onClick={() => setActiveIndex(index())}
|
||||
onFocus={() => setActiveIndex(index())}
|
||||
onBlur={() => setActiveIndex(undefined)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
setActiveIndex(index())
|
||||
}}
|
||||
>
|
||||
<div data-slot="model-trend-token-band">
|
||||
<div data-slot="model-trend-token-bar" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={activeTooltip()} keyed>
|
||||
{(active) => (
|
||||
<div
|
||||
data-component="chart-tooltip"
|
||||
data-placement={active.index > usage().length * 0.62 ? "left" : "right"}
|
||||
style={
|
||||
{
|
||||
"--model-trend-tooltip-left": `${active.bounds.x}%`,
|
||||
"--model-trend-tooltip-right": `${active.bounds.x + active.bounds.width}%`,
|
||||
"--model-trend-tooltip-y": `${active.tooltipY}`,
|
||||
} as JSX.CSSProperties
|
||||
}
|
||||
>
|
||||
<strong>{formatModelUsageTooltipDate(active.point.date, props.data?.updatedAt ?? null)}</strong>
|
||||
<span>
|
||||
{props.formatValue(props.value(active.point))} {props.valueUnit}
|
||||
</span>
|
||||
<div data-slot="tooltip-divider" />
|
||||
<p>
|
||||
<span data-slot="tooltip-label">
|
||||
<i data-kind={props.lineTone === "active" ? "users" : "tokens"} /> {props.rowLabel}
|
||||
</span>
|
||||
<b>{props.formatValue(props.value(active.point))}</b>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div data-slot="model-trend-months" aria-hidden="true">
|
||||
<For each={monthTicks()}>
|
||||
{(tick) => (
|
||||
<span
|
||||
data-align={tick.align}
|
||||
style={{ "--model-trend-month-left": `${tick.left}%` } as JSX.CSSProperties}
|
||||
>
|
||||
{tick.label}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
@@ -880,32 +580,35 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M
|
||||
}
|
||||
>
|
||||
{(data) => (
|
||||
<>
|
||||
<div data-slot="model-efficiency-pattern" aria-hidden="true" />
|
||||
<div data-component="model-efficiency-grid">
|
||||
<MetricCard label={i18n.t("model.totalSpendLabel")} value={formatMoney(data().totals.cost)} />
|
||||
<MetricCard
|
||||
label={i18n.t("model.costInput")}
|
||||
value={formatCatalogUnitPrice(props.catalog?.cost?.input)}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.costOutput")}
|
||||
value={formatCatalogUnitPrice(props.catalog?.cost?.output)}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.averageCostSession")}
|
||||
value={formatSessionCost(data().totals.costPerSession)}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.averageTokensSession")}
|
||||
value={formatTokens(data().totals.tokensPerSession)}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.cacheRatio")}
|
||||
value={`${formatPercent(data().totals.cacheRatio)} ${i18n.t("model.inputTokens")}`}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div data-component="model-metric-grid" data-variant="dense">
|
||||
<MetricCard
|
||||
label={i18n.t("model.cost")}
|
||||
value={formatMoney(data().totals.cost)}
|
||||
detail={i18n.t("model.totalSpend")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.costPerMillion")}
|
||||
value={
|
||||
props.catalog?.cost ? formatCatalogPrice(props.catalog.cost) : formatMoney(data().totals.costPerMillion)
|
||||
}
|
||||
detail={props.catalog?.cost ? i18n.t("model.inputOutput") : i18n.t("model.observedTokens")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.costSession")}
|
||||
value={formatSessionCost(data().totals.costPerSession)}
|
||||
detail={i18n.t("model.average")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.tokensSession")}
|
||||
value={formatTokens(data().totals.tokensPerSession)}
|
||||
detail={i18n.t("model.average")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.cacheRatio")}
|
||||
value={formatPercent(data().totals.cacheRatio)}
|
||||
detail={i18n.t("model.inputTokens")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
@@ -1125,12 +828,12 @@ function ModelPeersSection(props: { data: StatsModelData | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard(props: { label: string; value: string; detail?: string; state?: "positive" | "negative" }) {
|
||||
function MetricCard(props: { label: string; value: string; detail: string; state?: "positive" | "negative" }) {
|
||||
return (
|
||||
<article data-component="model-metric" data-state={props.state}>
|
||||
<span>{props.label}</span>
|
||||
<strong>{props.value}</strong>
|
||||
<Show when={props.detail}>{(detail) => <p>{detail()}</p>}</Show>
|
||||
<p>{props.detail}</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1143,16 +846,10 @@ function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) {
|
||||
href={language.route(`${import.meta.env.BASE_URL}${providerSlug(props.peer.provider)}/${props.peer.slug}`)}
|
||||
data-active={props.active ? "true" : undefined}
|
||||
>
|
||||
<span data-slot="model-peer-rank" aria-label={props.active ? `Rank ${props.peer.rank}` : undefined}>
|
||||
<Show when={!props.active}>{String(props.peer.rank).padStart(2, "0")}</Show>
|
||||
</span>
|
||||
<span data-slot="model-peer-avatar">
|
||||
<ProviderIcon aria-hidden="true" id={getProviderIconId(props.peer.author)} />
|
||||
</span>
|
||||
<span data-slot="model-peer-copy">
|
||||
<strong>{props.peer.model}</strong>
|
||||
<em>{props.peer.author}</em>
|
||||
</span>
|
||||
<span>{String(props.peer.rank).padStart(2, "0")}</span>
|
||||
<ProviderIcon aria-hidden="true" id={getProviderIconId(props.peer.author)} />
|
||||
<strong>{props.peer.model}</strong>
|
||||
<em>{props.peer.author}</em>
|
||||
<b>{formatTokens(props.peer.tokens)}</b>
|
||||
</a>
|
||||
</li>
|
||||
@@ -1317,101 +1014,6 @@ function formatMomentumDateLabel(date: string) {
|
||||
return `${shortMonths[parsed.getUTCMonth()]} ${parsed.getUTCDate()}`
|
||||
}
|
||||
|
||||
function modelUsageStripHeight(value: number, max: number) {
|
||||
if (value <= 0 || max <= 0) return "0px"
|
||||
return `max(2px, ${(value / max) * 100}%)`
|
||||
}
|
||||
|
||||
function modelUsageLineY(value: number, max: number) {
|
||||
if (value <= 0 || max <= 0) return 100
|
||||
return Math.max(0, 100 - (value / max) * 100)
|
||||
}
|
||||
|
||||
function modelUsagePointX(index: number, count: number) {
|
||||
if (count <= 1) return 50
|
||||
return ((index + 0.5) / count) * 100
|
||||
}
|
||||
|
||||
function modelUsageColumnBounds(index: number, count: number) {
|
||||
if (count <= 0) return { x: 0, width: 100 }
|
||||
return { x: (index / count) * 100, width: 100 / count }
|
||||
}
|
||||
|
||||
function modelUsageColumnInnerBounds(index: number, count: number) {
|
||||
const bounds = modelUsageColumnBounds(index, count)
|
||||
const inset = Math.min(bounds.width * 0.1, 0.24)
|
||||
return { x: bounds.x + inset, width: Math.max(0.001, bounds.width - inset * 2) }
|
||||
}
|
||||
|
||||
type ModelUsageLinePoint = { x: number; y: number }
|
||||
|
||||
function modelUsageLinePath(points: ModelUsageLinePoint[]) {
|
||||
if (points.length === 0) return ""
|
||||
if (points.length === 1) return `M ${formatUsagePathNumber(points[0].x)} ${formatUsagePathNumber(points[0].y)}`
|
||||
|
||||
return points.slice(0, -1).reduce(
|
||||
(path, point, index) => {
|
||||
const next = points[index + 1]
|
||||
const previous = points[index - 1] ?? point
|
||||
const afterNext = points[index + 2] ?? next
|
||||
const controlA = {
|
||||
x: point.x + (next.x - previous.x) / 6,
|
||||
y: clampUsagePercent(point.y + (next.y - previous.y) / 6),
|
||||
}
|
||||
const controlB = {
|
||||
x: next.x - (afterNext.x - point.x) / 6,
|
||||
y: clampUsagePercent(next.y - (afterNext.y - point.y) / 6),
|
||||
}
|
||||
return `${path} C ${formatUsagePathNumber(controlA.x)} ${formatUsagePathNumber(controlA.y)}, ${formatUsagePathNumber(controlB.x)} ${formatUsagePathNumber(controlB.y)}, ${formatUsagePathNumber(next.x)} ${formatUsagePathNumber(next.y)}`
|
||||
},
|
||||
`M ${formatUsagePathNumber(points[0].x)} ${formatUsagePathNumber(points[0].y)}`,
|
||||
)
|
||||
}
|
||||
|
||||
function modelUsageAreaPath(points: ModelUsageLinePoint[], bottom: number) {
|
||||
const path = modelUsageLinePath(points)
|
||||
if (!path || points.length === 0) return ""
|
||||
const first = points[0]
|
||||
const last = points[points.length - 1]
|
||||
return `${path} L ${formatUsagePathNumber(last.x)} ${formatUsagePathNumber(bottom)} L ${formatUsagePathNumber(first.x)} ${formatUsagePathNumber(bottom)} Z`
|
||||
}
|
||||
|
||||
function formatUsagePathNumber(value: number) {
|
||||
return Number(value.toFixed(3))
|
||||
}
|
||||
|
||||
function clampUsagePercent(value: number) {
|
||||
return Math.max(0, Math.min(100, value))
|
||||
}
|
||||
|
||||
function modelUsageMonthTicks(points: ModelUsagePoint[], updatedAt: string | null) {
|
||||
const seen = new Set<string>()
|
||||
return points.flatMap((point, index) => {
|
||||
const parsed = parseMomentumDate(point.date, updatedAt)
|
||||
const label = shortMonths[parsed.getUTCMonth()]
|
||||
if (!label || seen.has(label)) return []
|
||||
seen.add(label)
|
||||
return [
|
||||
{
|
||||
label,
|
||||
left: modelUsagePointX(index, points.length),
|
||||
align: index === 0 ? "start" : index >= points.length - 2 ? "end" : "center",
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function formatModelUsageTooltipDate(value: string, updatedAt: string | null) {
|
||||
const date = parseMomentumDate(value, updatedAt)
|
||||
if (date.getUTCFullYear() === 1970) return value
|
||||
return new Intl.DateTimeFormat("en", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function formatRankLabel(rank: number | null) {
|
||||
if (rank === null) return "--"
|
||||
return `#${String(rank).padStart(2, "0")}`
|
||||
@@ -1519,9 +1121,8 @@ function formatMoney(value: number) {
|
||||
return `$${value.toFixed(value >= 10 ? 0 : 2)}`
|
||||
}
|
||||
|
||||
function formatCatalogUnitPrice(value: number | undefined) {
|
||||
if (value === undefined) return "-"
|
||||
return `${formatModelPrice(value)} / 1M`
|
||||
function formatCatalogPrice(value: ModelCatalogCost) {
|
||||
return `${formatModelPrice(value.input)} / ${formatModelPrice(value.output)}`
|
||||
}
|
||||
|
||||
function formatModelPrice(value: number) {
|
||||
|
||||
@@ -3913,50 +3913,6 @@
|
||||
color: #b82d35;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-efficiency-pattern"] {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin-bottom: 40px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--stats-text) 10%, transparent);
|
||||
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-position: center top;
|
||||
mask-repeat: repeat;
|
||||
mask-size: 6px 6px;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-position: center top;
|
||||
-webkit-mask-repeat: repeat;
|
||||
-webkit-mask-size: 6px 6px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
gap: 0;
|
||||
height: 120px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
background: var(--stats-layer);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] span {
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] strong {
|
||||
margin-top: 32px;
|
||||
font-size: 28px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-panel"][data-variant="lab-models"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -4915,254 +4871,9 @@
|
||||
background: var(--lab-usage-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] {
|
||||
--model-trend-gap: 4px;
|
||||
--model-trend-column-gutter: calc(var(--model-trend-gap) / 2);
|
||||
--model-trend-bar-height: 76px;
|
||||
--model-trend-line-bottom: 124px;
|
||||
--model-trend-bar: #dbdbdb;
|
||||
--model-trend-line: #808080;
|
||||
--model-trend-active: #3b5cf6;
|
||||
--model-trend-band: #f2f2f2;
|
||||
grid-template-rows: minmax(0, 450px) 14px;
|
||||
gap: 40px;
|
||||
height: 504px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-plot"] {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-plot"]::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(to bottom, transparent 42%, color-mix(in srgb, var(--stats-text) 3%, transparent));
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: calc(100% - var(--model-trend-line-bottom));
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line"][data-layer="base"] {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line"][data-layer="active"] {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-area-layer"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-area-layer"] [data-slot="model-trend-area"] {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line"] path {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.5;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line-base"] {
|
||||
stroke: var(--model-trend-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line-base"][data-tone="active"] {
|
||||
stroke: var(--model-trend-active);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-line-active"] {
|
||||
stroke: var(--model-trend-active);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-end-marker"] {
|
||||
position: absolute;
|
||||
top: var(--model-trend-end-top);
|
||||
left: var(--model-trend-end-x);
|
||||
z-index: 3;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--model-trend-bar);
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-end-marker"][data-tone="active"] {
|
||||
background: var(--model-trend-active);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-bars"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-column"] {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-column"]::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-column"][data-active="true"]::before,
|
||||
[data-page="stats"] [data-slot="model-trend-column"]:focus-visible::before {
|
||||
background: var(--model-trend-band);
|
||||
box-shadow:
|
||||
inset var(--model-trend-column-gutter) 0 0 var(--stats-bg),
|
||||
inset calc(var(--model-trend-column-gutter) * -1) 0 0 var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-token-band"] {
|
||||
position: absolute;
|
||||
right: var(--model-trend-column-gutter);
|
||||
bottom: 0;
|
||||
left: var(--model-trend-column-gutter);
|
||||
z-index: 2;
|
||||
height: var(--model-trend-bar-height);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-token-bar"] {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: var(--model-trend-token-height);
|
||||
background: var(--model-trend-bar);
|
||||
transition:
|
||||
background 120ms ease,
|
||||
opacity 120ms ease;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-highlight-bars="true"]
|
||||
[data-slot="model-trend-column"][data-active="true"]
|
||||
[data-slot="model-trend-token-bar"] {
|
||||
background: var(--model-trend-active);
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-highlight-bars="true"]
|
||||
[data-slot="model-trend-column"][data-muted="true"]
|
||||
[data-slot="model-trend-token-bar"] {
|
||||
opacity: 0.64;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-months"] {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-months"] span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--model-trend-month-left);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-months"] span[data-align="start"] {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-trend-months"] span[data-align="end"] {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] {
|
||||
z-index: 6;
|
||||
top: clamp(12px, calc(var(--model-trend-tooltip-y) * 1% - 46px), calc(100% - 108px));
|
||||
width: 192px;
|
||||
min-width: 192px;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"][data-placement="right"] {
|
||||
right: auto;
|
||||
left: calc(var(--model-trend-tooltip-right) + 8px);
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"][data-placement="left"] {
|
||||
right: calc(100% - var(--model-trend-tooltip-left) + 8px);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"]
|
||||
p {
|
||||
height: 16px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"]
|
||||
i[data-kind="tokens"] {
|
||||
background: var(--model-trend-active);
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"]
|
||||
i[data-kind="users"] {
|
||||
background: var(--model-trend-active);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-template-rows: repeat(5, 64px);
|
||||
gap: 12px 40px;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
@@ -5170,12 +4881,11 @@
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] a {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 32px minmax(0, 1fr) minmax(64px, auto);
|
||||
gap: 16px;
|
||||
grid-template-columns: 38px 24px minmax(0, 1fr) minmax(96px, auto) minmax(80px, auto);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 64px;
|
||||
padding: 15px 16px;
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--stats-line);
|
||||
background: var(--stats-layer);
|
||||
color: var(--stats-text);
|
||||
@@ -5184,56 +4894,25 @@
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] a:hover,
|
||||
[data-page="stats"] [data-component="model-peer-list"] a[data-active="true"] {
|
||||
border-color: var(--stats-text);
|
||||
border-color: var(--stats-line-strong);
|
||||
background: var(--stats-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-peer-rank"],
|
||||
[data-page="stats"] [data-component="model-peer-list"] span,
|
||||
[data-page="stats"] [data-component="model-peer-list"] em,
|
||||
[data-page="stats"] [data-component="model-peer-list"] b {
|
||||
color: var(--stats-muted);
|
||||
font-size: 13px;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-peer-rank"] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] a[data-active="true"] [data-slot="model-peer-rank"]::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: "";
|
||||
background: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-peer-avatar"] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--stats-muted);
|
||||
background: var(--stats-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-peer-avatar"] svg {
|
||||
[data-page="stats"] [data-component="model-peer-list"] svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-peer-copy"] {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
color: var(--stats-muted);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] strong {
|
||||
@@ -5242,16 +4921,11 @@
|
||||
color: var(--stats-text);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] b {
|
||||
justify-self: end;
|
||||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"][data-theme="dark"],
|
||||
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) {
|
||||
color-scheme: dark;
|
||||
@@ -5292,15 +4966,6 @@
|
||||
--lab-usage-band: #242424;
|
||||
}
|
||||
|
||||
[data-page="stats"][data-theme="dark"] [data-component="model-usage-chart"][data-variant="model-trend"],
|
||||
:root[data-stats-theme="dark"]
|
||||
[data-page="stats"]:not([data-theme="light"])
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"] {
|
||||
--model-trend-bar: #3a3a3a;
|
||||
--model-trend-line: #a3a3a3;
|
||||
--model-trend-band: #242424;
|
||||
}
|
||||
|
||||
[data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"],
|
||||
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
|
||||
background: #242424f2;
|
||||
@@ -5466,14 +5131,6 @@
|
||||
--lab-usage-band: #242424;
|
||||
}
|
||||
|
||||
:root:not([data-stats-theme="light"])
|
||||
[data-page="stats"]:not([data-theme="light"])
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"] {
|
||||
--model-trend-bar: #3a3a3a;
|
||||
--model-trend-line: #a3a3a3;
|
||||
--model-trend-band: #242424;
|
||||
}
|
||||
|
||||
:root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
|
||||
background: #242424f2;
|
||||
}
|
||||
@@ -5572,27 +5229,15 @@
|
||||
[data-page="stats"][data-theme="dark"]
|
||||
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||
[data-component="chart-tooltip"],
|
||||
[data-page="stats"][data-theme="dark"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"],
|
||||
:root[data-stats-theme="dark"]
|
||||
[data-page="stats"]:not([data-theme="light"])
|
||||
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||
[data-component="chart-tooltip"],
|
||||
:root[data-stats-theme="dark"]
|
||||
[data-page="stats"]:not([data-theme="light"])
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"],
|
||||
:root:not([data-stats-theme="light"])
|
||||
[data-page="stats"]:not([data-theme="light"])
|
||||
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||
[data-component="chart-tooltip"],
|
||||
:root:not([data-stats-theme="light"])
|
||||
[data-page="stats"]:not([data-theme="light"])
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"],
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"],
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] {
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] {
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
@@ -5783,18 +5428,16 @@
|
||||
|
||||
[data-page="stats"] [data-component="model-metric-grid"],
|
||||
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"],
|
||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] a {
|
||||
grid-template-columns: 32px 32px minmax(0, 1fr) minmax(64px, auto);
|
||||
gap: 12px;
|
||||
grid-template-columns: 32px 22px minmax(0, 1fr) minmax(72px, auto);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] em {
|
||||
display: block;
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="chart-footer"] {
|
||||
@@ -5871,13 +5514,6 @@
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"] {
|
||||
position: absolute;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="footer"] {
|
||||
padding: 88px 24px 24px;
|
||||
}
|
||||
@@ -6211,7 +5847,6 @@
|
||||
|
||||
[data-page="stats"] [data-component="model-metric-grid"],
|
||||
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"],
|
||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -6220,15 +5855,6 @@
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] {
|
||||
height: 112px;
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] strong {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-usage-chart"] {
|
||||
--model-usage-mobile-bar-width: 16px;
|
||||
--model-usage-mobile-edge-space: 48px;
|
||||
@@ -6309,56 +5935,12 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] {
|
||||
--model-usage-mobile-bar-width: 20px;
|
||||
--model-usage-mobile-edge-space: 48px;
|
||||
--model-usage-mobile-track-width: calc(
|
||||
var(--model-usage-count) * var(--model-usage-mobile-bar-width) + var(--model-usage-mobile-edge-space)
|
||||
);
|
||||
--model-trend-bar-height: 64px;
|
||||
--model-trend-line-bottom: 104px;
|
||||
grid-template-rows: minmax(0, 360px) 14px;
|
||||
gap: 24px;
|
||||
height: 398px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-slot="model-trend-plot"],
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-slot="model-trend-months"] {
|
||||
direction: ltr;
|
||||
width: var(--model-usage-mobile-track-width);
|
||||
min-width: var(--model-usage-mobile-track-width);
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-slot="model-trend-column"] {
|
||||
flex: 0 0 var(--model-usage-mobile-bar-width);
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="model-usage-chart"][data-variant="model-trend"]
|
||||
[data-component="chart-tooltip"] {
|
||||
top: auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] {
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: auto;
|
||||
grid-template-rows: none;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] a {
|
||||
grid-template-columns: 28px 32px minmax(0, 1fr) minmax(56px, auto);
|
||||
gap: 12px;
|
||||
grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-peer-rank"] {
|
||||
width: 28px;
|
||||
[data-page="stats"] [data-component="model-peer-list"] svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -646,8 +646,8 @@ function buildModelTokenMix(aggregate: ModelAggregate): ModelMixEntry[] {
|
||||
}
|
||||
|
||||
function buildModelPeers(peers: ModelAggregate[], rank: number, totalTokens: number): ModelPeerEntry[] {
|
||||
const start = Math.max(0, Math.min(rank - 5, Math.max(peers.length - 10, 0)))
|
||||
return peers.slice(start, start + 10).map((item, index) => ({
|
||||
const start = Math.max(0, Math.min(rank - 4, Math.max(peers.length - 7, 0)))
|
||||
return peers.slice(start, start + 7).map((item, index) => ({
|
||||
model: item.model,
|
||||
provider: item.provider,
|
||||
author: formatProvider(item.provider),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.17.15",
|
||||
"version": "1.17.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -173,10 +173,7 @@ function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
|
||||
|
||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: (props: {
|
||||
defaultTheme?: string
|
||||
onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark", scheme: ColorScheme) => void
|
||||
}) => {
|
||||
init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => {
|
||||
const themeId = normalize(read(STORAGE_KEYS.THEME_ID) ?? props.defaultTheme) ?? "oc-2"
|
||||
const colorScheme = (read(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null) ?? "system"
|
||||
const mode = colorScheme === "system" ? getSystemMode() : colorScheme
|
||||
@@ -215,9 +212,9 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
return task
|
||||
}
|
||||
|
||||
const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark", scheme: ColorScheme) => {
|
||||
const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => {
|
||||
applyThemeCss(theme, themeId, mode)
|
||||
props.onThemeApplied?.(theme, mode, scheme)
|
||||
props.onThemeApplied?.(theme, mode)
|
||||
}
|
||||
|
||||
const ids = () => {
|
||||
@@ -281,7 +278,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
createEffect(() => {
|
||||
const theme = store.themes[store.themeId]
|
||||
if (!theme) return
|
||||
applyTheme(theme, store.themeId, store.mode, store.colorScheme)
|
||||
applyTheme(theme, store.themeId, store.mode)
|
||||
})
|
||||
|
||||
const setTheme = (id: string) => {
|
||||
@@ -336,7 +333,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
? getSystemMode()
|
||||
: store.previewScheme
|
||||
: store.mode
|
||||
applyTheme(theme, next, mode, store.previewScheme ?? store.colorScheme)
|
||||
applyTheme(theme, next, mode)
|
||||
})
|
||||
},
|
||||
previewColorScheme: (scheme: ColorScheme) => {
|
||||
@@ -347,7 +344,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
if (!theme) return
|
||||
if ((store.previewThemeId ?? store.themeId) !== id) return
|
||||
if (store.previewScheme !== scheme) return
|
||||
applyTheme(theme, id, mode, scheme)
|
||||
applyTheme(theme, id, mode)
|
||||
})
|
||||
},
|
||||
commitPreview: () => {
|
||||
@@ -365,7 +362,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
setStore("previewScheme", null)
|
||||
void load(store.themeId).then((theme) => {
|
||||
if (!theme) return
|
||||
applyTheme(theme, store.themeId, store.mode, store.colorScheme)
|
||||
applyTheme(theme, store.themeId, store.mode)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user