Compare commits

...
Author SHA1 Message Date
Brendan Allan 9dbba2fa54 feat(core): restore session archiving 2026-10-07 22:33:20 +08:00
opencode-agent[bot]andBrendonovich 119cb15d1d refactor(app): remove configurable default server (#53731)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 20:46:51 +08:00
opencode-agent[bot] e1b7299bc9 chore(core): refresh bundled models.dev snapshot 2026-10-07 12:22:51 +00:00
opencode-agent[bot]andBrendonovich 68aa4f8017 feat(app): show recent sessions in the empty command palette (#53727)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 10:58:35 +00:00
David HillandBrendonovich 69219470a2 feat(app): show recently closed tabs menu (#51973)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 10:24:51 +00:00
opencode-agent[bot]andBrendonovich 10b11ba946 fix(app): keep live terminals through transient connect failures (#52232)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 10:04:03 +00:00
opencode-agent[bot]andBrendonovich 2e3d3a735a fix(app): use filled clear icon in model and branch searches (#53511)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 17:12:10 +08:00
opencode-agent[bot]andBrendonovich f655e7b2b5 fix(app): wrap mobile project controls responsively (#50484)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 17:11:57 +08:00
opencode-agent[bot]andneriousy 6273bd7924 fix(core): stop sending API key metadata as request body (#53546)
Co-authored-by: neriousy <filip@anoma.ly>
2026-10-07 10:36:22 +02:00
Aiden Cline 34cf183c4c feat(ai): support between_tools thinking in Anthropic Messages (#53601) 2026-10-06 23:57:20 -05:00
opencode-agent[bot]andBrendonovich fd99516e51 fix(session-ui): keep Markdown text selectable (#53665)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-10-07 04:33:34 +00:00
271 changed files with 899 additions and 1112 deletions

No files matched your search

@@ -285,7 +285,7 @@ const AnthropicThinkingEnabled = Schema.Struct({
...AnthropicThinkingFields,
})
const AnthropicThinkingAdaptive = Schema.Struct({ type: Schema.tag("adaptive"), ...AnthropicThinkingFields })
const AnthropicThinkingDisabled = Schema.Struct({ type: Schema.tag("disabled") })
const AnthropicThinkingDisabled = Schema.Struct({ type: Schema.Literals(["disabled", "between_tools"]) })
const AnthropicThinking = Schema.Union([AnthropicThinkingEnabled, AnthropicThinkingAdaptive, AnthropicThinkingDisabled])
type AnthropicThinking = typeof AnthropicThinking.Type
@@ -1001,18 +1001,21 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoi
// TODO: Move per-model capability heuristics (`supportsEffortUpdates`, `supportsNativeSystemUpdates`,
// `supportsThinkingBlockBinding`) into explicit model/provider `compatibility` metadata so the protocol
// only reads `request.model.compatibility`.
const isThinkingOff = Schema.is(AnthropicThinkingDisabled)
// Per-turn effort started with Claude Opus 5 and every Claude 5.1 model; later versions of any family inherit it.
const supportsEffortUpdates = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsEffortUpdates
const supportsEffortUpdates = (request: LLMRequest) => {
if (isThinkingOff(request.providerOptions?.thinking)) return false
const override = request.model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
const version = claudeVersion(model.id)
const version = claudeVersion(request.model.id)
if (version === undefined) return false
if (version.family === "opus" && version.major >= 5) return true
return version.major > 5 || (version.major === 5 && version.minor >= 1)
}
const applyThinkingBindingDefault = (model: LLMRequest["model"], thinking: AnthropicThinking | undefined) => {
if (thinking?.type === "disabled") return thinking
if (isThinkingOff(thinking)) return thinking
if (!supportsThinkingBlockBinding(model)) return thinking
return {
...(thinking ?? { type: "adaptive" as const }),
@@ -1607,7 +1610,7 @@ export const protocol = Protocol.make({
}),
step,
},
supportsEffortUpdates: (request) => supportsEffortUpdates(request.model),
supportsEffortUpdates,
})
export const transport = <
@@ -1653,7 +1656,7 @@ function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "con
betas.push("mid-conversation-output-config-2026-07-01")
const thinking = body.thinking
if (thinking && thinking.type !== "disabled" && thinking.block_binding) betas.push(THINKING_BINDING_BETA)
if (thinking && !isThinkingOff(thinking) && thinking.block_binding) betas.push(THINKING_BINDING_BETA)
return betas
}
@@ -476,7 +476,9 @@ const MIN_THINKING_BUDGET = 1_024
const isThinkingDisabled = Schema.is(
Schema.Struct({
additionalModelRequestFields: Schema.Struct({ thinking: Schema.Struct({ type: Schema.Literal("disabled") }) }),
additionalModelRequestFields: Schema.Struct({
thinking: Schema.Struct({ type: Schema.Literals(["disabled", "between_tools"]) }),
}),
}),
)
@@ -65,8 +65,8 @@ const messagesRoute = Route.make({
...AnthropicMessages.protocol,
// Mantle rejects mid-conversation `output_config` on Opus 5.0; support starts at 5.1+.
supportsEffortUpdates: (request) => {
const override = request.model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
if (!(AnthropicMessages.protocol.supportsEffortUpdates?.(request) ?? false)) return false
if (request.model.compatibility?.supportsEffortUpdates !== undefined) return true
const version = claudeVersion(request.model.id)
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
},
+16
View File
@@ -268,6 +268,22 @@ describe("Anthropic Messages effort updates", () => {
}),
)
it.effect("strips markers when thinking is disabled or between_tools", () =>
Effect.gen(function* () {
const sonnet = anthropic("claude-sonnet-5-5")
const betweenTools = yield* compileRequest(
LLM.request({
model: sonnet,
messages: conversation,
providerOptions: { thinking: { type: "between_tools" }, effort: "low" },
}),
)
expect(systemMessages(betweenTools.body)).toHaveLength(0)
expect(betweenTools.body.output_config).toEqual({ effort: "low" })
}),
)
it.effect("strips markers for Opus 5.0 on Bedrock Mantle Messages while lowering Opus 5.5", () =>
Effect.gen(function* () {
const mantle = AmazonBedrockMantle.configure({ apiKey: "test", region: "us-east-1" })
@@ -44,6 +44,7 @@ it.effect("preserves explicit thinking settings and combines required beta heade
Effect.gen(function* () {
for (const thinking of [
{ type: "disabled" },
{ type: "between_tools" },
{ type: "adaptive", block_binding: { prefix_mismatch_behavior: "error" } },
] as const) {
const request = LLM.request({
@@ -55,7 +56,7 @@ it.effect("preserves explicit thinking settings and combines required beta heade
const prepared = yield* AnthropicMessages.route.prepareTransport(compiled.body, request)
expect(compiled.body.thinking).toEqual(thinking)
expect(prepared.request.headers["anthropic-beta"]).toBe(
thinking.type === "disabled"
thinking.type === "disabled" || thinking.type === "between_tools"
? "interleaved-thinking-2025-05-14,compact-2026-01-12"
: "interleaved-thinking-2025-05-14,compact-2026-01-12,thinking-binding-controls-2026-08-01",
)
@@ -38,6 +38,16 @@ test("home commands do not wait for session search", async ({ page }) => {
release.resolve()
})
test("lists recent sessions before a query is entered", async ({ page }) => {
const { dialog } = await openCommandPalette(page, true)
const session = dialog.getByRole("option", { name: /Palette fixture session/ })
await expect(dialog.getByText("Recent sessions", { exact: true })).toBeVisible()
await expect(session).toHaveAttribute("aria-selected", "false")
await session.click()
await expect(dialog).toHaveCount(0)
await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible()
})
test("appends search results without resetting the selected command", async ({ page }) => {
const { dialog, input } = await openCommandPalette(page)
const files = Promise.withResolvers<void>()
@@ -106,6 +106,37 @@ for (const mount of ["cached", "explicit"] as const) {
})
}
test("reconnects the same terminal after a transient connection failure", async ({ page }) => {
const { pty } = await openSession(page, workspace)
let tickets = 0
await page.route(/\/api\/pty\/[^/]+\/connect-token/, (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
tickets += 1
if (tickets !== 2) return route.fallback()
return route.fulfill({
status: 503,
headers: { "access-control-allow-origin": "*" },
body: "Service Unavailable",
})
})
await page.keyboard.press("Control+Backquote")
const terminal = page.locator('[data-component="terminal"]')
await expect(terminal.locator("textarea")).toHaveCount(1)
await expect.poll(() => pty.sockets.length).toBe(1)
const id = pty.sockets[0]!.id
await terminal.evaluate((element) => element.setAttribute("data-connection-probe", "original"))
await pty.sockets[0]!.close(1011, "Temporary disconnection")
await expect.poll(() => tickets).toBe(3)
await expect.poll(() => pty.sockets.length).toBe(2)
expect(pty.sockets[1]!.id).toBe(id)
expect(pty.rejected).toEqual([])
await expect(terminal).toHaveAttribute("data-connection-probe", "original")
expect(pty.created.map((item) => item.id)).toEqual([id])
})
test("focuses a terminal created from the new-terminal button", async ({ page }) => {
const { editor } = await openSession(page, workspace)
const terminal = page.locator('[data-component="terminal"]')
@@ -56,7 +56,6 @@ export function mount(input: { server: string; route: string; direction: Directi
return (
<AppInterface
servers={[server]}
defaultServer={ServerConnection.key(server)}
canonicalLocalServer={ServerConnection.key(server)}
router={(props) => <MemoryRouter {...props} history={history} />}
>
+12 -4
View File
@@ -63,6 +63,12 @@ export class MockShellNotFound extends Schema.TaggedError<MockShellNotFound>()("
message: Schema.String,
}) {}
// The server's error for an unknown PTY, or one owned by another workspace.
export class MockPtyNotFound extends Schema.TaggedError<MockPtyNotFound>()("PtyNotFoundError", {
ptyID: Schema.String,
message: Schema.String,
}) {}
// The server's error for a request without its password.
export class MockUnauthorized extends Schema.TaggedError<MockUnauthorized>()("UnauthorizedError", {
message: Schema.String,
@@ -75,6 +81,8 @@ export class MockUnsupported extends Schema.TaggedError<MockUnsupported>()("Mock
const Unsupported = MockUnsupported.pipe(HttpApiSchema.status(501))
const PtyMissing = [MockNotFound.pipe(HttpApiSchema.status(404)), MockPtyNotFound.pipe(HttpApiSchema.status(404))]
const Group = HttpApiGroup.make("mock")
.add(HttpApiEndpoint.get("info", "/api/info", { success: Json }))
.add(
@@ -242,7 +250,7 @@ const Group = HttpApiGroup.make("mock")
HttpApiEndpoint.get("ptyGet", "/api/pty/:ptyID", {
params: PtyParams,
success: Json,
error: MockNotFound.pipe(HttpApiSchema.status(404)),
error: PtyMissing,
}),
)
.add(
@@ -250,21 +258,21 @@ const Group = HttpApiGroup.make("mock")
params: PtyParams,
payload: Pty.UpdateInput,
success: Json,
error: MockNotFound.pipe(HttpApiSchema.status(404)),
error: PtyMissing,
}),
)
.add(
HttpApiEndpoint.delete("ptyRemove", "/api/pty/:ptyID", {
params: PtyParams,
success: NoContent,
error: MockNotFound.pipe(HttpApiSchema.status(404)),
error: PtyMissing,
}),
)
.add(
HttpApiEndpoint.post("ptyConnectToken", "/api/pty/:ptyID/connect-token", {
params: PtyParams,
success: Json,
error: MockNotFound.pipe(HttpApiSchema.status(404)),
error: PtyMissing,
}),
)
.add(
+13 -2
View File
@@ -11,6 +11,7 @@ import {
MockBadRequest,
MockInternal,
MockNotFound,
MockPtyNotFound,
MockShellNotFound,
MockUnauthorized,
MockUnsupported,
@@ -149,7 +150,14 @@ export type MockPtyInfo = {
pid: number
}
export type MockPtySocket = { id: string; url: URL; input: string[]; closed: boolean; send(data: string): void }
export type MockPtySocket = {
id: string
url: URL
input: string[]
closed: boolean
send(data: string): void
close(code: number, reason: string): Promise<void>
}
export type MockPty = {
list: MockPtyInfo[]
@@ -447,6 +455,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
input: [],
closed: false,
send: (data) => ws.send(data),
close: (code, reason) => ws.close({ code, reason }),
}
ws.onMessage((message) => socket.input.push(message.toString()))
@@ -697,7 +706,9 @@ function mockHandlers(
const directory = requestDirectory(config, request)
const found = state.pty.find(id, directory)
return found ? Effect.succeed(found) : Effect.fail(new MockNotFound({ message: "PTY not found" }))
return found
? Effect.succeed(found)
: Effect.fail(new MockPtyNotFound({ ptyID: id, message: `PTY not found: ${id}` }))
}),
),
)
@@ -241,11 +241,7 @@ export function mount(input: {
return (
<Show when={extensions.ready()}>
<AppInterface
servers={servers()}
defaultServer={ServerConnection.Key.make("sidecar")}
router={(props) => <MemoryRouter {...props} history={history} />}
/>
<AppInterface servers={servers()} router={(props) => <MemoryRouter {...props} history={history} />} />
</Show>
)
}
@@ -4,7 +4,6 @@ import { render } from "solid-js/web"
import type { Bridge } from "@opencode/gui-extensions/sdk/bridge"
import { AppBaseProviders, AppInterface } from "../../src/app"
import { PlatformProvider, type Platform } from "../../src/runtime/platform/platform"
import { ServerConnection } from "../../src/runtime/server/registry"
// The app on a Windows desktop platform, so the titlebar shows the app menu. Menu actions are listed in an output.
export function mount(input: { server: string }) {
@@ -59,7 +58,6 @@ export function mount(input: { server: string }) {
<output aria-label="Desktop menu actions">{store.actions.join(",")}</output>
<AppInterface
servers={[{ type: "sidecar", variant: "base", displayName: "Local Server", http: { url: input.server } }]}
defaultServer={ServerConnection.Key.make("sidecar")}
router={(props) => <MemoryRouter {...props} history={history} />}
/>
</AppBaseProviders>
+1 -6
View File
@@ -86,7 +86,6 @@ export function AppBaseProviders(
export function AppInterface(props: {
children?: JSX.Element
defaultServer?: ServerConnection.Key
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
router?: Component<BaseRouterProps>
@@ -108,11 +107,7 @@ export function AppInterface(props: {
)
return (
<ServersProvider
defaultServer={props.defaultServer}
canonicalLocalServer={props.canonicalLocalServer}
servers={props.servers}
>
<ServersProvider canonicalLocalServer={props.canonicalLocalServer} servers={props.servers}>
<SettingsProvider>
<Dynamic component={props.router ?? Router} root={Root}>
<AppRoutes />
+1 -2
View File
@@ -16,7 +16,7 @@ import pkg from "../package.json"
import { ServerConnection } from "@/runtime/server/registry"
const getLocale = () => {
if (typeof navigator !== "object") return "en" as const
if (typeof navigator === "undefined") return "en" as const
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
for (const language of languages) {
@@ -101,7 +101,6 @@ if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
<PlatformProvider value={web.platform}>
<AppBaseProviders locale={locale}>
<AppInterface
defaultServer={web.defaultServerUrl ? ServerConnection.Key.make(web.defaultServerUrl) : undefined}
canonicalLocalServer={server ? ServerConnection.key(server) : undefined}
servers={server ? [server] : []}
>
@@ -86,10 +86,6 @@ export function createHomeProjectsController(home: HomeController) {
const key = ServerConnection.key(conn)
setState("collapsed", key, !state().collapsed[key])
},
canDefault: serverManagement.defaults.available,
defaultKey: serverManagement.defaults.key,
setDefault: (conn: ServerConnection.Any | undefined) =>
serverManagement.defaults.set(conn ? ServerConnection.key(conn) : null),
canRemove: (conn: ServerConnection.Any) => serverManagement.connection.canRemove(ServerConnection.key(conn)),
remove: (conn: ServerConnection.Any) => serverManagement.connection.remove(ServerConnection.key(conn)),
canHide: (conn: ServerConnection.Any) => serverManagement.connection.canHide(ServerConnection.key(conn)),
@@ -19,8 +19,6 @@ export function HomeProjects(props: {
serverHealth={props.projects.server.health}
projectsForServer={props.projects.server.projects}
collapsed={props.projects.server.collapsed}
canDefaultServer={props.projects.server.canDefault()}
defaultServerKey={props.projects.server.defaultKey()}
canRevealProject={props.projects.project.canReveal}
unseenCount={props.projects.project.unseenCount}
onWheel={props.scroll.viewport.containWheel}
@@ -29,7 +27,6 @@ export function HomeProjects(props: {
onAuthenticateServer={props.projects.server.authenticate}
onToggleCollapsed={props.projects.server.toggleCollapsed}
onEditServer={props.projects.server.edit}
onSetDefaultServer={props.projects.server.setDefault}
canRemoveServer={props.projects.server.canRemove}
onRemoveServer={props.projects.server.remove}
canHideServer={props.projects.server.canHide}
-10
View File
@@ -52,8 +52,6 @@ export type HomeProjectsViewProps = {
serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined
projectsForServer: (server: ServerConnection.Any) => LocalProject[]
collapsed: (server: ServerConnection.Any) => boolean
canDefaultServer: boolean
defaultServerKey: ServerConnection.Key | null | undefined
canRevealProject: (server: ServerConnection.Any) => boolean
unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
onWheel: (event: WheelEvent) => void
@@ -62,7 +60,6 @@ export type HomeProjectsViewProps = {
onAuthenticateServer?: (server: ServerConnection.Any) => void
onToggleCollapsed: (server: ServerConnection.Any) => void
onEditServer: (server: ServerConnection.Http) => void
onSetDefaultServer: (server: ServerConnection.Any | undefined) => void
canRemoveServer: (server: ServerConnection.Any) => boolean
onRemoveServer: (server: ServerConnection.Any) => void
canHideServer: (server: ServerConnection.Any) => boolean
@@ -347,12 +344,9 @@ function HomeServerRow(props: {
language: HomeProjectsViewProps["language"]
projectsForServer: HomeProjectsViewProps["projectsForServer"]
contextMenuOpen: HomeProjectsContextMenuProps["contextMenuOpen"]
canDefaultServer: HomeProjectsViewProps["canDefaultServer"]
defaultServerKey: HomeProjectsViewProps["defaultServerKey"]
onFocusServer: HomeProjectsViewProps["onFocusServer"]
onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"]
onEditServer: HomeProjectsViewProps["onEditServer"]
onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"]
canRemoveServer: HomeProjectsViewProps["canRemoveServer"]
onRemoveServer: HomeProjectsViewProps["onRemoveServer"]
canHideServer: HomeProjectsViewProps["canHideServer"]
@@ -470,13 +464,9 @@ function HomeServerRow(props: {
<ServerRowMenuView
server={props.server}
labels={serverMenuLabels(props.language)}
canDefault={props.canDefaultServer}
isDefault={props.defaultServerKey === ServerConnection.key(props.server)}
canRemove={props.canRemoveServer(props.server)}
canHide={props.canHideServer(props.server)}
onEdit={props.onEditServer}
onSetDefault={() => props.onSetDefaultServer(props.server)}
onRemoveDefault={() => props.onSetDefaultServer(undefined)}
onRemove={() => props.onRemoveServer(props.server)}
onHide={() => props.onHideServer(props.server)}
open={props.contextMenuOpen(contextMenuID())}
@@ -8,6 +8,7 @@ import {
createCommandPaletteCommandEntry,
createServerSessionEntries,
type CommandPaletteEntry,
type PaletteHighlight,
} from "@/shell/commands/palette"
import { CommandPaletteView, matchesCommandPaletteEntry } from "@/shell/commands/dialog"
@@ -20,7 +21,7 @@ export function HomeCommandPalette(props: {
const global = useGlobal()
const language = useLanguage()
const server = global.ensureServerCtx(props.server)
const state = { cleanup: undefined as (() => void) | void, committed: false }
const state: PaletteHighlight = { cleanup: undefined, committed: false }
const commandEntries = createMemo(() => {
const category = language.t("palette.group.commands")
@@ -36,6 +37,7 @@ export function HomeCommandPalette(props: {
get: (sessionID, signal) => server.sdk.api.session.get({ sessionID }, { signal }),
untitled: () => language.t("command.session.new"),
category: () => language.t("command.category.session"),
recentCategory: () => language.t("palette.group.recentSessions"),
})
const highlight = (item: CommandPaletteEntry | undefined) => {
+1 -1
View File
@@ -70,7 +70,7 @@ export function NewSessionView(props: {
<PromptProjectAddButton controller={props.project} />
</Show>
<Show when={props.project.selected()}>
<div class="flex min-h-7 min-w-0 flex-col items-center justify-center gap-0 text-v2-text-text-faint sm:flex-row">
<div class="flex min-h-7 min-w-0 flex-row flex-wrap items-center justify-center gap-0 text-v2-text-text-faint">
<PromptProjectSelector controller={props.project} placement="bottom" />
<Show
when={props.workspace.bar.visible()}
@@ -327,7 +327,7 @@ export function PromptWorkspaceSelector(props: {
setTimeout(() => requestAnimationFrame(() => branchSearchInput?.focus({ preventScroll: true })))
}}
>
<div class="flex h-7 shrink-0 items-center gap-2 rounded-sm pl-3 pr-2.5 text-v2-icon-icon-muted">
<div class="flex h-7 shrink-0 items-center gap-2 rounded-sm pl-3 pr-1 text-v2-icon-icon-muted">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
<input
ref={(element) => {
@@ -355,7 +355,7 @@ export function PromptWorkspaceSelector(props: {
<Show when={search.branches.trim()}>
<button
type="button"
class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
class="flex size-5 items-center justify-center rounded-sm bg-transparent text-v2-icon-icon-faint transition-colors hover:text-v2-icon-icon-base focus-visible:text-v2-icon-icon-base active:text-v2-icon-icon-base"
onPointerDown={(event) => event.preventDefault()}
onClick={() => {
setSearch("branches", "")
@@ -363,7 +363,7 @@ export function PromptWorkspaceSelector(props: {
}}
aria-label={language.t("common.clear")}
>
<Icon name="close-small" size="small" />
<Icon name="circle-xmark" />
</button>
</Show>
</div>
+19 -30
View File
@@ -1,7 +1,5 @@
import { Button } from "@opencode/ui/button"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode/ui/dialog"
import { Icon } from "@opencode/ui/icon"
import { IconButton } from "@opencode/ui/icon-button"
import { Switch } from "@opencode/ui/switch"
import { TextInput } from "@opencode/ui/text-input"
import { useFilteredList } from "@opencode/ui/hooks"
@@ -24,7 +22,7 @@ export const DialogManageModels: Component = () => {
const local = useLocal()
const language = useLanguage()
const dialog = useDialog()
const [store, setStore] = createStore({ collapsed: {} as Record<string, boolean> })
const [store, setStore] = createStore<{ collapsed: Record<string, boolean> }>({ collapsed: {} })
const directory = () => decode64(local.slug())
const handleConnectProvider = () => {
@@ -107,33 +105,24 @@ export const DialogManageModels: Component = () => {
</DialogHeader>
<DialogBody class="flex min-h-0 flex-1 flex-col">
<div class="px-4 pt-px pb-3">
<div class="relative">
<TextInput
type="search"
appearance="base"
class="!w-full self-stretch"
value={list.filter()}
onInput={(event) => list.onInput(event.currentTarget.value)}
placeholder={language.t("dialog.model.search.placeholder")}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
autofocus
aria-label={language.t("dialog.model.search.placeholder")}
/>
<Show when={list.filter()}>
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="settings-tab-search-clear"
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
onClick={() => list.clear()}
aria-label={language.t("common.clear")}
/>
</Show>
</div>
<TextInput
type="search"
appearance="base"
class="!w-full self-stretch"
value={list.filter()}
onInput={(event) => list.onInput(event.currentTarget.value)}
placeholder={language.t("dialog.model.search.placeholder")}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
autofocus
aria-label={language.t("dialog.model.search.placeholder")}
showClearButton={!!list.filter()}
clearIcon="circle-xmark"
clearLabel={language.t("common.clear")}
onClearClick={() => list.clear()}
/>
</div>
<div data-slot="manage-models-scroll" class="relative min-h-0 flex-1">
<div class="settings-panel settings-models h-full px-4 pt-1 pb-4">
@@ -9,7 +9,6 @@ import { Button } from "@opencode/ui/button"
import { Badge } from "@opencode/ui/badge"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode/ui/dialog"
import { Icon } from "@opencode/ui/icon"
import { IconButton } from "@opencode/ui/icon-button"
import { ScrollView } from "@opencode/ui/scroll-view"
import { Tooltip } from "@opencode/ui/tooltip"
import { Menu } from "@opencode/ui/menu"
@@ -186,55 +185,46 @@ const ModelList: Component<{
return (
<div class="flex min-h-0 flex-1 flex-col">
<div data-slot="model-selector-search" class="shrink-0 pt-px pb-3" classList={{ "px-4": !props.mobile }}>
<div class="relative">
<TextInput
type="search"
appearance="base"
class="!w-full self-stretch"
placeholder={language.t("dialog.model.search.placeholder")}
value={store.search}
autofocus={!props.mobile}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
onInput={(event) => setSearch(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.altKey || event.metaKey) return
<TextInput
type="search"
appearance="base"
class="!w-full self-stretch"
placeholder={language.t("dialog.model.search.placeholder")}
value={store.search}
autofocus={!props.mobile}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
onInput={(event) => setSearch(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.altKey || event.metaKey) return
if (event.key === "ArrowDown") {
event.preventDefault()
moveActive(1)
if (event.key === "ArrowDown") {
event.preventDefault()
moveActive(1)
return
}
return
}
if (event.key === "ArrowUp") {
event.preventDefault()
moveActive(-1)
if (event.key === "ArrowUp") {
event.preventDefault()
moveActive(-1)
return
}
return
}
if (event.key === "Enter" && !event.isComposing) {
event.preventDefault()
selectActive()
}
}}
aria-label={language.t("dialog.model.search.placeholder")}
/>
<Show when={store.search}>
<IconButton
type="button"
variant="ghost-muted"
size="small"
class="settings-tab-search-clear"
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
onClick={() => setSearch("")}
aria-label={language.t("common.clear")}
/>
</Show>
</div>
if (event.key === "Enter" && !event.isComposing) {
event.preventDefault()
selectActive()
}
}}
aria-label={language.t("dialog.model.search.placeholder")}
showClearButton={!!store.search}
clearIcon="circle-xmark"
clearLabel={language.t("common.clear")}
onClearClick={() => setSearch("")}
/>
</div>
<div class="relative min-h-0" classList={{ "flex-1": !props.mobile }}>
<div
@@ -613,7 +603,7 @@ export function ModelSelectorPopoverView(props: {
onCloseAutoFocus={dismiss.onCloseAutoFocus}
>
<div class="flex flex-col p-0.5">
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-2.5 text-v2-icon-icon-muted">
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-1 text-v2-icon-icon-muted">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
<input
ref={(el) => (searchRef = el)}
@@ -663,12 +653,12 @@ export function ModelSelectorPopoverView(props: {
<Show when={store.search.trim()}>
<button
type="button"
class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
class="flex size-5 items-center justify-center rounded-sm bg-transparent text-v2-icon-icon-faint transition-colors hover:text-v2-icon-icon-base focus-visible:text-v2-icon-icon-base active:text-v2-icon-icon-base"
onPointerDown={(event) => event.preventDefault()}
onClick={() => setSearch("")}
aria-label={language.t("common.clear")}
>
<Icon name="close" size="small" />
<Icon name="circle-xmark" />
</button>
</Show>
</div>
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "የይለፍ ቃል",
"dialog.server.edit.title": "አገልጋይ አርትዕ",
"dialog.server.menu.edit": "አርትዕ",
"dialog.server.menu.default": "ነባሪ አዘጋጅ",
"dialog.server.menu.defaultRemove": "ነባሪውን አስወግድ",
"dialog.server.status.default": "ነባሪ",
"dialog.project.edit.title": "ፕሮጀክት አርትዕ",
"dialog.project.edit.icon": "አዶ",
"dialog.project.edit.icon.alt": "የፕሮጀክት አዶ",
-3
View File
@@ -297,9 +297,6 @@ export const dict = {
"dialog.server.add.password": "كلمة المرور (اختياري)",
"dialog.server.edit.title": "تحرير الخادم",
"dialog.server.menu.edit": "تعديل",
"dialog.server.menu.default": "تعيين كافتراضي",
"dialog.server.menu.defaultRemove": "إزالة الافتراضي",
"dialog.server.status.default": "افتراضي",
"dialog.project.edit.title": "تحرير المشروع",
"dialog.project.edit.icon": "أيقونة",
"dialog.project.edit.icon.alt": "أيقونة المشروع",
-3
View File
@@ -288,9 +288,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serveri redaktə et",
"dialog.server.menu.edit": "Redaktə et",
"dialog.server.menu.default": "Standart olaraq təyin et",
"dialog.server.menu.defaultRemove": "Standartı sil",
"dialog.server.status.default": "Standart",
"dialog.project.edit.title": "Layihəni redaktə et",
"dialog.project.edit.icon": "İkon",
"dialog.project.edit.icon.alt": "Layihə ikonu",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "парола",
"dialog.server.edit.title": "Редактиране на сървъра",
"dialog.server.menu.edit": "Редактиране",
"dialog.server.menu.default": "Задайте по подразбиране",
"dialog.server.menu.defaultRemove": "Премахване на подразбиране",
"dialog.server.status.default": "По подразбиране",
"dialog.project.edit.title": "Редактиране на проекта",
"dialog.project.edit.icon": "Икона",
"dialog.project.edit.icon.alt": "Икона на проекта",
-3
View File
@@ -290,9 +290,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "পাসওয়ার্ড",
"dialog.server.edit.title": "সার্ভার সম্পাদনা করুন",
"dialog.server.menu.edit": "সম্পাদনা করুন",
"dialog.server.menu.default": "ডিফল্ট হিসেবে সেট করুন",
"dialog.server.menu.defaultRemove": "ডিফল্ট সরান",
"dialog.server.status.default": "ডিফল্ট",
"dialog.project.edit.title": "প্রকল্প সম্পাদনা করুন",
"dialog.project.edit.icon": "আইকন",
"dialog.project.edit.icon.alt": "প্রকল্প আইকন",
-3
View File
@@ -508,9 +508,6 @@ export const dict = {
"dialog.server.add.password": "Senha (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.menu.edit": "Editar",
"dialog.server.menu.default": "Definir como padrão",
"dialog.server.menu.defaultRemove": "Remover padrão",
"dialog.server.status.default": "Padrão",
"dialog.project.edit.title": "Editar projeto",
"dialog.project.edit.icon": "Ícone",
"dialog.project.edit.icon.alt": "Ícone do projeto",
-3
View File
@@ -312,9 +312,6 @@ export const dict = {
"dialog.server.edit.title": "Uredi server",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Postavi kao podrazumijevano",
"dialog.server.menu.defaultRemove": "Ukloni podrazumijevano",
"dialog.server.status.default": "Podrazumijevano",
"dialog.project.edit.title": "Uredi projekat",
"dialog.project.edit.icon": "Ikonica",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "contrasenya",
"dialog.server.edit.title": "Edita el servidor",
"dialog.server.menu.edit": "Edita",
"dialog.server.menu.default": "Estableix com a predeterminat",
"dialog.server.menu.defaultRemove": "Elimina el valor predeterminat",
"dialog.server.status.default": "Per defecte",
"dialog.project.edit.title": "Edita el projecte",
"dialog.project.edit.icon": "Icona",
"dialog.project.edit.icon.alt": "Icona del projecte",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upravit server",
"dialog.server.menu.edit": "Upravit",
"dialog.server.menu.default": "Nastavit jako výchozí",
"dialog.server.menu.defaultRemove": "Odebrat výchozí",
"dialog.server.status.default": "Výchozí",
"dialog.project.edit.title": "Upravit projekt",
"dialog.project.edit.icon": "ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
-3
View File
@@ -446,9 +446,6 @@ export const dict = {
"dialog.server.edit.title": "Rediger server",
"dialog.server.menu.edit": "Rediger",
"dialog.server.menu.default": "Sæt som standard",
"dialog.server.menu.defaultRemove": "Fjern som standard",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Rediger projekt",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -442,9 +442,6 @@ export const dict = {
"dialog.server.add.password": "Passwort (optional)",
"dialog.server.edit.title": "Server bearbeiten",
"dialog.server.menu.edit": "Bearbeiten",
"dialog.server.menu.default": "Als Standard festlegen",
"dialog.server.menu.defaultRemove": "Standard entfernen",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Projekt bearbeiten",
"dialog.project.edit.icon": "Symbol",
"dialog.project.edit.icon.alt": "Projektsymbol",
-3
View File
@@ -292,9 +292,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "ޕާސްވަރޑް",
"dialog.server.edit.title": "އެޑިޓް ސަރވަރ",
"dialog.server.menu.edit": "ބަދަލު ގެނައުން",
"dialog.server.menu.default": "ޑިފޯލްޓް ގޮތުގައި ސެޓްކުރުން",
"dialog.server.menu.defaultRemove": "ޑިފޯލްޓް ނައްތާލާށެވެ",
"dialog.server.status.default": "ޑީފޯލްޓް",
"dialog.project.edit.title": "އެޑިޓް ޕްރޮޖެކްޓް",
"dialog.project.edit.icon": "އައިކޮން",
"dialog.project.edit.icon.alt": "ޕްރޮޖެކްޓް އައިކޮން",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "གསང༌ཨང",
"dialog.server.edit.title": "སར་བར་ཞུན་དག་འབད།",
"dialog.server.menu.edit": "ཞུན༌དག",
"dialog.server.menu.default": "སྔོན་སྒྲིག་སྦེ་གཞི་སྒྲིག་འབད།",
"dialog.server.menu.defaultRemove": "སྔོན་སྒྲིག་རྩ་བསྐྲད་གཏང་།",
"dialog.server.status.default": "འཐུས་ཤོར",
"dialog.project.edit.title": "ཞུན་དག་ལས་གཞི།",
"dialog.project.edit.icon": "ངོས་དཔར།",
"dialog.project.edit.icon.alt": "ལས་འགུལ་ངོས་དཔར།",
-3
View File
@@ -288,9 +288,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "κωδικός πρόσβασης",
"dialog.server.edit.title": "Επεξεργασία διακομιστή",
"dialog.server.menu.edit": "Επεξεργασία",
"dialog.server.menu.default": "Ορισμός ως προεπιλογή",
"dialog.server.menu.defaultRemove": "Κατάργηση προεπιλογής",
"dialog.server.status.default": "Προεπιλογή",
"dialog.project.edit.title": "Επεξεργασία έργου",
"dialog.project.edit.icon": "Εικονίδιο",
"dialog.project.edit.icon.alt": "Εικονίδιο έργου",
+1 -3
View File
@@ -86,6 +86,7 @@ export const dict = {
"palette.empty": "No results found",
"palette.group.commands": "Commands",
"palette.group.files": "Files",
"palette.group.recentSessions": "Recent sessions",
"dialog.provider.search.placeholder": "Search providers",
"dialog.provider.empty": "No providers found",
@@ -377,12 +378,9 @@ export const dict = {
"Your sign-in for this server is no longer valid. Paste a new pairing link, or enter the server password.",
"dialog.server.menu.edit": "Edit",
"dialog.server.menu.default": "Set as default",
"dialog.server.menu.defaultRemove": "Remove default",
"dialog.server.menu.remove": "Remove",
"dialog.server.menu.hide": "Hide from project list",
"dialog.server.menu.show": "Show in project list",
"dialog.server.status.default": "Default",
"server.row.incompatible":
"This server is running OpenCode {{version}}, which isn't compatible with this app. Upgrade it to OpenCode V2 to continue.",
-3
View File
@@ -535,9 +535,6 @@ export const dict = {
"dialog.server.edit.title": "Editar servidor",
"dialog.server.menu.edit": "Editar",
"dialog.server.menu.default": "Establecer como predeterminado",
"dialog.server.menu.defaultRemove": "Quitar como predeterminado",
"dialog.server.status.default": "Predeterminado",
"dialog.project.edit.title": "Editar proyecto",
"dialog.project.edit.icon": "Icono",
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parool",
"dialog.server.edit.title": "Muuda serverit",
"dialog.server.menu.edit": "Muuda",
"dialog.server.menu.default": "Määra vaikeväärtuseks",
"dialog.server.menu.defaultRemove": "Eemalda vaikeseade",
"dialog.server.status.default": "Vaikimisi",
"dialog.project.edit.title": "Redigeeri projekti",
"dialog.project.edit.icon": "Ikoon",
"dialog.project.edit.icon.alt": "Projekti ikoon",
-3
View File
@@ -295,9 +295,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "رمز عبور",
"dialog.server.edit.title": "ویرایش سرور",
"dialog.server.menu.edit": "ویرایش کنید",
"dialog.server.menu.default": "به عنوان پیش فرض تنظیم کنید",
"dialog.server.menu.defaultRemove": "حذف پیش فرض",
"dialog.server.status.default": "پیش فرض",
"dialog.project.edit.title": "ویرایش پروژه",
"dialog.project.edit.icon": "نماد",
"dialog.project.edit.icon.alt": "نماد پروژه",
-3
View File
@@ -424,9 +424,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "salasana",
"dialog.server.edit.title": "Muokkaa palvelinta",
"dialog.server.menu.edit": "Muokkaa",
"dialog.server.menu.default": "Aseta oletukseksi",
"dialog.server.menu.defaultRemove": "Poista oletuspalvelin",
"dialog.server.status.default": "Oletus",
"dialog.project.edit.title": "Muokkaa projektia",
"dialog.project.edit.icon": "Kuvake",
"dialog.project.edit.icon.alt": "Projektikuvake",
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "loyniorð",
"dialog.server.edit.title": "Rætta ambætara",
"dialog.server.menu.edit": "Rætta",
"dialog.server.menu.default": "Set sum forsett",
"dialog.server.menu.defaultRemove": "Strika forsett",
"dialog.server.status.default": "Forsett",
"dialog.project.edit.title": "Rætta verkætlan",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Verkætlanarmerki",
-3
View File
@@ -516,9 +516,6 @@ export const dict = {
"dialog.server.add.password": "Mot de passe (optionnel)",
"dialog.server.edit.title": "Modifier le serveur",
"dialog.server.menu.edit": "Modifier",
"dialog.server.menu.default": "Définir par défaut",
"dialog.server.menu.defaultRemove": "Ne plus utiliser par défaut",
"dialog.server.status.default": "Défaut",
"dialog.project.edit.title": "Modifier le projet",
"dialog.project.edit.icon": "Icône",
"dialog.project.edit.icon.alt": "Icône du projet",
-3
View File
@@ -294,9 +294,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "סיסמה",
"dialog.server.edit.title": "ערוך שרת",
"dialog.server.menu.edit": "ערוך",
"dialog.server.menu.default": "הגדר כברירת מחדל",
"dialog.server.menu.defaultRemove": "הסר את ברירת המחדל",
"dialog.server.status.default": "ברירת מחדל",
"dialog.project.edit.title": "ערוך פרויקט",
"dialog.project.edit.icon": "סמל",
"dialog.project.edit.icon.alt": "סמל פרויקט",
-3
View File
@@ -292,9 +292,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्वर संपादित करें",
"dialog.server.menu.edit": "संपादित करें",
"dialog.server.menu.default": "डिफाल्ट के रूप में सेट",
"dialog.server.menu.defaultRemove": "डिफ़ॉल्ट हटाएँ",
"dialog.server.status.default": "डिफ़ॉल्ट",
"dialog.project.edit.title": "प्रोजेक्ट संपादित करें",
"dialog.project.edit.icon": "आइकन",
"dialog.project.edit.icon.alt": "प्रोजेक्ट आइकन",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "lozinka",
"dialog.server.edit.title": "Uredi poslužitelj",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Postavi kao zadano",
"dialog.server.menu.defaultRemove": "Ukloni zadano",
"dialog.server.status.default": "Zadano",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projekta",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "jelszó",
"dialog.server.edit.title": "Szerver szerkesztése",
"dialog.server.menu.edit": "Szerkesztés",
"dialog.server.menu.default": "Beállítás alapértelmezettként",
"dialog.server.menu.defaultRemove": "Alapértelmezés eltávolítása",
"dialog.server.status.default": "Alapértelmezett",
"dialog.project.edit.title": "Projekt szerkesztése",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Projekt ikonra",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "գաղտնաբառ",
"dialog.server.edit.title": "Խմբագրել սերվերը",
"dialog.server.menu.edit": "Խմբագրել",
"dialog.server.menu.default": "Սահմանել որպես լռելյայն",
"dialog.server.menu.defaultRemove": "Հեռացնել լռելյայն",
"dialog.server.status.default": "Լռելյայն",
"dialog.project.edit.title": "Խմբագրել նախագիծը",
"dialog.project.edit.icon": "Պատկերակ",
"dialog.project.edit.icon.alt": "Նախագծի պատկերակ",
-3
View File
@@ -323,9 +323,6 @@ export const dict = {
"dialog.server.edit.title": "Sunting server",
"dialog.server.menu.edit": "Sunting",
"dialog.server.menu.default": "Tetapkan sebagai bawaan",
"dialog.server.menu.defaultRemove": "Hapus bawaan",
"dialog.server.status.default": "Bawaan",
"dialog.project.edit.title": "Sunting proyek",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "lykilorð",
"dialog.server.edit.title": "Breyta miðlara",
"dialog.server.menu.edit": "Breyta",
"dialog.server.menu.default": "Stillt sem sjálfgefið",
"dialog.server.menu.defaultRemove": "Fjarlægja sjálfgefið",
"dialog.server.status.default": "Sjálfgefið",
"dialog.project.edit.title": "Breyta verkefni",
"dialog.project.edit.icon": "Táknmynd",
"dialog.project.edit.icon.alt": "Verkefnistákn",
-3
View File
@@ -446,9 +446,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Modifica server",
"dialog.server.menu.edit": "Modifica",
"dialog.server.menu.default": "Imposta come predefinito",
"dialog.server.menu.defaultRemove": "Rimuovi predefinito",
"dialog.server.status.default": "Predefinito",
"dialog.project.edit.title": "Modifica progetto",
"dialog.project.edit.icon": "Icona",
"dialog.project.edit.icon.alt": "Icona del progetto",
-3
View File
@@ -296,9 +296,6 @@ export const dict = {
"dialog.server.add.password": "パスワード (オプション)",
"dialog.server.edit.title": "サーバーを編集",
"dialog.server.menu.edit": "編集",
"dialog.server.menu.default": "デフォルトに設定",
"dialog.server.menu.defaultRemove": "デフォルト設定を解除",
"dialog.server.status.default": "デフォルト",
"dialog.project.edit.title": "プロジェクトを編集",
"dialog.project.edit.icon": "アイコン",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "პაროლი",
"dialog.server.edit.title": "სერვერის რედაქტირება",
"dialog.server.menu.edit": "რედაქტირება",
"dialog.server.menu.default": "დაყენება ნაგულისხმევად",
"dialog.server.menu.defaultRemove": "ნაგულისხმევის ამოღება",
"dialog.server.status.default": "ნაგულისხმევი",
"dialog.project.edit.title": "პროექტის რედაქტირება",
"dialog.project.edit.icon": "ხატულა",
"dialog.project.edit.icon.alt": "პროექტის ხატულა",
-3
View File
@@ -293,9 +293,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "ពាក្យសម្ងាត់",
"dialog.server.edit.title": "កែសម្រួលម៉ាស៊ីនមេ",
"dialog.server.menu.edit": "កែសម្រួល",
"dialog.server.menu.default": "កំណត់ជាលំនាំដើម",
"dialog.server.menu.defaultRemove": "លុបលំនាំដើមចេញ",
"dialog.server.status.default": "លំនាំដើម",
"dialog.project.edit.title": "កែសម្រួលគម្រោង",
"dialog.project.edit.icon": "រូបតំណាង",
"dialog.project.edit.icon.alt": "រូបតំណាងគម្រោង",
-3
View File
@@ -218,9 +218,6 @@ export const dict = {
"dialog.server.add.password": "비밀번호 (선택 사항)",
"dialog.server.edit.title": "서버 편집",
"dialog.server.menu.edit": "편집",
"dialog.server.menu.default": "기본값으로 설정",
"dialog.server.menu.defaultRemove": "기본값 제거",
"dialog.server.status.default": "기본값",
"dialog.project.edit.title": "프로젝트 편집",
"dialog.project.edit.icon": "아이콘",
"dialog.project.edit.icon.alt": "프로젝트 아이콘",
-3
View File
@@ -293,9 +293,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "ລະຫັດຜ່ານ",
"dialog.server.edit.title": "ແກ້ໄຂເຊີບເວີ",
"dialog.server.menu.edit": "ແກ້ໄຂ",
"dialog.server.menu.default": "ຕັ້ງເປັນຄ່າເລີ່ມຕົ້ນ",
"dialog.server.menu.defaultRemove": "ເອົາຄ່າເລີ່ມຕົ້ນ",
"dialog.server.status.default": "ຄ່າເລີ່ມຕົ້ນ",
"dialog.project.edit.title": "ແກ້ໄຂໂຄງການ",
"dialog.project.edit.icon": "ໄອຄອນ",
"dialog.project.edit.icon.alt": "ໄອຄອນໂຄງການ",
-3
View File
@@ -299,9 +299,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "slaptažodis",
"dialog.server.edit.title": "Redaguoti serverį",
"dialog.server.menu.edit": "Redaguoti",
"dialog.server.menu.default": "Nustatyti kaip numatytąjį",
"dialog.server.menu.defaultRemove": "Pašalinti numatytąjį",
"dialog.server.status.default": "Numatytoji",
"dialog.project.edit.title": "Redaguoti projektą",
"dialog.project.edit.icon": "Piktograma",
"dialog.project.edit.icon.alt": "Projekto piktograma",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parole",
"dialog.server.edit.title": "Rediģēt serveri",
"dialog.server.menu.edit": "Rediģēt",
"dialog.server.menu.default": "Iestatīt kā noklusējumu",
"dialog.server.menu.defaultRemove": "Noņemt noklusējumu",
"dialog.server.status.default": "Noklusējuma",
"dialog.project.edit.title": "Rediģēt projektu",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Projekta ikona",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
"dialog.server.menu.edit": "Уреди",
"dialog.server.menu.default": "Поставете како стандардно",
"dialog.server.menu.defaultRemove": "Отстрани стандардно",
"dialog.server.status.default": "Стандардно",
"dialog.project.edit.title": "Уреди проект",
"dialog.project.edit.icon": "Икона",
"dialog.project.edit.icon.alt": "Икона на проектот",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "нууц үг",
"dialog.server.edit.title": "Сервер засах",
"dialog.server.menu.edit": "Засварлах",
"dialog.server.menu.default": "Өгөгдмөл болгож тохируулах",
"dialog.server.menu.defaultRemove": "Өгөгдмөлийг арилгах",
"dialog.server.status.default": "Өгөгдмөл",
"dialog.project.edit.title": "Төслийг засварлах",
"dialog.project.edit.icon": "Дүрс",
"dialog.project.edit.icon.alt": "Төслийн дүрс тэмдэг",
-3
View File
@@ -295,9 +295,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "kata laluan",
"dialog.server.edit.title": "Edit pelayan",
"dialog.server.menu.edit": "Sunting",
"dialog.server.menu.default": "Tetapkan sebagai lalai",
"dialog.server.menu.defaultRemove": "Buang lalai",
"dialog.server.status.default": "Lalai",
"dialog.project.edit.title": "Edit projek",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Ikon projek",
-3
View File
@@ -296,9 +296,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "စကားဝှက်",
"dialog.server.edit.title": "ဆာဗာကို တည်းဖြတ်ပါ။",
"dialog.server.menu.edit": "တည်းဖြတ်ရန်",
"dialog.server.menu.default": "မူရင်းအဖြစ် သတ်မှတ်ပါ။",
"dialog.server.menu.defaultRemove": "မူရင်းကို ဖယ်ရှားပါ။",
"dialog.server.status.default": "မူရင်း",
"dialog.project.edit.title": "ပရောဂျက်ကို တည်းဖြတ်ပါ။",
"dialog.project.edit.icon": "သင်္ကေတ",
"dialog.project.edit.icon.alt": "ပရောဂျက် သင်္ကေတ",
-3
View File
@@ -291,9 +291,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्भर सम्पादन गर्नुहोस्",
"dialog.server.menu.edit": "सम्पादन गर्नुहोस्",
"dialog.server.menu.default": "पूर्वनिर्धारित रूपमा सेट गर्नुहोस्",
"dialog.server.menu.defaultRemove": "पूर्वनिर्धारित हटाउनुहोस्",
"dialog.server.status.default": "पूर्वनिर्धारित",
"dialog.project.edit.title": "परियोजना सम्पादन गर्नुहोस्",
"dialog.project.edit.icon": "आइकन",
"dialog.project.edit.icon.alt": "परियोजना आइकन",
-3
View File
@@ -498,9 +498,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "wachtwoord",
"dialog.server.edit.title": "Server bewerken",
"dialog.server.menu.edit": "Bewerken",
"dialog.server.menu.default": "Als standaard instellen",
"dialog.server.menu.defaultRemove": "Standaard verwijderen",
"dialog.server.status.default": "Standaard",
"dialog.project.edit.title": "Project bewerken",
"dialog.project.edit.icon": "Pictogram",
"dialog.project.edit.icon.alt": "Projectpictogram",
-3
View File
@@ -507,9 +507,6 @@ export const dict = {
"dialog.server.edit.title": "Rediger server",
"dialog.server.menu.edit": "Rediger",
"dialog.server.menu.default": "Sett som standard",
"dialog.server.menu.defaultRemove": "Fjern standard",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Rediger prosjekt",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -293,9 +293,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور وچ ترمیم کرو",
"dialog.server.menu.edit": "ترمیم کرو",
"dialog.server.menu.default": "ڈیفالٹ دے طور تے سیٹ کرو",
"dialog.server.menu.defaultRemove": "ڈیفالٹ ہٹاؤ",
"dialog.server.status.default": "طے شدہ",
"dialog.project.edit.title": "پروجیکٹ وچ ترمیم کرو",
"dialog.project.edit.icon": "آئکن",
"dialog.project.edit.icon.alt": "پروجیکٹ آئیکن",
-3
View File
@@ -289,9 +289,6 @@ export const dict = {
"dialog.server.add.password": "Hasło (opcjonalnie)",
"dialog.server.edit.title": "Edytuj serwer",
"dialog.server.menu.edit": "Edytuj",
"dialog.server.menu.default": "Ustaw jako domyślny",
"dialog.server.menu.defaultRemove": "Usuń domyślny",
"dialog.server.status.default": "Domyślny",
"dialog.project.edit.title": "Edytuj projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parolă",
"dialog.server.edit.title": "Editează server",
"dialog.server.menu.edit": "Editează",
"dialog.server.menu.default": "Setează ca implicit",
"dialog.server.menu.defaultRemove": "Elimină implicitul",
"dialog.server.status.default": "Implicit",
"dialog.project.edit.title": "Editează proiectul",
"dialog.project.edit.icon": "Pictogramă",
"dialog.project.edit.icon.alt": "Pictograma proiectului",
-3
View File
@@ -311,9 +311,6 @@ export const dict = {
"dialog.server.edit.title": "Редактировать сервер",
"dialog.server.menu.edit": "Редактировать",
"dialog.server.menu.default": "Сделать по умолчанию",
"dialog.server.menu.defaultRemove": "Отменить выбор по умолчанию",
"dialog.server.status.default": "По умолч.",
"dialog.project.edit.title": "Редактировать проект",
"dialog.project.edit.icon": "Значок",
-3
View File
@@ -289,9 +289,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "මුරපදය",
"dialog.server.edit.title": "සේවාදායකය සංස්කරණය කරන්න",
"dialog.server.menu.edit": "සංස්කරණය කරන්න",
"dialog.server.menu.default": "පෙරනිමිය ලෙස සකසන්න",
"dialog.server.menu.defaultRemove": "පෙරනිමිය ඉවත් කරන්න",
"dialog.server.status.default": "පෙරනිමිය",
"dialog.project.edit.title": "ව්‍යාපෘතිය සංස්කරණය කරන්න",
"dialog.project.edit.icon": "නිරූපකය",
"dialog.project.edit.icon.alt": "ව්යාපෘති නිරූපකය",
-3
View File
@@ -311,9 +311,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upraviť server",
"dialog.server.menu.edit": "Upraviť",
"dialog.server.menu.default": "Nastaviť ako predvolený",
"dialog.server.menu.defaultRemove": "Odstrániť predvolený",
"dialog.server.status.default": "Predvolený",
"dialog.project.edit.title": "Upraviť projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
-3
View File
@@ -311,9 +311,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "geslo",
"dialog.server.edit.title": "Uredi strežnik",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Nastavi kot privzeto",
"dialog.server.menu.defaultRemove": "Odstrani privzeto",
"dialog.server.status.default": "Privzeto",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projekta",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "fjalëkalimin",
"dialog.server.edit.title": "Redakto serverin",
"dialog.server.menu.edit": "Redakto",
"dialog.server.menu.default": "Cakto si parazgjedhje",
"dialog.server.menu.defaultRemove": "Hiq parazgjedhjen",
"dialog.server.status.default": "E paracaktuar",
"dialog.project.edit.title": "Redakto projektin",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona e projektit",
-3
View File
@@ -299,9 +299,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
"dialog.server.menu.edit": "Уреди",
"dialog.server.menu.default": "Подесите као подразумевано",
"dialog.server.menu.defaultRemove": "Уклони подразумевано",
"dialog.server.status.default": "подразумевано",
"dialog.project.edit.title": "Уреди пројекат",
"dialog.project.edit.icon": "Икона",
"dialog.project.edit.icon.alt": "икона пројекта",
-3
View File
@@ -491,9 +491,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "lösenord",
"dialog.server.edit.title": "Redigera server",
"dialog.server.menu.edit": "Redigera",
"dialog.server.menu.default": "Ställ in som standard",
"dialog.server.menu.defaultRemove": "Ta bort standard",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Redigera projekt",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Projektikonen",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "парол",
"dialog.server.edit.title": "Серверро таҳрир кунед",
"dialog.server.menu.edit": "Таҳрир",
"dialog.server.menu.default": "Ҳамчун пешфарз таъин кунед",
"dialog.server.menu.defaultRemove": "Пешфарзро хориҷ кунед",
"dialog.server.status.default": "Пешфарз",
"dialog.project.edit.title": "Таҳрири лоиҳа",
"dialog.project.edit.icon": "Нишона",
"dialog.project.edit.icon.alt": "Нишонаи лоиҳа",
-3
View File
@@ -319,9 +319,6 @@ export const dict = {
"dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์",
"dialog.server.menu.edit": "แก้ไข",
"dialog.server.menu.default": "ตั้งเป็นค่าเริ่มต้น",
"dialog.server.menu.defaultRemove": "เอาค่าเริ่มต้นออก",
"dialog.server.status.default": "ค่าเริ่มต้น",
"dialog.project.edit.title": "แก้ไขโปรเจกต์",
"dialog.project.edit.icon": "ไอคอน",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serweri redaktirläň",
"dialog.server.menu.edit": "Redaktirläň",
"dialog.server.menu.default": "Dymmaklyk hökmünde düzüň",
"dialog.server.menu.defaultRemove": "Dymmaklygy aýyryň",
"dialog.server.status.default": "Bellenen",
"dialog.project.edit.title": "Taslamany redaktirläň",
"dialog.project.edit.icon": "Nyşan",
"dialog.project.edit.icon.alt": "Taslamanyň nyşany",
-3
View File
@@ -324,9 +324,6 @@ export const dict = {
"dialog.server.edit.title": "Sunucuyu düzenle",
"dialog.server.menu.edit": "Düzenle",
"dialog.server.menu.default": "Varsayılan olarak ayarla",
"dialog.server.menu.defaultRemove": "Varsayılanı kaldır",
"dialog.server.status.default": "Varsayılan",
"dialog.project.edit.title": "Projeyi düzenle",
"dialog.project.edit.icon": "Simge",
-3
View File
@@ -341,9 +341,6 @@ export const dict = {
"dialog.server.edit.title": "Редагувати сервер",
"dialog.server.menu.edit": "Редагувати",
"dialog.server.menu.default": "Встановити за замовчуванням",
"dialog.server.menu.defaultRemove": "Скасувати використання за замовчуванням",
"dialog.server.status.default": "За замовчуванням",
"dialog.project.edit.title": "Редагувати проєкт",
"dialog.project.edit.icon": "Іконка",
-3
View File
@@ -295,9 +295,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور میں ترمیم کریں۔",
"dialog.server.menu.edit": "ترمیم کریں۔",
"dialog.server.menu.default": "بطور ڈیفالٹ سیٹ کریں۔",
"dialog.server.menu.defaultRemove": "ڈیفالٹ کو ہٹا دیں۔",
"dialog.server.status.default": "طے شدہ",
"dialog.project.edit.title": "پروجیکٹ میں ترمیم کریں۔",
"dialog.project.edit.icon": "آئیکن",
"dialog.project.edit.icon.alt": "پروجیکٹ کا آئیکن",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serverni tahrirlash",
"dialog.server.menu.edit": "Tahrirlash",
"dialog.server.menu.default": "Standart sifatida o'rnating",
"dialog.server.menu.defaultRemove": "Standartni olib tashlang",
"dialog.server.status.default": "Standart",
"dialog.project.edit.title": "Loyihani tahrirlash",
"dialog.project.edit.icon": "Belgi",
"dialog.project.edit.icon.alt": "Loyiha belgisi",
-3
View File
@@ -299,9 +299,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "mật khẩu",
"dialog.server.edit.title": "Chỉnh sửa máy chủ",
"dialog.server.menu.edit": "Chỉnh sửa",
"dialog.server.menu.default": "Đặt làm mặc định",
"dialog.server.menu.defaultRemove": "Xóa mặc định",
"dialog.server.status.default": "Mặc định",
"dialog.project.edit.title": "Chỉnh sửa dự án",
"dialog.project.edit.icon": "Biểu tượng",
"dialog.project.edit.icon.alt": "Biểu tượng dự án",
-3
View File
@@ -336,9 +336,6 @@ export const dict = {
"dialog.server.add.password": "密码(可选)",
"dialog.server.edit.title": "编辑服务器",
"dialog.server.menu.edit": "编辑",
"dialog.server.menu.default": "设为默认",
"dialog.server.menu.defaultRemove": "取消默认",
"dialog.server.status.default": "默认",
"dialog.project.edit.title": "编辑项目",
"dialog.project.edit.icon": "图标",
-3
View File
@@ -321,9 +321,6 @@ export const dict = {
"dialog.server.edit.title": "編輯伺服器",
"dialog.server.menu.edit": "編輯",
"dialog.server.menu.default": "設為預設",
"dialog.server.menu.defaultRemove": "取消預設",
"dialog.server.status.default": "預設",
"dialog.project.edit.title": "編輯專案",
"dialog.project.edit.icon": "圖示",
@@ -2,7 +2,6 @@ import { createSimpleContext } from "@opencode/ui/context"
import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
import type { Accessor } from "solid-js"
import type { DesktopMenuAction } from "@/shell/commands/desktop-menu"
import { ServerConnection } from "@/runtime/server/registry"
import type { DraftStore } from "@/runtime/persistence/drafts"
import type { Bridge } from "@opencode/gui-extensions/sdk/bridge"
@@ -81,12 +80,6 @@ type PlatformBase = {
/** Fetch override */
fetch?: typeof fetch
/** Get the configured default server URL (platform-specific) */
getDefaultServer?(): Promise<ServerConnection.Key | null>
/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServer?(url: ServerConnection.Key | null): Promise<void> | void
/** Webview zoom level (desktop only) */
webviewZoom?: Accessor<number>
+1 -40
View File
@@ -1,13 +1,7 @@
import { createBrowserDraftStore } from "@/runtime/persistence/drafts"
import { ServerConnection } from "@/runtime/server/registry"
import type { Platform } from "./platform"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
export function createWebPlatform(version: string) {
const currentServerUrl = getCurrentServerUrl()
const storedServerUrl = readDefaultServerUrl()
const platform: Platform = {
platform: "web",
draftStore: createBrowserDraftStore(),
@@ -43,18 +37,11 @@ export function createWebPlatform(version: string) {
notification.close()
}
},
getDefaultServer: async () => {
const stored = readDefaultServerUrl()
return stored ? ServerConnection.Key.make(stored) : null
},
setDefaultServer: writeDefaultServerUrl,
}
return {
platform,
currentServerUrl,
defaultServerUrl: storedServerUrl ?? currentServerUrl,
currentServerUrl: getCurrentServerUrl(),
}
}
@@ -72,29 +59,3 @@ function getCurrentServerUrl() {
return location.origin
}
function readDefaultServerUrl() {
if (typeof localStorage === "undefined") return null
try {
return localStorage.getItem(DEFAULT_SERVER_URL_KEY)
} catch {
return null
}
}
function writeDefaultServerUrl(value: string | null) {
if (typeof localStorage === "undefined") return
try {
if (value !== null) {
localStorage.setItem(DEFAULT_SERVER_URL_KEY, value)
return
}
localStorage.removeItem(DEFAULT_SERVER_URL_KEY)
} catch {
return
}
}
+1 -5
View File
@@ -214,11 +214,7 @@ export namespace ServerConnection {
export const { use: useServers, provider: ServersProvider } = createSimpleContext({
name: "Server",
gate: true,
init: (props: {
defaultServer?: ServerConnection.Key
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
}) => {
init: (props: { canonicalLocalServer?: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
const [store, setStore, _, hydrated] = persisted(
{
...Persist.global("server"),
@@ -5,21 +5,19 @@ import { sortServerConnections } from "./controller"
const server = (url: string): ServerConnection.Http => ({ type: "http", http: { url } })
describe("sortServerConnections", () => {
test("places the default first and preserves health and insertion ordering", () => {
test("orders by health and preserves insertion ordering", () => {
const first = server("http://first")
const offline = server("http://offline")
const preferred = server("http://preferred")
const unknown = server("http://unknown")
const result = sortServerConnections({
servers: [first, offline, preferred, unknown],
servers: [first, offline, unknown],
health: {
[ServerConnection.key(first)]: { healthy: true },
[ServerConnection.key(offline)]: { healthy: false },
},
defaultKey: ServerConnection.key(preferred),
})
expect(result).toEqual([preferred, first, unknown, offline])
expect(result).toEqual([first, unknown, offline])
})
})
@@ -1,58 +1,15 @@
import { createMemo, createResource } from "solid-js"
import { createMemo } from "solid-js"
import { useGlobal } from "@/runtime/server/runtime"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { ServerConnection, useServers } from "@/runtime/server/registry"
import { useTabs } from "@/shell/tabs/tabs"
import { type ServerHealth } from "@/runtime/server/health"
import { showToast } from "@/shell/notifications/toast"
import { useExtensionServers } from "@/runtime/extension/servers"
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
function useDefaultServer() {
const language = useLanguage()
const platform = usePlatform()
const [defaultKey, defaultKeyActions] = createResource(
async () => {
try {
return (await platform.getDefaultServer?.()) ?? null
} catch (err) {
showRequestError(language, err)
return null
}
},
{ initialValue: null },
)
const set = async (key: ServerConnection.Key | null) => {
try {
await platform.setDefaultServer?.(key)
defaultKeyActions.mutate(key)
} catch (err) {
showRequestError(language, err)
}
}
return {
key: () => defaultKey.latest,
available: createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer),
set,
}
}
export function sortServerConnections(input: {
servers: ServerConnection.Any[]
health: Record<string, ServerHealth | undefined>
defaultKey: ServerConnection.Key | null
}) {
const order = new Map(input.servers.map((item, index) => [item, index] as const))
@@ -65,10 +22,6 @@ export function sortServerConnections(input: {
}
return input.servers.slice().sort((a, b) => {
const preferred =
Number(ServerConnection.key(b) === input.defaultKey) - Number(ServerConnection.key(a) === input.defaultKey)
if (preferred !== 0) return preferred
const health = rank(input.health[ServerConnection.key(a)]) - rank(input.health[ServerConnection.key(b)])
if (health !== 0) return health
@@ -81,24 +34,23 @@ export function useServerActionsController() {
const server = useServers()
const extensions = useExtensionServers()
const tabs = useTabs()
const platform = usePlatform()
const language = useLanguage()
const defaults = useDefaultServer()
const remove = async (key: ServerConnection.Key) => {
try {
await extensions.entry(key)?.entry.remove?.()
tabs.removeServer(key)
server.remove(key)
if ((await platform.getDefaultServer?.()) === key) await defaults.set(null)
} catch (err) {
showRequestError(language, err)
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
}
return {
defaults,
connection: {
canRemove: server.canRemove,
remove,
@@ -126,7 +78,6 @@ export function useServerCollectionController() {
sortServerConnections({
servers: items(),
health: global.servers.health,
defaultKey: actions.defaults.key(),
}),
)
@@ -23,11 +23,6 @@ export function ExtensionServerRow(props: { server: ServerConnection.Key; contro
authenticationRequired={indicator.auth}
/>
),
default: {
available: () => props.controller.defaults.available(),
current: () => props.controller.defaults.key() === props.server,
set: (value) => void props.controller.defaults.set(value ? props.server : null),
},
remove: () => props.controller.connection.remove(props.server),
Items: () => <ServerRowItems server={props.server} />,
}
@@ -20,14 +20,10 @@ export const ServerRowMenu: Component<{
<ServerRowMenuView
server={props.server}
labels={serverMenuLabels(language)}
canDefault={props.domain.defaults.available()}
isDefault={props.domain.defaults.key() === key}
canRemove={props.domain.connection.canRemove(key)}
canHide={props.domain.connection.canHide(key)}
hidden={props.domain.connection.isHidden(key)}
onEdit={props.onEdit}
onSetDefault={() => props.domain.defaults.set(key)}
onRemoveDefault={() => props.domain.defaults.set(null)}
onRemove={() => props.domain.connection.remove(key)}
onHide={() => props.domain.connection.setHidden(key, true)}
onShow={() => props.domain.connection.setHidden(key, false)}
@@ -42,8 +38,6 @@ export function serverMenuLabels(language: ReturnType<typeof useLanguage>) {
more: language.t("common.moreOptions"),
server: language.t("settings.section.server"),
edit: language.t("dialog.server.menu.edit"),
default: language.t("dialog.server.menu.default"),
defaultRemove: language.t("dialog.server.menu.defaultRemove"),
remove: language.t("dialog.server.menu.remove"),
hide: language.t("dialog.server.menu.hide"),
show: language.t("dialog.server.menu.show"),
@@ -53,14 +47,10 @@ export function serverMenuLabels(language: ReturnType<typeof useLanguage>) {
export const ServerRowMenuView: Component<{
server: ServerConnection.Any
labels: ReturnType<typeof serverMenuLabels>
canDefault: boolean
isDefault: boolean
canRemove: boolean
canHide?: boolean
hidden?: boolean
onEdit: (server: ServerConnection.Http) => void
onSetDefault: () => void
onRemoveDefault: () => void
onRemove: () => void
onHide?: () => void
onShow?: () => void
@@ -93,12 +83,6 @@ export const ServerRowMenuView: Component<{
>
{props.labels.edit}
</Menu.Item>
<Show when={props.canDefault && !props.isDefault}>
<Menu.Item onSelect={props.onSetDefault}>{props.labels.default}</Menu.Item>
</Show>
<Show when={props.canDefault && props.isDefault}>
<Menu.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</Menu.Item>
</Show>
<Show when={props.hidden}>
<Menu.Item onSelect={props.onShow}>{props.labels.show}</Menu.Item>
</Show>
@@ -1,4 +1,3 @@
import { Badge } from "@opencode/ui/badge"
import { Button } from "@opencode/ui/button"
import { useDialog } from "@opencode/ui/context/dialog"
import { createMemo, Show, type Component } from "solid-js"
@@ -82,9 +81,6 @@ export const SettingsServerGeneral: Component<{
</Button>
)}
</Show>
<Show when={controller.defaults.available() && controller.defaults.key() === props.entry.key}>
<Badge>{language.t("dialog.server.status.default")}</Badge>
</Show>
<ServerRowMenu server={server()} domain={controller} onEdit={edit} />
</div>
</div>
-8
View File
@@ -1217,14 +1217,6 @@
.settings-project-card[data-menu="true"] {
background: var(--v2-background-bg-layer-02);
}
.settings-tab-search-clear {
position: absolute;
top: 50%;
inset-inline-end: 6px;
z-index: 1;
transform: translateY(-50%);
}
.settings-models {
gap: 12px;
}
+1 -7
View File
@@ -248,13 +248,7 @@ function RootSettings() {
const ordered = createMemo(() => {
const order = new Map(list().map((server, index) => [ServerConnection.key(server), index]))
return inventory().toSorted((a, b) => {
const preferred = Number(b.key === servers.defaults.key()) - Number(a.key === servers.defaults.key())
if (preferred) return preferred
return (order.get(a.key) ?? list().length) - (order.get(b.key) ?? list().length)
})
return inventory().toSorted((a, b) => (order.get(a.key) ?? list().length) - (order.get(b.key) ?? list().length))
})
const sourceServer = createMemo(() => {
+11 -10
View File
@@ -73,7 +73,7 @@ export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => vo
}
/** The highlighted option's preview cleanup, and whether the palette committed a choice. */
type PaletteHighlight = { cleanup: (() => void) | void; committed: boolean }
export type PaletteHighlight = { cleanup: (() => void) | void; committed: boolean }
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
const command = useCommand()
@@ -148,6 +148,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
get: (sessionID, signal) => serverSDK.api.session.get({ sessionID }, { signal }),
untitled: () => language.t("command.session.new"),
category: () => language.t("command.category.session"),
recentCategory: () => language.t("palette.group.recentSessions"),
})
const highlight = (item: CommandPaletteEntry | undefined) => {
@@ -232,6 +233,7 @@ export function createServerSessionEntries(props: {
get: (sessionID: string, signal: AbortSignal) => Promise<SessionInfo>
untitled: () => string
category: () => string
recentCategory: () => string
}) {
let abort: AbortController | undefined
@@ -240,17 +242,13 @@ export function createServerSessionEntries(props: {
return async (text: string): Promise<CommandPaletteEntry[]> => {
const search = text.trim()
if (!search) {
abort?.abort()
return []
}
abort?.abort()
const current = new AbortController()
abort = current
// Typed searches wait for a pause; an empty query lists recent sessions right away.
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 100)
const timer = setTimeout(resolve, search ? 100 : 0)
current.signal.addEventListener(
"abort",
() => {
@@ -264,8 +262,9 @@ export function createServerSessionEntries(props: {
if (current.signal.aborted) return []
const opened = props.opened()
const stored = props.stored().map((project) => ({ ...project, expanded: false }))
const category = search ? props.category() : props.recentCategory()
return Promise.all([
const entries = await Promise.all([
props.load(search, current.signal).then(
(result) => result.data,
() => [],
@@ -289,7 +288,7 @@ export function createServerSessionEntries(props: {
type: "session" as const,
title: session.title || props.untitled(),
description: project ? displayName(project) : getFilename(session.location.directory),
category: props.category(),
category,
directory: session.location.directory,
sessionID: session.id,
server: props.server,
@@ -300,5 +299,7 @@ export function createServerSessionEntries(props: {
},
),
)
return search ? entries : entries.slice(0, ENTRY_LIMIT)
}
}
+37 -5
View File
@@ -1,4 +1,4 @@
import type { SessionTab, Tab } from "./tabs"
import type { SessionTab, Tab, TabInfo } from "./tabs"
import type { TabStorage } from "./schema"
export type ClosedTab = typeof TabStorage.ClosedTab.Type
@@ -7,15 +7,43 @@ const CLOSED_TAB_LIMIT = 25
// Only session tabs are recorded; closing a draft tab deletes its persisted
// state, so a reopened draft would come back empty anyway.
export function pushClosedTab(stack: ClosedTab[], tab: Tab, index: number): ClosedTab[] {
export function pushClosedTab(stack: ClosedTab[], tab: Tab, index: number, info?: TabInfo): ClosedTab[] {
if (tab.type !== "session") return stack
return [...stack, { tab: { ...tab }, index }].slice(-CLOSED_TAB_LIMIT)
const entry: ClosedTab = { tab: { ...tab }, index }
if (info) entry.info = { ...info }
return [...stack.filter((item) => !sameTab(item.tab, tab)), entry].slice(-CLOSED_TAB_LIMIT)
}
export function listClosedTabs(stack: ClosedTab[], tabs: Tab[]) {
const seen = new Set<string>()
return stack.toReversed().filter((entry) => {
const key = `${entry.tab.server}\n${entry.tab.sessionId}`
if (seen.has(key)) return false
seen.add(key)
return !isOpen(tabs, entry.tab)
})
}
export type TakenClosedTab = { entry?: ClosedTab; stack: ClosedTab[] }
// Pops the most recently closed tab that is not open again,
// discarding stale entries along the way.
export function takeClosedTab(stack: ClosedTab[], tabs: Tab[]): { entry?: ClosedTab; stack: ClosedTab[] } {
export function takeClosedTab(stack: ClosedTab[], tabs: Tab[], target?: SessionTab): TakenClosedTab {
if (target) {
const index = stack.findLastIndex((entry) => sameTab(entry.tab, target))
const entry = stack[index]
if (!entry || isOpen(tabs, entry.tab)) return { stack }
return { entry, stack: [...stack.slice(0, index), ...stack.slice(index + 1)] }
}
const remaining = [...stack]
while (remaining.length) {
@@ -40,5 +68,9 @@ export function nextTabAfterClose(tabs: Tab[], index: number, active: boolean) {
}
function isOpen(tabs: Tab[], tab: SessionTab) {
return tabs.some((item) => item.type === "session" && item.server === tab.server && item.sessionId === tab.sessionId)
return tabs.some((item) => item.type === "session" && sameTab(item, tab))
}
function sameTab(first: SessionTab, second: SessionTab) {
return first.server === second.server && first.sessionId === second.sessionId
}
+6 -1
View File
@@ -42,6 +42,7 @@ export const Recent = Persistence.struct({
export const Info = Persistence.struct({
title: Schema.optional(Schema.String),
directory: Schema.optional(Schema.String),
prompted: Schema.optional(Schema.Boolean),
})
export const Infos = Schema.Record(Schema.String, Schema.mutableKey(Info))
@@ -59,7 +60,11 @@ export const Regions = Schema.Record(
),
)
export const ClosedTab = Schema.Struct({ tab: SessionCodec, index: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) })
export const ClosedTab = Persistence.struct({
tab: SessionCodec,
index: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
info: Persistence.optional(Info),
})
export const Closed = Persistence.array(ClosedTab)
+94 -7
View File
@@ -1,7 +1,14 @@
import { describe, expect, test } from "bun:test"
import { createRoot, getOwner, onCleanup } from "solid-js"
import { createTabMemory } from "./memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed"
import {
listClosedTabs,
nextTabAfterClose,
pushClosedTab,
removeClosedTabs,
takeClosedTab,
type ClosedTab,
} from "./closed"
import { findSessionTab, sessionIDHasOpenTab, tabHref, tabKey, type SessionTab, type Tab } from "./tabs"
import { Schema } from "effect"
import { TabStorage } from "./schema"
@@ -144,10 +151,21 @@ describe("tab memory", () => {
})
describe("closed tab stack", () => {
test("records session tabs with their index and ignores draft tabs", () => {
test("records session tabs with their index", () => {
const stack = pushClosedTab([], sessionTab("a"), 2, {
title: "Alpha",
directory: "/project",
prompted: true,
})
expect(stack).toEqual([
{ tab: sessionTab("a"), index: 2, info: { title: "Alpha", directory: "/project", prompted: true } },
])
})
test("ignores draft tabs", () => {
const draft: Tab = { type: "draft", draftID: "d1", server, directory: "/tmp" }
expect(pushClosedTab([], sessionTab("a"), 2)).toEqual([{ tab: sessionTab("a"), index: 2 }])
expect(pushClosedTab([], draft, 0)).toEqual([])
})
@@ -162,16 +180,85 @@ describe("closed tab stack", () => {
expect(stack.at(-1)?.tab.sessionId).toBe("s29")
})
test("pops the most recently closed tab that is not already open", () => {
test("keeps only the newest close record for a session", () => {
const stack = pushClosedTab(
pushClosedTab([], sessionTab("a"), 1, { title: "Old", directory: "/old", prompted: true }),
sessionTab("a"),
3,
{ title: "New", directory: "/new", prompted: true },
)
expect(stack).toEqual([
{ tab: sessionTab("a"), index: 3, info: { title: "New", directory: "/new", prompted: true } },
])
})
test("pops the most recently closed tab", () => {
const stack = [
{ tab: sessionTab("a"), index: 0 },
{ tab: sessionTab("b"), index: 1 },
]
expect(takeClosedTab(stack, [])).toEqual({ entry: stack[1], stack: [stack[0]] })
expect(takeClosedTab(stack, [sessionTab("b")])).toEqual({ entry: stack[0], stack: [] })
expect(takeClosedTab(stack, [sessionTab("a"), sessionTab("b")])).toEqual({ entry: undefined, stack: [] })
const result = takeClosedTab(stack, [])
expect(result.entry?.tab.sessionId).toBe("b")
expect(result.stack).toEqual([{ tab: sessionTab("a"), index: 0 }])
})
test("skips entries whose tab is already open", () => {
const stack = [
{ tab: sessionTab("a"), index: 0 },
{ tab: sessionTab("b"), index: 1 },
]
const result = takeClosedTab(stack, [sessionTab("b")])
expect(result.entry?.tab.sessionId).toBe("a")
expect(result.stack).toEqual([])
})
test("lists closed tabs from newest to oldest and excludes open tabs", () => {
const stack = [
{ tab: sessionTab("a"), index: 0 },
{ tab: sessionTab("b"), index: 1 },
{ tab: sessionTab("c"), index: 2 },
]
expect(listClosedTabs(stack, [sessionTab("b")]).map((entry) => entry.tab.sessionId)).toEqual(["c", "a"])
})
test("deduplicates previously stored close records", () => {
const stack = [
{ tab: sessionTab("a"), index: 0 },
{ tab: sessionTab("b"), index: 1 },
{ tab: sessionTab("a"), index: 2 },
]
expect(listClosedTabs(stack, []).map((entry) => [entry.tab.sessionId, entry.index])).toEqual([
["a", 2],
["b", 1],
])
})
test("takes a selected closed tab without removing newer entries", () => {
const stack = [
{ tab: sessionTab("a"), index: 0 },
{ tab: sessionTab("b"), index: 1 },
{ tab: sessionTab("c"), index: 2 },
]
const result = takeClosedTab(stack, [], sessionTab("a"))
expect(result.entry?.tab.sessionId).toBe("a")
expect(result.stack.map((entry) => entry.tab.sessionId)).toEqual(["b", "c"])
})
test("returns no entry when everything is open or empty", () => {
expect(takeClosedTab([], []).entry).toBeUndefined()
const result = takeClosedTab([{ tab: sessionTab("a"), index: 0 }], [sessionTab("a")])
expect(result.entry).toBeUndefined()
expect(result.stack).toEqual([])
})
test("purges removed sessions", () => {
+22 -9
View File
@@ -416,24 +416,26 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
if (!tab) return
if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index))
if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index, info[tabKey(tab)]))
removeTab(index)
},
reopenClosedTab() {
reopenClosedTab(target?: SessionTab, options?: { append?: boolean }) {
if (!closedReady()) {
void closedReady.promise?.then(() => actions.reopenClosedTab())
void closedReady.promise?.then(() => actions.reopenClosedTab(target, options))
return
}
const result = takeClosedTab(closed, store)
const result = takeClosedTab(closed, store, target)
if (result.stack.length === closed.length) return
setClosed(() => result.stack)
const entry = result.entry
if (!entry) return
const index = Math.min(entry.index, store.length)
const index = options?.append ? store.length : Math.min(entry.index, store.length)
if (entry.info) setInfo(tabKey(entry.tab), entry.info)
void startTransition(() => {
setStore(
produce((tabs) => {
@@ -532,12 +534,23 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
for (const key of removed) removeInfo(key)
},
rememberSessionInfo(tab: SessionTab, session: SessionInfo) {
rememberSessionInfo(tab: SessionTab, session: SessionInfo, prompted: boolean) {
const key = tabKey(tab)
const next = { title: session.title, directory: session.location.directory }
const current = info[key]
if (current && current.title === next.title && current.directory === next.directory) return
const next = {
title: session.title,
directory: session.location.directory,
prompted: current?.prompted === true || prompted,
}
if (
current &&
current.title === next.title &&
current.directory === next.directory &&
current.prompted === next.prompted
)
return
console.debug("[tabs] update persisted session info", { key, sessionID: session.id, current, next })
setInfo(key, next)
},
@@ -626,6 +639,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
}
return { ...actions, store, info, ready, infoReady, recentReady, regionsReady }
return { ...actions, store, info, closed, ready, infoReady, recentReady, closedReady, regionsReady }
},
})
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff. Show more