mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-08 00:08:23 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4327a67fa | ||
|
|
6b9f03b13f | ||
|
|
5183ea45c3 | ||
|
|
053e534e5a | ||
|
|
8bd8960154 | ||
|
|
d5154fb74e | ||
|
|
70c6c8cbc5 | ||
|
|
e2979039f1 | ||
|
|
119cb15d1d | ||
|
|
e1b7299bc9 | ||
|
|
68aa4f8017 | ||
|
|
69219470a2 |
No files matched your search
@@ -33,7 +33,7 @@ const Options = Schema.Struct({
|
||||
store: lenient(Schema.Boolean),
|
||||
thinkingLevel: lenient(ThinkingLevel),
|
||||
thinkingSummaries: lenient(knownString<"auto" | "none">()),
|
||||
serviceTier: lenient(knownString<"standard" | "flex" | "priority">()),
|
||||
serviceTier: lenient(knownString<"standard" | "flex" | "priority" | "deferred">()),
|
||||
})
|
||||
export type OptionsInput = typeof Options.Encoded
|
||||
export type ProviderOptionsInput = OptionsInput
|
||||
|
||||
@@ -9,7 +9,8 @@ export type OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity
|
||||
// in lockstep with `openai-node/src/resources/responses/responses.ts`.
|
||||
export const OpenAIResponseIncludables = OpenResponsesOptions.ResponseIncludables
|
||||
export type OpenAIResponseIncludable = OpenResponsesOptions.ResponseIncludable
|
||||
export const OpenAIServiceTiers = [...OpenResponsesOptions.ServiceTiers, "scale"] as const
|
||||
// Mirrors OpenAI's `ServiceTier` union from the official SDK.
|
||||
export const OpenAIServiceTiers = [...OpenResponsesOptions.ServiceTiers, "scale", "fast", "ultrafast"] as const
|
||||
export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] | (string & {})
|
||||
|
||||
export const OpenAIReasoningEffort = OpenResponsesOptions.ReasoningEffort
|
||||
|
||||
@@ -9,6 +9,7 @@ LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffo
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "low" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "scale" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "ultrafast" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "future-tier" } })
|
||||
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "max" } })
|
||||
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
|
||||
|
||||
@@ -325,11 +325,13 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("passes through provider-defined service tiers", () =>
|
||||
it.effect("passes through provider-defined and future service tiers", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(LLMRequest.update(request, { providerOptions: { serviceTier: "scale" } }))
|
||||
for (const serviceTier of ["scale", "ultrafast", "future-tier"]) {
|
||||
const prepared = yield* compileRequest(LLMRequest.update(request, { providerOptions: { serviceTier } }))
|
||||
|
||||
expect(prepared.body.service_tier).toBe("scale")
|
||||
expect(prepared.body.service_tier).toBe(serviceTier)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -143,7 +143,7 @@ test("open file tab browses, searches, and tracks missing files", async ({ page
|
||||
const panel = page.locator("#review-panel")
|
||||
const sidebar = panel.locator('[data-slot="session-review-v2-sidebar"]')
|
||||
const sidebarToggle = panel.getByRole("button", { name: "Toggle file tree" })
|
||||
const contextButton = page.getByRole("button", { name: "View context usage" })
|
||||
const contextButton = page.getByRole("button", { name: "Toggle session context" })
|
||||
const openFile = panel.getByRole("button", { name: "Open file" })
|
||||
const tab = (name: string) => panel.getByRole("tab", { name, exact: true })
|
||||
await contextButton.click()
|
||||
@@ -309,7 +309,7 @@ test("context closes the side region only when its button opened it", async ({ p
|
||||
})
|
||||
const panel = page.locator("#review-panel")
|
||||
const toggle = page.getByRole("button", { name: "Toggle review", exact: true })
|
||||
const contextButton = page.getByRole("button", { name: "View context usage" })
|
||||
const contextButton = page.getByRole("button", { name: "Toggle session context" })
|
||||
const context = panel.getByRole("tab", { name: "Context", exact: true })
|
||||
|
||||
await contextButton.click()
|
||||
@@ -630,7 +630,7 @@ test("restores review state and the side-panel tab per session", async ({ page }
|
||||
await page.getByRole("option", { name: "Branch changes" }).click()
|
||||
await page.getByRole("button", { name: "beta.ts" }).click()
|
||||
await selectedFile("beta.ts")
|
||||
await page.getByRole("button", { name: "View context usage" }).click()
|
||||
await page.getByRole("button", { name: "Toggle session context" }).click()
|
||||
await selectedTab("Context")
|
||||
|
||||
await switchSession("Gamma review state")
|
||||
|
||||
@@ -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} />}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -7,14 +7,23 @@ import type {
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { formatServerError } from "@/runtime/server/errors"
|
||||
import { useData } from "@/runtime/server/current"
|
||||
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
|
||||
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
||||
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" | "external" }>
|
||||
|
||||
type Authorization = IntegrationOauthConnectOutput["data"]
|
||||
|
||||
type Polling = {
|
||||
generation: number
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
disposed: boolean
|
||||
// An attempt the server still considers open; cancelled when the dialog goes away.
|
||||
attempt?: Authorization
|
||||
}
|
||||
|
||||
// OpenCode Go and OpenCode Zen both bill through the OpenCode Console, so the
|
||||
// Console sign-in is the connection method for both providers.
|
||||
export const CONSOLE_INTEGRATION = "opencode"
|
||||
@@ -35,7 +44,7 @@ export function providerFormDefaults(fields: ProviderConnectMethod["form"]) {
|
||||
if (actual === undefined) return false
|
||||
|
||||
const equal = Array.isArray(actual)
|
||||
? typeof condition.value === "string" && actual.includes(condition.value)
|
||||
? actual.some((item) => item === condition.value)
|
||||
: actual === condition.value
|
||||
|
||||
return condition.op === "eq" ? equal : !equal
|
||||
@@ -43,7 +52,7 @@ export function providerFormDefaults(fields: ProviderConnectMethod["form"]) {
|
||||
|
||||
if (!active) return answer
|
||||
|
||||
return { ...answer, [field.key]: field.default }
|
||||
return Object.assign(answer, { [field.key]: field.default })
|
||||
}, {})
|
||||
}
|
||||
|
||||
@@ -74,9 +83,8 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
// Not createResource: the dialog is owned by whichever page opened it, so reading a pending
|
||||
// resource here would suspend that page's <Suspense> and blank the screen behind the dialog.
|
||||
const [integration, setIntegration] = createStore({
|
||||
const [integration, setIntegration] = createStore<{ loading: boolean; latest?: IntegrationInfo }>({
|
||||
loading: true,
|
||||
latest: undefined as IntegrationInfo | undefined,
|
||||
})
|
||||
|
||||
createEffect(
|
||||
@@ -100,7 +108,8 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
const methods = createMemo<ProviderConnectMethod[]>(() => {
|
||||
const values = integration.latest?.methods.filter(
|
||||
(method): method is ProviderConnectMethod => method.type === "key" || method.type === "oauth",
|
||||
(method): method is ProviderConnectMethod =>
|
||||
method.type === "key" || method.type === "oauth" || method.type === "external",
|
||||
)
|
||||
|
||||
if (values?.length) return [...values]
|
||||
@@ -108,14 +117,19 @@ export function createProviderConnectionController(options: {
|
||||
return [{ type: "key", label: language.t("provider.connect.method.apiKey") }]
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
methodIndex: undefined as number | undefined,
|
||||
authorization: undefined as Authorization | undefined,
|
||||
formAnswer: undefined as FormAnswer | undefined,
|
||||
const [store, setStore] = createStore<{
|
||||
methodIndex?: number
|
||||
authorization?: Authorization
|
||||
formAnswer?: FormAnswer
|
||||
// Nothing is in flight until a method is selected; `busy()` reads this, so a truthy initial
|
||||
// value would keep multi-method providers on the spinner instead of the method list.
|
||||
state: undefined as "pending" | "waiting" | "refreshing" | "ready" | "error" | "form" | undefined,
|
||||
error: undefined as string | undefined,
|
||||
state?: "pending" | "waiting" | "refreshing" | "ready" | "error" | "form"
|
||||
error?: string
|
||||
auto: boolean
|
||||
connected: boolean
|
||||
browserFailed: boolean
|
||||
statusFailed: boolean
|
||||
}>({
|
||||
auto: false,
|
||||
// The credential is stored; a retry only needs to reload the catalogs.
|
||||
connected: false,
|
||||
@@ -124,12 +138,9 @@ export function createProviderConnectionController(options: {
|
||||
statusFailed: false,
|
||||
})
|
||||
|
||||
const polling = {
|
||||
const polling: Polling = {
|
||||
generation: 0,
|
||||
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
disposed: false,
|
||||
// An attempt the server still considers open; cancelled when the dialog goes away.
|
||||
attempt: undefined as Authorization | undefined,
|
||||
}
|
||||
|
||||
const currentMethod = createMemo(() =>
|
||||
@@ -206,8 +217,6 @@ export function createProviderConnectionController(options: {
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error))
|
||||
|
||||
const cancelAttempt = () => {
|
||||
const attempt = polling.attempt
|
||||
polling.attempt = undefined
|
||||
@@ -276,7 +285,9 @@ export function createProviderConnectionController(options: {
|
||||
setStore("statusFailed", true)
|
||||
dispatch({
|
||||
type: "auth.error",
|
||||
error: isConsole() ? language.t("provider.connect.console.statusFailed") : errorMessage(result.error),
|
||||
error: isConsole()
|
||||
? language.t("provider.connect.console.statusFailed")
|
||||
: formatServerError(result.error, language.t),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -358,8 +369,6 @@ export function createProviderConnectionController(options: {
|
||||
return
|
||||
}
|
||||
|
||||
if (selected.type !== "oauth") return
|
||||
|
||||
if (selected.form?.some((field) => field.type !== "string")) {
|
||||
dispatch({ type: "auth.error", error: language.t("provider.connect.error.unsupportedFields") })
|
||||
|
||||
@@ -368,11 +377,35 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
dispatch({ type: "auth.pending" })
|
||||
|
||||
if (selected.type === "external") {
|
||||
const saved = await serverSDK.api.integration.connect
|
||||
.external({
|
||||
integrationID: options.provider(),
|
||||
methodID: selected.id,
|
||||
answer: Object.keys(merged).length ? merged : undefined,
|
||||
location: location(),
|
||||
})
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (polling.disposed || generation !== polling.generation) return
|
||||
|
||||
if (!saved.ok) {
|
||||
dispatch({ type: "auth.error", error: formatServerError(saved.error, language.t) })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await finish()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const result = await serverSDK.api.integration.oauth
|
||||
.connect({
|
||||
integrationID: options.provider(),
|
||||
methodID: selected.id,
|
||||
...(Object.keys(merged).length ? { answer: merged } : {}),
|
||||
answer: Object.keys(merged).length ? merged : undefined,
|
||||
location: location(),
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -404,7 +437,9 @@ export function createProviderConnectionController(options: {
|
||||
if (!result.ok) {
|
||||
dispatch({
|
||||
type: "auth.error",
|
||||
error: isConsole() ? language.t("provider.connect.console.startFailed") : errorMessage(result.error),
|
||||
error: isConsole()
|
||||
? language.t("provider.connect.console.startFailed")
|
||||
: formatServerError(result.error, language.t),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -448,7 +483,7 @@ export function createProviderConnectionController(options: {
|
||||
integrationID: options.keyProvider?.() ?? options.provider(),
|
||||
location: location(),
|
||||
key,
|
||||
...(store.formAnswer ? { answer: store.formAnswer } : {}),
|
||||
answer: store.formAnswer,
|
||||
})
|
||||
await finish()
|
||||
}
|
||||
@@ -468,7 +503,8 @@ export function createProviderConnectionController(options: {
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (!result.ok) return errorMessage(result.error) || language.t("provider.connect.oauth.code.invalid")
|
||||
if (!result.ok)
|
||||
return formatServerError(result.error, language.t, language.t("provider.connect.oauth.code.invalid"))
|
||||
await finish()
|
||||
|
||||
return undefined
|
||||
@@ -496,11 +532,13 @@ export function createProviderConnectionController(options: {
|
||||
authorization: () => store.authorization,
|
||||
browserFailed: () => store.browserFailed,
|
||||
// True while nothing useful can be shown yet: the integration is loading, a method is
|
||||
// about to be picked automatically, or the authorization request is in flight.
|
||||
// about to be picked automatically, the authorization request is in flight, or an external
|
||||
// method, which has no view of its own, is refreshing the catalogs after saving.
|
||||
busy: () =>
|
||||
integration.loading ||
|
||||
(store.methodIndex === undefined && !store.auto && autoIndex() !== undefined) ||
|
||||
store.state === "pending",
|
||||
store.state === "pending" ||
|
||||
(store.state === "refreshing" && currentMethod()?.type === "external"),
|
||||
auth: {
|
||||
state: () => store.state,
|
||||
error: () => store.error,
|
||||
|
||||
@@ -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": "የፕሮጀክት አዶ",
|
||||
|
||||
@@ -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": "أيقونة المشروع",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Икона на проекта",
|
||||
|
||||
@@ -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": "প্রকল্প আইকন",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ޕްރޮޖެކްޓް އައިކޮން",
|
||||
|
||||
@@ -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": "ལས་འགུལ་ངོས་དཔར།",
|
||||
|
||||
@@ -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": "Εικονίδιο έργου",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "نماد پروژه",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "סמל פרויקט",
|
||||
|
||||
@@ -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": "प्रोजेक्ट आइकन",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Նախագծի պատկերակ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "アイコン",
|
||||
|
||||
@@ -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": "პროექტის ხატულა",
|
||||
|
||||
@@ -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": "រូបតំណាងគម្រោង",
|
||||
|
||||
@@ -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": "프로젝트 아이콘",
|
||||
|
||||
@@ -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": "ໄອຄອນໂຄງການ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Икона на проектот",
|
||||
|
||||
@@ -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": "Төслийн дүрс тэмдэг",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ပရောဂျက် သင်္ကေတ",
|
||||
|
||||
@@ -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": "परियोजना आइकन",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "پروجیکٹ آئیکن",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Значок",
|
||||
|
||||
@@ -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": "ව්යාපෘති නිරූපකය",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "икона пројекта",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Нишонаи лоиҳа",
|
||||
|
||||
@@ -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": "ไอคอน",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Іконка",
|
||||
|
||||
@@ -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": "پروجیکٹ کا آئیکن",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "图标",
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import { createMemo, For, Show, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Menu } from "@opencode/ui/menu"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { ServerConnection } from "@/runtime/server/registry"
|
||||
import { useGlobal, useServerCtx } from "@/runtime/server/runtime"
|
||||
import { SessionTabAvatarView } from "@/shell/layout/session-tab-avatar"
|
||||
import { listClosedTabs, type ClosedTab } from "@/shell/tabs/closed"
|
||||
import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { sessionTabTitle } from "./tab-title"
|
||||
|
||||
export function RecentlyClosedTabsMenu(props: {
|
||||
onNewTab: () => void
|
||||
tooltip?: JSX.Element
|
||||
vertical?: boolean
|
||||
keybind?: string
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const tabs = useTabs()
|
||||
|
||||
const recent = createMemo(() =>
|
||||
listClosedTabs(tabs.closed, tabs.store).filter((entry) => entry.info?.prompted === true),
|
||||
)
|
||||
|
||||
return (
|
||||
<Menu.Context modal={false}>
|
||||
<Show
|
||||
when={props.vertical}
|
||||
fallback={
|
||||
<Tooltip placement="bottom" value={props.tooltip}>
|
||||
<Menu.Context.Trigger
|
||||
as={IconButton}
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<Icon name="plus" />}
|
||||
onClick={props.onNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Menu.Context.Trigger
|
||||
as="button"
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-new-session"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base"
|
||||
onClick={props.onNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
>
|
||||
<Icon name="edit" class="shrink-0" />
|
||||
<span class="min-w-0 truncate">{language.t("command.session.new")}</span>
|
||||
<span
|
||||
class="ms-auto hidden min-w-0 truncate text-v2-text-text-faint group-hover:block group-focus-visible:block"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<bdi dir="ltr">{props.keybind}</bdi>
|
||||
</span>
|
||||
</Menu.Context.Trigger>
|
||||
</Show>
|
||||
<Menu.Context.Portal>
|
||||
<Menu.Context.Content class="max-h-[66.667dvh] w-56 max-w-56 overflow-y-auto">
|
||||
<Menu.Group>
|
||||
<Menu.GroupLabel>{language.t("home.recentlyClosed")}</Menu.GroupLabel>
|
||||
<For each={recent()}>
|
||||
{(entry) => (
|
||||
<RecentlyClosedTabItem
|
||||
entry={entry}
|
||||
onSelect={() => tabs.reopenClosedTab(entry.tab, { append: true })}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Menu.Group>
|
||||
</Menu.Context.Content>
|
||||
</Menu.Context.Portal>
|
||||
</Menu.Context>
|
||||
)
|
||||
}
|
||||
|
||||
function RecentlyClosedTabItem(props: { entry: ClosedTab; onSelect: () => void }) {
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const [state, setState] = createStore({ truncated: false })
|
||||
const placement = language.direction() === "rtl" ? "left-start" : "right-start"
|
||||
|
||||
const serverCtx = useServerCtx(() =>
|
||||
global.servers.list().find((item) => ServerConnection.key(item) === props.entry.tab.server),
|
||||
)
|
||||
|
||||
const session = createMemo(() => serverCtx()?.data.session.get(props.entry.tab.sessionId))
|
||||
const directory = () => props.entry.info?.directory ?? session()?.location.directory ?? ""
|
||||
|
||||
const project = createMemo(() => {
|
||||
const value = session()
|
||||
|
||||
if (value) return serverCtx()?.projects.forSession(value)
|
||||
|
||||
if (!directory()) return
|
||||
|
||||
return serverCtx()?.projects.resolve({ worktree: directory(), expanded: false })
|
||||
})
|
||||
|
||||
const title = () => sessionTabTitle(session()?.title ?? props.entry.info?.title, language.t("session.tab.session"))
|
||||
|
||||
return (
|
||||
<Menu.Item class="max-w-56" onSelect={props.onSelect}>
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<SessionTabAvatarView project={project()} directory={directory()} unread={false} loading={false} />
|
||||
</span>
|
||||
<Tooltip
|
||||
placement={placement}
|
||||
value={<bdi dir="auto">{title()}</bdi>}
|
||||
disabled={!state.truncated}
|
||||
class="min-w-0 flex-1"
|
||||
contentClass="max-w-[min(480px,calc(100vw-32px))] break-words"
|
||||
>
|
||||
<span
|
||||
ref={(element) =>
|
||||
createResizeObserver(element, () => setState("truncated", element.scrollWidth > element.clientWidth))
|
||||
}
|
||||
dir="auto"
|
||||
class="block min-w-0 truncate text-start"
|
||||
>
|
||||
{title()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
@@ -162,7 +162,11 @@ function SessionTabEntry(props: {
|
||||
const value = session()
|
||||
|
||||
if (!value) return
|
||||
tabs.rememberSessionInfo(props.tab, value)
|
||||
tabs.rememberSessionInfo(
|
||||
props.tab,
|
||||
value,
|
||||
props.serverCtx?.data.session.message.list(value.id).some((message) => message.type === "user") ?? false,
|
||||
)
|
||||
const current = sdk()
|
||||
|
||||
if (!current) return
|
||||
|
||||
@@ -28,6 +28,7 @@ import { MobileDrawer, MobileDrawerContent, MobileDrawerLabel, MobileDrawerTrigg
|
||||
import { sessionTabTitle } from "./tab-title"
|
||||
import { SessionTabAvatar } from "@/shell/layout/session-tab-avatar"
|
||||
import { SessionProgressIndicatorV2 } from "@opencode/session-ui/v2/session-progress-indicator-v2"
|
||||
import { RecentlyClosedTabsMenu } from "./recently-closed-tabs-menu"
|
||||
import { useSettingsDialog } from "@/settings/command"
|
||||
import { rootSession } from "@/shell/routes/session"
|
||||
import { TitlebarItem } from "@opencode/gui-extensions/sdk"
|
||||
@@ -692,25 +693,15 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
|
||||
}}
|
||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||
/>
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
value={
|
||||
<RecentlyClosedTabsMenu
|
||||
onNewTab={openNewTab}
|
||||
tooltip={
|
||||
<>
|
||||
{language.t("command.session.new")}
|
||||
<Keybind keys={command.keybindParts("tab.new")} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<Icon name="plus" />}
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</Tooltip>
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -732,23 +723,11 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
|
||||
<ChannelIndicator sidebar />
|
||||
</Show>
|
||||
{homeButton(true)}
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-new-session"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base"
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
>
|
||||
<Icon name="edit" class="shrink-0" />
|
||||
<span class="min-w-0 truncate">{language.t("command.session.new")}</span>
|
||||
<span
|
||||
class="ms-auto hidden min-w-0 truncate text-v2-text-text-faint group-hover:block group-focus-visible:block"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<bdi dir="ltr">{command.keybind("tab.new")}</bdi>
|
||||
</span>
|
||||
</button>
|
||||
<RecentlyClosedTabsMenu
|
||||
vertical
|
||||
onNewTab={openNewTab}
|
||||
keybind={command.keybind("tab.new")}
|
||||
/>
|
||||
<div class="h-4 w-full shrink-0" aria-hidden="true" />
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-1">
|
||||
<TitlebarTabStrip
|
||||
|
||||
@@ -127,9 +127,29 @@ const authenticate = Effect.fn("cli.auth.login.authenticate")(function* (
|
||||
) {
|
||||
if (method.type === "key") return yield* keyLogin(client, integration, method, answer)
|
||||
if (method.type === "command") return yield* commandLogin(client, integration, method)
|
||||
if (method.type === "external") return yield* externalLogin(client, integration, method, answer)
|
||||
return yield* oauthLogin(client, integration, method, answer)
|
||||
})
|
||||
|
||||
const externalLogin = Effect.fn("cli.auth.login.external")(function* (
|
||||
client: OpenCodeClient,
|
||||
integration: IntegrationInfo,
|
||||
method: Extract<ConnectMethod, { type: "external" }>,
|
||||
answer?: FormAnswer,
|
||||
) {
|
||||
const progress = spinner()
|
||||
progress.start("Saving credential...")
|
||||
yield* request((signal) =>
|
||||
client.integration.connect.external(
|
||||
{ integrationID: integration.id, methodID: method.id, answer, location },
|
||||
{ signal },
|
||||
),
|
||||
).pipe(
|
||||
Effect.tap(() => Effect.sync(() => progress.stop(`Connected to ${integration.name}`))),
|
||||
Effect.tapCause(() => Effect.sync(() => progress.stop("Authentication failed", 1))),
|
||||
)
|
||||
})
|
||||
|
||||
const keyLogin = Effect.fn("cli.auth.login.key")(function* (
|
||||
client: OpenCodeClient,
|
||||
integration: IntegrationInfo,
|
||||
|
||||
@@ -1597,6 +1597,18 @@ export type IntegrationConnectKeyOperation<E = never> = (
|
||||
input: IntegrationConnectKeyInput,
|
||||
) => Effect.Effect<IntegrationConnectKeyOutput, E>
|
||||
|
||||
export type IntegrationConnectExternalInput = {
|
||||
readonly integrationID: Integration.ID
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
readonly methodID: Integration.MethodID
|
||||
readonly answer?: Form.Answer | undefined
|
||||
readonly label?: string | undefined
|
||||
}
|
||||
export type IntegrationConnectExternalOutput = void
|
||||
export type IntegrationConnectExternalOperation<E = never> = (
|
||||
input: IntegrationConnectExternalInput,
|
||||
) => Effect.Effect<IntegrationConnectExternalOutput, E>
|
||||
|
||||
export type IntegrationOauthConnectInput = {
|
||||
readonly integrationID: Integration.ID
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
@@ -1687,7 +1699,10 @@ export interface IntegrationApi<E = never> {
|
||||
readonly list: IntegrationListOperation<E>
|
||||
readonly get: IntegrationGetOperation<E>
|
||||
readonly wellknown: { readonly add: IntegrationWellknownAddOperation<E> }
|
||||
readonly connect: { readonly key: IntegrationConnectKeyOperation<E> }
|
||||
readonly connect: {
|
||||
readonly key: IntegrationConnectKeyOperation<E>
|
||||
readonly external: IntegrationConnectExternalOperation<E>
|
||||
}
|
||||
readonly oauth: {
|
||||
readonly connect: IntegrationOauthConnectOperation<E>
|
||||
readonly status: IntegrationOauthStatusOperation<E>
|
||||
|
||||
@@ -127,6 +127,8 @@ import type {
|
||||
IntegrationWellknownAddOutput,
|
||||
IntegrationConnectKeyInput,
|
||||
IntegrationConnectKeyOutput,
|
||||
IntegrationConnectExternalInput,
|
||||
IntegrationConnectExternalOutput,
|
||||
IntegrationOauthConnectInput,
|
||||
IntegrationOauthConnectOutput,
|
||||
IntegrationOauthStatusInput,
|
||||
@@ -897,6 +899,16 @@ const EndpointIntegrationConnectKey = (raw: RawClient["server.integration"]) =>
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const EndpointIntegrationConnectExternal =
|
||||
(raw: RawClient["server.integration"]) => (input: IntegrationConnectExternalInput) =>
|
||||
preserveEffect<IntegrationConnectExternalOutput>()(
|
||||
raw["integration.connect.external"]({
|
||||
params: { integrationID: input["integrationID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const EndpointIntegrationOauthConnect =
|
||||
(raw: RawClient["server.integration"]) => (input: IntegrationOauthConnectInput) =>
|
||||
preserveEffect<IntegrationOauthConnectOutput>()(
|
||||
@@ -965,7 +977,7 @@ const adaptGroupIntegration = (raw: RawClient["server.integration"]) => ({
|
||||
list: EndpointIntegrationList(raw),
|
||||
get: EndpointIntegrationGet(raw),
|
||||
wellknown: { add: EndpointIntegrationWellknownAdd(raw) },
|
||||
connect: { key: EndpointIntegrationConnectKey(raw) },
|
||||
connect: { key: EndpointIntegrationConnectKey(raw), external: EndpointIntegrationConnectExternal(raw) },
|
||||
oauth: {
|
||||
connect: EndpointIntegrationOauthConnect(raw),
|
||||
status: EndpointIntegrationOauthStatus(raw),
|
||||
|
||||
@@ -121,6 +121,8 @@ import type {
|
||||
IntegrationWellknownAddOutput,
|
||||
IntegrationConnectKeyInput,
|
||||
IntegrationConnectKeyOutput,
|
||||
IntegrationConnectExternalInput,
|
||||
IntegrationConnectExternalOutput,
|
||||
IntegrationOauthConnectInput,
|
||||
IntegrationOauthConnectOutput,
|
||||
IntegrationOauthStatusInput,
|
||||
@@ -1228,6 +1230,19 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
external: (input: IntegrationConnectExternalInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectExternalOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/external`,
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
oauth: {
|
||||
connect: (input: IntegrationOauthConnectInput, requestOptions?: RequestOptions) =>
|
||||
|
||||
Loaded 100 of 270 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user