Compare commits

..
265 changed files with 919 additions and 1187 deletions

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)
}
}),
)
+3 -3
View File
@@ -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>
+1 -6
View File
@@ -86,7 +86,6 @@ export function AppBaseProviders(
export function AppInterface(props: {
children?: JSX.Element
defaultServer?: ServerConnection.Key
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
router?: Component<BaseRouterProps>
@@ -108,11 +107,7 @@ export function AppInterface(props: {
)
return (
<ServersProvider
defaultServer={props.defaultServer}
canonicalLocalServer={props.canonicalLocalServer}
servers={props.servers}
>
<ServersProvider canonicalLocalServer={props.canonicalLocalServer} servers={props.servers}>
<SettingsProvider>
<Dynamic component={props.router ?? Router} root={Root}>
<AppRoutes />
+1 -2
View File
@@ -16,7 +16,7 @@ import pkg from "../package.json"
import { ServerConnection } from "@/runtime/server/registry"
const getLocale = () => {
if (typeof navigator !== "object") return "en" as const
if (typeof navigator === "undefined") return "en" as const
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
for (const language of languages) {
@@ -101,7 +101,6 @@ if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
<PlatformProvider value={web.platform}>
<AppBaseProviders locale={locale}>
<AppInterface
defaultServer={web.defaultServerUrl ? ServerConnection.Key.make(web.defaultServerUrl) : undefined}
canonicalLocalServer={server ? ServerConnection.key(server) : undefined}
servers={server ? [server] : []}
>
@@ -86,10 +86,6 @@ export function createHomeProjectsController(home: HomeController) {
const key = ServerConnection.key(conn)
setState("collapsed", key, !state().collapsed[key])
},
canDefault: serverManagement.defaults.available,
defaultKey: serverManagement.defaults.key,
setDefault: (conn: ServerConnection.Any | undefined) =>
serverManagement.defaults.set(conn ? ServerConnection.key(conn) : null),
canRemove: (conn: ServerConnection.Any) => serverManagement.connection.canRemove(ServerConnection.key(conn)),
remove: (conn: ServerConnection.Any) => serverManagement.connection.remove(ServerConnection.key(conn)),
canHide: (conn: ServerConnection.Any) => serverManagement.connection.canHide(ServerConnection.key(conn)),
@@ -19,8 +19,6 @@ export function HomeProjects(props: {
serverHealth={props.projects.server.health}
projectsForServer={props.projects.server.projects}
collapsed={props.projects.server.collapsed}
canDefaultServer={props.projects.server.canDefault()}
defaultServerKey={props.projects.server.defaultKey()}
canRevealProject={props.projects.project.canReveal}
unseenCount={props.projects.project.unseenCount}
onWheel={props.scroll.viewport.containWheel}
@@ -29,7 +27,6 @@ export function HomeProjects(props: {
onAuthenticateServer={props.projects.server.authenticate}
onToggleCollapsed={props.projects.server.toggleCollapsed}
onEditServer={props.projects.server.edit}
onSetDefaultServer={props.projects.server.setDefault}
canRemoveServer={props.projects.server.canRemove}
onRemoveServer={props.projects.server.remove}
canHideServer={props.projects.server.canHide}
-10
View File
@@ -52,8 +52,6 @@ export type HomeProjectsViewProps = {
serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined
projectsForServer: (server: ServerConnection.Any) => LocalProject[]
collapsed: (server: ServerConnection.Any) => boolean
canDefaultServer: boolean
defaultServerKey: ServerConnection.Key | null | undefined
canRevealProject: (server: ServerConnection.Any) => boolean
unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
onWheel: (event: WheelEvent) => void
@@ -62,7 +60,6 @@ export type HomeProjectsViewProps = {
onAuthenticateServer?: (server: ServerConnection.Any) => void
onToggleCollapsed: (server: ServerConnection.Any) => void
onEditServer: (server: ServerConnection.Http) => void
onSetDefaultServer: (server: ServerConnection.Any | undefined) => void
canRemoveServer: (server: ServerConnection.Any) => boolean
onRemoveServer: (server: ServerConnection.Any) => void
canHideServer: (server: ServerConnection.Any) => boolean
@@ -347,12 +344,9 @@ function HomeServerRow(props: {
language: HomeProjectsViewProps["language"]
projectsForServer: HomeProjectsViewProps["projectsForServer"]
contextMenuOpen: HomeProjectsContextMenuProps["contextMenuOpen"]
canDefaultServer: HomeProjectsViewProps["canDefaultServer"]
defaultServerKey: HomeProjectsViewProps["defaultServerKey"]
onFocusServer: HomeProjectsViewProps["onFocusServer"]
onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"]
onEditServer: HomeProjectsViewProps["onEditServer"]
onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"]
canRemoveServer: HomeProjectsViewProps["canRemoveServer"]
onRemoveServer: HomeProjectsViewProps["onRemoveServer"]
canHideServer: HomeProjectsViewProps["canHideServer"]
@@ -470,13 +464,9 @@ function HomeServerRow(props: {
<ServerRowMenuView
server={props.server}
labels={serverMenuLabels(props.language)}
canDefault={props.canDefaultServer}
isDefault={props.defaultServerKey === ServerConnection.key(props.server)}
canRemove={props.canRemoveServer(props.server)}
canHide={props.canHideServer(props.server)}
onEdit={props.onEditServer}
onSetDefault={() => props.onSetDefaultServer(props.server)}
onRemoveDefault={() => props.onSetDefaultServer(undefined)}
onRemove={() => props.onRemoveServer(props.server)}
onHide={() => props.onHideServer(props.server)}
open={props.contextMenuOpen(contextMenuID())}
@@ -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,
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "የይለፍ ቃል",
"dialog.server.edit.title": "አገልጋይ አርትዕ",
"dialog.server.menu.edit": "አርትዕ",
"dialog.server.menu.default": "ነባሪ አዘጋጅ",
"dialog.server.menu.defaultRemove": "ነባሪውን አስወግድ",
"dialog.server.status.default": "ነባሪ",
"dialog.project.edit.title": "ፕሮጀክት አርትዕ",
"dialog.project.edit.icon": "አዶ",
"dialog.project.edit.icon.alt": "የፕሮጀክት አዶ",
-3
View File
@@ -297,9 +297,6 @@ export const dict = {
"dialog.server.add.password": "كلمة المرور (اختياري)",
"dialog.server.edit.title": "تحرير الخادم",
"dialog.server.menu.edit": "تعديل",
"dialog.server.menu.default": "تعيين كافتراضي",
"dialog.server.menu.defaultRemove": "إزالة الافتراضي",
"dialog.server.status.default": "افتراضي",
"dialog.project.edit.title": "تحرير المشروع",
"dialog.project.edit.icon": "أيقونة",
"dialog.project.edit.icon.alt": "أيقونة المشروع",
-3
View File
@@ -288,9 +288,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serveri redaktə et",
"dialog.server.menu.edit": "Redaktə et",
"dialog.server.menu.default": "Standart olaraq təyin et",
"dialog.server.menu.defaultRemove": "Standartı sil",
"dialog.server.status.default": "Standart",
"dialog.project.edit.title": "Layihəni redaktə et",
"dialog.project.edit.icon": "İkon",
"dialog.project.edit.icon.alt": "Layihə ikonu",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "парола",
"dialog.server.edit.title": "Редактиране на сървъра",
"dialog.server.menu.edit": "Редактиране",
"dialog.server.menu.default": "Задайте по подразбиране",
"dialog.server.menu.defaultRemove": "Премахване на подразбиране",
"dialog.server.status.default": "По подразбиране",
"dialog.project.edit.title": "Редактиране на проекта",
"dialog.project.edit.icon": "Икона",
"dialog.project.edit.icon.alt": "Икона на проекта",
-3
View File
@@ -290,9 +290,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "পাসওয়ার্ড",
"dialog.server.edit.title": "সার্ভার সম্পাদনা করুন",
"dialog.server.menu.edit": "সম্পাদনা করুন",
"dialog.server.menu.default": "ডিফল্ট হিসেবে সেট করুন",
"dialog.server.menu.defaultRemove": "ডিফল্ট সরান",
"dialog.server.status.default": "ডিফল্ট",
"dialog.project.edit.title": "প্রকল্প সম্পাদনা করুন",
"dialog.project.edit.icon": "আইকন",
"dialog.project.edit.icon.alt": "প্রকল্প আইকন",
-3
View File
@@ -508,9 +508,6 @@ export const dict = {
"dialog.server.add.password": "Senha (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.menu.edit": "Editar",
"dialog.server.menu.default": "Definir como padrão",
"dialog.server.menu.defaultRemove": "Remover padrão",
"dialog.server.status.default": "Padrão",
"dialog.project.edit.title": "Editar projeto",
"dialog.project.edit.icon": "Ícone",
"dialog.project.edit.icon.alt": "Ícone do projeto",
-3
View File
@@ -312,9 +312,6 @@ export const dict = {
"dialog.server.edit.title": "Uredi server",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Postavi kao podrazumijevano",
"dialog.server.menu.defaultRemove": "Ukloni podrazumijevano",
"dialog.server.status.default": "Podrazumijevano",
"dialog.project.edit.title": "Uredi projekat",
"dialog.project.edit.icon": "Ikonica",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "contrasenya",
"dialog.server.edit.title": "Edita el servidor",
"dialog.server.menu.edit": "Edita",
"dialog.server.menu.default": "Estableix com a predeterminat",
"dialog.server.menu.defaultRemove": "Elimina el valor predeterminat",
"dialog.server.status.default": "Per defecte",
"dialog.project.edit.title": "Edita el projecte",
"dialog.project.edit.icon": "Icona",
"dialog.project.edit.icon.alt": "Icona del projecte",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upravit server",
"dialog.server.menu.edit": "Upravit",
"dialog.server.menu.default": "Nastavit jako výchozí",
"dialog.server.menu.defaultRemove": "Odebrat výchozí",
"dialog.server.status.default": "Výchozí",
"dialog.project.edit.title": "Upravit projekt",
"dialog.project.edit.icon": "ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
-3
View File
@@ -446,9 +446,6 @@ export const dict = {
"dialog.server.edit.title": "Rediger server",
"dialog.server.menu.edit": "Rediger",
"dialog.server.menu.default": "Sæt som standard",
"dialog.server.menu.defaultRemove": "Fjern som standard",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Rediger projekt",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -442,9 +442,6 @@ export const dict = {
"dialog.server.add.password": "Passwort (optional)",
"dialog.server.edit.title": "Server bearbeiten",
"dialog.server.menu.edit": "Bearbeiten",
"dialog.server.menu.default": "Als Standard festlegen",
"dialog.server.menu.defaultRemove": "Standard entfernen",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Projekt bearbeiten",
"dialog.project.edit.icon": "Symbol",
"dialog.project.edit.icon.alt": "Projektsymbol",
-3
View File
@@ -292,9 +292,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "ޕާސްވަރޑް",
"dialog.server.edit.title": "އެޑިޓް ސަރވަރ",
"dialog.server.menu.edit": "ބަދަލު ގެނައުން",
"dialog.server.menu.default": "ޑިފޯލްޓް ގޮތުގައި ސެޓްކުރުން",
"dialog.server.menu.defaultRemove": "ޑިފޯލްޓް ނައްތާލާށެވެ",
"dialog.server.status.default": "ޑީފޯލްޓް",
"dialog.project.edit.title": "އެޑިޓް ޕްރޮޖެކްޓް",
"dialog.project.edit.icon": "އައިކޮން",
"dialog.project.edit.icon.alt": "ޕްރޮޖެކްޓް އައިކޮން",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "གསང༌ཨང",
"dialog.server.edit.title": "སར་བར་ཞུན་དག་འབད།",
"dialog.server.menu.edit": "ཞུན༌དག",
"dialog.server.menu.default": "སྔོན་སྒྲིག་སྦེ་གཞི་སྒྲིག་འབད།",
"dialog.server.menu.defaultRemove": "སྔོན་སྒྲིག་རྩ་བསྐྲད་གཏང་།",
"dialog.server.status.default": "འཐུས་ཤོར",
"dialog.project.edit.title": "ཞུན་དག་ལས་གཞི།",
"dialog.project.edit.icon": "ངོས་དཔར།",
"dialog.project.edit.icon.alt": "ལས་འགུལ་ངོས་དཔར།",
-3
View File
@@ -288,9 +288,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "κωδικός πρόσβασης",
"dialog.server.edit.title": "Επεξεργασία διακομιστή",
"dialog.server.menu.edit": "Επεξεργασία",
"dialog.server.menu.default": "Ορισμός ως προεπιλογή",
"dialog.server.menu.defaultRemove": "Κατάργηση προεπιλογής",
"dialog.server.status.default": "Προεπιλογή",
"dialog.project.edit.title": "Επεξεργασία έργου",
"dialog.project.edit.icon": "Εικονίδιο",
"dialog.project.edit.icon.alt": "Εικονίδιο έργου",
-3
View File
@@ -378,12 +378,9 @@ export const dict = {
"Your sign-in for this server is no longer valid. Paste a new pairing link, or enter the server password.",
"dialog.server.menu.edit": "Edit",
"dialog.server.menu.default": "Set as default",
"dialog.server.menu.defaultRemove": "Remove default",
"dialog.server.menu.remove": "Remove",
"dialog.server.menu.hide": "Hide from project list",
"dialog.server.menu.show": "Show in project list",
"dialog.server.status.default": "Default",
"server.row.incompatible":
"This server is running OpenCode {{version}}, which isn't compatible with this app. Upgrade it to OpenCode V2 to continue.",
-3
View File
@@ -535,9 +535,6 @@ export const dict = {
"dialog.server.edit.title": "Editar servidor",
"dialog.server.menu.edit": "Editar",
"dialog.server.menu.default": "Establecer como predeterminado",
"dialog.server.menu.defaultRemove": "Quitar como predeterminado",
"dialog.server.status.default": "Predeterminado",
"dialog.project.edit.title": "Editar proyecto",
"dialog.project.edit.icon": "Icono",
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parool",
"dialog.server.edit.title": "Muuda serverit",
"dialog.server.menu.edit": "Muuda",
"dialog.server.menu.default": "Määra vaikeväärtuseks",
"dialog.server.menu.defaultRemove": "Eemalda vaikeseade",
"dialog.server.status.default": "Vaikimisi",
"dialog.project.edit.title": "Redigeeri projekti",
"dialog.project.edit.icon": "Ikoon",
"dialog.project.edit.icon.alt": "Projekti ikoon",
-3
View File
@@ -295,9 +295,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "رمز عبور",
"dialog.server.edit.title": "ویرایش سرور",
"dialog.server.menu.edit": "ویرایش کنید",
"dialog.server.menu.default": "به عنوان پیش فرض تنظیم کنید",
"dialog.server.menu.defaultRemove": "حذف پیش فرض",
"dialog.server.status.default": "پیش فرض",
"dialog.project.edit.title": "ویرایش پروژه",
"dialog.project.edit.icon": "نماد",
"dialog.project.edit.icon.alt": "نماد پروژه",
-3
View File
@@ -424,9 +424,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "salasana",
"dialog.server.edit.title": "Muokkaa palvelinta",
"dialog.server.menu.edit": "Muokkaa",
"dialog.server.menu.default": "Aseta oletukseksi",
"dialog.server.menu.defaultRemove": "Poista oletuspalvelin",
"dialog.server.status.default": "Oletus",
"dialog.project.edit.title": "Muokkaa projektia",
"dialog.project.edit.icon": "Kuvake",
"dialog.project.edit.icon.alt": "Projektikuvake",
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "loyniorð",
"dialog.server.edit.title": "Rætta ambætara",
"dialog.server.menu.edit": "Rætta",
"dialog.server.menu.default": "Set sum forsett",
"dialog.server.menu.defaultRemove": "Strika forsett",
"dialog.server.status.default": "Forsett",
"dialog.project.edit.title": "Rætta verkætlan",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Verkætlanarmerki",
-3
View File
@@ -516,9 +516,6 @@ export const dict = {
"dialog.server.add.password": "Mot de passe (optionnel)",
"dialog.server.edit.title": "Modifier le serveur",
"dialog.server.menu.edit": "Modifier",
"dialog.server.menu.default": "Définir par défaut",
"dialog.server.menu.defaultRemove": "Ne plus utiliser par défaut",
"dialog.server.status.default": "Défaut",
"dialog.project.edit.title": "Modifier le projet",
"dialog.project.edit.icon": "Icône",
"dialog.project.edit.icon.alt": "Icône du projet",
-3
View File
@@ -294,9 +294,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "סיסמה",
"dialog.server.edit.title": "ערוך שרת",
"dialog.server.menu.edit": "ערוך",
"dialog.server.menu.default": "הגדר כברירת מחדל",
"dialog.server.menu.defaultRemove": "הסר את ברירת המחדל",
"dialog.server.status.default": "ברירת מחדל",
"dialog.project.edit.title": "ערוך פרויקט",
"dialog.project.edit.icon": "סמל",
"dialog.project.edit.icon.alt": "סמל פרויקט",
-3
View File
@@ -292,9 +292,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्वर संपादित करें",
"dialog.server.menu.edit": "संपादित करें",
"dialog.server.menu.default": "डिफाल्ट के रूप में सेट",
"dialog.server.menu.defaultRemove": "डिफ़ॉल्ट हटाएँ",
"dialog.server.status.default": "डिफ़ॉल्ट",
"dialog.project.edit.title": "प्रोजेक्ट संपादित करें",
"dialog.project.edit.icon": "आइकन",
"dialog.project.edit.icon.alt": "प्रोजेक्ट आइकन",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "lozinka",
"dialog.server.edit.title": "Uredi poslužitelj",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Postavi kao zadano",
"dialog.server.menu.defaultRemove": "Ukloni zadano",
"dialog.server.status.default": "Zadano",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projekta",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "jelszó",
"dialog.server.edit.title": "Szerver szerkesztése",
"dialog.server.menu.edit": "Szerkesztés",
"dialog.server.menu.default": "Beállítás alapértelmezettként",
"dialog.server.menu.defaultRemove": "Alapértelmezés eltávolítása",
"dialog.server.status.default": "Alapértelmezett",
"dialog.project.edit.title": "Projekt szerkesztése",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Projekt ikonra",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "գաղտնաբառ",
"dialog.server.edit.title": "Խմբագրել սերվերը",
"dialog.server.menu.edit": "Խմբագրել",
"dialog.server.menu.default": "Սահմանել որպես լռելյայն",
"dialog.server.menu.defaultRemove": "Հեռացնել լռելյայն",
"dialog.server.status.default": "Լռելյայն",
"dialog.project.edit.title": "Խմբագրել նախագիծը",
"dialog.project.edit.icon": "Պատկերակ",
"dialog.project.edit.icon.alt": "Նախագծի պատկերակ",
-3
View File
@@ -323,9 +323,6 @@ export const dict = {
"dialog.server.edit.title": "Sunting server",
"dialog.server.menu.edit": "Sunting",
"dialog.server.menu.default": "Tetapkan sebagai bawaan",
"dialog.server.menu.defaultRemove": "Hapus bawaan",
"dialog.server.status.default": "Bawaan",
"dialog.project.edit.title": "Sunting proyek",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -298,9 +298,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "lykilorð",
"dialog.server.edit.title": "Breyta miðlara",
"dialog.server.menu.edit": "Breyta",
"dialog.server.menu.default": "Stillt sem sjálfgefið",
"dialog.server.menu.defaultRemove": "Fjarlægja sjálfgefið",
"dialog.server.status.default": "Sjálfgefið",
"dialog.project.edit.title": "Breyta verkefni",
"dialog.project.edit.icon": "Táknmynd",
"dialog.project.edit.icon.alt": "Verkefnistákn",
-3
View File
@@ -446,9 +446,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Modifica server",
"dialog.server.menu.edit": "Modifica",
"dialog.server.menu.default": "Imposta come predefinito",
"dialog.server.menu.defaultRemove": "Rimuovi predefinito",
"dialog.server.status.default": "Predefinito",
"dialog.project.edit.title": "Modifica progetto",
"dialog.project.edit.icon": "Icona",
"dialog.project.edit.icon.alt": "Icona del progetto",
-3
View File
@@ -296,9 +296,6 @@ export const dict = {
"dialog.server.add.password": "パスワード (オプション)",
"dialog.server.edit.title": "サーバーを編集",
"dialog.server.menu.edit": "編集",
"dialog.server.menu.default": "デフォルトに設定",
"dialog.server.menu.defaultRemove": "デフォルト設定を解除",
"dialog.server.status.default": "デフォルト",
"dialog.project.edit.title": "プロジェクトを編集",
"dialog.project.edit.icon": "アイコン",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "პაროლი",
"dialog.server.edit.title": "სერვერის რედაქტირება",
"dialog.server.menu.edit": "რედაქტირება",
"dialog.server.menu.default": "დაყენება ნაგულისხმევად",
"dialog.server.menu.defaultRemove": "ნაგულისხმევის ამოღება",
"dialog.server.status.default": "ნაგულისხმევი",
"dialog.project.edit.title": "პროექტის რედაქტირება",
"dialog.project.edit.icon": "ხატულა",
"dialog.project.edit.icon.alt": "პროექტის ხატულა",
-3
View File
@@ -293,9 +293,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "ពាក្យសម្ងាត់",
"dialog.server.edit.title": "កែសម្រួលម៉ាស៊ីនមេ",
"dialog.server.menu.edit": "កែសម្រួល",
"dialog.server.menu.default": "កំណត់ជាលំនាំដើម",
"dialog.server.menu.defaultRemove": "លុបលំនាំដើមចេញ",
"dialog.server.status.default": "លំនាំដើម",
"dialog.project.edit.title": "កែសម្រួលគម្រោង",
"dialog.project.edit.icon": "រូបតំណាង",
"dialog.project.edit.icon.alt": "រូបតំណាងគម្រោង",
-3
View File
@@ -218,9 +218,6 @@ export const dict = {
"dialog.server.add.password": "비밀번호 (선택 사항)",
"dialog.server.edit.title": "서버 편집",
"dialog.server.menu.edit": "편집",
"dialog.server.menu.default": "기본값으로 설정",
"dialog.server.menu.defaultRemove": "기본값 제거",
"dialog.server.status.default": "기본값",
"dialog.project.edit.title": "프로젝트 편집",
"dialog.project.edit.icon": "아이콘",
"dialog.project.edit.icon.alt": "프로젝트 아이콘",
-3
View File
@@ -293,9 +293,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "ລະຫັດຜ່ານ",
"dialog.server.edit.title": "ແກ້ໄຂເຊີບເວີ",
"dialog.server.menu.edit": "ແກ້ໄຂ",
"dialog.server.menu.default": "ຕັ້ງເປັນຄ່າເລີ່ມຕົ້ນ",
"dialog.server.menu.defaultRemove": "ເອົາຄ່າເລີ່ມຕົ້ນ",
"dialog.server.status.default": "ຄ່າເລີ່ມຕົ້ນ",
"dialog.project.edit.title": "ແກ້ໄຂໂຄງການ",
"dialog.project.edit.icon": "ໄອຄອນ",
"dialog.project.edit.icon.alt": "ໄອຄອນໂຄງການ",
-3
View File
@@ -299,9 +299,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "slaptažodis",
"dialog.server.edit.title": "Redaguoti serverį",
"dialog.server.menu.edit": "Redaguoti",
"dialog.server.menu.default": "Nustatyti kaip numatytąjį",
"dialog.server.menu.defaultRemove": "Pašalinti numatytąjį",
"dialog.server.status.default": "Numatytoji",
"dialog.project.edit.title": "Redaguoti projektą",
"dialog.project.edit.icon": "Piktograma",
"dialog.project.edit.icon.alt": "Projekto piktograma",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parole",
"dialog.server.edit.title": "Rediģēt serveri",
"dialog.server.menu.edit": "Rediģēt",
"dialog.server.menu.default": "Iestatīt kā noklusējumu",
"dialog.server.menu.defaultRemove": "Noņemt noklusējumu",
"dialog.server.status.default": "Noklusējuma",
"dialog.project.edit.title": "Rediģēt projektu",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Projekta ikona",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
"dialog.server.menu.edit": "Уреди",
"dialog.server.menu.default": "Поставете како стандардно",
"dialog.server.menu.defaultRemove": "Отстрани стандардно",
"dialog.server.status.default": "Стандардно",
"dialog.project.edit.title": "Уреди проект",
"dialog.project.edit.icon": "Икона",
"dialog.project.edit.icon.alt": "Икона на проектот",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "нууц үг",
"dialog.server.edit.title": "Сервер засах",
"dialog.server.menu.edit": "Засварлах",
"dialog.server.menu.default": "Өгөгдмөл болгож тохируулах",
"dialog.server.menu.defaultRemove": "Өгөгдмөлийг арилгах",
"dialog.server.status.default": "Өгөгдмөл",
"dialog.project.edit.title": "Төслийг засварлах",
"dialog.project.edit.icon": "Дүрс",
"dialog.project.edit.icon.alt": "Төслийн дүрс тэмдэг",
-3
View File
@@ -295,9 +295,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "kata laluan",
"dialog.server.edit.title": "Edit pelayan",
"dialog.server.menu.edit": "Sunting",
"dialog.server.menu.default": "Tetapkan sebagai lalai",
"dialog.server.menu.defaultRemove": "Buang lalai",
"dialog.server.status.default": "Lalai",
"dialog.project.edit.title": "Edit projek",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Ikon projek",
-3
View File
@@ -296,9 +296,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "စကားဝှက်",
"dialog.server.edit.title": "ဆာဗာကို တည်းဖြတ်ပါ။",
"dialog.server.menu.edit": "တည်းဖြတ်ရန်",
"dialog.server.menu.default": "မူရင်းအဖြစ် သတ်မှတ်ပါ။",
"dialog.server.menu.defaultRemove": "မူရင်းကို ဖယ်ရှားပါ။",
"dialog.server.status.default": "မူရင်း",
"dialog.project.edit.title": "ပရောဂျက်ကို တည်းဖြတ်ပါ။",
"dialog.project.edit.icon": "သင်္ကေတ",
"dialog.project.edit.icon.alt": "ပရောဂျက် သင်္ကေတ",
-3
View File
@@ -291,9 +291,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्भर सम्पादन गर्नुहोस्",
"dialog.server.menu.edit": "सम्पादन गर्नुहोस्",
"dialog.server.menu.default": "पूर्वनिर्धारित रूपमा सेट गर्नुहोस्",
"dialog.server.menu.defaultRemove": "पूर्वनिर्धारित हटाउनुहोस्",
"dialog.server.status.default": "पूर्वनिर्धारित",
"dialog.project.edit.title": "परियोजना सम्पादन गर्नुहोस्",
"dialog.project.edit.icon": "आइकन",
"dialog.project.edit.icon.alt": "परियोजना आइकन",
-3
View File
@@ -498,9 +498,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "wachtwoord",
"dialog.server.edit.title": "Server bewerken",
"dialog.server.menu.edit": "Bewerken",
"dialog.server.menu.default": "Als standaard instellen",
"dialog.server.menu.defaultRemove": "Standaard verwijderen",
"dialog.server.status.default": "Standaard",
"dialog.project.edit.title": "Project bewerken",
"dialog.project.edit.icon": "Pictogram",
"dialog.project.edit.icon.alt": "Projectpictogram",
-3
View File
@@ -507,9 +507,6 @@ export const dict = {
"dialog.server.edit.title": "Rediger server",
"dialog.server.menu.edit": "Rediger",
"dialog.server.menu.default": "Sett som standard",
"dialog.server.menu.defaultRemove": "Fjern standard",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Rediger prosjekt",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -293,9 +293,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور وچ ترمیم کرو",
"dialog.server.menu.edit": "ترمیم کرو",
"dialog.server.menu.default": "ڈیفالٹ دے طور تے سیٹ کرو",
"dialog.server.menu.defaultRemove": "ڈیفالٹ ہٹاؤ",
"dialog.server.status.default": "طے شدہ",
"dialog.project.edit.title": "پروجیکٹ وچ ترمیم کرو",
"dialog.project.edit.icon": "آئکن",
"dialog.project.edit.icon.alt": "پروجیکٹ آئیکن",
-3
View File
@@ -289,9 +289,6 @@ export const dict = {
"dialog.server.add.password": "Hasło (opcjonalnie)",
"dialog.server.edit.title": "Edytuj serwer",
"dialog.server.menu.edit": "Edytuj",
"dialog.server.menu.default": "Ustaw jako domyślny",
"dialog.server.menu.defaultRemove": "Usuń domyślny",
"dialog.server.status.default": "Domyślny",
"dialog.project.edit.title": "Edytuj projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
-3
View File
@@ -285,9 +285,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parolă",
"dialog.server.edit.title": "Editează server",
"dialog.server.menu.edit": "Editează",
"dialog.server.menu.default": "Setează ca implicit",
"dialog.server.menu.defaultRemove": "Elimină implicitul",
"dialog.server.status.default": "Implicit",
"dialog.project.edit.title": "Editează proiectul",
"dialog.project.edit.icon": "Pictogramă",
"dialog.project.edit.icon.alt": "Pictograma proiectului",
-3
View File
@@ -311,9 +311,6 @@ export const dict = {
"dialog.server.edit.title": "Редактировать сервер",
"dialog.server.menu.edit": "Редактировать",
"dialog.server.menu.default": "Сделать по умолчанию",
"dialog.server.menu.defaultRemove": "Отменить выбор по умолчанию",
"dialog.server.status.default": "По умолч.",
"dialog.project.edit.title": "Редактировать проект",
"dialog.project.edit.icon": "Значок",
-3
View File
@@ -289,9 +289,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "මුරපදය",
"dialog.server.edit.title": "සේවාදායකය සංස්කරණය කරන්න",
"dialog.server.menu.edit": "සංස්කරණය කරන්න",
"dialog.server.menu.default": "පෙරනිමිය ලෙස සකසන්න",
"dialog.server.menu.defaultRemove": "පෙරනිමිය ඉවත් කරන්න",
"dialog.server.status.default": "පෙරනිමිය",
"dialog.project.edit.title": "ව්‍යාපෘතිය සංස්කරණය කරන්න",
"dialog.project.edit.icon": "නිරූපකය",
"dialog.project.edit.icon.alt": "ව්යාපෘති නිරූපකය",
-3
View File
@@ -311,9 +311,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upraviť server",
"dialog.server.menu.edit": "Upraviť",
"dialog.server.menu.default": "Nastaviť ako predvolený",
"dialog.server.menu.defaultRemove": "Odstrániť predvolený",
"dialog.server.status.default": "Predvolený",
"dialog.project.edit.title": "Upraviť projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projektu",
-3
View File
@@ -311,9 +311,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "geslo",
"dialog.server.edit.title": "Uredi strežnik",
"dialog.server.menu.edit": "Uredi",
"dialog.server.menu.default": "Nastavi kot privzeto",
"dialog.server.menu.defaultRemove": "Odstrani privzeto",
"dialog.server.status.default": "Privzeto",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona projekta",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "fjalëkalimin",
"dialog.server.edit.title": "Redakto serverin",
"dialog.server.menu.edit": "Redakto",
"dialog.server.menu.default": "Cakto si parazgjedhje",
"dialog.server.menu.defaultRemove": "Hiq parazgjedhjen",
"dialog.server.status.default": "E paracaktuar",
"dialog.project.edit.title": "Redakto projektin",
"dialog.project.edit.icon": "Ikona",
"dialog.project.edit.icon.alt": "Ikona e projektit",
-3
View File
@@ -299,9 +299,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
"dialog.server.menu.edit": "Уреди",
"dialog.server.menu.default": "Подесите као подразумевано",
"dialog.server.menu.defaultRemove": "Уклони подразумевано",
"dialog.server.status.default": "подразумевано",
"dialog.project.edit.title": "Уреди пројекат",
"dialog.project.edit.icon": "Икона",
"dialog.project.edit.icon.alt": "икона пројекта",
-3
View File
@@ -491,9 +491,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "lösenord",
"dialog.server.edit.title": "Redigera server",
"dialog.server.menu.edit": "Redigera",
"dialog.server.menu.default": "Ställ in som standard",
"dialog.server.menu.defaultRemove": "Ta bort standard",
"dialog.server.status.default": "Standard",
"dialog.project.edit.title": "Redigera projekt",
"dialog.project.edit.icon": "Ikon",
"dialog.project.edit.icon.alt": "Projektikonen",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "парол",
"dialog.server.edit.title": "Серверро таҳрир кунед",
"dialog.server.menu.edit": "Таҳрир",
"dialog.server.menu.default": "Ҳамчун пешфарз таъин кунед",
"dialog.server.menu.defaultRemove": "Пешфарзро хориҷ кунед",
"dialog.server.status.default": "Пешфарз",
"dialog.project.edit.title": "Таҳрири лоиҳа",
"dialog.project.edit.icon": "Нишона",
"dialog.project.edit.icon.alt": "Нишонаи лоиҳа",
-3
View File
@@ -319,9 +319,6 @@ export const dict = {
"dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์",
"dialog.server.menu.edit": "แก้ไข",
"dialog.server.menu.default": "ตั้งเป็นค่าเริ่มต้น",
"dialog.server.menu.defaultRemove": "เอาค่าเริ่มต้นออก",
"dialog.server.status.default": "ค่าเริ่มต้น",
"dialog.project.edit.title": "แก้ไขโปรเจกต์",
"dialog.project.edit.icon": "ไอคอน",
-3
View File
@@ -286,9 +286,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serweri redaktirläň",
"dialog.server.menu.edit": "Redaktirläň",
"dialog.server.menu.default": "Dymmaklyk hökmünde düzüň",
"dialog.server.menu.defaultRemove": "Dymmaklygy aýyryň",
"dialog.server.status.default": "Bellenen",
"dialog.project.edit.title": "Taslamany redaktirläň",
"dialog.project.edit.icon": "Nyşan",
"dialog.project.edit.icon.alt": "Taslamanyň nyşany",
-3
View File
@@ -324,9 +324,6 @@ export const dict = {
"dialog.server.edit.title": "Sunucuyu düzenle",
"dialog.server.menu.edit": "Düzenle",
"dialog.server.menu.default": "Varsayılan olarak ayarla",
"dialog.server.menu.defaultRemove": "Varsayılanı kaldır",
"dialog.server.status.default": "Varsayılan",
"dialog.project.edit.title": "Projeyi düzenle",
"dialog.project.edit.icon": "Simge",
-3
View File
@@ -341,9 +341,6 @@ export const dict = {
"dialog.server.edit.title": "Редагувати сервер",
"dialog.server.menu.edit": "Редагувати",
"dialog.server.menu.default": "Встановити за замовчуванням",
"dialog.server.menu.defaultRemove": "Скасувати використання за замовчуванням",
"dialog.server.status.default": "За замовчуванням",
"dialog.project.edit.title": "Редагувати проєкт",
"dialog.project.edit.icon": "Іконка",
-3
View File
@@ -295,9 +295,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور میں ترمیم کریں۔",
"dialog.server.menu.edit": "ترمیم کریں۔",
"dialog.server.menu.default": "بطور ڈیفالٹ سیٹ کریں۔",
"dialog.server.menu.defaultRemove": "ڈیفالٹ کو ہٹا دیں۔",
"dialog.server.status.default": "طے شدہ",
"dialog.project.edit.title": "پروجیکٹ میں ترمیم کریں۔",
"dialog.project.edit.icon": "آئیکن",
"dialog.project.edit.icon.alt": "پروجیکٹ کا آئیکن",
-3
View File
@@ -287,9 +287,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serverni tahrirlash",
"dialog.server.menu.edit": "Tahrirlash",
"dialog.server.menu.default": "Standart sifatida o'rnating",
"dialog.server.menu.defaultRemove": "Standartni olib tashlang",
"dialog.server.status.default": "Standart",
"dialog.project.edit.title": "Loyihani tahrirlash",
"dialog.project.edit.icon": "Belgi",
"dialog.project.edit.icon.alt": "Loyiha belgisi",
-3
View File
@@ -299,9 +299,6 @@ export const dict = {
"dialog.server.add.passwordPlaceholder": "mật khẩu",
"dialog.server.edit.title": "Chỉnh sửa máy chủ",
"dialog.server.menu.edit": "Chỉnh sửa",
"dialog.server.menu.default": "Đặt làm mặc định",
"dialog.server.menu.defaultRemove": "Xóa mặc định",
"dialog.server.status.default": "Mặc định",
"dialog.project.edit.title": "Chỉnh sửa dự án",
"dialog.project.edit.icon": "Biểu tượng",
"dialog.project.edit.icon.alt": "Biểu tượng dự án",
-3
View File
@@ -336,9 +336,6 @@ export const dict = {
"dialog.server.add.password": "密码(可选)",
"dialog.server.edit.title": "编辑服务器",
"dialog.server.menu.edit": "编辑",
"dialog.server.menu.default": "设为默认",
"dialog.server.menu.defaultRemove": "取消默认",
"dialog.server.status.default": "默认",
"dialog.project.edit.title": "编辑项目",
"dialog.project.edit.icon": "图标",
-3
View File
@@ -321,9 +321,6 @@ export const dict = {
"dialog.server.edit.title": "編輯伺服器",
"dialog.server.menu.edit": "編輯",
"dialog.server.menu.default": "設為預設",
"dialog.server.menu.defaultRemove": "取消預設",
"dialog.server.status.default": "預設",
"dialog.project.edit.title": "編輯專案",
"dialog.project.edit.icon": "圖示",
@@ -2,7 +2,6 @@ import { createSimpleContext } from "@opencode/ui/context"
import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
import type { Accessor } from "solid-js"
import type { DesktopMenuAction } from "@/shell/commands/desktop-menu"
import { ServerConnection } from "@/runtime/server/registry"
import type { DraftStore } from "@/runtime/persistence/drafts"
import type { Bridge } from "@opencode/gui-extensions/sdk/bridge"
@@ -81,12 +80,6 @@ type PlatformBase = {
/** Fetch override */
fetch?: typeof fetch
/** Get the configured default server URL (platform-specific) */
getDefaultServer?(): Promise<ServerConnection.Key | null>
/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServer?(url: ServerConnection.Key | null): Promise<void> | void
/** Webview zoom level (desktop only) */
webviewZoom?: Accessor<number>
+1 -40
View File
@@ -1,13 +1,7 @@
import { createBrowserDraftStore } from "@/runtime/persistence/drafts"
import { ServerConnection } from "@/runtime/server/registry"
import type { Platform } from "./platform"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
export function createWebPlatform(version: string) {
const currentServerUrl = getCurrentServerUrl()
const storedServerUrl = readDefaultServerUrl()
const platform: Platform = {
platform: "web",
draftStore: createBrowserDraftStore(),
@@ -43,18 +37,11 @@ export function createWebPlatform(version: string) {
notification.close()
}
},
getDefaultServer: async () => {
const stored = readDefaultServerUrl()
return stored ? ServerConnection.Key.make(stored) : null
},
setDefaultServer: writeDefaultServerUrl,
}
return {
platform,
currentServerUrl,
defaultServerUrl: storedServerUrl ?? currentServerUrl,
currentServerUrl: getCurrentServerUrl(),
}
}
@@ -72,29 +59,3 @@ function getCurrentServerUrl() {
return location.origin
}
function readDefaultServerUrl() {
if (typeof localStorage === "undefined") return null
try {
return localStorage.getItem(DEFAULT_SERVER_URL_KEY)
} catch {
return null
}
}
function writeDefaultServerUrl(value: string | null) {
if (typeof localStorage === "undefined") return
try {
if (value !== null) {
localStorage.setItem(DEFAULT_SERVER_URL_KEY, value)
return
}
localStorage.removeItem(DEFAULT_SERVER_URL_KEY)
} catch {
return
}
}
+1 -5
View File
@@ -214,11 +214,7 @@ export namespace ServerConnection {
export const { use: useServers, provider: ServersProvider } = createSimpleContext({
name: "Server",
gate: true,
init: (props: {
defaultServer?: ServerConnection.Key
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
}) => {
init: (props: { canonicalLocalServer?: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
const [store, setStore, _, hydrated] = persisted(
{
...Persist.global("server"),
@@ -5,21 +5,19 @@ import { sortServerConnections } from "./controller"
const server = (url: string): ServerConnection.Http => ({ type: "http", http: { url } })
describe("sortServerConnections", () => {
test("places the default first and preserves health and insertion ordering", () => {
test("orders by health and preserves insertion ordering", () => {
const first = server("http://first")
const offline = server("http://offline")
const preferred = server("http://preferred")
const unknown = server("http://unknown")
const result = sortServerConnections({
servers: [first, offline, preferred, unknown],
servers: [first, offline, unknown],
health: {
[ServerConnection.key(first)]: { healthy: true },
[ServerConnection.key(offline)]: { healthy: false },
},
defaultKey: ServerConnection.key(preferred),
})
expect(result).toEqual([preferred, first, unknown, offline])
expect(result).toEqual([first, unknown, offline])
})
})
@@ -1,58 +1,15 @@
import { createMemo, createResource } from "solid-js"
import { createMemo } from "solid-js"
import { useGlobal } from "@/runtime/server/runtime"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { ServerConnection, useServers } from "@/runtime/server/registry"
import { useTabs } from "@/shell/tabs/tabs"
import { type ServerHealth } from "@/runtime/server/health"
import { showToast } from "@/shell/notifications/toast"
import { useExtensionServers } from "@/runtime/extension/servers"
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
function useDefaultServer() {
const language = useLanguage()
const platform = usePlatform()
const [defaultKey, defaultKeyActions] = createResource(
async () => {
try {
return (await platform.getDefaultServer?.()) ?? null
} catch (err) {
showRequestError(language, err)
return null
}
},
{ initialValue: null },
)
const set = async (key: ServerConnection.Key | null) => {
try {
await platform.setDefaultServer?.(key)
defaultKeyActions.mutate(key)
} catch (err) {
showRequestError(language, err)
}
}
return {
key: () => defaultKey.latest,
available: createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer),
set,
}
}
export function sortServerConnections(input: {
servers: ServerConnection.Any[]
health: Record<string, ServerHealth | undefined>
defaultKey: ServerConnection.Key | null
}) {
const order = new Map(input.servers.map((item, index) => [item, index] as const))
@@ -65,10 +22,6 @@ export function sortServerConnections(input: {
}
return input.servers.slice().sort((a, b) => {
const preferred =
Number(ServerConnection.key(b) === input.defaultKey) - Number(ServerConnection.key(a) === input.defaultKey)
if (preferred !== 0) return preferred
const health = rank(input.health[ServerConnection.key(a)]) - rank(input.health[ServerConnection.key(b)])
if (health !== 0) return health
@@ -81,24 +34,23 @@ export function useServerActionsController() {
const server = useServers()
const extensions = useExtensionServers()
const tabs = useTabs()
const platform = usePlatform()
const language = useLanguage()
const defaults = useDefaultServer()
const remove = async (key: ServerConnection.Key) => {
try {
await extensions.entry(key)?.entry.remove?.()
tabs.removeServer(key)
server.remove(key)
if ((await platform.getDefaultServer?.()) === key) await defaults.set(null)
} catch (err) {
showRequestError(language, err)
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
}
return {
defaults,
connection: {
canRemove: server.canRemove,
remove,
@@ -126,7 +78,6 @@ export function useServerCollectionController() {
sortServerConnections({
servers: items(),
health: global.servers.health,
defaultKey: actions.defaults.key(),
}),
)
@@ -23,11 +23,6 @@ export function ExtensionServerRow(props: { server: ServerConnection.Key; contro
authenticationRequired={indicator.auth}
/>
),
default: {
available: () => props.controller.defaults.available(),
current: () => props.controller.defaults.key() === props.server,
set: (value) => void props.controller.defaults.set(value ? props.server : null),
},
remove: () => props.controller.connection.remove(props.server),
Items: () => <ServerRowItems server={props.server} />,
}
@@ -20,14 +20,10 @@ export const ServerRowMenu: Component<{
<ServerRowMenuView
server={props.server}
labels={serverMenuLabels(language)}
canDefault={props.domain.defaults.available()}
isDefault={props.domain.defaults.key() === key}
canRemove={props.domain.connection.canRemove(key)}
canHide={props.domain.connection.canHide(key)}
hidden={props.domain.connection.isHidden(key)}
onEdit={props.onEdit}
onSetDefault={() => props.domain.defaults.set(key)}
onRemoveDefault={() => props.domain.defaults.set(null)}
onRemove={() => props.domain.connection.remove(key)}
onHide={() => props.domain.connection.setHidden(key, true)}
onShow={() => props.domain.connection.setHidden(key, false)}
@@ -42,8 +38,6 @@ export function serverMenuLabels(language: ReturnType<typeof useLanguage>) {
more: language.t("common.moreOptions"),
server: language.t("settings.section.server"),
edit: language.t("dialog.server.menu.edit"),
default: language.t("dialog.server.menu.default"),
defaultRemove: language.t("dialog.server.menu.defaultRemove"),
remove: language.t("dialog.server.menu.remove"),
hide: language.t("dialog.server.menu.hide"),
show: language.t("dialog.server.menu.show"),
@@ -53,14 +47,10 @@ export function serverMenuLabels(language: ReturnType<typeof useLanguage>) {
export const ServerRowMenuView: Component<{
server: ServerConnection.Any
labels: ReturnType<typeof serverMenuLabels>
canDefault: boolean
isDefault: boolean
canRemove: boolean
canHide?: boolean
hidden?: boolean
onEdit: (server: ServerConnection.Http) => void
onSetDefault: () => void
onRemoveDefault: () => void
onRemove: () => void
onHide?: () => void
onShow?: () => void
@@ -93,12 +83,6 @@ export const ServerRowMenuView: Component<{
>
{props.labels.edit}
</Menu.Item>
<Show when={props.canDefault && !props.isDefault}>
<Menu.Item onSelect={props.onSetDefault}>{props.labels.default}</Menu.Item>
</Show>
<Show when={props.canDefault && props.isDefault}>
<Menu.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</Menu.Item>
</Show>
<Show when={props.hidden}>
<Menu.Item onSelect={props.onShow}>{props.labels.show}</Menu.Item>
</Show>
@@ -1,4 +1,3 @@
import { Badge } from "@opencode/ui/badge"
import { Button } from "@opencode/ui/button"
import { useDialog } from "@opencode/ui/context/dialog"
import { createMemo, Show, type Component } from "solid-js"
@@ -82,9 +81,6 @@ export const SettingsServerGeneral: Component<{
</Button>
)}
</Show>
<Show when={controller.defaults.available() && controller.defaults.key() === props.entry.key}>
<Badge>{language.t("dialog.server.status.default")}</Badge>
</Show>
<ServerRowMenu server={server()} domain={controller} onEdit={edit} />
</div>
</div>
+1 -7
View File
@@ -248,13 +248,7 @@ function RootSettings() {
const ordered = createMemo(() => {
const order = new Map(list().map((server, index) => [ServerConnection.key(server), index]))
return inventory().toSorted((a, b) => {
const preferred = Number(b.key === servers.defaults.key()) - Number(a.key === servers.defaults.key())
if (preferred) return preferred
return (order.get(a.key) ?? list().length) - (order.get(b.key) ?? list().length)
})
return inventory().toSorted((a, b) => (order.get(a.key) ?? list().length) - (order.get(b.key) ?? list().length))
})
const sourceServer = createMemo(() => {
@@ -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,
+16 -1
View File
@@ -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>
+13 -1
View File
@@ -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) =>
@@ -2314,6 +2314,8 @@ export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string;
export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFields }
export type IntegrationExternalMethod = { id: string; type: "external"; label: string; form?: FormFields }
export type CredentialEntry = {
id: string
integrationID: string
@@ -2351,6 +2353,7 @@ export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationExternalMethod
| IntegrationEnvMethod
export type FormCreated = {
@@ -5688,6 +5691,28 @@ export type IntegrationConnectKeyInput = {
export type IntegrationConnectKeyOutput = void
export type IntegrationConnectExternalInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly methodID: {
readonly methodID: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly label?: string | undefined
}["methodID"]
readonly answer?: {
readonly methodID: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly label?: string | undefined
}["answer"]
readonly label?: {
readonly methodID: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly label?: string | undefined
}["label"]
}
export type IntegrationConnectExternalOutput = void
export type IntegrationOauthConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
+5 -5
View File
@@ -40,31 +40,31 @@
"workerd": "./src/pty/pty.workerd.ts",
"bun": "./src/pty/pty.bun.ts",
"node": "./src/pty/pty.node.ts",
"default": "./src/pty/pty.node.ts"
"default": "./src/pty/pty.bun.ts"
},
"#persistent-pty-binary": {
"workerd": "./src/persistent-pty/binary.workerd.ts",
"bun": "./src/persistent-pty/binary.bun.ts",
"node": "./src/persistent-pty/binary.node.ts",
"default": "./src/persistent-pty/binary.node.ts"
"default": "./src/persistent-pty/binary.bun.ts"
},
"#fff": {
"workerd": "./src/filesystem/fff.workerd.ts",
"bun": "./src/filesystem/fff.bun.ts",
"node": "./src/filesystem/fff.node.ts",
"default": "./src/filesystem/fff.node.ts"
"default": "./src/filesystem/fff.bun.ts"
},
"#photon-wasm": {
"workerd": "./src/image/photon-wasm.workerd.ts",
"bun": "./src/image/photon-wasm.bun.ts",
"node": "./src/image/photon-wasm.node.ts",
"default": "./src/image/photon-wasm.node.ts"
"default": "./src/image/photon-wasm.bun.ts"
},
"#shell-parser-wasm": {
"workerd": "./src/shell/parser-wasm.workerd.ts",
"bun": "./src/shell/parser-wasm.bun.ts",
"node": "./src/shell/parser-wasm.node.ts",
"default": "./src/shell/parser-wasm.node.ts"
"default": "./src/shell/parser-wasm.bun.ts"
},
"#v1-migration": {
"types": "./src/database/v1-migration.bun.ts",
+6 -2
View File
@@ -2,10 +2,14 @@ export * as ConfigMarkdown from "./markdown.js"
import matter from "gray-matter"
export function parse(content: string) {
// Passing options bypasses gray-matter's module-global content cache, which
// it populates before parsing: a failed YAML parse poisons the entry and
// every later parse of the same content replays it without throwing, so the
// sanitize fallback below never runs. Upstream: jonschlinkert/gray-matter#166.
try {
return matter(content)
return matter(content, {})
} catch {
return matter(sanitize(content))
return matter(sanitize(content), {})
}
}
+53 -1
View File
@@ -45,6 +45,9 @@ export type CommandMethod = Integration.CommandMethod
export const KeyMethod = Integration.KeyMethod
export type KeyMethod = Integration.KeyMethod
export const ExternalMethod = Integration.ExternalMethod
export type ExternalMethod = Integration.ExternalMethod
export const EnvMethod = Integration.EnvMethod
export type EnvMethod = Integration.EnvMethod
@@ -82,6 +85,11 @@ export interface KeyImplementation {
readonly method: KeyMethod
}
export interface ExternalImplementation {
readonly integrationID: ID
readonly method: ExternalMethod
}
export interface CommandImplementation {
readonly integrationID: ID
readonly method: CommandMethod
@@ -92,7 +100,12 @@ export interface EnvImplementation {
readonly method: EnvMethod
}
export type Implementation = OAuthImplementation | CommandImplementation | KeyImplementation | EnvImplementation
export type Implementation =
| OAuthImplementation
| CommandImplementation
| KeyImplementation
| ExternalImplementation
| EnvImplementation
export const Attempt = Integration.Attempt
export type Attempt = Integration.Attempt
@@ -180,6 +193,17 @@ export interface Interface extends State.Transformable<Editor> {
/** User-facing label for the stored credential. */
readonly label?: string
}) => Effect.Effect<void, AuthorizationError>
/** Runs an external method and stores a reference configured by its form answers. */
readonly external: (input: {
/** Integration receiving the credential. */
readonly integrationID: ID
/** External method that defines the form and credential source. */
readonly methodID: MethodID
/** Values collected from the method's form fields. */
readonly answer?: Form.Answer
/** User-facing label for the stored credential. */
readonly label?: string
}) => Effect.Effect<void, AuthorizationError>
/** Selects a stored credential as the active integration connection. */
readonly activate: (credentialID: Credential.ID) => Effect.Effect<void>
/** Updates a stored credential exposed as a connection. */
@@ -327,6 +351,8 @@ const layer = Layer.effect(
return method.id === implementation.method.id
if (method.type === "command" && implementation.method.type === "command")
return method.id === implementation.method.id
if (method.type === "external" && implementation.method.type === "external")
return method.id === implementation.method.id
return true
})
if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
@@ -345,6 +371,7 @@ const layer = Layer.effect(
if (candidate.type !== method.type) return false
if (candidate.type === "oauth" && method.type === "oauth") return candidate.id === method.id
if (candidate.type === "command" && method.type === "command") return candidate.id === method.id
if (candidate.type === "external" && method.type === "external") return candidate.id === method.id
return true
})
if (index !== -1) current.methods.splice(index, 1)
@@ -736,6 +763,31 @@ const layer = Layer.effect(
}),
})
}),
external: Effect.fn("Integration.connection.external")(function* (input) {
const method = state
.get()
.integrations.get(input.integrationID)
?.methods.find((method) => method.type === "external" && method.id === input.methodID)
if (method?.type !== "external")
return yield* new AuthorizationError({ cause: new Error(`External method not found: ${input.methodID}`) })
const answer = input.answer ?? {}
if (method.form) {
const invalid = Form.validateFields(method.form) ?? Form.validateAnswer(method.form, answer)
if (invalid) return yield* new AuthorizationError({ cause: new Error(invalid) })
}
if (!method.form && Object.keys(answer).length > 0) {
return yield* new AuthorizationError({ cause: new Error("External method does not accept a form answer") })
}
yield* createCredential({
integrationID: input.integrationID,
label: input.label,
value: Credential.External.make({
type: "external",
methodID: method.id,
...(Object.keys(answer).length > 0 ? { metadata: answer } : {}),
}),
})
}),
activate: Effect.fn("Integration.connection.activate")((credentialID) => credentials.activate(credentialID)),
update: Effect.fn("Integration.connection.update")((credentialID, updates) =>
credentials.update(credentialID, updates),
@@ -1,6 +1,6 @@
// opencode-pty does not ship Windows binaries.
export const available = process.platform !== "win32"
export async function resolveBinary(_bin: string) {
export async function resolveBinary() {
return process.env.OPENCODE_PTY_BIN || "opencode-pty"
}
@@ -1,5 +1,5 @@
export const available = false
export async function resolveBinary(_bin: string): Promise<string> {
export async function resolveBinary(): Promise<string> {
throw new Error("Persistent PTYs are unavailable in this runtime")
}
+13
View File
@@ -286,6 +286,13 @@ export const make = Effect.fn("PluginHost.make")(function* (
answer: input.answer,
label: input.label,
}),
external: (input) =>
integration.connection.external({
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
answer: input.answer,
label: input.label,
}),
},
oauth: {
connect: (input) =>
@@ -665,6 +672,12 @@ function methodImplementation(input: IntegrationMethodRegistration): Integration
method: { ...input.method, id: Integration.MethodID.make(input.method.id) },
}
}
if (input.method.type === "external") {
return {
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, id: Integration.MethodID.make(input.method.id) },
}
}
return {
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
@@ -13,6 +13,7 @@ import { Integration } from "../../integration.js"
import { Model } from "../../model.js"
import { OauthCallbackPage } from "../../oauth/page.js"
import { Provider } from "../../provider.js"
import { SessionAffinity } from "../../session/affinity.js"
import type { PluginInternal } from "../internal.js"
// First-time sign-in registers a user-owned client; OpenAI returns its issued client ID on the callback.
@@ -264,6 +265,21 @@ export const ChatGPTPlugin = define({
}),
{ providerID },
)
yield* ctx.session.hook(
"model.request",
(evt) =>
Effect.gen(function* () {
if (!chatgpt) return
const session = yield* ctx.session
.get({ sessionID: evt.sessionID })
.pipe(Effect.orElseSucceed(() => undefined))
// Mirror the Codex client's session headers: ChatGPT derives prompt-cache affinity from session-id.
evt.headers["session-id"] = session ? SessionAffinity.get(session) : evt.sessionID
evt.headers["thread-id"] = evt.sessionID
evt.headers["x-client-request-id"] = evt.sessionID
}),
{ providerID },
)
yield* ctx.provider.transform((providers) => {
const item = providers.get(providerID)
if (!item) return
+25 -5
View File
@@ -55,7 +55,7 @@ const forkTitle = (value?: string) => {
return `${value} (fork #1)`
}
function applyUsage(db: DatabaseService, sessionID: SessionSchema.ID, value: Usage) {
function applyUsage(db: DatabaseService, sessionID: SessionSchema.ID, value: Usage, timeUpdated?: number) {
return db
.update(SessionTable)
.set({
@@ -65,13 +65,22 @@ function applyUsage(db: DatabaseService, sessionID: SessionSchema.ID, value: Usa
tokens_reasoning: sql`${SessionTable.tokens_reasoning} + ${value.tokens.reasoning}`,
tokens_cache_read: sql`${SessionTable.tokens_cache_read} + ${value.tokens.cache.read}`,
tokens_cache_write: sql`${SessionTable.tokens_cache_write} + ${value.tokens.cache.write}`,
time_updated: sql`${SessionTable.time_updated}`,
time_updated: timeUpdated ?? sql`${SessionTable.time_updated}`,
})
.where(eq(SessionTable.id, sessionID))
.run()
.pipe(Effect.orDie)
}
function touch(db: DatabaseService, event: MessageEvent) {
return db
.update(SessionTable)
.set({ time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
}
const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* (
db: DatabaseService,
bus: Bus.Interface,
@@ -681,19 +690,30 @@ const layer = Layer.effectDiscard(
yield* bus.project(SessionEvent.Skill.Activated, (event) => run(db, event))
yield* bus.project(SessionEvent.Shell.Started, (event) => run(db, event))
yield* bus.project(SessionEvent.Shell.Ended, (event) => run(db, event))
yield* bus.project(SessionEvent.Step.Started, (event) => run(db, event))
yield* bus.project(SessionEvent.Step.Started, (event) =>
Effect.gen(function* () {
yield* run(db, event)
yield* touch(db, event)
}),
)
yield* bus.project(SessionEvent.Step.Streamed, (event) => run(db, event))
yield* bus.project(SessionEvent.Step.Ended, (event) =>
Effect.gen(function* () {
yield* run(db, event)
yield* applyUsage(db, event.data.sessionID, event.data)
yield* applyUsage(db, event.data.sessionID, event.data, event.created)
}),
)
yield* bus.project(SessionEvent.Step.Failed, (event) =>
Effect.gen(function* () {
yield* run(db, event)
if (event.data.cost !== undefined && event.data.tokens !== undefined)
yield* applyUsage(db, event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens })
yield* applyUsage(
db,
event.data.sessionID,
{ cost: event.data.cost, tokens: event.data.tokens },
event.created,
)
else yield* touch(db, event)
}),
)
yield* bus.project(SessionEvent.Text.Started, (event) => run(db, event))
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test"
import { ConfigMarkdown } from "@opencode/core/config/markdown"
const invalidYaml = `---
description: Use when the user needs to crawl pages. Keywords: crawl, scrape
---
body`
describe("ConfigMarkdown.parse", () => {
test("recovers unquoted-colon frontmatter via the sanitize fallback", () => {
const parsed = ConfigMarkdown.parse(invalidYaml)
expect(parsed.data.description).toBe("Use when the user needs to crawl pages. Keywords: crawl, scrape")
expect(parsed.content.trim()).toBe("body")
})
test("recovers the same content again after a previous failed parse", () => {
// gray-matter caches by content before parsing; a poisoned entry used to
// make every later parse of the same text return silently without data.
expect(() => ConfigMarkdown.parse("---\ndescription: [unclosed\n---\nbody")).toThrow()
const parsed = ConfigMarkdown.parse(invalidYaml)
expect(parsed.data.description).toBe("Use when the user needs to crawl pages. Keywords: crawl, scrape")
})
test("keeps throwing for the same unparseable content on every call", () => {
const input = "---\ndescription: [unclosed\n---\nbody"
expect(() => ConfigMarkdown.parse(input)).toThrow()
expect(() => ConfigMarkdown.parse(input)).toThrow()
})
test("parses plain content without frontmatter", () => {
const parsed = ConfigMarkdown.parse("just body")
expect(parsed.content).toBe("just body")
expect(parsed.data).toEqual({})
})
})
Loaded 100 of 265 files, more files were not shown because too many files have changed in this diff. Show more