refactor(tui): simplify config and tool display

This commit is contained in:
Dax Raad 2026-07-11 19:51:19 -04:00
commit ac1ddc3f83
44 changed files with 264 additions and 315 deletions

View file

@ -6,7 +6,7 @@
// history ring. All are async because they read config or hit the SDK, but
// none block each other.
import { Context, Effect, Layer } from "effect"
import { resolve } from "@opencode-ai/tui/config"
import { resolve } from "@opencode-ai/tui/config/v1"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"

View file

@ -13,7 +13,7 @@
// → OpenTUI split-footer renderer writes to terminal
import type { OpenCodeClient, ReferenceListOutput } from "@opencode-ai/client/promise"
import type { FilePart, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2"
import type { TuiConfig } from "@opencode-ai/tui/config"
import type { TuiConfig } from "@opencode-ai/tui/config/v1"
export type RunFilePart = {
type: "file"

View file

@ -1,7 +1,7 @@
export * as TuiConfig from "./tui-config"
import { Global } from "@opencode-ai/core/global"
import { TuiConfig } from "@opencode-ai/tui/config"
import { TuiConfig } from "@opencode-ai/tui/config/v1"
import { Effect, FileSystem, Option, Schema } from "effect"
import { parse, type ParseError } from "jsonc-parser"
import path from "path"

View file

@ -2,7 +2,7 @@
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { testRender, useRenderer } from "@opentui/solid"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
import { resolve } from "@opencode-ai/tui/config"
import { resolve } from "@opencode-ai/tui/config/v1"
import { expect, test } from "bun:test"
import { createComponent, createSignal } from "solid-js"
import { RunFooterView } from "../src/mini/footer.view"

View file

@ -2,7 +2,7 @@
import { Config } from "@/config/config"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { TuiConfig } from "@opencode-ai/tui/config"
import { TuiConfig } from "@opencode-ai/tui/config/v1"
import { Schema } from "effect"
type JsonSchema = Record<string, unknown>

View file

@ -1,4 +1,4 @@
import { TuiConfig } from "@opencode-ai/tui/config"
import { TuiConfig } from "@opencode-ai/tui/config/v1"
import { isRecord } from "@opencode-ai/tui/util/record"
import { Filesystem } from "@/util/filesystem"
import { Schema } from "effect"

View file

@ -2,7 +2,7 @@ import path from "path"
import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser"
import { unique } from "remeda"
import { Option, Schema } from "effect"
import { TuiConfig } from "@opencode-ai/tui/config"
import { TuiConfig } from "@opencode-ai/tui/config/v1"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"

View file

@ -15,14 +15,14 @@ import { Global } from "@opencode-ai/core/global"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CurrentWorkingDirectory } from "./tui-cwd"
import { ConfigPlugin } from "@/config/plugin"
import { TuiKeybind } from "@opencode-ai/tui/config/keybind"
import { TuiKeybind } from "@opencode-ai/tui/config/v1/keybind"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { Filesystem } from "@/util/filesystem"
import { ConfigVariable } from "@/config/variable"
import { Npm } from "@opencode-ai/core/npm"
import { FormatError, FormatUnknownError } from "@/cli/error"
import { TuiConfig } from "@opencode-ai/tui/config"
import { TuiConfig } from "@opencode-ai/tui/config/v1"
export const Info = TuiConfig.Info
export type Info = TuiConfig.Info

View file

@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import type { AudioPlayOptions, AudioSound } from "@opentui/core"
import { createTuiAttention } from "@opencode-ai/tui/attention"
import type { TuiConfig } from "@opencode-ai/tui/config"
import type { TuiConfig } from "@opencode-ai/tui/config/v1"
type FocusEvent = "focus" | "blur"

View file

@ -1,6 +1,6 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
import { OpenCode } from "@opencode-ai/client/promise"
import type { Resolved } from "@opencode-ai/tui/config"
import type { Resolved } from "@opencode-ai/tui/config/v1"
import { resolveDiffStyle, resolveModelInfo, resolveRunTuiConfig } from "@opencode-ai/cli/mini/runtime.boot"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"

View file

@ -1,8 +1,8 @@
import { spyOn } from "bun:test"
import path from "path"
import { resolve, type Info, type Resolved } from "@opencode-ai/tui/config"
import { resolve, type Info, type Resolved } from "@opencode-ai/tui/config/v1"
import { TuiConfig } from "../../src/config/tui"
import { TuiKeybind } from "@opencode-ai/tui/config/keybind"
import { TuiKeybind } from "@opencode-ai/tui/config/v1/keybind"
type PluginSpec = string | [string, Record<string, unknown>]
type PluginOrigin = {

View file

@ -12,7 +12,10 @@
"exports": {
".": "./src/index.tsx",
"./builtins": "./src/feature-plugins/builtins.ts",
"./config": "./src/config/index.tsx",
"./config/v1": "./src/config/v1/index.tsx",
"./config/v1/keybind": "./src/config/v1/keybind.ts",
"./config/v2": "./src/config/v2/index.ts",
"./config/v2/keybind": "./src/config/v2/keybind.ts",
"./context/args": "./src/context/args.tsx",
"./context/epilogue": "./src/context/epilogue.tsx",
"./context/exit": "./src/context/exit.tsx",
@ -30,7 +33,6 @@
"./editor-zed": "./src/editor-zed.ts",
"./runtime": "./src/runtime.tsx",
"./terminal-win32": "./src/terminal-win32.ts",
"./config/keybind": "./src/config/keybind.ts",
"./keymap": "./src/keymap.tsx",
"./prompt/content": "./src/prompt/content.ts",
"./prompt/display": "./src/prompt/display.ts",

View file

@ -68,7 +68,7 @@ import * as Model from "./util/model"
import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig, type TuiConfig } from "./config"
import { TuiConfigProvider, useTuiConfig, type TuiConfig } from "./config/v1"
import { createTuiApiAdapters } from "./plugin/adapters"
import { createTuiApi } from "./plugin/api"
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"

View file

@ -10,7 +10,7 @@ import type {
TuiAttentionSoundPack,
TuiAttentionSoundPackInfo,
} from "@opencode-ai/plugin/tui"
import { AttentionSoundName, type TuiConfig } from "./config"
import { AttentionSoundName, type TuiConfig } from "./config/v1"
import { Schema } from "effect"
import stripAnsi from "strip-ansi"
import * as TuiAudio from "./audio"

View file

@ -8,7 +8,7 @@ import {
useKeymapSelector,
useOpencodeKeymap,
} from "../keymap"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]

View file

@ -9,7 +9,7 @@ import type { McpServer } from "@opencode-ai/client"
import { useClipboard } from "../context/clipboard"
import { useToast } from "../ui/toast"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { getScrollAcceleration } from "../util/scroll"
import { useBindings } from "../keymap"

View file

@ -1,7 +1,7 @@
import { InputRenderable, TextAttributes } from "@opentui/core"
import { Slug } from "@opencode-ai/core/util/slug"
import { createSignal, onMount } from "solid-js"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { useTheme } from "../context/theme"
import { useBindings, useCommandShortcut } from "../keymap"
import { useDialog, type DialogContext } from "../ui/dialog"

View file

@ -5,7 +5,7 @@ import { createMemo, For } from "solid-js"
import { createStore } from "solid-js/store"
import { FilePath } from "../ui/file-path"
import { useTheme } from "../context/theme"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { useDialog, type DialogContext } from "../ui/dialog"
import { getScrollAcceleration } from "../util/scroll"

View file

@ -11,7 +11,7 @@ import { useSDK } from "../../context/sdk"
import { useData } from "../../context/data"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiPaths } from "../../context/runtime"
import { useTuiConfig } from "../../config"
import { useTuiConfig } from "../../config/v1"
import { useLocation } from "../../context/location"
import { useTheme, selectedForeground } from "../../context/theme"
import { SplitBorder } from "../../ui/border"

View file

@ -48,7 +48,7 @@ import { createFadeIn } from "../../util/signal"
import { DialogSkill } from "../dialog-skill"
import { useArgs } from "../../context/args"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
import { useTuiConfig } from "../../config"
import { useTuiConfig } from "../../config/v1"
import { usePromptMove } from "./move"
import { readLocalAttachment } from "./local-attachment"
import { useData } from "../../context/data"

View file

@ -97,8 +97,6 @@ export const Definitions = {
session_interrupt: keybind("escape", "Interrupt current session"),
session_background: keybind("ctrl+b", "Background blocking session tools"),
session_compact: keybind("<leader>c", "Compact the session"),
session_toggle_timestamps: keybind("none", "Toggle message timestamps"),
session_toggle_generic_tool_output: keybind("none", "Toggle generic tool output"),
session_queued_prompts: keybind("<leader>q", "Manage queued prompts"),
session_child_first: keybind("down,<leader>down", "Toggle subagent picker"),
session_child_cycle: keybind("right", "Go to next child session"),
@ -146,7 +144,6 @@ export const Definitions = {
messages_undo: keybind("<leader>u", "Undo message"),
messages_redo: keybind("<leader>r", "Redo message"),
messages_toggle_conceal: keybind("<leader>h", "Toggle code block concealment in messages"),
tool_details: keybind("none", "Toggle tool details visibility"),
display_thinking: keybind("none", "Toggle thinking blocks visibility"),
prompt_submit: keybind("none", "Submit prompt"),
@ -303,8 +300,6 @@ export const CommandMap = {
session_interrupt: "session.interrupt",
session_background: "session.background",
session_compact: "session.compact",
session_toggle_timestamps: "session.toggle.timestamps",
session_toggle_generic_tool_output: "session.toggle.generic_tool_output",
session_queued_prompts: "session.queued_prompts",
session_child_first: "session.child.first",
session_child_cycle: "session.child.next",
@ -350,7 +345,6 @@ export const CommandMap = {
messages_undo: "session.undo",
messages_redo: "session.redo",
messages_toggle_conceal: "session.toggle.conceal",
tool_details: "session.toggle.actions",
display_thinking: "session.toggle.thinking",
prompt_submit: "prompt.submit",
prompt_editor_context_clear: "prompt.editor_context.clear",

View file

@ -0,0 +1,97 @@
export * as TuiConfigV2 from "."
import { Schema } from "effect"
import { TuiKeybind } from "./keybind"
export const Plugin = Schema.Union([
Schema.String,
Schema.Struct({
package: Schema.String,
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}),
])
export const Info = Schema.Struct({
theme: Schema.optional(
Schema.Struct({
name: Schema.optional(Schema.String),
mode: Schema.optional(Schema.Literals(["system", "dark", "light"])),
}),
),
keybinds: Schema.optional(TuiKeybind.KeybindOverrides),
plugins: Schema.optional(Schema.Array(Plugin)),
leader: Schema.optional(
Schema.Struct({
timeout: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
}),
),
scroll: Schema.optional(
Schema.Struct({
speed: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))),
acceleration: Schema.optional(Schema.Boolean),
}),
),
attention: Schema.optional(
Schema.Struct({
enabled: Schema.optional(Schema.Boolean),
notifications: Schema.optional(Schema.Boolean),
sound: Schema.optional(Schema.Boolean),
volume: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1))),
sound_pack: Schema.optional(Schema.String),
sounds: Schema.optional(
Schema.Record(
Schema.Literals(["default", "question", "permission", "error", "done", "subagent_done"]),
Schema.optionalKey(Schema.String),
),
),
}),
),
diffs: Schema.optional(
Schema.Struct({
wrap: Schema.optional(Schema.Literals(["word", "none"])),
tree: Schema.optional(Schema.Boolean),
single: Schema.optional(Schema.Boolean),
view: Schema.optional(Schema.Literals(["auto", "split", "unified"])),
}),
),
terminal: Schema.optional(
Schema.Struct({
title: Schema.optional(Schema.Boolean),
}),
),
composer: Schema.optional(
Schema.Struct({
file_context: Schema.optional(Schema.Boolean),
paste_summary: Schema.optional(Schema.Boolean),
}),
),
session: Schema.optional(
Schema.Struct({
sidebar: Schema.optional(Schema.Literals(["auto", "hide"])),
scrollbar: Schema.optional(Schema.Boolean),
thinking: Schema.optional(Schema.Literals(["show", "hide"])),
group_exploration: Schema.optional(Schema.Boolean),
directory_filter: Schema.optional(Schema.Boolean),
}),
),
which_key: Schema.optional(
Schema.Struct({
layout: Schema.optional(Schema.Literals(["dock", "overlay"])),
pending_preview: Schema.optional(Schema.Boolean),
}),
),
hints: Schema.optional(
Schema.Struct({
tips: Schema.optional(Schema.Boolean),
getting_started: Schema.optional(Schema.Boolean),
}),
),
updates: Schema.optional(
Schema.Struct({
skipped: Schema.optional(Schema.String),
}),
),
animations: Schema.optional(Schema.Boolean),
mouse: Schema.optional(Schema.Boolean),
})
export type Info = Schema.Schema.Type<typeof Info>

View file

@ -0,0 +1,6 @@
export * as TuiKeybind from "./keybind"
import { Schema } from "effect"
export const KeybindOverrides = Schema.Struct({})
export type KeybindOverrides = Schema.Schema.Type<typeof KeybindOverrides>

View file

@ -23,7 +23,7 @@ import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useKV } from "./kv"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { Global } from "@opencode-ai/core/global"
import { Glob } from "@opencode-ai/core/util/glob"
import { readFile } from "node:fs/promises"

View file

@ -14,8 +14,8 @@ import {
} from "@opentui/keymap/extras"
import { KeymapProvider, useKeymap, useKeymapSelector, useBindings } from "@opentui/keymap/solid"
import { createMemo, type Accessor } from "solid-js"
import { useTuiConfig } from "./config"
import { TuiKeybind } from "./config/keybind"
import { useTuiConfig } from "./config/v1"
import { TuiKeybind } from "./config/v1/keybind"
export const LEADER_TOKEN = "leader"
export const OPENCODE_BASE_MODE = "base"

View file

@ -1,5 +1,5 @@
import type { TuiDialogSelectOption, TuiPluginApi, TuiSlotProps } from "@opencode-ai/plugin/tui"
import type { TuiConfig } from "../config"
import type { TuiConfig } from "../config/v1"
import type { useEvent } from "../context/event"
import type { useRoute } from "../context/route"
import type { useSDK } from "../context/sdk"

View file

@ -1,6 +1,6 @@
// Legacy `api.command` bridge for v1 plugins; remove in v2.
import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui"
import { TuiKeybind } from "../config/keybind"
import { TuiKeybind } from "../config/v1/keybind"
import type { DialogContext } from "../ui/dialog"
const COMMAND_PALETTE_SHOW = "command.palette.show"

View file

@ -4,7 +4,7 @@ import type {
TuiPluginInstallResult,
TuiPluginStatus,
} from "@opencode-ai/plugin/tui"
import type { TuiConfig } from "../config"
import type { TuiConfig } from "../config/v1"
import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js"
import { createPluginRoutes } from "./api"
import { createSlots, type HostSlots } from "./slots"

View file

@ -9,7 +9,7 @@ import { useLocal } from "../context/local"
import { usePluginRuntime } from "../plugin/runtime"
import { useEditorContext } from "../context/editor"
import { useTerminalDimensions } from "@opentui/solid"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { HomeSessionDestinationProvider } from "./home/session-destination"
import { useData } from "../context/data"
import { LocationProvider } from "../context/location"

View file

@ -10,7 +10,7 @@ import { useSDK } from "../../context/sdk"
import { useClipboard } from "../../context/clipboard"
import { SplitBorder } from "../../ui/border"
import { useToast } from "../../ui/toast"
import { useTuiConfig } from "../../config"
import { useTuiConfig } from "../../config/v1"
import { useBindings, useOpencodeModeStack } from "../../keymap"
const FORM_MODE = "form"

View file

@ -64,7 +64,7 @@ import { FormPrompt } from "./form"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import { DialogExportResult } from "../../ui/dialog-export-result"
import { sessionEpilogue } from "../../util/presentation"
import { useTuiConfig } from "../../config"
import { useTuiConfig } from "../../config/v1"
import { useClipboard } from "../../context/clipboard"
import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking"
import { getScrollAcceleration } from "../../util/scroll"
@ -89,11 +89,8 @@ const sessionBindingCommands = [
"session.redo",
"session.sidebar.toggle",
"session.toggle.conceal",
"session.toggle.timestamps",
"session.toggle.thinking",
"session.toggle.actions",
"session.toggle.scrollbar",
"session.toggle.generic_tool_output",
"session.toggle.exploration_grouping",
"session.first",
"session.last",
@ -127,9 +124,6 @@ const context = createContext<{
conceal: () => boolean
thinkingMode: () => ThinkingMode
showThinking: () => boolean
showTimestamps: () => boolean
showDetails: () => boolean
showGenericToolOutput: () => boolean
groupExploration: () => boolean
diffWrapMode: () => "word" | "none"
models: () => ModelInfo[]
@ -208,13 +202,9 @@ export function Session() {
const thinking = useThinkingMode()
const thinkingMode = thinking.mode
const showThinking = createMemo(() => true)
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, _setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true)
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
const [groupExploration, setGroupExploration] = kv.signal("exploration_grouping", true)
const wide = createMemo(() => dimensions().width > 120)
@ -224,7 +214,6 @@ export function Session() {
if (sidebar() === "auto" && wide()) return true
return false
})
const showTimestamps = createMemo(() => timestamps() === "show")
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
const models = createMemo(() => data.location.model.list(location()) ?? [])
@ -491,19 +480,6 @@ export function Session() {
dialog.clear()
},
},
{
title: showTimestamps() ? "Hide timestamps" : "Show timestamps",
value: "session.toggle.timestamps",
category: "Session",
slash: {
name: "timestamps",
aliases: ["toggle-timestamps"],
},
run: () => {
setTimestamps((prev) => (prev === "show" ? "hide" : "show"))
dialog.clear()
},
},
{
title: (() => {
const next = nextThinkingMode(thinkingMode())
@ -521,15 +497,6 @@ export function Session() {
dialog.clear()
},
},
{
title: showDetails() ? "Hide tool details" : "Show tool details",
value: "session.toggle.actions",
category: "Session",
run: () => {
setShowDetails((prev) => !prev)
dialog.clear()
},
},
{
title: "Toggle session scrollbar",
value: "session.toggle.scrollbar",
@ -539,15 +506,6 @@ export function Session() {
dialog.clear()
},
},
{
title: showGenericToolOutput() ? "Hide generic tool output" : "Show generic tool output",
value: "session.toggle.generic_tool_output",
category: "Session",
run: () => {
setShowGenericToolOutput((prev) => !prev)
dialog.clear()
},
},
{
title: groupExploration() ? "Show exploration tools individually" : "Group exploration tools",
value: "session.toggle.exploration_grouping",
@ -727,13 +685,7 @@ export function Session() {
try {
const sessionData = session()
if (!sessionData) return
const transcript = formatSessionTranscript(
sessionData,
messages(),
showThinking(),
showDetails(),
showAssistantMetadata(),
)
const transcript = formatSessionTranscript(sessionData, messages(), showThinking())
await clipboard.write?.(transcript)
toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
} catch {
@ -754,19 +706,13 @@ export function Session() {
const sessionData = session()
if (!sessionData) return
const options = await DialogExportOptions.show(dialog, showThinking(), showDetails(), showAssistantMetadata())
const options = await DialogExportOptions.show(dialog, showThinking())
if (options === null) return
const content =
options.format === "markdown"
? formatSessionTranscript(
sessionData,
messages(),
options.thinking,
options.toolDetails,
options.assistantMetadata,
)
? formatSessionTranscript(sessionData, messages(), options.thinking)
: await (async () => {
if (options.debug) {
const events: { readonly created: number }[] = []
@ -922,9 +868,6 @@ export function Session() {
conceal,
thinkingMode,
showThinking,
showTimestamps,
showDetails,
showGenericToolOutput,
groupExploration,
diffWrapMode,
models,
@ -1534,13 +1477,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
<text fg={theme.text}>{props.message.text}</text>
<Show when={files().length}>
<box
flexDirection="row"
paddingBottom={ctx.showTimestamps() ? 1 : 0}
paddingTop={1}
gap={1}
flexWrap="wrap"
>
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
{(file) => {
const label = file.mime === "application/x-directory" ? "dir" : "file"
@ -1557,11 +1494,6 @@ function UserMessage(props: { message: SessionMessageUser }) {
</For>
</box>
</Show>
<Show when={ctx.showTimestamps()}>
<text fg={theme.textMuted}>
<span style={{ fg: theme.textMuted }}>{Locale.todayTimeOrDateTime(props.message.time.created)}</span>
</text>
</Show>
</box>
</box>
</Show>
@ -1865,31 +1797,7 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
// Pending messages moved to individual tool pending functions
function ToolPart(props: { part: SessionMessageAssistantTool }) {
const ctx = use()
const data = useData()
const display = createMemo(() => toolDisplay(props.part.name))
const activeBackgroundWork = createMemo(() => {
if (props.part.state.status === "streaming") return false
if (display() === "shell") {
const shellID = stringValue(props.part.state.structured.shellID)
return Boolean(shellID && data.shell.get(shellID))
}
if (display() === "subagent") {
const sessionID =
stringValue(props.part.state.structured.sessionID) ?? stringValue(props.part.state.structured.sessionId)
return Boolean(sessionID && data.session.status(sessionID) === "running")
}
return false
})
// Hide tool if showDetails is false and tool completed successfully
const shouldHide = createMemo(() => {
if (ctx.showDetails()) return false
if (activeBackgroundWork()) return false
if (props.part.state.status !== "completed") return false
if (display() === "shell") return false
return true
})
const toolprops = {
get metadata() {
@ -1913,52 +1821,50 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
}
return (
<Show when={!shouldHide()}>
<Switch>
<Match when={display() === "shell"}>
<Shell {...toolprops} />
</Match>
<Match when={display() === "glob"}>
<Glob {...toolprops} />
</Match>
<Match when={display() === "read"}>
<Read {...toolprops} />
</Match>
<Match when={display() === "grep"}>
<Grep {...toolprops} />
</Match>
<Match when={display() === "webfetch"}>
<WebFetch {...toolprops} />
</Match>
<Match when={display() === "websearch"}>
<WebSearch {...toolprops} />
</Match>
<Match when={display() === "write"}>
<Write {...toolprops} />
</Match>
<Match when={display() === "edit"}>
<Edit {...toolprops} />
</Match>
<Match when={display() === "subagent"}>
<Subagent {...toolprops} />
</Match>
<Match when={display() === "execute"}>
<Execute {...toolprops} />
</Match>
<Match when={display() === "patch"}>
<ApplyPatch {...toolprops} />
</Match>
<Match when={display() === "question"}>
<Question {...toolprops} />
</Match>
<Match when={display() === "skill"}>
<Skill {...toolprops} />
</Match>
<Match when={true}>
<GenericTool {...toolprops} />
</Match>
</Switch>
</Show>
<Switch>
<Match when={display() === "shell"}>
<Shell {...toolprops} />
</Match>
<Match when={display() === "glob"}>
<Glob {...toolprops} />
</Match>
<Match when={display() === "read"}>
<Read {...toolprops} />
</Match>
<Match when={display() === "grep"}>
<Grep {...toolprops} />
</Match>
<Match when={display() === "webfetch"}>
<WebFetch {...toolprops} />
</Match>
<Match when={display() === "websearch"}>
<WebSearch {...toolprops} />
</Match>
<Match when={display() === "write"}>
<Write {...toolprops} />
</Match>
<Match when={display() === "edit"}>
<Edit {...toolprops} />
</Match>
<Match when={display() === "subagent"}>
<Subagent {...toolprops} />
</Match>
<Match when={display() === "execute"}>
<Execute {...toolprops} />
</Match>
<Match when={display() === "patch"}>
<ApplyPatch {...toolprops} />
</Match>
<Match when={display() === "question"}>
<Question {...toolprops} />
</Match>
<Match when={display() === "skill"}>
<Skill {...toolprops} />
</Match>
<Match when={true}>
<GenericTool {...toolprops} />
</Match>
</Switch>
)
}
@ -1970,40 +1876,55 @@ type ToolProps = {
part: SessionMessageAssistantTool
}
function GenericTool(props: ToolProps) {
const { theme } = useTheme()
const ctx = use()
const { theme, syntax } = useTheme()
const output = createMemo(() => props.output?.trim() ?? "")
const args = createMemo(() => JSON.stringify(props.input, null, 2))
const [expanded, setExpanded] = createSignal(false)
const maxLines = 3
const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
const limited = createMemo(() => {
if (expanded() || !collapsed().overflow) return output()
return collapsed().output
})
const expandable = createMemo(() => Object.keys(props.input).length > 0 || output().length > 0)
return (
<Show
when={props.output && ctx.showGenericToolOutput()}
fallback={
<InlineTool icon="⚙" pending="Writing command..." complete={true} part={props.part}>
{props.tool} {input(props.input)}
</InlineTool>
}
<BlockTool
title={`${props.tool}`}
part={props.part}
spinner={props.part.state.status === "streaming" || props.part.state.status === "running"}
onClick={expandable() ? () => setExpanded((value) => !value) : undefined}
>
<BlockTool
title={`# ${props.tool} ${input(props.input)}`}
part={props.part}
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
>
<box gap={1}>
<text fg={theme.text}>{limited()}</text>
<Show when={collapsed().overflow}>
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
<Show when={expanded()}>
<box gap={1} paddingTop={1}>
<Show when={Object.keys(props.input).length > 0}>
<box gap={1}>
<text>
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> Input </span>
</text>
<box paddingLeft={1}>
<code
content={args()}
filetype="json"
syntaxStyle={syntax()}
conceal={false}
drawUnstyledText={false}
fg={theme.text}
/>
</box>
</box>
</Show>
<Show when={output()}>
{(value) => (
<box gap={1}>
<text>
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> Output </span>
</text>
<box paddingLeft={1}>
<text fg={theme.text} wrapMode="word">
{value()}
</text>
</box>
</box>
)}
</Show>
</box>
</BlockTool>
</Show>
</Show>
</BlockTool>
)
}
@ -2106,12 +2027,7 @@ export function InlineToolRow(props: {
<Spinner color={props.color} children={props.children} />
</Match>
<Match when={true}>
<Show
fallback={
<Spinner color={props.color}>{props.pending}</Spinner>
}
when={props.complete || props.failed}
>
<Show fallback={<Spinner color={props.color}>{props.pending}</Spinner>} when={props.complete || props.failed}>
<box flexDirection="row">
<text
width={INLINE_TOOL_ICON_WIDTH}
@ -2183,11 +2099,7 @@ function BlockTool(props: {
{(title) => (
<Show
when={props.spinner}
fallback={
<text fg={permission() ? theme.warning : theme.textMuted}>
{title()}
</text>
}
fallback={<text fg={permission() ? theme.warning : theme.textMuted}>{title()}</text>}
>
<Spinner color={permission() ? theme.warning : theme.textMuted}>{title().replace(/^# /, "")}</Spinner>
</Show>
@ -2676,11 +2588,7 @@ function ApplyPatch(props: ToolProps) {
: undefined
}
title={
targets().length === 1
? undefined
: props.part.state.status === "error"
? "# Patch failed"
: "Patching"
targets().length === 1 ? undefined : props.part.state.status === "error" ? "# Patch failed" : "Patching"
}
part={props.part}
spinner={props.part.state.status === "streaming" || props.part.state.status === "running"}
@ -2806,13 +2714,7 @@ function recordValue(value: unknown): Record<string, unknown> | undefined {
return value as Record<string, unknown>
}
function formatSessionTranscript(
session: SessionInfo,
messages: SessionMessageInfo[],
thinking: boolean,
toolDetails: boolean,
assistantMetadata: boolean,
) {
function formatSessionTranscript(session: SessionInfo, messages: SessionMessageInfo[], thinking: boolean) {
const body = messages.flatMap((message) => {
if (message.type === "user") return [`## User\n\n${message.text}`]
if (message.type === "shell")
@ -2821,7 +2723,6 @@ function formatSessionTranscript(
const content = message.content.flatMap((item) => {
if (item.type === "text") return [item.text]
if (item.type === "reasoning") return thinking ? [`_Thinking:_\n\n${item.text}`] : []
if (!toolDetails) return [`**Tool: ${item.name}**`]
const input = typeof item.state.input === "string" ? item.state.input : JSON.stringify(item.state.input, null, 2)
const output =
item.state.status === "error"
@ -2833,13 +2734,7 @@ function formatSessionTranscript(
.join("\n")
return [`**Tool: ${item.name}**\n\n**Input:**\n\`\`\`json\n${input}\n\`\`\`\n\n${output}`]
})
const duration = message.time.completed
? ` · ${((message.time.completed - message.time.created) / 1000).toFixed(1)}s`
: ""
const heading = assistantMetadata
? `## Assistant (${message.agent} · ${message.model.providerID}/${message.model.id}${duration})`
: "## Assistant"
return [`${heading}\n\n${content.join("\n\n")}`]
return [`## Assistant\n\n${content.join("\n\n")}`]
})
return `# ${session.title}\n\n**Session ID:** ${session.id}\n**Created:** ${new Date(session.time.created).toLocaleString()}\n**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n---\n\n${body.join("\n\n---\n\n")}\n`
}

View file

@ -12,7 +12,7 @@ import { filetype } from "../../util/filetype"
import { Locale } from "../../util/locale"
import { webSearchProviderLabel } from "../../util/tool-display"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiConfig } from "../../config"
import { useTuiConfig } from "../../config/v1"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
import { usePathFormatter } from "../../context/path-format"

View file

@ -1,7 +1,7 @@
import { useData } from "../../context/data"
import { createMemo, Show } from "solid-js"
import { useTheme } from "../../context/theme"
import { useTuiConfig } from "../../config"
import { useTuiConfig } from "../../config/v1"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { usePluginRuntime } from "../../plugin/runtime"

View file

@ -9,20 +9,11 @@ export type ExportFormat = "markdown" | "json"
export type DialogExportOptionsProps = {
defaultThinking: boolean
defaultToolDetails: boolean
defaultAssistantMetadata: boolean
onConfirm?: (options: {
action: "copy" | "export"
format: ExportFormat
debug: boolean
thinking: boolean
toolDetails: boolean
assistantMetadata: boolean
}) => void
onConfirm?: (options: { action: "copy" | "export"; format: ExportFormat; debug: boolean; thinking: boolean }) => void
onCancel?: () => void
}
type Active = ExportFormat | "debug" | "thinking" | "toolDetails" | "assistantMetadata" | "copy" | "export"
type Active = ExportFormat | "debug" | "thinking" | "copy" | "export"
export function DialogExportOptions(props: DialogExportOptionsProps) {
const dialog = useDialog()
@ -31,8 +22,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
format: "markdown" as ExportFormat,
debug: false,
thinking: props.defaultThinking,
toolDetails: props.defaultToolDetails,
assistantMetadata: props.defaultAssistantMetadata,
active: "markdown" as Active,
})
@ -42,8 +31,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
format: store.format,
debug: store.debug,
thinking: store.thinking,
toolDetails: store.toolDetails,
assistantMetadata: store.assistantMetadata,
})
const activate = () => {
@ -53,8 +40,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
}
if (store.active === "debug") setStore("debug", !store.debug)
if (store.active === "thinking") setStore("thinking", !store.thinking)
if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
if (store.active === "assistantMetadata") setStore("assistantMetadata", !store.assistantMetadata)
if (store.active === "copy" || store.active === "export") confirm(store.active)
}
@ -67,7 +52,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
cmd: () => {
const order: Active[] =
store.format === "markdown"
? ["markdown", "json", "thinking", "toolDetails", "assistantMetadata", "copy", "export"]
? ["markdown", "json", "thinking", "copy", "export"]
: ["markdown", "json", "debug", "copy", "export"]
setStore("active", order[(order.indexOf(store.active) + 1) % order.length])
},
@ -86,11 +71,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
setStore("active", format)
}
const toggle = (option: "thinking" | "toolDetails" | "assistantMetadata") => {
setStore("active", option)
setStore(option, !store[option])
}
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
@ -121,30 +101,19 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
</box>
</box>
<Show when={store.format === "markdown"}>
<box flexDirection="column">
<For
each={
[
["thinking", "Include thinking"],
["toolDetails", "Include tool details"],
["assistantMetadata", "Include assistant metadata"],
] as const
}
>
{(item) => (
<box
flexDirection="row"
gap={1}
backgroundColor={store.active === item[0] ? theme.backgroundElement : undefined}
onMouseUp={() => toggle(item[0])}
>
<text fg={store.active === item[0] ? theme.primary : theme.textMuted}>
{store[item[0]] ? "[x]" : "[ ]"}
</text>
<text fg={store.active === item[0] ? theme.primary : theme.text}>{item[1]}</text>
</box>
)}
</For>
<box
flexDirection="row"
gap={1}
backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
onMouseUp={() => {
setStore("active", "thinking")
setStore("thinking", !store.thinking)
}}
>
<text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
{store.thinking ? "[x]" : "[ ]"}
</text>
<text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
</box>
</Show>
<Show when={store.format === "json"}>
@ -170,12 +139,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
>
<text fg={theme.text}>Copy</text>
</box>
<box
paddingLeft={4}
paddingRight={4}
backgroundColor={theme.primary}
onMouseUp={() => confirm("export")}
>
<box paddingLeft={4} paddingRight={4} backgroundColor={theme.primary} onMouseUp={() => confirm("export")}>
<text fg={theme.selectedListItemText}>Export</text>
</box>
</box>
@ -183,26 +147,17 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
)
}
DialogExportOptions.show = (
dialog: DialogContext,
defaultThinking: boolean,
defaultToolDetails: boolean,
defaultAssistantMetadata: boolean,
) => {
DialogExportOptions.show = (dialog: DialogContext, defaultThinking: boolean) => {
return new Promise<{
action: "copy" | "export"
format: ExportFormat
debug: boolean
thinking: boolean
toolDetails: boolean
assistantMetadata: boolean
} | null>((resolve) => {
dialog.replace(
() => (
<DialogExportOptions
defaultThinking={defaultThinking}
defaultToolDetails={defaultToolDetails}
defaultAssistantMetadata={defaultAssistantMetadata}
onConfirm={(options) => resolve(options)}
onCancel={() => resolve(null)}
/>

View file

@ -3,7 +3,7 @@ import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js"
import { Spinner } from "../component/spinner"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { useBindings, useCommandShortcut } from "../keymap"
export type DialogPromptProps = {

View file

@ -17,7 +17,7 @@ import { isDeepEqual } from "remeda"
import { useDialog, type DialogContext } from "./dialog"
import { Locale } from "../util/locale"
import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../config"
import { useTuiConfig } from "../config/v1"
import { formatKeyBindings, useBindings, useKeymapSelector } from "../keymap"
export interface DialogSelectProps<T> {

View file

@ -8,7 +8,7 @@ import path from "node:path"
import { onCleanup } from "solid-js"
import { tmpdir } from "../../fixture/fixture"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import type { TuiKeybind } from "../../../src/config/keybind"
import type { TuiKeybind } from "../../../src/config/v1/keybind"
import { TestTuiContexts } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
@ -41,7 +41,7 @@ async function mountPrompt(input: {
import("../../../src/ui/dialog-prompt"),
import("../../../src/context/kv"),
import("../../../src/context/theme"),
import("../../../src/config"),
import("../../../src/config/v1"),
import("../../../src/ui/toast"),
import("../../../src/keymap"),
])

View file

@ -6,7 +6,7 @@ import type { JSX } from "solid-js"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { TuiConfigProvider } from "../../../src/config/v1"
import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import { TestTuiContexts } from "../../fixture/tui-environment"
import {

View file

@ -7,9 +7,9 @@ import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition }
import type { Session } from "@opencode-ai/sdk/v2"
import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { TuiConfigProvider } from "../../../src/config/v1"
import { SDKProvider } from "../../../src/context/sdk"
import { TuiKeybind } from "../../../src/config/keybind"
import { TuiKeybind } from "../../../src/config/v1/keybind"
import { OpencodeKeymapProvider } from "../../../src/keymap"
import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
import { createTuiPluginApi } from "../../fixture/tui-plugin"

View file

@ -10,7 +10,7 @@ import type { FormWithLocation } from "../../../src/context/data"
import { KVProvider } from "../../../src/context/kv"
import { SDKProvider } from "../../../src/context/sdk"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { TuiConfigProvider } from "../../../src/config/v1"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
import { ToastProvider } from "../../../src/ui/toast"
import { tmpdir } from "../../fixture/fixture"

View file

@ -11,7 +11,7 @@ import {
TuiConfigProvider,
type Info as TuiConfigInfo,
useTuiConfig,
} from "../src/config"
} from "../src/config/v1"
const decodeInfo = Schema.decodeUnknownSync(Info)
const decodePlugin = Schema.decodeUnknownSync(PluginSpec)

View file

@ -1,5 +1,5 @@
import { resolve, type Info, type Resolved } from "../../src/config"
import { TuiKeybind } from "../../src/config/keybind"
import { resolve, type Info, type Resolved } from "../../src/config/v1"
import { TuiKeybind } from "../../src/config/v1/keybind"
type ResolvedInput = Omit<Info, "attention" | "keybinds" | "leader_timeout"> & {
attention?: Partial<Resolved["attention"]>

View file

@ -4,7 +4,7 @@ import { createBindingLookup } from "@opentui/keymap/extras"
import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test"
import { onCleanup } from "solid-js"
import { TuiKeybind } from "../src/config/keybind"
import { TuiKeybind } from "../src/config/v1/keybind"
import {
formatKeySequence,
getOpencodeModeStack,