From d8070f7f23f44ee986510532e5fa4cbcee7a6f07 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 11:27:54 -0400 Subject: [PATCH] refactor(opencode): move tui state io to AppFileSystem --- .../cli/cmd/tui/component/prompt/frecency.tsx | 26 ++++++++++++---- .../cli/cmd/tui/component/prompt/history.tsx | 26 ++++++++++++---- .../cli/cmd/tui/component/prompt/stash.tsx | 30 ++++++++++++++----- .../opencode/src/cli/cmd/tui/context/kv.tsx | 24 ++++++++++++--- .../src/cli/cmd/tui/context/local.tsx | 22 ++++++++++++-- .../src/cli/cmd/tui/routes/session/index.tsx | 17 ++++++++--- .../src/cli/cmd/tui/util/clipboard.ts | 13 ++++++-- .../opencode/src/cli/cmd/tui/util/editor.ts | 22 ++++++++++++-- 8 files changed, 146 insertions(+), 34 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx index 3ea8826ef8..afb7d5c92a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx @@ -1,10 +1,12 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Effect } from "effect" import { onMount } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "../../context/helper" -import { appendFile, writeFile } from "fs/promises" +import { appendFile } from "fs/promises" function calculateFrecency(entry?: { frequency: number; lastOpen: number }): number { if (!entry) return 0 @@ -19,8 +21,22 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont name: "Frecency", init: () => { const frecencyPath = path.join(Global.Path.state, "frecency.jsonl") + const read = () => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readFileString(frecencyPath) + }), + ) + const write = (content: string) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(frecencyPath, content) + }), + ) onMount(async () => { - const text = await Filesystem.readText(frecencyPath).catch(() => "") + const text = await read().catch(() => "") const lines = text .split("\n") .filter(Boolean) @@ -54,7 +70,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont if (sorted.length > 0) { const content = sorted.map((entry) => JSON.stringify(entry)).join("\n") + "\n" - writeFile(frecencyPath, content).catch(() => {}) + write(content).catch(() => {}) } }) @@ -77,7 +93,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont .slice(0, MAX_FRECENCY_ENTRIES) setStore("data", Object.fromEntries(sorted)) const content = sorted.map(([path, entry]) => JSON.stringify({ path, ...entry })).join("\n") + "\n" - writeFile(frecencyPath, content).catch(() => {}) + write(content).catch(() => {}) } } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index d49dd5c7b6..03c5fde505 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -1,10 +1,12 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Effect } from "effect" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import { createSimpleContext } from "../../context/helper" -import { appendFile, writeFile } from "fs/promises" +import { appendFile } from "fs/promises" import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2" export type PromptInfo = { @@ -31,8 +33,22 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create name: "PromptHistory", init: () => { const historyPath = path.join(Global.Path.state, "prompt-history.jsonl") + const read = () => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readFileString(historyPath) + }), + ) + const write = (content: string) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(historyPath, content) + }), + ) onMount(async () => { - const text = await Filesystem.readText(historyPath).catch(() => "") + const text = await read().catch(() => "") const lines = text .split("\n") .filter(Boolean) @@ -51,7 +67,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create // Rewrite file with only valid entries to self-heal corruption if (lines.length > 0) { const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(historyPath, content).catch(() => {}) + write(content).catch(() => {}) } }) @@ -97,7 +113,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create if (trimmed) { const content = store.history.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(historyPath, content).catch(() => {}) + write(content).catch(() => {}) return } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx index ef3eb329a9..93f6e8e9d7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx @@ -1,10 +1,12 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Effect } from "effect" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import { createSimpleContext } from "../../context/helper" -import { appendFile, writeFile } from "fs/promises" +import { appendFile } from "fs/promises" import type { PromptInfo } from "./history" export type StashEntry = { @@ -19,8 +21,22 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp name: "PromptStash", init: () => { const stashPath = path.join(Global.Path.state, "prompt-stash.jsonl") + const read = () => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readFileString(stashPath) + }), + ) + const write = (content: string) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(stashPath, content) + }), + ) onMount(async () => { - const text = await Filesystem.readText(stashPath).catch(() => "") + const text = await read().catch(() => "") const lines = text .split("\n") .filter(Boolean) @@ -39,7 +55,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp // Rewrite file with only valid entries to self-heal corruption if (lines.length > 0) { const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(stashPath, content).catch(() => {}) + write(content).catch(() => {}) } }) @@ -66,7 +82,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp if (trimmed) { const content = store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(stashPath, content).catch(() => {}) + write(content).catch(() => {}) return } @@ -82,7 +98,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp ) const content = store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "" - writeFile(stashPath, content).catch(() => {}) + write(content).catch(() => {}) return entry }, remove(index: number) { @@ -94,7 +110,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp ) const content = store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "" - writeFile(stashPath, content).catch(() => {}) + write(content).catch(() => {}) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 7a52156f88..639da36378 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,5 +1,7 @@ import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Effect } from "effect" import { createSignal, type Setter } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" @@ -11,10 +13,24 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ const [ready, setReady] = createSignal(false) const [store, setStore] = createStore>() const filePath = path.join(Global.Path.state, "kv.json") + const read = () => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readJson(filePath) + }), + ) + const write = (data: unknown) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeJson(filePath, data) + }), + ) - Filesystem.readJson(filePath) + read() .then((x) => { - setStore(x) + if (typeof x === "object" && x !== null) setStore(x as Record) }) .catch(() => {}) .finally(() => { @@ -44,7 +60,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ }, set(key: string, value: any) { setStore(key, value) - Filesystem.writeJson(filePath, store) + write(store) }, } return result diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index ec3931b209..e1a28745cb 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -1,18 +1,20 @@ import { createStore } from "solid-js/store" import { batch, createEffect, createMemo } from "solid-js" +import { Effect } from "effect" import { useSync } from "@tui/context/sync" import { useTheme } from "@tui/context/theme" import { uniqueBy } from "remeda" import path from "path" import { Global } from "@/global" +import { AppRuntime } from "@/effect/app-runtime" import { iife } from "@/util/iife" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { createSimpleContext } from "./helper" import { useToast } from "../ui/toast" import { Provider } from "@/provider/provider" import { useArgs } from "./args" import { useSDK } from "./sdk" import { RGBA } from "@opentui/core" -import { Filesystem } from "@/util/filesystem" export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", @@ -124,6 +126,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const state = { pending: false, } + const read = () => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readJson(filePath) + }), + ) + const write = (data: unknown) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeJson(filePath, data) + }), + ) function save() { if (!modelStore.ready) { @@ -131,14 +147,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return } state.pending = false - Filesystem.writeJson(filePath, { + write({ recent: modelStore.recent, favorite: modelStore.favorite, variant: modelStore.variant, }) } - Filesystem.readJson(filePath) + read() .then((x: any) => { if (Array.isArray(x.recent)) setModelStore("recent", x.recent) if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index c7790006f4..4d5d3adf31 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -74,8 +74,9 @@ import { Editor } from "../../util/editor" import stripAnsi from "strip-ansi" import { usePromptRef } from "../../context/prompt" import { useExit } from "../../context/exit" -import { Filesystem } from "@/util/filesystem" import { Global } from "@/global" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { PermissionPrompt } from "./permission" import { QuestionPrompt } from "./question" import { DialogExportOptions } from "../../ui/dialog-export-options" @@ -87,6 +88,7 @@ import { getScrollAcceleration } from "../../util/scroll" import { TuiPluginRuntime } from "../../plugin" import { DialogGoUpsell } from "../../component/dialog-go-upsell" import { SessionRetry } from "@/session/retry" +import { Effect } from "effect" addDefaultParsers(parsers.parsers) @@ -915,13 +917,20 @@ export function Session() { const exportDir = process.cwd() const filename = options.filename.trim() const filepath = path.join(exportDir, filename) + const write = (content: string) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(filepath, content) + }), + ) - await Filesystem.write(filepath, transcript) + await write(transcript) // Open with EDITOR if available const result = await Editor.open({ value: transcript, renderer }) if (result !== undefined) { - await Filesystem.write(filepath, result) + await write(result) } toast.show({ message: `Session exported to ${filename}`, variant: "success" }) @@ -2236,7 +2245,7 @@ function Skill(props: ToolProps) { function Diagnostics(props: { diagnostics?: Record[]>; filePath: string }) { const { theme } = useTheme() const errors = createMemo(() => { - const normalized = Filesystem.normalizePath(props.filePath) + const normalized = AppFileSystem.normalizePath(props.filePath) const arr = props.diagnostics?.[normalized] ?? [] return arr.filter((x) => x.severity === 1).slice(0, 3) }) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 87c0a63abc..df5786ed9a 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -1,10 +1,12 @@ import { platform, release } from "os" import clipboardy from "clipboardy" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Effect } from "effect" import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" import fs from "fs/promises" -import { Filesystem } from "../../../../util/filesystem" import { Process } from "../../../../util/process" import { which } from "../../../../util/which" @@ -58,8 +60,13 @@ export namespace Clipboard { ], { nothrow: true }, ) - const buffer = await Filesystem.readBytes(tmpfile) - return { data: buffer.toString("base64"), mime: "image/png" } + const buffer = await AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readFile(tmpfile) + }), + ) + return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" } } catch { } finally { await fs.rm(tmpfile, { force: true }).catch(() => {}) diff --git a/packages/opencode/src/cli/cmd/tui/util/editor.ts b/packages/opencode/src/cli/cmd/tui/util/editor.ts index 9eaae99fce..733d4c6a79 100644 --- a/packages/opencode/src/cli/cmd/tui/util/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/editor.ts @@ -1,9 +1,11 @@ import { defer } from "@/util/defer" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Effect } from "effect" import { rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { CliRenderer } from "@opentui/core" -import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" export namespace Editor { @@ -12,9 +14,23 @@ export namespace Editor { if (!editor) return const filepath = join(tmpdir(), `${Date.now()}.md`) + const write = (content: string) => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(filepath, content) + }), + ) + const read = () => + AppRuntime.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* fs.readFileString(filepath) + }), + ) await using _ = defer(async () => rm(filepath, { force: true })) - await Filesystem.write(filepath, opts.value) + await write(opts.value) opts.renderer.suspend() opts.renderer.currentRenderBuffer.clear() try { @@ -26,7 +42,7 @@ export namespace Editor { shell: process.platform === "win32", }) await proc.exited - const content = await Filesystem.readText(filepath) + const content = await read() return content || undefined } finally { opts.renderer.currentRenderBuffer.clear()