Compare commits

...
Author SHA1 Message Date
Kit Langton df7e2fe63a feat: migrate Provider.Model and Provider.Info to Effect Schema, add config providers HttpApi slice
- migrate Provider.Model from Zod to Schema.Class with Schema.mutableKey
  on fields that the provider service mutates (id, name, cost, options,
  headers, variants, api.id)
- migrate Provider.Info from Zod to Schema.Class with Schema.mutableKey
  on mutated fields (name, env, options, models)
- inner types (Mode, Interleaved, Capabilities, Cache, Cost, Limit, Api)
  stay as Schema.Struct to avoid adding named refs to the SDK
- derive .zod compat surfaces for existing Hono routes
- add config httpapi route with GET /config/providers using typed
  ConfigProvidersResponse schema
- wire into httpapi/server.ts and bridge /config/providers behind flag
- zero SDK diff confirmed
2026-04-15 23:50:09 -04:00
opencode-agent[bot] 225a769411 chore: generate 2026-04-16 03:42:25 +00:00
Kit Langton 0e20382396 fix: resolve circular sibling imports causing runtime ReferenceError (#22752) 2026-04-15 23:41:34 -04:00
Kit Langton 509bc11f81 feat: unwrap lsp namespaces to flat exports + barrel (#22748) 2026-04-15 23:30:52 -04:00
Kit Langton f24207844f feat: unwrap storage namespaces to flat exports + barrel (#22747) 2026-04-15 23:30:49 -04:00
Kit Langton 1ca257e356 feat: unwrap config namespaces to flat exports + barrel (#22746) 2026-04-15 23:29:14 -04:00
Kit Langton d4cfbd020d feat: unwrap effect namespaces to flat exports + barrel (#22745) 2026-04-15 23:29:12 -04:00
Kit Langton 581d5208ca feat: unwrap share namespaces to flat exports + barrel (#22744) 2026-04-15 23:28:46 -04:00
Kit Langton a427a28fa9 feat: unwrap project namespaces to flat exports + barrel (#22743) 2026-04-15 23:28:46 -04:00
opencode-agent[bot] 0beaf04df5 chore: generate 2026-04-16 03:28:30 +00:00
Kit Langton 80f1f1b5b8 feat: enable type-aware no-floating-promises rule, fix all 177 violations (#22741)
publish-github-action / publish (push) Has been cancelled
2026-04-15 23:27:32 -04:00
Kit Langton 343a564183 feat: unwrap 11 util namespaces to flat exports + barrel (#22739) 2026-04-15 23:15:58 -04:00
306 changed files with 6725 additions and 6644 deletions
+7 -1
View File
@@ -30,7 +30,13 @@
// postMessage target origin not relevant for this codebase // postMessage target origin not relevant for this codebase
"unicorn/require-post-message-target-origin": "off", "unicorn/require-post-message-target-origin": "off",
// Side-effectful constructors are intentional in some places // Side-effectful constructors are intentional in some places
"no-new": "off" "no-new": "off",
// Type-aware: catch unhandled promises
"typescript/no-floating-promises": "warn"
},
"options": {
"typeAware": true
}, },
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"] "ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"]
} }
+15
View File
@@ -20,6 +20,7 @@
"glob": "13.0.5", "glob": "13.0.5",
"husky": "9.1.7", "husky": "9.1.7",
"oxlint": "1.60.0", "oxlint": "1.60.0",
"oxlint-tsgolint": "0.21.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "3.18.10", "sst": "3.18.10",
@@ -1680,6 +1681,18 @@
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="], "@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-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=="], "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="],
@@ -4100,6 +4113,8 @@
"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": ["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-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="],
+1 -1
View File
@@ -513,7 +513,7 @@ async function subscribeSessionEvents() {
const decoder = new TextDecoder() const decoder = new TextDecoder()
let text = "" let text = ""
;(async () => { void (async () => {
while (true) { while (true) {
try { try {
const { done, value } = await reader.read() const { done, value } = await reader.read()
+1
View File
@@ -87,6 +87,7 @@
"glob": "13.0.5", "glob": "13.0.5",
"husky": "9.1.7", "husky": "9.1.7",
"oxlint": "1.60.0", "oxlint": "1.60.0",
"oxlint-tsgolint": "0.21.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "3.18.10", "sst": "3.18.10",
+2 -2
View File
@@ -197,12 +197,12 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
fallback={ fallback={
<ConnectionError <ConnectionError
onRetry={() => { onRetry={() => {
if (checkMode() === "background") healthCheckActions.refetch() if (checkMode() === "background") void healthCheckActions.refetch()
}} }}
onServerSelected={(key) => { onServerSelected={(key) => {
setCheckMode("blocking") setCheckMode("blocking")
server.setActive(key) server.setActive(key)
healthCheckActions.refetch() void healthCheckActions.refetch()
}} }}
/> />
} }
@@ -327,7 +327,7 @@ export function DialogConnectProvider(props: { provider: string }) {
if (loading()) return if (loading()) return
if (methods().length === 1) { if (methods().length === 1) {
auto = true auto = true
selectMethod(0) void selectMethod(0)
} }
}) })
@@ -373,7 +373,7 @@ export function DialogConnectProvider(props: { provider: string }) {
key={(m) => m?.label} key={(m) => m?.label}
onSelect={async (selected, index) => { onSelect={async (selected, index) => {
if (!selected) return if (!selected) return
selectMethod(index) void selectMethod(index)
}} }}
> >
{(i) => ( {(i) => (
@@ -348,8 +348,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const open = (path: string) => { const open = (path: string) => {
const value = file.tab(path) const value = file.tab(path)
tabs().open(value) void tabs().open(value)
file.load(path) void file.load(path)
if (!view().reviewPanel.opened()) view().reviewPanel.open() if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
props.onOpenFile?.(path) props.onOpenFile?.(path)
@@ -344,7 +344,7 @@ export function DialogSelectServer() {
createEffect(() => { createEffect(() => {
items() items()
refreshHealth() void refreshHealth()
const interval = setInterval(refreshHealth, 10_000) const interval = setInterval(refreshHealth, 10_000)
onCleanup(() => clearInterval(interval)) onCleanup(() => clearInterval(interval))
}) })
@@ -498,7 +498,7 @@ export function DialogSelectServer() {
async function handleRemove(url: ServerConnection.Key) { async function handleRemove(url: ServerConnection.Key) {
server.remove(url) server.remove(url)
if ((await platform.getDefaultServer?.()) === url) { if ((await platform.getDefaultServer?.()) === url) {
platform.setDefaultServer?.(null) void platform.setDefaultServer?.(null)
} }
} }
@@ -536,7 +536,7 @@ export function DialogSelectServer() {
items={sortedItems} items={sortedItems}
key={(x) => x.http.url} key={(x) => x.http.url}
onSelect={(x) => { onSelect={(x) => {
if (x) select(x) if (x) void select(x)
}} }}
divider={true} 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" 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"
+5 -5
View File
@@ -212,9 +212,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!view().reviewPanel.opened()) view().reviewPanel.open() if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
const tab = files.tab(item.path) const tab = files.tab(item.path)
tabs().open(tab) void tabs().open(tab)
tabs().setActive(tab) tabs().setActive(tab)
Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
} }
const recent = createMemo(() => { const recent = createMemo(() => {
@@ -1139,7 +1139,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
if (working()) { if (working()) {
abort() void abort()
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return return
@@ -1205,7 +1205,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return return
} }
if (working()) { if (working()) {
abort() void abort()
event.preventDefault() event.preventDefault()
} }
return return
@@ -1245,7 +1245,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
) { ) {
return return
} }
handleSubmit(event) void handleSubmit(event)
} }
} }
@@ -295,7 +295,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const mode = input.mode() const mode = input.mode()
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
if (input.working()) abort() if (input.working()) void abort()
return return
} }
@@ -24,7 +24,7 @@ function openSessionContext(args: {
}) { }) {
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open() if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
args.tabs.open("context") void args.tabs.open("context")
args.tabs.setActive("context") args.tabs.setActive("context")
} }
@@ -44,7 +44,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
const close = () => { const close = () => {
const count = terminal.all().length const count = terminal.all().length
terminal.close(props.terminal.id) void terminal.close(props.terminal.id)
if (count === 1) { if (count === 1) {
props.onClose?.() props.onClose?.()
} }
+1 -1
View File
@@ -415,7 +415,7 @@ export const Terminal = (props: TerminalProps) => {
if (local.autoFocus !== false) focusTerminal() if (local.autoFocus !== false) focusTerminal()
if (typeof document !== "undefined" && document.fonts) { if (typeof document !== "undefined" && document.fonts) {
document.fonts.ready.then(scheduleFit) void document.fonts.ready.then(scheduleFit)
} }
const onResize = t.onResize((size) => { const onResize = t.onResize((size) => {
+5 -5
View File
@@ -237,7 +237,7 @@ function createGlobalSync() {
}) })
sessionLoads.set(directory, promise) sessionLoads.set(directory, promise)
promise.finally(() => { void promise.finally(() => {
sessionLoads.delete(directory) sessionLoads.delete(directory)
children.unpin(directory) children.unpin(directory)
}) })
@@ -273,7 +273,7 @@ function createGlobalSync() {
})() })()
booting.set(directory, promise) booting.set(directory, promise)
promise.finally(() => { void promise.finally(() => {
booting.delete(directory) booting.delete(directory)
children.unpin(directory) children.unpin(directory)
}) })
@@ -317,7 +317,7 @@ function createGlobalSync() {
setSessionTodo, setSessionTodo,
vcsCache: children.vcsCache.get(directory), vcsCache: children.vcsCache.get(directory),
loadLsp: () => { loadLsp: () => {
sdkFor(directory) void sdkFor(directory)
.lsp.status() .lsp.status()
.then((x) => { .then((x) => {
setStore("lsp", x.data ?? []) setStore("lsp", x.data ?? [])
@@ -359,13 +359,13 @@ function createGlobalSync() {
eventFrame = undefined eventFrame = undefined
eventTimer = setTimeout(() => { eventTimer = setTimeout(() => {
eventTimer = undefined eventTimer = undefined
globalSDK.event.start() void globalSDK.event.start()
}, 0) }, 0)
}) })
} else { } else {
eventTimer = setTimeout(() => { eventTimer = setTimeout(() => {
eventTimer = undefined eventTimer = undefined
globalSDK.event.start() void globalSDK.event.start()
}, 0) }, 0)
} }
void bootstrap() void bootstrap()
+1 -1
View File
@@ -582,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
open(directory: string) { open(directory: string) {
const root = rootFor(directory) const root = rootFor(directory)
if (server.projects.list().find((x) => x.worktree === root)) return if (server.projects.list().find((x) => x.worktree === root)) return
globalSync.project.loadSessions(root) void globalSync.project.loadSessions(root)
server.projects.open(root) server.projects.open(root)
}, },
close(directory: string) { close(directory: string) {
+2 -2
View File
@@ -117,7 +117,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
entry?.value.clear() entry?.value.clear()
} }
removePersisted(Persist.workspace(dir, "terminal"), platform) void removePersisted(Persist.workspace(dir, "terminal"), platform)
const legacy = new Set(getLegacyTerminalStorageKeys(dir)) const legacy = new Set(getLegacyTerminalStorageKeys(dir))
for (const id of sessionIDs ?? []) { for (const id of sessionIDs ?? []) {
@@ -126,7 +126,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
} }
} }
for (const key of legacy) { for (const key of legacy) {
removePersisted({ key }, platform) void removePersisted({ key }, platform)
} }
} }
+10 -10
View File
@@ -956,7 +956,7 @@ export default function Layout(props: ParentProps) {
// warm up child store to prevent flicker // warm up child store to prevent flicker
globalSync.child(target.worktree) globalSync.child(target.worktree)
openProject(target.worktree) void openProject(target.worktree)
} }
function navigateSessionByUnseen(offset: number) { function navigateSessionByUnseen(offset: number) {
@@ -1094,7 +1094,7 @@ export default function Layout(props: ParentProps) {
disabled: !params.dir || !params.id, disabled: !params.dir || !params.id,
onSelect: () => { onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id) const session = currentSessions().find((s) => s.id === params.id)
if (session) archiveSession(session) if (session) void archiveSession(session)
}, },
}, },
{ {
@@ -1360,11 +1360,11 @@ export default function Layout(props: ParentProps) {
if (!server.isLocal()) return if (!server.isLocal()) return
for (const directory of collectOpenProjectDeepLinks(urls)) { for (const directory of collectOpenProjectDeepLinks(urls)) {
openProject(directory) void openProject(directory)
} }
for (const link of collectNewSessionDeepLinks(urls)) { for (const link of collectNewSessionDeepLinks(urls)) {
openProject(link.directory, false) void openProject(link.directory, false)
const slug = base64Encode(link.directory) const slug = base64Encode(link.directory)
if (link.prompt) { if (link.prompt) {
setSessionHandoff(slug, { prompt: link.prompt }) setSessionHandoff(slug, { prompt: link.prompt })
@@ -1453,11 +1453,11 @@ export default function Layout(props: ParentProps) {
function resolve(result: string | string[] | null) { function resolve(result: string | string[] | null) {
if (Array.isArray(result)) { if (Array.isArray(result)) {
for (const directory of result) { for (const directory of result) {
openProject(directory, false) void openProject(directory, false)
} }
navigateToProject(result[0]) void navigateToProject(result[0])
} else if (result) { } else if (result) {
openProject(result) void openProject(result)
} }
} }
@@ -1825,7 +1825,7 @@ export default function Layout(props: ParentProps) {
const next = new Set(dirs) const next = new Set(dirs)
for (const directory of next) { for (const directory of next) {
if (loadedSessionDirs.has(directory)) continue if (loadedSessionDirs.has(directory)) continue
globalSync.project.loadSessions(directory) void globalSync.project.loadSessions(directory)
} }
loadedSessionDirs.clear() loadedSessionDirs.clear()
@@ -2110,7 +2110,7 @@ export default function Layout(props: ParentProps) {
onSave={(next) => { onSave={(next) => {
const item = project() const item = project()
if (!item) return if (!item) return
renameProject(item, next) void renameProject(item, next)
}} }}
class="text-14-medium text-text-strong truncate" class="text-14-medium text-text-strong truncate"
displayClass="text-14-medium text-text-strong truncate" displayClass="text-14-medium text-text-strong truncate"
@@ -2242,7 +2242,7 @@ export default function Layout(props: ParentProps) {
onClick={() => { onClick={() => {
const item = project() const item = project()
if (!item) return if (!item) return
createWorkspace(item) void createWorkspace(item)
}} }}
> >
{language.t("workspace.new")} {language.t("workspace.new")}
@@ -277,7 +277,7 @@ const WorkspaceSessionList = (props: {
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10" class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
size="large" size="large"
onClick={(e: MouseEvent) => { onClick={(e: MouseEvent) => {
props.loadMore() void props.loadMore()
;(e.currentTarget as HTMLButtonElement).blur() ;(e.currentTarget as HTMLButtonElement).blur()
}} }}
> >
+1 -1
View File
@@ -484,7 +484,7 @@ export default function Page() {
if (!tab) return if (!tab) return
const path = file.pathFromTab(tab) const path = file.pathFromTab(tab)
if (path) file.load(path) if (path) void file.load(path)
}) })
createEffect( createEffect(
+1 -1
View File
@@ -117,7 +117,7 @@ export const createOpenReviewFile = (input: {
input.openTab(tab) input.openTab(tab)
input.setActive(tab) input.setActive(tab)
} }
if (maybePromise instanceof Promise) maybePromise.then(open) if (maybePromise instanceof Promise) void maybePromise.then(open)
else open() else open()
}) })
} }
@@ -105,4 +105,4 @@ async function main() {
console.log(`✓ Sitemap generated at ${outputPath}`) console.log(`✓ Sitemap generated at ${outputPath}`)
} }
main() void main()
@@ -766,7 +766,7 @@ export default function Spotlight(props: SpotlightProps) {
} }
} }
initializeWebGPU() void initializeWebGPU()
onCleanup(() => { onCleanup(() => {
if (cleanupFunctionRef) { if (cleanupFunctionRef) {
@@ -298,7 +298,7 @@ export default function BlackSubscribe() {
// Resolve stripe promise once // Resolve stripe promise once
createEffect(() => { createEffect(() => {
stripePromise.then((s) => { void stripePromise.then((s) => {
if (s) setStripe(s) if (s) setStripe(s)
}) })
}) })
@@ -77,7 +77,7 @@ export default function Download() {
const handleCopyClick = (command: string) => (event: Event) => { const handleCopyClick = (command: string) => (event: Event) => {
const button = event.currentTarget as HTMLButtonElement const button = event.currentTarget as HTMLButtonElement
navigator.clipboard.writeText(command) void navigator.clipboard.writeText(command)
button.setAttribute("data-copied", "") button.setAttribute("data-copied", "")
setTimeout(() => { setTimeout(() => {
button.removeAttribute("data-copied") button.removeAttribute("data-copied")
+1 -1
View File
@@ -35,7 +35,7 @@ export default function Home() {
const button = event.currentTarget as HTMLButtonElement const button = event.currentTarget as HTMLButtonElement
const text = button.textContent const text = button.textContent
if (text) { if (text) {
navigator.clipboard.writeText(text) void navigator.clipboard.writeText(text)
button.setAttribute("data-copied", "") button.setAttribute("data-copied", "")
setTimeout(() => { setTimeout(() => {
button.removeAttribute("data-copied") button.removeAttribute("data-copied")
+1 -1
View File
@@ -27,7 +27,7 @@ export default function Home() {
const callback = () => { const callback = () => {
const text = button.textContent const text = button.textContent
if (text) { if (text) {
navigator.clipboard.writeText(text) void navigator.clipboard.writeText(text)
button.setAttribute("data-copied", "") button.setAttribute("data-copied", "")
setTimeout(() => { setTimeout(() => {
button.removeAttribute("data-copied") button.removeAttribute("data-copied")
@@ -26,14 +26,14 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
const minute = timestamp.substring(10, 12) const minute = timestamp.substring(10, 12)
const second = timestamp.substring(12, 14) const second = timestamp.substring(12, 14)
waitUntil( void waitUntil(
Resource.ZenDataNew.put( Resource.ZenDataNew.put(
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`, `data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
JSON.stringify({ timestamp, ...data }), JSON.stringify({ timestamp, ...data }),
), ),
) )
waitUntil( void waitUntil(
Resource.ZenDataNew.put( Resource.ZenDataNew.put(
`meta/${data.modelName}/${sessionId}/${requestId}.json`, `meta/${data.modelName}/${sessionId}/${requestId}.json`,
JSON.stringify({ timestamp, ...metadata }), JSON.stringify({ timestamp, ...metadata }),
+2 -2
View File
@@ -1,5 +1,5 @@
if (location.pathname === "/loading") { if (location.pathname === "/loading") {
import("./loading") void import("./loading")
} else { } else {
import("./") void import("./")
} }
+1 -1
View File
@@ -410,7 +410,7 @@ const createPlatform = (): Platform => {
} }
let menuTrigger = null as null | ((id: string) => void) let menuTrigger = null as null | ((id: string) => void)
createMenu((id) => { void createMenu((id) => {
menuTrigger?.(id) menuTrigger?.(id)
}) })
void listenForDeepLinks() void listenForDeepLinks()
+1 -1
View File
@@ -48,7 +48,7 @@ render(() => {
}) })
onCleanup(() => { onCleanup(() => {
listener.then((cb) => cb()) void listener.then((cb) => cb())
timers.forEach(clearTimeout) timers.forEach(clearTimeout)
}) })
}) })
+1 -1
View File
@@ -186,5 +186,5 @@ export async function createMenu(trigger: (id: string) => void) {
}), }),
], ],
}) })
menu.setAsAppMenu() void menu.setAsAppMenu()
} }
+1 -1
View File
@@ -17,7 +17,7 @@ const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_Z
const applyZoom = (next: number) => { const applyZoom = (next: number) => {
setWebviewZoom(next) setWebviewZoom(next)
invoke("plugin:webview|set_webview_zoom", { void invoke("plugin:webview|set_webview_zoom", {
value: next, value: next,
}) })
} }
+1 -1
View File
@@ -37,4 +37,4 @@ async function test() {
await Share.remove({ id: shareInfo.id, secret: shareInfo.secret }) await Share.remove({ id: shareInfo.id, secret: shareInfo.secret })
} }
test() void test()
+1 -1
View File
@@ -112,7 +112,7 @@ async function main() {
} }
try { try {
main() void main()
} catch (error) { } catch (error) {
console.error("Postinstall script error:", error.message) console.error("Postinstall script error:", error.message)
process.exit(0) process.exit(0)
+1 -1
View File
@@ -2,7 +2,7 @@
import { z } from "zod" import { z } from "zod"
import { Config } from "../src/config" import { Config } from "../src/config"
import { TuiConfig } from "../src/config/tui" import { TuiConfig } from "../src/config"
function generate(schema: z.ZodType) { function generate(schema: z.ZodType) {
const result = z.toJSONSchema(schema, { const result = z.toJSONSchema(schema, {
+1 -1
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Effect, Layer, Option, Schema, Context } from "effect" import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage"
import { AccountStateTable, AccountTable } from "./account.sql" import { AccountStateTable, AccountTable } from "./account.sql"
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema" import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
import { normalizeServerUrl } from "./url" import { normalizeServerUrl } from "./url"
+4 -4
View File
@@ -31,9 +31,9 @@ import {
type Usage, type Usage,
} from "@agentclientprotocol/sdk" } from "@agentclientprotocol/sdk"
import { Log } from "../util/log" import { Log } from "../util"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util"
import { Hash } from "@opencode-ai/shared/util/hash" import { Hash } from "@opencode-ai/shared/util/hash"
import { ACPSessionManager } from "./session" import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types" import type { ACPConfig } from "./types"
@@ -242,7 +242,7 @@ export namespace ACP {
const newContent = getNewContent(content, diff) const newContent = getNewContent(content, diff)
if (newContent) { if (newContent) {
this.connection.writeTextFile({ void this.connection.writeTextFile({
sessionId: session.id, sessionId: session.id,
path: filepath, path: filepath,
content: newContent, content: newContent,
@@ -1253,7 +1253,7 @@ export namespace ACP {
) )
setTimeout(() => { setTimeout(() => {
this.connection.sessionUpdate({ void this.connection.sessionUpdate({
sessionId, sessionId,
update: { update: {
sessionUpdate: "available_commands_update", sessionUpdate: "available_commands_update",
+1 -1
View File
@@ -1,6 +1,6 @@
import { RequestError, type McpServer } from "@agentclientprotocol/sdk" import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
import type { ACPSessionState } from "./types" import type { ACPSessionState } from "./types"
import { Log } from "@/util/log" import { Log } from "@/util"
import type { OpencodeClient } from "@opencode-ai/sdk/v2" import type { OpencodeClient } from "@opencode-ai/sdk/v2"
const log = Log.create({ service: "acp-session-manager" }) const log = Log.create({ service: "acp-session-manager" })
+1 -1
View File
@@ -1,7 +1,7 @@
import z from "zod" import z from "zod"
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
import { EffectBridge } from "@/effect" import { EffectBridge } from "@/effect"
import { Log } from "../util/log" import { Log } from "../util"
import { BusEvent } from "./bus-event" import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global" import { GlobalBus } from "./global"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
+1 -1
View File
@@ -1,4 +1,4 @@
import { Log } from "@/util/log" import { Log } from "@/util"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
+1 -1
View File
@@ -7,7 +7,7 @@ import { Agent } from "../../agent/agent"
import { Provider } from "../../provider" import { Provider } from "../../provider"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import matter from "gray-matter" import matter from "gray-matter"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { EOL } from "os" import { EOL } from "os"
+2 -2
View File
@@ -1,11 +1,11 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { spawn } from "child_process" import { spawn } from "child_process"
import { Database } from "../../storage/db" import { Database } from "../../storage"
import { drizzle } from "drizzle-orm/bun-sqlite" import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database as BunDatabase } from "bun:sqlite" import { Database as BunDatabase } from "bun:sqlite"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { JsonMigration } from "../../storage/json-migration" import { JsonMigration } from "../../storage"
import { EOL } from "os" import { EOL } from "os"
import { errorMessage } from "../../util/error" import { errorMessage } from "../../util/error"
+1 -1
View File
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../../effect/app-runtime"
import { Effect } from "effect" import { Effect } from "effect"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { Log } from "../../../util/log" import { Log } from "../../../util"
import { EOL } from "os" import { EOL } from "os"
export const LSPCommand = cmd({ export const LSPCommand = cmd({
+2 -2
View File
@@ -1,6 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { Project } from "../../../project/project" import { Project } from "../../../project"
import { Log } from "../../../util/log" import { Log } from "../../../util"
import { cmd } from "../cmd" import { cmd } from "../cmd"
export const ScrapCommand = cmd({ export const ScrapCommand = cmd({
+3 -3
View File
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { exec } from "child_process" import { exec } from "child_process"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda" import { map, pipe, sortBy, values } from "remeda"
import { Octokit } from "@octokit/rest" import { Octokit } from "@octokit/rest"
@@ -21,7 +21,7 @@ import { cmd } from "./cmd"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider/models"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share/session" import { SessionShare } from "@/share"
import { Session } from "../../session" import { Session } from "../../session"
import type { SessionID } from "../../session/schema" import type { SessionID } from "../../session/schema"
import { MessageID, PartID } 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 { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util/process" import { Process } from "@/util"
import { Effect } from "effect" import { Effect } from "effect"
type GitHubAuthor = { type GitHubAuthor = {
+3 -3
View File
@@ -4,12 +4,12 @@ import { Session } from "../../session"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db" import { Database } from "../../storage"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { ShareNext } from "../../share/share-next" import { ShareNext } from "../../share"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */ /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
+1 -1
View File
@@ -13,7 +13,7 @@ import { Installation } from "../../installation"
import path from "path" import path from "path"
import { Global } from "../../global" import { Global } from "../../global"
import { modify, applyEdits } from "jsonc-parser" import { modify, applyEdits } from "jsonc-parser"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { AppRuntime } from "../../effect/app-runtime" import { AppRuntime } from "../../effect/app-runtime"
import { Effect } from "effect" import { Effect } from "effect"
+3 -3
View File
@@ -1,14 +1,14 @@
import { intro, log, outro, spinner } from "@clack/prompts" import { intro, log, outro, spinner } from "@clack/prompts"
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { ConfigPaths } from "../../config/paths" import { ConfigPaths } from "../../config"
import { Global } from "../../global" import { Global } from "../../global"
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install" import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
import { resolvePluginTarget } from "../../plugin/shared" import { resolvePluginTarget } from "../../plugin/shared"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { errorMessage } from "../../util/error" import { errorMessage } from "../../util/error"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import { Process } from "../../util/process" import { Process } from "../../util"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
+1 -1
View File
@@ -3,7 +3,7 @@ import { cmd } from "./cmd"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { Process } from "@/util/process" import { Process } from "@/util"
export const PrCommand = cmd({ export const PrCommand = cmd({
command: "pr <number>", command: "pr <number>",
+1 -1
View File
@@ -12,7 +12,7 @@ import { Global } from "../../global"
import { Plugin } from "../../plugin" import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin" import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process" import { Process } from "../../util"
import { text } from "node:stream/consumers" import { text } from "node:stream/consumers"
import { Effect } from "effect" import { Effect } from "effect"
+2 -2
View File
@@ -6,7 +6,7 @@ import { cmd } from "./cmd"
import { Flag } from "../../flag/flag" import { Flag } from "../../flag/flag"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server" import { Server } from "../../server/server"
import { Provider } from "../../provider" import { Provider } from "../../provider"
@@ -25,7 +25,7 @@ import { TaskTool } from "../../tool/task"
import { SkillTool } from "../../tool/skill" import { SkillTool } from "../../tool/skill"
import { BashTool } from "../../tool/bash" import { BashTool } from "../../tool/bash"
import { TodoWriteTool } from "../../tool/todo" import { TodoWriteTool } from "../../tool/todo"
import { Locale } from "../../util/locale" import { Locale } from "../../util"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
type ToolProps<T> = { type ToolProps<T> = {
+3 -3
View File
@@ -4,10 +4,10 @@ import { Session } from "../../session"
import { SessionID } from "../../session/schema" import { SessionID } from "../../session/schema"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { UI } from "../ui" import { UI } from "../ui"
import { Locale } from "../../util/locale" import { Locale } from "../../util"
import { Flag } from "../../flag/flag" import { Flag } from "../../flag/flag"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import { Process } from "../../util/process" import { Process } from "../../util"
import { EOL } from "os" import { EOL } from "os"
import path from "path" import path from "path"
import { which } from "../../util/which" import { which } from "../../util/which"
+2 -2
View File
@@ -2,9 +2,9 @@ import type { Argv } from "yargs"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { Session } from "../../session" import { Session } from "../../session"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db" import { Database } from "../../storage"
import { SessionTable } from "../../session/session.sql" import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project/project" import { Project } from "../../project"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
+4 -4
View File
@@ -57,7 +57,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open" import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt" import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
import { FormatError, FormatUnknownError } from "@/cli/error" import { FormatError, FormatUnknownError } from "@/cli/error"
@@ -350,7 +350,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
if (match) { if (match) {
continued = true continued = true
if (args.fork) { if (args.fork) {
sdk.client.session.fork({ sessionID: match }).then((result) => { void sdk.client.session.fork({ sessionID: match }).then((result) => {
if (result.data?.id) { if (result.data?.id) {
route.navigate({ type: "session", sessionID: result.data.id }) route.navigate({ type: "session", sessionID: result.data.id })
} else { } else {
@@ -370,7 +370,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
createEffect(() => { createEffect(() => {
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
forked = true forked = true
sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
if (result.data?.id) { if (result.data?.id) {
route.navigate({ type: "session", sessionID: result.data.id }) route.navigate({ type: "session", sessionID: result.data.id })
} else { } else {
@@ -818,7 +818,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`, `Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
) )
exit() void exit()
}) })
const plugin = createMemo(() => { const plugin = createMemo(() => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { cmd } from "../cmd"
import { UI } from "@/cli/ui" import { UI } from "@/cli/ui"
import { tui } from "./app" import { tui } from "./app"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { existsSync } from "fs" import { existsSync } from "fs"
@@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync"
import { map, pipe, entries, sortBy } from "remeda" import { map, pipe, entries, sortBy } from "remeda"
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select" import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
@@ -3,14 +3,14 @@ import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { createMemo, createResource, createSignal, onMount } from "solid-js" import { createMemo, createResource, createSignal, onMount } from "solid-js"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { useProject } from "@tui/context/project" import { useProject } from "@tui/context/project"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk" import { useSDK } from "../context/sdk"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { DialogSessionRename } from "./dialog-session-rename" import { DialogSessionRename } from "./dialog-session-rename"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
import { createDebouncedSignal } from "../util/signal" import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create" import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
@@ -145,7 +145,7 @@ export function DialogSessionList() {
title: "delete", title: "delete",
onTrigger: async (option) => { onTrigger: async (option) => {
if (toDelete() === option.value) { if (toDelete() === option.value) {
sdk.client.session.delete({ void sdk.client.session.delete({
sessionID: option.value, sessionID: option.value,
}) })
setToDelete(undefined) setToDelete(undefined)
@@ -19,7 +19,7 @@ export function DialogSessionRename(props: DialogSessionRenameProps) {
title="Rename Session" title="Rename Session"
value={session()?.title} value={session()?.title}
onConfirm={(value) => { onConfirm={(value) => {
sdk.client.session.update({ void sdk.client.session.update({
sessionID: props.session, sessionID: props.session,
title: value, title: value,
}) })
@@ -1,7 +1,7 @@
import { useDialog } from "@tui/ui/dialog" import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select" import { DialogSelect } from "@tui/ui/dialog-select"
import { createMemo, createSignal } from "solid-js" import { createMemo, createSignal } from "solid-js"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { usePromptStash, type StashEntry } from "./prompt/stash" import { usePromptStash, type StashEntry } from "./prompt/stash"
@@ -26,7 +26,7 @@ export function ErrorComponent(props: {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.ctrl && evt.name === "c") { if (evt.ctrl && evt.name === "c") {
handleExit() void handleExit()
} }
}) })
const [copied, setCopied] = createSignal(false) const [copied, setCopied] = createSignal(false)
@@ -56,7 +56,7 @@ export function ErrorComponent(props: {
issueURL.searchParams.set("opencode-version", Installation.VERSION) issueURL.searchParams.set("opencode-version", Installation.VERSION)
const copyIssueURL = () => { const copyIssueURL = () => {
Clipboard.copy(issueURL.toString()).then(() => { void Clipboard.copy(issueURL.toString()).then(() => {
setCopied(true) setCopied(true)
}) })
} }
@@ -12,7 +12,7 @@ import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command" import { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid" import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import type { PromptInfo } from "./history" import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency" import { useFrecency } from "./frecency"
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Global } from "@/global" import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { onMount } from "solid-js" import { onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "../../context/helper" import { createSimpleContext } from "../../context/helper"
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Global } from "@/global" import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { onMount } from "solid-js" import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store" import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../../context/helper" import { createSimpleContext } from "../../context/helper"
@@ -3,7 +3,7 @@ import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, S
import "opentui-spinner/solid" import "opentui-spinner/solid"
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { EmptyBorder, SplitBorder } from "@tui/component/border" import { EmptyBorder, SplitBorder } from "@tui/component/border"
@@ -27,7 +27,7 @@ import { Clipboard } from "../../util/clipboard"
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event" import { TuiEvent } from "../../event"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { formatDuration } from "@/util/format" import { formatDuration } from "@/util/format"
import { createColors, createFrames } from "../../ui/spinner.ts" import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog" import { useDialog } from "@tui/ui/dialog"
@@ -235,7 +235,7 @@ export function Prompt(props: PromptProps) {
hidden: true, hidden: true,
onSelect: (dialog) => { onSelect: (dialog) => {
if (!input.focused) return if (!input.focused) return
submit() void submit()
dialog.clear() dialog.clear()
}, },
}, },
@@ -280,7 +280,7 @@ export function Prompt(props: PromptProps) {
}, 5000) }, 5000)
if (store.interrupt >= 2) { if (store.interrupt >= 2) {
sdk.client.session.abort({ void sdk.client.session.abort({
sessionID: props.sessionID, sessionID: props.sessionID,
}) })
setStore("interrupt", 0) setStore("interrupt", 0)
@@ -429,7 +429,7 @@ export function Prompt(props: PromptProps) {
setStore("extmarkToPartIndex", new Map()) setStore("extmarkToPartIndex", new Map())
}, },
submit() { submit() {
submit() void submit()
}, },
} }
@@ -604,12 +604,12 @@ export function Prompt(props: PromptProps) {
if (!store.prompt.input) return if (!store.prompt.input) return
const trimmed = store.prompt.input.trim() const trimmed = store.prompt.input.trim()
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") { if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
exit() void exit()
return return
} }
const selectedModel = local.model.current() const selectedModel = local.model.current()
if (!selectedModel) { if (!selectedModel) {
promptModelWarning() void promptModelWarning()
return return
} }
@@ -660,7 +660,7 @@ export function Prompt(props: PromptProps) {
const variant = local.model.variant.current() const variant = local.model.variant.current()
if (store.mode === "shell") { if (store.mode === "shell") {
sdk.client.session.shell({ void sdk.client.session.shell({
sessionID, sessionID,
agent: local.agent.current().name, agent: local.agent.current().name,
model: { model: {
@@ -685,7 +685,7 @@ export function Prompt(props: PromptProps) {
const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1) const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "") const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
sdk.client.session.command({ void sdk.client.session.command({
sessionID, sessionID,
command: command.slice(1), command: command.slice(1),
arguments: args, arguments: args,
@@ -1208,7 +1208,7 @@ export function Prompt(props: PromptProps) {
const r = retry() const r = retry()
if (!r) return if (!r) return
if (isTruncated()) { if (isTruncated()) {
DialogAlert.show(dialog, "Retry Error", r.message) void DialogAlert.show(dialog, "Retry Error", r.message)
} }
} }
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Global } from "@/global" import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { onMount } from "solid-js" import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store" import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../../context/helper" import { createSimpleContext } from "../../context/helper"
@@ -1,7 +1,7 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import type { KeyBinding } from "@opentui/core" import type { KeyBinding } from "@opentui/core"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
const TEXTAREA_ACTIONS = [ const TEXTAREA_ACTIONS = [
"submit", "submit",
@@ -1,7 +1,7 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
import { pipe, mapValues } from "remeda" import { pipe, mapValues } from "remeda"
import type { TuiConfig } from "@/config/tui" import type { TuiConfig } from "@/config"
import type { ParsedKey, Renderable } from "@opentui/core" import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid" import { useKeyboard, useRenderer } from "@opentui/solid"
@@ -1,5 +1,5 @@
import { Global } from "@/global" import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { createSignal, type Setter } from "solid-js" import { createSignal, type Setter } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
@@ -44,7 +44,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
}, },
set(key: string, value: any) { set(key: string, value: any) {
setStore(key, value) setStore(key, value)
Filesystem.writeJson(filePath, store) void Filesystem.writeJson(filePath, store)
}, },
} }
return result return result
@@ -12,7 +12,7 @@ import { Provider } from "@/provider"
import { useArgs } from "./args" import { useArgs } from "./args"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core" import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local", name: "Local",
@@ -131,7 +131,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return return
} }
state.pending = false state.pending = false
Filesystem.writeJson(filePath, { void Filesystem.writeJson(filePath, {
recent: modelStore.recent, recent: modelStore.recent,
favorite: modelStore.favorite, favorite: modelStore.favorite,
variant: modelStore.variant, variant: modelStore.variant,
@@ -28,7 +28,7 @@ import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit" import { useExit } from "./exit"
import { useArgs } from "./args" import { useArgs } from "./args"
import { batch, createEffect, on } from "solid-js" import { batch, createEffect, on } from "solid-js"
import { Log } from "@/util/log" import { Log } from "@/util"
import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state" import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
@@ -111,7 +111,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
event.subscribe((event) => { event.subscribe((event) => {
switch (event.type) { switch (event.type) {
case "server.instance.disposed": case "server.instance.disposed":
bootstrap() void bootstrap()
break break
case "permission.replied": { case "permission.replied": {
const requests = store.permission[event.properties.sessionID] const requests = store.permission[event.properties.sessionID]
@@ -336,7 +336,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
case "lsp.updated": { case "lsp.updated": {
const workspace = project.workspace.current() const workspace = project.workspace.current()
sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? [])) void sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? []))
break break
} }
@@ -415,7 +415,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.then(() => { .then(() => {
if (store.status !== "complete") setStore("status", "partial") if (store.status !== "complete") setStore("status", "partial")
// non-blocking // non-blocking
Promise.all([ void Promise.all([
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))), consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))), sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))),
@@ -40,7 +40,7 @@ import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid" import { useRenderer } from "@opentui/solid"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Global } from "@/global" import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { useTuiConfig } from "./tui-config" import { useTuiConfig } from "./tui-config"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
@@ -329,7 +329,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
}) })
function init() { function init() {
Promise.allSettled([ void Promise.allSettled([
resolveSystemTheme(store.mode), resolveSystemTheme(store.mode),
getCustomThemes() getCustomThemes()
.then((custom) => { .then((custom) => {
@@ -377,7 +377,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
if (store.mode === mode) return if (store.mode === mode) return
setStore("mode", mode) setStore("mode", mode)
renderer.clearPaletteCache() renderer.clearPaletteCache()
resolveSystemTheme(mode) void resolveSystemTheme(mode)
} }
function pin(mode: "dark" | "light" = store.mode) { function pin(mode: "dark" | "light" = store.mode) {
@@ -1,4 +1,4 @@
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({ export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({
@@ -1,4 +1,4 @@
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui" import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
@@ -78,7 +78,7 @@ function Install(props: { api: TuiPluginApi }) {
} }
setBusy(true) setBusy(true)
props.api.plugins void props.api.plugins
.install(mod, { global: global() }) .install(mod, { global: global() })
.then((out) => { .then((out) => {
if (!out.ok) { if (!out.ok) {
@@ -188,7 +188,7 @@ function View(props: { api: TuiPluginApi }) {
if (!item) return if (!item) return
setLock(true) setLock(true)
const task = item.active ? props.api.plugins.deactivate(x) : props.api.plugins.activate(x) const task = item.active ? props.api.plugins.deactivate(x) : props.api.plugins.activate(x)
task void task
.then((ok) => { .then((ok) => {
if (!ok) { if (!ok) {
props.api.ui.toast({ props.api.ui.toast({
@@ -8,7 +8,7 @@ import type { useSDK } from "@tui/context/sdk"
import type { useSync } from "@tui/context/sync" import type { useSync } from "@tui/context/sync"
import type { useTheme } from "@tui/context/theme" import type { useTheme } from "@tui/context/theme"
import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog" import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog"
import type { TuiConfig } from "@/config/tui" import type { TuiConfig } from "@/config"
import { createPluginKeybind } from "../context/plugin-keybinds" import { createPluginKeybind } from "../context/plugin-keybinds"
import type { useKV } from "../context/kv" import type { useKV } from "../context/kv"
import { DialogAlert } from "../ui/dialog-alert" import { DialogAlert } from "../ui/dialog-alert"
@@ -14,8 +14,8 @@ import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { Config } from "@/config" import { Config } from "@/config"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config"
import { Log } from "@/util/log" import { Log } from "@/util"
import { errorData, errorMessage } from "@/util/error" import { errorData, errorMessage } from "@/util/error"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
@@ -32,8 +32,8 @@ import { PluginMeta } from "@/plugin/meta"
import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install" import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
import { hasTheme, upsertTheme } from "../context/theme" import { hasTheme, upsertTheme } from "../context/theme"
import { Global } from "@/global" import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { Process } from "@/util/process" import { Process } from "@/util"
import { Flock } from "@opencode-ai/shared/util/flock" import { Flock } from "@opencode-ai/shared/util/flock"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
@@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2" import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useDialog } from "../../ui/dialog" import { useDialog } from "../../ui/dialog"
@@ -29,7 +29,7 @@ export function DialogMessage(props: {
const msg = message() const msg = message()
if (!msg) return if (!msg) return
sdk.client.session.revert({ void sdk.client.session.revert({
sessionID: props.sessionID, sessionID: props.sessionID,
messageID: msg.id, messageID: msg.id,
}) })
@@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2" import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { DialogMessage } from "./dialog-message" import { DialogMessage } from "./dialog-message"
import { useDialog } from "../../ui/dialog" import { useDialog } from "../../ui/dialog"
import type { PromptInfo } from "../../component/prompt/history" import type { PromptInfo } from "../../component/prompt/history"
@@ -33,7 +33,7 @@ import type {
ReasoningPart, ReasoningPart,
} from "@opencode-ai/sdk/v2" } from "@opencode-ai/sdk/v2"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import type { Tool } from "@/tool/tool" import type { Tool } from "@/tool/tool"
import type { ReadTool } from "@/tool/read" import type { ReadTool } from "@/tool/read"
import type { WriteTool } from "@/tool/write" import type { WriteTool } from "@/tool/write"
@@ -73,7 +73,7 @@ import { Editor } from "../../util/editor"
import stripAnsi from "strip-ansi" import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt" import { usePromptRef } from "../../context/prompt"
import { useExit } from "../../context/exit" import { useExit } from "../../context/exit"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { Global } from "@/global" import { Global } from "@/global"
import { PermissionPrompt } from "./permission" import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question" import { QuestionPrompt } from "./question"
@@ -241,7 +241,7 @@ export function Session() {
if (kv.get(GO_UPSELL_DONT_SHOW)) return if (kv.get(GO_UPSELL_DONT_SHOW)) return
DialogGoUpsell.show(dialog).then((dontShowAgain) => { void DialogGoUpsell.show(dialog).then((dontShowAgain) => {
if (dontShowAgain) kv.set(GO_UPSELL_DONT_SHOW, true) if (dontShowAgain) kv.set(GO_UPSELL_DONT_SHOW, true)
kv.set(GO_UPSELL_LAST_SEEN_AT, Date.now()) kv.set(GO_UPSELL_LAST_SEEN_AT, Date.now())
}) })
@@ -272,7 +272,7 @@ export function Session() {
useKeyboard((evt) => { useKeyboard((evt) => {
if (!session()?.parentID) return if (!session()?.parentID) return
if (keybind.match("app_exit", evt)) { if (keybind.match("app_exit", evt)) {
exit() void exit()
} }
}) })
@@ -483,7 +483,7 @@ export function Session() {
}) })
return return
} }
sdk.client.session.summarize({ void sdk.client.session.summarize({
sessionID: route.sessionID, sessionID: route.sessionID,
modelID: selectedModel.modelID, modelID: selectedModel.modelID,
providerID: selectedModel.providerID, providerID: selectedModel.providerID,
@@ -529,7 +529,7 @@ export function Session() {
const revert = session()?.revert?.messageID const revert = session()?.revert?.messageID
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user") const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
if (!message) return if (!message) return
sdk.client.session void sdk.client.session
.revert({ .revert({
sessionID: route.sessionID, sessionID: route.sessionID,
messageID: message.id, messageID: message.id,
@@ -568,13 +568,13 @@ export function Session() {
if (!messageID) return if (!messageID) return
const message = messages().find((x) => x.role === "user" && x.id > messageID) const message = messages().find((x) => x.role === "user" && x.id > messageID)
if (!message) { if (!message) {
sdk.client.session.unrevert({ void sdk.client.session.unrevert({
sessionID: route.sessionID, sessionID: route.sessionID,
}) })
prompt?.set({ input: "", parts: [] }) prompt?.set({ input: "", parts: [] })
return return
} }
sdk.client.session.revert({ void sdk.client.session.revert({
sessionID: route.sessionID, sessionID: route.sessionID,
messageID: message.id, messageID: message.id,
}) })
@@ -1966,7 +1966,7 @@ function Task(props: ToolProps<typeof TaskTool>) {
onMount(() => { onMount(() => {
if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length) if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length)
sync.session.sync(props.metadata.sessionId) void sync.session.sync(props.metadata.sessionId)
}) })
const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? []) const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? [])
@@ -11,8 +11,8 @@ import { useSync } from "../../context/sync"
import { useTextareaKeybindings } from "../../component/textarea-keybindings" import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import path from "path" import path from "path"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { Global } from "@/global" import { Global } from "@/global"
import { useDialog } from "../../ui/dialog" import { useDialog } from "../../ui/dialog"
import { getScrollAcceleration } from "../../util/scroll" import { getScrollAcceleration } from "../../util/scroll"
@@ -184,7 +184,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
onSelect={(option) => { onSelect={(option) => {
setStore("stage", "permission") setStore("stage", "permission")
if (option === "cancel") return if (option === "cancel") return
sdk.client.permission.reply({ void sdk.client.permission.reply({
reply: "always", reply: "always",
requestID: props.request.id, requestID: props.request.id,
}) })
@@ -194,7 +194,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
<Match when={store.stage === "reject"}> <Match when={store.stage === "reject"}>
<RejectPrompt <RejectPrompt
onConfirm={(message) => { onConfirm={(message) => {
sdk.client.permission.reply({ void sdk.client.permission.reply({
reply: "reject", reply: "reject",
requestID: props.request.id, requestID: props.request.id,
message: message || undefined, message: message || undefined,
@@ -447,13 +447,13 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
setStore("stage", "reject") setStore("stage", "reject")
return return
} }
sdk.client.permission.reply({ void sdk.client.permission.reply({
reply: "reject", reply: "reject",
requestID: props.request.id, requestID: props.request.id,
}) })
return return
} }
sdk.client.permission.reply({ void sdk.client.permission.reply({
reply: "once", reply: "once",
requestID: props.request.id, requestID: props.request.id,
}) })
@@ -45,14 +45,14 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
function submit() { function submit() {
const answers = questions().map((_, i) => store.answers[i] ?? []) const answers = questions().map((_, i) => store.answers[i] ?? [])
sdk.client.question.reply({ void sdk.client.question.reply({
requestID: props.request.id, requestID: props.request.id,
answers, answers,
}) })
} }
function reject() { function reject() {
sdk.client.question.reject({ void sdk.client.question.reject({
requestID: props.request.id, requestID: props.request.id,
}) })
} }
@@ -67,7 +67,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
setStore("custom", inputs) setStore("custom", inputs)
} }
if (single()) { if (single()) {
sdk.client.question.reply({ void sdk.client.question.reply({
requestID: props.request.id, requestID: props.request.id,
answers: [[answer]], answers: [[answer]],
}) })
@@ -6,7 +6,7 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command" import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind" import { useKeybind } from "../../context/keybind"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { useTerminalDimensions } from "@opentui/solid" import { useTerminalDimensions } from "@opentui/solid"
export function SubagentFooter() { export function SubagentFooter() {
+4 -4
View File
@@ -1,19 +1,19 @@
import { cmd } from "@/cli/cmd/cmd" import { cmd } from "@/cli/cmd/cmd"
import { tui } from "./app" import { tui } from "./app"
import { Rpc } from "@/util/rpc" import { Rpc } from "@/util"
import { type rpc } from "./worker" import { type rpc } from "./worker"
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { UI } from "@/cli/ui" import { UI } from "@/cli/ui"
import { Log } from "@/util/log" import { Log } from "@/util"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
import { withTimeout } from "@/util/timeout" import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import type { GlobalEvent } from "@opencode-ai/sdk/v2" import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { writeHeapSnapshot } from "v8" import { writeHeapSnapshot } from "v8"
@@ -4,7 +4,7 @@ import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { For } from "solid-js" import { For } from "solid-js"
import { useKeyboard } from "@opentui/solid" import { useKeyboard } from "@opentui/solid"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
export type DialogConfirmProps = { export type DialogConfirmProps = {
title: string title: string
@@ -8,8 +8,8 @@ import * as fuzzysort from "fuzzysort"
import { isDeepEqual } from "remeda" import { isDeepEqual } from "remeda"
import { useDialog, type DialogContext } from "@tui/ui/dialog" import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { useKeybind } from "@tui/context/keybind" import { useKeybind } from "@tui/context/keybind"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import { getScrollAcceleration } from "../util/scroll" import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../context/tui-config" import { useTuiConfig } from "../context/tui-config"
@@ -4,8 +4,8 @@ import { lazy } from "../../../../util/lazy.js"
import { tmpdir } from "os" import { tmpdir } from "os"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { Filesystem } from "../../../../util/filesystem" import { Filesystem } from "../../../../util"
import { Process } from "../../../../util/process" import { Process } from "../../../../util"
import { which } from "../../../../util/which" import { which } from "../../../../util/which"
/** /**
@@ -3,8 +3,8 @@ import { rm } from "node:fs/promises"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { join } from "node:path" import { join } from "node:path"
import { CliRenderer } from "@opentui/core" import { CliRenderer } from "@opentui/core"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { Process } from "@/util/process" import { Process } from "@/util"
export namespace Editor { export namespace Editor {
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> { export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
@@ -1,5 +1,5 @@
import { MacOSScrollAccel, type ScrollAcceleration } from "@opentui/core" import { MacOSScrollAccel, type ScrollAcceleration } from "@opentui/core"
import type { TuiConfig } from "@/config/tui" import type { TuiConfig } from "@/config"
export class CustomSpeedScroll implements ScrollAcceleration { export class CustomSpeedScroll implements ScrollAcceleration {
constructor(private speed: number) {} constructor(private speed: number) {}
@@ -2,7 +2,7 @@ import { Player } from "cli-sound"
import { mkdirSync } from "node:fs" import { mkdirSync } from "node:fs"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { basename, join } from "node:path" import { basename, join } from "node:path"
import { Process } from "@/util/process" import { Process } from "@/util"
import { which } from "@/util/which" import { which } from "@/util/which"
import pulseA from "../asset/pulse-a.wav" with { type: "file" } import pulseA from "../asset/pulse-a.wav" with { type: "file" }
import pulseB from "../asset/pulse-b.wav" with { type: "file" } import pulseB from "../asset/pulse-b.wav" with { type: "file" }
@@ -1,5 +1,5 @@
import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util/locale" import { Locale } from "@/util"
import * as Model from "./model" import * as Model from "./model"
export type TranscriptOptions = { export type TranscriptOptions = {
+2 -2
View File
@@ -1,9 +1,9 @@
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { Server } from "@/server/server" import { Server } from "@/server/server"
import { Log } from "@/util/log" import { Log } from "@/util"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceBootstrap } from "@/project/bootstrap"
import { Rpc } from "@/util/rpc" import { Rpc } from "@/util"
import { upgrade } from "@/cli/upgrade" import { upgrade } from "@/cli/upgrade"
import { Config } from "@/config" import { Config } from "@/config"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
+2 -2
View File
@@ -7,8 +7,8 @@ import { Global } from "../../global"
import fs from "fs/promises" import fs from "fs/promises"
import path from "path" import path from "path"
import os from "os" import os from "os"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util"
import { Process } from "../../util/process" import { Process } from "../../util"
interface UninstallArgs { interface UninstallArgs {
keepConfig: boolean keepConfig: boolean
+1 -1
View File
@@ -1,5 +1,5 @@
import { AccountServiceError, AccountTransportError } from "@/account" import { AccountServiceError, AccountTransportError } from "@/account"
import { ConfigMarkdown } from "@/config/markdown" import { ConfigMarkdown } from "@/config"
import { errorFormat } from "@/util/error" import { errorFormat } from "@/util/error"
import { Config } from "../config" import { Config } from "../config"
import { MCP } from "../mcp" import { MCP } from "../mcp"
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path"
import { writeHeapSnapshot } from "node:v8" import { writeHeapSnapshot } from "node:v8"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Global } from "@/global" import { Global } from "@/global"
import { Log } from "@/util/log" import { Log } from "@/util"
const log = Log.create({ service: "heap" }) const log = Log.create({ service: "heap" })
const MINUTE = 60_000 const MINUTE = 60_000
+8 -8
View File
@@ -1,8 +1,8 @@
import { Log } from "../util/log" import { Log } from "../util"
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import os from "os" import os from "os"
import { Process } from "../util/process" import { Process } from "../util"
import z from "zod" import z from "zod"
import { mergeDeep, pipe, unique } from "remeda" import { mergeDeep, pipe, unique } from "remeda"
import { Global } from "../global" import { Global } from "../global"
@@ -19,9 +19,9 @@ import {
printParseErrorCode, printParseErrorCode,
} from "jsonc-parser" } from "jsonc-parser"
import { Instance, type InstanceContext } from "../project/instance" import { Instance, type InstanceContext } from "../project/instance"
import { LSPServer } from "../lsp/server" import * as LSPServer from "../lsp/server"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { ConfigMarkdown } from "./markdown" import * as ConfigMarkdown from "./markdown"
import { existsSync } from "fs" import { existsSync } from "fs"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
@@ -29,7 +29,7 @@ import { Event } from "../server/event"
import { Glob } from "@opencode-ai/shared/util/glob" import { Glob } from "@opencode-ai/shared/util/glob"
import { Account } from "@/account" import { Account } from "@/account"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import { ConfigPaths } from "./paths" import * as ConfigPaths from "./paths"
import type { ConsoleState } from "./console-state" import type { ConsoleState } from "./console-state"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
@@ -171,7 +171,7 @@ async function loadCommand(dir: string) {
? err.data.message ? err.data.message
: `Failed to parse command ${item}` : `Failed to parse command ${item}`
const { Session } = await import("@/session") const { Session } = await import("@/session")
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load command", { command: item, err }) log.error("failed to load command", { command: item, err })
return undefined return undefined
}) })
@@ -210,7 +210,7 @@ async function loadAgent(dir: string) {
? err.data.message ? err.data.message
: `Failed to parse agent ${item}` : `Failed to parse agent ${item}`
const { Session } = await import("@/session") const { Session } = await import("@/session")
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load agent", { agent: item, err }) log.error("failed to load agent", { agent: item, err })
return undefined return undefined
}) })
@@ -248,7 +248,7 @@ async function loadMode(dir: string) {
? err.data.message ? err.data.message
: `Failed to parse mode ${item}` : `Failed to parse mode ${item}`
const { Session } = await import("@/session") const { Session } = await import("@/session")
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load mode", { mode: item, err }) log.error("failed to load mode", { mode: item, err })
return undefined return undefined
}) })
+3
View File
@@ -1 +1,4 @@
export * as Config from "./config" export * as Config from "./config"
export * as ConfigMarkdown from "./markdown"
export * as ConfigPaths from "./paths"
export * as TuiConfig from "./tui"
+91 -93
View File
@@ -1,99 +1,97 @@
import { NamedError } from "@opencode-ai/shared/util/error" import { NamedError } from "@opencode-ai/shared/util/error"
import matter from "gray-matter" import matter from "gray-matter"
import { z } from "zod" import { z } from "zod"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util"
export namespace ConfigMarkdown { export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g export const SHELL_REGEX = /!`([^`]+)`/g
export const SHELL_REGEX = /!`([^`]+)`/g
export function files(template: string) { export function files(template: string) {
return Array.from(template.matchAll(FILE_REGEX)) return Array.from(template.matchAll(FILE_REGEX))
}
export function shell(template: string) {
return Array.from(template.matchAll(SHELL_REGEX))
}
// other coding agents like claude code allow invalid yaml in their
// frontmatter, we need to fallback to a more permissive parser for those cases
export function fallbackSanitization(content: string): string {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
if (!match) return content
const frontmatter = match[1]
const lines = frontmatter.split(/\r?\n/)
const result: string[] = []
for (const line of lines) {
// skip comments and empty lines
if (line.trim().startsWith("#") || line.trim() === "") {
result.push(line)
continue
}
// skip lines that are continuations (indented)
if (line.match(/^\s+/)) {
result.push(line)
continue
}
// match key: value pattern
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
if (!kvMatch) {
result.push(line)
continue
}
const key = kvMatch[1]
const value = kvMatch[2].trim()
// skip if value is empty, already quoted, or uses block scalar
if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
result.push(line)
continue
}
// if value contains a colon, convert to block scalar
if (value.includes(":")) {
result.push(`${key}: |-`)
result.push(` ${value}`)
continue
}
result.push(line)
}
const processed = result.join("\n")
return content.replace(frontmatter, () => processed)
}
export async function parse(filePath: string) {
const template = await Filesystem.readText(filePath)
try {
const md = matter(template)
return md
} catch {
try {
return matter(fallbackSanitization(template))
} catch (err) {
throw new FrontmatterError(
{
path: filePath,
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
},
{ cause: err },
)
}
}
}
export const FrontmatterError = NamedError.create(
"ConfigFrontmatterError",
z.object({
path: z.string(),
message: z.string(),
}),
)
} }
export function shell(template: string) {
return Array.from(template.matchAll(SHELL_REGEX))
}
// other coding agents like claude code allow invalid yaml in their
// frontmatter, we need to fallback to a more permissive parser for those cases
export function fallbackSanitization(content: string): string {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
if (!match) return content
const frontmatter = match[1]
const lines = frontmatter.split(/\r?\n/)
const result: string[] = []
for (const line of lines) {
// skip comments and empty lines
if (line.trim().startsWith("#") || line.trim() === "") {
result.push(line)
continue
}
// skip lines that are continuations (indented)
if (line.match(/^\s+/)) {
result.push(line)
continue
}
// match key: value pattern
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
if (!kvMatch) {
result.push(line)
continue
}
const key = kvMatch[1]
const value = kvMatch[2].trim()
// skip if value is empty, already quoted, or uses block scalar
if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
result.push(line)
continue
}
// if value contains a colon, convert to block scalar
if (value.includes(":")) {
result.push(`${key}: |-`)
result.push(` ${value}`)
continue
}
result.push(line)
}
const processed = result.join("\n")
return content.replace(frontmatter, () => processed)
}
export async function parse(filePath: string) {
const template = await Filesystem.readText(filePath)
try {
const md = matter(template)
return md
} catch {
try {
return matter(fallbackSanitization(template))
} catch (err) {
throw new FrontmatterError(
{
path: filePath,
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
},
{ cause: err },
)
}
}
}
export const FrontmatterError = NamedError.create(
"ConfigFrontmatterError",
z.object({
path: z.string(),
message: z.string(),
}),
)
+147 -149
View File
@@ -3,165 +3,163 @@ import os from "os"
import z from "zod" import z from "zod"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
import { NamedError } from "@opencode-ai/shared/util/error" import { NamedError } from "@opencode-ai/shared/util/error"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Global } from "@/global" import { Global } from "@/global"
export namespace ConfigPaths { export async function projectFiles(name: string, directory: string, worktree: string) {
export async function projectFiles(name: string, directory: string, worktree: string) { return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true }) }
}
export async function directories(directory: string, worktree: string) {
return [
Global.Path.config,
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: directory,
stop: worktree,
}),
)
: []),
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
}),
)),
...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
]
}
export function fileInDirectory(dir: string, name: string) {
return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)]
}
export const JsonError = NamedError.create(
"ConfigJsonError",
z.object({
path: z.string(),
message: z.string().optional(),
}),
)
export const InvalidError = NamedError.create(
"ConfigInvalidError",
z.object({
path: z.string(),
issues: z.custom<z.core.$ZodIssue[]>().optional(),
message: z.string().optional(),
}),
)
/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
export async function readFile(filepath: string) {
return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
if (err.code === "ENOENT") return
throw new JsonError({ path: filepath }, { cause: err })
})
}
type ParseSource = string | { source: string; dir: string }
function source(input: ParseSource) {
return typeof input === "string" ? input : input.source
}
function dir(input: ParseSource) {
return typeof input === "string" ? path.dirname(input) : input.dir
}
/** Apply {env:VAR} and {file:path} substitutions to config text. */
async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
if (!fileMatches.length) return text
const configDir = dir(input)
const configSource = source(input)
let out = ""
let cursor = 0
for (const match of fileMatches) {
const token = match[0]
const index = match.index!
out += text.slice(cursor, index)
const lineStart = text.lastIndexOf("\n", index - 1) + 1
const prefix = text.slice(lineStart, index).trimStart()
if (prefix.startsWith("//")) {
out += token
cursor = index + token.length
continue
}
let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
export async function directories(directory: string, worktree: string) { const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
return [ const fileContent = (
Global.Path.config, await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG if (missing === "empty") return ""
? await Array.fromAsync(
Filesystem.up({ const errMsg = `bad file reference: "${token}"`
targets: [".opencode"], if (error.code === "ENOENT") {
start: directory, throw new InvalidError(
stop: worktree, {
}), path: configSource,
message: errMsg + ` ${resolvedPath} does not exist`,
},
{ cause: error },
) )
: []), }
...(await Array.fromAsync( throw new InvalidError({ path: configSource, message: errMsg }, { cause: error })
Filesystem.up({ })
targets: [".opencode"], ).trim()
start: Global.Path.home,
stop: Global.Path.home,
}),
)),
...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
]
}
export function fileInDirectory(dir: string, name: string) { out += JSON.stringify(fileContent).slice(1, -1)
return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)] cursor = index + token.length
} }
export const JsonError = NamedError.create( out += text.slice(cursor)
"ConfigJsonError", return out
z.object({ }
path: z.string(),
message: z.string().optional(), /** Substitute and parse JSONC text, throwing JsonError on syntax errors. */
}), export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
) const configSource = source(input)
text = await substitute(text, input, missing)
export const InvalidError = NamedError.create(
"ConfigInvalidError", const errors: JsoncParseError[] = []
z.object({ const data = parseJsonc(text, errors, { allowTrailingComma: true })
path: z.string(), if (errors.length) {
issues: z.custom<z.core.$ZodIssue[]>().optional(), const lines = text.split("\n")
message: z.string().optional(), const errorDetails = errors
}), .map((e) => {
) const beforeOffset = text.substring(0, e.offset).split("\n")
const line = beforeOffset.length
/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */ const column = beforeOffset[beforeOffset.length - 1].length + 1
export async function readFile(filepath: string) { const problemLine = lines[line - 1]
return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
if (err.code === "ENOENT") return const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
throw new JsonError({ path: filepath }, { cause: err }) if (!problemLine) return error
})
} return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
})
type ParseSource = string | { source: string; dir: string } .join("\n")
function source(input: ParseSource) { throw new JsonError({
return typeof input === "string" ? input : input.source path: configSource,
} message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
function dir(input: ParseSource) {
return typeof input === "string" ? path.dirname(input) : input.dir
}
/** Apply {env:VAR} and {file:path} substitutions to config text. */
async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
}) })
const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
if (!fileMatches.length) return text
const configDir = dir(input)
const configSource = source(input)
let out = ""
let cursor = 0
for (const match of fileMatches) {
const token = match[0]
const index = match.index!
out += text.slice(cursor, index)
const lineStart = text.lastIndexOf("\n", index - 1) + 1
const prefix = text.slice(lineStart, index).trimStart()
if (prefix.startsWith("//")) {
out += token
cursor = index + token.length
continue
}
let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = (
await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
if (missing === "empty") return ""
const errMsg = `bad file reference: "${token}"`
if (error.code === "ENOENT") {
throw new InvalidError(
{
path: configSource,
message: errMsg + ` ${resolvedPath} does not exist`,
},
{ cause: error },
)
}
throw new InvalidError({ path: configSource, message: errMsg }, { cause: error })
})
).trim()
out += JSON.stringify(fileContent).slice(1, -1)
cursor = index + token.length
}
out += text.slice(cursor)
return out
} }
/** Substitute and parse JSONC text, throwing JsonError on syntax errors. */ return data
export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
const configSource = source(input)
text = await substitute(text, input, missing)
const errors: JsoncParseError[] = []
const data = parseJsonc(text, errors, { allowTrailingComma: true })
if (errors.length) {
const lines = text.split("\n")
const errorDetails = errors
.map((e) => {
const beforeOffset = text.substring(0, e.offset).split("\n")
const line = beforeOffset.length
const column = beforeOffset[beforeOffset.length - 1].length + 1
const problemLine = lines[line - 1]
const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
if (!problemLine) return error
return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
})
.join("\n")
throw new JsonError({
path: configSource,
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
})
}
return data
}
} }
+3 -3
View File
@@ -2,12 +2,12 @@ import path from "path"
import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser" import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser"
import { unique } from "remeda" import { unique } from "remeda"
import z from "zod" import z from "zod"
import { ConfigPaths } from "./paths" import * as ConfigPaths from "./paths"
import { TuiInfo, TuiOptions } from "./tui-schema" import { TuiInfo, TuiOptions } from "./tui-schema"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Log } from "@/util/log" import { Log } from "@/util"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util"
import { Global } from "@/global" import { Global } from "@/global"
const log = Log.create({ service: "tui.migrate" }) const log = Log.create({ service: "tui.migrate" })
+1 -1
View File
@@ -1,5 +1,5 @@
import z from "zod" import z from "zod"
import { Config } from "." import * as Config from "./config"
const KeybindOverride = z const KeybindOverride = z
.object( .object(

Some files were not shown because too many files have changed in this diff Show More