bots-garden/mini-mepublic Fork
⑂ dda11d4
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-09-21T13:05:22+02:00 Browse files
dda11d4 parent: 8d5240d
modified .tickets/issues/0002-add-a-about-flag.yaml +55 -4
@@ -1,14 +1,65 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 2
33 title: Add a about flag
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-15T22:23:07.065Z
9-updatedAt: 2026-09-18T18:50:37.595Z
9+updatedAt: 2026-09-21T05:20:00.000Z
1010 labels: []
1111 milestone: v0.0.3
12-body: ""
13-tasks: []
12+body: |
13+ ## Implementation note (Claude, 2026-09-21)
14+
15+ `mm --about` prints what the program is and exits. `-about` is the same flag (Go's `flag` package takes both spellings).
16+
17+ ```
18+ Mini Me (mm) v0.0.3
19+ A minimalist coding agent for local LLMs, written in Go.
20+
21+ One agent loop, three front ends:
22+ mm [config.yaml] a terminal REPL, line by line
23+ mm -tui [config.yaml] one terminal window: conversation, input box, status bar
24+ mm -acp the Agent Client Protocol on stdio, for Zed and other editors
25+
26+ Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp.
27+ Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API.
28+
29+ https://git.rickub.com/bots-garden/mini-me
30+ Docs: docs/en (English), docs/fr (français) · mm -h for the flags
31+ ```
32+
33+ `internal/version/about.go`, next to `Short()` and `Long()`: it is the same question asked with more room — "what am I running?" — and a binary that can answer it needs no network and no file to do so. The version shown is the one `--version` reports, by construction.
34+
35+ Answered in `main.go` right after `flag.Parse()`, **before the configuration is read**, exactly as `--version` is: asking what `mm` is must work in any directory, with no config file and no model server running.
36+
37+ Deliberately one screen. An `--about` that scrolls is a README in the wrong place, and the README is one link away — hence the last line rather than a copy of it.
38+
39+ Tests: `internal/version/about_test.go` — the name, the version, the project URL and each front end are present; it stays under twenty lines; it does not end with a newline (the caller's `Println` adds one); and it still reports the right version on an unstamped build, which is what anyone building from source gets.
40+
41+ Docs EN+FR: `reference/cli.md` (invocation line, flag table, the Version section) and the README install block.
42+
43+ Not done: no `/about` slash command. The ticket says flag, and the three terminal commands that exist (`/help`, `/quit`, `/abort`) are about the session, not the program. Say so if you want one — it is a row in `internal/command`.
44+tasks:
45+ - id: 1
46+ title: "--about prints what mm is, before the configuration is read"
47+ state: closed
48+ priority: none
49+ author:
50+ name: k33g
51+ email: ph.charriere@gmail.com
52+ createdAt: 2026-09-21T05:00:00.000Z
53+ updatedAt: 2026-09-21T05:20:00.000Z
54+ closedAt: 2026-09-21T05:20:00.000Z
55+ - id: 2
56+ title: "Tests and EN+FR docs"
57+ state: closed
58+ priority: none
59+ author:
60+ name: k33g
61+ email: ph.charriere@gmail.com
62+ createdAt: 2026-09-21T05:00:00.000Z
63+ updatedAt: 2026-09-21T05:20:00.000Z
64+ closedAt: 2026-09-21T05:20:00.000Z
1465 comments: []
@@ -1,14 +1,65 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 22 id: 2
3 title: Add a about flag3 title: Add a about flag
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-15T22:23:07.065Z8 createdAt: 2026-09-15T22:23:07.065Z
9-updatedAt: 2026-09-18T18:50:37.595Z9+updatedAt: 2026-09-21T05:20:00.000Z
10 labels: []10 labels: []
11 milestone: v0.0.311 milestone: v0.0.3
12-body: ""12+body: |
13-tasks: []13+ ## Implementation note (Claude, 2026-09-21)
14+
15+ `mm --about` prints what the program is and exits. `-about` is the same flag (Go's `flag` package takes both spellings).
16+
17+ ```
18+ Mini Me (mm) v0.0.3
19+ A minimalist coding agent for local LLMs, written in Go.
20+
21+ One agent loop, three front ends:
22+ mm [config.yaml] a terminal REPL, line by line
23+ mm -tui [config.yaml] one terminal window: conversation, input box, status bar
24+ mm -acp the Agent Client Protocol on stdio, for Zed and other editors
25+
26+ Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp.
27+ Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API.
28+
29+ https://git.rickub.com/bots-garden/mini-me
30+ Docs: docs/en (English), docs/fr (français) · mm -h for the flags
31+ ```
32+
33+ `internal/version/about.go`, next to `Short()` and `Long()`: it is the same question asked with more room — "what am I running?" — and a binary that can answer it needs no network and no file to do so. The version shown is the one `--version` reports, by construction.
34+
35+ Answered in `main.go` right after `flag.Parse()`, **before the configuration is read**, exactly as `--version` is: asking what `mm` is must work in any directory, with no config file and no model server running.
36+
37+ Deliberately one screen. An `--about` that scrolls is a README in the wrong place, and the README is one link away — hence the last line rather than a copy of it.
38+
39+ Tests: `internal/version/about_test.go` — the name, the version, the project URL and each front end are present; it stays under twenty lines; it does not end with a newline (the caller's `Println` adds one); and it still reports the right version on an unstamped build, which is what anyone building from source gets.
40+
41+ Docs EN+FR: `reference/cli.md` (invocation line, flag table, the Version section) and the README install block.
42+
43+ Not done: no `/about` slash command. The ticket says flag, and the three terminal commands that exist (`/help`, `/quit`, `/abort`) are about the session, not the program. Say so if you want one — it is a row in `internal/command`.
44+tasks:
45+ - id: 1
46+ title: "--about prints what mm is, before the configuration is read"
47+ state: closed
48+ priority: none
49+ author:
50+ name: k33g
51+ email: ph.charriere@gmail.com
52+ createdAt: 2026-09-21T05:00:00.000Z
53+ updatedAt: 2026-09-21T05:20:00.000Z
54+ closedAt: 2026-09-21T05:20:00.000Z
55+ - id: 2
56+ title: "Tests and EN+FR docs"
57+ state: closed
58+ priority: none
59+ author:
60+ name: k33g
61+ email: ph.charriere@gmail.com
62+ createdAt: 2026-09-21T05:00:00.000Z
63+ updatedAt: 2026-09-21T05:20:00.000Z
64+ closedAt: 2026-09-21T05:20:00.000Z
14 comments: []65 comments: []
modified .tickets/issues/0004-do-we-need-mcp.yaml +104 -2
@@ -6,10 +6,112 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-15T22:38:45.129Z
9-updatedAt: 2026-09-18T18:45:14.523Z
9+updatedAt: 2026-09-21T05:45:00.000Z
1010 labels: []
1111 milestone: v0.0.3
1212 body: |
1313 I would say no - use a CLI
14-tasks: []
14+
15+ ---
16+
17+ ## Analysis (Claude, 2026-09-21 — not implemented)
18+
19+ **Conclusion: the "no" holds — but the reason above is not the durable one, and the ticket can be closed on evidence for about half a day's work, without writing a line of MCP.**
20+
21+ ### The fact that reframes the question
22+
23+ `genkit/go v1.10.0` — already a direct dependency — **ships an MCP client**: `plugins/mcp/` (`client.go`, `host.go`, `tools.go`, and `server.go` for the reverse direction). `MCPHost.GetActiveTools(ctx, g)` returns `[]ai.Tool`, and `ai.Tool` satisfies `ai.ToolRef`.
24+
25+ So the naive version is genuinely ~10 lines in `assembleTools` and it compiles. **That is a trap**, and it is the most important thing in this ticket.
26+
27+ (It does pull `github.com/mark3labs/mcp-go` into a deliberately small dependency list — not in `go.sum` today.)
28+
29+ ### Why a raw `GetActiveTools` splice must never ship
30+
31+ The plugin's generated tool skips every contract `internal/tools` enforces:
32+
33+ | Skipped | Consequence |
34+ |---|---|
35+ | the `ui.ActiveSink()` branch | **It never calls `Allow`.** In ACP and in the window, third-party code would run **un-gated** while mm's own `bash` asks for permission. Under the default `permissions: auto` that is worse still. |
36+ | `record()` | the loop detector never sees MCP calls; a model looping on a remote tool loops until `maxTurns` |
37+ | `truncate()` | a server returning 400 KB of JSON drops it whole into a 12B context, `maxOutput` ignored |
38+ | the error contract | missing argument and transport failure come back as **Go errors**. mm returns failures as TEXT and reserves a Go error for `detector.Stuck`. An MCP typo would end the turn instead of teaching the model. |
39+ | text extraction / `echoOutput` | returns a raw `*mcp.CallToolResult`; in the REPL an MCP call would print nothing at all |
40+
41+ **If MCP ever lands here, it lands as wrapped tools** — one `genkit.DefineTool` per discovered tool, body following the `tools.LSP` / `lspEvented` skeleton (sink branch → ToolStart → Allow → ToolRunning → run → ToolEnd → `record(d, name, title, truncate(out))`). About 60 lines in `internal/tools/mcp.go` plus a thin `internal/mcp`.
42+
43+ **Name collisions are two distinct failures**, both fatal: `genkit.DefineTool` **panics** on a duplicate registry key (a server exposing `bash` crashes mm at start-up), and `resolveUniqueTools` fails generation with `duplicate tool "x"` — every turn. So: prefix with the server name, then reject anything colliding with the six built-ins and warn, the way a missing LSP binary is skipped.
44+
45+ **Permissions for a tool you did not write**: `ui.ToolEvent.Kind` decides gating, and MCP does not tell you the kind — only optional server-authored `readOnlyHint`/`destructiveHint` annotations, i.e. a claim by the party you have not audited. Classify every MCP tool as `execute` and gate it, with a per-server `permissions: ask` override.
46+
47+ ### What carries over from `internal/lsp`, and what does not
48+
49+ Nearly free: the config shape and its validation; "skip with a warning, never refuse to start"; `launchProcess` with `tail` on stderr (MCP stdio is the same JSON-RPC-2.0-on-pipes shape); one client per process, closed at exit. Note `main.go`'s `set.close = client.Close` is a plain assignment — a second closer means chaining it.
50+
51+ Differs, in order of how much it hurts:
52+
53+ 1. **Not lazy.** `internal/lsp` starts a server on first use and its tool description is static config text. MCP's tool list only exists after connecting, so start-up must connect and `tools/list` every server, blocking, with a timeout and a degrade path, **before the banner**.
54+ 2. **Prompt cost, every single request.** 20 MCP tools sit beside mm's 6 in every call, forever.
55+ 3. **Schemas at runtime** → no Go struct with `jsonschema_description` tags. You lose per-field coaxing *and* the input normalisation that makes the detector treat twenty mangled variants as one call.
56+ 4. **HTTP servers** have no `exec.LookPath` analogue — availability becomes a network probe — and raise auth/secrets, for which mm has only `apiKeyEnv`.
57+ 5. A stdio server dying mid-session: `internal/lsp` restarts it; here the tool list was baked into the prompt at start-up.
58+
59+ ### The honest case FOR "use a CLI"
60+
61+ Strongest point first, and it is not simplicity:
62+
63+ - **`<cli> --help` is dynamic discovery that costs nothing until used.** The system prompt already instructs exactly that. MCP pays for discovery in every request forever. **For mm's target model class this is the whole ballgame** — `normalize`, `cleanSymbol` and `renameNeedsName` exist in `internal/tools/lsp.go` because mellum2-12b already mangles a six-field schema.
64+ - **Composition**: pipes, `jq`, `&&` let the model shrink a 400 KB answer to five lines *before* it costs context. An MCP tool returns what it returns.
65+ - Everything already built applies for free: truncate, the 30 s budget, cancellation through the turn context, the temp-file fix that stops a background job hanging the turn (ticket 26), loop detection on (command, output), the echo, one permission dialog users already understand.
66+ - **`skills/` + `read_skill` is the MCP-description equivalent**, loaded on demand, in the repo, editable without Go. Server-authored tool descriptions are neither.
67+ - Auth lives in the CLI (`gh auth`, `docker login`), not in mm.
68+ - `internal/acp/acp.go` already states the position in code: *"bob's whole point is the opposite — one built-in tool."*
69+
70+ What a CLI genuinely does not give: a validated argument schema (the model composes a command line as free text, and `maxTurns: 40` pays for the `--help` round trips), and **per-tool permission granularity** — `allow_always` on `bash` grants `rm -rf` forever, where MCP's grant is per tool. That second one is real, but it can be closed more cheaply with a command allowlist on `bash` than by adding MCP.
71+
72+ ### Where a CLI is genuinely not enough
73+
74+ Only two, and both are narrow:
75+
76+ - **(a) Remote-only capability** — a hosted endpoint over streamable HTTP with OAuth and no CLI. `bash` + `curl` means the model hand-writes JSON-RPC envelopes: a bad client reimplemented in the prompt.
77+ - **(b) `bashTool: false`** — a supported configuration in which the CLI route does not exist at all. "Capability without arbitrary execution" is a coherent product; today that mm can only read and edit files.
78+
79+ (Granular permissions, cross-call state such as a live browser page or an open DB transaction, and interactivity — MCP elicitation, for which mm has no UI — are real but each has a cheaper answer.)
80+
81+ ### The cheapest experiment — half a day, zero MCP code
82+
83+ Measure the **cost** side first, since that is what the recommendation rests on. With `agent.lsp.yaml`'s model and prompt, add N throwaway tools carrying realistic MCP-sized schemas (a `genkit.DefineTool` + `ai.WithInputSchema` loop, ~15 lines, deleted afterwards) so the tool list goes 6 → 16 → 26. Re-run the existing prompts from `tests/` and `demo/` and count: does the model still pick `lsp` over grep, does `edit_file` still land, how many turns, and what does `LastInputTokens()` say the prompt now costs.
84+
85+ **If a 12B model degrades at 16 tools, this ticket closes on evidence.** Only if it survives is it worth thirty minutes on a throwaway branch splicing `mcp.NewMCPHost` + `GetActiveTools` against one real server — not to ship, but to feel the permission, detector and truncate holes first-hand.
86+
87+ ### Adjacent
88+
89+ Tickets 32 (`--prompt`) and 33 (invoke another mini-me) are mm *becoming* the CLI — the same bet as this ticket. For mm being **invoked**, the protocol question is ACP, not MCP-client, which is the instinct already written in 33.
90+
91+ `plugins/mcp` also has a `server.go`: **mm exposing its own tools over MCP** is a different and cheaper question, and deserves its own ticket rather than being folded into this one.
92+tasks:
93+ - id: 1
94+ title: "Run the tool-count experiment (6 → 16 → 26 dummy tools) and record where the model degrades"
95+ state: open
96+ priority: medium
97+ author:
98+ name: k33g
99+ email: ph.charriere@gmail.com
100+ createdAt: 2026-09-21T05:45:00.000Z
101+ - id: 2
102+ title: "Close this ticket on that evidence, recording the falsifiable reason rather than 'MCP is bloat'"
103+ state: open
104+ priority: medium
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-09-21T05:45:00.000Z
109+ - id: 3
110+ title: "Open a separate ticket for the reverse direction: mm exposing its tools over MCP (plugins/mcp server.go)"
111+ state: open
112+ priority: low
113+ author:
114+ name: k33g
115+ email: ph.charriere@gmail.com
116+ createdAt: 2026-09-21T05:45:00.000Z
15117 comments: []
@@ -6,10 +6,112 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-15T22:38:45.129Z8 createdAt: 2026-09-15T22:38:45.129Z
9-updatedAt: 2026-09-18T18:45:14.523Z9+updatedAt: 2026-09-21T05:45:00.000Z
10 labels: []10 labels: []
11 milestone: v0.0.311 milestone: v0.0.3
12 body: |12 body: |
13 I would say no - use a CLI13 I would say no - use a CLI
14-tasks: []14+
15+ ---
16+
17+ ## Analysis (Claude, 2026-09-21 — not implemented)
18+
19+ **Conclusion: the "no" holds — but the reason above is not the durable one, and the ticket can be closed on evidence for about half a day's work, without writing a line of MCP.**
20+
21+ ### The fact that reframes the question
22+
23+ `genkit/go v1.10.0` — already a direct dependency — **ships an MCP client**: `plugins/mcp/` (`client.go`, `host.go`, `tools.go`, and `server.go` for the reverse direction). `MCPHost.GetActiveTools(ctx, g)` returns `[]ai.Tool`, and `ai.Tool` satisfies `ai.ToolRef`.
24+
25+ So the naive version is genuinely ~10 lines in `assembleTools` and it compiles. **That is a trap**, and it is the most important thing in this ticket.
26+
27+ (It does pull `github.com/mark3labs/mcp-go` into a deliberately small dependency list — not in `go.sum` today.)
28+
29+ ### Why a raw `GetActiveTools` splice must never ship
30+
31+ The plugin's generated tool skips every contract `internal/tools` enforces:
32+
33+ | Skipped | Consequence |
34+ |---|---|
35+ | the `ui.ActiveSink()` branch | **It never calls `Allow`.** In ACP and in the window, third-party code would run **un-gated** while mm's own `bash` asks for permission. Under the default `permissions: auto` that is worse still. |
36+ | `record()` | the loop detector never sees MCP calls; a model looping on a remote tool loops until `maxTurns` |
37+ | `truncate()` | a server returning 400 KB of JSON drops it whole into a 12B context, `maxOutput` ignored |
38+ | the error contract | missing argument and transport failure come back as **Go errors**. mm returns failures as TEXT and reserves a Go error for `detector.Stuck`. An MCP typo would end the turn instead of teaching the model. |
39+ | text extraction / `echoOutput` | returns a raw `*mcp.CallToolResult`; in the REPL an MCP call would print nothing at all |
40+
41+ **If MCP ever lands here, it lands as wrapped tools** — one `genkit.DefineTool` per discovered tool, body following the `tools.LSP` / `lspEvented` skeleton (sink branch → ToolStart → Allow → ToolRunning → run → ToolEnd → `record(d, name, title, truncate(out))`). About 60 lines in `internal/tools/mcp.go` plus a thin `internal/mcp`.
42+
43+ **Name collisions are two distinct failures**, both fatal: `genkit.DefineTool` **panics** on a duplicate registry key (a server exposing `bash` crashes mm at start-up), and `resolveUniqueTools` fails generation with `duplicate tool "x"` — every turn. So: prefix with the server name, then reject anything colliding with the six built-ins and warn, the way a missing LSP binary is skipped.
44+
45+ **Permissions for a tool you did not write**: `ui.ToolEvent.Kind` decides gating, and MCP does not tell you the kind — only optional server-authored `readOnlyHint`/`destructiveHint` annotations, i.e. a claim by the party you have not audited. Classify every MCP tool as `execute` and gate it, with a per-server `permissions: ask` override.
46+
47+ ### What carries over from `internal/lsp`, and what does not
48+
49+ Nearly free: the config shape and its validation; "skip with a warning, never refuse to start"; `launchProcess` with `tail` on stderr (MCP stdio is the same JSON-RPC-2.0-on-pipes shape); one client per process, closed at exit. Note `main.go`'s `set.close = client.Close` is a plain assignment — a second closer means chaining it.
50+
51+ Differs, in order of how much it hurts:
52+
53+ 1. **Not lazy.** `internal/lsp` starts a server on first use and its tool description is static config text. MCP's tool list only exists after connecting, so start-up must connect and `tools/list` every server, blocking, with a timeout and a degrade path, **before the banner**.
54+ 2. **Prompt cost, every single request.** 20 MCP tools sit beside mm's 6 in every call, forever.
55+ 3. **Schemas at runtime** → no Go struct with `jsonschema_description` tags. You lose per-field coaxing *and* the input normalisation that makes the detector treat twenty mangled variants as one call.
56+ 4. **HTTP servers** have no `exec.LookPath` analogue — availability becomes a network probe — and raise auth/secrets, for which mm has only `apiKeyEnv`.
57+ 5. A stdio server dying mid-session: `internal/lsp` restarts it; here the tool list was baked into the prompt at start-up.
58+
59+ ### The honest case FOR "use a CLI"
60+
61+ Strongest point first, and it is not simplicity:
62+
63+ - **`<cli> --help` is dynamic discovery that costs nothing until used.** The system prompt already instructs exactly that. MCP pays for discovery in every request forever. **For mm's target model class this is the whole ballgame** — `normalize`, `cleanSymbol` and `renameNeedsName` exist in `internal/tools/lsp.go` because mellum2-12b already mangles a six-field schema.
64+ - **Composition**: pipes, `jq`, `&&` let the model shrink a 400 KB answer to five lines *before* it costs context. An MCP tool returns what it returns.
65+ - Everything already built applies for free: truncate, the 30 s budget, cancellation through the turn context, the temp-file fix that stops a background job hanging the turn (ticket 26), loop detection on (command, output), the echo, one permission dialog users already understand.
66+ - **`skills/` + `read_skill` is the MCP-description equivalent**, loaded on demand, in the repo, editable without Go. Server-authored tool descriptions are neither.
67+ - Auth lives in the CLI (`gh auth`, `docker login`), not in mm.
68+ - `internal/acp/acp.go` already states the position in code: *"bob's whole point is the opposite — one built-in tool."*
69+
70+ What a CLI genuinely does not give: a validated argument schema (the model composes a command line as free text, and `maxTurns: 40` pays for the `--help` round trips), and **per-tool permission granularity** — `allow_always` on `bash` grants `rm -rf` forever, where MCP's grant is per tool. That second one is real, but it can be closed more cheaply with a command allowlist on `bash` than by adding MCP.
71+
72+ ### Where a CLI is genuinely not enough
73+
74+ Only two, and both are narrow:
75+
76+ - **(a) Remote-only capability** — a hosted endpoint over streamable HTTP with OAuth and no CLI. `bash` + `curl` means the model hand-writes JSON-RPC envelopes: a bad client reimplemented in the prompt.
77+ - **(b) `bashTool: false`** — a supported configuration in which the CLI route does not exist at all. "Capability without arbitrary execution" is a coherent product; today that mm can only read and edit files.
78+
79+ (Granular permissions, cross-call state such as a live browser page or an open DB transaction, and interactivity — MCP elicitation, for which mm has no UI — are real but each has a cheaper answer.)
80+
81+ ### The cheapest experiment — half a day, zero MCP code
82+
83+ Measure the **cost** side first, since that is what the recommendation rests on. With `agent.lsp.yaml`'s model and prompt, add N throwaway tools carrying realistic MCP-sized schemas (a `genkit.DefineTool` + `ai.WithInputSchema` loop, ~15 lines, deleted afterwards) so the tool list goes 6 → 16 → 26. Re-run the existing prompts from `tests/` and `demo/` and count: does the model still pick `lsp` over grep, does `edit_file` still land, how many turns, and what does `LastInputTokens()` say the prompt now costs.
84+
85+ **If a 12B model degrades at 16 tools, this ticket closes on evidence.** Only if it survives is it worth thirty minutes on a throwaway branch splicing `mcp.NewMCPHost` + `GetActiveTools` against one real server — not to ship, but to feel the permission, detector and truncate holes first-hand.
86+
87+ ### Adjacent
88+
89+ Tickets 32 (`--prompt`) and 33 (invoke another mini-me) are mm *becoming* the CLI — the same bet as this ticket. For mm being **invoked**, the protocol question is ACP, not MCP-client, which is the instinct already written in 33.
90+
91+ `plugins/mcp` also has a `server.go`: **mm exposing its own tools over MCP** is a different and cheaper question, and deserves its own ticket rather than being folded into this one.
92+tasks:
93+ - id: 1
94+ title: "Run the tool-count experiment (6 → 16 → 26 dummy tools) and record where the model degrades"
95+ state: open
96+ priority: medium
97+ author:
98+ name: k33g
99+ email: ph.charriere@gmail.com
100+ createdAt: 2026-09-21T05:45:00.000Z
101+ - id: 2
102+ title: "Close this ticket on that evidence, recording the falsifiable reason rather than 'MCP is bloat'"
103+ state: open
104+ priority: medium
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-09-21T05:45:00.000Z
109+ - id: 3
110+ title: "Open a separate ticket for the reverse direction: mm exposing its tools over MCP (plugins/mcp server.go)"
111+ state: open
112+ priority: low
113+ author:
114+ name: k33g
115+ email: ph.charriere@gmail.com
116+ createdAt: 2026-09-21T05:45:00.000Z
15 comments: []117 comments: []
modified .tickets/issues/0015-improve-skills-detection.yaml +120 -3
@@ -6,9 +6,126 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-16T05:52:51.530Z
9-updatedAt: 2026-09-18T18:46:02.559Z
9+updatedAt: 2026-09-21T05:45:00.000Z
1010 labels: []
1111 milestone: v0.0.3
12-body: ""
13-tasks: []
12+body: |
13+ ## Analysis (Claude, 2026-09-21 — not implemented)
14+
15+ ### "Detection" means three different things
16+
17+ | Reading | State |
18+ |---|---|
19+ | **(a) discovery on disk** — which files count as a skill | mostly solid; two real defects remain, and neither is what the title probably means |
20+ | **(b) the model detecting that a skill APPLIES** | **this is the broken one**, and the repository currently ships the signal for it and throws it away |
21+ | **(c) diagnosing a miss** — seeing that a skill should have fired and did not | essentially unsupported, and it is the prerequisite for knowing whether any fix to (b) worked |
22+
23+ ### Three concrete defects
24+
25+ **1. The catalogue advertises a name `read_skill` cannot resolve.** `skills.Catalogue` names a skill by its front-matter `name`; `skills.Read` looks it up **by path**. When they differ, the tool fails on the very string the catalogue told the model to use, with a message that contradicts itself:
26+
27+ ```
28+ No skill named "renamed". Available skills: renamed
29+ ```
30+
31+ The loop detector then flags and hard-stops the turn after six attempts. `docs/{en,fr}/reference/skill-format.md` documents `name` as "shown in the catalogue **and accepted by `read_skill`**" — which is false. The shipped skills all have `name:` equal to the directory name, so it does not bite today; it bites the first user who renames one.
32+
33+ **2. `keywords:` is written, documented, and discarded.** Every shipped skill carries a rich bilingual `keywords:` line (`greetings`: `greet, hello, hi, bonjour, coucou, saluer`; `lsp-navigation`: 17 terms). `parseHeader` reads exactly two keys, `name` and `description`, and `skill-format.md` states plainly that *"the `keywords` key is accepted in the file and ignored by the loader"*. **The lexical bridge from "salue Bob" or "qui appelle cette fonction ?" to a skill name exists on disk and never reaches the model.** For a 2B–12B model that is the whole of reading (b).
34+
35+ **3. `bg-jobs` is a skill that does not exist.** All three shipped YAMLs *and* the built-in default prompt in `internal/config/config.go` instruct: *"Read the `bg-jobs` skill before you wait on, inspect or stop such a job."* `skills/` contains only `greetings`, `lsp-navigation`, `vulcan-salute`, `what-time-is-it`. **Every background-job request drives the model into a guaranteed-failing `read_skill` call**, and nothing detects the mismatch — not a test, not the banner. *(Verified independently, twice.)*
36+
37+ Related: the SKILLS prompt block scopes skills to *"any request to DO something to a **Go project**"*, while three of the four shipped skills are greetings, a Vulcan salute and a clock. The instruction actively misroutes.
38+
39+ ### And no way to see a miss
40+
41+ - A BOM, a leading blank line, a folded `description: >`, or a typo (`descripton:`) yields a skill in the catalogue with a bare name and **no description** — the worst possible entry. Nothing warns: the banner only fires when the count is **zero**.
42+ - The `· 📖 N skill(s)` recap is **omitted at zero**, unlike `⚙ N command(s)` which always prints. So there is no signal at all distinguishing "no skill applied" from "a skill applied and the model ignored it".
43+ - `engine.Skills` counts `read_skill` **responses**, failures included: `📖 2 skill(s)` can mean two `No skill named …`.
44+ - The session `errors` trail — the artefact meant for *"a coding agent asked why the run went wrong"* — has three categories (`generation`, `compaction`, `resume`) and never records a failed `read_skill`.
45+
46+ ### Tier 1 — cheap and certain, do these regardless
47+
48+ 1. **Resolve `read_skill` through the catalogue, not the path.** A `map[name]path` built in `List`, keeping the `filepath.Base` barrier. ~15 lines + 2 tests. One decision: duplicate `name:` across two files — first wins, plus a banner warning.
49+ 2. **Ship `skills/bg-jobs/SKILL.md`, or delete the sentence from all four prompts.** Minutes. **The durable fix is the test**: assert that every skill name quoted in a shipped YAML and in the built-in default prompt actually resolves. ~20 lines, and it prevents the whole class.
50+ 3. **Parse `keywords:` and render it in the catalogue.** One case in `parseHeader`, one format change in `Catalogue`. The data is already written everywhere. Cap keywords per skill (say 8) so a 30-skill catalogue does not blow up, and correct `skill-format.md`. It cannot make matching worse, and it retires a documented inconsistency either way.
51+ 4. **Warn on a description-less skill**, not just on zero skills: `N skill(s) with no description in <dir> (<names>) — the model will pick blindly`.
52+ 5. **Print `📖` unconditionally**, like `⚙`, and count only *successful* loads. `📖 0 skill(s)` on a turn where a skill should have fired is exactly the observation this ticket needs and cannot make today. Touches three recap sites — keep them consistent; print it only when `read_skill` is declared.
53+
54+ ### Tier 2 — the actual (b) work, and it needs measurement first
55+
56+ 6. **Build a skill-routing eval before touching the prompt.** ~20 `prompt → expected skill (or none)` pairs, run against the configured local model, reporting hit / miss / false positive. At `temperature: 0.0` a small model is reproducible, so the numbers mean something. **Items 7–9 are guesses until this exists.** If only one Tier-2 item gets done, do this one. Ticket 32's `--prompt` may be the cheapest driver.
57+ 7. **Fix the SKILLS prompt block**: drop "Go project"; state it as *"before acting, check the `read_skill` catalogue for a matching procedure; if one matches, load it first; if none matches, proceed normally"* — the no-match escape is missing today. Add the block to the built-in default prompt, which has none at all. Risk is real: prompt changes on a 12B are non-monotonic, and an over-strong "always call read_skill first" produces spurious calls.
58+ 8. **Repeat the catalogue in the system prompt.** Today the model sees the skill list *only* through the tool schema. Small models often weight the system prompt more heavily. ~20 lines — but it doubles the catalogue's token cost and breaks the package's stated principle ("turn that directory into ONE STRING"). Treat as an A/B, not a default.
59+ 9. **Richer entries — "when to use it"**, extracted from a section or a `when:` key. `lsp-navigation/SKILL.md` already has such a section. Changes the documented format.
60+
61+ ### Tier 3 — speculative
62+
63+ 10. **Record tool-level failures in the session `errors` trail** (a `FailTool` category). Turns `jq .errors <session>` into a real answer to "why didn't the skill fire?". Costs an architectural thread from `internal/tools` to the `Keeper`, which is `Keeper`-free by design. Only if (c) proves a recurring need.
64+ 11. **Deterministic pre-routing** — match the message against `keywords` in Go and pre-load or nudge. Moves the decision out of the model; false positives pre-load the wrong procedure. The fallback if item 6 shows 3/7/8 are not enough. **Do not start here.**
65+ 12. Deeper nesting (`skills/<group>/<name>/SKILL.md`) and hot reload. Genkit tool descriptions are fixed at `DefineTool` time, so hot reload means re-declaring the tool per turn. No demonstrated demand. **Rank last: neither is this ticket.**
66+
67+ ### Worth one curl before any prompt work
68+
69+ The whole design rests on the OpenAI-compatible server delivering the full tool description to the model, intact and untruncated. Nobody has checked. It is cheap, and it would rule out a whole class of explanation.
70+
71+ ### Not re-opened
72+
73+ Decisions in force per `.memory/summary.md`, none of which the above touches: both layouts accepted; the flat file wins when both exist; `skills/README.md` skipped; relative `skillsDir` resolved by `resolveSkillsDir`; banner warning at count zero.
74+tasks:
75+ - id: 1
76+ title: "Resolve read_skill through the catalogue's name, not the file path"
77+ state: open
78+ priority: high
79+ author:
80+ name: k33g
81+ email: ph.charriere@gmail.com
82+ createdAt: 2026-09-21T05:45:00.000Z
83+ - id: 2
84+ title: "bg-jobs: ship the skill or delete the sentence from the 3 YAMLs and the default prompt"
85+ state: open
86+ priority: high
87+ author:
88+ name: k33g
89+ email: ph.charriere@gmail.com
90+ createdAt: 2026-09-21T05:45:00.000Z
91+ - id: 3
92+ title: "Test that every skill named in a shipped prompt actually resolves"
93+ state: open
94+ priority: high
95+ author:
96+ name: k33g
97+ email: ph.charriere@gmail.com
98+ createdAt: 2026-09-21T05:45:00.000Z
99+ - id: 4
100+ title: "Parse keywords and render them in the catalogue; correct skill-format.md"
101+ state: open
102+ priority: medium
103+ author:
104+ name: k33g
105+ email: ph.charriere@gmail.com
106+ createdAt: 2026-09-21T05:45:00.000Z
107+ - id: 5
108+ title: "Warn on a skill with no description; print the 📖 recap unconditionally and count only successes"
109+ state: open
110+ priority: medium
111+ author:
112+ name: k33g
113+ email: ph.charriere@gmail.com
114+ createdAt: 2026-09-21T05:45:00.000Z
115+ - id: 6
116+ title: "Build a skill-routing eval before any prompt change"
117+ state: open
118+ priority: medium
119+ author:
120+ name: k33g
121+ email: ph.charriere@gmail.com
122+ createdAt: 2026-09-21T05:45:00.000Z
123+ - id: 7
124+ title: "Then: fix the SKILLS prompt block and add it to the built-in default prompt"
125+ state: open
126+ priority: low
127+ author:
128+ name: k33g
129+ email: ph.charriere@gmail.com
130+ createdAt: 2026-09-21T05:45:00.000Z
14131 comments: []
@@ -6,9 +6,126 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-16T05:52:51.530Z8 createdAt: 2026-09-16T05:52:51.530Z
9-updatedAt: 2026-09-18T18:46:02.559Z9+updatedAt: 2026-09-21T05:45:00.000Z
10 labels: []10 labels: []
11 milestone: v0.0.311 milestone: v0.0.3
12-body: ""12+body: |
13-tasks: []13+ ## Analysis (Claude, 2026-09-21 — not implemented)
14+
15+ ### "Detection" means three different things
16+
17+ | Reading | State |
18+ |---|---|
19+ | **(a) discovery on disk** — which files count as a skill | mostly solid; two real defects remain, and neither is what the title probably means |
20+ | **(b) the model detecting that a skill APPLIES** | **this is the broken one**, and the repository currently ships the signal for it and throws it away |
21+ | **(c) diagnosing a miss** — seeing that a skill should have fired and did not | essentially unsupported, and it is the prerequisite for knowing whether any fix to (b) worked |
22+
23+ ### Three concrete defects
24+
25+ **1. The catalogue advertises a name `read_skill` cannot resolve.** `skills.Catalogue` names a skill by its front-matter `name`; `skills.Read` looks it up **by path**. When they differ, the tool fails on the very string the catalogue told the model to use, with a message that contradicts itself:
26+
27+ ```
28+ No skill named "renamed". Available skills: renamed
29+ ```
30+
31+ The loop detector then flags and hard-stops the turn after six attempts. `docs/{en,fr}/reference/skill-format.md` documents `name` as "shown in the catalogue **and accepted by `read_skill`**" — which is false. The shipped skills all have `name:` equal to the directory name, so it does not bite today; it bites the first user who renames one.
32+
33+ **2. `keywords:` is written, documented, and discarded.** Every shipped skill carries a rich bilingual `keywords:` line (`greetings`: `greet, hello, hi, bonjour, coucou, saluer`; `lsp-navigation`: 17 terms). `parseHeader` reads exactly two keys, `name` and `description`, and `skill-format.md` states plainly that *"the `keywords` key is accepted in the file and ignored by the loader"*. **The lexical bridge from "salue Bob" or "qui appelle cette fonction ?" to a skill name exists on disk and never reaches the model.** For a 2B–12B model that is the whole of reading (b).
34+
35+ **3. `bg-jobs` is a skill that does not exist.** All three shipped YAMLs *and* the built-in default prompt in `internal/config/config.go` instruct: *"Read the `bg-jobs` skill before you wait on, inspect or stop such a job."* `skills/` contains only `greetings`, `lsp-navigation`, `vulcan-salute`, `what-time-is-it`. **Every background-job request drives the model into a guaranteed-failing `read_skill` call**, and nothing detects the mismatch — not a test, not the banner. *(Verified independently, twice.)*
36+
37+ Related: the SKILLS prompt block scopes skills to *"any request to DO something to a **Go project**"*, while three of the four shipped skills are greetings, a Vulcan salute and a clock. The instruction actively misroutes.
38+
39+ ### And no way to see a miss
40+
41+ - A BOM, a leading blank line, a folded `description: >`, or a typo (`descripton:`) yields a skill in the catalogue with a bare name and **no description** — the worst possible entry. Nothing warns: the banner only fires when the count is **zero**.
42+ - The `· 📖 N skill(s)` recap is **omitted at zero**, unlike `⚙ N command(s)` which always prints. So there is no signal at all distinguishing "no skill applied" from "a skill applied and the model ignored it".
43+ - `engine.Skills` counts `read_skill` **responses**, failures included: `📖 2 skill(s)` can mean two `No skill named …`.
44+ - The session `errors` trail — the artefact meant for *"a coding agent asked why the run went wrong"* — has three categories (`generation`, `compaction`, `resume`) and never records a failed `read_skill`.
45+
46+ ### Tier 1 — cheap and certain, do these regardless
47+
48+ 1. **Resolve `read_skill` through the catalogue, not the path.** A `map[name]path` built in `List`, keeping the `filepath.Base` barrier. ~15 lines + 2 tests. One decision: duplicate `name:` across two files — first wins, plus a banner warning.
49+ 2. **Ship `skills/bg-jobs/SKILL.md`, or delete the sentence from all four prompts.** Minutes. **The durable fix is the test**: assert that every skill name quoted in a shipped YAML and in the built-in default prompt actually resolves. ~20 lines, and it prevents the whole class.
50+ 3. **Parse `keywords:` and render it in the catalogue.** One case in `parseHeader`, one format change in `Catalogue`. The data is already written everywhere. Cap keywords per skill (say 8) so a 30-skill catalogue does not blow up, and correct `skill-format.md`. It cannot make matching worse, and it retires a documented inconsistency either way.
51+ 4. **Warn on a description-less skill**, not just on zero skills: `N skill(s) with no description in <dir> (<names>) — the model will pick blindly`.
52+ 5. **Print `📖` unconditionally**, like `⚙`, and count only *successful* loads. `📖 0 skill(s)` on a turn where a skill should have fired is exactly the observation this ticket needs and cannot make today. Touches three recap sites — keep them consistent; print it only when `read_skill` is declared.
53+
54+ ### Tier 2 — the actual (b) work, and it needs measurement first
55+
56+ 6. **Build a skill-routing eval before touching the prompt.** ~20 `prompt → expected skill (or none)` pairs, run against the configured local model, reporting hit / miss / false positive. At `temperature: 0.0` a small model is reproducible, so the numbers mean something. **Items 7–9 are guesses until this exists.** If only one Tier-2 item gets done, do this one. Ticket 32's `--prompt` may be the cheapest driver.
57+ 7. **Fix the SKILLS prompt block**: drop "Go project"; state it as *"before acting, check the `read_skill` catalogue for a matching procedure; if one matches, load it first; if none matches, proceed normally"* — the no-match escape is missing today. Add the block to the built-in default prompt, which has none at all. Risk is real: prompt changes on a 12B are non-monotonic, and an over-strong "always call read_skill first" produces spurious calls.
58+ 8. **Repeat the catalogue in the system prompt.** Today the model sees the skill list *only* through the tool schema. Small models often weight the system prompt more heavily. ~20 lines — but it doubles the catalogue's token cost and breaks the package's stated principle ("turn that directory into ONE STRING"). Treat as an A/B, not a default.
59+ 9. **Richer entries — "when to use it"**, extracted from a section or a `when:` key. `lsp-navigation/SKILL.md` already has such a section. Changes the documented format.
60+
61+ ### Tier 3 — speculative
62+
63+ 10. **Record tool-level failures in the session `errors` trail** (a `FailTool` category). Turns `jq .errors <session>` into a real answer to "why didn't the skill fire?". Costs an architectural thread from `internal/tools` to the `Keeper`, which is `Keeper`-free by design. Only if (c) proves a recurring need.
64+ 11. **Deterministic pre-routing** — match the message against `keywords` in Go and pre-load or nudge. Moves the decision out of the model; false positives pre-load the wrong procedure. The fallback if item 6 shows 3/7/8 are not enough. **Do not start here.**
65+ 12. Deeper nesting (`skills/<group>/<name>/SKILL.md`) and hot reload. Genkit tool descriptions are fixed at `DefineTool` time, so hot reload means re-declaring the tool per turn. No demonstrated demand. **Rank last: neither is this ticket.**
66+
67+ ### Worth one curl before any prompt work
68+
69+ The whole design rests on the OpenAI-compatible server delivering the full tool description to the model, intact and untruncated. Nobody has checked. It is cheap, and it would rule out a whole class of explanation.
70+
71+ ### Not re-opened
72+
73+ Decisions in force per `.memory/summary.md`, none of which the above touches: both layouts accepted; the flat file wins when both exist; `skills/README.md` skipped; relative `skillsDir` resolved by `resolveSkillsDir`; banner warning at count zero.
74+tasks:
75+ - id: 1
76+ title: "Resolve read_skill through the catalogue's name, not the file path"
77+ state: open
78+ priority: high
79+ author:
80+ name: k33g
81+ email: ph.charriere@gmail.com
82+ createdAt: 2026-09-21T05:45:00.000Z
83+ - id: 2
84+ title: "bg-jobs: ship the skill or delete the sentence from the 3 YAMLs and the default prompt"
85+ state: open
86+ priority: high
87+ author:
88+ name: k33g
89+ email: ph.charriere@gmail.com
90+ createdAt: 2026-09-21T05:45:00.000Z
91+ - id: 3
92+ title: "Test that every skill named in a shipped prompt actually resolves"
93+ state: open
94+ priority: high
95+ author:
96+ name: k33g
97+ email: ph.charriere@gmail.com
98+ createdAt: 2026-09-21T05:45:00.000Z
99+ - id: 4
100+ title: "Parse keywords and render them in the catalogue; correct skill-format.md"
101+ state: open
102+ priority: medium
103+ author:
104+ name: k33g
105+ email: ph.charriere@gmail.com
106+ createdAt: 2026-09-21T05:45:00.000Z
107+ - id: 5
108+ title: "Warn on a skill with no description; print the 📖 recap unconditionally and count only successes"
109+ state: open
110+ priority: medium
111+ author:
112+ name: k33g
113+ email: ph.charriere@gmail.com
114+ createdAt: 2026-09-21T05:45:00.000Z
115+ - id: 6
116+ title: "Build a skill-routing eval before any prompt change"
117+ state: open
118+ priority: medium
119+ author:
120+ name: k33g
121+ email: ph.charriere@gmail.com
122+ createdAt: 2026-09-21T05:45:00.000Z
123+ - id: 7
124+ title: "Then: fix the SKILLS prompt block and add it to the built-in default prompt"
125+ state: open
126+ priority: low
127+ author:
128+ name: k33g
129+ email: ph.charriere@gmail.com
130+ createdAt: 2026-09-21T05:45:00.000Z
14 comments: []131 comments: []
added .tickets/issues/0024-reduce-model-id-display-length-in-the-header.yaml +79 -0
new file mode 100644
@@ -0,0 +1,79 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 24
3+title: Reduce Model ID display length in the header
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-18T18:57:19.208Z
9+updatedAt: 2026-09-21T05:20:00.000Z
10+labels: []
11+milestone: v0.0.3
12+epic: Improve TUI
13+body: |
14+ ## Implementation note (Claude, 2026-09-21)
15+
16+ A model identifier is an address, not a name. A local registry hands out things like
17+
18+ ```
19+ huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
20+ ```
21+
22+ — seventy characters, of which maybe thirty say anything. The window's header is the one line on screen for the whole session, and everything beside the model (the provider, the context gauge, the session id) was being pushed off the right edge by it.
23+
24+ `internal/tui/modelname.go`, `shortModel()`, applied in `runner.header()`:
25+
26+ | Step | Why |
27+ |---|---|
28+ | Drop everything up to the last `/` | the registry host and the organisation are routing, not identity |
29+ | Drop a trailing `-gguf`, or `-gguf-<quant>` | the file format is not information, and that form repeats the tag |
30+ | Keep the tag after `:` whole | it is the part that says what is actually loaded, and what tells two otherwise identical models apart |
31+ | Cap at 28 characters, ellipsis included | a header that is all model is not a header |
32+
33+ ```
34+ huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M
35+ …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M
36+ huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M → meta-llama-guard-2-8…:Q4_K_M
37+ ai/smollm2 → smollm2
38+ gpt-4o → gpt-4o
39+ ```
40+
41+ The third line is the case that makes the rule careful rather than clever: `-smashed` is a real variant marker (pruna's compressed weights), so `-gguf` is only dropped when it ends the name or is followed by something that looks like a quantisation (`q4_k_m`, `bf16`, `iq3_xxs`). A blunt "cut at -gguf" would have silently renamed that model to a different one.
42+
43+ **The full identifier stays reachable**: only the header is shortened. The start-up banner prints the identifier exactly as configured, and the window keeps that banner as the conversation's first line — so what is running is always one scroll away, and `--version`-style questions are never answered by a truncated string.
44+
45+ Tests: `internal/tui/modelname_test.go` — the five shapes above, plus a bound that holds for any input including a 500-character one, the tag surviving the cut, and `header()` itself losing the registry host while keeping the model, the provider and the context gauge.
46+
47+ Docs EN+FR: `reference/cli.md`, window-mode table, a "Model name" row.
48+tasks:
49+ - id: 1
50+ title: "Shorten the model identifier in the window header, keeping the tag"
51+ state: closed
52+ priority: none
53+ author:
54+ name: k33g
55+ email: ph.charriere@gmail.com
56+ createdAt: 2026-09-21T05:00:00.000Z
57+ updatedAt: 2026-09-21T05:20:00.000Z
58+ closedAt: 2026-09-21T05:20:00.000Z
59+ - id: 2
60+ title: "Never mistake a variant marker for the format suffix"
61+ state: closed
62+ priority: none
63+ author:
64+ name: k33g
65+ email: ph.charriere@gmail.com
66+ createdAt: 2026-09-21T05:00:00.000Z
67+ updatedAt: 2026-09-21T05:20:00.000Z
68+ closedAt: 2026-09-21T05:20:00.000Z
69+ - id: 3
70+ title: "Tests and EN+FR docs"
71+ state: closed
72+ priority: none
73+ author:
74+ name: k33g
75+ email: ph.charriere@gmail.com
76+ createdAt: 2026-09-21T05:00:00.000Z
77+ updatedAt: 2026-09-21T05:20:00.000Z
78+ closedAt: 2026-09-21T05:20:00.000Z
79+comments: []
new file mode 100644
@@ -0,0 +1,79 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 24
3+title: Reduce Model ID display length in the header
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-18T18:57:19.208Z
9+updatedAt: 2026-09-21T05:20:00.000Z
10+labels: []
11+milestone: v0.0.3
12+epic: Improve TUI
13+body: |
14+ ## Implementation note (Claude, 2026-09-21)
15+
16+ A model identifier is an address, not a name. A local registry hands out things like
17+
18+ ```
19+ huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
20+ ```
21+
22+ — seventy characters, of which maybe thirty say anything. The window's header is the one line on screen for the whole session, and everything beside the model (the provider, the context gauge, the session id) was being pushed off the right edge by it.
23+
24+ `internal/tui/modelname.go`, `shortModel()`, applied in `runner.header()`:
25+
26+ | Step | Why |
27+ |---|---|
28+ | Drop everything up to the last `/` | the registry host and the organisation are routing, not identity |
29+ | Drop a trailing `-gguf`, or `-gguf-<quant>` | the file format is not information, and that form repeats the tag |
30+ | Keep the tag after `:` whole | it is the part that says what is actually loaded, and what tells two otherwise identical models apart |
31+ | Cap at 28 characters, ellipsis included | a header that is all model is not a header |
32+
33+ ```
34+ huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M
35+ …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M
36+ huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M → meta-llama-guard-2-8…:Q4_K_M
37+ ai/smollm2 → smollm2
38+ gpt-4o → gpt-4o
39+ ```
40+
41+ The third line is the case that makes the rule careful rather than clever: `-smashed` is a real variant marker (pruna's compressed weights), so `-gguf` is only dropped when it ends the name or is followed by something that looks like a quantisation (`q4_k_m`, `bf16`, `iq3_xxs`). A blunt "cut at -gguf" would have silently renamed that model to a different one.
42+
43+ **The full identifier stays reachable**: only the header is shortened. The start-up banner prints the identifier exactly as configured, and the window keeps that banner as the conversation's first line — so what is running is always one scroll away, and `--version`-style questions are never answered by a truncated string.
44+
45+ Tests: `internal/tui/modelname_test.go` — the five shapes above, plus a bound that holds for any input including a 500-character one, the tag surviving the cut, and `header()` itself losing the registry host while keeping the model, the provider and the context gauge.
46+
47+ Docs EN+FR: `reference/cli.md`, window-mode table, a "Model name" row.
48+tasks:
49+ - id: 1
50+ title: "Shorten the model identifier in the window header, keeping the tag"
51+ state: closed
52+ priority: none
53+ author:
54+ name: k33g
55+ email: ph.charriere@gmail.com
56+ createdAt: 2026-09-21T05:00:00.000Z
57+ updatedAt: 2026-09-21T05:20:00.000Z
58+ closedAt: 2026-09-21T05:20:00.000Z
59+ - id: 2
60+ title: "Never mistake a variant marker for the format suffix"
61+ state: closed
62+ priority: none
63+ author:
64+ name: k33g
65+ email: ph.charriere@gmail.com
66+ createdAt: 2026-09-21T05:00:00.000Z
67+ updatedAt: 2026-09-21T05:20:00.000Z
68+ closedAt: 2026-09-21T05:20:00.000Z
69+ - id: 3
70+ title: "Tests and EN+FR docs"
71+ state: closed
72+ priority: none
73+ author:
74+ name: k33g
75+ email: ph.charriere@gmail.com
76+ createdAt: 2026-09-21T05:00:00.000Z
77+ updatedAt: 2026-09-21T05:20:00.000Z
78+ closedAt: 2026-09-21T05:20:00.000Z
79+comments: []
deleted .tickets/issues/0024-reduce-model-id-display-length.yaml +0 -15
deleted file mode 100644
@@ -1,15 +0,0 @@
1-# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 24
3-title: Reduce Model ID display length
4-state: open
5-author:
6- name: k33g
7- email: ph.charriere@gmail.com
8-createdAt: 2026-09-18T18:57:19.208Z
9-updatedAt: 2026-09-18T18:58:05.672Z
10-labels: []
11-milestone: v0.0.3
12-epic: Improve TUI
13-body: ""
14-tasks: []
15-comments: []
deleted file mode 100644
@@ -1,15 +0,0 @@
1-# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 24
3-title: Reduce Model ID display length
4-state: open
5-author:
6- name: k33g
7- email: ph.charriere@gmail.com
8-createdAt: 2026-09-18T18:57:19.208Z
9-updatedAt: 2026-09-18T18:58:05.672Z
10-labels: []
11-milestone: v0.0.3
12-epic: Improve TUI
13-body: ""
14-tasks: []
15-comments: []
modified .tickets/issues/0030-how-to-use-mini-me-with-claude.yaml +113 -3
@@ -6,9 +6,119 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:00:56.971Z
9-updatedAt: 2026-09-21T04:00:56.971Z
9+updatedAt: 2026-09-21T05:45:00.000Z
1010 labels: []
1111 milestone: v0.0.3
12-body: ""
13-tasks: []
12+body: |
13+ ## Analysis (Claude, 2026-09-21 — not implemented)
14+
15+ **This ticket is not symmetric with 31. One experiment decides which of two very different routes it takes, and the experiment costs a `curl` and an API key. Run it before writing anything.**
16+
17+ ### The experiment
18+
19+ ```
20+ curl https://api.anthropic.com/v1/chat/completions \
21+ -H "Authorization: Bearer $ANTHROPIC_API_KEY" -H 'Content-Type: application/json' \
22+ -d '{"model":"<a claude id>","stream":true,
23+ "tools":[{"type":"function","function":{"name":"bash","parameters":{...}}}],
24+ "messages":[{"role":"user","content":"list the files here"}]}'
25+ ```
26+
27+ **Does a tool call come back?**
28+
29+ - **Yes** → the same ~20-line registry entry as ticket 31. Half a day.
30+ - **No** → Claude earns a genuinely new `Provider` on Genkit's native `plugins/anthropic`. Two to three days, and it is the case `internal/engine/provider.go` already anticipates in its own words: *"adding a protocol (Anthropic's Messages API…) is a new type."*
31+
32+ Why it is genuinely open: Anthropic's OpenAI-compatible endpoint **is live** (measured — `POST /v1/chat/completions` with no key returns an OpenAI-shaped `{"error":{"code":"invalid_request_error", …}}`), but Genkit's own `compat_oai/anthropic` plugin marks every model `Tools: false` with the comment *"Anthropic supports tool use, but it's not compatible with the OpenAI API"*. That assertion could not be checked without a key, and it dates from 2025. An agent that cannot call tools is not an agent, so this is the whole ticket.
33+
34+ Note mm is **not** bound by that flag: `openaiCompat.Open` declares its own model with `compat_oai.Multimodal`. The flag is Genkit's claim about the wire, not a gate on us.
35+
36+ ### What Genkit v1.10.0 actually ships
37+
38+ | Plugin | Verdict |
39+ |---|---|
40+ | `plugins/compat_oai/anthropic` | **Useless here.** It reads `ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL` from the environment itself, which fights `Backend`; it hard-codes eight model ids; and it marks `Tools: false`. `openaiCompat.Open` already does everything it does, better. |
41+ | `plugins/anthropic` (native) | The real Messages API on `anthropic-sdk-go`. `Tools: true`, `ToolChoice`, `Constrained`. **Streams `ai.NewReasoningPart` on `thinking_delta` and round-trips the thinking signature.** `Init` panics when the key is unset. |
42+
43+ Neither `anthropic-sdk-go` nor `google.golang.org/genai` is in the module cache today — the native route adds real dependencies.
44+
45+ ### Route A — registry entry (if tools work)
46+
47+ Same shape as ticket 31: `baseURL: "https://api.anthropic.com/v1"`, `apiKeyEnv: "ANTHROPIC_API_KEY"`, `keyRequired: true`, `fallback: ""`. Same caveats as 31, with one extra:
48+
49+ - **`reachable()` breaks harder.** It sends a bare GET to `/models` and demands 200; Anthropic's `/v1/models` is the *native* route and returns 400 `"anthropic-version: header is required"` even with a Bearer token. Same `probeRemote` fix as ticket 31, plus the vendor header.
50+ - **Reasoning is the real hazard.** `reasoningTap` (ticket 29) scans for `delta.reasoning_content` / `delta.reasoning`. Anthropic's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — and `engine.go` spells out why that is not cosmetic: `beat()` lives in that listener, so a model silent longer than `watchdogTimeout` is killed as stalled while it is working. **Claude with extended thinking on is exactly that shape.** Mitigations: a much larger `watchdogTimeout` in the shipped YAML, or teaching `reasoningTap` a third spelling if one turns up.
51+
52+ ### Route B — native provider (if tools do not work)
53+
54+ A new `Provider` implementation, not a registry entry. What changes beyond the obvious:
55+
56+ - **No middleware hook.** `option.WithMiddleware(reasoningTap)` is `openai-go/option`; `anthropic-sdk-go` has a same-named, different package. **You do not need it**: the native plugin already emits reasoning parts, so `beat()` fires through the ordinary streaming callback and you add `think.emit(chunk.Reasoning())` next to `emit(chunk.Text())`. Reasoning gets *simpler* — but `Generate` grows a second path and `WithReasoning` becomes provider-conditional.
57+ - **`sampling:` stops being portable.** `config.Cfg.Sampling` goes raw into `ai.WithConfig`. The native plugin maps it onto `anthropic.MessageNewParams` and **hard-errors `"maxTokens not set"` when `max_tokens` is absent**. Either a per-provider dialect or a translation step in `Provider`. *(Unverified: the SDK is not in the module cache.)*
58+ - `Init` **panics** on a missing key where `New`→`Open` returns errors. `keyRequired` catches the ordinary case first, but `Open` wants a guard.
59+ - `Explain` is new work: `errors.As(err, &openai.Error)` never matches, and `statusInText` may not match the other SDK's error strings. No branch exists for Anthropic's 529 overloaded.
60+ - What Route B buys that Route A never can: **extended thinking with signatures flowing straight into the 💭 block through `chunk.Reasoning()`, with the watchdog fed for free**, plus `ThoughtsTokens` in the usage.
61+
62+ ### Cost
63+
64+ | | Route A | Route B |
65+ |---|---|---|
66+ | Code | ~40 lines + ~20 for `probeRemote` | ~150–250 lines, new file |
67+ | New dependencies | none | `anthropic-sdk-go`, not cached |
68+ | Touches | `init()`, one probe func | `Generate`, `Explain`, sampling, `Probe`, `Open` |
69+ | Effort | half a day | 2–3 days |
70+ | Risk | concentrated in one yes/no question | diffuse: a second code path, forever |
71+
72+ ### To decide before coding
73+
74+ 1. **Which surface?** Claude's direct API, or Bedrock? One registry entry cannot serve both.
75+ 2. **Is thinking in scope?** If "support Claude" means "extended thinking visible in the 💭 block", Route A almost certainly cannot deliver it and the answer flips before the experiment is even run.
76+ 3. **Per-vendor entries, or one generic `openai` entry** where the user supplies `baseUrl`? That single entry would cover Claude, Gemini, OpenRouter, Groq, Together and vLLM at once, and is arguably the real ticket behind both 30 and 31. It trades good per-vendor `Explain` words for reach.
77+ 4. Which model ids ship, and are they pinned? Hosted ids go stale in months.
78+ 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed. mm already captures `Usage.InputTokens`.
79+
80+ ### What I would resist
81+
82+ Doing the native route for **both** vendors. Gemini through `googlegenai` costs a silent sampling mismatch and an extra dependency tree for a capability its OpenAI layer already provides (ticket 31).
83+tasks:
84+ - id: 1
85+ title: "EXPERIMENT FIRST: does Anthropic's /v1/chat/completions return tool calls? One curl with a real key."
86+ state: open
87+ priority: high
88+ author:
89+ name: k33g
90+ email: ph.charriere@gmail.com
91+ createdAt: 2026-09-21T05:45:00.000Z
92+ - id: 2
93+ title: "Decide Route A (registry entry) or Route B (native plugins/anthropic) on that result"
94+ state: open
95+ priority: high
96+ author:
97+ name: k33g
98+ email: ph.charriere@gmail.com
99+ createdAt: 2026-09-21T05:45:00.000Z
100+ - id: 3
101+ title: "Check what happens to extended thinking: does reasoningTap see anything, and does the watchdog survive?"
102+ state: open
103+ priority: high
104+ author:
105+ name: k33g
106+ email: ph.charriere@gmail.com
107+ createdAt: 2026-09-21T05:45:00.000Z
108+ - id: 4
109+ title: "probeRemote with the anthropic-version header, so the banner stops crying wolf"
110+ state: open
111+ priority: medium
112+ author:
113+ name: k33g
114+ email: ph.charriere@gmail.com
115+ createdAt: 2026-09-21T05:45:00.000Z
116+ - id: 5
117+ title: "agent.claude.yaml and EN+FR how-to + providers/configuration docs"
118+ state: open
119+ priority: low
120+ author:
121+ name: k33g
122+ email: ph.charriere@gmail.com
123+ createdAt: 2026-09-21T05:45:00.000Z
14124 comments: []
@@ -6,9 +6,119 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:00:56.971Z8 createdAt: 2026-09-21T04:00:56.971Z
9-updatedAt: 2026-09-21T04:00:56.971Z9+updatedAt: 2026-09-21T05:45:00.000Z
10 labels: []10 labels: []
11 milestone: v0.0.311 milestone: v0.0.3
12-body: ""12+body: |
13-tasks: []13+ ## Analysis (Claude, 2026-09-21 — not implemented)
14+
15+ **This ticket is not symmetric with 31. One experiment decides which of two very different routes it takes, and the experiment costs a `curl` and an API key. Run it before writing anything.**
16+
17+ ### The experiment
18+
19+ ```
20+ curl https://api.anthropic.com/v1/chat/completions \
21+ -H "Authorization: Bearer $ANTHROPIC_API_KEY" -H 'Content-Type: application/json' \
22+ -d '{"model":"<a claude id>","stream":true,
23+ "tools":[{"type":"function","function":{"name":"bash","parameters":{...}}}],
24+ "messages":[{"role":"user","content":"list the files here"}]}'
25+ ```
26+
27+ **Does a tool call come back?**
28+
29+ - **Yes** → the same ~20-line registry entry as ticket 31. Half a day.
30+ - **No** → Claude earns a genuinely new `Provider` on Genkit's native `plugins/anthropic`. Two to three days, and it is the case `internal/engine/provider.go` already anticipates in its own words: *"adding a protocol (Anthropic's Messages API…) is a new type."*
31+
32+ Why it is genuinely open: Anthropic's OpenAI-compatible endpoint **is live** (measured — `POST /v1/chat/completions` with no key returns an OpenAI-shaped `{"error":{"code":"invalid_request_error", …}}`), but Genkit's own `compat_oai/anthropic` plugin marks every model `Tools: false` with the comment *"Anthropic supports tool use, but it's not compatible with the OpenAI API"*. That assertion could not be checked without a key, and it dates from 2025. An agent that cannot call tools is not an agent, so this is the whole ticket.
33+
34+ Note mm is **not** bound by that flag: `openaiCompat.Open` declares its own model with `compat_oai.Multimodal`. The flag is Genkit's claim about the wire, not a gate on us.
35+
36+ ### What Genkit v1.10.0 actually ships
37+
38+ | Plugin | Verdict |
39+ |---|---|
40+ | `plugins/compat_oai/anthropic` | **Useless here.** It reads `ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL` from the environment itself, which fights `Backend`; it hard-codes eight model ids; and it marks `Tools: false`. `openaiCompat.Open` already does everything it does, better. |
41+ | `plugins/anthropic` (native) | The real Messages API on `anthropic-sdk-go`. `Tools: true`, `ToolChoice`, `Constrained`. **Streams `ai.NewReasoningPart` on `thinking_delta` and round-trips the thinking signature.** `Init` panics when the key is unset. |
42+
43+ Neither `anthropic-sdk-go` nor `google.golang.org/genai` is in the module cache today — the native route adds real dependencies.
44+
45+ ### Route A — registry entry (if tools work)
46+
47+ Same shape as ticket 31: `baseURL: "https://api.anthropic.com/v1"`, `apiKeyEnv: "ANTHROPIC_API_KEY"`, `keyRequired: true`, `fallback: ""`. Same caveats as 31, with one extra:
48+
49+ - **`reachable()` breaks harder.** It sends a bare GET to `/models` and demands 200; Anthropic's `/v1/models` is the *native* route and returns 400 `"anthropic-version: header is required"` even with a Bearer token. Same `probeRemote` fix as ticket 31, plus the vendor header.
50+ - **Reasoning is the real hazard.** `reasoningTap` (ticket 29) scans for `delta.reasoning_content` / `delta.reasoning`. Anthropic's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — and `engine.go` spells out why that is not cosmetic: `beat()` lives in that listener, so a model silent longer than `watchdogTimeout` is killed as stalled while it is working. **Claude with extended thinking on is exactly that shape.** Mitigations: a much larger `watchdogTimeout` in the shipped YAML, or teaching `reasoningTap` a third spelling if one turns up.
51+
52+ ### Route B — native provider (if tools do not work)
53+
54+ A new `Provider` implementation, not a registry entry. What changes beyond the obvious:
55+
56+ - **No middleware hook.** `option.WithMiddleware(reasoningTap)` is `openai-go/option`; `anthropic-sdk-go` has a same-named, different package. **You do not need it**: the native plugin already emits reasoning parts, so `beat()` fires through the ordinary streaming callback and you add `think.emit(chunk.Reasoning())` next to `emit(chunk.Text())`. Reasoning gets *simpler* — but `Generate` grows a second path and `WithReasoning` becomes provider-conditional.
57+ - **`sampling:` stops being portable.** `config.Cfg.Sampling` goes raw into `ai.WithConfig`. The native plugin maps it onto `anthropic.MessageNewParams` and **hard-errors `"maxTokens not set"` when `max_tokens` is absent**. Either a per-provider dialect or a translation step in `Provider`. *(Unverified: the SDK is not in the module cache.)*
58+ - `Init` **panics** on a missing key where `New`→`Open` returns errors. `keyRequired` catches the ordinary case first, but `Open` wants a guard.
59+ - `Explain` is new work: `errors.As(err, &openai.Error)` never matches, and `statusInText` may not match the other SDK's error strings. No branch exists for Anthropic's 529 overloaded.
60+ - What Route B buys that Route A never can: **extended thinking with signatures flowing straight into the 💭 block through `chunk.Reasoning()`, with the watchdog fed for free**, plus `ThoughtsTokens` in the usage.
61+
62+ ### Cost
63+
64+ | | Route A | Route B |
65+ |---|---|---|
66+ | Code | ~40 lines + ~20 for `probeRemote` | ~150–250 lines, new file |
67+ | New dependencies | none | `anthropic-sdk-go`, not cached |
68+ | Touches | `init()`, one probe func | `Generate`, `Explain`, sampling, `Probe`, `Open` |
69+ | Effort | half a day | 2–3 days |
70+ | Risk | concentrated in one yes/no question | diffuse: a second code path, forever |
71+
72+ ### To decide before coding
73+
74+ 1. **Which surface?** Claude's direct API, or Bedrock? One registry entry cannot serve both.
75+ 2. **Is thinking in scope?** If "support Claude" means "extended thinking visible in the 💭 block", Route A almost certainly cannot deliver it and the answer flips before the experiment is even run.
76+ 3. **Per-vendor entries, or one generic `openai` entry** where the user supplies `baseUrl`? That single entry would cover Claude, Gemini, OpenRouter, Groq, Together and vLLM at once, and is arguably the real ticket behind both 30 and 31. It trades good per-vendor `Explain` words for reach.
77+ 4. Which model ids ship, and are they pinned? Hosted ids go stale in months.
78+ 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed. mm already captures `Usage.InputTokens`.
79+
80+ ### What I would resist
81+
82+ Doing the native route for **both** vendors. Gemini through `googlegenai` costs a silent sampling mismatch and an extra dependency tree for a capability its OpenAI layer already provides (ticket 31).
83+tasks:
84+ - id: 1
85+ title: "EXPERIMENT FIRST: does Anthropic's /v1/chat/completions return tool calls? One curl with a real key."
86+ state: open
87+ priority: high
88+ author:
89+ name: k33g
90+ email: ph.charriere@gmail.com
91+ createdAt: 2026-09-21T05:45:00.000Z
92+ - id: 2
93+ title: "Decide Route A (registry entry) or Route B (native plugins/anthropic) on that result"
94+ state: open
95+ priority: high
96+ author:
97+ name: k33g
98+ email: ph.charriere@gmail.com
99+ createdAt: 2026-09-21T05:45:00.000Z
100+ - id: 3
101+ title: "Check what happens to extended thinking: does reasoningTap see anything, and does the watchdog survive?"
102+ state: open
103+ priority: high
104+ author:
105+ name: k33g
106+ email: ph.charriere@gmail.com
107+ createdAt: 2026-09-21T05:45:00.000Z
108+ - id: 4
109+ title: "probeRemote with the anthropic-version header, so the banner stops crying wolf"
110+ state: open
111+ priority: medium
112+ author:
113+ name: k33g
114+ email: ph.charriere@gmail.com
115+ createdAt: 2026-09-21T05:45:00.000Z
116+ - id: 5
117+ title: "agent.claude.yaml and EN+FR how-to + providers/configuration docs"
118+ state: open
119+ priority: low
120+ author:
121+ name: k33g
122+ email: ph.charriere@gmail.com
123+ createdAt: 2026-09-21T05:45:00.000Z
14 comments: []124 comments: []
added .tickets/issues/0031-how-to-use-mini-me-with-gemini.yaml +109 -0
new file mode 100644
@@ -0,0 +1,109 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 31
3+title: How to use Mini Me with Gemini
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-21T04:01:15.166Z
9+updatedAt: 2026-09-21T05:45:00.000Z
10+labels: []
11+milestone: v0.0.3
12+body: |
13+ ## Analysis (Claude, 2026-09-21 — not implemented)
14+
15+ **Recommendation: one registry entry on the existing `openaiCompat` implementation. Roughly 20 lines. Do this one first — it is the cheap, low-risk half of the pair with ticket 30.**
16+
17+ ### Measured, not assumed
18+
19+ Google's OpenAI-compatible layer is live at `https://generativelanguage.googleapis.com/v1beta/openai`:
20+
21+ ```
22+ POST …/v1beta/openai/chat/completions → 400 "Missing or invalid Authorization header."
23+ GET …/v1beta/openai/models → 404 without a key; 400 "Please pass a valid API key" with a bogus one
24+ ```
25+
26+ Function calling is a first-class surface there, so unlike ticket 30 there is no open question about whether tool use survives.
27+
28+ ### The entry
29+
30+ In `internal/engine/openai_compat.go`'s `init()`, beside `dmr` and `llamacpp`:
31+
32+ ```
33+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai"
34+ apiKeyEnv: "GEMINI_API_KEY"
35+ keyRequired: true
36+ fallback: ""
37+ ```
38+
39+ API-key handling then works untouched: `keyRequired` + `apiKeyEnv` + `Resolve` already produce the right refusal when the variable is unset.
40+
41+ ### What does NOT carry over
42+
43+ | Thing | What happens | Fix |
44+ |---|---|---|
45+ | **`Probe` / reachability** | `reachable()` sends no headers and demands a 200 on `/models`. Gemini never gives one. The banner would print `[warning: … nothing answers at …]` at **every** start-up. | Small, and the seam exists: `probe` is already a pluggable func field on `openaiCompat`. A `probeRemote` that treats 400/401/403 as "the host is there, we are just not authenticated". |
46+ | **Context window** | No `/props` analogue, so `ctx: unknown` forever — and with it `main.go`'s "compression is on but will never trigger" warning. | `contextWindow:` must be set explicitly in the shipped YAML. |
47+ | **Reasoning (ticket 29)** | `reasoningTap` scans for `delta.reasoning_content` / `delta.reasoning`. Gemini's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — see ticket 30, same hazard, same mitigation. | Verify with a key; a generous `watchdogTimeout` in the YAML meanwhile. |
48+ | **`Explain` hints** | Inherited free (it is a method on `*openaiCompat`), and the 401 branch already cites `apiKeyEnv`, which is right for a paid API. But `startHint`/`notFoundHint` need vendor words, the `jinja` branch is dead, and there is no branch for 400 (unknown model, unsupported parameter) or 402. | New words, no new structure. |
49+ | **`toolCallGuard`** | Only accumulates when `ToolRequest.Input` is a string, i.e. partial-JSON argument deltas. If the endpoint delivers tool calls whole, the guard is inert — harmless, just no protection. | Nothing. |
50+
51+ ### Config and docs
52+
53+ **No change to `config.Config`** — `provider`, `baseUrl`, `apiKeyEnv`, `contextWindow`, `sampling`, `watchdogTimeout` already express everything. What is needed: the comment listing the known provider names; a shipped `agent.gemini.yaml` modelled on `agent.llamacpp.yaml` (concrete `model:`, `fallback: ""`, explicit `contextWindow:`, generous `watchdogTimeout:`); `docs/{en,fr}/explanation/providers.md` — its opening sentence still says "two servers"; `reference/configuration.md`; and a new `how-to/run-with-gemini.md` in both languages, the pattern every previous provider followed.
54+
55+ ### Deliberately NOT recommended
56+
57+ The native `plugins/googlegenai` route. It costs a new dependency tree (`google.golang.org/genai`, plus `cloud.google.com/go/auth`), a second code path in `Generate`, and a **silent** sampling mismatch: that plugin maps `sampling` onto `genai.GenerateContentConfig`, whose tags are camelCase (`topP`, `maxOutputTokens`), so mm's snake_case `top_p` and `max_tokens` would be dropped without a word. It would buy native reasoning parts — which the compat layer plus `reasoningTap` may already give. Not worth it for a capability the OpenAI layer provides.
58+
59+ Note **Vertex AI does not fit `Backend` at all** — project + location + ADC, no `apiKeyEnv`, no `baseUrl`. If Vertex is wanted, that is a different ticket with new config keys.
60+
61+ ### To decide before coding
62+
63+ 1. Gemini API (`GEMINI_API_KEY`) or Vertex AI? One registry entry cannot serve both.
64+ 2. Which model ids ship in the YAML, and are they pinned? Hosted ids go stale in months.
65+ 3. **Is this really two tickets, or one?** A single generic `openai` entry where the user supplies `baseUrl` would cover Gemini, Claude, OpenRouter, Groq, Together and vLLM at once. It trades good per-vendor `Explain` words for reach. See ticket 30.
66+ 4. `parallel_tool_calls: false` is in the shipped YAMLs — keep sending it to a remote provider that may reject or ignore it?
67+ 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed.
68+tasks:
69+ - id: 1
70+ title: "Registry entry for the Gemini OpenAI-compatible endpoint (baseURL, GEMINI_API_KEY, keyRequired)"
71+ state: open
72+ priority: medium
73+ author:
74+ name: k33g
75+ email: ph.charriere@gmail.com
76+ createdAt: 2026-09-21T05:45:00.000Z
77+ - id: 2
78+ title: "probeRemote: treat 400/401/403 as reachable, so the banner stops crying wolf"
79+ state: open
80+ priority: medium
81+ author:
82+ name: k33g
83+ email: ph.charriere@gmail.com
84+ createdAt: 2026-09-21T05:45:00.000Z
85+ - id: 3
86+ title: "Check whether the endpoint streams reasoning, and whether reasoningTap sees it"
87+ state: open
88+ priority: high
89+ author:
90+ name: k33g
91+ email: ph.charriere@gmail.com
92+ createdAt: 2026-09-21T05:45:00.000Z
93+ - id: 4
94+ title: "Explain: vendor words for startHint/notFoundHint, a 400 branch, drop the dead jinja branch"
95+ state: open
96+ priority: low
97+ author:
98+ name: k33g
99+ email: ph.charriere@gmail.com
100+ createdAt: 2026-09-21T05:45:00.000Z
101+ - id: 5
102+ title: "agent.gemini.yaml with an explicit contextWindow, and EN+FR how-to + providers/configuration docs"
103+ state: open
104+ priority: low
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-09-21T05:45:00.000Z
109+comments: []
new file mode 100644
@@ -0,0 +1,109 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 31
3+title: How to use Mini Me with Gemini
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-09-21T04:01:15.166Z
9+updatedAt: 2026-09-21T05:45:00.000Z
10+labels: []
11+milestone: v0.0.3
12+body: |
13+ ## Analysis (Claude, 2026-09-21 — not implemented)
14+
15+ **Recommendation: one registry entry on the existing `openaiCompat` implementation. Roughly 20 lines. Do this one first — it is the cheap, low-risk half of the pair with ticket 30.**
16+
17+ ### Measured, not assumed
18+
19+ Google's OpenAI-compatible layer is live at `https://generativelanguage.googleapis.com/v1beta/openai`:
20+
21+ ```
22+ POST …/v1beta/openai/chat/completions → 400 "Missing or invalid Authorization header."
23+ GET …/v1beta/openai/models → 404 without a key; 400 "Please pass a valid API key" with a bogus one
24+ ```
25+
26+ Function calling is a first-class surface there, so unlike ticket 30 there is no open question about whether tool use survives.
27+
28+ ### The entry
29+
30+ In `internal/engine/openai_compat.go`'s `init()`, beside `dmr` and `llamacpp`:
31+
32+ ```
33+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai"
34+ apiKeyEnv: "GEMINI_API_KEY"
35+ keyRequired: true
36+ fallback: ""
37+ ```
38+
39+ API-key handling then works untouched: `keyRequired` + `apiKeyEnv` + `Resolve` already produce the right refusal when the variable is unset.
40+
41+ ### What does NOT carry over
42+
43+ | Thing | What happens | Fix |
44+ |---|---|---|
45+ | **`Probe` / reachability** | `reachable()` sends no headers and demands a 200 on `/models`. Gemini never gives one. The banner would print `[warning: … nothing answers at …]` at **every** start-up. | Small, and the seam exists: `probe` is already a pluggable func field on `openaiCompat`. A `probeRemote` that treats 400/401/403 as "the host is there, we are just not authenticated". |
46+ | **Context window** | No `/props` analogue, so `ctx: unknown` forever — and with it `main.go`'s "compression is on but will never trigger" warning. | `contextWindow:` must be set explicitly in the shipped YAML. |
47+ | **Reasoning (ticket 29)** | `reasoningTap` scans for `delta.reasoning_content` / `delta.reasoning`. Gemini's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — see ticket 30, same hazard, same mitigation. | Verify with a key; a generous `watchdogTimeout` in the YAML meanwhile. |
48+ | **`Explain` hints** | Inherited free (it is a method on `*openaiCompat`), and the 401 branch already cites `apiKeyEnv`, which is right for a paid API. But `startHint`/`notFoundHint` need vendor words, the `jinja` branch is dead, and there is no branch for 400 (unknown model, unsupported parameter) or 402. | New words, no new structure. |
49+ | **`toolCallGuard`** | Only accumulates when `ToolRequest.Input` is a string, i.e. partial-JSON argument deltas. If the endpoint delivers tool calls whole, the guard is inert — harmless, just no protection. | Nothing. |
50+
51+ ### Config and docs
52+
53+ **No change to `config.Config`** — `provider`, `baseUrl`, `apiKeyEnv`, `contextWindow`, `sampling`, `watchdogTimeout` already express everything. What is needed: the comment listing the known provider names; a shipped `agent.gemini.yaml` modelled on `agent.llamacpp.yaml` (concrete `model:`, `fallback: ""`, explicit `contextWindow:`, generous `watchdogTimeout:`); `docs/{en,fr}/explanation/providers.md` — its opening sentence still says "two servers"; `reference/configuration.md`; and a new `how-to/run-with-gemini.md` in both languages, the pattern every previous provider followed.
54+
55+ ### Deliberately NOT recommended
56+
57+ The native `plugins/googlegenai` route. It costs a new dependency tree (`google.golang.org/genai`, plus `cloud.google.com/go/auth`), a second code path in `Generate`, and a **silent** sampling mismatch: that plugin maps `sampling` onto `genai.GenerateContentConfig`, whose tags are camelCase (`topP`, `maxOutputTokens`), so mm's snake_case `top_p` and `max_tokens` would be dropped without a word. It would buy native reasoning parts — which the compat layer plus `reasoningTap` may already give. Not worth it for a capability the OpenAI layer provides.
58+
59+ Note **Vertex AI does not fit `Backend` at all** — project + location + ADC, no `apiKeyEnv`, no `baseUrl`. If Vertex is wanted, that is a different ticket with new config keys.
60+
61+ ### To decide before coding
62+
63+ 1. Gemini API (`GEMINI_API_KEY`) or Vertex AI? One registry entry cannot serve both.
64+ 2. Which model ids ship in the YAML, and are they pinned? Hosted ids go stale in months.
65+ 3. **Is this really two tickets, or one?** A single generic `openai` entry where the user supplies `baseUrl` would cover Gemini, Claude, OpenRouter, Groq, Together and vLLM at once. It trades good per-vendor `Explain` words for reach. See ticket 30.
66+ 4. `parallel_tool_calls: false` is in the shipped YAMLs — keep sending it to a remote provider that may reject or ignore it?
67+ 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed.
68+tasks:
69+ - id: 1
70+ title: "Registry entry for the Gemini OpenAI-compatible endpoint (baseURL, GEMINI_API_KEY, keyRequired)"
71+ state: open
72+ priority: medium
73+ author:
74+ name: k33g
75+ email: ph.charriere@gmail.com
76+ createdAt: 2026-09-21T05:45:00.000Z
77+ - id: 2
78+ title: "probeRemote: treat 400/401/403 as reachable, so the banner stops crying wolf"
79+ state: open
80+ priority: medium
81+ author:
82+ name: k33g
83+ email: ph.charriere@gmail.com
84+ createdAt: 2026-09-21T05:45:00.000Z
85+ - id: 3
86+ title: "Check whether the endpoint streams reasoning, and whether reasoningTap sees it"
87+ state: open
88+ priority: high
89+ author:
90+ name: k33g
91+ email: ph.charriere@gmail.com
92+ createdAt: 2026-09-21T05:45:00.000Z
93+ - id: 4
94+ title: "Explain: vendor words for startHint/notFoundHint, a 400 branch, drop the dead jinja branch"
95+ state: open
96+ priority: low
97+ author:
98+ name: k33g
99+ email: ph.charriere@gmail.com
100+ createdAt: 2026-09-21T05:45:00.000Z
101+ - id: 5
102+ title: "agent.gemini.yaml with an explicit contextWindow, and EN+FR how-to + providers/configuration docs"
103+ state: open
104+ priority: low
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-09-21T05:45:00.000Z
109+comments: []
modified .tickets/issues/0032-add-prompt-flag.yaml +2 -1
@@ -6,8 +6,9 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:47:19.089Z
9-updatedAt: 2026-09-21T04:47:19.089Z
9+updatedAt: 2026-09-21T04:58:31.828Z
1010 labels: []
11+milestone: v0.04
1112 epic: CLI
1213 body: ""
1314 tasks: []
@@ -6,8 +6,9 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:47:19.089Z8 createdAt: 2026-09-21T04:47:19.089Z
9-updatedAt: 2026-09-21T04:47:19.089Z9+updatedAt: 2026-09-21T04:58:31.828Z
10 labels: []10 labels: []
11+milestone: v0.04
11 epic: CLI12 epic: CLI
12 body: ""13 body: ""
13 tasks: []14 tasks: []
modified .tickets/issues/0033-invoke-another-mini-me.yaml +2 -1
@@ -6,8 +6,9 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:48:03.596Z
9-updatedAt: 2026-09-21T04:48:35.612Z
9+updatedAt: 2026-09-21T04:58:54.762Z
1010 labels: []
11+milestone: v0.04
1112 epic: Builtin tools
1213 body: |
1314 Should I use ACP ? I think yes
@@ -6,8 +6,9 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:48:03.596Z8 createdAt: 2026-09-21T04:48:03.596Z
9-updatedAt: 2026-09-21T04:48:35.612Z9+updatedAt: 2026-09-21T04:58:54.762Z
10 labels: []10 labels: []
11+milestone: v0.04
11 epic: Builtin tools12 epic: Builtin tools
12 body: |13 body: |
13 Should I use ACP ? I think yes14 Should I use ACP ? I think yes
renamed .tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml +2 -2
similarity index 74%
rename from .tickets/issues/0035-create-sbx-templat-kit-for-mini-me.yaml
rename to .tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 35
3-title: Create SBX Templat + Kit for Mini-Me
3+title: Create SBX Template + Kit for Mini-Me
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:51:17.051Z
9-updatedAt: 2026-09-21T04:51:17.051Z
9+updatedAt: 2026-09-21T04:55:19.972Z
1010 labels: []
1111 milestone: v0.0.3
1212 body: ""
similarity index 74%
rename from .tickets/issues/0035-create-sbx-templat-kit-for-mini-me.yaml
rename to .tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 352 id: 35
3-title: Create SBX Templat + Kit for Mini-Me3+title: Create SBX Template + Kit for Mini-Me
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:51:17.051Z8 createdAt: 2026-09-21T04:51:17.051Z
9-updatedAt: 2026-09-21T04:51:17.051Z9+updatedAt: 2026-09-21T04:55:19.972Z
10 labels: []10 labels: []
11 milestone: v0.0.311 milestone: v0.0.3
12 body: ""12 body: ""
renamed .tickets/issues/0036-hooks.yaml +5 -5
similarity index 57%
rename from .tickets/issues/0031-hoe-to-use-mini-me-with-gemini.yaml
rename to .tickets/issues/0036-hooks.yaml
@@ -1,14 +1,14 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 31
3-title: Hoe to use Mini Me with Gemini
2+id: 36
3+title: Hooks
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
8-createdAt: 2026-09-21T04:01:15.166Z
9-updatedAt: 2026-09-21T04:01:15.166Z
8+createdAt: 2026-09-21T06:32:48.566Z
9+updatedAt: 2026-09-21T06:32:48.566Z
1010 labels: []
11-milestone: v0.0.3
11+milestone: v0.04
1212 body: ""
1313 tasks: []
1414 comments: []
similarity index 57%
rename from .tickets/issues/0031-hoe-to-use-mini-me-with-gemini.yaml
rename to .tickets/issues/0036-hooks.yaml
@@ -1,14 +1,14 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 312+id: 36
3-title: Hoe to use Mini Me with Gemini3+title: Hooks
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8-createdAt: 2026-09-21T04:01:15.166Z8+createdAt: 2026-09-21T06:32:48.566Z
9-updatedAt: 2026-09-21T04:01:15.166Z9+updatedAt: 2026-09-21T06:32:48.566Z
10 labels: []10 labels: []
11-milestone: v0.0.311+milestone: v0.04
12 body: ""12 body: ""
13 tasks: []13 tasks: []
14 comments: []14 comments: []
modified README.md +1 -0
@@ -28,6 +28,7 @@ cd mini-me
2828 ./install.sh # go build, then copies mm to /usr/local/bin
2929 mm -h # the flags
3030 mm --version # which mm this is
31+mm --about # what it is, in one screen
3132 ```
3233 > `install.sh` works only on macOS and Linux
3334
@@ -28,6 +28,7 @@ cd mini-me
28 ./install.sh # go build, then copies mm to /usr/local/bin28 ./install.sh # go build, then copies mm to /usr/local/bin
29 mm -h # the flags29 mm -h # the flags
30 mm --version # which mm this is30 mm --version # which mm this is
31+mm --about # what it is, in one screen
31 ```32 ```
32 > `install.sh` works only on macOS and Linux33 > `install.sh` works only on macOS and Linux
33 34
modified docs/en/reference/cli.md +4 -2
@@ -5,7 +5,7 @@
55 ## Invocation
66
77 ```
8-mm [-config <path>] [-acp] [-tui] [--version] [<path>]
8+mm [-config <path>] [-acp] [-tui] [--version] [--about] [<path>]
99 ```
1010
1111 | Element | Type | Default | Description |
@@ -15,6 +15,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>]
1515 | `-acp` | bool | `false` | Serve the Agent Client Protocol on stdin/stdout instead of the terminal REPL. |
1616 | `-tui` | bool | `false` | Open the single-window mode (conversation above, input box below). Same effect as `tui: true` in the YAML; ignored with `-acp`. Falls back to the line mode when stdin or stdout is not a terminal. |
1717 | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. |
18+| `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. |
1819 | `-h` | — | — | Print the flag usage and exit. |
1920
2021 ## Version
@@ -25,7 +26,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>]
2526 mm v0.0.3 (go1.26.5, darwin/arm64)
2627 ```
2728
28-The same version opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters.
29+`mm --about` prints the same version inside a short description of the program. The version also opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters.
2930
3031 | Where it comes from | What you see |
3132 |---|---|
@@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
9293 | Screen | The terminal's alternate screen, released on exit; the terminal's own scrollback is not written to. |
9394 | Banner | Not printed on stdout; shown as the first line of the conversation. |
9495 | Spinner | Disabled; the rule under the conversation animates instead. |
96+| Model name | The header shows a short form of the model identifier: registry host and organisation dropped, the `-gguf` format suffix dropped, the tag kept, capped at 28 characters (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). The full identifier is in the start-up banner, which the window keeps as the conversation's first line. |
9597 | Layout | Header line (agent, model `@` provider, context use as `ctx N% (used/window)` or `ctx ~used (window unknown)`, `session <id>` when sessions are on); conversation with a scroll bar; rule with state and key hints; 3-row input box; status bar (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` on the left, `<version> · N msgs · N tools` on the right — the version is dropped when the two sides would not fit, so the key hints are never truncated to make room for it). |
9698 | Tool calls | `‣ <tool> <detail> <status> <time>` where status is `… pending`, `… waiting for permission`, `… running`, `✓ done` or `✗ failed`; output folded to `previewLines` lines (`… N more line(s)`); `+`/`-` lines of `write_file`/`edit_file` output coloured. |
9799 | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. |
@@ -5,7 +5,7 @@
5 ## Invocation5 ## Invocation
6 6
7 ```7 ```
8-mm [-config <path>] [-acp] [-tui] [--version] [<path>]8+mm [-config <path>] [-acp] [-tui] [--version] [--about] [<path>]
9 ```9 ```
10 10
11 | Element | Type | Default | Description |11 | Element | Type | Default | Description |
@@ -15,6 +15,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>]
15 | `-acp` | bool | `false` | Serve the Agent Client Protocol on stdin/stdout instead of the terminal REPL. |15 | `-acp` | bool | `false` | Serve the Agent Client Protocol on stdin/stdout instead of the terminal REPL. |
16 | `-tui` | bool | `false` | Open the single-window mode (conversation above, input box below). Same effect as `tui: true` in the YAML; ignored with `-acp`. Falls back to the line mode when stdin or stdout is not a terminal. |16 | `-tui` | bool | `false` | Open the single-window mode (conversation above, input box below). Same effect as `tui: true` in the YAML; ignored with `-acp`. Falls back to the line mode when stdin or stdout is not a terminal. |
17 | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. |17 | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. |
18+| `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. |
18 | `-h` | — | — | Print the flag usage and exit. |19 | `-h` | — | — | Print the flag usage and exit. |
19 20
20 ## Version21 ## Version
@@ -25,7 +26,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>]
25 mm v0.0.3 (go1.26.5, darwin/arm64)26 mm v0.0.3 (go1.26.5, darwin/arm64)
26 ```27 ```
27 28
28-The same version opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters.29+`mm --about` prints the same version inside a short description of the program. The version also opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters.
29 30
30 | Where it comes from | What you see |31 | Where it comes from | What you see |
31 |---|---|32 |---|---|
@@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
92 | Screen | The terminal's alternate screen, released on exit; the terminal's own scrollback is not written to. |93 | Screen | The terminal's alternate screen, released on exit; the terminal's own scrollback is not written to. |
93 | Banner | Not printed on stdout; shown as the first line of the conversation. |94 | Banner | Not printed on stdout; shown as the first line of the conversation. |
94 | Spinner | Disabled; the rule under the conversation animates instead. |95 | Spinner | Disabled; the rule under the conversation animates instead. |
96+| Model name | The header shows a short form of the model identifier: registry host and organisation dropped, the `-gguf` format suffix dropped, the tag kept, capped at 28 characters (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). The full identifier is in the start-up banner, which the window keeps as the conversation's first line. |
95 | Layout | Header line (agent, model `@` provider, context use as `ctx N% (used/window)` or `ctx ~used (window unknown)`, `session <id>` when sessions are on); conversation with a scroll bar; rule with state and key hints; 3-row input box; status bar (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` on the left, `<version> · N msgs · N tools` on the right — the version is dropped when the two sides would not fit, so the key hints are never truncated to make room for it). |97 | Layout | Header line (agent, model `@` provider, context use as `ctx N% (used/window)` or `ctx ~used (window unknown)`, `session <id>` when sessions are on); conversation with a scroll bar; rule with state and key hints; 3-row input box; status bar (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` on the left, `<version> · N msgs · N tools` on the right — the version is dropped when the two sides would not fit, so the key hints are never truncated to make room for it). |
96 | Tool calls | `‣ <tool> <detail> <status> <time>` where status is `… pending`, `… waiting for permission`, `… running`, `✓ done` or `✗ failed`; output folded to `previewLines` lines (`… N more line(s)`); `+`/`-` lines of `write_file`/`edit_file` output coloured. |98 | Tool calls | `‣ <tool> <detail> <status> <time>` where status is `… pending`, `… waiting for permission`, `… running`, `✓ done` or `✗ failed`; output folded to `previewLines` lines (`… N more line(s)`); `+`/`-` lines of `write_file`/`edit_file` output coloured. |
97 | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. |99 | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. |
modified docs/fr/reference/cli.md +4 -2
@@ -5,7 +5,7 @@
55 ## Invocation
66
77 ```
8-mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>]
8+mm [-config <chemin>] [-acp] [-tui] [--version] [--about] [<chemin>]
99 ```
1010
1111 | Élément | Type | Défaut | Description |
@@ -15,6 +15,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>]
1515 | `-acp` | booléen | `false` | Sert l'Agent Client Protocol sur stdin/stdout au lieu du REPL du terminal. |
1616 | `-tui` | booléen | `false` | Ouvre le mode fenêtre unique (conversation en haut, zone de saisie en bas). Même effet que `tui: true` dans le YAML ; ignoré avec `-acp`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal. |
1717 | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. |
18+| `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. |
1819 | `-h` | — | — | Affiche l'usage des options et quitte. |
1920
2021 ## Version
@@ -25,7 +26,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>]
2526 mm v0.0.3 (go1.26.5, darwin/arm64)
2627 ```
2728
28-La même version ouvre la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs.
29+`mm --about` affiche la même version à l'intérieur d'une courte description du programme. La version ouvre aussi la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs.
2930
3031 | D'où elle vient | Ce que vous voyez |
3132 |---|---|
@@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
9293 | Écran | L'écran alternatif du terminal, rendu à la sortie ; l'historique de défilement du terminal n'est pas écrit. |
9394 | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. |
9495 | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. |
96+| Nom du modèle | L'en-tête affiche une forme courte de l'identifiant : hôte du registre et organisation retirés, suffixe de format `-gguf` retiré, tag conservé, plafonné à 28 caractères (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). L'identifiant complet figure dans la bannière de démarrage, que la fenêtre conserve comme première ligne de la conversation. |
9597 | Disposition | Ligne d'en-tête (agent, modèle `@` fournisseur, usage du contexte en `ctx N% (utilisé/fenêtre)` ou `ctx ~utilisé (window unknown)`, `session <id>` quand les sessions sont actives) ; conversation avec barre de défilement ; règle avec l'état et les rappels de touches ; zone de saisie de 3 lignes ; barre d'état (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` à gauche, `<version> · N msgs · N tools` à droite — la version disparaît quand les deux côtés ne tiennent pas, pour que les rappels de touches ne soient jamais tronqués à cause d'elle). |
9698 | Appels d'outils | `‣ <outil> <détail> <statut> <durée>` où le statut est `… pending`, `… waiting for permission`, `… running`, `✓ done` ou `✗ failed` ; sortie repliée à `previewLines` lignes (`… N more line(s)`) ; lignes `+`/`-` des sorties de `write_file`/`edit_file` colorées. |
9799 | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. |
@@ -5,7 +5,7 @@
5 ## Invocation5 ## Invocation
6 6
7 ```7 ```
8-mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>]8+mm [-config <chemin>] [-acp] [-tui] [--version] [--about] [<chemin>]
9 ```9 ```
10 10
11 | Élément | Type | Défaut | Description |11 | Élément | Type | Défaut | Description |
@@ -15,6 +15,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>]
15 | `-acp` | booléen | `false` | Sert l'Agent Client Protocol sur stdin/stdout au lieu du REPL du terminal. |15 | `-acp` | booléen | `false` | Sert l'Agent Client Protocol sur stdin/stdout au lieu du REPL du terminal. |
16 | `-tui` | booléen | `false` | Ouvre le mode fenêtre unique (conversation en haut, zone de saisie en bas). Même effet que `tui: true` dans le YAML ; ignoré avec `-acp`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal. |16 | `-tui` | booléen | `false` | Ouvre le mode fenêtre unique (conversation en haut, zone de saisie en bas). Même effet que `tui: true` dans le YAML ; ignoré avec `-acp`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal. |
17 | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. |17 | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. |
18+| `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. |
18 | `-h` | — | — | Affiche l'usage des options et quitte. |19 | `-h` | — | — | Affiche l'usage des options et quitte. |
19 20
20 ## Version21 ## Version
@@ -25,7 +26,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>]
25 mm v0.0.3 (go1.26.5, darwin/arm64)26 mm v0.0.3 (go1.26.5, darwin/arm64)
26 ```27 ```
27 28
28-La même version ouvre la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs.29+`mm --about` affiche la même version à l'intérieur d'une courte description du programme. La version ouvre aussi la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs.
29 30
30 | D'où elle vient | Ce que vous voyez |31 | D'où elle vient | Ce que vous voyez |
31 |---|---|32 |---|---|
@@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
92 | Écran | L'écran alternatif du terminal, rendu à la sortie ; l'historique de défilement du terminal n'est pas écrit. |93 | Écran | L'écran alternatif du terminal, rendu à la sortie ; l'historique de défilement du terminal n'est pas écrit. |
93 | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. |94 | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. |
94 | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. |95 | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. |
96+| Nom du modèle | L'en-tête affiche une forme courte de l'identifiant : hôte du registre et organisation retirés, suffixe de format `-gguf` retiré, tag conservé, plafonné à 28 caractères (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). L'identifiant complet figure dans la bannière de démarrage, que la fenêtre conserve comme première ligne de la conversation. |
95 | Disposition | Ligne d'en-tête (agent, modèle `@` fournisseur, usage du contexte en `ctx N% (utilisé/fenêtre)` ou `ctx ~utilisé (window unknown)`, `session <id>` quand les sessions sont actives) ; conversation avec barre de défilement ; règle avec l'état et les rappels de touches ; zone de saisie de 3 lignes ; barre d'état (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` à gauche, `<version> · N msgs · N tools` à droite — la version disparaît quand les deux côtés ne tiennent pas, pour que les rappels de touches ne soient jamais tronqués à cause d'elle). |97 | Disposition | Ligne d'en-tête (agent, modèle `@` fournisseur, usage du contexte en `ctx N% (utilisé/fenêtre)` ou `ctx ~utilisé (window unknown)`, `session <id>` quand les sessions sont actives) ; conversation avec barre de défilement ; règle avec l'état et les rappels de touches ; zone de saisie de 3 lignes ; barre d'état (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` à gauche, `<version> · N msgs · N tools` à droite — la version disparaît quand les deux côtés ne tiennent pas, pour que les rappels de touches ne soient jamais tronqués à cause d'elle). |
96 | Appels d'outils | `‣ <outil> <détail> <statut> <durée>` où le statut est `… pending`, `… waiting for permission`, `… running`, `✓ done` ou `✗ failed` ; sortie repliée à `previewLines` lignes (`… N more line(s)`) ; lignes `+`/`-` des sorties de `write_file`/`edit_file` colorées. |98 | Appels d'outils | `‣ <outil> <détail> <statut> <durée>` où le statut est `… pending`, `… waiting for permission`, `… running`, `✓ done` ou `✗ failed` ; sortie repliée à `previewLines` lignes (`… N more line(s)`) ; lignes `+`/`-` des sorties de `write_file`/`edit_file` colorées. |
97 | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. |99 | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. |
added internal/tui/modelname.go +89 -0
new file mode 100644
@@ -0,0 +1,89 @@
1+package tui
2+
3+import "strings"
4+
5+// Model identifiers are addresses, not names. A local registry hands out
6+// things like
7+//
8+// huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
9+//
10+// seventy characters of which maybe thirty say anything, and the header is
11+// the one line that is on screen the whole session — next to the agent, the
12+// provider, the context gauge and the session id, all of which get pushed off
13+// the right edge by it (ticket 24).
14+//
15+// The full identifier stays reachable: the start-up banner prints it as it
16+// is, and the window keeps that banner as the conversation's first line.
17+
18+// modelWidth is how much of the header one model may take. Past it the name
19+// is cut with an ellipsis: a header that is all model is not a header.
20+const modelWidth = 28
21+
22+// quantLike matches the tokens a GGUF quantisation suffix is made of —
23+// "q4_k_m", "bf16", "iq3_xxs", "f16". Used only to recognise a repeat of the
24+// tag inside the name, never to strip something that might be a real variant:
25+// "…-gguf-smashed" keeps its "smashed", which says the weights were pruned.
26+func quantLike(s string) bool {
27+ s = strings.ToLower(s)
28+ switch {
29+ case strings.HasPrefix(s, "q") && len(s) > 1 && s[1] >= '0' && s[1] <= '9':
30+ return true
31+ case strings.HasPrefix(s, "iq") && len(s) > 2 && s[2] >= '0' && s[2] <= '9':
32+ return true
33+ case s == "bf16", s == "f16", s == "f32":
34+ return true
35+ }
36+ return false
37+}
38+
39+// shortModel is the header's name for a model: the last path segment, without
40+// the file format, with the tag kept — the tag is the one part of the address
41+// that says what is actually loaded — and capped.
42+//
43+// huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M
44+// …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M
45+// ai/smollm2 → smollm2
46+// gpt-4o → gpt-4o
47+func shortModel(id string) string {
48+ if id == "" {
49+ return ""
50+ }
51+ // The registry host and the organisation are routing, not identity.
52+ if i := strings.LastIndex(id, "/"); i >= 0 {
53+ id = id[i+1:]
54+ }
55+ name, tag, hasTag := strings.Cut(id, ":")
56+ name = dropFormat(name)
57+ if !hasTag {
58+ return clipName(name, modelWidth)
59+ }
60+ // The tag is kept whole and the name pays for it: ":Q4_K_M" is four fewer
61+ // characters than the name would lose, and it is the part that changes
62+ // between two otherwise identical models.
63+ return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag
64+}
65+
66+// dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where
67+// the quantisation is already about to be shown as the tag.
68+func dropFormat(name string) string {
69+ lower := strings.ToLower(name)
70+ if i := strings.LastIndex(lower, "-gguf"); i > 0 {
71+ rest := lower[i+len("-gguf"):]
72+ if rest == "" || (strings.HasPrefix(rest, "-") && quantLike(rest[1:])) {
73+ return name[:i]
74+ }
75+ }
76+ return name
77+}
78+
79+// clipName cuts a name to width runes, ellipsis included in the count.
80+func clipName(name string, width int) string {
81+ r := []rune(name)
82+ if len(r) <= width {
83+ return name
84+ }
85+ if width <= 1 {
86+ return "…"
87+ }
88+ return string(r[:width-1]) + "…"
89+}
new file mode 100644
@@ -0,0 +1,89 @@
1+package tui
2+
3+import "strings"
4+
5+// Model identifiers are addresses, not names. A local registry hands out
6+// things like
7+//
8+// huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
9+//
10+// seventy characters of which maybe thirty say anything, and the header is
11+// the one line that is on screen the whole session — next to the agent, the
12+// provider, the context gauge and the session id, all of which get pushed off
13+// the right edge by it (ticket 24).
14+//
15+// The full identifier stays reachable: the start-up banner prints it as it
16+// is, and the window keeps that banner as the conversation's first line.
17+
18+// modelWidth is how much of the header one model may take. Past it the name
19+// is cut with an ellipsis: a header that is all model is not a header.
20+const modelWidth = 28
21+
22+// quantLike matches the tokens a GGUF quantisation suffix is made of —
23+// "q4_k_m", "bf16", "iq3_xxs", "f16". Used only to recognise a repeat of the
24+// tag inside the name, never to strip something that might be a real variant:
25+// "…-gguf-smashed" keeps its "smashed", which says the weights were pruned.
26+func quantLike(s string) bool {
27+ s = strings.ToLower(s)
28+ switch {
29+ case strings.HasPrefix(s, "q") && len(s) > 1 && s[1] >= '0' && s[1] <= '9':
30+ return true
31+ case strings.HasPrefix(s, "iq") && len(s) > 2 && s[2] >= '0' && s[2] <= '9':
32+ return true
33+ case s == "bf16", s == "f16", s == "f32":
34+ return true
35+ }
36+ return false
37+}
38+
39+// shortModel is the header's name for a model: the last path segment, without
40+// the file format, with the tag kept — the tag is the one part of the address
41+// that says what is actually loaded — and capped.
42+//
43+// huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M
44+// …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M
45+// ai/smollm2 → smollm2
46+// gpt-4o → gpt-4o
47+func shortModel(id string) string {
48+ if id == "" {
49+ return ""
50+ }
51+ // The registry host and the organisation are routing, not identity.
52+ if i := strings.LastIndex(id, "/"); i >= 0 {
53+ id = id[i+1:]
54+ }
55+ name, tag, hasTag := strings.Cut(id, ":")
56+ name = dropFormat(name)
57+ if !hasTag {
58+ return clipName(name, modelWidth)
59+ }
60+ // The tag is kept whole and the name pays for it: ":Q4_K_M" is four fewer
61+ // characters than the name would lose, and it is the part that changes
62+ // between two otherwise identical models.
63+ return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag
64+}
65+
66+// dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where
67+// the quantisation is already about to be shown as the tag.
68+func dropFormat(name string) string {
69+ lower := strings.ToLower(name)
70+ if i := strings.LastIndex(lower, "-gguf"); i > 0 {
71+ rest := lower[i+len("-gguf"):]
72+ if rest == "" || (strings.HasPrefix(rest, "-") && quantLike(rest[1:])) {
73+ return name[:i]
74+ }
75+ }
76+ return name
77+}
78+
79+// clipName cuts a name to width runes, ellipsis included in the count.
80+func clipName(name string, width int) string {
81+ r := []rune(name)
82+ if len(r) <= width {
83+ return name
84+ }
85+ if width <= 1 {
86+ return "…"
87+ }
88+ return string(r[:width-1]) + "…"
89+}
added internal/tui/modelname_test.go +75 -0
new file mode 100644
@@ -0,0 +1,75 @@
1+package tui
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+func TestShortModel(t *testing.T) {
9+ cases := []struct{ name, id, want string }{
10+ {"host and org go, format goes, tag stays",
11+ "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M", "qwen3.5-2b:Q4_K_M"},
12+ {"a name repeating its own quantisation drops it once",
13+ "huggingface.co/unsloth/gemma-4-e2b-it-gguf:Q4_K_M", "gemma-4-e2b-it:Q4_K_M"},
14+ {"a real variant marker is NOT mistaken for a format suffix",
15+ "huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M",
16+ "meta-llama-guard-2-8…:Q4_K_M"},
17+ {"short ids are left alone", "ai/smollm2", "smollm2"},
18+ {"an id with no path at all", "gpt-4o", "gpt-4o"},
19+ {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"},
20+ {"empty stays empty", "", ""},
21+ }
22+ for _, tc := range cases {
23+ t.Run(tc.name, func(t *testing.T) {
24+ if got := shortModel(tc.id); got != tc.want {
25+ t.Errorf("shortModel(%q)\n got %q\nwant %q", tc.id, got, tc.want)
26+ }
27+ })
28+ }
29+}
30+
31+// Whatever the id, the header's share of it is bounded — that is the ticket.
32+func TestShortModelIsBounded(t *testing.T) {
33+ ids := []string{
34+ "huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M",
35+ "huggingface.co/vendor/" + strings.Repeat("very-long-", 20) + "name:Q8_0",
36+ strings.Repeat("x", 500),
37+ }
38+ for _, id := range ids {
39+ got := shortModel(id)
40+ if len([]rune(got)) > modelWidth+1 { // +1: the ":" before a kept tag
41+ t.Errorf("shortModel(%.40q…) = %q, %d runes — the header is all model", id, got, len([]rune(got)))
42+ }
43+ }
44+}
45+
46+// The tag survives the cut: it is what tells two otherwise identical models
47+// apart, and it is the part a user checks.
48+func TestShortModelKeepsTheTag(t *testing.T) {
49+ got := shortModel("huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M")
50+ if !strings.HasSuffix(got, ":Q4_K_M") {
51+ t.Errorf("shortModel = %q, want it to end with the tag", got)
52+ }
53+ if !strings.HasPrefix(got, "mellum2-12b") {
54+ t.Errorf("shortModel = %q, want it to start with the model's own name", got)
55+ }
56+}
57+
58+// The header uses it, and the rest of the header still fits beside it.
59+func TestHeaderUsesTheShortName(t *testing.T) {
60+ r, _ := newTestRunner(t, &fakeModel{answer: "hi"})
61+ r.opts.ModelName = "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M"
62+
63+ got := r.header()
64+ if strings.Contains(got, "huggingface.co") {
65+ t.Errorf("the header still carries the registry host: %q", got)
66+ }
67+ if !strings.Contains(got, "qwen3.5-2b:Q4_K_M") {
68+ t.Errorf("the header lost the model: %q", got)
69+ }
70+ for _, want := range []string{"mm ·", "@ p", "ctx"} {
71+ if !strings.Contains(got, want) {
72+ t.Errorf("the header lost %q: %q", want, got)
73+ }
74+ }
75+}
new file mode 100644
@@ -0,0 +1,75 @@
1+package tui
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+func TestShortModel(t *testing.T) {
9+ cases := []struct{ name, id, want string }{
10+ {"host and org go, format goes, tag stays",
11+ "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M", "qwen3.5-2b:Q4_K_M"},
12+ {"a name repeating its own quantisation drops it once",
13+ "huggingface.co/unsloth/gemma-4-e2b-it-gguf:Q4_K_M", "gemma-4-e2b-it:Q4_K_M"},
14+ {"a real variant marker is NOT mistaken for a format suffix",
15+ "huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M",
16+ "meta-llama-guard-2-8…:Q4_K_M"},
17+ {"short ids are left alone", "ai/smollm2", "smollm2"},
18+ {"an id with no path at all", "gpt-4o", "gpt-4o"},
19+ {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"},
20+ {"empty stays empty", "", ""},
21+ }
22+ for _, tc := range cases {
23+ t.Run(tc.name, func(t *testing.T) {
24+ if got := shortModel(tc.id); got != tc.want {
25+ t.Errorf("shortModel(%q)\n got %q\nwant %q", tc.id, got, tc.want)
26+ }
27+ })
28+ }
29+}
30+
31+// Whatever the id, the header's share of it is bounded — that is the ticket.
32+func TestShortModelIsBounded(t *testing.T) {
33+ ids := []string{
34+ "huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M",
35+ "huggingface.co/vendor/" + strings.Repeat("very-long-", 20) + "name:Q8_0",
36+ strings.Repeat("x", 500),
37+ }
38+ for _, id := range ids {
39+ got := shortModel(id)
40+ if len([]rune(got)) > modelWidth+1 { // +1: the ":" before a kept tag
41+ t.Errorf("shortModel(%.40q…) = %q, %d runes — the header is all model", id, got, len([]rune(got)))
42+ }
43+ }
44+}
45+
46+// The tag survives the cut: it is what tells two otherwise identical models
47+// apart, and it is the part a user checks.
48+func TestShortModelKeepsTheTag(t *testing.T) {
49+ got := shortModel("huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M")
50+ if !strings.HasSuffix(got, ":Q4_K_M") {
51+ t.Errorf("shortModel = %q, want it to end with the tag", got)
52+ }
53+ if !strings.HasPrefix(got, "mellum2-12b") {
54+ t.Errorf("shortModel = %q, want it to start with the model's own name", got)
55+ }
56+}
57+
58+// The header uses it, and the rest of the header still fits beside it.
59+func TestHeaderUsesTheShortName(t *testing.T) {
60+ r, _ := newTestRunner(t, &fakeModel{answer: "hi"})
61+ r.opts.ModelName = "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M"
62+
63+ got := r.header()
64+ if strings.Contains(got, "huggingface.co") {
65+ t.Errorf("the header still carries the registry host: %q", got)
66+ }
67+ if !strings.Contains(got, "qwen3.5-2b:Q4_K_M") {
68+ t.Errorf("the header lost the model: %q", got)
69+ }
70+ for _, want := range []string{"mm ·", "@ p", "ctx"} {
71+ if !strings.Contains(got, want) {
72+ t.Errorf("the header lost %q: %q", want, got)
73+ }
74+ }
75+}
modified internal/tui/turn.go +1 -1
@@ -334,7 +334,7 @@ func (r *runner) record(f store.Failure) {
334334 // session is being saved.
335335 func (r *runner) header() string {
336336 var b strings.Builder
337- fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, r.opts.ModelName, r.opts.Provider)
337+ fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, shortModel(r.opts.ModelName), r.opts.Provider)
338338 used := max(r.opts.Model.LastInputTokens(), compact.Estimate(r.messages))
339339 if w := r.opts.Window(); w > 0 {
340340 fmt.Fprintf(&b, " · ctx %d%% (%s/%s)", used*100/w, compact.Kilo(used), compact.Kilo(w))
@@ -334,7 +334,7 @@ func (r *runner) record(f store.Failure) {
334 // session is being saved.334 // session is being saved.
335 func (r *runner) header() string {335 func (r *runner) header() string {
336 var b strings.Builder336 var b strings.Builder
337- fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, r.opts.ModelName, r.opts.Provider)337+ fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, shortModel(r.opts.ModelName), r.opts.Provider)
338 used := max(r.opts.Model.LastInputTokens(), compact.Estimate(r.messages))338 used := max(r.opts.Model.LastInputTokens(), compact.Estimate(r.messages))
339 if w := r.opts.Window(); w > 0 {339 if w := r.opts.Window(); w > 0 {
340 fmt.Fprintf(&b, " · ctx %d%% (%s/%s)", used*100/w, compact.Kilo(used), compact.Kilo(w))340 fmt.Fprintf(&b, " · ctx %d%% (%s/%s)", used*100/w, compact.Kilo(used), compact.Kilo(w))
added internal/version/about.go +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+package version
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+// About is what `mm --about` prints: what this program is, which one it is,
9+// and where to go next. It lives beside Short and Long because it is the same
10+// question asked with more room — "what am I running?" — and because a binary
11+// that can say what it is needs no network and no file to do it.
12+//
13+// Deliberately short. An --about that scrolls is a README in the wrong place,
14+// and the README is one link away.
15+
16+// Home is where the project lives. A constant, not a guess from the module
17+// path: the module is named `mm`, which says nothing about where it came from.
18+const Home = "https://git.rickub.com/bots-garden/mini-me"
19+
20+// About returns the block, ending without a trailing newline.
21+func About() string {
22+ var b strings.Builder
23+ fmt.Fprintf(&b, "Mini Me (mm) %s\n", Short())
24+ b.WriteString("A minimalist coding agent for local LLMs, written in Go.\n\n")
25+ b.WriteString("One agent loop, three front ends:\n")
26+ b.WriteString(" mm [config.yaml] a terminal REPL, line by line\n")
27+ b.WriteString(" mm -tui [config.yaml] one terminal window: conversation, input box, status bar\n")
28+ b.WriteString(" mm -acp the Agent Client Protocol on stdio, for Zed and other editors\n\n")
29+ b.WriteString("Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp.\n")
30+ b.WriteString("Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API.\n\n")
31+ fmt.Fprintf(&b, "%s\n", Home)
32+ b.WriteString("Docs: docs/en (English), docs/fr (français) · mm -h for the flags")
33+ return b.String()
34+}
new file mode 100644
@@ -0,0 +1,34 @@
1+package version
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+// About is what `mm --about` prints: what this program is, which one it is,
9+// and where to go next. It lives beside Short and Long because it is the same
10+// question asked with more room — "what am I running?" — and because a binary
11+// that can say what it is needs no network and no file to do it.
12+//
13+// Deliberately short. An --about that scrolls is a README in the wrong place,
14+// and the README is one link away.
15+
16+// Home is where the project lives. A constant, not a guess from the module
17+// path: the module is named `mm`, which says nothing about where it came from.
18+const Home = "https://git.rickub.com/bots-garden/mini-me"
19+
20+// About returns the block, ending without a trailing newline.
21+func About() string {
22+ var b strings.Builder
23+ fmt.Fprintf(&b, "Mini Me (mm) %s\n", Short())
24+ b.WriteString("A minimalist coding agent for local LLMs, written in Go.\n\n")
25+ b.WriteString("One agent loop, three front ends:\n")
26+ b.WriteString(" mm [config.yaml] a terminal REPL, line by line\n")
27+ b.WriteString(" mm -tui [config.yaml] one terminal window: conversation, input box, status bar\n")
28+ b.WriteString(" mm -acp the Agent Client Protocol on stdio, for Zed and other editors\n\n")
29+ b.WriteString("Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp.\n")
30+ b.WriteString("Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API.\n\n")
31+ fmt.Fprintf(&b, "%s\n", Home)
32+ b.WriteString("Docs: docs/en (English), docs/fr (français) · mm -h for the flags")
33+ return b.String()
34+}
added internal/version/about_test.go +40 -0
new file mode 100644
@@ -0,0 +1,40 @@
1+package version
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+// --about answers "what am I running?", so the two things that change between
9+// two binaries — the name and the version — must both be in it, and it must
10+// stay short enough to read without scrolling.
11+func TestAbout(t *testing.T) {
12+ saved := stamped
13+ t.Cleanup(func() { stamped = saved })
14+ stamped = "v0.0.3"
15+
16+ got := About()
17+ for _, want := range []string{"Mini Me", "mm", "v0.0.3", Home, "-tui", "-acp", "bash"} {
18+ if !strings.Contains(got, want) {
19+ t.Errorf("About() does not mention %q:\n%s", want, got)
20+ }
21+ }
22+ if n := strings.Count(got, "\n") + 1; n > 20 {
23+ t.Errorf("About() is %d lines: that is a README, not an --about", n)
24+ }
25+ if strings.HasSuffix(got, "\n") {
26+ t.Error("About() ends with a newline; the caller's Println adds one")
27+ }
28+}
29+
30+// It works on a binary with no stamp — the case for anyone building from
31+// source, which is most people reading it.
32+func TestAboutWithoutAStamp(t *testing.T) {
33+ saved := stamped
34+ t.Cleanup(func() { stamped = saved })
35+ stamped = ""
36+
37+ if got := About(); !strings.Contains(got, Short()) {
38+ t.Errorf("About() does not carry the version it reports:\n%s", got)
39+ }
40+}
new file mode 100644
@@ -0,0 +1,40 @@
1+package version
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+// --about answers "what am I running?", so the two things that change between
9+// two binaries — the name and the version — must both be in it, and it must
10+// stay short enough to read without scrolling.
11+func TestAbout(t *testing.T) {
12+ saved := stamped
13+ t.Cleanup(func() { stamped = saved })
14+ stamped = "v0.0.3"
15+
16+ got := About()
17+ for _, want := range []string{"Mini Me", "mm", "v0.0.3", Home, "-tui", "-acp", "bash"} {
18+ if !strings.Contains(got, want) {
19+ t.Errorf("About() does not mention %q:\n%s", want, got)
20+ }
21+ }
22+ if n := strings.Count(got, "\n") + 1; n > 20 {
23+ t.Errorf("About() is %d lines: that is a README, not an --about", n)
24+ }
25+ if strings.HasSuffix(got, "\n") {
26+ t.Error("About() ends with a newline; the caller's Println adds one")
27+ }
28+}
29+
30+// It works on a binary with no stamp — the case for anyone building from
31+// source, which is most people reading it.
32+func TestAboutWithoutAStamp(t *testing.T) {
33+ saved := stamped
34+ t.Cleanup(func() { stamped = saved })
35+ stamped = ""
36+
37+ if got := About(); !strings.Contains(got, Short()) {
38+ t.Errorf("About() does not carry the version it reports:\n%s", got)
39+ }
40+}
modified main.go +7 -0
@@ -67,6 +67,7 @@ func main() {
6767 acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL")
6868 tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal")
6969 showVersion := flag.Bool("version", false, "print the version and exit")
70+ showAbout := flag.Bool("about", false, "print what mm is, and exit")
7071 flag.Parse()
7172 // Answered before anything can go wrong: asking which mm this is must
7273 // work without a config file, without a model server, and in a directory
@@ -76,6 +77,12 @@ func main() {
7677 fmt.Println(version.Long())
7778 return
7879 }
80+ // Same rule as --version: answered before the configuration is looked
81+ // for, so it works in any directory with no model server running.
82+ if *showAbout {
83+ fmt.Println(version.About())
84+ return
85+ }
7986 if *configPath == "" && flag.NArg() > 0 {
8087 *configPath = flag.Arg(0)
8188 }
@@ -67,6 +67,7 @@ func main() {
67 acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL")67 acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL")
68 tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal")68 tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal")
69 showVersion := flag.Bool("version", false, "print the version and exit")69 showVersion := flag.Bool("version", false, "print the version and exit")
70+ showAbout := flag.Bool("about", false, "print what mm is, and exit")
70 flag.Parse()71 flag.Parse()
71 // Answered before anything can go wrong: asking which mm this is must72 // Answered before anything can go wrong: asking which mm this is must
72 // work without a config file, without a model server, and in a directory73 // work without a config file, without a model server, and in a directory
@@ -76,6 +77,12 @@ func main() {
76 fmt.Println(version.Long())77 fmt.Println(version.Long())
77 return78 return
78 }79 }
80+ // Same rule as --version: answered before the configuration is looked
81+ // for, so it works in any directory with no model server running.
82+ if *showAbout {
83+ fmt.Println(version.About())
84+ return
85+ }
79 if *configPath == "" && flag.NArg() > 0 {86 if *configPath == "" && flag.NArg() > 0 {
80 *configPath = flag.Arg(0)87 *configPath = flag.Arg(0)
81 }88 }