From 3c32013eb122d794089e011d2ec7077395d6f1c4 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 Mar 2026 17:11:34 +0800 Subject: [PATCH 001/155] fix: preserve image attachments when selecting slash commands (#19771) --- packages/app/src/components/prompt-input.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 1cc7c578d3..c8f72b8d2f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -624,17 +624,18 @@ export const PromptInput: Component = (props) => { if (!cmd) return promptProbe.select(cmd.id) closePopover() + const images = imageAttachments() if (cmd.type === "custom") { const text = `/${cmd.trigger} ` setEditorText(text) - prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + prompt.set([{ type: "text", content: text, start: 0, end: text.length }, ...images], text.length) focusEditorEnd() return } clearEditor() - prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0) + prompt.set([...DEFAULT_PROMPT, ...images], 0) command.trigger(cmd.id, "slash") } From 8e4bab51812fccf3b69713904159a4394b3a29ab Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 30 Mar 2026 13:51:07 +0200 Subject: [PATCH 002/155] update plugin themes when plugin was updated (#20052) --- packages/opencode/specs/tui-plugins.md | 4 +- .../src/cli/cmd/tui/context/theme.tsx | 12 ++ .../src/cli/cmd/tui/plugin/runtime.ts | 110 +++++++++++++----- packages/opencode/src/plugin/meta.ts | 37 ++++-- packages/opencode/src/util/filesystem.ts | 9 +- .../test/cli/tui/plugin-loader.test.ts | 103 ++++++++++++++++ 6 files changed, 237 insertions(+), 38 deletions(-) diff --git a/packages/opencode/specs/tui-plugins.md b/packages/opencode/specs/tui-plugins.md index 31edcf114a..5a7caa75b9 100644 --- a/packages/opencode/specs/tui-plugins.md +++ b/packages/opencode/specs/tui-plugins.md @@ -269,7 +269,9 @@ Theme install behavior: - Relative theme paths are resolved from the plugin root. - Theme name is the JSON basename. -- Install is skipped if that theme name already exists. +- First install writes only when the destination file is missing. +- If the theme name already exists, install is skipped unless plugin metadata state is `updated`. +- On `updated`, host only rewrites themes previously tracked for that plugin and only when source `mtime`/`size` changed. - Local plugins persist installed themes under the local `.opencode/themes` area near the plugin config source. - Global plugins persist installed themes under the global `themes` dir. - Invalid or unreadable theme files are ignored. diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index dcef2cb466..4857f7a4d2 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -183,6 +183,18 @@ export function addTheme(name: string, theme: unknown) { return true } +export function upsertTheme(name: string, theme: unknown) { + if (!name) return false + if (!isTheme(theme)) return false + if (customThemes[name] !== undefined) { + customThemes[name] = theme + } else { + pluginThemes[name] = theme + } + syncThemes() + return true +} + export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { const defs = theme.defs ?? {} function resolveColor(c: ColorValue, chain: string[] = []): RGBA { diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 0e1674bdac..e992577a6e 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -31,7 +31,7 @@ import { } from "@/plugin/shared" import { PluginMeta } from "@/plugin/meta" import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install" -import { addTheme, hasTheme } from "../context/theme" +import { hasTheme, upsertTheme } from "../context/theme" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" @@ -49,7 +49,8 @@ type PluginLoad = { source: PluginSource | "internal" id: string module: TuiPluginModule - install_theme: TuiTheme["install"] + theme_meta: TuiConfig.PluginMeta + theme_root: string } type Api = HostPluginApi @@ -64,6 +65,7 @@ type PluginEntry = { id: string load: PluginLoad meta: TuiPluginMeta + themes: Record plugin: TuiPlugin options: Config.PluginOptions | undefined enabled: boolean @@ -143,12 +145,54 @@ function resolveRoot(root: string) { return path.resolve(process.cwd(), root) } -function createThemeInstaller(meta: TuiConfig.PluginMeta, root: string, spec: string): TuiTheme["install"] { +function createThemeInstaller( + meta: TuiConfig.PluginMeta, + root: string, + spec: string, + plugin: PluginEntry, +): TuiTheme["install"] { return async (file) => { const raw = file.startsWith("file://") ? fileURLToPath(file) : file const src = path.isAbsolute(raw) ? raw : path.resolve(root, raw) - const theme = path.basename(src, path.extname(src)) - if (hasTheme(theme)) return + const name = path.basename(src, path.extname(src)) + const source_dir = path.dirname(meta.source) + const local_dir = + path.basename(source_dir) === ".opencode" + ? path.join(source_dir, "themes") + : path.join(source_dir, ".opencode", "themes") + const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes") + const dest = path.join(dest_dir, `${name}.json`) + const stat = await Filesystem.statAsync(src) + const mtime = stat ? Math.floor(typeof stat.mtimeMs === "bigint" ? Number(stat.mtimeMs) : stat.mtimeMs) : undefined + const size = stat ? (typeof stat.size === "bigint" ? Number(stat.size) : stat.size) : undefined + const exists = hasTheme(name) + const prev = plugin.themes[name] + + if (exists) { + if (plugin.meta.state !== "updated") return + if (!prev) { + if (await Filesystem.exists(dest)) { + plugin.themes[name] = { + src, + dest, + mtime, + size, + } + await PluginMeta.setTheme(plugin.id, name, plugin.themes[name]!).catch((error) => { + log.warn("failed to track tui plugin theme", { + path: spec, + id: plugin.id, + theme: src, + dest, + error, + }) + }) + } + return + } + if (prev.dest !== dest) return + if (prev.mtime === mtime && prev.size === size) return + } const text = await Filesystem.readText(src).catch((error) => { log.warn("failed to read tui plugin theme", { path: spec, theme: src, error }) @@ -170,20 +214,28 @@ function createThemeInstaller(meta: TuiConfig.PluginMeta, root: string, spec: st return } - const source_dir = path.dirname(meta.source) - const local_dir = - path.basename(source_dir) === ".opencode" - ? path.join(source_dir, "themes") - : path.join(source_dir, ".opencode", "themes") - const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes") - const dest = path.join(dest_dir, `${theme}.json`) - if (!(await Filesystem.exists(dest))) { + if (exists || !(await Filesystem.exists(dest))) { await Filesystem.write(dest, text).catch((error) => { log.warn("failed to persist tui plugin theme", { path: spec, theme: src, dest, error }) }) } - addTheme(theme, data) + upsertTheme(name, data) + plugin.themes[name] = { + src, + dest, + mtime, + size, + } + await PluginMeta.setTheme(plugin.id, name, plugin.themes[name]!).catch((error) => { + log.warn("failed to track tui plugin theme", { + path: spec, + id: plugin.id, + theme: src, + dest, + error, + }) + }) } } @@ -222,7 +274,6 @@ async function loadExternalPlugin( } const root = resolveRoot(source === "file" ? spec : target) - const install_theme = createThemeInstaller(meta, root, spec) const entry = await resolvePluginEntrypoint(spec, target, "tui").catch((error) => { fail("failed to resolve tui plugin entry", { path: spec, target, retry, error }) return @@ -253,7 +304,8 @@ async function loadExternalPlugin( source, id, module: mod, - install_theme, + theme_meta: meta, + theme_root: root, } } @@ -297,14 +349,11 @@ function loadInternalPlugin(item: InternalTuiPlugin): PluginLoad { source: "internal", id: item.id, module: item, - install_theme: createThemeInstaller( - { - scope: "global", - source: target, - }, - process.cwd(), - spec, - ), + theme_meta: { + scope: "global", + source: target, + }, + theme_root: process.cwd(), } } @@ -436,7 +485,7 @@ async function activatePluginEntry(state: RuntimeState, plugin: PluginEntry, per if (plugin.scope) return true const scope = createPluginScope(plugin.load, plugin.id) - const api = pluginApi(state, plugin.load, scope, plugin.id) + const api = pluginApi(state, plugin, scope, plugin.id) const ok = await Promise.resolve() .then(async () => { await plugin.plugin(api, plugin.options, plugin.meta) @@ -479,9 +528,10 @@ async function deactivatePluginById(state: RuntimeState | undefined, id: string, return deactivatePluginEntry(state, plugin, persist) } -function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope, base: string): TuiPluginApi { +function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScope, base: string): TuiPluginApi { const api = runtime.api const host = runtime.slots + const load = plugin.load const command: TuiPluginApi["command"] = { register(cb) { return scope.track(api.command.register(cb)) @@ -504,7 +554,7 @@ function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope, } const theme: TuiPluginApi["theme"] = Object.assign(Object.create(api.theme), { - install: load.install_theme, + install: createThemeInstaller(load.theme_meta, load.theme_root, load.spec, plugin), }) const event: TuiPluginApi["event"] = { @@ -563,13 +613,14 @@ function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope, } } -function collectPluginEntries(load: PluginLoad, meta: TuiPluginMeta) { +function collectPluginEntries(load: PluginLoad, meta: TuiPluginMeta, themes: Record = {}) { const options = load.item ? Config.pluginOptions(load.item) : undefined return [ { id: load.id, load, meta, + themes, plugin: load.module.tui, options, enabled: true, @@ -661,7 +712,8 @@ async function addExternalPluginEntries(state: RuntimeState, ready: PluginLoad[] } const row = createMeta(entry.source, entry.spec, entry.target, hit, entry.id) - for (const plugin of collectPluginEntries(entry, row)) { + const themes = hit?.entry.themes ? { ...hit.entry.themes } : {} + for (const plugin of collectPluginEntries(entry, row, themes)) { if (!addPluginEntry(state, plugin)) { ok = false continue diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index bf93870cb0..cbfaf6ae15 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -11,6 +11,13 @@ import { parsePluginSpecifier, pluginSource } from "./shared" export namespace PluginMeta { type Source = "file" | "npm" + export type Theme = { + src: string + dest: string + mtime?: number + size?: number + } + export type Entry = { id: string source: Source @@ -24,6 +31,7 @@ export namespace PluginMeta { time_changed: number load_count: number fingerprint: string + themes?: Record } export type State = "first" | "updated" | "same" @@ -35,7 +43,7 @@ export namespace PluginMeta { } type Store = Record - type Core = Omit + type Core = Omit type Row = Touch & { core: Core } function storePath() { @@ -52,11 +60,11 @@ export namespace PluginMeta { return } - function modifiedAt(file: string) { - const stat = Filesystem.stat(file) + async function modifiedAt(file: string) { + const stat = await Filesystem.statAsync(file) if (!stat) return - const value = stat.mtimeMs - return Math.floor(typeof value === "bigint" ? Number(value) : value) + const mtime = stat.mtimeMs + return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime) } function resolvedTarget(target: string) { @@ -66,7 +74,7 @@ export namespace PluginMeta { async function npmVersion(target: string) { const resolved = resolvedTarget(target) - const stat = Filesystem.stat(resolved) + const stat = await Filesystem.statAsync(resolved) const dir = stat?.isDirectory() ? resolved : path.dirname(resolved) return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json")) .then((item) => item.version) @@ -84,7 +92,7 @@ export namespace PluginMeta { source, spec, target, - modified: file ? modifiedAt(file) : undefined, + modified: file ? await modifiedAt(file) : undefined, } } @@ -122,6 +130,7 @@ export namespace PluginMeta { time_changed: prev?.time_changed ?? now, load_count: (prev?.load_count ?? 0) + 1, fingerprint: fingerprint(core), + themes: prev?.themes, } const state: State = !prev ? "first" : prev.fingerprint === entry.fingerprint ? "same" : "updated" if (state === "updated") entry.time_changed = now @@ -158,6 +167,20 @@ export namespace PluginMeta { }) } + export async function setTheme(id: string, name: string, theme: Theme): Promise { + const file = storePath() + await Flock.withLock(lock(file), async () => { + const store = await read(file) + const entry = store[id] + if (!entry) return + entry.themes = { + ...(entry.themes ?? {}), + [name]: theme, + } + await Filesystem.writeJson(file, store) + }) + } + export async function list(): Promise { const file = storePath() return Flock.withLock(lock(file), async () => read(file)) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index b4ae46df13..29f79e9587 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, readFile, writeFile } from "fs/promises" +import { chmod, mkdir, readFile, stat as statFile, writeFile } from "fs/promises" import { createWriteStream, existsSync, statSync } from "fs" import { lookup } from "mime-types" import { realpathSync } from "fs" @@ -25,6 +25,13 @@ export namespace Filesystem { return statSync(p, { throwIfNoEntry: false }) ?? undefined } + export async function statAsync(p: string): Promise | undefined> { + return statFile(p).catch((e) => { + if (isEnoent(e)) return undefined + throw e + }) + } + export async function size(p: string): Promise { const s = stat(p)?.size ?? 0 return typeof s === "bigint" ? Number(s) : s diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index 9e72754975..143c060e9c 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -561,3 +561,106 @@ describe("tui.plugin.loader", () => { expect(data.leaked_global_to_local).toBe(false) }) }) + +test("updates installed theme when plugin metadata changes", async () => { + await using tmp = await tmpdir<{ + spec: string + pluginPath: string + themePath: string + dest: string + themeName: string + }>({ + init: async (dir) => { + const pluginPath = path.join(dir, "theme-update-plugin.ts") + const spec = pathToFileURL(pluginPath).href + const themeFile = "theme-update.json" + const themePath = path.join(dir, themeFile) + const dest = path.join(dir, ".opencode", "themes", themeFile) + const themeName = themeFile.replace(/\.json$/, "") + const configPath = path.join(dir, "tui.json") + + await Bun.write(themePath, JSON.stringify({ theme: { primary: "#111111" } }, null, 2)) + await Bun.write( + pluginPath, + `export default { + id: "demo.theme-update", + tui: async (api, options) => { + if (!options?.theme_path) return + await api.theme.install(options.theme_path) + }, +} +`, + ) + await Bun.write( + configPath, + JSON.stringify( + { + plugin: [[spec, { theme_path: `./${themeFile}` }]], + }, + null, + 2, + ), + ) + + return { + spec, + pluginPath, + themePath, + dest, + themeName, + } + }, + }) + + process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json") + const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) + const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const install = spyOn(Config, "installDependencies").mockResolvedValue() + + const api = () => + createTuiPluginApi({ + theme: { + has(name) { + return allThemes()[name] !== undefined + }, + }, + }) + + try { + await TuiPluginRuntime.init(api()) + await TuiPluginRuntime.dispose() + await expect(fs.readFile(tmp.extra.dest, "utf8")).resolves.toContain("#111111") + + await Bun.write(tmp.extra.themePath, JSON.stringify({ theme: { primary: "#222222" } }, null, 2)) + await Bun.write( + tmp.extra.pluginPath, + `export default { + id: "demo.theme-update", + tui: async (api, options) => { + if (!options?.theme_path) return + await api.theme.install(options.theme_path) + }, +} +// v2 +`, + ) + const stamp = new Date(Date.now() + 10_000) + await fs.utimes(tmp.extra.pluginPath, stamp, stamp) + await fs.utimes(tmp.extra.themePath, stamp, stamp) + + await TuiPluginRuntime.init(api()) + const text = await fs.readFile(tmp.extra.dest, "utf8") + expect(text).toContain("#222222") + expect(text).not.toContain("#111111") + const list = await Filesystem.readJson }>>( + process.env.OPENCODE_PLUGIN_META_FILE!, + ) + expect(list["demo.theme-update"]?.themes?.[tmp.extra.themeName]?.dest).toBe(tmp.extra.dest) + } finally { + await TuiPluginRuntime.dispose() + cwd.mockRestore() + wait.mockRestore() + install.mockRestore() + delete process.env.OPENCODE_PLUGIN_META_FILE + } +}) From 14f9e21d5c3f4e853dee8ca133693dd3b915b634 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 30 Mar 2026 14:33:01 +0200 Subject: [PATCH 003/155] pluggable home footer (#20057) --- .../cmd/tui/feature-plugins/home/footer.tsx | 93 +++++++++++++++++++ .../src/cli/cmd/tui/plugin/internal.ts | 2 + .../opencode/src/cli/cmd/tui/routes/home.tsx | 61 +----------- packages/plugin/src/tui.ts | 1 + 4 files changed, 99 insertions(+), 58 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx new file mode 100644 index 0000000000..8047c26458 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx @@ -0,0 +1,93 @@ +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" +import { createMemo, Match, Show, Switch } from "solid-js" +import { Global } from "@/global" + +const id = "internal:home-footer" + +function Directory(props: { api: TuiPluginApi }) { + const theme = () => props.api.theme.current + const dir = createMemo(() => { + const dir = props.api.state.path.directory || process.cwd() + const out = dir.replace(Global.Path.home, "~") + const branch = props.api.state.vcs?.branch + if (branch) return out + ":" + branch + return out + }) + + return {dir()} +} + +function Mcp(props: { api: TuiPluginApi }) { + const theme = () => props.api.theme.current + const list = createMemo(() => props.api.state.mcp()) + const has = createMemo(() => list().length > 0) + const err = createMemo(() => list().some((item) => item.status === "failed")) + const count = createMemo(() => list().filter((item) => item.status === "connected").length) + + return ( + + + + + + + + + 0 ? theme().success : theme().textMuted }}>⊙ + + + {count()} MCP + + /status + + + ) +} + +function Version(props: { api: TuiPluginApi }) { + const theme = () => props.api.theme.current + + return ( + + {props.api.app.version} + + ) +} + +function View(props: { api: TuiPluginApi }) { + return ( + + + + + + + ) +} + +const tui: TuiPlugin = async (api) => { + api.slots.register({ + order: 100, + slots: { + home_footer() { + return + }, + }, + }) +} + +const plugin: TuiPluginModule & { id: string } = { + id, + tui, +} + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts index 9e28bbd2e3..856ee0ebb1 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts @@ -1,3 +1,4 @@ +import HomeFooter from "../feature-plugins/home/footer" import HomeTips from "../feature-plugins/home/tips" import SidebarContext from "../feature-plugins/sidebar/context" import SidebarMcp from "../feature-plugins/sidebar/mcp" @@ -14,6 +15,7 @@ export type InternalTuiPlugin = TuiPluginModule & { } export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [ + HomeFooter, HomeTips, SidebarContext, SidebarMcp, diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index b63bf2d2df..8826df314b 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,15 +1,11 @@ import { Prompt, type PromptRef } from "@tui/component/prompt" -import { createEffect, createMemo, Match, on, onMount, Show, Switch } from "solid-js" -import { useTheme } from "@tui/context/theme" +import { createEffect, on, onMount } from "solid-js" import { Logo } from "../component/logo" -import { Locale } from "@/util/locale" import { useSync } from "../context/sync" import { Toast } from "../ui/toast" import { useArgs } from "../context/args" -import { useDirectory } from "../context/directory" import { useRouteData } from "@tui/context/route" import { usePromptRef } from "../context/prompt" -import { Installation } from "@/installation" import { useLocal } from "../context/local" import { TuiPluginRuntime } from "../plugin" @@ -22,37 +18,8 @@ const placeholder = { export function Home() { const sync = useSync() - const { theme } = useTheme() const route = useRouteData("home") const promptRef = usePromptRef() - const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0) - const mcpError = createMemo(() => { - return Object.values(sync.data.mcp).some((x) => x.status === "failed") - }) - - const connectedMcpCount = createMemo(() => { - return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length - }) - - const Hint = ( - - 0}> - - - - mcp errors{" "} - ctrl+x s - - - {" "} - {Locale.pluralize(connectedMcpCount(), "{} mcp server", "{} mcp servers")} - - - - - - ) - let prompt: PromptRef | undefined const args = useArgs() const local = useLocal() @@ -81,7 +48,6 @@ export function Home() { }, ), ) - const directory = useDirectory() return ( <> @@ -101,7 +67,6 @@ export function Home() { prompt = r promptRef.set(r) }} - hint={Hint} workspaceID={route.workspaceID} placeholders={placeholder} /> @@ -111,28 +76,8 @@ export function Home() { - - {directory()} - - - - - - - - - 0 ? theme.success : theme.textMuted }}>⊙ - - - {connectedMcpCount()} MCP - - /status - - - - - {Installation.VERSION} - + + ) diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index bbf3494909..b082f6abe4 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -296,6 +296,7 @@ export type TuiSlotMap = { workspace_id?: string } home_bottom: {} + home_footer: {} sidebar_title: { session_id: string title: string From c2f78224ae59263eada831051a6ece1c65126b1a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:50:42 -0500 Subject: [PATCH 004/155] chore(app): cleanup (#20062) --- .../composer/session-question-dock.tsx | 167 ++-- packages/app/src/pages/session/file-tabs.tsx | 273 +++--- .../pages/session/use-session-commands.tsx | 794 ++++++++++-------- 3 files changed, 664 insertions(+), 570 deletions(-) diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 7ba07b15d0..ef1e52d264 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -11,6 +11,47 @@ import { useSDK } from "@/context/sdk" const cache = new Map() +function Mark(props: { multi: boolean; picked: boolean; onClick?: (event: MouseEvent) => void }) { + return ( + + ) +} + +function Option(props: { + multi: boolean + picked: boolean + label: string + description?: string + disabled: boolean + onClick: VoidFunction +}) { + return ( + + ) +} + export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => { const sdk = useSDK() const language = useLanguage() @@ -41,6 +82,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit return language.t("session.question.progress", { current: n, total: total() }) }) + const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer") + const customPlaceholder = () => language.t("ui.question.custom.placeholder") + const last = createMemo(() => store.tab >= total() - 1) const customUpdate = (value: string, selected: boolean = on()) => { @@ -164,6 +208,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? [])) + const answered = (i: number) => { + if ((store.answers[i]?.length ?? 0) > 0) return true + return store.customOn[i] === true && (store.custom[i] ?? "").trim().length > 0 + } + + const picked = (answer: string) => store.answers[store.tab]?.includes(answer) ?? false + const pick = (answer: string, custom: boolean = false) => { setStore("answers", store.tab, [answer]) if (custom) setStore("custom", store.tab, answer) @@ -230,6 +281,24 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit customUpdate(input()) } + const resizeInput = (el: HTMLTextAreaElement) => { + el.style.height = "0px" + el.style.height = `${el.scrollHeight}px` + } + + const focusCustom = (el: HTMLTextAreaElement) => { + setTimeout(() => { + el.focus() + resizeInput(el) + }, 0) + } + + const toggleCustomMark = (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + customToggle() + } + const next = () => { if (sending()) return if (store.editing) commitCustom() @@ -270,10 +339,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit type="button" data-slot="question-progress-segment" data-active={i() === store.tab} - data-answered={ - (store.answers[i()]?.length ?? 0) > 0 || - (store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0) - } + data-answered={answered(i())} disabled={sending()} onClick={() => jump(i())} aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`} @@ -307,43 +373,23 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
- {(opt, i) => { - const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false - return ( - - ) - }} + {(opt, i) => ( + - + - {language.t("ui.messagePart.option.typeOwnAnswer")} - {input() || language.t("ui.question.custom.placeholder")} + {customLabel()} + {input() || customPlaceholder()} } @@ -394,33 +426,13 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit commitCustom() }} > - + - {language.t("ui.messagePart.option.typeOwnAnswer")} + {customLabel()}