diff --git a/CHANGELOG.md b/CHANGELOG.md index 980259f91e..90f911bd39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ file is the human-readable summary of what changed and why. ## Unreleased +### Agent + +- **MiniMax models get a prompt written for their class** — prompt selection + matches on the model id and had no MiniMax case, so every MiniMax model fell + through to the generic default, which is written for Claude. They now share + the prompt used for Kimi, which contains nothing Kimi-specific: it is the one + for open-weight agentic models, spelling out that tools must be used rather + than described, encouraging parallel tool calls, giving `` + authority, and matching the user's language. + ## 1.18.4-7 ### TUI diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 8fa9420335..4c4b5dbd66 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -180,7 +180,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx` | Permission-mode indicator + shift+tab cycle. keybind.ts: added `permission_mode_cycle: keybind("shift+tab", ...)` + its `permission.mode` CommandMap entry; `agent_cycle_reverse` default `"shift+tab"` → `"none"` (tab still cycles agents forward; previous-agent stays rebindable). prompt/index.tsx: the footer badge next to the agent name always renders in prompt mode — gray `manual` (`theme.textMuted`) in normal mode, red `auto` (`theme.error`) in auto mode (upstream only showed gray `auto`); the footer's shortcut hints gain a `shift+tab modes` text (via a `modeShortcut` `useCommandShortcut("permission.mode")` memo) left of `tab agents`, in the same `Show` gate (see the hints-and-usage row above for when that gate renders). app.tsx: `"permission.mode"` added to `appBindingCommands` so the binding is gathered. | The permission mode is always visible (manual/auto) with auto in red as a danger signal, and shift+tab toggles it without opening the palette. | Keep the `permission_mode_cycle` definition + mapping, the `agent_cycle_reverse` `"none"` default, the badge `Show` block, the `modeShortcut` memo + hint text, and the `appBindingCommands` entry; take upstream's surrounding changes. If upstream reworks the auto badge or rebinds shift+tab, re-apply on top and re-audit the shift+tab conflict. | | `packages/core/src/filesystem/search.ts`, `packages/core/src/ripgrep/binary.ts`, `packages/core/src/filesystem.ts`, `packages/core/src/tool/read-filesystem.ts`, `packages/core/test/filesystem/search.test.ts`, `packages/core/test/location-filesystem.test.ts`, `packages/core/test/ripgrep-binary.test.ts`, `packages/core/test/preload.ts` | Windows file-search fallback robustness. search.ts: the background ripgrep index walk no longer dies silently (`Effect.orDie` in a forked fiber) — each attempt resets the accumulated file/directory state, failures log a warning ("file index scan failed, file search may be empty"), and the walk retries forever on a 1s exponential backoff capped at 30s spacing, so installing rg mid-session self-heals; directory entries are suffixed and detected with `"/"` instead of `path.sep` (both ripgrep and fff layers), and fallback `find` queries normalize `\` to `/` and collapse doubled slashes (directory tab-expansion appends `/` to an already-`/`-suffixed entry). binary.ts: `Effect.cached` on ripgrep binary resolution (which memoizes failure exits for the process lifetime) replaced with a success-only cache behind a semaphore, so a failed rg download (offline, blocked proxy) retries on the next use instead of bricking search until restart; the download tail tries GitHub first (each attempt bounded to 30s) and falls back to the CoDev landing-page mirror (the base64-encoded `MIRROR` constant plus `/rg---[.exe]`, a raw executable — no archive/extract — staged in a scoped temp dir then copied into place), via `Effect.catchCause` so a proxy block page (200 + HTML) that dies in `extract` as a defect still reaches the mirror. filesystem.ts `list` + read-filesystem.ts `list`: directory suffix `path.sep` → `"/"`; `fs.list` also `/`-normalizes `path.relative` output. Tests: search.test.ts pins `"/"`-suffixed directories, adds query-normalization coverage, and a stub-Ripgrep scan-retry test asserting the partial first walk is discarded; location-filesystem.test.ts pins `"src/"`; ripgrep-binary.test.ts adds mocked-HTTP coverage of the fallback order, success caching, and failure non-caching; preload.ts redirects `XDG_CACHE_HOME` to a fresh temp dir so no test can read or clobber the developer's real `~/.cache/codev`. | On Windows fff is disabled by default (upstream `e4300e9b74`), so the `@` mention popup and file search depend entirely on the ripgrep fallback; a missing `rg.exe` plus one failed download left the index permanently empty with no user-visible signal (the CI-workflows row's choco preinstall papered over the same swallowed failure). `\`-suffixed directory entries also broke `endsWith("/")` directory-mime detection in the run CLI and poisoned drill-down queries with backslashes that can never match the `/`-normalized index. | Keep our retry/reset scan, success-only binary cache, `"/"` suffixes, and query normalization; take upstream's surrounding changes. If upstream reworks the fallback or re-enables fff on win32, preserve the invariants: entry paths are `/`-normalized with `/`-suffixed directories on every platform, index-scan failures are logged and retried, a failed binary download is never cached, and the GitHub download keeps its CoDev-mirror fallback — the mirror filename scheme must match what codev-landing-page hosts under `public/docs/code/downloads` (and codevhub's `RG_VERSION` pin in its `src/lib/ripgrep.ts`); bump all three together when changing `VERSION`. | | `packages/tui/src/context/permission.tsx`, `packages/tui/src/app.tsx`, `packages/tui/src/ui/dialog-confirm.tsx`, `packages/tui/src/ui/toast.tsx` | Auto-approve consent gate. permission.tsx: mode always starts `"normal"` (upstream initialized it from `args.auto`); `toggle()` replaced by `set(mode)` plus a per-run `consented` flag + `consent()`. app.tsx: shared `enableAutoApprove()` (title in the `AUTO_APPROVE_TITLE` module const, body from an `autoApproveMessage` accessor, and a shorter one-line `autoApproveToast` accessor for the toast, which only has a few seconds on screen; both redden the word "dangerous" with `theme.error` and are passed unevaluated because renderables can only be created during render) shows a `DialogConfirm` opened on cancel and, on confirm, a warning toast under the same title, then sets the mode; both entry points funnel through it — an `onMount` when `args.auto` is set, and the `permission.mode` command (shift+tab / palette), which enables via the gate and disables directly. Consent is asked once per run; later toggles are silent. dialog-confirm.tsx: optional `defaultChoice` prop + 5th `DialogConfirm.show` arg selecting the initially-active button (omitted keeps upstream's confirm-first default); `message` widened to `JSX.Element \| (() => JSX.Element)` and called at the render site. toast.tsx: same `message` widening, resolved by a `message()` helper in `Toast()` (fork-only test file: `packages/tui/test/auto-approve-consent.view.test.tsx`). | Auto-approve is dangerous, so `--auto` (and its hidden aliases) and the first shift+tab into auto require explicit confirmation; declining or dismissing the dialog stays in manual mode, opening on cancel keeps a stray return from granting it, and the risk word is red like the `auto` badge. | Keep the consent flag + gate flow, the `toggle()` removal, the `defaultChoice` prop, and the accessor-aware `message` on both dialog and toast; take upstream's surrounding changes. If upstream adds new writers of the permission mode, route their enable path through `enableAutoApprove()`. | -| `packages/opencode/src/session/system.ts` | Upstream's `provider()` branch chain is renamed `select()` (body otherwise untouched) and `provider()` now returns `select(model).map(rebrand)`; `environment()` gained a leading line naming the product and the `codev` command | The prompts the model receives are upstream's, so it identified itself as opencode and sent users to upstream's issue tracker and docs. Rewriting names as the prompt is assembled keeps the `prompt/*.txt` files byte-identical to upstream, so their hunks always apply and a newly added upstream brand mention is rebranded automatically. The identity also goes in the environment block because an agent's own `prompt` replaces the provider prompt wholesale (see `llm/request.ts`), and the environment block is on every path. | Keep the wrapper and the identity line; take upstream's `select()` body verbatim — never hand-rebrand the prompt text it returns. A new upstream prompt file or branch needs nothing here; `test/session/brand.test.ts` fails if its wording isn't covered by the rules in `packages/codev-brand`. | +| `packages/opencode/src/session/system.ts` | Upstream's `provider()` branch chain is renamed `select()` and `provider()` now returns `select(model).map(rebrand)`; inside `select()` the `kimi` branch also matches `minimax`; `environment()` gained a leading line naming the product and the `codev` command | The prompts the model receives are upstream's, so it identified itself as opencode and sent users to upstream's issue tracker and docs. Rewriting names as the prompt is assembled keeps the `prompt/*.txt` files byte-identical to upstream, so their hunks always apply and a newly added upstream brand mention is rebranded automatically. The identity also goes in the environment block because an agent's own `prompt` replaces the provider prompt wholesale (see `llm/request.ts`), and the environment block is on every path. MiniMax is a primary model for this fork's gateway and had no branch at all, so it fell through to `PROMPT_DEFAULT`, which is written for Claude; `PROMPT_KIMI` contains nothing Kimi-specific and is upstream's prompt for open-weight agentic models. | Keep the wrapper, the identity line, and `minimax` on the kimi branch; take upstream's `select()` body verbatim otherwise — never hand-rebrand the prompt text it returns. If upstream adds its own MiniMax branch, drop ours and keep theirs. A new upstream prompt file or branch needs nothing here; `test/session/brand.test.ts` fails if its wording isn't covered by the rules in `packages/codev-brand`. | | `packages/opencode/src/tool/registry.ts` | The assembled tool `description` is wrapped in `rebrand(...)` | Tool descriptions are model-visible and name the product (`tool/lsp.txt`). One wrapper at the point where every tool's final description is built covers all of them, including text upstream adds later. | Keep the wrapper around whatever expression upstream's description becomes; take upstream's surrounding changes. | | `packages/core/src/plugin/skill.ts`, `packages/opencode/src/skill/index.ts`, `packages/core/test/plugin/skill.test.ts` | Built-in skill `customize-opencode` → `customize-codev` in both registrations (plus `location` `/builtin/customize-codev.md`); descriptions rewritten to name CoDev Code; the markdown body wrapped in `rebrand(...)`; `Skill.get`/`Skill.require` resolve the old name through a `lookup` helper; test expectations updated | The skill's name and description are listed in the system prompt, so they are among the most visible brand strings the model sees; the body is upstream's config guide. The alias keeps existing configs and `skill` permission rules that name `customize-opencode` working. | Keep our name, the alias, and the `rebrand` wrapper; take upstream's `customize-opencode.md` body verbatim (it is deliberately not hand-edited for branding). | | `packages/opencode/src/command/template/initialize.txt` | "OpenCode" → "CoDev Code" (×2) | `/init` writes this guidance into the user's `AGENTS.md`. Edited in place rather than wrapped because the file is already fork-divergent (config-file rename row) and barely churns. | Keep our two strings; take upstream's surrounding text and rebrand any new "OpenCode" mention. | diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index a4d429d999..0a76d8354e 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -42,7 +42,12 @@ function select(model: Provider.Model) { if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI] if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC] if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY] - if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI] + // PROMPT_KIMI carries nothing Kimi-specific: it is the prompt for open-weight + // agentic models, which need tool use spelled out, parallel calls encouraged, + // `` given authority, and the user's language matched. That + // fits MiniMax better than PROMPT_DEFAULT, which is written for Claude. + if (model.api.id.toLowerCase().includes("kimi") || model.api.id.toLowerCase().includes("minimax")) + return [PROMPT_KIMI] return [PROMPT_DEFAULT] } diff --git a/packages/opencode/test/session/brand.test.ts b/packages/opencode/test/session/brand.test.ts index 2037f5507f..afda6309a9 100644 --- a/packages/opencode/test/session/brand.test.ts +++ b/packages/opencode/test/session/brand.test.ts @@ -13,6 +13,7 @@ const model = (id: string) => ({ api: { id }, providerID: "test" }) as Provider. // One id per branch of SystemPrompt.provider, so a prompt file that stops being // reachable — or a new branch — shows up here. const MODELS = [ + "unmatched-model-v1", "MiniMax/MiniMax-M3", "claude-opus-4", "gemini-2.5-pro", @@ -34,7 +35,7 @@ describe("brand", () => { }) test("provider prompts route support at the fork", () => { - const prompt = SystemPrompt.provider(model("MiniMax/MiniMax-M3"))[0] + const prompt = SystemPrompt.provider(model("unmatched-model-v1"))[0] expect(prompt).toContain("https://github.com/quickbeard/codev-code/issues") // Literal, not DOCS_URL, so this independently pins the docs base the model // is handed. Base64 for the same reason codev-brand encodes it. diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index d9d47be3df..86ca9f0541 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -90,6 +90,13 @@ describe("session.system", () => { ) }) + test("MiniMax model IDs share the open-weight agentic prompt with Kimi", () => { + const kimi = SystemPrompt.provider({ api: { id: "kimi-k2-thinking" } } as Provider.Model)[0] + for (const id of ["MiniMax/MiniMax-M3", "minimax/minimax-m2.5", "MiniMax-M1"]) { + expect(SystemPrompt.provider({ api: { id } } as Provider.Model)[0]).toBe(kimi) + } + }) + it.effect("skills output is sorted by name and stable across calls", () => Effect.gen(function* () { const prompt = yield* SystemPrompt.Service