From 4f5054ed0d4487194003c424353a496ed47f20e6 Mon Sep 17 00:00:00 2001 From: Coja Date: Thu, 17 Sep 2026 21:25:54 +0200 Subject: [PATCH] [Sync] opencode 2.x --- README.md | 6 +- common/.config/fish/config.fish | 15 +++- common/.config/opencode/README.md | 73 +++++++++++++++++++ common/.config/opencode/opencode.json | 42 +---------- .../opencode/plugins/verbose/server.ts | 8 ++ .../.config/opencode/plugins/verbose/tui.ts | 62 ++++++++++++++++ .../.config/opencode/tui-plugins/verbose.ts | 71 ------------------ common/.config/opencode/tui.json | 5 +- lw/.config/opencode/opencode.json | 42 +---------- 9 files changed, 161 insertions(+), 163 deletions(-) create mode 100644 common/.config/opencode/README.md create mode 100644 common/.config/opencode/plugins/verbose/server.ts create mode 100644 common/.config/opencode/plugins/verbose/tui.ts delete mode 100644 common/.config/opencode/tui-plugins/verbose.ts diff --git a/README.md b/README.md index 214edb5..ca66a8e 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,10 @@ Because configs are symlinks into the repo, **you almost never run `install.sh` | fl's Hyprland | overrides ~9 hypr files (`hyprland.conf`, keybinds, windowrules, theme colors, monitors); inherits the rest from `gui` | | Unique apps | in the owning host overlay (fl: llamacpp/evremap; lw: xremap) | -`aichat`, `pi`, and `opencode` are **single shared configs** in `common` (LAN-first, with the cloud -provider also listed so any host can switch) — no per-host overrides. +`aichat`, `pi`, and `opencode` share one config in `common` (LAN-first, with the cloud provider also +listed so any host can switch). `lw` carries a host overlay of all three that adds its local CPU +llama.cpp models — mirror edits between the common and `lw` copies. The opencode tree is documented +in `common/.config/opencode/README.md`. ## HyDE dependency (important for GUI hosts) diff --git a/common/.config/fish/config.fish b/common/.config/fish/config.fish index 0dbabf0..a2b7393 100644 --- a/common/.config/fish/config.fish +++ b/common/.config/fish/config.fish @@ -5,10 +5,17 @@ set -gx EDITOR nvim set -gx PAGER less set -gx QT_FONT_DPI 96 set -gx PI_SKIP_VERSION_CHECK 1 -# opencode GPT/Codex OAuth overlay — only where `opencode auth login` stored an OpenAI credential, -# so hosts without the login don't get an unusable provider in the model picker. -if test -r ~/.local/share/opencode/auth.json; and grep -q '"openai"' ~/.local/share/opencode/auth.json - set -gx OPENCODE_CONFIG "$HOME/.config/opencode/openai-gpt.jsonc" +# opencode GPT/Codex OAuth overlay (~/.config/opencode/openai-gpt.jsonc). opencode 2.x ignores +# OPENCODE_CONFIG / OPENCODE_CONFIG_CONTENT, but it merges a second global file, opencode.jsonc. +# Link the overlay under that name only where `opencode auth login` stored an OpenAI credential, +# so other hosts don't get an unusable provider; drop our link again if the login goes away. +# (Alternative: ship the link from the host's dots overlay instead of this startup check.) +set -l _oc_auth ~/.local/share/opencode/auth.json +set -l _oc_link ~/.config/opencode/opencode.jsonc +if test -r $_oc_auth; and grep -q '"openai"' $_oc_auth + test -e $_oc_link; or ln -s openai-gpt.jsonc $_oc_link +else if test -L $_oc_link; and test (readlink $_oc_link) = openai-gpt.jsonc + rm $_oc_link end set -gx MANPAGER "nvim +Man!" # fzf's built-in walker is the one search tool that ignores .ignore files, so point it at diff --git a/common/.config/opencode/README.md b/common/.config/opencode/README.md new file mode 100644 index 0000000..f608fe9 --- /dev/null +++ b/common/.config/opencode/README.md @@ -0,0 +1,73 @@ +# opencode config + +Config tree for [opencode](https://opencode.ai) (2.x), tuned for **small local models** behind a +llama.cpp router. Stowed into `~/.config/opencode/` as per-file symlinks; editing here edits the live +config (restart opencode, or `opencode service restart`, to pick changes up — the 2.x background +service caches config). + +## Layout + +| Path | What | +|---|---| +| `opencode.json` | Providers, permissions, primary agents, defaults. Written in the v1 schema, which 2.x still accepts. | +| `openai-gpt.jsonc` | Optional GPT/Codex OAuth overlay (OpenAI provider + `opencode-openai-codex-auth` plugin). Not loaded by itself — see *GPT overlay*. | +| `AGENTS.md` | Global rules prepended to every session. Same text as the pi config's `AGENTS.md`: terse, verify-before-asserting, small-model friendly. | +| `agents/*.md` | Four subagents with real prompts and scoped permissions: `code-reviewer`, `test-runner` (hidden), `doc-writer` (edits docs only), `security-auditor` (hidden). | +| `commands/*.md` | `/commit` (Conventional Commits message, never commits) and `/review` (read-only diff review), both on the `plan` agent. | +| `plugins/verbose/` | 2.x TUI plugin: `v`, `/verbose`, or the palette entry toggles the *verbose view* — thinking blocks expanded and tool calls ungrouped — reading the live state from the built-in toggles so a mixed state converges. `server.ts` is a no-op half required by the loader. | +| `themes/catppuccin-mocha.json` | Catppuccin Mocha (dark) / Latte (light) theme following the pi theme's semantic mapping. Built-in `catppuccin` exists too; this one matches pi. | +| `tui.json` | TUI settings in the v1 schema (2.x compat): theme, leader timeout, stacked diffs, block cursor, attention notifications, a few extra keybinds. | + +The sandbox wrapper lives in `../fish/functions/opencodes.fish`: `opencodes` runs opencode under +bwrap with the stowed config dirs, `~/.cache/opencode`, state and data bound, and only the needed +environment passed through. Extra arguments go to opencode. + +## Providers and models + +Two OpenAI-compatible providers point at the same llama.cpp router: `llama.cpp` on the LAN and +`duskadiy` through the public hostname (`apiKey` from `$DUSKADIY_API_KEY`). Model ids are the preset +section names in `fl/.config/llamacpp/config.ini`; keep the lists in sync with it and with the pi and +aichat configs. Both providers use a one-hour request timeout — the default five minutes cut off +long local replies. + +Decisions that look odd but are deliberate: + +- **No `small_model`.** The router runs `--models-max 1`; any second model id evicts the resident one. + With `small_model` unset, title generation falls back to the session's model. Subagents carry no + pinned model for the same reason — the task tool uses the caller's model. +- **No `temperature` / `top_p` from opencode.** Verified by capturing request bodies: only + `max_tokens` is sent, so the router presets own sampling. +- **`reasoning: true`** only on the presets that actually think (no `reasoning-budget = 0`); + GLM also replays `reasoning_content` in history because its preset sets `reasoning-preserve`. +- **`snapshot: false`** — file-change undo is not used. **`tool_output`** capped at 200 lines / + 16 KiB for 24k-context models. **`compaction.reserved`/`prune`** are inert at this context size. +- **`question: allow`** everywhere so agents ask instead of guessing. + +## Agents + +`auto` is the default agent and cycles with `build` and `plan` on Tab. It runs without prompts except +for the serious set — git history rewriting and pushing, `rm`, killing processes, `systemctl`, +network transfers, package installs — which ask, and `sudo`/`dd`/`mkfs`/secret-file reads, which are +denied. Colors: build primary, plan info, auto yellow. All three cap agentic iterations at 30. + +Permission rules resolve **global first, agent rules appended, last match wins**, so an agent that +wants a blanket rule must restate its own denies after it (the `auto` block does). + +## GPT overlay + +`openai-gpt.jsonc` is a template. opencode 2.x ignores `OPENCODE_CONFIG`, but it merges a second +global file named `opencode.jsonc`. `fish/config.fish` links `opencode.jsonc → openai-gpt.jsonc` +only on a host whose `~/.local/share/opencode/auth.json` holds an OpenAI credential (from +`opencode auth login`), and removes the link again when the login is gone. + +## Host overlays + +`lw/.config/opencode/opencode.json` is a fork of the common file that adds the laptop's local CPU +llama.cpp provider (`lwcpp`). Mirror every edit between the two. + +## Lifting pieces into a stock opencode + +Everything is plain files: copy `agents/`, `commands/`, `plugins/verbose/` or the theme into your own +`~/.config/opencode/`. The plugin needs 2.x (it uses the `keymap.layer` / `dispatch` API and is +discovered as a directory under `plugins/`). The permission blocks in `opencode.json` are independent +of the providers and can be pasted as-is. diff --git a/common/.config/opencode/opencode.json b/common/.config/opencode/opencode.json index 110962b..3b47917 100644 --- a/common/.config/opencode/opencode.json +++ b/common/.config/opencode/opencode.json @@ -1,6 +1,7 @@ { "$schema": "https://opencode.ai/config.json", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", + "default_agent": "auto", "instructions": ["CLAUDE.md", "AGENTS.md"], "share": "disabled", "autoupdate": "notify", @@ -303,47 +304,6 @@ "grep * .env*": "deny" } } - }, - "code-reviewer": { - "description": "Reviews code for best practices and potential issues", - "prompt": "You are a code reviewer. Focus on security, performance, and maintainability.", - "mode": "subagent", - "permission": { - "edit": "deny" - } - }, - "test-runner": { - "description": "Runs and interprets test suites", - "prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.", - "mode": "subagent", - "hidden": true, - "permission": { - "edit": "deny" - } - }, - "doc-writer": { - "description": "Maintains project documentation and docstrings", - "prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.", - "mode": "subagent", - "permission": { - "edit": { - "*": "deny", - "**/*.md": "allow", - "**/*.mdx": "allow", - "**/*.markdown": "allow", - "**/*.txt": "allow", - "**/*.rst": "allow" - } - } - }, - "security-auditor": { - "description": "Scans for security vulnerabilities and hardcoded secrets", - "prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.", - "mode": "subagent", - "hidden": true, - "permission": { - "edit": "deny" - } } } } diff --git a/common/.config/opencode/plugins/verbose/server.ts b/common/.config/opencode/plugins/verbose/server.ts new file mode 100644 index 0000000..82e5a3f --- /dev/null +++ b/common/.config/opencode/plugins/verbose/server.ts @@ -0,0 +1,8 @@ +// opencode v2 plugin "coja.verbose" — server half. Intentionally empty: all behaviour is in ./tui.ts. +// A local plugin is a DIRECTORY under ~/.config/opencode/plugins/ whose `server`/`index` and `tui` +// modules are resolved by name (packages/plugin/src/host.ts resolve()). No imports on purpose: +// `define()` is an identity helper, and a dependency-free module needs no node_modules. +export default { + id: "coja.verbose", + setup() {}, +} diff --git a/common/.config/opencode/plugins/verbose/tui.ts b/common/.config/opencode/plugins/verbose/tui.ts new file mode 100644 index 0000000..6135d06 --- /dev/null +++ b/common/.config/opencode/plugins/verbose/tui.ts @@ -0,0 +1,62 @@ +// opencode v2 TUI plugin: one key toggles the "verbose view". +// +// v2 dropped the v1 tool-details / timestamps / generic-output toggles. The session view now has two +// display toggles that matter: thinking blocks (session.thinking show|hide) and exploration grouping +// (session.grouping auto|none — "auto" folds read/glob/grep tool calls into one collapsed group). +// Verbose = thinking expanded AND tool calls shown individually. Both settings persist in the TUI config. +// +// State is read from the built-in commands' live titles (the only plugin-visible signal): +// session.toggle.thinking → "Collapse thinking" while expanded, else "Expand thinking" +// session.toggle.exploration_grouping → "Show tool calls individually" while grouped, else "Group related tool calls" +// then only the toggles that differ from the target are dispatched, so a mixed state converges. +// +// Auto-discovered from ~/.config/opencode/plugins/verbose/ (server.ts + this file). Key configurable +// via plugin options; default v. Also /verbose and a command-palette entry. +const THINKING = "session.toggle.thinking" +const GROUPING = "session.toggle.exploration_grouping" + +type Command = { readonly id?: string; readonly title?: string } +type Context = { + readonly options?: Readonly> + readonly keymap: { + layer(input: () => { readonly commands?: readonly unknown[] }): void + dispatch(id: string, input?: string): void + commands(): readonly Command[] + } + readonly ui: { readonly toast: { show(o: { message: string; variant?: string; duration?: number }): void } } +} + +export default { + id: "coja.verbose", + setup(context: Context) { + const key = typeof context.options?.key === "string" && context.options.key ? (context.options.key as string) : "v" + const title = (id: string) => context.keymap.commands().find((c) => c.id === id)?.title ?? "" + + context.keymap.layer(() => ({ + commands: [ + { + id: "verbose.toggle", + title: "Toggle verbose view (expand thinking, ungroup tool calls)", + group: "Session", + bind: key, + palette: true, + slash: { name: "verbose" }, + run() { + const thinkingTitle = title(THINKING) + const groupingTitle = title(GROUPING) + if (!thinkingTitle && !groupingTitle) { + context.ui.toast.show({ message: "Verbose view: open a session first", variant: "warning", duration: 2000 }) + return + } + const thinkingVerbose = thinkingTitle.startsWith("Collapse") + const groupingVerbose = groupingTitle.startsWith("Group") + const on = !(thinkingVerbose && groupingVerbose) + if (thinkingTitle && thinkingVerbose !== on) context.keymap.dispatch(THINKING) + if (groupingTitle && groupingVerbose !== on) context.keymap.dispatch(GROUPING) + context.ui.toast.show({ message: on ? "Verbose view on" : "Verbose view off", variant: "info", duration: 1500 }) + }, + }, + ], + })) + }, +} diff --git a/common/.config/opencode/tui-plugins/verbose.ts b/common/.config/opencode/tui-plugins/verbose.ts deleted file mode 100644 index 44f9931..0000000 --- a/common/.config/opencode/tui-plugins/verbose.ts +++ /dev/null @@ -1,71 +0,0 @@ -// opencode TUI plugin: one key toggles the whole "verbose view". -// -// The session view keeps four independent display toggles in the TUI's persisted KV store -// (~/.local/state/opencode/kv.json). Built-in commands only flip one each. This plugin reads all -// four and drives them to the same state: if every one is on, turn all off; otherwise turn all on. -// Writing the KV keys directly is what the built-in toggles do too (kv.signal is a reactive -// store), so the open session re-renders immediately and the choice survives restarts. -// -// Declared in tui.json: "plugin": [["./tui-plugins/verbose.ts", { "key": "v" }]] -// (TUI plugins are never auto-discovered, and this dir is deliberately NOT ~/.config/opencode/plugins/, -// which the server-side plugin loader scans and would reject a TUI-only module.) -// Also available as /verbose and from the command palette. -import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" - -// KV keys + values as used by packages/tui/src/routes/session/index.tsx and context/thinking.ts (v1.18.29) -const KEYS = { - details: "tool_details_visibility", // boolean, default true - generic: "generic_tool_output_visibility", // boolean, default false - timestamps: "timestamps", // "show" | "hide", default "hide" - thinking: "thinking_mode", // "show" | "hide", default "hide" -} as const - -const COMMAND = "verbose.toggle" - -function current(api: TuiPluginApi) { - return { - details: api.kv.get(KEYS.details, true) === true, - generic: api.kv.get(KEYS.generic, false) === true, - timestamps: api.kv.get(KEYS.timestamps, "hide") === "show", - thinking: api.kv.get(KEYS.thinking, "hide") === "show", - } -} - -function apply(api: TuiPluginApi, on: boolean) { - api.kv.set(KEYS.details, on) - api.kv.set(KEYS.generic, on) - api.kv.set(KEYS.timestamps, on ? "show" : "hide") - api.kv.set(KEYS.thinking, on ? "show" : "hide") -} - -const tui: TuiPlugin = async (api, options) => { - const opts = (options ?? {}) as Record - const key = typeof opts.key === "string" && opts.key.length > 0 ? opts.key : "v" - - api.keymap.registerLayer({ - mode: "base", - commands: [ - { - name: COMMAND, - title: "Toggle verbose view (tool details, generic output, thinking, timestamps)", - category: "Session", - namespace: "palette", - slashName: "verbose", - run() { - const state = current(api) - const on = !Object.values(state).every(Boolean) - apply(api, on) - api.ui.toast({ - variant: "info", - message: on ? "Verbose view on" : "Verbose view off", - duration: 1500, - }) - }, - }, - ], - bindings: [{ key, cmd: COMMAND, desc: "Toggle verbose view" }], - }) -} - -const plugin: TuiPluginModule & { id: string } = { id: "coja.verbose", tui } -export default plugin diff --git a/common/.config/opencode/tui.json b/common/.config/opencode/tui.json index c760569..04ecb48 100644 --- a/common/.config/opencode/tui.json +++ b/common/.config/opencode/tui.json @@ -1,7 +1,6 @@ { "$schema": "https://opencode.ai/tui.json", "theme": "lucent-orng", - "plugin": [["./tui-plugins/verbose.ts", { "key": "v" }]], "leader_timeout": 1500, "diff_style": "stacked", "cursor": { "style": "block", "blinking": false }, @@ -11,8 +10,6 @@ "prompt_stash_pop": "o", "session_fork": "f", "messages_last_user": "k", - "display_thinking": "i", - "tool_details": "d", - "session_toggle_timestamps": "w" + "display_thinking": "i" } } diff --git a/lw/.config/opencode/opencode.json b/lw/.config/opencode/opencode.json index e767200..917479d 100644 --- a/lw/.config/opencode/opencode.json +++ b/lw/.config/opencode/opencode.json @@ -1,6 +1,7 @@ { "$schema": "https://opencode.ai/config.json", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", + "default_agent": "auto", "instructions": ["CLAUDE.md", "AGENTS.md"], "share": "disabled", "autoupdate": "notify", @@ -321,47 +322,6 @@ "grep * .env*": "deny" } } - }, - "code-reviewer": { - "description": "Reviews code for best practices and potential issues", - "prompt": "You are a code reviewer. Focus on security, performance, and maintainability.", - "mode": "subagent", - "permission": { - "edit": "deny" - } - }, - "test-runner": { - "description": "Runs and interprets test suites", - "prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.", - "mode": "subagent", - "hidden": true, - "permission": { - "edit": "deny" - } - }, - "doc-writer": { - "description": "Maintains project documentation and docstrings", - "prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.", - "mode": "subagent", - "permission": { - "edit": { - "*": "deny", - "**/*.md": "allow", - "**/*.mdx": "allow", - "**/*.markdown": "allow", - "**/*.txt": "allow", - "**/*.rst": "allow" - } - } - }, - "security-auditor": { - "description": "Scans for security vulnerabilities and hardcoded secrets", - "prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.", - "mode": "subagent", - "hidden": true, - "permission": { - "edit": "deny" - } } } }