From f24683e661f3bd5bbd84a7869c05e291a3770f6a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 28 Dec 2025 17:27:11 -0500 Subject: [PATCH 01/21] feat: add command-aware permission request system for granular tool approval --- .opencode/agent/git-committer.md | 10 - .opencode/opencode.jsonc | 1 + packages/opencode/src/acp/agent.ts | 2 +- packages/opencode/src/agent/agent.ts | 301 ++---- packages/opencode/src/cli/cmd/agent.ts | 3 +- packages/opencode/src/cli/cmd/run.ts | 6 +- packages/opencode/src/cli/cmd/tui/app.tsx | 3 +- .../cmd/tui/component/dialog-permission.tsx | 53 ++ .../opencode/src/cli/cmd/tui/context/sync.tsx | 42 +- .../src/cli/cmd/tui/routes/session/footer.tsx | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 880 +++++++++--------- .../cli/cmd/tui/routes/session/permission.tsx | 237 +++++ .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 1 + packages/opencode/src/config/config.ts | 111 ++- packages/opencode/src/permission/arity.ts | 162 ++++ packages/opencode/src/permission/index.ts | 7 +- packages/opencode/src/permission/next.ts | 226 +++++ packages/opencode/src/server/server.ts | 36 + packages/opencode/src/session/llm.ts | 14 +- packages/opencode/src/session/processor.ts | 51 +- packages/opencode/src/session/prompt.ts | 14 +- packages/opencode/src/session/system.ts | 2 +- packages/opencode/src/tool/bash.ts | 101 +- packages/opencode/src/tool/codesearch.ts | 32 +- packages/opencode/src/tool/edit.ts | 94 +- packages/opencode/src/tool/glob.ts | 20 +- packages/opencode/src/tool/grep.ts | 21 +- packages/opencode/src/tool/ls.ts | 19 +- packages/opencode/src/tool/patch.ts | 66 +- packages/opencode/src/tool/read.ts | 53 +- packages/opencode/src/tool/registry.ts | 23 - packages/opencode/src/tool/skill.ts | 144 ++- packages/opencode/src/tool/task.ts | 16 + packages/opencode/src/tool/webfetch.ts | 34 +- packages/opencode/src/tool/websearch.ts | 38 +- packages/opencode/src/tool/write.ts | 28 +- packages/opencode/test/agent/agent.test.ts | 468 ++++++++-- packages/opencode/test/config/config.test.ts | 218 ++++- packages/opencode/test/fixture/fixture.ts | 11 + .../opencode/test/permission/arity.test.ts | 33 + .../opencode/test/permission/next.test.ts | 663 +++++++++++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 39 + packages/sdk/js/src/v2/gen/types.gen.ts | 271 +++--- packages/sdk/openapi.json | 4 + 44 files changed, 3176 insertions(+), 1384 deletions(-) delete mode 100644 .opencode/agent/git-committer.md create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx create mode 100644 packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx create mode 100644 packages/opencode/src/permission/arity.ts create mode 100644 packages/opencode/src/permission/next.ts create mode 100644 packages/opencode/test/permission/arity.test.ts create mode 100644 packages/opencode/test/permission/next.test.ts diff --git a/.opencode/agent/git-committer.md b/.opencode/agent/git-committer.md deleted file mode 100644 index 49c3e3de19f..00000000000 --- a/.opencode/agent/git-committer.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -description: Use this agent when you are asked to commit and push code changes to a git repository. -mode: subagent ---- - -You commit and push to git - -Commit messages should be brief since they are used to generate release notes. - -Messages should say WHY the change was made and not WHAT was changed. diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index cbcbb0c6518..28ac3c4f891 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -5,6 +5,7 @@ // "url": "https://enterprise.dev.opencode.ai", // }, "instructions": ["STYLE_GUIDE.md"], + "permission": "ask", "provider": { "opencode": { "options": {}, diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index e6419dd7665..9c5cfd15ec6 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -80,7 +80,7 @@ export namespace ACP { toolCall: { toolCallId: permission.callID ?? permission.id, status: "pending", - title: permission.title, + title: permission.message, rawInput: permission.metadata, kind: toToolKind(permission.type), locations: toLocations(permission.type, permission.metadata), diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index ad665e5d6ee..46a43a6deb5 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -4,16 +4,14 @@ import { Provider } from "../provider/provider" import { generateObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" -import { mergeDeep } from "remeda" -import { Log } from "../util/log" - -const log = Log.create({ service: "agent" }) import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" import PROMPT_EXPLORE from "./prompt/explore.txt" import PROMPT_SUMMARY from "./prompt/summary.txt" import PROMPT_TITLE from "./prompt/title.txt" +import { PermissionNext } from "@/permission/next" +import { mergeDeep } from "remeda" export namespace Agent { export const Info = z @@ -23,18 +21,10 @@ export namespace Agent { mode: z.enum(["subagent", "primary", "all"]), native: z.boolean().optional(), hidden: z.boolean().optional(), - default: z.boolean().optional(), topP: z.number().optional(), temperature: z.number().optional(), color: z.string().optional(), - permission: z.object({ - edit: Config.Permission, - bash: z.record(z.string(), Config.Permission), - skill: z.record(z.string(), Config.Permission), - webfetch: Config.Permission.optional(), - doom_loop: Config.Permission.optional(), - external_directory: Config.Permission.optional(), - }), + permission: PermissionNext.Ruleset, model: z .object({ modelID: z.string(), @@ -42,9 +32,8 @@ export namespace Agent { }) .optional(), prompt: z.string().optional(), - tools: z.record(z.string(), z.boolean()), options: z.record(z.string(), z.any()), - maxSteps: z.number().int().positive().optional(), + steps: z.number().int().positive().optional(), }) .meta({ ref: "Agent", @@ -53,113 +42,72 @@ export namespace Agent { const state = Instance.state(async () => { const cfg = await Config.get() - const defaultTools = cfg.tools ?? {} - const defaultPermission: Info["permission"] = { - edit: "allow", - bash: { + const permission: PermissionNext.Ruleset = PermissionNext.merge( + PermissionNext.fromConfig({ "*": "allow", - }, - skill: { - "*": "allow", - }, - webfetch: "allow", - doom_loop: "ask", - external_directory: "ask", - } - const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {}) - - const planPermission = mergeAgentPermissions( - { - edit: "deny", - bash: { - "cut*": "allow", - "diff*": "allow", - "du*": "allow", - "file *": "allow", - "find * -delete*": "ask", - "find * -exec*": "ask", - "find * -fprint*": "ask", - "find * -fls*": "ask", - "find * -fprintf*": "ask", - "find * -ok*": "ask", - "find *": "allow", - "git diff*": "allow", - "git log*": "allow", - "git show*": "allow", - "git status*": "allow", - "git branch": "allow", - "git branch -v": "allow", - "grep*": "allow", - "head*": "allow", - "less*": "allow", - "ls*": "allow", - "more*": "allow", - "pwd*": "allow", - "rg*": "allow", - "sort --output=*": "ask", - "sort -o *": "ask", - "sort*": "allow", - "stat*": "allow", - "tail*": "allow", - "tree -o *": "ask", - "tree*": "allow", - "uniq*": "allow", - "wc*": "allow", - "whereis*": "allow", - "which*": "allow", - "*": "ask", - }, - webfetch: "allow", - }, - cfg.permission ?? {}, + doom_loop: "ask", + external_directory: "ask", + }), + PermissionNext.fromConfig(cfg.permission ?? {}), ) const result: Record = { build: { name: "build", - tools: { ...defaultTools }, options: {}, - permission: agentPermission, + permission, mode: "primary", native: true, }, plan: { name: "plan", options: {}, - permission: planPermission, - tools: { - ...defaultTools, - }, + permission: PermissionNext.merge( + permission, + PermissionNext.fromConfig({ + edit: { + "*": "deny", + ".opencode/plan/*.md": "allow", + }, + }), + ), mode: "primary", native: true, }, general: { name: "general", description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`, - tools: { - todoread: false, - todowrite: false, - ...defaultTools, - }, + permission: PermissionNext.merge( + permission, + PermissionNext.fromConfig({ + todoread: "deny", + todowrite: "deny", + }), + ), options: {}, - permission: agentPermission, mode: "subagent", native: true, hidden: true, }, explore: { name: "explore", - tools: { - todoread: false, - todowrite: false, - edit: false, - write: false, - ...defaultTools, - }, + permission: PermissionNext.merge( + permission, + PermissionNext.fromConfig({ + "*": "deny", + grep: "allow", + glob: "allow", + list: "allow", + bash: "allow", + webfetch: "allow", + websearch: "allow", + codesearch: "allow", + read: "allow", + }), + ), description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`, prompt: PROMPT_EXPLORE, options: {}, - permission: agentPermission, mode: "subagent", native: true, }, @@ -169,11 +117,10 @@ export namespace Agent { native: true, hidden: true, prompt: PROMPT_COMPACTION, - tools: { - "*": false, - }, + permission: PermissionNext.fromConfig({ + "*": "deny", + }), options: {}, - permission: agentPermission, }, title: { name: "title", @@ -181,9 +128,10 @@ export namespace Agent { options: {}, native: true, hidden: true, - permission: agentPermission, + permission: PermissionNext.fromConfig({ + "*": "deny", + }), prompt: PROMPT_TITLE, - tools: {}, }, summary: { name: "summary", @@ -191,11 +139,13 @@ export namespace Agent { options: {}, native: true, hidden: true, - permission: agentPermission, + permission: PermissionNext.fromConfig({ + "*": "deny", + }), prompt: PROMPT_SUMMARY, - tools: {}, }, } + for (const [key, value] of Object.entries(cfg.agent ?? {})) { if (value.disable) { delete result[key] @@ -206,74 +156,22 @@ export namespace Agent { item = result[key] = { name: key, mode: "all", - permission: agentPermission, + permission, options: {}, - tools: {}, native: false, } - const { - name, - model, - prompt, - tools, - description, - temperature, - top_p, - mode, - permission, - color, - maxSteps, - ...extra - } = value - item.options = { - ...item.options, - ...extra, - } - if (model) item.model = Provider.parseModel(model) - if (prompt) item.prompt = prompt - if (tools) - item.tools = { - ...item.tools, - ...tools, - } - item.tools = { - ...defaultTools, - ...item.tools, - } - if (description) item.description = description - if (temperature != undefined) item.temperature = temperature - if (top_p != undefined) item.topP = top_p - if (mode) item.mode = mode - if (color) item.color = color - // just here for consistency & to prevent it from being added as an option - if (name) item.name = name - if (maxSteps != undefined) item.maxSteps = maxSteps - - if (permission ?? cfg.permission) { - item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {}) - } + if (value.model) item.model = Provider.parseModel(value.model) + item.prompt = value.prompt ?? item.prompt + item.description = value.description ?? item.description + item.temperature = value.temperature ?? item.temperature + item.topP = value.top_p ?? item.topP + item.mode = value.mode ?? item.mode + item.color = value.color ?? item.color + item.name = value.options?.name ?? item.name + item.steps = value.steps ?? item.steps + item.options = mergeDeep(item.options, value.options ?? {}) + item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {})) } - - // Mark the default agent - const defaultName = cfg.default_agent ?? "build" - const defaultCandidate = result[defaultName] - if (defaultCandidate && defaultCandidate.mode !== "subagent") { - defaultCandidate.default = true - } else { - // Fall back to "build" if configured default is invalid - if (result["build"]) { - result["build"].default = true - } - } - - const hasPrimaryAgents = Object.values(result).filter((a) => a.mode !== "subagent" && !a.hidden).length > 0 - if (!hasPrimaryAgents) { - throw new Config.InvalidError({ - path: "config", - message: "No primary agents are available. Please configure at least one agent with mode 'primary' or 'all'.", - }) - } - return result }) @@ -285,10 +183,8 @@ export namespace Agent { return state().then((x) => Object.values(x)) } - export async function defaultAgent(): Promise { - const agents = await state() - const defaultCandidate = Object.values(agents).find((a) => a.default) - return defaultCandidate?.name ?? "build" + export async function defaultAgent() { + return state().then((x) => Object.keys(x)[0]) } export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) { @@ -329,70 +225,3 @@ export namespace Agent { return result.object } } - -function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] { - if (typeof basePermission.bash === "string") { - basePermission.bash = { - "*": basePermission.bash, - } - } - if (typeof overridePermission.bash === "string") { - overridePermission.bash = { - "*": overridePermission.bash, - } - } - - if (typeof basePermission.skill === "string") { - basePermission.skill = { - "*": basePermission.skill, - } - } - if (typeof overridePermission.skill === "string") { - overridePermission.skill = { - "*": overridePermission.skill, - } - } - const merged = mergeDeep(basePermission ?? {}, overridePermission ?? {}) as any - let mergedBash - if (merged.bash) { - if (typeof merged.bash === "string") { - mergedBash = { - "*": merged.bash, - } - } else if (typeof merged.bash === "object") { - mergedBash = mergeDeep( - { - "*": "allow", - }, - merged.bash, - ) - } - } - - let mergedSkill - if (merged.skill) { - if (typeof merged.skill === "string") { - mergedSkill = { - "*": merged.skill, - } - } else if (typeof merged.skill === "object") { - mergedSkill = mergeDeep( - { - "*": "allow", - }, - merged.skill, - ) - } - } - - const result: Agent.Info["permission"] = { - edit: merged.edit ?? "allow", - webfetch: merged.webfetch ?? "allow", - bash: mergedBash ?? { "*": "allow" }, - skill: mergedSkill ?? { "*": "allow" }, - doom_loop: merged.doom_loop, - external_directory: merged.external_directory, - } - - return result -} diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index 60dd9cc75a2..b57de0ae464 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -241,7 +241,8 @@ const AgentListCommand = cmd({ }) for (const agent of sortedAgents) { - process.stdout.write(`${agent.name} (${agent.mode})${EOL}`) + process.stdout.write(`${agent.name} (${agent.mode})` + EOL) + process.stdout.write(` ${JSON.stringify(agent.permission, null, 2)}` + EOL) } }, }) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 0c371b864ce..876570fd6c0 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -202,14 +202,14 @@ export const RunCommand = cmd({ break } - if (event.type === "permission.updated") { + if (event.type === "permission.next.asked") { const permission = event.properties if (permission.sessionID !== sessionID) continue const result = await select({ - message: `Permission required to run: ${permission.title}`, + message: `Permission required to run: ${permission.message}`, options: [ { value: "once", label: "Allow once" }, - { value: "always", label: "Always allow" }, + { value: "always", label: "Always allow: " + permission.always.join(", ") }, { value: "reject", label: "Reject" }, ], initialValue: "once", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8b7b68273ac..f71bab12551 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -4,7 +4,6 @@ import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { Installation } from "@/installation" -import { Global } from "@/global" import { Flag } from "@/flag/flag" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" @@ -35,6 +34,7 @@ import { Provider } from "@/provider/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" +import { Permission } from "./component/dialog-permission" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { // can't set raw mode if not a TTY @@ -608,6 +608,7 @@ function App() { } }} > + diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx new file mode 100644 index 00000000000..51ce27b77fb --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx @@ -0,0 +1,53 @@ +import { onMount } from "solid-js" +import { useDialog } from "../ui/dialog" +import { TextAttributes } from "@opentui/core" +import { useTheme } from "../context/theme" + +export function Permission() { + const dialog = useDialog() + onMount(() => {}) + return null +} + +function DialogPermission() { + const dialog = useDialog() + const { theme } = useTheme() + + onMount(() => { + dialog.setSize("medium") + }) + + return ( + { + console.log(e) + }} + ref={(r) => { + setTimeout(() => { + r?.focus() + }, 1) + }} + > + + Permission Request + esc + + Change to foo directory and create bar file + $ cd foo && touch bar + + + Allow + + + Always allow the touch command + + + Reject + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 2528a499896..7d67f3e76ae 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -7,7 +7,7 @@ import type { Config, Todo, Command, - Permission, + PermissionRequest, LspStatus, McpStatus, FormatterStatus, @@ -39,7 +39,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ agent: Agent[] command: Command[] permission: { - [sessionID: string]: Permission[] + [sessionID: string]: PermissionRequest[] } config: Config session: Session[] @@ -97,36 +97,38 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.event.listen((e) => { const event = e.details switch (event.type) { - case "permission.updated": { - const permissions = store.permission[event.properties.sessionID] - if (!permissions) { - setStore("permission", event.properties.sessionID, [event.properties]) - break - } - const match = Binary.search(permissions, event.properties.id, (p) => p.id) + case "permission.next.replied": { + const requests = store.permission[event.properties.sessionID] + if (!requests) break + const match = Binary.search(requests, event.properties.requestID, (r) => r.id) + if (!match.found) break setStore( "permission", event.properties.sessionID, produce((draft) => { - if (match.found) { - draft[match.index] = event.properties - return - } - draft.push(event.properties) + draft.splice(match.index, 1) }), ) break } - case "permission.replied": { - const permissions = store.permission[event.properties.sessionID] - const match = Binary.search(permissions, event.properties.permissionID, (p) => p.id) - if (!match.found) break + case "permission.next.asked": { + const request = event.properties + const requests = store.permission[request.sessionID] + if (!requests) { + setStore("permission", request.sessionID, [request]) + break + } + const match = Binary.search(requests, request.id, (r) => r.id) + if (match.found) { + setStore("permission", request.sessionID, match.index, reconcile(request)) + break + } setStore( "permission", - event.properties.sessionID, + request.sessionID, produce((draft) => { - draft.splice(match.index, 1) + draft.splice(match.index, 0, request) }), ) break diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx index 69082c870ba..3d1315ccde7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx @@ -59,7 +59,7 @@ export function Footer() { 0}> - {permissions().length} Permission + {permissions().length} Permission {permissions().length > 1 ? "s" : ""} 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 374645abb35..d770931af39 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -9,7 +9,6 @@ import { Show, Switch, useContext, - type Component, } from "solid-js" import { Dynamic } from "solid-js/web" import path from "path" @@ -23,6 +22,7 @@ import { addDefaultParsers, MacOSScrollAccel, type ScrollAcceleration, + TextAttributes, } from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2" @@ -40,7 +40,7 @@ import type { EditTool } from "@/tool/edit" import type { PatchTool } from "@/tool/patch" import type { WebFetchTool } from "@/tool/webfetch" import type { TaskTool } from "@/tool/task" -import { useKeyboard, useRenderer, useTerminalDimensions, type BoxProps, type JSX } from "@opentui/solid" +import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import { useSDK } from "@tui/context/sdk" import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "@tui/context/keybind" @@ -66,6 +66,7 @@ import stripAnsi from "strip-ansi" import { Footer } from "./footer.tsx" import { usePromptRef } from "../../context/prompt" import { Filesystem } from "@/util/filesystem" +import { PermissionPrompt } from "./permission" import { DialogExportOptions } from "../../ui/dialog-export-options" addDefaultParsers(parsers.parsers) @@ -82,12 +83,12 @@ class CustomSpeedScroll implements ScrollAcceleration { const context = createContext<{ width: number + sessionID: string conceal: () => boolean showThinking: () => boolean showTimestamps: () => boolean usernameVisible: () => boolean showDetails: () => boolean - userMessageMarkdown: () => boolean diffWrapMode: () => "word" | "none" sync: ReturnType }>() @@ -125,7 +126,6 @@ export function Session() { const [usernameVisible, setUsernameVisible] = createSignal(kv.get("username_visible", true)) const [showDetails, setShowDetails] = createSignal(kv.get("tool_details_visibility", true)) const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false)) - const [userMessageMarkdown, setUserMessageMarkdown] = createSignal(kv.get("user_message_markdown", true)) const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word") const [animationsEnabled, setAnimationsEnabled] = createSignal(kv.get("animations_enabled", true)) @@ -571,19 +571,6 @@ export function Session() { dialog.clear() }, }, - { - title: userMessageMarkdown() ? "Disable user message markdown" : "Enable user message markdown", - value: "session.toggle.user_message_markdown", - category: "Session", - onSelect: (dialog) => { - setUserMessageMarkdown((prev) => { - const next = !prev - kv.set("user_message_markdown", next) - return next - }) - dialog.clear() - }, - }, { title: animationsEnabled() ? "Disable animations" : "Enable animations", value: "session.toggle.animations", @@ -990,12 +977,12 @@ export function Session() { get width() { return contentWidth() }, + sessionID: route.sessionID, conceal, showThinking, showTimestamps, usernameVisible, showDetails, - userMessageMarkdown, diffWrapMode, sync, }} @@ -1121,17 +1108,24 @@ export function Session() { - { - prompt = r - promptRef.set(r) - }} - disabled={permissions().length > 0} - onSubmit={() => { - toBottom() - }} - sessionID={route.sessionID} - /> + + 0}> + + + + { + prompt = r + promptRef.set(r) + }} + disabled={permissions().length > 0} + onSubmit={() => { + toBottom() + }} + sessionID={route.sessionID} + /> + +