mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-16 10:54:52 +00:00
Compare commits
1 Commits
dev
...
kit/ns-uti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f618408fa |
@@ -7,7 +7,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -28,7 +28,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"categories": {
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"typescript/no-base-to-string": "warn",
|
||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||
"require-yield": "off",
|
||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||
@@ -17,35 +10,7 @@
|
||||
// Intentional control char matching (ANSI escapes, null byte sanitization)
|
||||
"no-control-regex": "off",
|
||||
// SST and plugin tools require triple-slash references
|
||||
"triple-slash-reference": "off",
|
||||
|
||||
// Suspicious category: suppress noisy rules
|
||||
// Effect's nested function* closures inherently shadow outer scope
|
||||
"no-shadow": "off",
|
||||
// Namespace-heavy codebase makes this too noisy
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
// Opinionated — .sort()/.reverse() mutation is fine in this codebase
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/no-array-reverse": "off",
|
||||
// Not relevant — this isn't a DOM event handler codebase
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
// Bundler handles module resolution
|
||||
"unicorn/require-module-specifiers": "off",
|
||||
// postMessage target origin not relevant for this codebase
|
||||
"unicorn/require-post-message-target-origin": "off",
|
||||
// Side-effectful constructors are intentional in some places
|
||||
"no-new": "off",
|
||||
|
||||
// Type-aware: catch unhandled promises
|
||||
"typescript/no-floating-promises": "warn",
|
||||
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||
"typescript/no-misused-spread": "warn"
|
||||
"triple-slash-reference": "off"
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"]
|
||||
}
|
||||
|
||||
17
bun.lock
17
bun.lock
@@ -20,7 +20,6 @@
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
@@ -523,9 +522,7 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/npmcli__arborist": "6.3.3",
|
||||
"@types/semver": "catalog:",
|
||||
},
|
||||
},
|
||||
@@ -1683,18 +1680,6 @@
|
||||
|
||||
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.21.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-81TmmuBcPedEA0MwRmObuQuXnCprS1UiHQWGe7pseqNAJzUWXeAPrayqKTACX92VpruJI+yvY0XJrFp11PpcTA=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.21.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sbjBr6zDduX8rNO0PTjhf7VYLCPWqdijWiMPp8e10qu6Tam1GdaVLaLlX8QrNupTgglO1GvqqgY/jcacWL8a6g=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.21.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jNrOcy53R5TJQfrK444Cm60bW9437xDoxPbm3AdvFSo/fhdFMllawc7uZC2Wzr+EAjTkW13K8R4QHzsUdBG9fQ=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.21.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWeRxJJILDE4b9UqHEWGBxcBc1TUS6zWHhxcyxTZMwf4q3wdKeu0OHYAcwLGJzoSjEIf6FTjyfPiRNil2oqsdg=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.21.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ob9AA9teI8ckPo1whV1smLr5NrqwgBv/8boDbK0YZG+fKgNGRwr1hBj1ORgFWOQaUBv+5njp5A0RAfJJjQ95QQ=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="],
|
||||
@@ -4115,8 +4100,6 @@
|
||||
|
||||
"oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="],
|
||||
|
||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="],
|
||||
|
||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||
|
||||
"p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="],
|
||||
|
||||
@@ -513,7 +513,7 @@ async function subscribeSessionEvents() {
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
let text = ""
|
||||
void (async () => {
|
||||
;(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const { done, value } = await reader.read()
|
||||
@@ -542,7 +542,7 @@ async function subscribeSessionEvents() {
|
||||
? JSON.stringify(part.state.input)
|
||||
: "Unknown"
|
||||
console.log()
|
||||
console.log(`${color}|`, `\x1b[0m\x1b[2m ${tool.padEnd(7, " ")}`, "", `\x1b[0m${title}`)
|
||||
console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
@@ -776,7 +776,7 @@ async function assertPermissions() {
|
||||
console.log(` permission: ${permission}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to check permissions: ${error}`)
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error })
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
||||
}
|
||||
|
||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-NJAK+cPjwn+2ojDLyyDmBQyx2pD+rILetp7VCylgjek=",
|
||||
"aarch64-linux": "sha256-q8NTtFQJoyM7TTvErGA6RtmUscxoZKD/mj9N6S5YhkA=",
|
||||
"aarch64-darwin": "sha256-/ccoSZNLef6j9j14HzpVqhKCR+czM3mhPKPH51mHO24=",
|
||||
"x86_64-darwin": "sha256-6Pd10sMHL/5ZoWNvGPwPn4/AIs1TKjt/3gFyrVpBaE0="
|
||||
"x86_64-linux": "sha256-VIgTxIjmZ4Bfwwdj/YFmRJdBpPHYhJSY31kh06EXX+0=",
|
||||
"aarch64-linux": "sha256-9118AS1ED0nrliURgZYBRuF/18RqXpUouhYJRlZ6jeA=",
|
||||
"aarch64-darwin": "sha256-ppo3MfSIGKQHJCdYEZiLFRc61PtcJ9J0kAXH1pNIonA=",
|
||||
"x86_64-darwin": "sha256-m+CZSOglBCTfNzbdBX6hXdDqqOzHNMzAddVp6BZVDtU="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
|
||||
@@ -121,10 +121,10 @@ function SessionProviders(props: ParentProps) {
|
||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||
return (
|
||||
<AppShellProviders>
|
||||
{/*<Suspense fallback={<Loading />}>*/}
|
||||
{props.appChildren}
|
||||
{props.children}
|
||||
{/*</Suspense>*/}
|
||||
<Suspense fallback={<Loading />}>
|
||||
{props.appChildren}
|
||||
{props.children}
|
||||
</Suspense>
|
||||
</AppShellProviders>
|
||||
)
|
||||
}
|
||||
@@ -184,41 +184,32 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/*<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>*/}
|
||||
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||
<Show
|
||||
when={startupHealthCheck()}
|
||||
fallback={
|
||||
<ConnectionError
|
||||
onRetry={() => {
|
||||
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||
if (checkMode() === "background") healthCheckActions.refetch()
|
||||
}}
|
||||
onServerSelected={(key) => {
|
||||
setCheckMode("blocking")
|
||||
server.setActive(key)
|
||||
void healthCheckActions.refetch()
|
||||
healthCheckActions.refetch()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Show>
|
||||
{/*</Show>*/}
|
||||
</Suspense>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
if (loading()) return
|
||||
if (methods().length === 1) {
|
||||
auto = true
|
||||
void selectMethod(0)
|
||||
selectMethod(0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -373,7 +373,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (selected, index) => {
|
||||
if (!selected) return
|
||||
void selectMethod(index)
|
||||
selectMethod(index)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
|
||||
@@ -348,8 +348,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
void tabs().open(value)
|
||||
void file.load(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("all")
|
||||
props.onOpenFile?.(path)
|
||||
|
||||
@@ -344,7 +344,7 @@ export function DialogSelectServer() {
|
||||
|
||||
createEffect(() => {
|
||||
items()
|
||||
void refreshHealth()
|
||||
refreshHealth()
|
||||
const interval = setInterval(refreshHealth, 10_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
@@ -498,7 +498,7 @@ export function DialogSelectServer() {
|
||||
async function handleRemove(url: ServerConnection.Key) {
|
||||
server.remove(url)
|
||||
if ((await platform.getDefaultServer?.()) === url) {
|
||||
void platform.setDefaultServer?.(null)
|
||||
platform.setDefaultServer?.(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +536,7 @@ export function DialogSelectServer() {
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) void select(x)
|
||||
if (x) select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
|
||||
@@ -54,8 +54,6 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
@@ -102,7 +100,6 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
||||
|
||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const sdk = useSDK()
|
||||
|
||||
const sync = useSync()
|
||||
const local = useLocal()
|
||||
const files = useFile()
|
||||
@@ -215,9 +212,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("all")
|
||||
const tab = files.tab(item.path)
|
||||
void tabs().open(tab)
|
||||
tabs().open(tab)
|
||||
tabs().setActive(tab)
|
||||
void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||
Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||
}
|
||||
|
||||
const recent = createMemo(() => {
|
||||
@@ -1142,7 +1139,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
if (working()) {
|
||||
void abort()
|
||||
abort()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
@@ -1208,7 +1205,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
if (working()) {
|
||||
void abort()
|
||||
abort()
|
||||
event.preventDefault()
|
||||
}
|
||||
return
|
||||
@@ -1248,18 +1245,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
) {
|
||||
return
|
||||
}
|
||||
void handleSubmit(event)
|
||||
handleSubmit(event)
|
||||
}
|
||||
}
|
||||
|
||||
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||
const agentsLoading = () => agentsQuery.isLoading
|
||||
|
||||
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||
|
||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
<PromptPopover
|
||||
@@ -1455,89 +1444,53 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||
<div class="size-4 shrink-0" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<Show when={!agentsLoading()}>
|
||||
<div data-component="prompt-agent-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={agentNames()}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={(value) => {
|
||||
local.agent.set(value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-agent" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-model"
|
||||
as="div"
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
style={control()}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
<div data-component="prompt-agent-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={agentNames()}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={(value) => {
|
||||
local.agent.set(value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-agent" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
model={local.model}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: control(),
|
||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||
"data-action": "prompt-model",
|
||||
<Button
|
||||
data-action="prompt-model"
|
||||
as="div"
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
style={control()}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||
})
|
||||
}}
|
||||
onClose={restoreFocus}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
@@ -1550,35 +1503,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</ModelSelectorPopover>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
<ModelSelectorPopover
|
||||
model={local.model}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: control(),
|
||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||
"data-action": "prompt-model",
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
onClose={restoreFocus}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</ModelSelectorPopover>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -295,7 +295,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const mode = input.mode()
|
||||
|
||||
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
|
||||
if (input.working()) void abort()
|
||||
if (input.working()) abort()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ function openSessionContext(args: {
|
||||
}) {
|
||||
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
||||
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
||||
void args.tabs.open("context")
|
||||
args.tabs.open("context")
|
||||
args.tabs.setActive("context")
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
|
||||
const close = () => {
|
||||
const count = terminal.all().length
|
||||
void terminal.close(props.terminal.id)
|
||||
terminal.close(props.terminal.id)
|
||||
if (count === 1) {
|
||||
props.onClose?.()
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
||||
let ws: WebSocket | undefined
|
||||
let term: Term | undefined
|
||||
let _ghostty: Ghostty
|
||||
let ghostty: Ghostty
|
||||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
let handleResize: () => void
|
||||
@@ -372,7 +372,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
_ghostty = g
|
||||
ghostty = g
|
||||
term = t
|
||||
output = terminalWriter((data, done) =>
|
||||
t.write(data, () => {
|
||||
@@ -415,7 +415,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
if (local.autoFocus !== false) focusTerminal()
|
||||
|
||||
if (typeof document !== "undefined" && document.fonts) {
|
||||
void document.fonts.ready.then(scheduleFit)
|
||||
document.fonts.ready.then(scheduleFit)
|
||||
}
|
||||
|
||||
const onResize = t.onResize((size) => {
|
||||
|
||||
@@ -252,48 +252,41 @@ export function Titlebar() {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex items-center shrink-0"
|
||||
classList={{
|
||||
"translate-x-0": !layout.sidebar.opened(),
|
||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Show when={hasProjects()}>
|
||||
<div class="flex items-center gap-0 transition-transform">
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Show when={hasProjects()}>
|
||||
<div
|
||||
class="flex items-center gap-0 transition-transform"
|
||||
classList={{
|
||||
"translate-x-0": !layout.sidebar.opened(),
|
||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
|
||||
@@ -128,7 +128,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
if (started) return run
|
||||
started = true
|
||||
run = (async () => {
|
||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||
while (!abort.signal.aborted && started) {
|
||||
attempt = new AbortController()
|
||||
lastEventAt = Date.now()
|
||||
|
||||
@@ -26,7 +26,6 @@ import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { sanitizeProject } from "./global-sync/utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -42,9 +41,6 @@ type GlobalStore = {
|
||||
reload: undefined | "pending" | "complete"
|
||||
}
|
||||
|
||||
export const loadSessionsQuery = (directory: string) =>
|
||||
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
@@ -71,7 +67,6 @@ function createGlobalSync() {
|
||||
config: {},
|
||||
reload: undefined,
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let active = true
|
||||
let projectWritten = false
|
||||
@@ -203,53 +198,46 @@ function createGlobalSync() {
|
||||
}
|
||||
|
||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||
const promise = queryClient
|
||||
.ensureQueryData({
|
||||
...loadSessionsQuery(directory),
|
||||
queryFn: () =>
|
||||
loadRootSessionsWithFallback({
|
||||
directory,
|
||||
limit,
|
||||
list: (query) => globalSDK.client.session.list(query),
|
||||
})
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
const limit = store.limit
|
||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
const project = getFilename(directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: formatServerError(err, language.t),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
const promise = loadRootSessionsWithFallback({
|
||||
directory,
|
||||
limit,
|
||||
list: (query) => globalSDK.client.session.list(query),
|
||||
})
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
const limit = store.limit
|
||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
const project = getFilename(directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: formatServerError(err, language.t),
|
||||
})
|
||||
})
|
||||
.then(() => {})
|
||||
|
||||
sessionLoads.set(directory, promise)
|
||||
void promise.finally(() => {
|
||||
promise.finally(() => {
|
||||
sessionLoads.delete(directory)
|
||||
children.unpin(directory)
|
||||
})
|
||||
@@ -262,9 +250,8 @@ function createGlobalSync() {
|
||||
if (pending) return pending
|
||||
|
||||
children.pin(directory)
|
||||
const promise = Promise.resolve().then(async () => {
|
||||
const promise = (async () => {
|
||||
const child = children.ensureChild(directory)
|
||||
child[1]("bootstrapPromise", promise!)
|
||||
const cache = children.vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
const sdk = sdkFor(directory)
|
||||
@@ -282,12 +269,11 @@ function createGlobalSync() {
|
||||
vcsCache: cache,
|
||||
loadSessions,
|
||||
translate: language.t,
|
||||
queryClient,
|
||||
})
|
||||
})
|
||||
})()
|
||||
|
||||
booting.set(directory, promise)
|
||||
void promise.finally(() => {
|
||||
promise.finally(() => {
|
||||
booting.delete(directory)
|
||||
children.unpin(directory)
|
||||
})
|
||||
@@ -331,7 +317,7 @@ function createGlobalSync() {
|
||||
setSessionTodo,
|
||||
vcsCache: children.vcsCache.get(directory),
|
||||
loadLsp: () => {
|
||||
void sdkFor(directory)
|
||||
sdkFor(directory)
|
||||
.lsp.status()
|
||||
.then((x) => {
|
||||
setStore("lsp", x.data ?? [])
|
||||
@@ -360,7 +346,6 @@ function createGlobalSync() {
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
queryClient,
|
||||
})
|
||||
bootedAt = Date.now()
|
||||
} finally {
|
||||
@@ -374,13 +359,13 @@ function createGlobalSync() {
|
||||
eventFrame = undefined
|
||||
eventTimer = setTimeout(() => {
|
||||
eventTimer = undefined
|
||||
void globalSDK.event.start()
|
||||
globalSDK.event.start()
|
||||
}, 0)
|
||||
})
|
||||
} else {
|
||||
eventTimer = setTimeout(() => {
|
||||
eventTimer = undefined
|
||||
void globalSDK.event.start()
|
||||
globalSDK.event.start()
|
||||
}, 0)
|
||||
}
|
||||
void bootstrap()
|
||||
|
||||
@@ -18,8 +18,6 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||
import { loadSessionsQuery } from "../global-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -73,7 +71,6 @@ export async function bootstrapGlobal(input: {
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const fast = [
|
||||
() =>
|
||||
@@ -83,16 +80,11 @@ export async function bootstrapGlobal(input: {
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null),
|
||||
queryFn: () =>
|
||||
retry(() =>
|
||||
input.globalSDK.provider.list().then((x) => {
|
||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||
return null
|
||||
}),
|
||||
),
|
||||
}),
|
||||
retry(() =>
|
||||
input.globalSDK.provider.list().then((x) => {
|
||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
@@ -180,12 +172,6 @@ function warmSessions(input: {
|
||||
).then(() => undefined)
|
||||
}
|
||||
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
sdk: OpencodeClient
|
||||
@@ -200,7 +186,6 @@ export async function bootstrapDirectory(input: {
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const loading = input.store.status !== "complete"
|
||||
const seededProject = projectID(input.directory, input.global.project)
|
||||
@@ -222,7 +207,97 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("lsp", [])
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||
const fast = [
|
||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? input.store.vcs
|
||||
input.setStore("vcs", next)
|
||||
if (next) input.vcsCache.setStore("value", next)
|
||||
}),
|
||||
),
|
||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.permission.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession(
|
||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||
)
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.question.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.mcp.status().then((x) => {
|
||||
input.setStore("mcp", x.data!)
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const errs = errors(await runAll(fast))
|
||||
if (errs.length > 0) {
|
||||
@@ -235,138 +310,36 @@ export async function bootstrapDirectory(input: {
|
||||
})
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadAgentsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||
() => null,
|
||||
),
|
||||
}),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? input.store.vcs
|
||||
input.setStore("vcs", next)
|
||||
if (next) input.vcsCache.setStore("value", next)
|
||||
}),
|
||||
),
|
||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.permission.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession(
|
||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||
)
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.question.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.mcp.status().then((x) => {
|
||||
input.setStore("mcp", x.data!)
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
if (slowErrs.length > 0) {
|
||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(slowErrs[0], input.translate),
|
||||
})
|
||||
}
|
||||
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
if (slowErrs.length > 0) {
|
||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(slowErrs[0], input.translate),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
}
|
||||
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void input.queryClient.ensureQueryData({
|
||||
...loadSessionsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -182,7 +182,6 @@ export function createChildStoreManager(input: {
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
bootstrapPromise: Promise.resolve(),
|
||||
})
|
||||
children[directory] = child
|
||||
disposers.set(directory, dispose)
|
||||
|
||||
@@ -72,7 +72,6 @@ export type State = {
|
||||
part: {
|
||||
[messageID: string]: Part[]
|
||||
}
|
||||
bootstrapPromise: Promise<void>
|
||||
}
|
||||
|
||||
export type VcsCache = {
|
||||
|
||||
@@ -582,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
open(directory: string) {
|
||||
const root = rootFor(directory)
|
||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||
void globalSync.project.loadSessions(root)
|
||||
globalSync.project.loadSessions(root)
|
||||
server.projects.open(root)
|
||||
},
|
||||
close(directory: string) {
|
||||
|
||||
@@ -117,7 +117,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
||||
entry?.value.clear()
|
||||
}
|
||||
|
||||
void removePersisted(Persist.workspace(dir, "terminal"), platform)
|
||||
removePersisted(Persist.workspace(dir, "terminal"), platform)
|
||||
|
||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
||||
for (const id of sessionIDs ?? []) {
|
||||
@@ -126,7 +126,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
||||
}
|
||||
}
|
||||
for (const key of legacy) {
|
||||
void removePersisted({ key }, platform)
|
||||
removePersisted({ key }, platform)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
packages/app/src/env.d.ts
vendored
1
packages/app/src/env.d.ts
vendored
@@ -3,7 +3,6 @@ import "solid-js"
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_OPENCODE_SERVER_HOST: string
|
||||
readonly VITE_OPENCODE_SERVER_PORT: string
|
||||
readonly OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { dict as en } from "./en"
|
||||
|
||||
export const dict = {
|
||||
"command.category.suggested": "추천",
|
||||
"command.category.view": "보기",
|
||||
|
||||
@@ -132,11 +132,9 @@ export default function Layout(props: ParentProps) {
|
||||
if (!slug) return { slug, dir: "" }
|
||||
const dir = decode64(slug)
|
||||
if (!dir) return { slug, dir: "" }
|
||||
const store = globalSync.peek(dir, { bootstrap: false })
|
||||
return {
|
||||
slug,
|
||||
store,
|
||||
dir: store[0].path.directory || dir,
|
||||
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
|
||||
}
|
||||
})
|
||||
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
||||
@@ -958,7 +956,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
// warm up child store to prevent flicker
|
||||
globalSync.child(target.worktree)
|
||||
void openProject(target.worktree)
|
||||
openProject(target.worktree)
|
||||
}
|
||||
|
||||
function navigateSessionByUnseen(offset: number) {
|
||||
@@ -1096,7 +1094,7 @@ export default function Layout(props: ParentProps) {
|
||||
disabled: !params.dir || !params.id,
|
||||
onSelect: () => {
|
||||
const session = currentSessions().find((s) => s.id === params.id)
|
||||
if (session) void archiveSession(session)
|
||||
if (session) archiveSession(session)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1362,11 +1360,11 @@ export default function Layout(props: ParentProps) {
|
||||
if (!server.isLocal()) return
|
||||
|
||||
for (const directory of collectOpenProjectDeepLinks(urls)) {
|
||||
void openProject(directory)
|
||||
openProject(directory)
|
||||
}
|
||||
|
||||
for (const link of collectNewSessionDeepLinks(urls)) {
|
||||
void openProject(link.directory, false)
|
||||
openProject(link.directory, false)
|
||||
const slug = base64Encode(link.directory)
|
||||
if (link.prompt) {
|
||||
setSessionHandoff(slug, { prompt: link.prompt })
|
||||
@@ -1455,11 +1453,11 @@ export default function Layout(props: ParentProps) {
|
||||
function resolve(result: string | string[] | null) {
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
void openProject(directory, false)
|
||||
openProject(directory, false)
|
||||
}
|
||||
void navigateToProject(result[0])
|
||||
navigateToProject(result[0])
|
||||
} else if (result) {
|
||||
void openProject(result)
|
||||
openProject(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1827,7 +1825,7 @@ export default function Layout(props: ParentProps) {
|
||||
const next = new Set(dirs)
|
||||
for (const directory of next) {
|
||||
if (loadedSessionDirs.has(directory)) continue
|
||||
void globalSync.project.loadSessions(directory)
|
||||
globalSync.project.loadSessions(directory)
|
||||
}
|
||||
|
||||
loadedSessionDirs.clear()
|
||||
@@ -2112,7 +2110,7 @@ export default function Layout(props: ParentProps) {
|
||||
onSave={(next) => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
void renameProject(item, next)
|
||||
renameProject(item, next)
|
||||
}}
|
||||
class="text-14-medium text-text-strong truncate"
|
||||
displayClass="text-14-medium text-text-strong truncate"
|
||||
@@ -2244,7 +2242,7 @@ export default function Layout(props: ParentProps) {
|
||||
onClick={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
void createWorkspace(item)
|
||||
createWorkspace(item)
|
||||
}}
|
||||
>
|
||||
{language.t("workspace.new")}
|
||||
@@ -2355,14 +2353,8 @@ export default function Layout(props: ParentProps) {
|
||||
/>
|
||||
)
|
||||
|
||||
const [loading] = createResource(
|
||||
() => route()?.store?.[0]?.bootstrapPromise,
|
||||
(p) => p,
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
{(autoselecting(), loading()) ?? ""}
|
||||
<Titlebar />
|
||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
|
||||
@@ -14,11 +14,10 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { type LocalProject } from "@/context/layout"
|
||||
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
|
||||
type InlineEditorComponent = (props: {
|
||||
id: string
|
||||
@@ -278,7 +277,7 @@ const WorkspaceSessionList = (props: {
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
||||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
void props.loadMore()
|
||||
props.loadMore()
|
||||
;(e.currentTarget as HTMLButtonElement).blur()
|
||||
}}
|
||||
>
|
||||
@@ -455,8 +454,7 @@ export const LocalWorkspace = (props: {
|
||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const loading = createMemo(() => query.isPending && count() === 0)
|
||||
const loading = createMemo(() => !booted() && count() === 0)
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||
const loadMore = async () => {
|
||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
@@ -473,7 +471,7 @@ export const LocalWorkspace = (props: {
|
||||
mobile={props.mobile}
|
||||
ctx={props.ctx}
|
||||
showNew={() => false}
|
||||
loading={() => query.isLoading}
|
||||
loading={loading}
|
||||
sessions={sessions}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
on,
|
||||
onMount,
|
||||
untrack,
|
||||
createResource,
|
||||
} from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
@@ -433,6 +432,7 @@ export default function Page() {
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const isChildSession = createMemo(() => !!info()?.parentID)
|
||||
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||
const canReview = createMemo(() => !!sync.project)
|
||||
const reviewTab = createMemo(() => isDesktop())
|
||||
const tabState = createSessionTabs({
|
||||
@@ -484,7 +484,7 @@ export default function Page() {
|
||||
if (!tab) return
|
||||
|
||||
const path = file.pathFromTab(tab)
|
||||
if (path) void file.load(path)
|
||||
if (path) file.load(path)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
@@ -805,9 +805,8 @@ export default function Page() {
|
||||
|
||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||
|
||||
const [sessionSync] = createResource(
|
||||
() => [sdk.directory, params.id] as const,
|
||||
([directory, id]) => {
|
||||
createEffect(
|
||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
||||
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
||||
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
||||
refreshFrame = undefined
|
||||
@@ -818,10 +817,13 @@ export default function Page() {
|
||||
const stale = !cached
|
||||
? false
|
||||
: (() => {
|
||||
const info = getSessionPrefetch(directory, id)
|
||||
const info = getSessionPrefetch(sdk.directory, id)
|
||||
if (!info) return true
|
||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||
})()
|
||||
untrack(() => {
|
||||
void sync.session.sync(id)
|
||||
})
|
||||
|
||||
refreshFrame = requestAnimationFrame(() => {
|
||||
refreshFrame = undefined
|
||||
@@ -833,9 +835,7 @@ export default function Page() {
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
|
||||
return sync.session.sync(id)
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
@@ -1882,7 +1882,6 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
{sessionSync() ?? ""}
|
||||
<SessionHeader />
|
||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||
<Show when={!isDesktop() && !!params.id}>
|
||||
|
||||
@@ -117,7 +117,7 @@ export const createOpenReviewFile = (input: {
|
||||
input.openTab(tab)
|
||||
input.setActive(tab)
|
||||
}
|
||||
if (maybePromise instanceof Promise) void maybePromise.then(open)
|
||||
if (maybePromise instanceof Promise) maybePromise.then(open)
|
||||
else open()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,9 +46,7 @@ describe("runtime adapters", () => {
|
||||
})
|
||||
|
||||
test("resolves speech recognition constructor with webkit precedence", () => {
|
||||
// oxlint-disable-next-line no-extraneous-class
|
||||
class SpeechCtor {}
|
||||
// oxlint-disable-next-line no-extraneous-class
|
||||
class WebkitCtor {}
|
||||
const ctor = getSpeechRecognitionCtor({
|
||||
SpeechRecognition: SpeechCtor,
|
||||
|
||||
@@ -16,10 +16,7 @@ export function createSdkForServer({
|
||||
|
||||
return createOpencodeClient({
|
||||
...config,
|
||||
headers: {
|
||||
...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers),
|
||||
...auth,
|
||||
},
|
||||
headers: { ...config.headers, ...auth },
|
||||
baseUrl: server.url,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,4 +105,4 @@ async function main() {
|
||||
console.log(`✓ Sitemap generated at ${outputPath}`)
|
||||
}
|
||||
|
||||
void main()
|
||||
main()
|
||||
|
||||
@@ -766,7 +766,7 @@ export default function Spotlight(props: SpotlightProps) {
|
||||
}
|
||||
}
|
||||
|
||||
void initializeWebGPU()
|
||||
initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
|
||||
@@ -298,7 +298,7 @@ export default function BlackSubscribe() {
|
||||
|
||||
// Resolve stripe promise once
|
||||
createEffect(() => {
|
||||
void stripePromise.then((s) => {
|
||||
stripePromise.then((s) => {
|
||||
if (s) setStripe(s)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { APIEvent } from "@solidjs/start"
|
||||
import type { DownloadPlatform } from "../types"
|
||||
|
||||
const prodAssetNames: Record<string, string> = {
|
||||
const assetNames: Record<string, string> = {
|
||||
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
|
||||
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
||||
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
||||
@@ -10,15 +10,6 @@ const prodAssetNames: Record<string, string> = {
|
||||
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
||||
} satisfies Record<DownloadPlatform, string>
|
||||
|
||||
const betaAssetNames: Record<string, string> = {
|
||||
"darwin-aarch64-dmg": "opencode-electron-mac-arm64.dmg",
|
||||
"darwin-x64-dmg": "opencode-electron-mac-x64.dmg",
|
||||
"windows-x64-nsis": "opencode-electron-win-x64.exe",
|
||||
"linux-x64-deb": "opencode-electron-linux-amd64.deb",
|
||||
"linux-x64-appimage": "opencode-electron-linux-x86_64.AppImage",
|
||||
"linux-x64-rpm": "opencode-electron-linux-x86_64.rpm",
|
||||
} satisfies Record<DownloadPlatform, string>
|
||||
|
||||
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
|
||||
const downloadNames: Record<string, string> = {
|
||||
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
|
||||
@@ -27,7 +18,7 @@ const downloadNames: Record<string, string> = {
|
||||
} satisfies { [K in DownloadPlatform]?: string }
|
||||
|
||||
export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||
const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform]
|
||||
const assetName = assetNames[platform]
|
||||
if (!assetName) return new Response(null, { status: 404 })
|
||||
|
||||
const resp = await fetch(
|
||||
@@ -46,5 +37,5 @@ export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||
const headers = new Headers(resp.headers)
|
||||
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
|
||||
|
||||
return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers })
|
||||
return new Response(resp.body, { ...resp, headers })
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function Download() {
|
||||
|
||||
const handleCopyClick = (command: string) => (event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
void navigator.clipboard.writeText(command)
|
||||
navigator.clipboard.writeText(command)
|
||||
button.setAttribute("data-copied", "")
|
||||
setTimeout(() => {
|
||||
button.removeAttribute("data-copied")
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Header } from "~/component/header"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { github } from "~/lib/github"
|
||||
import { createMemo } from "solid-js"
|
||||
import { config } from "~/config"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
@@ -29,12 +30,12 @@ function CopyStatus() {
|
||||
export default function Home() {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const _githubData = createAsync(() => github())
|
||||
const githubData = createAsync(() => github())
|
||||
const handleCopyClick = (event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
const text = button.textContent
|
||||
if (text) {
|
||||
void navigator.clipboard.writeText(text)
|
||||
navigator.clipboard.writeText(text)
|
||||
button.setAttribute("data-copied", "")
|
||||
setTimeout(() => {
|
||||
button.removeAttribute("data-copied")
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Home() {
|
||||
const callback = () => {
|
||||
const text = button.textContent
|
||||
if (text) {
|
||||
void navigator.clipboard.writeText(text)
|
||||
navigator.clipboard.writeText(text)
|
||||
button.setAttribute("data-copied", "")
|
||||
setTimeout(() => {
|
||||
button.removeAttribute("data-copied")
|
||||
|
||||
@@ -116,9 +116,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
|
||||
const setUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
|
||||
@@ -10,11 +10,11 @@ import { formError, localizeError } from "~/lib/form-error"
|
||||
|
||||
const setMonthlyLimit = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const limit = form.get("limit") as string | null
|
||||
const limit = form.get("limit")?.toString()
|
||||
if (!limit) return { error: formError.limitRequired }
|
||||
const numericLimit = parseInt(limit)
|
||||
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { formError, formErrorReloadAmountMin, formErrorReloadTriggerMin, localiz
|
||||
|
||||
const reload = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(await withActor(() => Billing.reload(), workspaceID), {
|
||||
revalidate: queryBillingInfo.key,
|
||||
@@ -21,11 +21,11 @@ const reload = action(async (form: FormData) => {
|
||||
|
||||
const setReload = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const reloadValue = (form.get("reload") as string | null) === "true"
|
||||
const amountStr = form.get("reloadAmount") as string | null
|
||||
const triggerStr = form.get("reloadTrigger") as string | null
|
||||
const reloadValue = form.get("reload")?.toString() === "true"
|
||||
const amountStr = form.get("reloadAmount")?.toString()
|
||||
const triggerStr = form.get("reloadTrigger")?.toString()
|
||||
|
||||
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
|
||||
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
|
||||
@@ -90,9 +90,9 @@ export function ReloadSection() {
|
||||
}
|
||||
const info = billingInfo()!
|
||||
setStore("show", true)
|
||||
setStore("reload", true)
|
||||
setStore("reloadAmount", String(info.reloadAmount))
|
||||
setStore("reloadTrigger", String(info.reloadTrigger))
|
||||
setStore("reload", info.reload ? true : true)
|
||||
setStore("reloadAmount", info.reloadAmount.toString())
|
||||
setStore("reloadTrigger", info.reloadTrigger.toString())
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -152,11 +152,11 @@ export function ReloadSection() {
|
||||
data-component="input"
|
||||
name="reloadAmount"
|
||||
type="number"
|
||||
min={String(billingInfo()?.reloadAmountMin ?? "")}
|
||||
min={billingInfo()?.reloadAmountMin.toString()}
|
||||
step="1"
|
||||
value={store.reloadAmount}
|
||||
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
|
||||
placeholder={String(billingInfo()?.reloadAmount ?? "")}
|
||||
placeholder={billingInfo()?.reloadAmount.toString()}
|
||||
disabled={!store.reload}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,11 +166,11 @@ export function ReloadSection() {
|
||||
data-component="input"
|
||||
name="reloadTrigger"
|
||||
type="number"
|
||||
min={String(billingInfo()?.reloadTriggerMin ?? "")}
|
||||
min={billingInfo()?.reloadTriggerMin.toString()}
|
||||
step="1"
|
||||
value={store.reloadTrigger}
|
||||
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
|
||||
placeholder={String(billingInfo()?.reloadTrigger ?? "")}
|
||||
placeholder={billingInfo()?.reloadTrigger.toString()}
|
||||
disabled={!store.reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -120,9 +120,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
|
||||
const setLiteUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
|
||||
@@ -12,18 +12,18 @@ import { formError, localizeError } from "~/lib/form-error"
|
||||
|
||||
const removeKey = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const id = form.get("id") as string | null
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: formError.idRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
|
||||
}, "key.remove")
|
||||
|
||||
const createKey = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const name = (form.get("name") as string | null)?.trim()
|
||||
const name = form.get("name")?.toString().trim()
|
||||
if (!name) return { error: formError.nameRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
|
||||
@@ -24,13 +24,13 @@ const listMembers = query(async (workspaceID: string) => {
|
||||
|
||||
const inviteMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const email = (form.get("email") as string | null)?.trim()
|
||||
const email = form.get("email")?.toString().trim()
|
||||
if (!email) return { error: formError.emailRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: formError.roleRequired }
|
||||
const limit = form.get("limit") as string | null
|
||||
const limit = form.get("limit")?.toString()
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||
return json(
|
||||
@@ -47,9 +47,9 @@ const inviteMember = action(async (form: FormData) => {
|
||||
|
||||
const removeMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const id = form.get("id") as string | null
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: formError.idRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
@@ -66,13 +66,13 @@ const removeMember = action(async (form: FormData) => {
|
||||
const updateMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
|
||||
const id = form.get("id") as string | null
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: formError.idRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: formError.roleRequired }
|
||||
const limit = form.get("limit") as string | null
|
||||
const limit = form.get("limit")?.toString()
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||
|
||||
@@ -118,7 +118,7 @@ function MemberRow(props: {
|
||||
}
|
||||
setStore("editing", true)
|
||||
setStore("selectedRole", props.member.role)
|
||||
setStore("limit", props.member.monthlyLimit != null ? String(props.member.monthlyLimit) : "")
|
||||
setStore("limit", props.member.monthlyLimit?.toString() ?? "")
|
||||
}
|
||||
|
||||
function hide() {
|
||||
|
||||
@@ -67,11 +67,11 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
||||
|
||||
const updateModel = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const model = form.get("model") as string | null
|
||||
const model = form.get("model")?.toString()
|
||||
if (!model) return { error: formError.modelRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const enabled = (form.get("enabled") as string | null) === "true"
|
||||
const enabled = form.get("enabled")?.toString() === "true"
|
||||
return json(
|
||||
withActor(async () => {
|
||||
if (enabled) {
|
||||
@@ -163,7 +163,7 @@ export function ModelSection() {
|
||||
<form action={updateModel} method="post">
|
||||
<input type="hidden" name="model" value={id} />
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<input type="hidden" name="enabled" value={String(isEnabled())} />
|
||||
<input type="hidden" name="enabled" value={isEnabled().toString()} />
|
||||
<label data-slot="model-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -21,9 +21,9 @@ function maskCredentials(credentials: string) {
|
||||
|
||||
const removeProvider = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const provider = form.get("provider") as string | null
|
||||
const provider = form.get("provider")?.toString()
|
||||
if (!provider) return { error: formError.providerRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
|
||||
revalidate: listProviders.key,
|
||||
@@ -32,11 +32,11 @@ const removeProvider = action(async (form: FormData) => {
|
||||
|
||||
const saveProvider = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const provider = form.get("provider") as string | null
|
||||
const credentials = form.get("credentials") as string | null
|
||||
const provider = form.get("provider")?.toString()
|
||||
const credentials = form.get("credentials")?.toString()
|
||||
if (!provider) return { error: formError.providerRequired }
|
||||
if (!credentials) return { error: formError.apiKeyRequired }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
@@ -59,13 +59,10 @@ function ProviderRow(props: { provider: Provider }) {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const providers = createAsync(() => listProviders(params.id!))
|
||||
const saveSubmission = useSubmission(
|
||||
saveProvider,
|
||||
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
|
||||
)
|
||||
const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
|
||||
const removeSubmission = useSubmission(
|
||||
removeProvider,
|
||||
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
|
||||
([fd]) => fd.get("provider")?.toString() === props.provider.key,
|
||||
)
|
||||
const [store, setStore] = createStore({ editing: false })
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ const getWorkspaceInfo = query(async (workspaceID: string) => {
|
||||
|
||||
const updateWorkspace = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const name = (form.get("name") as string | null)?.trim()
|
||||
const name = form.get("name")?.toString().trim()
|
||||
if (!name) return { error: formError.workspaceNameRequired }
|
||||
if (name.length > 255) return { error: formError.nameTooLong }
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
|
||||
@@ -26,14 +26,14 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
|
||||
const minute = timestamp.substring(10, 12)
|
||||
const second = timestamp.substring(12, 14)
|
||||
|
||||
void waitUntil(
|
||||
waitUntil(
|
||||
Resource.ZenDataNew.put(
|
||||
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...data }),
|
||||
),
|
||||
)
|
||||
|
||||
void waitUntil(
|
||||
waitUntil(
|
||||
Resource.ZenDataNew.put(
|
||||
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...metadata }),
|
||||
|
||||
@@ -60,9 +60,6 @@ export default defineConfig({
|
||||
plugins: [appPlugin],
|
||||
publicDir: "../../../app/public",
|
||||
root: "src/renderer",
|
||||
define: {
|
||||
"import.meta.env.VITE_OPENCODE_CHANNEL": JSON.stringify(channel),
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
||||
@@ -28,7 +28,7 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string
|
||||
const output = execFileSync("wsl", ["-e", "wslpath", flag, path])
|
||||
return output.toString().trim()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to run wslpath: ${String(error)}`, { cause: error })
|
||||
throw new Error(`Failed to run wslpath: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
if (location.pathname === "/loading") {
|
||||
void import("./loading")
|
||||
import("./loading")
|
||||
} else {
|
||||
void import("./")
|
||||
import("./")
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ const createPlatform = (): Platform => {
|
||||
}
|
||||
|
||||
let menuTrigger = null as null | ((id: string) => void)
|
||||
void createMenu((id) => {
|
||||
createMenu((id) => {
|
||||
menuTrigger?.(id)
|
||||
})
|
||||
void listenForDeepLinks()
|
||||
|
||||
@@ -48,7 +48,7 @@ render(() => {
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
void listener.then((cb) => cb())
|
||||
listener.then((cb) => cb())
|
||||
timers.forEach(clearTimeout)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -186,5 +186,5 @@ export async function createMenu(trigger: (id: string) => void) {
|
||||
}),
|
||||
],
|
||||
})
|
||||
void menu.setAsAppMenu()
|
||||
menu.setAsAppMenu()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_Z
|
||||
|
||||
const applyZoom = (next: number) => {
|
||||
setWebviewZoom(next)
|
||||
void invoke("plugin:webview|set_webview_zoom", {
|
||||
invoke("plugin:webview|set_webview_zoom", {
|
||||
value: next,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,4 +37,4 @@ async function test() {
|
||||
await Share.remove({ id: shareInfo.id, secret: shareInfo.secret })
|
||||
}
|
||||
|
||||
void test()
|
||||
test()
|
||||
|
||||
@@ -13,7 +13,6 @@ type Env = {
|
||||
}
|
||||
|
||||
export class SyncServer extends DurableObject<Env> {
|
||||
// oxlint-disable-next-line no-useless-constructor
|
||||
constructor(ctx: DurableObjectState, env: Env) {
|
||||
super(ctx, env)
|
||||
}
|
||||
|
||||
3
packages/opencode/.gitignore
vendored
3
packages/opencode/.gitignore
vendored
@@ -1,9 +1,6 @@
|
||||
research
|
||||
dist
|
||||
dist-*
|
||||
gen
|
||||
app.log
|
||||
src/provider/models-snapshot.js
|
||||
src/provider/models-snapshot.d.ts
|
||||
script/build-*.ts
|
||||
temporary-*.md
|
||||
|
||||
31
packages/opencode/.opencode/package-lock.json
generated
Normal file
31
packages/opencode/.opencode/package-lock.json
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": ".opencode",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.2.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "1.2.6",
|
||||
"zod": "4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.2.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,12 +39,6 @@ See `specs/effect/migration.md` for the compact pattern reference and examples.
|
||||
- Do the work directly in the `InstanceState.make` closure — `ScopedCache` handles run-once semantics. Don't add fibers, `ensure()` callbacks, or `started` flags on top.
|
||||
- Use `Effect.addFinalizer` or `Effect.acquireRelease` inside the `InstanceState.make` closure for cleanup (subscriptions, process teardown, etc.).
|
||||
- Use `Effect.forkScoped` inside the closure for background stream consumers — the fiber is interrupted when the instance is disposed.
|
||||
- To make a service's `init()` non-blocking, fork `InstanceState.get(state)` at the `init()` call site (e.g. `Effect.forkIn(scope)`), not by forking work inside the `InstanceState.make` closure. Forking inside the closure leaves state incomplete for other methods that read it.
|
||||
- `src/project/bootstrap.ts` already wraps every service `init()` in `Effect.forkDetach`, so `init()` is fire-and-forget in production. Keep `init()` methods synchronous internally; the caller controls concurrency.
|
||||
|
||||
## Effect v4 beta API
|
||||
|
||||
- `Effect.fork` and `Effect.forkDaemon` do not exist. Use `Effect.forkIn(scope)` to fork a fiber into a specific scope.
|
||||
|
||||
## Preferred Effect services
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||
"dev:temporary": "bun run --conditions=browser ./src/temporary.ts",
|
||||
"db": "bun drizzle-kit"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -64,10 +64,27 @@ function findBinary() {
|
||||
|
||||
return { binaryPath, binaryName }
|
||||
} catch (error) {
|
||||
throw new Error(`Could not find package ${packageName}: ${error.message}`, { cause: error })
|
||||
throw new Error(`Could not find package ${packageName}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function prepareBinDirectory(binaryName) {
|
||||
const binDir = path.join(__dirname, "bin")
|
||||
const targetPath = path.join(binDir, binaryName)
|
||||
|
||||
// Ensure bin directory exists
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Remove existing binary/symlink if it exists
|
||||
if (fs.existsSync(targetPath)) {
|
||||
fs.unlinkSync(targetPath)
|
||||
}
|
||||
|
||||
return { binDir, targetPath }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
if (os.platform() === "win32") {
|
||||
@@ -95,7 +112,7 @@ async function main() {
|
||||
}
|
||||
|
||||
try {
|
||||
void main()
|
||||
main()
|
||||
} catch (error) {
|
||||
console.error("Postinstall script error:", error.message)
|
||||
process.exit(0)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { z } from "zod"
|
||||
import { Config } from "../src/config"
|
||||
import { TuiConfig } from "../src/cli/cmd/tui/config/tui"
|
||||
import { TuiConfig } from "../src/config/tui"
|
||||
|
||||
function generate(schema: z.ZodType) {
|
||||
const result = z.toJSONSchema(schema, {
|
||||
@@ -33,7 +33,7 @@ function generate(schema: z.ZodType) {
|
||||
schema.examples = [schema.default]
|
||||
}
|
||||
|
||||
schema.description = [schema.description || "", `default: \`${String(schema.default)}\``]
|
||||
schema.description = [schema.description || "", `default: \`${schema.default}\``]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
.trim()
|
||||
|
||||
@@ -121,46 +121,17 @@ Why `question` first:
|
||||
|
||||
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
|
||||
|
||||
### 4. Bridge into Hono behind a feature flag
|
||||
### 4. Build in parallel, do not bridge into Hono
|
||||
|
||||
The `HttpApi` routes are bridged into the Hono server via `HttpRouter.toWebHandler` with a shared `memoMap`. This means:
|
||||
The `HttpApi` implementation lives under `src/server/instance/httpapi/` as a standalone Effect HTTP server. It is **not mounted into the Hono app**. There is no `toWebHandler` bridge, no Hono `Handler` export, and no `.route()` call wiring it into `experimental.ts`.
|
||||
|
||||
- one process, one port — no separate server
|
||||
- the Effect handler shares layer instances with `AppRuntime` (same `Question.Service`, etc.)
|
||||
- Effect middleware handles auth and instance lookup independently from Hono middleware
|
||||
- Hono's `.all()` catch-all intercepts matching paths before the Hono route handlers
|
||||
The standalone server (`httpapi/server.ts`) can be started independently and proves the routes work. Tests exercise it via `HttpRouter.serve` with `NodeHttpServer.layerTest`.
|
||||
|
||||
The bridge is gated behind `OPENCODE_EXPERIMENTAL_HTTPAPI` (or `OPENCODE_EXPERIMENTAL`). When the flag is off (default), all requests go through the original Hono handlers unchanged.
|
||||
The goal is to build enough route coverage in the Effect server that the Hono server can eventually be replaced entirely. Until then, the two implementations exist side by side but are completely separate processes.
|
||||
|
||||
```ts
|
||||
// in instance/index.ts
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw))
|
||||
}
|
||||
```
|
||||
### 5. Migrate JSON route groups gradually
|
||||
|
||||
The Hono route handlers are always registered (after the bridge) so `hono-openapi` generates the OpenAPI spec entries that feed SDK codegen. When the flag is on, these handlers are dead code — the `.all()` bridge matches first.
|
||||
|
||||
### 5. Observability
|
||||
|
||||
The `webHandler` provides `Observability.layer` via `Layer.provideMerge`. Since the `memoMap` is shared with `AppRuntime`, the tracing provider is deduplicated — no extra initialization cost.
|
||||
|
||||
This gives:
|
||||
|
||||
- **spans**: `Effect.fn("QuestionHttpApi.list")` etc. appear in traces alongside service-layer spans
|
||||
- **HTTP logs**: `HttpMiddleware.logger` emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` annotations, flowing to motel via `OtlpLogger`
|
||||
|
||||
### 6. Migrate JSON route groups gradually
|
||||
|
||||
As each route group is ported to `HttpApi`:
|
||||
|
||||
1. change its `root` path from `/experimental/httpapi/<group>` to `/<group>`
|
||||
2. add `.all("/<group>", handler)` / `.all("/<group>/*", handler)` to the flag block in `instance/index.ts`
|
||||
3. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
|
||||
4. verify SDK output is unchanged
|
||||
|
||||
Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
||||
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
||||
|
||||
## Schema rule for HttpApi work
|
||||
|
||||
@@ -331,43 +302,36 @@ The first slice is successful if:
|
||||
- OpenAPI is generated from the `HttpApi` contract
|
||||
- the tests are straightforward enough that the next slice feels mechanical
|
||||
|
||||
## Learnings
|
||||
## Learnings from the question slice
|
||||
|
||||
### Schema
|
||||
The first parallel `question` spike gave us a concrete pattern to reuse.
|
||||
|
||||
- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`.
|
||||
- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes.
|
||||
- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects.
|
||||
- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes.
|
||||
- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes.
|
||||
|
||||
### Integration
|
||||
|
||||
- `HttpRouter.toWebHandler` with the shared `memoMap` from `run-service.ts` cleanly bridges Effect routes into Hono — one process, one port, shared layer instances.
|
||||
- `Observability.layer` must be explicitly provided via `Layer.provideMerge` in the routes layer for OTEL spans and HTTP logs to flow. The `memoMap` deduplicates it with `AppRuntime` — no extra cost.
|
||||
- `HttpMiddleware.logger` (enabled by default when `disableLogger` is not set) emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` — these flow through `OtlpLogger` to motel.
|
||||
- Hono OpenAPI stubs must remain registered for SDK codegen until the SDK pipeline reads from the Effect OpenAPI spec instead.
|
||||
- the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag gates the bridge at the Hono router level — default off, no behavior change unless opted in.
|
||||
- the experimental slice should stay as a standalone Effect server and keep calling the existing service layer unchanged.
|
||||
- compare generated OpenAPI semantically at the route and schema level.
|
||||
|
||||
## Route inventory
|
||||
|
||||
Status legend:
|
||||
|
||||
- `bridged` - Effect HttpApi slice exists and is bridged into Hono behind the flag
|
||||
- `done` - Effect HttpApi slice exists but not yet bridged
|
||||
- `done` - parallel `HttpApi` slice exists
|
||||
- `next` - good near-term candidate
|
||||
- `later` - possible, but not first wave
|
||||
- `defer` - not a good early `HttpApi` target
|
||||
|
||||
Current instance route inventory:
|
||||
|
||||
- `question` - `bridged`
|
||||
endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject`
|
||||
- `permission` - `bridged`
|
||||
endpoints: `GET /permission`, `POST /permission/:requestID/reply`
|
||||
- `provider` - `bridged` (partial)
|
||||
bridged endpoint: `GET /provider/auth`
|
||||
not yet ported: `GET /provider`, OAuth mutations
|
||||
- `question` - `done`
|
||||
endpoints in slice: `GET /question`, `POST /question/:requestID/reply`
|
||||
- `permission` - `done`
|
||||
endpoints in slice: `GET /permission`, `POST /permission/:requestID/reply`
|
||||
- `provider` - `next`
|
||||
best next endpoint: `GET /provider/auth`
|
||||
later endpoint: `GET /provider`
|
||||
defer first-wave OAuth mutations
|
||||
- `config` - `next`
|
||||
best next endpoint: `GET /config/providers`
|
||||
later endpoint: `GET /config`
|
||||
@@ -407,13 +371,7 @@ Recommended near-term sequence after the first spike:
|
||||
- [x] keep the underlying service calls identical to the current handlers
|
||||
- [x] compare generated OpenAPI against the current Hono/OpenAPI setup
|
||||
- [x] document how auth, instance lookup, and error mapping would compose in the new stack
|
||||
- [x] bridge Effect routes into Hono via `toWebHandler` with shared `memoMap`
|
||||
- [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag
|
||||
- [x] verify OTEL spans and HTTP logs flow to motel
|
||||
- [x] bridge question, permission, and provider auth routes
|
||||
- [ ] port remaining provider endpoints (`GET /provider`, OAuth mutations)
|
||||
- [ ] port `config` read endpoints
|
||||
- [ ] decide when to remove the flag and make Effect routes the default
|
||||
- [ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default
|
||||
|
||||
## Rule of thumb
|
||||
|
||||
|
||||
@@ -442,58 +442,3 @@ Going forward:
|
||||
- If a file grows large enough that it's hard to navigate, split by concern
|
||||
(errors.ts, schema.ts, etc.) for readability. Not for tree-shaking — the
|
||||
bundler handles that.
|
||||
|
||||
## Circular import rules
|
||||
|
||||
Barrel files (`index.ts` with `export * as`) introduce circular import risks.
|
||||
These cause `ReferenceError: Cannot access 'X' before initialization` at
|
||||
runtime — not caught by the type checker.
|
||||
|
||||
### Rule 1: Sibling files never import through their own barrel
|
||||
|
||||
Files in the same directory must import directly from the source file, never
|
||||
through `"."` or `"@/<own-dir>"`:
|
||||
|
||||
```ts
|
||||
// BAD — circular: index.ts re-exports both files, so A → index → B → index → A
|
||||
import { Sibling } from "."
|
||||
|
||||
// GOOD — direct, no cycle
|
||||
import * as Sibling from "./sibling"
|
||||
```
|
||||
|
||||
### Rule 2: Cross-directory imports must not form cycles through barrels
|
||||
|
||||
If `src/lsp/lsp.ts` imports `Config` from `"../config"`, and
|
||||
`src/config/config.ts` imports `LSPServer` from `"../lsp"`, that's a cycle:
|
||||
|
||||
```
|
||||
lsp/lsp.ts → config/index.ts → config/config.ts → lsp/index.ts → lsp/lsp.ts 💥
|
||||
```
|
||||
|
||||
Fix by importing the specific file, breaking the cycle:
|
||||
|
||||
```ts
|
||||
// In config/config.ts — import directly, not through the lsp barrel
|
||||
import * as LSPServer from "../lsp/server"
|
||||
```
|
||||
|
||||
### Why the type checker doesn't catch this
|
||||
|
||||
TypeScript resolves types lazily — it doesn't evaluate module-scope
|
||||
expressions. The `ReferenceError` only happens at runtime when a module-scope
|
||||
`const` or function call accesses a value from a circular dependency that
|
||||
hasn't finished initializing. The SDK build step (`bun run --conditions=browser
|
||||
./src/index.ts generate`) is the reliable way to catch these because it
|
||||
evaluates all modules eagerly.
|
||||
|
||||
### How to verify
|
||||
|
||||
After any namespace conversion, run:
|
||||
|
||||
```bash
|
||||
cd packages/opencode
|
||||
bun run --conditions=browser ./src/index.ts generate
|
||||
```
|
||||
|
||||
If this completes without `ReferenceError`, the module graph is safe.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer, Option, Schema, Context } from "effect"
|
||||
|
||||
import { Database } from "@/storage"
|
||||
import { Database } from "@/storage/db"
|
||||
import { AccountStateTable, AccountTable } from "./account.sql"
|
||||
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
|
||||
import { normalizeServerUrl } from "./url"
|
||||
|
||||
@@ -31,9 +31,9 @@ import {
|
||||
type Usage,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
import { Log } from "../util"
|
||||
import { Log } from "../util/log"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Filesystem } from "../util"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Hash } from "@opencode-ai/shared/util/hash"
|
||||
import { ACPSessionManager } from "./session"
|
||||
import type { ACPConfig } from "./types"
|
||||
@@ -49,7 +49,6 @@ import { z } from "zod"
|
||||
import { LoadAPIKeyError } from "ai"
|
||||
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { applyPatch } from "diff"
|
||||
import { InstallationVersion } from "@/installation/version"
|
||||
|
||||
type ModeOption = { id: string; name: string; description?: string }
|
||||
type ModelOption = { modelId: string; name: string }
|
||||
@@ -243,7 +242,7 @@ export namespace ACP {
|
||||
const newContent = getNewContent(content, diff)
|
||||
|
||||
if (newContent) {
|
||||
void this.connection.writeTextFile({
|
||||
this.connection.writeTextFile({
|
||||
sessionId: session.id,
|
||||
path: filepath,
|
||||
content: newContent,
|
||||
@@ -571,7 +570,7 @@ export namespace ACP {
|
||||
authMethods: [authMethod],
|
||||
agentInfo: {
|
||||
name: "OpenCode",
|
||||
version: InstallationVersion,
|
||||
version: Installation.VERSION,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1254,7 +1253,7 @@ export namespace ACP {
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
void this.connection.sessionUpdate({
|
||||
this.connection.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "available_commands_update",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { ACPSessionState } from "./types"
|
||||
import { Log } from "@/util"
|
||||
import { Log } from "@/util/log"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
|
||||
const log = Log.create({ service: "acp-session-manager" })
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Provider } from "../provider"
|
||||
import { ModelID, ProviderID } from "../provider/schema"
|
||||
import { generateObject, streamObject, type ModelMessage } from "ai"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Truncate } from "../tool"
|
||||
import { Truncate } from "../tool/truncate"
|
||||
import { Auth } from "../auth"
|
||||
import { ProviderTransform } from "../provider"
|
||||
import { ProviderTransform } from "../provider/transform"
|
||||
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
@@ -20,7 +20,7 @@ import path from "path"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Skill } from "../skill"
|
||||
import { Effect, Context, Layer } from "effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export namespace BusEvent {
|
||||
properties: def.properties,
|
||||
})
|
||||
.meta({
|
||||
ref: `Event.${def.type}`,
|
||||
ref: "Event" + "." + def.type,
|
||||
})
|
||||
})
|
||||
.toArray()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import z from "zod"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import { Log } from "../util"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { Log } from "../util/log"
|
||||
import { BusEvent } from "./bus-event"
|
||||
import { GlobalBus } from "./global"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
const log = Log.create({ service: "bus" })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Log } from "@/util"
|
||||
import { Log } from "@/util/log"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { cmd } from "./cmd"
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Agent } from "../../agent/agent"
|
||||
import { Provider } from "../../provider"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import matter from "gray-matter"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { EOL } from "os"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { spawn } from "child_process"
|
||||
import { Database } from "../../storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
import { Database as BunDatabase } from "bun:sqlite"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { JsonMigration } from "../../storage"
|
||||
import { JsonMigration } from "../../storage/json-migration"
|
||||
import { EOL } from "os"
|
||||
import { errorMessage } from "../../util/error"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Provider } from "../../../provider"
|
||||
import { Session } from "../../../session"
|
||||
import type { MessageV2 } from "../../../session/message-v2"
|
||||
import { MessageID, PartID } from "../../../session/schema"
|
||||
import { ToolRegistry } from "../../../tool"
|
||||
import { ToolRegistry } from "../../../tool/registry"
|
||||
import { Instance } from "../../../project/instance"
|
||||
import { Permission } from "../../../permission"
|
||||
import { iife } from "../../../util/iife"
|
||||
@@ -111,7 +111,6 @@ function parseToolParams(input?: string) {
|
||||
} catch (evalError) {
|
||||
throw new Error(
|
||||
`Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
|
||||
{ cause: evalError },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../../effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
import { Log } from "../../../util"
|
||||
import { Log } from "../../../util/log"
|
||||
import { EOL } from "os"
|
||||
|
||||
export const LSPCommand = cmd({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EOL } from "os"
|
||||
import { Project } from "../../../project"
|
||||
import { Log } from "../../../util"
|
||||
import { Project } from "../../../project/project"
|
||||
import { Log } from "../../../util/log"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
export const ScrapCommand = cmd({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "path"
|
||||
import { exec } from "child_process"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import { Octokit } from "@octokit/rest"
|
||||
@@ -18,10 +18,10 @@ import type {
|
||||
} from "@octokit/webhooks-types"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { ModelsDev } from "../../provider"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { SessionShare } from "@/share"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Session } from "../../session"
|
||||
import type { SessionID } from "../../session/schema"
|
||||
import { MessageID, PartID } from "../../session/schema"
|
||||
@@ -32,7 +32,7 @@ import { SessionPrompt } from "@/session/prompt"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { Process } from "@/util"
|
||||
import { Process } from "@/util/process"
|
||||
import { Effect } from "effect"
|
||||
|
||||
type GitHubAuthor = {
|
||||
@@ -1031,7 +1031,6 @@ export const GithubRunCommand = cmd({
|
||||
console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error)
|
||||
throw new Error(
|
||||
"Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.",
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1222,7 +1221,7 @@ export const GithubRunCommand = cmd({
|
||||
console.log(` permission: ${permission}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to check permissions: ${error}`)
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error })
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
||||
}
|
||||
|
||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Session } from "../../session"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { cmd } from "./cmd"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Database } from "../../storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { ShareNext } from "../../share"
|
||||
import { ShareNext } from "../../share/share-next"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||
|
||||
@@ -10,11 +10,10 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
||||
import { Config } from "../../config"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Installation } from "../../installation"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import path from "path"
|
||||
import { Global } from "../../global"
|
||||
import { modify, applyEdits } from "jsonc-parser"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { Bus } from "../../bus"
|
||||
import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
@@ -698,7 +697,7 @@ export const McpDebugCommand = cmd({
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "opencode-debug", version: InstallationVersion },
|
||||
clientInfo: { name: "opencode-debug", version: Installation.VERSION },
|
||||
},
|
||||
id: 1,
|
||||
}),
|
||||
@@ -747,7 +746,7 @@ export const McpDebugCommand = cmd({
|
||||
try {
|
||||
const client = new Client({
|
||||
name: "opencode-debug",
|
||||
version: InstallationVersion,
|
||||
version: Installation.VERSION,
|
||||
})
|
||||
await client.connect(transport)
|
||||
prompts.log.success("Connection successful (already authenticated)")
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Argv } from "yargs"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Provider } from "../../provider"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { ModelsDev } from "../../provider"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { cmd } from "./cmd"
|
||||
import { UI } from "../ui"
|
||||
import { EOL } from "os"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { intro, log, outro, spinner } from "@clack/prompts"
|
||||
import type { Argv } from "yargs"
|
||||
|
||||
import { ConfigPaths } from "../../config"
|
||||
import { ConfigPaths } from "../../config/paths"
|
||||
import { Global } from "../../global"
|
||||
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
|
||||
import { resolvePluginTarget } from "../../plugin/shared"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { errorMessage } from "../../util/error"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Process } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { Process } from "../../util/process"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cmd } from "./cmd"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Process } from "@/util"
|
||||
import { Process } from "@/util/process"
|
||||
|
||||
export const PrCommand = cmd({
|
||||
command: "pr <number>",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "../../provider"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
@@ -12,7 +12,7 @@ import { Global } from "../../global"
|
||||
import { Plugin } from "../../plugin"
|
||||
import { Instance } from "../../project/instance"
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
import { Process } from "../../util"
|
||||
import { Process } from "../../util/process"
|
||||
import { text } from "node:stream/consumers"
|
||||
import { Effect } from "effect"
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import { cmd } from "./cmd"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { Server } from "../../server/server"
|
||||
import { Provider } from "../../provider"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import { Permission } from "../../permission"
|
||||
import { Tool } from "../../tool"
|
||||
import { Tool } from "../../tool/tool"
|
||||
import { GlobTool } from "../../tool/glob"
|
||||
import { GrepTool } from "../../tool/grep"
|
||||
import { ReadTool } from "../../tool/read"
|
||||
@@ -25,7 +25,7 @@ import { TaskTool } from "../../tool/task"
|
||||
import { SkillTool } from "../../tool/skill"
|
||||
import { BashTool } from "../../tool/bash"
|
||||
import { TodoWriteTool } from "../../tool/todo"
|
||||
import { Locale } from "../../util"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
type ToolProps<T> = {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Session } from "../../session"
|
||||
import { SessionID } from "../../session/schema"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { Locale } from "../../util"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Filesystem } from "../../util"
|
||||
import { Process } from "../../util"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { Process } from "../../util/process"
|
||||
import { EOL } from "os"
|
||||
import path from "path"
|
||||
import { which } from "../../util/which"
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Argv } from "yargs"
|
||||
import { cmd } from "./cmd"
|
||||
import { Session } from "../../session"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Database } from "../../storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { SessionTable } from "../../session/session.sql"
|
||||
import { Project } from "../../project"
|
||||
import { Project } from "../../project/project"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import * as Clipboard from "@tui/util/clipboard"
|
||||
import * as Selection from "@tui/util/selection"
|
||||
import * as Terminal from "@tui/util/terminal"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { Selection } from "@tui/util/selection"
|
||||
import { Terminal } from "@tui/util/terminal"
|
||||
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import {
|
||||
@@ -57,7 +57,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||
|
||||
@@ -235,10 +235,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
renderer,
|
||||
})
|
||||
const [ready, setReady] = createSignal(false)
|
||||
TuiPluginRuntime.init({
|
||||
api,
|
||||
config: tuiConfig,
|
||||
})
|
||||
TuiPluginRuntime.init(api)
|
||||
.catch((error) => {
|
||||
console.error("Failed to load TUI plugins", error)
|
||||
})
|
||||
@@ -353,7 +350,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
if (match) {
|
||||
continued = true
|
||||
if (args.fork) {
|
||||
void sdk.client.session.fork({ sessionID: match }).then((result) => {
|
||||
sdk.client.session.fork({ sessionID: match }).then((result) => {
|
||||
if (result.data?.id) {
|
||||
route.navigate({ type: "session", sessionID: result.data.id })
|
||||
} else {
|
||||
@@ -373,7 +370,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
createEffect(() => {
|
||||
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
|
||||
forked = true
|
||||
void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
|
||||
sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
|
||||
if (result.data?.id) {
|
||||
route.navigate({ type: "session", sessionID: result.data.id })
|
||||
} else {
|
||||
@@ -821,7 +818,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
|
||||
)
|
||||
|
||||
void exit()
|
||||
exit()
|
||||
})
|
||||
|
||||
const plugin = createMemo(() => {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { cmd } from "../cmd"
|
||||
import { UI } from "@/cli/ui"
|
||||
import { tui } from "./app"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
export const AttachCommand = cmd({
|
||||
command: "attach <url>",
|
||||
@@ -64,7 +66,10 @@ export const AttachCommand = cmd({
|
||||
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
||||
return { Authorization: auth }
|
||||
})()
|
||||
const config = await TuiConfig.get()
|
||||
const config = await Instance.provide({
|
||||
directory: directory && existsSync(directory) ? directory : process.cwd(),
|
||||
fn: () => TuiConfig.get(),
|
||||
})
|
||||
await tui({
|
||||
url: args.url,
|
||||
config,
|
||||
|
||||
@@ -20,7 +20,7 @@ export function DialogAgent() {
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Select agent"
|
||||
current={local.agent.current()?.name}
|
||||
current={local.agent.current().name}
|
||||
options={options()}
|
||||
onSelect={(option) => {
|
||||
local.agent.set(option.value)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync"
|
||||
import { map, pipe, entries, sortBy } from "remeda"
|
||||
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { Keybind } from "@/util"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TextAttributes } from "@opentui/core"
|
||||
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
|
||||
import { DialogModel } from "./dialog-model"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import * as Clipboard from "@tui/util/clipboard"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||
import { Locale } from "@/util"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { Keybind } from "@/util"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { createDebouncedSignal } from "../util/signal"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
|
||||
@@ -145,7 +145,7 @@ export function DialogSessionList() {
|
||||
title: "delete",
|
||||
onTrigger: async (option) => {
|
||||
if (toDelete() === option.value) {
|
||||
void sdk.client.session.delete({
|
||||
sdk.client.session.delete({
|
||||
sessionID: option.value,
|
||||
})
|
||||
setToDelete(undefined)
|
||||
|
||||
@@ -19,7 +19,7 @@ export function DialogSessionRename(props: DialogSessionRenameProps) {
|
||||
title="Rename Session"
|
||||
value={session()?.title}
|
||||
onConfirm={(value) => {
|
||||
void sdk.client.session.update({
|
||||
sdk.client.session.update({
|
||||
sessionID: props.session,
|
||||
title: value,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { Locale } from "@/util"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import { usePromptStash, type StashEntry } from "./prompt/stash"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import * as Clipboard from "@tui/util/clipboard"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { createSignal } from "solid-js"
|
||||
import { InstallationVersion } from "@/installation/version"
|
||||
import { Installation } from "@/installation"
|
||||
import { win32FlushInputBuffer } from "../win32"
|
||||
import { getScrollAcceleration } from "../util/scroll"
|
||||
|
||||
@@ -26,7 +26,7 @@ export function ErrorComponent(props: {
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
void handleExit()
|
||||
handleExit()
|
||||
}
|
||||
})
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
@@ -53,10 +53,10 @@ export function ErrorComponent(props: {
|
||||
)
|
||||
}
|
||||
|
||||
issueURL.searchParams.set("opencode-version", InstallationVersion)
|
||||
issueURL.searchParams.set("opencode-version", Installation.VERSION)
|
||||
|
||||
const copyIssueURL = () => {
|
||||
void Clipboard.copy(issueURL.toString()).then(() => {
|
||||
Clipboard.copy(issueURL.toString()).then(() => {
|
||||
setCopied(true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
||||
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
|
||||
import { useTheme, tint } from "@tui/context/theme"
|
||||
import * as Sound from "@tui/util/sound"
|
||||
import { Sound } from "@tui/util/sound"
|
||||
import { logo } from "@/cli/logo"
|
||||
|
||||
// Shadow markers (rendered chars in parens):
|
||||
@@ -520,7 +520,7 @@ export function Logo() {
|
||||
const shadow = tint(theme.background, ink, 0.25)
|
||||
const attrs = bold ? TextAttributes.BOLD : undefined
|
||||
|
||||
return Array.from(line).map((char, i) => {
|
||||
return [...line].map((char, i) => {
|
||||
const h = field(off + i, y, frame)
|
||||
const n = wave(off + i, y, frame, lit(char)) + h
|
||||
const s = wave(off + i, y, dusk, false) + h
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useTheme, selectedForeground } from "@tui/context/theme"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { Locale } from "@/util"
|
||||
import { Locale } from "@/util/locale"
|
||||
import type { PromptInfo } from "./history"
|
||||
import { useFrecency } from "./frecency"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user