[Sync] opencode 2.x

This commit is contained in:
Coja
2026-09-17 21:25:54 +02:00
parent d8f6b30e2c
commit 4f5054ed0d
9 changed files with 161 additions and 163 deletions
+11 -4
View File
@@ -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
+73
View File
@@ -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: `<leader>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.
+1 -41
View File
@@ -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"
}
}
}
}
@@ -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() {},
}
@@ -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 <leader>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<Record<string, unknown>>
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) : "<leader>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 })
},
},
],
}))
},
}
@@ -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": "<leader>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<boolean>(KEYS.details, true) === true,
generic: api.kv.get<boolean>(KEYS.generic, false) === true,
timestamps: api.kv.get<string>(KEYS.timestamps, "hide") === "show",
thinking: api.kv.get<string>(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<string, unknown>
const key = typeof opts.key === "string" && opts.key.length > 0 ? opts.key : "<leader>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
+1 -4
View File
@@ -1,7 +1,6 @@
{
"$schema": "https://opencode.ai/tui.json",
"theme": "lucent-orng",
"plugin": [["./tui-plugins/verbose.ts", { "key": "<leader>v" }]],
"leader_timeout": 1500,
"diff_style": "stacked",
"cursor": { "style": "block", "blinking": false },
@@ -11,8 +10,6 @@
"prompt_stash_pop": "<leader>o",
"session_fork": "<leader>f",
"messages_last_user": "<leader>k",
"display_thinking": "<leader>i",
"tool_details": "<leader>d",
"session_toggle_timestamps": "<leader>w"
"display_thinking": "<leader>i"
}
}