mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-30 06:36:08 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
366614537e | ||
|
|
154d1e7f70 | ||
|
|
8be2027a2a |
@@ -339,7 +339,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -632,7 +632,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -3855,7 +3855,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="],
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-uGan7MQaNfmV5SH9wiMJkZ7D5QxM4KVPRcjIHtPCM5icZjgXl5mrHMjr3/e7TAHa9OjEJKT0a2FxzPgwRtN3PQ=="],
|
||||
|
||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
|
||||
+2
-6
@@ -288,12 +288,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
server: {
|
||||
placement: { region: "aws:us-east-2" },
|
||||
transform: {
|
||||
worker: (args) => {
|
||||
args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [
|
||||
...(flags ?? []),
|
||||
"global_fetch_strictly_public",
|
||||
])
|
||||
args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }]
|
||||
worker: {
|
||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-Z/YgK5sf0L9MG7hMIb+wj3ias6iNo8L4oFHYNtRHreM=",
|
||||
"aarch64-linux": "sha256-MoYZmeh6RcDUN7a132wexFNwkHz+Q9Ijs+qiacuKJ90=",
|
||||
"aarch64-darwin": "sha256-OHo3x7UcJzrPMAF9Ij/9Rhamd+dfpb5QuxP89cy7Rw4=",
|
||||
"x86_64-darwin": "sha256-ElzZVqkQPYTAd0nvLCB5uel9xtOqMK07QFW8MDr174E="
|
||||
"x86_64-linux": "sha256-/ZtluLolVq1ZFAYgbDScze9GZR0NF9baRQXvfEoOnkI=",
|
||||
"aarch64-linux": "sha256-kdh7Dpira7cx9lvtozFsjLrDqMBrUMcgxkhfsqrutws=",
|
||||
"aarch64-darwin": "sha256-chPeMCX+8hvOQ32hTANIn7FmONsSv9t3dKTWTaHerjs=",
|
||||
"x86_64-darwin": "sha256-rMc0dDZB+YbXrjYwXTIt7QvHUxSp//wDJQ5hUdR+LYM="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { CommandProvider, useCommand, type CommandOption } from "@/context/command"
|
||||
import { CommentsProvider } from "@/context/comments"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||
import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
|
||||
import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
|
||||
import { GlobalProvider, useGlobal } from "@/context/global"
|
||||
import { HighlightsProvider } from "@/context/highlights"
|
||||
@@ -63,7 +63,13 @@ 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 {
|
||||
legacySessionHref,
|
||||
legacySessionServer,
|
||||
parseServerKey,
|
||||
retainServerKey,
|
||||
sessionHref,
|
||||
} from "./utils/session-route"
|
||||
import { createSessionLineage } from "@/pages/session/session-lineage"
|
||||
|
||||
import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
|
||||
@@ -111,16 +117,22 @@ const SessionRoute = () => {
|
||||
function TargetServerRoute(props: ParentProps) {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const global = useGlobal()
|
||||
const initial = parseServerKey(params.serverKey)
|
||||
const key = createMemo<ServerConnection.Key | undefined>(
|
||||
(previous) => retainServerKey(previous, params.serverKey),
|
||||
initial,
|
||||
)
|
||||
const conn = createMemo(() => {
|
||||
const key = requireServerKey(params.serverKey)
|
||||
return global.servers.list().find((item) => ServerConnection.key(item) === key)
|
||||
const current = key()
|
||||
if (!current) return undefined
|
||||
return global.servers.list().find((item) => ServerConnection.key(item) === current)
|
||||
})
|
||||
|
||||
return (
|
||||
// Owns the server-identity remount. Session changes must NOT remount this
|
||||
// subtree (SessionRouteErrorBoundary resets and createSessionLineage
|
||||
// re-resolves reactively instead); both rely on this key for server changes.
|
||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||
<Show when={key()} keyed fallback={<Navigate href="/" />}>
|
||||
<ServerSDKProvider server={conn}>
|
||||
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
@@ -135,16 +147,23 @@ const TargetSessionRoute = () => (
|
||||
)
|
||||
|
||||
function LegacyTargetSessionRoute() {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
return (
|
||||
<TargetServerRoute>
|
||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
|
||||
<LegacyTargetSessionRedirect />
|
||||
</SessionRouteErrorBoundary>
|
||||
<LegacyTargetSessionRouteContent />
|
||||
</TargetServerRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyTargetSessionRouteContent() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const serverSDK = useServerSDK()
|
||||
return (
|
||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={ServerConnection.key(serverSDK().server)}>
|
||||
<LegacyTargetSessionRedirect />
|
||||
</SessionRouteErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyTargetSessionRedirect() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -9,86 +9,53 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-titlebar-tab] {
|
||||
--tab-base: var(--v2-background-bg-deep);
|
||||
--tab-overlay: transparent;
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, :has(> [data-slot="tab-link"]:focus-visible)):not([data-state="pressed"]):not(
|
||||
[data-dragging="true"]
|
||||
):not([data-editing="true"]) {
|
||||
--tab-base: var(--v2-background-bg-layer-02);
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is([data-state="pressed"], [data-dragging="true"], [data-editing="true"]) {
|
||||
--tab-base: var(--v2-background-bg-layer-02);
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] {
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-editing="true"] [data-slot="tab-close"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-titlebar-tab-list] {
|
||||
gap: 6px;
|
||||
gap: 13.5px;
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot] {
|
||||
--tab-separator: var(--v2-background-bg-layer-03);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] [data-titlebar-tab-slot] {
|
||||
--tab-separator: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot]:not(:first-child):not([data-active="true"])::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: -3.75px;
|
||||
left: -6.75px;
|
||||
width: 1.5px;
|
||||
height: 12px;
|
||||
border-radius: 9999px;
|
||||
background: var(--tab-separator);
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot][data-active="true"] + [data-titlebar-tab-slot]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover)::before,
|
||||
[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover) + [data-titlebar-tab-slot]::before {
|
||||
[data-titlebar-tab] [data-slot="tab-close"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 28px;
|
||||
width: 16px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to right, transparent, var(--tab-bg));
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-link"] {
|
||||
--tab-title-fade-offset: 4px;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
black 0,
|
||||
black calc(100% - var(--tab-title-fade-offset) - 16px),
|
||||
transparent calc(100% - var(--tab-title-fade-offset)),
|
||||
transparent 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
black 0,
|
||||
black calc(100% - var(--tab-title-fade-offset) - 16px),
|
||||
transparent calc(100% - var(--tab-title-fade-offset)),
|
||||
transparent 100%
|
||||
);
|
||||
[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-close"]::before {
|
||||
display: block;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-title-overflow="true"]:is(:hover, [data-active="true"]):not([data-editing="true"])
|
||||
[data-slot="tab-link"] {
|
||||
--tab-title-fade-offset: 24px;
|
||||
[data-titlebar-tab][data-title-overflow="true"]:hover:not([data-editing="true"]) [data-slot="tab-close"]::before,
|
||||
[data-titlebar-tab][data-title-overflow="true"][data-active="true"]:not([data-editing="true"])
|
||||
[data-slot="tab-close"]::before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
[data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"])
|
||||
|
||||
@@ -180,11 +180,11 @@ export function TabNavItem(props: {
|
||||
data-slot="titlebar-tab-item"
|
||||
data-title-overflow={titleOverflowing()}
|
||||
data-editing={editing()}
|
||||
class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] px-1.5 [container-type:inline-size]"
|
||||
class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
classList={{ invisible: props.hidden }}
|
||||
data-active={props.active}
|
||||
data-dragging={props.dragging}
|
||||
data-state={props.active || props.pressed ? "pressed" : undefined}
|
||||
data-pressed={props.pressed}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
||||
event.preventDefault()
|
||||
@@ -276,7 +276,7 @@ export function TabNavItem(props: {
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div data-slot="tab-close">
|
||||
<div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
|
||||
<IconButtonV2
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
@@ -334,8 +334,8 @@ export function DraftTabItem(props: {
|
||||
data-slot="titlebar-tab-item"
|
||||
data-active={props.active}
|
||||
data-dragging={props.dragging}
|
||||
data-state={props.active || props.pressed ? "pressed" : undefined}
|
||||
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] px-1.5 [container-type:inline-size] whitespace-nowrap"
|
||||
data-pressed={props.pressed}
|
||||
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
classList={{ invisible: props.hidden }}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== MIDDLE_MOUSE_BUTTON) return
|
||||
@@ -381,7 +381,7 @@ export function DraftTabItem(props: {
|
||||
{props.title}
|
||||
</span>
|
||||
</a>
|
||||
<div data-slot="tab-close">
|
||||
<div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
|
||||
<IconButtonV2
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createPathHelpers } from "./file/path"
|
||||
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||
import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||
import { requireServerKey } from "@/utils/session-route"
|
||||
import { parseServerKey } from "@/utils/session-route"
|
||||
import { type DraftTab, useTabs } from "./tabs"
|
||||
import { closeSessionTab, openSessionTab, previewSessionTab, type SessionTabs } from "./layout-tabs"
|
||||
|
||||
@@ -138,10 +138,12 @@ export const currentRoute = (pathname: string, search: string): LayoutRoute => {
|
||||
}
|
||||
|
||||
if (parts[0] === "server" && parts[2] === "session" && parts[3]) {
|
||||
const server = parseServerKey(parts[1])
|
||||
if (!server) return { type: "home" }
|
||||
return {
|
||||
type: "session",
|
||||
sessionId: parts[3],
|
||||
server: requireServerKey(parts[1]),
|
||||
server,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { playSoundById } from "@/utils/sound"
|
||||
import { useGlobal } from "./global"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { type DraftTab, useTabs } from "./tabs"
|
||||
import { requireServerKey } from "@/utils/session-route"
|
||||
import { parseServerKey } from "@/utils/session-route"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
type NotificationBase = {
|
||||
@@ -125,7 +125,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const states = new Map<ServerScope, { dispose: () => void; state: NotificationState }>()
|
||||
|
||||
const activeServer = createMemo(() => {
|
||||
if (params.serverKey) return requireServerKey(params.serverKey)
|
||||
if (params.serverKey) return parseServerKey(params.serverKey) ?? server.key
|
||||
if (search.draftId) {
|
||||
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)
|
||||
if (draft) return draft.server
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useGlobal } from "./global"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { type DraftTab, useTabs } from "./tabs"
|
||||
import { useSettings } from "./settings"
|
||||
import { requireServerKey } from "@/utils/session-route"
|
||||
import { parseServerKey } from "@/utils/session-route"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
import { normalizePermissionRequest } from "./global-sync/utils"
|
||||
import {
|
||||
@@ -71,7 +71,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
})
|
||||
|
||||
const activeServer = createMemo(() => {
|
||||
if (params.serverKey && settings.general.newLayoutDesigns()) return requireServerKey(params.serverKey)
|
||||
if (params.serverKey && settings.general.newLayoutDesigns()) return parseServerKey(params.serverKey) ?? server.key
|
||||
return activeDraft()?.server ?? server.key
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useParams, useSearchParams } from "@solidjs/router"
|
||||
import { createMemo, createResource, createRoot, getOwner, onCleanup } from "solid-js"
|
||||
import { requireServerKey } from "@/utils/session-route"
|
||||
import { ServerConnection } from "./server"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { useSettings } from "./settings"
|
||||
@@ -100,8 +99,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
const owner = getOwner()
|
||||
const serverKey = () =>
|
||||
params.serverKey ? requireServerKey(params.serverKey) : ServerConnection.key(serverSDK().server)
|
||||
const serverKey = () => ServerConnection.key(serverSDK().server)
|
||||
const scope = (): PromptScope =>
|
||||
search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id }
|
||||
const load = (scope: PromptScope) => {
|
||||
|
||||
@@ -99,7 +99,7 @@ import { diffs as list } from "@/utils/diffs"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { formatServerError, isLocalSessionNotFoundError, isSessionNotFoundError } from "@/utils/server-errors"
|
||||
import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
|
||||
import { legacySessionHref, sessionHref } from "@/utils/session-route"
|
||||
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
||||
import { createSessionOwnership } from "./session/session-ownership"
|
||||
import { createSessionLineage } from "./session/session-lineage"
|
||||
@@ -158,6 +158,7 @@ export function SessionPage() {
|
||||
// workspace-scoped state (terminal, directory providers) lives below.
|
||||
export function TargetSessionRouteContent() {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const directory = createMemo(() => serverSync().session.lineage.peek(params.id)?.session.directory)
|
||||
return (
|
||||
@@ -165,7 +166,7 @@ export function TargetSessionRouteContent() {
|
||||
// when session content falls back to the route error boundary.
|
||||
<TargetServerScopedProviders directory={directory} sessionID={() => params.id}>
|
||||
<TargetSessionSettingsCommand />
|
||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)} padded>
|
||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={ServerConnection.key(serverSDK().server)} padded>
|
||||
<ResolvedTargetSessionRoute />
|
||||
</SessionRouteErrorBoundary>
|
||||
</TargetServerScopedProviders>
|
||||
@@ -244,10 +245,11 @@ function SessionErrorFallback(props: { error: unknown; sessionID?: string; serve
|
||||
}
|
||||
|
||||
function ResolvedTargetSessionRoute() {
|
||||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const params = useParams<{ id: string }>()
|
||||
const serverSDK = useServerSDK()
|
||||
const tabs = useTabs()
|
||||
const sync = useServerSync()
|
||||
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
||||
const serverKey = createMemo(() => ServerConnection.key(serverSDK().server))
|
||||
const current = createSessionLineage(
|
||||
() => params.id,
|
||||
() => sync().session.lineage,
|
||||
@@ -2161,7 +2163,7 @@ export default function Page() {
|
||||
if (!id) return
|
||||
navigate(
|
||||
params.serverKey
|
||||
? sessionHref(requireServerKey(params.serverKey), id)
|
||||
? sessionHref(ServerConnection.key(serverSDK().server), id)
|
||||
: legacySessionHref(sdk().directory, id),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -63,10 +63,11 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
|
||||
import { legacySessionHref, sessionHref } from "@/utils/session-route"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
|
||||
@@ -793,7 +794,9 @@ export function MessageTimeline(props: {
|
||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
||||
if (params.id !== sessionID) return
|
||||
const href = (id: string) =>
|
||||
params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id)
|
||||
params.serverKey
|
||||
? sessionHref(ServerConnection.key(serverSDK().server), id)
|
||||
: legacySessionHref(sdk().directory, id)
|
||||
if (parentID) {
|
||||
navigate(href(parentID))
|
||||
return
|
||||
@@ -803,7 +806,7 @@ export function MessageTimeline(props: {
|
||||
return
|
||||
}
|
||||
if (params.serverKey) {
|
||||
tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory })
|
||||
tabs.newDraft({ server: ServerConnection.key(serverSDK().server), directory: sdk().directory })
|
||||
return
|
||||
}
|
||||
navigate(`/${params.dir}/session`)
|
||||
@@ -907,7 +910,9 @@ export function MessageTimeline(props: {
|
||||
const id = parentID()
|
||||
if (!id) return
|
||||
navigate(
|
||||
params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id),
|
||||
params.serverKey
|
||||
? sessionHref(ServerConnection.key(serverSDK().server), id)
|
||||
: legacySessionHref(sdk().directory, id),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { legacySessionHref, legacySessionServer, requireServerKey, rootSession, sessionHref } from "./session-route"
|
||||
import {
|
||||
legacySessionHref,
|
||||
legacySessionServer,
|
||||
parseServerKey,
|
||||
retainServerKey,
|
||||
rootSession,
|
||||
sessionHref,
|
||||
} from "./session-route"
|
||||
|
||||
describe("session routes", () => {
|
||||
test("uses the unique persisted server for a legacy session route", () => {
|
||||
@@ -31,11 +38,17 @@ describe("session routes", () => {
|
||||
const href = sessionHref(server, "session-1")
|
||||
|
||||
expect(href).toBe("/server/aHR0cHM6Ly9leGFtcGxlLmNvbTo0MDk2/session/session-1")
|
||||
expect(requireServerKey(href.split("/")[2])).toBe(server)
|
||||
expect(parseServerKey(href.split("/")[2])).toBe(server)
|
||||
})
|
||||
|
||||
test("rejects malformed server keys", () => {
|
||||
expect(() => requireServerKey("not-base64")).toThrow("Invalid server route")
|
||||
test("returns undefined for malformed server routes", () => {
|
||||
expect(parseServerKey("not-base64")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("retains the current server while a route is being removed", () => {
|
||||
const server = ServerConnection.Key.make("https://example.com:4096")
|
||||
expect(retainServerKey(server, undefined)).toBe(server)
|
||||
expect(retainServerKey(server, "not-base64")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("builds the legacy directory-keyed route", () => {
|
||||
|
||||
@@ -10,12 +10,17 @@ export function legacySessionHref(directory: string, sessionID: string) {
|
||||
return `/${base64Encode(directory)}/session/${sessionID}`
|
||||
}
|
||||
|
||||
export function requireServerKey(segment: string | undefined) {
|
||||
export function parseServerKey(segment: string | undefined) {
|
||||
const key = decode64(segment)
|
||||
if (!key || base64Encode(key) !== segment) throw new Error("Invalid server route")
|
||||
if (!key || base64Encode(key) !== segment) return undefined
|
||||
return ServerConnection.Key.make(key)
|
||||
}
|
||||
|
||||
export function retainServerKey(previous: ServerConnection.Key | undefined, segment: string | undefined) {
|
||||
if (segment === undefined) return previous
|
||||
return parseServerKey(segment)
|
||||
}
|
||||
|
||||
export function legacySessionServer(
|
||||
tabs: readonly { type: "session"; server: ServerConnection.Key; sessionId: string }[],
|
||||
sessionID: string,
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.12.1",
|
||||
"gitlab-ai-provider": "6.12.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
|
||||
@@ -13,7 +13,6 @@ import { CodexAuthPlugin } from "./openai/codex"
|
||||
import { Session } from "@/session/session"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { CopilotAuthPlugin } from "./github-copilot/copilot"
|
||||
import { ModalPlugin } from "./modal/modal"
|
||||
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
|
||||
import { PoeAuthPlugin } from "opencode-poe-auth"
|
||||
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
|
||||
@@ -71,7 +70,6 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] {
|
||||
experimentalWebSockets: experimentalWebSocketsEnabled({ enabled: flags.experimentalWebSockets }),
|
||||
}),
|
||||
CopilotAuthPlugin,
|
||||
ModalPlugin,
|
||||
GitlabAuthPlugin,
|
||||
PoeAuthPlugin,
|
||||
CloudflareWorkersAuthPlugin,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
import { ModalModels } from "./models"
|
||||
|
||||
export async function ModalPlugin(): Promise<Hooks> {
|
||||
return {
|
||||
provider: {
|
||||
id: "modal",
|
||||
async models(provider, ctx) {
|
||||
const apiKey = ctx.auth?.type === "api" ? ctx.auth.key : process.env.MODAL_PROXY_TOKEN
|
||||
const baseURL = Object.values(provider.models)[0]?.api.url
|
||||
if (!apiKey || !baseURL) return {}
|
||||
|
||||
return ModalModels.get(baseURL, apiKey, provider.models).catch(() => ({}))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import type { Model } from "@opencode-ai/sdk/v2"
|
||||
import { Schema } from "effect"
|
||||
|
||||
const reasoningOption = Schema.Struct({
|
||||
type: Schema.Literal("effort"),
|
||||
values: Schema.Array(Schema.NullOr(Schema.String)),
|
||||
})
|
||||
|
||||
const response = Schema.Struct({
|
||||
data: Schema.Array(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
base_model_id: Schema.optional(Schema.String),
|
||||
hugging_face_id: Schema.optional(Schema.String),
|
||||
name: Schema.optional(Schema.String),
|
||||
input_modalities: Schema.optional(Schema.Array(Schema.String)),
|
||||
output_modalities: Schema.optional(Schema.Array(Schema.String)),
|
||||
context_length: Schema.optional(Schema.Number),
|
||||
max_output_length: Schema.optional(Schema.Number),
|
||||
pricing: Schema.optional(
|
||||
Schema.Struct({
|
||||
prompt: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
|
||||
completion: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
|
||||
input_cache_read: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
|
||||
}),
|
||||
),
|
||||
supported_sampling_parameters: Schema.optional(Schema.Array(Schema.String)),
|
||||
supported_features: Schema.optional(Schema.Array(Schema.String)),
|
||||
reasoning_options: Schema.optional(Schema.Array(reasoningOption)),
|
||||
interleaved: Schema.optional(
|
||||
Schema.Union([
|
||||
Schema.Boolean,
|
||||
Schema.Struct({
|
||||
field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const decode = Schema.decodeUnknownSync(response)
|
||||
|
||||
function price(value: string | number | undefined, fallback: number) {
|
||||
if (value === undefined) return fallback
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed * 1_000_000 : fallback
|
||||
}
|
||||
|
||||
export async function get(baseURL: string, apiKey: string, existing: Record<string, Model>) {
|
||||
const data = await fetch(`${baseURL.replace(/\/+$/, "")}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch Modal models: ${res.status}`)
|
||||
return decode(await res.json())
|
||||
})
|
||||
|
||||
return Object.fromEntries(
|
||||
data.data.map((item) => {
|
||||
const template = existing[item.base_model_id ?? item.hugging_face_id ?? item.id]
|
||||
const model: Model = {
|
||||
id: item.id,
|
||||
providerID: "modal",
|
||||
name: item.name ?? template?.name ?? item.id,
|
||||
family: template?.family,
|
||||
api: {
|
||||
id: item.id,
|
||||
url: baseURL,
|
||||
npm: template?.api.npm ?? "@ai-sdk/openai-compatible",
|
||||
},
|
||||
status: template?.status ?? "active",
|
||||
headers: { ...template?.headers },
|
||||
options: { ...template?.options },
|
||||
cost: {
|
||||
input: price(item.pricing?.prompt, template?.cost.input ?? 0),
|
||||
output: price(item.pricing?.completion, template?.cost.output ?? 0),
|
||||
cache: {
|
||||
read: price(item.pricing?.input_cache_read, template?.cost.cache.read ?? 0),
|
||||
write: template?.cost.cache.write ?? 0,
|
||||
},
|
||||
},
|
||||
limit: {
|
||||
context: item.context_length ?? template?.limit.context ?? 0,
|
||||
input: template?.limit.input,
|
||||
output: item.max_output_length ?? template?.limit.output ?? 0,
|
||||
},
|
||||
capabilities: {
|
||||
temperature:
|
||||
item.supported_sampling_parameters?.includes("temperature") ?? template?.capabilities.temperature ?? false,
|
||||
reasoning: item.supported_features?.includes("reasoning") ?? template?.capabilities.reasoning ?? false,
|
||||
attachment:
|
||||
item.input_modalities?.some((modality) => modality !== "text") ??
|
||||
template?.capabilities.attachment ??
|
||||
false,
|
||||
toolcall: item.supported_features?.includes("tools") ?? template?.capabilities.toolcall ?? true,
|
||||
input: {
|
||||
text: item.input_modalities?.includes("text") ?? template?.capabilities.input.text ?? true,
|
||||
audio: item.input_modalities?.includes("audio") ?? template?.capabilities.input.audio ?? false,
|
||||
image: item.input_modalities?.includes("image") ?? template?.capabilities.input.image ?? false,
|
||||
video: item.input_modalities?.includes("video") ?? template?.capabilities.input.video ?? false,
|
||||
pdf: item.input_modalities?.includes("pdf") ?? template?.capabilities.input.pdf ?? false,
|
||||
},
|
||||
output: {
|
||||
text: item.output_modalities?.includes("text") ?? template?.capabilities.output.text ?? true,
|
||||
audio: item.output_modalities?.includes("audio") ?? template?.capabilities.output.audio ?? false,
|
||||
image: item.output_modalities?.includes("image") ?? template?.capabilities.output.image ?? false,
|
||||
video: item.output_modalities?.includes("video") ?? template?.capabilities.output.video ?? false,
|
||||
pdf: item.output_modalities?.includes("pdf") ?? template?.capabilities.output.pdf ?? false,
|
||||
},
|
||||
interleaved: item.interleaved ?? template?.capabilities.interleaved ?? false,
|
||||
},
|
||||
release_date: template?.release_date ?? "",
|
||||
}
|
||||
model.variants =
|
||||
item.reasoning_options === undefined
|
||||
? template?.variants
|
||||
: Object.fromEntries(
|
||||
item.reasoning_options.flatMap((option) =>
|
||||
option.values.map((value) => {
|
||||
const effort = value ?? "none"
|
||||
return [effort, { reasoningEffort: effort }]
|
||||
}),
|
||||
),
|
||||
)
|
||||
return [item.id, model]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export * as ModalModels from "./models"
|
||||
@@ -1,198 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import type { Model, Provider } from "@opencode-ai/sdk/v2"
|
||||
import { ModalPlugin } from "@/plugin/modal/modal"
|
||||
|
||||
const BASE_MODEL_ID = "thinkingmachines/Inkling-NVFP4"
|
||||
const RUNTIME_MODEL_ID = "workspace--inkling.us-west.modal.direct"
|
||||
const FALLBACK_RUNTIME_MODEL_ID = "workspace--inkling-fallback.us-west.modal.direct"
|
||||
|
||||
function makeProvider(baseURL: string): Provider {
|
||||
const template: Model = {
|
||||
id: BASE_MODEL_ID,
|
||||
providerID: "modal",
|
||||
name: "Inkling",
|
||||
family: "ling",
|
||||
api: {
|
||||
id: BASE_MODEL_ID,
|
||||
url: baseURL,
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
status: "active",
|
||||
headers: {},
|
||||
options: {},
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 4,
|
||||
cache: {
|
||||
read: 0.2,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
limit: {
|
||||
context: 128_000,
|
||||
output: 8_192,
|
||||
},
|
||||
capabilities: {
|
||||
temperature: true,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: {
|
||||
text: true,
|
||||
audio: true,
|
||||
image: true,
|
||||
video: false,
|
||||
pdf: false,
|
||||
},
|
||||
output: {
|
||||
text: true,
|
||||
audio: false,
|
||||
image: false,
|
||||
video: false,
|
||||
pdf: false,
|
||||
},
|
||||
interleaved: {
|
||||
field: "reasoning_content",
|
||||
},
|
||||
},
|
||||
release_date: "2026-07-15",
|
||||
variants: {
|
||||
fallback: {
|
||||
reasoningEffort: "fallback",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
id: "modal",
|
||||
name: "Modal",
|
||||
source: "api",
|
||||
env: ["MODAL_PROXY_TOKEN"],
|
||||
options: {},
|
||||
models: {
|
||||
[template.id]: template,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test("discovers Modal workspace models", async () => {
|
||||
const requests: Array<{ authorization: string | null; path: string }> = []
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(request) {
|
||||
requests.push({
|
||||
authorization: request.headers.get("authorization"),
|
||||
path: new URL(request.url).pathname,
|
||||
})
|
||||
return Response.json({
|
||||
data: [
|
||||
{
|
||||
id: RUNTIME_MODEL_ID,
|
||||
base_model_id: BASE_MODEL_ID,
|
||||
name: "Thinking Machines: Inkling",
|
||||
input_modalities: ["text", "image", "audio"],
|
||||
output_modalities: ["text"],
|
||||
context_length: 1_048_576,
|
||||
max_output_length: 262_144,
|
||||
pricing: {
|
||||
prompt: "0.0000012",
|
||||
completion: "0.000005",
|
||||
input_cache_read: "0.00000027",
|
||||
},
|
||||
supported_sampling_parameters: ["temperature"],
|
||||
supported_features: ["tools", "reasoning"],
|
||||
reasoning_options: [
|
||||
{
|
||||
type: "effort",
|
||||
values: ["none", "low", "medium", "high", "xhigh", "max"],
|
||||
},
|
||||
],
|
||||
interleaved: {
|
||||
field: "reasoning_content",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: FALLBACK_RUNTIME_MODEL_ID,
|
||||
base_model_id: BASE_MODEL_ID,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
})
|
||||
const provider = makeProvider(`${server.url}v1`)
|
||||
|
||||
const plugin = await ModalPlugin()
|
||||
const models = await plugin.provider!.models!(provider, {
|
||||
auth: {
|
||||
type: "api",
|
||||
key: "test-token",
|
||||
},
|
||||
})
|
||||
const model = models[RUNTIME_MODEL_ID]
|
||||
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
authorization: "Bearer test-token",
|
||||
path: "/v1/models",
|
||||
},
|
||||
])
|
||||
expect(Object.keys(models)).toEqual([RUNTIME_MODEL_ID, FALLBACK_RUNTIME_MODEL_ID])
|
||||
expect(model.api).toEqual({
|
||||
id: RUNTIME_MODEL_ID,
|
||||
url: `${server.url}v1`,
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
})
|
||||
expect(model.family).toBe("ling")
|
||||
expect(model.capabilities.interleaved).toEqual({ field: "reasoning_content" })
|
||||
expect(model.capabilities.input).toEqual({
|
||||
text: true,
|
||||
audio: true,
|
||||
image: true,
|
||||
video: false,
|
||||
pdf: false,
|
||||
})
|
||||
expect(model.cost).toEqual({
|
||||
input: 1.2,
|
||||
output: 5,
|
||||
cache: {
|
||||
read: 0.27,
|
||||
write: 0,
|
||||
},
|
||||
})
|
||||
expect(model.limit).toEqual({
|
||||
context: 1_048_576,
|
||||
output: 262_144,
|
||||
})
|
||||
expect(model.variants).toEqual({
|
||||
none: { reasoningEffort: "none" },
|
||||
low: { reasoningEffort: "low" },
|
||||
medium: { reasoningEffort: "medium" },
|
||||
high: { reasoningEffort: "high" },
|
||||
xhigh: { reasoningEffort: "xhigh" },
|
||||
max: { reasoningEffort: "max" },
|
||||
})
|
||||
expect(models[FALLBACK_RUNTIME_MODEL_ID].variants).toEqual({
|
||||
fallback: {
|
||||
reasoningEffort: "fallback",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("hides Modal models when discovery fails", async () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response(null, { status: 503 })
|
||||
},
|
||||
})
|
||||
|
||||
const plugin = await ModalPlugin()
|
||||
const models = await plugin.provider!.models!(makeProvider(`${server.url}v1`), {
|
||||
auth: {
|
||||
type: "api",
|
||||
key: "test-token",
|
||||
},
|
||||
})
|
||||
|
||||
expect(models).toEqual({})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { deferSelect } from "./select-defer"
|
||||
|
||||
test("defers controlled select updates until the current selection closes", async () => {
|
||||
const order: string[] = []
|
||||
deferSelect(() => order.push("update"), "theme")
|
||||
order.push("close")
|
||||
|
||||
expect(order).toEqual(["close"])
|
||||
await Promise.resolve()
|
||||
expect(order).toEqual(["close", "update"])
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
export function deferSelect<T>(callback: ((value: T) => void) | undefined, value: T) {
|
||||
// Kobalte closes the popup after onChange returns. Controlled updates must wait
|
||||
// so they cannot rebuild the value or options during portal cleanup.
|
||||
queueMicrotask(() => callback?.(value))
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from
|
||||
import { pipe, groupBy, entries, map } from "remeda"
|
||||
import { Button, ButtonProps } from "./button"
|
||||
import { Icon } from "./icon"
|
||||
import { deferSelect } from "./select-defer"
|
||||
|
||||
export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
|
||||
placeholder?: string
|
||||
@@ -124,7 +125,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
onChange={(v) => {
|
||||
local.onSelect?.(v ?? undefined)
|
||||
deferSelect(local.onSelect, v ?? undefined)
|
||||
stop()
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Select as Kobalte } from "@kobalte/core/select"
|
||||
import { Show, createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { deferSelect } from "../../components/select-defer"
|
||||
import "./select-v2.css"
|
||||
|
||||
function groupOptions<T>(options: T[], groupBy?: (x: T) => string): { category: string; options: T[] }[] {
|
||||
@@ -165,7 +166,7 @@ export function SelectV2<T>(props: SelectV2Props<T>) {
|
||||
)}
|
||||
onChange={(next) => {
|
||||
const v = next == null ? null : Array.isArray(next) ? ((next[0] as T) ?? null) : (next as T)
|
||||
local.onSelect?.(v)
|
||||
deferSelect(local.onSelect, v)
|
||||
stop()
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
|
||||
@@ -54,7 +54,6 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
| [opencode-firecrawl](https://github.com/firecrawl/opencode-firecrawl) | Web scraping, crawling, and search via the Firecrawl CLI |
|
||||
| [opencode-jfrog-plugin](https://github.com/jfrog/opencode-jfrog-plugin) | JFrog Plugin for seamless integration of Opencode users to JFrog platform |
|
||||
| [opencode-goal-plugin](https://github.com/willytop8/OpenCode-goal-plugin) | Session-scoped `/goal` workflow that keeps objectives in context and auto-continues until complete |
|
||||
| [opencode-tavily](https://github.com/tavily-ai/opencode-tavily) | Web search, extraction, crawling, and deep research via the Tavily CLI |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user