bots-garden/mini-mepublic Fork
⑂ d973158
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-10-25T23:46:58+01:00 Browse files
d973158 parent: a7ddac6
modified .tickets/issues/0158-calculate-the-duration-of-a-completion.yaml +71 -4
@@ -6,9 +6,76 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-25T18:15:39.756Z
9-updatedAt: 2026-10-25T18:15:39.756Z
10-labels: []
9+updatedAt: 2026-10-25T22:40:00.000Z
10+labels:
11+ - enhancement
12+ - To be tested
1113 milestone: v1.0.1
12-body: ""
13-tasks: []
14+body: |+
15+ ## Need
16+
17+ See how long a completion takes: in the window's footer, the duration should tick while the model works.
18+
19+ ## What was done
20+
21+ - New `internal/tui/stopwatch.go`: `stopwatch` (start / stop / label) and `formatDuration` — tenths under a minute (`4.2s`), minutes and seconds beyond (`2m05s`).
22+ - The runner starts it when a turn or an `/ask` starts (`startTurn`, `startAsk`) and stops it in `settle`; `/compact` is not timed.
23+ - The status bar's counters lead with it: `<version> · ⏱ 4.2s · N msgs · N tools`. Nothing before the first question; while a job runs it ticks with the window's 100 ms animation; once the turn has ended (answered, failed or cancelled) it keeps the final duration until the next question.
24+ - Measured from the question sent to the turn settled: tools, hooks, a decider's routing and an automatic compaction run inside the turn are included.
25+ - Config key `showCompletionTime` (bool, default `true`): `false` hides the duration and keeps the status bar to its counters (asked after the user felt the ticking slowed the display; the window already redraws every 100 ms during a turn for the spinner, so the duration only changes a few cells of the bar).
26+
27+ ## Verified
28+
29+ - Tests: `TestStopwatchTicksThenKeepsTheLastDuration`, `TestFormatDurationUsesMinutesFromOneMinute`, `TestATurnIsTimedInTheStatusBar`. `go vet ./...` and `go test ./...` green.
30+ - Live: window through a pty with `demos/struggle-signals` (`FAKE=silent`), bottom row read every second: `⏱ 1.0s`, `⏱ 2.0s`, then `✖ stalled ⏱ 3.0s` frozen when the watchdog ended the turn.
31+
32+ ## Not done
33+
34+ - Line mode: its spinner already shows the elapsed seconds; no final duration is printed.
35+ - Editor (ACP): no duration sent.
36+
37+ ## Docs
38+
39+ EN + FR: `reference/cli.md` (window layout row, new "Completion duration" row), `how-to/use-the-window-mode.md` (step 3), `reference/configuration.md` (`showCompletionTime`).
40+tasks:
41+ - id: 1
42+ title: "tui: stopwatch + formatDuration; started by a turn and an /ask, stopped in settle"
43+ state: closed
44+ priority: none
45+ author:
46+ name: k33g
47+ email: ph.charriere@gmail.com
48+ createdAt: 2026-10-25T22:20:00.000Z
49+ - id: 2
50+ title: "Status bar: ⏱ <duration> before the counters"
51+ state: closed
52+ priority: none
53+ author:
54+ name: k33g
55+ email: ph.charriere@gmail.com
56+ createdAt: 2026-10-25T22:20:00.000Z
57+ - id: 3
58+ title: "Tests + live check in the window (pty)"
59+ state: closed
60+ priority: none
61+ author:
62+ name: k33g
63+ email: ph.charriere@gmail.com
64+ createdAt: 2026-10-25T22:20:00.000Z
65+ - id: 4
66+ title: "Docs EN + FR"
67+ state: closed
68+ priority: none
69+ author:
70+ name: k33g
71+ email: ph.charriere@gmail.com
72+ createdAt: 2026-10-25T22:20:00.000Z
73+ - id: 5
74+ title: "Config key showCompletionTime (default true) + tests + docs"
75+ state: closed
76+ priority: none
77+ author:
78+ name: k33g
79+ email: ph.charriere@gmail.com
80+ createdAt: 2026-10-25T22:40:00.000Z
1481 comments: []
@@ -6,9 +6,76 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-25T18:15:39.756Z8 createdAt: 2026-10-25T18:15:39.756Z
9-updatedAt: 2026-10-25T18:15:39.756Z9+updatedAt: 2026-10-25T22:40:00.000Z
10-labels: []10+labels:
11+ - enhancement
12+ - To be tested
11 milestone: v1.0.113 milestone: v1.0.1
12-body: ""14+body: |+
13-tasks: []15+ ## Need
16+
17+ See how long a completion takes: in the window's footer, the duration should tick while the model works.
18+
19+ ## What was done
20+
21+ - New `internal/tui/stopwatch.go`: `stopwatch` (start / stop / label) and `formatDuration` — tenths under a minute (`4.2s`), minutes and seconds beyond (`2m05s`).
22+ - The runner starts it when a turn or an `/ask` starts (`startTurn`, `startAsk`) and stops it in `settle`; `/compact` is not timed.
23+ - The status bar's counters lead with it: `<version> · ⏱ 4.2s · N msgs · N tools`. Nothing before the first question; while a job runs it ticks with the window's 100 ms animation; once the turn has ended (answered, failed or cancelled) it keeps the final duration until the next question.
24+ - Measured from the question sent to the turn settled: tools, hooks, a decider's routing and an automatic compaction run inside the turn are included.
25+ - Config key `showCompletionTime` (bool, default `true`): `false` hides the duration and keeps the status bar to its counters (asked after the user felt the ticking slowed the display; the window already redraws every 100 ms during a turn for the spinner, so the duration only changes a few cells of the bar).
26+
27+ ## Verified
28+
29+ - Tests: `TestStopwatchTicksThenKeepsTheLastDuration`, `TestFormatDurationUsesMinutesFromOneMinute`, `TestATurnIsTimedInTheStatusBar`. `go vet ./...` and `go test ./...` green.
30+ - Live: window through a pty with `demos/struggle-signals` (`FAKE=silent`), bottom row read every second: `⏱ 1.0s`, `⏱ 2.0s`, then `✖ stalled ⏱ 3.0s` frozen when the watchdog ended the turn.
31+
32+ ## Not done
33+
34+ - Line mode: its spinner already shows the elapsed seconds; no final duration is printed.
35+ - Editor (ACP): no duration sent.
36+
37+ ## Docs
38+
39+ EN + FR: `reference/cli.md` (window layout row, new "Completion duration" row), `how-to/use-the-window-mode.md` (step 3), `reference/configuration.md` (`showCompletionTime`).
40+tasks:
41+ - id: 1
42+ title: "tui: stopwatch + formatDuration; started by a turn and an /ask, stopped in settle"
43+ state: closed
44+ priority: none
45+ author:
46+ name: k33g
47+ email: ph.charriere@gmail.com
48+ createdAt: 2026-10-25T22:20:00.000Z
49+ - id: 2
50+ title: "Status bar: ⏱ <duration> before the counters"
51+ state: closed
52+ priority: none
53+ author:
54+ name: k33g
55+ email: ph.charriere@gmail.com
56+ createdAt: 2026-10-25T22:20:00.000Z
57+ - id: 3
58+ title: "Tests + live check in the window (pty)"
59+ state: closed
60+ priority: none
61+ author:
62+ name: k33g
63+ email: ph.charriere@gmail.com
64+ createdAt: 2026-10-25T22:20:00.000Z
65+ - id: 4
66+ title: "Docs EN + FR"
67+ state: closed
68+ priority: none
69+ author:
70+ name: k33g
71+ email: ph.charriere@gmail.com
72+ createdAt: 2026-10-25T22:20:00.000Z
73+ - id: 5
74+ title: "Config key showCompletionTime (default true) + tests + docs"
75+ state: closed
76+ priority: none
77+ author:
78+ name: k33g
79+ email: ph.charriere@gmail.com
80+ createdAt: 2026-10-25T22:40:00.000Z
14 comments: []81 comments: []
added .tickets/issues/0161-unload-model-should-be-an-option-of-the-usage-of.yaml +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 161
3+title: Unload model should be an option of the usage of multi agents
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-25T22:29:12.597Z
9+updatedAt: 2026-10-25T22:29:34.372Z
10+labels: []
11+milestone: v1.0.1
12+body: |
13+ `mmConfigs`
14+tasks: []
15+comments: []
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 161
3+title: Unload model should be an option of the usage of multi agents
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-25T22:29:12.597Z
9+updatedAt: 2026-10-25T22:29:34.372Z
10+labels: []
11+milestone: v1.0.1
12+body: |
13+ `mmConfigs`
14+tasks: []
15+comments: []
modified demos/coding-helpers/README.en.md +23 -3
@@ -8,9 +8,9 @@ Ask what a Go or Rust concept is, or how something is done, and get an explanati
88 |---|---|---|---|
99 | 🧭 coordinator | `agent.yaml` | MiniCPM5 2B (`hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M`, 1.6 GB) | picks `go` or `rust`, sends the question with the conversation |
1010 | 🐹 go | `agent.go.yaml` | Mellum2 12B A2.5B thinking (`huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M`, 8.1 GB) | documents Go |
11-| 🦀 rust | `agent.rust.yaml` | the same Mellum2 model, loaded once | documents Rust |
11+| 🦀 rust | `agent.rust.yaml` | the same Mellum2 model | documents Rust |
1212
13-The specialists **document, they do not build**: they explain and show snippets, and only write a complete program when you ask for one. They have no tools, so they never touch your files. No skills, no hooks.
13+The specialists **document, they do not build**: they explain and show snippets, and only write a complete program when you ask for one. They have no tools, so they never touch your files. No skills. Two hooks make the models take turns in memory: the coordinator's model and Mellum2 are **never loaded together**, so the demo needs room for 8.1 GB, not 10 — see [Memory](#memory-the-models-take-turns).
1414
1515 ## Run
1616
@@ -23,7 +23,7 @@ cd demos/coding-helpers
2323 mm # reads ./agent.yaml
2424 ```
2525
26-`LLMMAN_CONTEXT_LENGTH` matters: without it, llmman loads each model with its full context window (131 072 tokens for both models here), and two models plus their caches may not fit in memory.
26+`LLMMAN_CONTEXT_LENGTH` matters: without it, llmman loads each model with its full context window (131 072 tokens for both models here), and Mellum2 with its cache may not fit in memory.
2727
2828 ## Use
2929
@@ -62,6 +62,26 @@ On 2026-10-15, llmman on the host, one conversation of five messages in the REPL
6262
6363 An earlier version (asked to write runnable programs) produced a Go struct-to-JSON program that compiled and printed exactly what the answer said; its Rust counterpart (Serde) was not compiled — no Rust toolchain in the test sandbox. 40 to 120 s per question.
6464
65+## Memory: the models take turns
66+
67+`agent.yaml` runs `hooks/unload.js` (the same script as [`demos/unload-models`](../unload-models/)) on every hand-off to `go` or `rust`:
68+
69+| Hook | When | Unloads |
70+|---|---|---|
71+| `preAsk`, `unload.js self` | right before the specialist starts | the coordinator's model (MiniCPM5) |
72+| `postAsk`, `unload.js asked` | once the specialist has answered, before the coordinator reads the answer | the specialist's model (Mellum2) |
73+
74+`displayHooks: true` lists both runs under each answer:
75+
76+```
77+ 🪝 1. preAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue
78+ 🪝 2. postAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue
79+```
80+
81+Measured on 2026-10-25 (llmman on the host, `mm -config agent.yaml -p "explique-moi ce qu'est le pattern matching en Rust, avec un exemple"`, `/api/ps` sampled twice a second): MiniCPM5 → nothing → Mellum2 → nothing → MiniCPM5, never both; 37 s for the question.
82+
83+The price: Mellum2 is reloaded for every question, even two in a row to the same specialist. To keep it loaded between questions, remove the `postAsk` hook — the two models are then together while the coordinator writes its line. `onError: warn`: if an unload fails, the hand-off goes on.
84+
6585 ## Known limits
6686
6787 - The coordinator is a 2B model: it routes Go and Rust questions well, but does not reliably refuse other languages.
@@ -8,9 +8,9 @@ Ask what a Go or Rust concept is, or how something is done, and get an explanati
8 |---|---|---|---|8 |---|---|---|---|
9 | 🧭 coordinator | `agent.yaml` | MiniCPM5 2B (`hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M`, 1.6 GB) | picks `go` or `rust`, sends the question with the conversation |9 | 🧭 coordinator | `agent.yaml` | MiniCPM5 2B (`hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M`, 1.6 GB) | picks `go` or `rust`, sends the question with the conversation |
10 | 🐹 go | `agent.go.yaml` | Mellum2 12B A2.5B thinking (`huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M`, 8.1 GB) | documents Go |10 | 🐹 go | `agent.go.yaml` | Mellum2 12B A2.5B thinking (`huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M`, 8.1 GB) | documents Go |
11-| 🦀 rust | `agent.rust.yaml` | the same Mellum2 model, loaded once | documents Rust |11+| 🦀 rust | `agent.rust.yaml` | the same Mellum2 model | documents Rust |
12 12
13-The specialists **document, they do not build**: they explain and show snippets, and only write a complete program when you ask for one. They have no tools, so they never touch your files. No skills, no hooks.13+The specialists **document, they do not build**: they explain and show snippets, and only write a complete program when you ask for one. They have no tools, so they never touch your files. No skills. Two hooks make the models take turns in memory: the coordinator's model and Mellum2 are **never loaded together**, so the demo needs room for 8.1 GB, not 10 — see [Memory](#memory-the-models-take-turns).
14 14
15 ## Run15 ## Run
16 16
@@ -23,7 +23,7 @@ cd demos/coding-helpers
23 mm # reads ./agent.yaml23 mm # reads ./agent.yaml
24 ```24 ```
25 25
26-`LLMMAN_CONTEXT_LENGTH` matters: without it, llmman loads each model with its full context window (131 072 tokens for both models here), and two models plus their caches may not fit in memory.26+`LLMMAN_CONTEXT_LENGTH` matters: without it, llmman loads each model with its full context window (131 072 tokens for both models here), and Mellum2 with its cache may not fit in memory.
27 27
28 ## Use28 ## Use
29 29
@@ -62,6 +62,26 @@ On 2026-10-15, llmman on the host, one conversation of five messages in the REPL
62 62
63 An earlier version (asked to write runnable programs) produced a Go struct-to-JSON program that compiled and printed exactly what the answer said; its Rust counterpart (Serde) was not compiled — no Rust toolchain in the test sandbox. 40 to 120 s per question.63 An earlier version (asked to write runnable programs) produced a Go struct-to-JSON program that compiled and printed exactly what the answer said; its Rust counterpart (Serde) was not compiled — no Rust toolchain in the test sandbox. 40 to 120 s per question.
64 64
65+## Memory: the models take turns
66+
67+`agent.yaml` runs `hooks/unload.js` (the same script as [`demos/unload-models`](../unload-models/)) on every hand-off to `go` or `rust`:
68+
69+| Hook | When | Unloads |
70+|---|---|---|
71+| `preAsk`, `unload.js self` | right before the specialist starts | the coordinator's model (MiniCPM5) |
72+| `postAsk`, `unload.js asked` | once the specialist has answered, before the coordinator reads the answer | the specialist's model (Mellum2) |
73+
74+`displayHooks: true` lists both runs under each answer:
75+
76+```
77+ 🪝 1. preAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue
78+ 🪝 2. postAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue
79+```
80+
81+Measured on 2026-10-25 (llmman on the host, `mm -config agent.yaml -p "explique-moi ce qu'est le pattern matching en Rust, avec un exemple"`, `/api/ps` sampled twice a second): MiniCPM5 → nothing → Mellum2 → nothing → MiniCPM5, never both; 37 s for the question.
82+
83+The price: Mellum2 is reloaded for every question, even two in a row to the same specialist. To keep it loaded between questions, remove the `postAsk` hook — the two models are then together while the coordinator writes its line. `onError: warn`: if an unload fails, the hand-off goes on.
84+
65 ## Known limits85 ## Known limits
66 86
67 - The coordinator is a 2B model: it routes Go and Rust questions well, but does not reliably refuse other languages.87 - The coordinator is a 2B model: it routes Go and Rust questions well, but does not reliably refuse other languages.
modified demos/coding-helpers/README.fr.md +23 -3
@@ -8,9 +8,9 @@ Demandez ce qu'est un concept de Go ou de Rust, ou comment on fait quelque chose
88 |---|---|---|---|
99 | 🧭 coordinateur | `agent.yaml` | MiniCPM5 2B (`hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M`, 1,6 Go) | choisit `go` ou `rust`, envoie la question avec la conversation |
1010 | 🐹 go | `agent.go.yaml` | Mellum2 12B A2.5B thinking (`huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M`, 8,1 Go) | documente Go |
11-| 🦀 rust | `agent.rust.yaml` | le même modèle Mellum2, chargé une seule fois | documente Rust |
11+| 🦀 rust | `agent.rust.yaml` | le même modèle Mellum2 | documente Rust |
1212
13-Les spécialistes **documentent, ils ne construisent pas** : ils expliquent et montrent des extraits, et n'écrivent un programme complet que si vous le demandez. Ils n'ont aucun outil, ils ne touchent donc jamais à vos fichiers. Ni skills ni hooks.
13+Les spécialistes **documentent, ils ne construisent pas** : ils expliquent et montrent des extraits, et n'écrivent un programme complet que si vous le demandez. Ils n'ont aucun outil, ils ne touchent donc jamais à vos fichiers. Pas de skills. Deux hooks font que les modèles se relaient en mémoire : le modèle du coordinateur et Mellum2 ne sont **jamais chargés ensemble**, la démo a donc besoin de place pour 8,1 Go, pas 10 — voir [Mémoire](#mémoire--les-modèles-se-relaient).
1414
1515 ## Lancer
1616
@@ -23,7 +23,7 @@ cd demos/coding-helpers
2323 mm # lit ./agent.yaml
2424 ```
2525
26-`LLMMAN_CONTEXT_LENGTH` compte : sans lui, llmman charge chaque modèle avec sa fenêtre de contexte complète (131 072 tokens pour les deux modèles ici), et deux modèles avec leurs caches peuvent ne pas tenir en mémoire.
26+`LLMMAN_CONTEXT_LENGTH` compte : sans lui, llmman charge chaque modèle avec sa fenêtre de contexte complète (131 072 tokens pour les deux modèles ici), et Mellum2 avec son cache peut ne pas tenir en mémoire.
2727
2828 ## Utiliser
2929
@@ -62,6 +62,26 @@ Le 2026-10-15, llmman sur l'hôte, une conversation de cinq messages dans le REP
6262
6363 Une version précédente (à qui l'on demandait des programmes exécutables) a produit un programme Go struct → JSON qui compilait et affichait exactement ce que la réponse annonçait ; son équivalent Rust (Serde) n'a pas été compilé — pas de chaîne Rust dans le sandbox de test. De 40 à 120 s par question.
6464
65+## Mémoire : les modèles se relaient
66+
67+`agent.yaml` lance `hooks/unload.js` (le même script que [`demos/unload-models`](../unload-models/)) à chaque passage de main à `go` ou `rust` :
68+
69+| Hook | Quand | Décharge |
70+|---|---|---|
71+| `preAsk`, `unload.js self` | juste avant que le spécialiste démarre | le modèle du coordinateur (MiniCPM5) |
72+| `postAsk`, `unload.js asked` | une fois que le spécialiste a répondu, avant que le coordinateur lise la réponse | le modèle du spécialiste (Mellum2) |
73+
74+`displayHooks: true` affiche les deux exécutions sous chaque réponse :
75+
76+```
77+ 🪝 1. preAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue
78+ 🪝 2. postAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue
79+```
80+
81+Mesuré le 2026-10-25 (llmman sur l'hôte, `mm -config agent.yaml -p "explique-moi ce qu'est le pattern matching en Rust, avec un exemple"`, `/api/ps` interrogé deux fois par seconde) : MiniCPM5 → rien → Mellum2 → rien → MiniCPM5, jamais les deux ; 37 s pour la question.
82+
83+Le prix : Mellum2 est rechargé à chaque question, même pour deux questions de suite au même spécialiste. Pour le garder chargé entre deux questions, retirez le hook `postAsk` — les deux modèles sont alors ensemble pendant que le coordinateur écrit sa ligne. `onError: warn` : si un déchargement échoue, le passage de main continue.
84+
6585 ## Limites connues
6686
6787 - Le coordinateur est un modèle de 2B : il aiguille bien les questions Go et Rust, mais ne refuse pas de façon fiable les autres langages.
@@ -8,9 +8,9 @@ Demandez ce qu'est un concept de Go ou de Rust, ou comment on fait quelque chose
8 |---|---|---|---|8 |---|---|---|---|
9 | 🧭 coordinateur | `agent.yaml` | MiniCPM5 2B (`hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M`, 1,6 Go) | choisit `go` ou `rust`, envoie la question avec la conversation |9 | 🧭 coordinateur | `agent.yaml` | MiniCPM5 2B (`hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M`, 1,6 Go) | choisit `go` ou `rust`, envoie la question avec la conversation |
10 | 🐹 go | `agent.go.yaml` | Mellum2 12B A2.5B thinking (`huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M`, 8,1 Go) | documente Go |10 | 🐹 go | `agent.go.yaml` | Mellum2 12B A2.5B thinking (`huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M`, 8,1 Go) | documente Go |
11-| 🦀 rust | `agent.rust.yaml` | le même modèle Mellum2, chargé une seule fois | documente Rust |11+| 🦀 rust | `agent.rust.yaml` | le même modèle Mellum2 | documente Rust |
12 12
13-Les spécialistes **documentent, ils ne construisent pas** : ils expliquent et montrent des extraits, et n'écrivent un programme complet que si vous le demandez. Ils n'ont aucun outil, ils ne touchent donc jamais à vos fichiers. Ni skills ni hooks.13+Les spécialistes **documentent, ils ne construisent pas** : ils expliquent et montrent des extraits, et n'écrivent un programme complet que si vous le demandez. Ils n'ont aucun outil, ils ne touchent donc jamais à vos fichiers. Pas de skills. Deux hooks font que les modèles se relaient en mémoire : le modèle du coordinateur et Mellum2 ne sont **jamais chargés ensemble**, la démo a donc besoin de place pour 8,1 Go, pas 10 — voir [Mémoire](#mémoire--les-modèles-se-relaient).
14 14
15 ## Lancer15 ## Lancer
16 16
@@ -23,7 +23,7 @@ cd demos/coding-helpers
23 mm # lit ./agent.yaml23 mm # lit ./agent.yaml
24 ```24 ```
25 25
26-`LLMMAN_CONTEXT_LENGTH` compte : sans lui, llmman charge chaque modèle avec sa fenêtre de contexte complète (131 072 tokens pour les deux modèles ici), et deux modèles avec leurs caches peuvent ne pas tenir en mémoire.26+`LLMMAN_CONTEXT_LENGTH` compte : sans lui, llmman charge chaque modèle avec sa fenêtre de contexte complète (131 072 tokens pour les deux modèles ici), et Mellum2 avec son cache peut ne pas tenir en mémoire.
27 27
28 ## Utiliser28 ## Utiliser
29 29
@@ -62,6 +62,26 @@ Le 2026-10-15, llmman sur l'hôte, une conversation de cinq messages dans le REP
62 62
63 Une version précédente (à qui l'on demandait des programmes exécutables) a produit un programme Go struct → JSON qui compilait et affichait exactement ce que la réponse annonçait ; son équivalent Rust (Serde) n'a pas été compilé — pas de chaîne Rust dans le sandbox de test. De 40 à 120 s par question.63 Une version précédente (à qui l'on demandait des programmes exécutables) a produit un programme Go struct → JSON qui compilait et affichait exactement ce que la réponse annonçait ; son équivalent Rust (Serde) n'a pas été compilé — pas de chaîne Rust dans le sandbox de test. De 40 à 120 s par question.
64 64
65+## Mémoire : les modèles se relaient
66+
67+`agent.yaml` lance `hooks/unload.js` (le même script que [`demos/unload-models`](../unload-models/)) à chaque passage de main à `go` ou `rust` :
68+
69+| Hook | Quand | Décharge |
70+|---|---|---|
71+| `preAsk`, `unload.js self` | juste avant que le spécialiste démarre | le modèle du coordinateur (MiniCPM5) |
72+| `postAsk`, `unload.js asked` | une fois que le spécialiste a répondu, avant que le coordinateur lise la réponse | le modèle du spécialiste (Mellum2) |
73+
74+`displayHooks: true` affiche les deux exécutions sous chaque réponse :
75+
76+```
77+ 🪝 1. preAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue
78+ 🪝 2. postAsk rust · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue
79+```
80+
81+Mesuré le 2026-10-25 (llmman sur l'hôte, `mm -config agent.yaml -p "explique-moi ce qu'est le pattern matching en Rust, avec un exemple"`, `/api/ps` interrogé deux fois par seconde) : MiniCPM5 → rien → Mellum2 → rien → MiniCPM5, jamais les deux ; 37 s pour la question.
82+
83+Le prix : Mellum2 est rechargé à chaque question, même pour deux questions de suite au même spécialiste. Pour le garder chargé entre deux questions, retirez le hook `postAsk` — les deux modèles sont alors ensemble pendant que le coordinateur écrit sa ligne. `onError: warn` : si un déchargement échoue, le passage de main continue.
84+
65 ## Limites connues85 ## Limites connues
66 86
67 - Le coordinateur est un modèle de 2B : il aiguille bien les questions Go et Rust, mais ne refuse pas de façon fiable les autres langages.87 - Le coordinateur est un modèle de 2B : il aiguille bien les questions Go et Rust, mais ne refuse pas de façon fiable les autres langages.
modified demos/coding-helpers/agent.yaml +25 -2
@@ -14,10 +14,16 @@
1414 # sends the conversation along (`with_context`), so the Rust specialist
1515 # knows what "the same thing" is.
1616 #
17-# Models (≈ 10 GB with both loaded):
17+# Models — never in memory together, so at most 8.1 GB at a time:
1818 #
1919 # coordinator MiniCPM5 2B hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M 1.6 GB
20-# go, rust Mellum2 12B thinking huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M 8.1 GB (one model, loaded once)
20+# go, rust Mellum2 12B thinking huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M 8.1 GB (one model for both)
21+#
22+# hooks/unload.js self preAsk unloads the COORDINATOR's model right
23+# before the specialist starts
24+# hooks/unload.js asked postAsk unloads the SPECIALIST's model once it
25+# has answered, before the coordinator is
26+# reloaded to read the answer
2127 #
2228 # llmman pull hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M
2329 # llmman pull huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M
@@ -58,6 +64,22 @@ sampling:
5864 chat_template_kwargs:
5965 enable_thinking: false
6066
67+# The models take turns in memory: the coordinator is unloaded right before
68+# a specialist starts, the specialist once it has answered (the same script
69+# as demos/unload-models). The price: Mellum2 is reloaded on every question.
70+# onError: warn — an unload is a saving, not a check: the hand-off goes on.
71+hooks:
72+ preAsk:
73+ - matcher: ^(go|rust)$
74+ command: node "$MM_CONFIG_DIR/hooks/unload.js" self
75+ timeout: 30s
76+ onError: warn
77+ postAsk:
78+ - matcher: ^(go|rust)$
79+ command: node "$MM_CONFIG_DIR/hooks/unload.js" asked
80+ timeout: 30s
81+ onError: warn
82+
6183 # During ask_agent every event of the specialist counts as a sign of life;
6284 # a 12B thinking model may reason a while before its first word.
6385 watchdogTimeout: 5m
@@ -67,6 +89,7 @@ firstTokenTimeout: 3m
6789 maxTurns: 3
6890 maxOutput: 20000
6991 displayCommands: true
92+displayHooks: true
7093
7194 yolo: true
7295 bashTool: false
@@ -14,10 +14,16 @@
14 # sends the conversation along (`with_context`), so the Rust specialist14 # sends the conversation along (`with_context`), so the Rust specialist
15 # knows what "the same thing" is.15 # knows what "the same thing" is.
16 #16 #
17-# Models (≈ 10 GB with both loaded):17+# Models — never in memory together, so at most 8.1 GB at a time:
18 #18 #
19 # coordinator MiniCPM5 2B hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M 1.6 GB19 # coordinator MiniCPM5 2B hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M 1.6 GB
20-# go, rust Mellum2 12B thinking huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M 8.1 GB (one model, loaded once)20+# go, rust Mellum2 12B thinking huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M 8.1 GB (one model for both)
21+#
22+# hooks/unload.js self preAsk unloads the COORDINATOR's model right
23+# before the specialist starts
24+# hooks/unload.js asked postAsk unloads the SPECIALIST's model once it
25+# has answered, before the coordinator is
26+# reloaded to read the answer
21 #27 #
22 # llmman pull hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M28 # llmman pull hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M
23 # llmman pull huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M29 # llmman pull huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M
@@ -58,6 +64,22 @@ sampling:
58 chat_template_kwargs:64 chat_template_kwargs:
59 enable_thinking: false65 enable_thinking: false
60 66
67+# The models take turns in memory: the coordinator is unloaded right before
68+# a specialist starts, the specialist once it has answered (the same script
69+# as demos/unload-models). The price: Mellum2 is reloaded on every question.
70+# onError: warn — an unload is a saving, not a check: the hand-off goes on.
71+hooks:
72+ preAsk:
73+ - matcher: ^(go|rust)$
74+ command: node "$MM_CONFIG_DIR/hooks/unload.js" self
75+ timeout: 30s
76+ onError: warn
77+ postAsk:
78+ - matcher: ^(go|rust)$
79+ command: node "$MM_CONFIG_DIR/hooks/unload.js" asked
80+ timeout: 30s
81+ onError: warn
82+
61 # During ask_agent every event of the specialist counts as a sign of life;83 # During ask_agent every event of the specialist counts as a sign of life;
62 # a 12B thinking model may reason a while before its first word.84 # a 12B thinking model may reason a while before its first word.
63 watchdogTimeout: 5m85 watchdogTimeout: 5m
@@ -67,6 +89,7 @@ firstTokenTimeout: 3m
67 maxTurns: 389 maxTurns: 3
68 maxOutput: 2000090 maxOutput: 20000
69 displayCommands: true91 displayCommands: true
92+displayHooks: true
70 93
71 yolo: true94 yolo: true
72 bashTool: false95 bashTool: false
added demos/coding-helpers/hooks/unload.js +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+#!/usr/bin/env node
2+// preAsk / postAsk hook: unloads one model so that two models are never in
3+// memory together during a hand-off.
4+//
5+// node unload.js self preAsk: the ASKING agent's own model, right before
6+// mm starts the asked agent. Only mm can do it at
7+// that moment: the model has just decided to call
8+// ask_agent, and it is needed again only to read the
9+// answer — the server reloads it then by itself. The
10+// model cannot do it (unload_model on its own model
11+// lasts until its next step).
12+// node unload.js asked postAsk: the ASKED agent's model, once it has
13+// answered and before the asking agent reads the
14+// answer — so the asker's model reloads alone.
15+//
16+// Input: {"model": "llmman/<model>", "ask": {"agentModel": "llmman/<model>"},
17+// ...}. Server: $LLMMAN_URL, else llmman's default address, else the one a
18+// Docker sandbox reaches the host by. Prints nothing: the hand-off goes on
19+// whatever happens (onError: warn in agent.yaml), an unload is a saving, not
20+// a check.
21+
22+const fs = require("fs");
23+
24+const input = JSON.parse(fs.readFileSync(0, "utf8"));
25+const which = process.argv[2];
26+const ref = which === "asked" ? input.ask && input.ask.agentModel : input.model;
27+if (!ref) {
28+ console.error(`unload.js ${which}: no model in the input`);
29+ process.exit(1);
30+}
31+const model = ref.replace(/^[^/]+\//, ""); // drop the "llmman/" provider prefix
32+
33+const servers = process.env.LLMMAN_URL
34+ ? [process.env.LLMMAN_URL]
35+ : ["http://127.0.0.1:17434", "http://host.docker.internal:17434"];
36+
37+async function unload() {
38+ for (const server of servers) {
39+ try {
40+ const resp = await fetch(server + "/api/generate", {
41+ method: "POST",
42+ headers: { "Content-Type": "application/json" },
43+ body: JSON.stringify({ model, keep_alive: 0 }),
44+ signal: AbortSignal.timeout(20_000),
45+ });
46+ const reply = await resp.json();
47+ if (reply.done_reason === "unload") return;
48+ throw new Error(`${server}: not confirmed: ${JSON.stringify(reply)}`);
49+ } catch (err) {
50+ if (server === servers[servers.length - 1]) throw err;
51+ }
52+ }
53+}
54+
55+unload().catch((err) => {
56+ console.error(`could not unload ${model}: ${err.message}`);
57+ process.exit(1);
58+});
new file mode 100644
@@ -0,0 +1,58 @@
1+#!/usr/bin/env node
2+// preAsk / postAsk hook: unloads one model so that two models are never in
3+// memory together during a hand-off.
4+//
5+// node unload.js self preAsk: the ASKING agent's own model, right before
6+// mm starts the asked agent. Only mm can do it at
7+// that moment: the model has just decided to call
8+// ask_agent, and it is needed again only to read the
9+// answer — the server reloads it then by itself. The
10+// model cannot do it (unload_model on its own model
11+// lasts until its next step).
12+// node unload.js asked postAsk: the ASKED agent's model, once it has
13+// answered and before the asking agent reads the
14+// answer — so the asker's model reloads alone.
15+//
16+// Input: {"model": "llmman/<model>", "ask": {"agentModel": "llmman/<model>"},
17+// ...}. Server: $LLMMAN_URL, else llmman's default address, else the one a
18+// Docker sandbox reaches the host by. Prints nothing: the hand-off goes on
19+// whatever happens (onError: warn in agent.yaml), an unload is a saving, not
20+// a check.
21+
22+const fs = require("fs");
23+
24+const input = JSON.parse(fs.readFileSync(0, "utf8"));
25+const which = process.argv[2];
26+const ref = which === "asked" ? input.ask && input.ask.agentModel : input.model;
27+if (!ref) {
28+ console.error(`unload.js ${which}: no model in the input`);
29+ process.exit(1);
30+}
31+const model = ref.replace(/^[^/]+\//, ""); // drop the "llmman/" provider prefix
32+
33+const servers = process.env.LLMMAN_URL
34+ ? [process.env.LLMMAN_URL]
35+ : ["http://127.0.0.1:17434", "http://host.docker.internal:17434"];
36+
37+async function unload() {
38+ for (const server of servers) {
39+ try {
40+ const resp = await fetch(server + "/api/generate", {
41+ method: "POST",
42+ headers: { "Content-Type": "application/json" },
43+ body: JSON.stringify({ model, keep_alive: 0 }),
44+ signal: AbortSignal.timeout(20_000),
45+ });
46+ const reply = await resp.json();
47+ if (reply.done_reason === "unload") return;
48+ throw new Error(`${server}: not confirmed: ${JSON.stringify(reply)}`);
49+ } catch (err) {
50+ if (server === servers[servers.length - 1]) throw err;
51+ }
52+ }
53+}
54+
55+unload().catch((err) => {
56+ console.error(`could not unload ${model}: ${err.message}`);
57+ process.exit(1);
58+});
modified docs/en/how-to/use-the-window-mode.md +1 -1
@@ -14,7 +14,7 @@ This guide shows how to run mini-me as a single full-screen window — the conve
1414
1515 2. Type your question in the box at the bottom and press **Enter**. A long line wraps at word boundaries inside the box (a word longer than a row is cut at the edge); the box keeps its three rows and scrolls them so the cursor's row stays visible, and ↑/↓ move the cursor from row to row. For a second paragraph, press **Ctrl-J** (or Alt-Enter where your terminal reports it) instead of Enter. A pasted block is inserted whole, never sent; a long one is collapsed into a `[Pasted #1 · …]` token (see below).
1616
17-3. Watch the answer stream into the conversation. Each tool call is one framed line — `‣ bash ls -1 ✓ done 0.03s` — followed by its output, folded to `previewLines` lines; the `+`/`-` lines of a file edit are coloured.
17+3. Watch the answer stream into the conversation. Each tool call is one framed line — `‣ bash ls -1 ✓ done 0.03s` — followed by its output, folded to `previewLines` lines; the `+`/`-` lines of a file edit are coloured. The `⏱` in the status bar counts the time since the question was sent, and keeps the final duration once the answer is done.
1818
1919 4. To add something while a turn runs, type it and press **Enter**: the turn is not stopped, the text waits (`⏳ Queued (1)`, and the rule reads `thinking · 1 queued`) and runs as the next question once the answer is written. Press **Esc** to stop a running turn; whatever is queued runs straight after — type the correction, then press Esc, to redirect the model at once.
2020
@@ -14,7 +14,7 @@ This guide shows how to run mini-me as a single full-screen window — the conve
14 14
15 2. Type your question in the box at the bottom and press **Enter**. A long line wraps at word boundaries inside the box (a word longer than a row is cut at the edge); the box keeps its three rows and scrolls them so the cursor's row stays visible, and ↑/↓ move the cursor from row to row. For a second paragraph, press **Ctrl-J** (or Alt-Enter where your terminal reports it) instead of Enter. A pasted block is inserted whole, never sent; a long one is collapsed into a `[Pasted #1 · …]` token (see below).15 2. Type your question in the box at the bottom and press **Enter**. A long line wraps at word boundaries inside the box (a word longer than a row is cut at the edge); the box keeps its three rows and scrolls them so the cursor's row stays visible, and ↑/↓ move the cursor from row to row. For a second paragraph, press **Ctrl-J** (or Alt-Enter where your terminal reports it) instead of Enter. A pasted block is inserted whole, never sent; a long one is collapsed into a `[Pasted #1 · …]` token (see below).
16 16
17-3. Watch the answer stream into the conversation. Each tool call is one framed line — `‣ bash ls -1 ✓ done 0.03s` — followed by its output, folded to `previewLines` lines; the `+`/`-` lines of a file edit are coloured.17+3. Watch the answer stream into the conversation. Each tool call is one framed line — `‣ bash ls -1 ✓ done 0.03s` — followed by its output, folded to `previewLines` lines; the `+`/`-` lines of a file edit are coloured. The `⏱` in the status bar counts the time since the question was sent, and keeps the final duration once the answer is done.
18 18
19 4. To add something while a turn runs, type it and press **Enter**: the turn is not stopped, the text waits (`⏳ Queued (1)`, and the rule reads `thinking · 1 queued`) and runs as the next question once the answer is written. Press **Esc** to stop a running turn; whatever is queued runs straight after — type the correction, then press Esc, to redirect the model at once.19 4. To add something while a turn runs, type it and press **Enter**: the turn is not stopped, the text waits (`⏳ Queued (1)`, and the rule reads `thinking · 1 queued`) and runs as the next question once the answer is written. Press **Esc** to stop a running turn; whatever is queued runs straight after — type the correction, then press Esc, to redirect the model at once.
20 20
modified docs/en/reference/cli.md +3 -2
@@ -118,7 +118,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
118118 | Banner | Not printed on stdout; shown as the first line of the conversation. |
119119 | Spinner | Disabled; the rule under the conversation animates instead. |
120120 | 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. |
121-| 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). |
121+| 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> · ⏱ <duration> · 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). |
122122 | 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. |
123123 | Pickers | `/` completes a command, `@` a path under the working directory, `$` a skill of the catalogue — the last two anywhere in a sentence, keeping their prefix. See [Slash commands](slash-commands.md#the-three-pickers). |
124124 | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. |
@@ -126,6 +126,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
126126 | Permissions | With `yolo: false` (and for MCP tools marked `ask`), the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject [n] never <tool>` until answered (`Esc` = `r`); a turn cancelled while asking rejects. |
127127 | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). |
128128 | Struggles | A badge on the right of the status bar until the next question — see [Struggle signals](#struggle-signals). |
129+| Completion duration | `⏱ <duration>` in the status bar, absent until the first question. During a turn or an `/ask` it counts up from the moment the question is sent, tools and hooks included, in tenths of a second (`⏱ 4.2s`) under a minute and in minutes and seconds beyond (`⏱ 2m05s`). When the turn ends — answered, failed or cancelled — it stops and keeps that duration until the next question. `/compact` is not timed. Hidden with `showCompletionTime: false` ([configuration](configuration.md)). |
129130 | Mouse | Not captured at start-up, so the terminal's click-and-drag selection works. `F2` captures it (wheel scrolling; `Shift`+drag selects) and releases it again. |
130131
131132 ## Prompt mode specifics
@@ -179,7 +180,7 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers
179180
180181 | Front end | Shown as |
181182 |-----------|----------|
182-| Window | A badge on the right of the status bar, in front of the counters (the version gives way): black on yellow for `⚠`, white on red for `✖`. The worst of the turn, with `(+N)` for the others; a severe signal beats a warning, the latest of the same level wins. It stays after the turn and goes when the next question is sent. |
183+| Window | A badge on the right of the status bar, in front of the duration and the counters (the version gives way): black on yellow for `⚠`, white on red for `✖`. The worst of the turn, with `(+N)` for the others; a severe signal beats a warning, the latest of the same level wins. It stays after the turn and goes when the next question is sent. |
183184 | Line mode, `-p` | One line `⚠ <signal>` / `✖ <signal>` on stdout when it happens: yellow or red on a terminal, plain otherwise. |
184185 | ACP | A paragraph `*⚠ mm: <signal>*` in the agent's message when it happens; not kept in the history. |
185186 | `-p` with `structuredOutput` | The plain line, on stderr. |
@@ -118,7 +118,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
118 | Banner | Not printed on stdout; shown as the first line of the conversation. |118 | Banner | Not printed on stdout; shown as the first line of the conversation. |
119 | Spinner | Disabled; the rule under the conversation animates instead. |119 | Spinner | Disabled; the rule under the conversation animates instead. |
120 | 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. |120 | 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. |
121-| 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). |121+| 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> · ⏱ <duration> · 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). |
122 | 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. |122 | 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. |
123 | Pickers | `/` completes a command, `@` a path under the working directory, `$` a skill of the catalogue — the last two anywhere in a sentence, keeping their prefix. See [Slash commands](slash-commands.md#the-three-pickers). |123 | Pickers | `/` completes a command, `@` a path under the working directory, `$` a skill of the catalogue — the last two anywhere in a sentence, keeping their prefix. See [Slash commands](slash-commands.md#the-three-pickers). |
124 | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. |124 | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. |
@@ -126,6 +126,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
126 | Permissions | With `yolo: false` (and for MCP tools marked `ask`), the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject [n] never <tool>` until answered (`Esc` = `r`); a turn cancelled while asking rejects. |126 | Permissions | With `yolo: false` (and for MCP tools marked `ask`), the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject [n] never <tool>` until answered (`Esc` = `r`); a turn cancelled while asking rejects. |
127 | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). |127 | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). |
128 | Struggles | A badge on the right of the status bar until the next question — see [Struggle signals](#struggle-signals). |128 | Struggles | A badge on the right of the status bar until the next question — see [Struggle signals](#struggle-signals). |
129+| Completion duration | `⏱ <duration>` in the status bar, absent until the first question. During a turn or an `/ask` it counts up from the moment the question is sent, tools and hooks included, in tenths of a second (`⏱ 4.2s`) under a minute and in minutes and seconds beyond (`⏱ 2m05s`). When the turn ends — answered, failed or cancelled — it stops and keeps that duration until the next question. `/compact` is not timed. Hidden with `showCompletionTime: false` ([configuration](configuration.md)). |
129 | Mouse | Not captured at start-up, so the terminal's click-and-drag selection works. `F2` captures it (wheel scrolling; `Shift`+drag selects) and releases it again. |130 | Mouse | Not captured at start-up, so the terminal's click-and-drag selection works. `F2` captures it (wheel scrolling; `Shift`+drag selects) and releases it again. |
130 131
131 ## Prompt mode specifics132 ## Prompt mode specifics
@@ -179,7 +180,7 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers
179 180
180 | Front end | Shown as |181 | Front end | Shown as |
181 |-----------|----------|182 |-----------|----------|
182-| Window | A badge on the right of the status bar, in front of the counters (the version gives way): black on yellow for `⚠`, white on red for `✖`. The worst of the turn, with `(+N)` for the others; a severe signal beats a warning, the latest of the same level wins. It stays after the turn and goes when the next question is sent. |183+| Window | A badge on the right of the status bar, in front of the duration and the counters (the version gives way): black on yellow for `⚠`, white on red for `✖`. The worst of the turn, with `(+N)` for the others; a severe signal beats a warning, the latest of the same level wins. It stays after the turn and goes when the next question is sent. |
183 | Line mode, `-p` | One line `⚠ <signal>` / `✖ <signal>` on stdout when it happens: yellow or red on a terminal, plain otherwise. |184 | Line mode, `-p` | One line `⚠ <signal>` / `✖ <signal>` on stdout when it happens: yellow or red on a terminal, plain otherwise. |
184 | ACP | A paragraph `*⚠ mm: <signal>*` in the agent's message when it happens; not kept in the history. |185 | ACP | A paragraph `*⚠ mm: <signal>*` in the agent's message when it happens; not kept in the history. |
185 | `-p` with `structuredOutput` | The plain line, on stderr. |186 | `-p` with `structuredOutput` | The plain line, on stderr. |
modified docs/en/reference/configuration.md +1 -0
@@ -18,6 +18,7 @@
1818 | `displayCommands` | bool | `false` | Print the numbered list of commands and file operations after each answer. |
1919 | `displayHooks` | bool | `false` | Show every hook that ran: event, command, tool or agent, decision, size of the context added (never its text). After each answer in the line mode, `-p` and the window; as a finished tool call per hook in an editor. See [hooks](hooks.md#display). |
2020 | `showThinking` | bool | `true` | Show what a thinking model reasons about before it answers. Only the display is optional: the reasoning is always read off the wire, because it is what keeps the watchdog alive while the model thinks. See [Thinking models](../explanation/thinking-models.md). |
21+| `showCompletionTime` | bool | `true` | Window mode: show `⏱ <duration>` in the status bar, ticking while a turn or an `/ask` runs, then the final duration until the next question. `false` keeps the status bar to its counters. See [the window mode](cli.md). |
2122 | `system` | string | built-in prompt | The system prompt, first message of every history. |
2223 | `structuredOutput` | string (Go declarations) | `""` | The shape every answer must follow, as JSON: Go `type` declarations and one `var` naming the root of the answer. See [`structuredOutput`](#structuredoutput) below. Empty = free-text answers. Incompatible with any declared tool. |
2324 | `decision` | mapping or absent | absent | Makes the agent a **decider**: its model is a decision model served by `POST /v1/systemone`, and every message it receives is answered with these questions. See [`decision`](#decision) below. Providers `llamacpp`, `llmman`, `ollama`, `openai` only. |
@@ -18,6 +18,7 @@
18 | `displayCommands` | bool | `false` | Print the numbered list of commands and file operations after each answer. |18 | `displayCommands` | bool | `false` | Print the numbered list of commands and file operations after each answer. |
19 | `displayHooks` | bool | `false` | Show every hook that ran: event, command, tool or agent, decision, size of the context added (never its text). After each answer in the line mode, `-p` and the window; as a finished tool call per hook in an editor. See [hooks](hooks.md#display). |19 | `displayHooks` | bool | `false` | Show every hook that ran: event, command, tool or agent, decision, size of the context added (never its text). After each answer in the line mode, `-p` and the window; as a finished tool call per hook in an editor. See [hooks](hooks.md#display). |
20 | `showThinking` | bool | `true` | Show what a thinking model reasons about before it answers. Only the display is optional: the reasoning is always read off the wire, because it is what keeps the watchdog alive while the model thinks. See [Thinking models](../explanation/thinking-models.md). |20 | `showThinking` | bool | `true` | Show what a thinking model reasons about before it answers. Only the display is optional: the reasoning is always read off the wire, because it is what keeps the watchdog alive while the model thinks. See [Thinking models](../explanation/thinking-models.md). |
21+| `showCompletionTime` | bool | `true` | Window mode: show `⏱ <duration>` in the status bar, ticking while a turn or an `/ask` runs, then the final duration until the next question. `false` keeps the status bar to its counters. See [the window mode](cli.md). |
21 | `system` | string | built-in prompt | The system prompt, first message of every history. |22 | `system` | string | built-in prompt | The system prompt, first message of every history. |
22 | `structuredOutput` | string (Go declarations) | `""` | The shape every answer must follow, as JSON: Go `type` declarations and one `var` naming the root of the answer. See [`structuredOutput`](#structuredoutput) below. Empty = free-text answers. Incompatible with any declared tool. |23 | `structuredOutput` | string (Go declarations) | `""` | The shape every answer must follow, as JSON: Go `type` declarations and one `var` naming the root of the answer. See [`structuredOutput`](#structuredoutput) below. Empty = free-text answers. Incompatible with any declared tool. |
23 | `decision` | mapping or absent | absent | Makes the agent a **decider**: its model is a decision model served by `POST /v1/systemone`, and every message it receives is answered with these questions. See [`decision`](#decision) below. Providers `llamacpp`, `llmman`, `ollama`, `openai` only. |24 | `decision` | mapping or absent | absent | Makes the agent a **decider**: its model is a decision model served by `POST /v1/systemone`, and every message it receives is answered with these questions. See [`decision`](#decision) below. Providers `llamacpp`, `llmman`, `ollama`, `openai` only. |
modified docs/fr/how-to/use-the-window-mode.md +1 -1
@@ -14,7 +14,7 @@ Ce guide montre comment lancer mini-me dans une fenêtre unique en plein écran
1414
1515 2. Tapez votre question dans la zone du bas et appuyez sur **Entrée**. Une ligne longue passe à la ligne entre les mots dans la zone (un mot plus long qu'une rangée est coupé au bord) ; la zone garde ses trois rangées et les fait défiler pour que celle du curseur reste visible, et ↑/↓ déplacent le curseur de rangée en rangée. Pour un second paragraphe, appuyez sur **Ctrl-J** (ou Alt-Entrée quand votre terminal le transmet) au lieu d'Entrée. Un bloc collé est inséré en entier, jamais envoyé ; un bloc long est replié en un jeton `[Pasted #1 · …]` (voir plus bas).
1616
17-3. Regardez la réponse arriver dans la conversation. Chaque appel d'outil tient sur une ligne encadrée — `‣ bash ls -1 ✓ done 0.03s` — suivie de sa sortie, repliée à `previewLines` lignes ; les lignes `+`/`-` d'une modification de fichier sont colorées.
17+3. Regardez la réponse arriver dans la conversation. Chaque appel d'outil tient sur une ligne encadrée — `‣ bash ls -1 ✓ done 0.03s` — suivie de sa sortie, repliée à `previewLines` lignes ; les lignes `+`/`-` d'une modification de fichier sont colorées. Le `⏱` de la barre d'état compte le temps écoulé depuis l'envoi de la question, et garde la durée finale une fois la réponse terminée.
1818
1919 4. Pour ajouter quelque chose pendant un tour, tapez-le et appuyez sur **Entrée** : le tour n'est pas arrêté, le texte attend (`⏳ Queued (1)`, et la règle affiche `thinking · 1 queued`) et part comme question suivante une fois la réponse écrite. Appuyez sur **Échap** pour arrêter un tour en cours ; ce qui attend part juste après — tapez la correction, puis Échap, pour réorienter le modèle tout de suite.
2020
@@ -14,7 +14,7 @@ Ce guide montre comment lancer mini-me dans une fenêtre unique en plein écran
14 14
15 2. Tapez votre question dans la zone du bas et appuyez sur **Entrée**. Une ligne longue passe à la ligne entre les mots dans la zone (un mot plus long qu'une rangée est coupé au bord) ; la zone garde ses trois rangées et les fait défiler pour que celle du curseur reste visible, et ↑/↓ déplacent le curseur de rangée en rangée. Pour un second paragraphe, appuyez sur **Ctrl-J** (ou Alt-Entrée quand votre terminal le transmet) au lieu d'Entrée. Un bloc collé est inséré en entier, jamais envoyé ; un bloc long est replié en un jeton `[Pasted #1 · …]` (voir plus bas).15 2. Tapez votre question dans la zone du bas et appuyez sur **Entrée**. Une ligne longue passe à la ligne entre les mots dans la zone (un mot plus long qu'une rangée est coupé au bord) ; la zone garde ses trois rangées et les fait défiler pour que celle du curseur reste visible, et ↑/↓ déplacent le curseur de rangée en rangée. Pour un second paragraphe, appuyez sur **Ctrl-J** (ou Alt-Entrée quand votre terminal le transmet) au lieu d'Entrée. Un bloc collé est inséré en entier, jamais envoyé ; un bloc long est replié en un jeton `[Pasted #1 · …]` (voir plus bas).
16 16
17-3. Regardez la réponse arriver dans la conversation. Chaque appel d'outil tient sur une ligne encadrée — `‣ bash ls -1 ✓ done 0.03s` — suivie de sa sortie, repliée à `previewLines` lignes ; les lignes `+`/`-` d'une modification de fichier sont colorées.17+3. Regardez la réponse arriver dans la conversation. Chaque appel d'outil tient sur une ligne encadrée — `‣ bash ls -1 ✓ done 0.03s` — suivie de sa sortie, repliée à `previewLines` lignes ; les lignes `+`/`-` d'une modification de fichier sont colorées. Le `⏱` de la barre d'état compte le temps écoulé depuis l'envoi de la question, et garde la durée finale une fois la réponse terminée.
18 18
19 4. Pour ajouter quelque chose pendant un tour, tapez-le et appuyez sur **Entrée** : le tour n'est pas arrêté, le texte attend (`⏳ Queued (1)`, et la règle affiche `thinking · 1 queued`) et part comme question suivante une fois la réponse écrite. Appuyez sur **Échap** pour arrêter un tour en cours ; ce qui attend part juste après — tapez la correction, puis Échap, pour réorienter le modèle tout de suite.19 4. Pour ajouter quelque chose pendant un tour, tapez-le et appuyez sur **Entrée** : le tour n'est pas arrêté, le texte attend (`⏳ Queued (1)`, et la règle affiche `thinking · 1 queued`) et part comme question suivante une fois la réponse écrite. Appuyez sur **Échap** pour arrêter un tour en cours ; ce qui attend part juste après — tapez la correction, puis Échap, pour réorienter le modèle tout de suite.
20 20
modified docs/fr/reference/cli.md +3 -2
@@ -118,7 +118,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
118118 | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. |
119119 | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. |
120120 | 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. |
121-| 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). |
121+| 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> · ⏱ <durée> · 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). |
122122 | 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. |
123123 | Sélecteurs | `/` complète une commande, `@` un chemin sous le répertoire de travail, `$` un skill du catalogue — les deux derniers n'importe où dans une phrase, en conservant leur préfixe. Voir [Commandes slash](slash-commands.md#les-trois-sélecteurs). |
124124 | 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`. |
@@ -126,6 +126,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
126126 | Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject [n] never <outil>` jusqu'à la réponse (`Échap` = `r`) ; un tour annulé pendant la question refuse. |
127127 | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). |
128128 | Difficultés | Un badge à droite de la barre d'état jusqu'à la question suivante — voir [Signaux de difficulté](#signaux-de-difficulté). |
129+| Durée de la complétion | `⏱ <durée>` dans la barre d'état, absent avant la première question. Pendant un tour ou un `/ask`, le compteur défile depuis l'envoi de la question, outils et hooks compris, au dixième de seconde (`⏱ 4.2s`) sous la minute, en minutes et secondes au-delà (`⏱ 2m05s`). À la fin du tour — réponse, échec ou annulation — il s'arrête et garde cette durée jusqu'à la question suivante. `/compact` n'est pas chronométré. Masqué avec `showCompletionTime: false` ([configuration](configuration.md)). |
129130 | Souris | Non capturée au démarrage, donc le cliquer-glisser du terminal sélectionne du texte. `F2` la capture (défilement à la molette ; `Maj`+glisser sélectionne) puis la rend. |
130131
131132 ## Spécificités du mode invite
@@ -179,7 +180,7 @@ Un tour annulé par l'utilisateur ne signale rien. `demos/struggle-signals/` dé
179180
180181 | Front end | Affiché comme |
181182 |-----------|---------------|
182-| Fenêtre | Un badge à droite de la barre d'état, devant les compteurs (la version s'efface) : noir sur jaune pour `⚠`, blanc sur rouge pour `✖`. Le plus grave du tour, avec `(+N)` pour les autres ; un signal grave l'emporte sur un avertissement, le plus récent d'un même niveau gagne. Il reste après le tour et disparaît quand la question suivante est envoyée. |
183+| Fenêtre | Un badge à droite de la barre d'état, devant la durée et les compteurs (la version s'efface) : noir sur jaune pour `⚠`, blanc sur rouge pour `✖`. Le plus grave du tour, avec `(+N)` pour les autres ; un signal grave l'emporte sur un avertissement, le plus récent d'un même niveau gagne. Il reste après le tour et disparaît quand la question suivante est envoyée. |
183184 | Mode ligne, `-p` | Une ligne `⚠ <signal>` / `✖ <signal>` sur stdout au moment où il arrive : jaune ou rouge dans un terminal, sans couleur sinon. |
184185 | ACP | Un paragraphe `*⚠ mm: <signal>*` dans le message de l'agent au moment où il arrive ; pas gardé dans l'historique. |
185186 | `-p` avec `structuredOutput` | La ligne sans couleur, sur stderr. |
@@ -118,7 +118,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
118 | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. |118 | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. |
119 | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. |119 | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. |
120 | 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. |120 | 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. |
121-| 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). |121+| 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> · ⏱ <durée> · 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). |
122 | 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. |122 | 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. |
123 | Sélecteurs | `/` complète une commande, `@` un chemin sous le répertoire de travail, `$` un skill du catalogue — les deux derniers n'importe où dans une phrase, en conservant leur préfixe. Voir [Commandes slash](slash-commands.md#les-trois-sélecteurs). |123 | Sélecteurs | `/` complète une commande, `@` un chemin sous le répertoire de travail, `$` un skill du catalogue — les deux derniers n'importe où dans une phrase, en conservant leur préfixe. Voir [Commandes slash](slash-commands.md#les-trois-sélecteurs). |
124 | 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`. |124 | 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`. |
@@ -126,6 +126,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
126 | Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject [n] never <outil>` jusqu'à la réponse (`Échap` = `r`) ; un tour annulé pendant la question refuse. |126 | Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject [n] never <outil>` jusqu'à la réponse (`Échap` = `r`) ; un tour annulé pendant la question refuse. |
127 | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). |127 | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). |
128 | Difficultés | Un badge à droite de la barre d'état jusqu'à la question suivante — voir [Signaux de difficulté](#signaux-de-difficulté). |128 | Difficultés | Un badge à droite de la barre d'état jusqu'à la question suivante — voir [Signaux de difficulté](#signaux-de-difficulté). |
129+| Durée de la complétion | `⏱ <durée>` dans la barre d'état, absent avant la première question. Pendant un tour ou un `/ask`, le compteur défile depuis l'envoi de la question, outils et hooks compris, au dixième de seconde (`⏱ 4.2s`) sous la minute, en minutes et secondes au-delà (`⏱ 2m05s`). À la fin du tour — réponse, échec ou annulation — il s'arrête et garde cette durée jusqu'à la question suivante. `/compact` n'est pas chronométré. Masqué avec `showCompletionTime: false` ([configuration](configuration.md)). |
129 | Souris | Non capturée au démarrage, donc le cliquer-glisser du terminal sélectionne du texte. `F2` la capture (défilement à la molette ; `Maj`+glisser sélectionne) puis la rend. |130 | Souris | Non capturée au démarrage, donc le cliquer-glisser du terminal sélectionne du texte. `F2` la capture (défilement à la molette ; `Maj`+glisser sélectionne) puis la rend. |
130 131
131 ## Spécificités du mode invite132 ## Spécificités du mode invite
@@ -179,7 +180,7 @@ Un tour annulé par l'utilisateur ne signale rien. `demos/struggle-signals/` dé
179 180
180 | Front end | Affiché comme |181 | Front end | Affiché comme |
181 |-----------|---------------|182 |-----------|---------------|
182-| Fenêtre | Un badge à droite de la barre d'état, devant les compteurs (la version s'efface) : noir sur jaune pour `⚠`, blanc sur rouge pour `✖`. Le plus grave du tour, avec `(+N)` pour les autres ; un signal grave l'emporte sur un avertissement, le plus récent d'un même niveau gagne. Il reste après le tour et disparaît quand la question suivante est envoyée. |183+| Fenêtre | Un badge à droite de la barre d'état, devant la durée et les compteurs (la version s'efface) : noir sur jaune pour `⚠`, blanc sur rouge pour `✖`. Le plus grave du tour, avec `(+N)` pour les autres ; un signal grave l'emporte sur un avertissement, le plus récent d'un même niveau gagne. Il reste après le tour et disparaît quand la question suivante est envoyée. |
183 | Mode ligne, `-p` | Une ligne `⚠ <signal>` / `✖ <signal>` sur stdout au moment où il arrive : jaune ou rouge dans un terminal, sans couleur sinon. |184 | Mode ligne, `-p` | Une ligne `⚠ <signal>` / `✖ <signal>` sur stdout au moment où il arrive : jaune ou rouge dans un terminal, sans couleur sinon. |
184 | ACP | Un paragraphe `*⚠ mm: <signal>*` dans le message de l'agent au moment où il arrive ; pas gardé dans l'historique. |185 | ACP | Un paragraphe `*⚠ mm: <signal>*` dans le message de l'agent au moment où il arrive ; pas gardé dans l'historique. |
185 | `-p` avec `structuredOutput` | La ligne sans couleur, sur stderr. |186 | `-p` avec `structuredOutput` | La ligne sans couleur, sur stderr. |
modified docs/fr/reference/configuration.md +1 -0
@@ -18,6 +18,7 @@
1818 | `displayCommands` | booléen | `false` | Affiche la liste numérotée des commandes et opérations sur fichiers après chaque réponse. |
1919 | `displayHooks` | booléen | `false` | Affiche chaque hook exécuté : événement, commande, outil ou agent, décision, taille du contexte ajouté (jamais son texte). Après chaque réponse en mode ligne, en `-p` et dans la fenêtre ; sous forme d'appel d'outil terminé, un par hook, dans un éditeur. Voir [hooks](hooks.md#affichage). |
2020 | `showThinking` | booléen | `true` | Affiche ce qu'un modèle à raisonnement réfléchit avant de répondre. Seul l'affichage est optionnel : le raisonnement est toujours lu sur le fil, car c'est lui qui maintient le chien de garde en vie pendant que le modèle réfléchit. Voir [Modèles à raisonnement](../explanation/thinking-models.md). |
21+| `showCompletionTime` | booléen | `true` | Mode fenêtre : afficher `⏱ <durée>` dans la barre d'état, qui défile pendant un tour ou un `/ask`, puis la durée finale jusqu'à la question suivante. `false` limite la barre d'état à ses compteurs. Voir [le mode fenêtre](cli.md). |
2122 | `system` | chaîne | prompt intégré | Le prompt système, premier message de chaque historique. |
2223 | `structuredOutput` | chaîne (déclarations Go) | `""` | La forme que doit suivre chaque réponse, en JSON : des déclarations `type` Go et une `var` qui nomme la racine de la réponse. Voir [`structuredOutput`](#structuredoutput) ci-dessous. Vide = réponses en texte libre. Incompatible avec tout outil déclaré. |
2324 | `decision` | mapping ou absent | absent | Fait de l'agent un **décideur** : son modèle est un modèle de décision servi par `POST /v1/systemone`, et chaque message reçu obtient pour réponse ces questions. Voir [`decision`](#decision) ci-dessous. Fournisseurs `llamacpp`, `llmman`, `ollama`, `openai` seulement. |
@@ -18,6 +18,7 @@
18 | `displayCommands` | booléen | `false` | Affiche la liste numérotée des commandes et opérations sur fichiers après chaque réponse. |18 | `displayCommands` | booléen | `false` | Affiche la liste numérotée des commandes et opérations sur fichiers après chaque réponse. |
19 | `displayHooks` | booléen | `false` | Affiche chaque hook exécuté : événement, commande, outil ou agent, décision, taille du contexte ajouté (jamais son texte). Après chaque réponse en mode ligne, en `-p` et dans la fenêtre ; sous forme d'appel d'outil terminé, un par hook, dans un éditeur. Voir [hooks](hooks.md#affichage). |19 | `displayHooks` | booléen | `false` | Affiche chaque hook exécuté : événement, commande, outil ou agent, décision, taille du contexte ajouté (jamais son texte). Après chaque réponse en mode ligne, en `-p` et dans la fenêtre ; sous forme d'appel d'outil terminé, un par hook, dans un éditeur. Voir [hooks](hooks.md#affichage). |
20 | `showThinking` | booléen | `true` | Affiche ce qu'un modèle à raisonnement réfléchit avant de répondre. Seul l'affichage est optionnel : le raisonnement est toujours lu sur le fil, car c'est lui qui maintient le chien de garde en vie pendant que le modèle réfléchit. Voir [Modèles à raisonnement](../explanation/thinking-models.md). |20 | `showThinking` | booléen | `true` | Affiche ce qu'un modèle à raisonnement réfléchit avant de répondre. Seul l'affichage est optionnel : le raisonnement est toujours lu sur le fil, car c'est lui qui maintient le chien de garde en vie pendant que le modèle réfléchit. Voir [Modèles à raisonnement](../explanation/thinking-models.md). |
21+| `showCompletionTime` | booléen | `true` | Mode fenêtre : afficher `⏱ <durée>` dans la barre d'état, qui défile pendant un tour ou un `/ask`, puis la durée finale jusqu'à la question suivante. `false` limite la barre d'état à ses compteurs. Voir [le mode fenêtre](cli.md). |
21 | `system` | chaîne | prompt intégré | Le prompt système, premier message de chaque historique. |22 | `system` | chaîne | prompt intégré | Le prompt système, premier message de chaque historique. |
22 | `structuredOutput` | chaîne (déclarations Go) | `""` | La forme que doit suivre chaque réponse, en JSON : des déclarations `type` Go et une `var` qui nomme la racine de la réponse. Voir [`structuredOutput`](#structuredoutput) ci-dessous. Vide = réponses en texte libre. Incompatible avec tout outil déclaré. |23 | `structuredOutput` | chaîne (déclarations Go) | `""` | La forme que doit suivre chaque réponse, en JSON : des déclarations `type` Go et une `var` qui nomme la racine de la réponse. Voir [`structuredOutput`](#structuredoutput) ci-dessous. Vide = réponses en texte libre. Incompatible avec tout outil déclaré. |
23 | `decision` | mapping ou absent | absent | Fait de l'agent un **décideur** : son modèle est un modèle de décision servi par `POST /v1/systemone`, et chaque message reçu obtient pour réponse ces questions. Voir [`decision`](#decision) ci-dessous. Fournisseurs `llamacpp`, `llmman`, `ollama`, `openai` seulement. |24 | `decision` | mapping ou absent | absent | Fait de l'agent un **décideur** : son modèle est un modèle de décision servi par `POST /v1/systemone`, et chaque message reçu obtient pour réponse ces questions. Voir [`decision`](#decision) ci-dessous. Fournisseurs `llamacpp`, `llmman`, `ollama`, `openai` seulement. |
added internal/config/completiontime_test.go +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+package config
2+
3+import "testing"
4+
5+// The completion time is shown unless a file turns it off (ticket 158).
6+func TestShowCompletionTimeDefaultsToTrueAndCanBeTurnedOff(t *testing.T) {
7+ if err := loadYAML(t, "model: m\n"); err != nil {
8+ t.Fatal(err)
9+ }
10+ if !Cfg.ShowCompletionTime {
11+ t.Error("showCompletionTime = false for a file that does not mention it")
12+ }
13+ if err := loadYAML(t, "model: m\nshowCompletionTime: false\n"); err != nil {
14+ t.Fatal(err)
15+ }
16+ if Cfg.ShowCompletionTime {
17+ t.Error("showCompletionTime: false was not read")
18+ }
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+package config
2+
3+import "testing"
4+
5+// The completion time is shown unless a file turns it off (ticket 158).
6+func TestShowCompletionTimeDefaultsToTrueAndCanBeTurnedOff(t *testing.T) {
7+ if err := loadYAML(t, "model: m\n"); err != nil {
8+ t.Fatal(err)
9+ }
10+ if !Cfg.ShowCompletionTime {
11+ t.Error("showCompletionTime = false for a file that does not mention it")
12+ }
13+ if err := loadYAML(t, "model: m\nshowCompletionTime: false\n"); err != nil {
14+ t.Fatal(err)
15+ }
16+ if Cfg.ShowCompletionTime {
17+ t.Error("showCompletionTime: false was not read")
18+ }
19+}
modified internal/config/config.go +14 -6
@@ -82,6 +82,11 @@ type Config struct {
8282 // while a model thinks (see internal/engine/reasoning.go).
8383 ShowThinking bool `yaml:"showThinking"`
8484
85+ // ShowCompletionTime: the window's status bar shows `⏱ <duration>`,
86+ // ticking while a turn or an /ask runs, then the final duration (ticket
87+ // 158). Display only: nothing else depends on it.
88+ ShowCompletionTime bool `yaml:"showCompletionTime"`
89+
8590 // System: the system prompt — what the agent is, and what it may do.
8691 System string `yaml:"system"`
8792
@@ -585,12 +590,15 @@ var Cfg = Config{
585590 // needs. `displayCommands: true` in the YAML turns it on.
586591 DisplayCommands: false,
587592 ShowThinking: true,
588- SkillsDir: "skills",
589- BashTool: true,
590- EditTools: true,
591- PlanTool: true,
592- AskUserTool: true,
593- UnloadModels: true,
593+ // On by default: asked for (ticket 158); `showCompletionTime: false`
594+ // keeps the status bar to its counters.
595+ ShowCompletionTime: true,
596+ SkillsDir: "skills",
597+ BashTool: true,
598+ EditTools: true,
599+ PlanTool: true,
600+ AskUserTool: true,
601+ UnloadModels: true,
594602 // FetchContentTool stays false (the zero value): see its field comment.
595603 System: `You are a coding agent working in a terminal.
596604 You have a "bash" tool to run shell commands.
@@ -82,6 +82,11 @@ type Config struct {
82 // while a model thinks (see internal/engine/reasoning.go).82 // while a model thinks (see internal/engine/reasoning.go).
83 ShowThinking bool `yaml:"showThinking"`83 ShowThinking bool `yaml:"showThinking"`
84 84
85+ // ShowCompletionTime: the window's status bar shows `⏱ <duration>`,
86+ // ticking while a turn or an /ask runs, then the final duration (ticket
87+ // 158). Display only: nothing else depends on it.
88+ ShowCompletionTime bool `yaml:"showCompletionTime"`
89+
85 // System: the system prompt — what the agent is, and what it may do.90 // System: the system prompt — what the agent is, and what it may do.
86 System string `yaml:"system"`91 System string `yaml:"system"`
87 92
@@ -585,12 +590,15 @@ var Cfg = Config{
585 // needs. `displayCommands: true` in the YAML turns it on.590 // needs. `displayCommands: true` in the YAML turns it on.
586 DisplayCommands: false,591 DisplayCommands: false,
587 ShowThinking: true,592 ShowThinking: true,
588- SkillsDir: "skills",593+ // On by default: asked for (ticket 158); `showCompletionTime: false`
589- BashTool: true,594+ // keeps the status bar to its counters.
590- EditTools: true,595+ ShowCompletionTime: true,
591- PlanTool: true,596+ SkillsDir: "skills",
592- AskUserTool: true,597+ BashTool: true,
593- UnloadModels: true,598+ EditTools: true,
599+ PlanTool: true,
600+ AskUserTool: true,
601+ UnloadModels: true,
594 // FetchContentTool stays false (the zero value): see its field comment.602 // FetchContentTool stays false (the zero value): see its field comment.
595 System: `You are a coding agent working in a terminal.603 System: `You are a coding agent working in a terminal.
596 You have a "bash" tool to run shell commands.604 You have a "bash" tool to run shell commands.
modified internal/tui/ask.go +1 -0
@@ -27,6 +27,7 @@ func (r *runner) startAsk(p Prompt, req command.AskRequest) {
2727 r.cancel = cancel
2828 r.cancelled.Store(false)
2929 r.busy.Store("asking " + req.Agent)
30+ r.watch.start()
3031 r.streamed.Store(false)
3132 msgs := append([]*ai.Message(nil), r.messages...)
3233 go func() {
@@ -27,6 +27,7 @@ func (r *runner) startAsk(p Prompt, req command.AskRequest) {
27 r.cancel = cancel27 r.cancel = cancel
28 r.cancelled.Store(false)28 r.cancelled.Store(false)
29 r.busy.Store("asking " + req.Agent)29 r.busy.Store("asking " + req.Agent)
30+ r.watch.start()
30 r.streamed.Store(false)31 r.streamed.Store(false)
31 msgs := append([]*ai.Message(nil), r.messages...)32 msgs := append([]*ai.Message(nil), r.messages...)
32 go func() {33 go func() {
added internal/tui/stopwatch.go +67 -0
new file mode 100644
@@ -0,0 +1,67 @@
1+package tui
2+
3+import (
4+ "fmt"
5+ "time"
6+)
7+
8+// stopwatch times the completion in progress for the status bar (ticket
9+// 158): from the question sent to the answer settled, tools and hooks
10+// included. While it runs the label ticks with the window's animation; once
11+// the job has settled it keeps the final duration until the next question.
12+// Only the loop goroutine touches it (startTurn, startAsk, settle, frame).
13+//
14+// var s stopwatch
15+// s.start()
16+// s.label() // "⏱ 0.0s", then "⏱ 0.1s"…
17+// s.stop()
18+// s.label() // "⏱ 4.2s", until the next start
19+type stopwatch struct {
20+ now func() time.Time // time.Now; a test sets its own clock
21+ started time.Time
22+ last time.Duration
23+ running bool
24+ used bool // nothing is shown before the first completion
25+}
26+
27+func (s *stopwatch) clock() time.Time {
28+ if s.now != nil {
29+ return s.now()
30+ }
31+ return time.Now()
32+}
33+
34+// start begins timing a new completion.
35+func (s *stopwatch) start() {
36+ s.started, s.running, s.used = s.clock(), true, true
37+}
38+
39+// stop freezes the duration; a stop with nothing running changes nothing.
40+func (s *stopwatch) stop() {
41+ if !s.running {
42+ return
43+ }
44+ s.last, s.running = s.clock().Sub(s.started), false
45+}
46+
47+// label is what the status bar shows: "" before the first completion.
48+func (s *stopwatch) label() string {
49+ if !s.used {
50+ return ""
51+ }
52+ d := s.last
53+ if s.running {
54+ d = s.clock().Sub(s.started)
55+ }
56+ return "⏱ " + formatDuration(d)
57+}
58+
59+// formatDuration gives tenths under a minute — they are what makes the
60+// label visibly move — and minutes and seconds beyond.
61+func formatDuration(d time.Duration) string {
62+ if d < time.Minute {
63+ return fmt.Sprintf("%.1fs", d.Seconds())
64+ }
65+ d = d.Round(time.Second)
66+ return fmt.Sprintf("%dm%02ds", int(d.Minutes()), int(d.Seconds())%60)
67+}
new file mode 100644
@@ -0,0 +1,67 @@
1+package tui
2+
3+import (
4+ "fmt"
5+ "time"
6+)
7+
8+// stopwatch times the completion in progress for the status bar (ticket
9+// 158): from the question sent to the answer settled, tools and hooks
10+// included. While it runs the label ticks with the window's animation; once
11+// the job has settled it keeps the final duration until the next question.
12+// Only the loop goroutine touches it (startTurn, startAsk, settle, frame).
13+//
14+// var s stopwatch
15+// s.start()
16+// s.label() // "⏱ 0.0s", then "⏱ 0.1s"…
17+// s.stop()
18+// s.label() // "⏱ 4.2s", until the next start
19+type stopwatch struct {
20+ now func() time.Time // time.Now; a test sets its own clock
21+ started time.Time
22+ last time.Duration
23+ running bool
24+ used bool // nothing is shown before the first completion
25+}
26+
27+func (s *stopwatch) clock() time.Time {
28+ if s.now != nil {
29+ return s.now()
30+ }
31+ return time.Now()
32+}
33+
34+// start begins timing a new completion.
35+func (s *stopwatch) start() {
36+ s.started, s.running, s.used = s.clock(), true, true
37+}
38+
39+// stop freezes the duration; a stop with nothing running changes nothing.
40+func (s *stopwatch) stop() {
41+ if !s.running {
42+ return
43+ }
44+ s.last, s.running = s.clock().Sub(s.started), false
45+}
46+
47+// label is what the status bar shows: "" before the first completion.
48+func (s *stopwatch) label() string {
49+ if !s.used {
50+ return ""
51+ }
52+ d := s.last
53+ if s.running {
54+ d = s.clock().Sub(s.started)
55+ }
56+ return "⏱ " + formatDuration(d)
57+}
58+
59+// formatDuration gives tenths under a minute — they are what makes the
60+// label visibly move — and minutes and seconds beyond.
61+func formatDuration(d time.Duration) string {
62+ if d < time.Minute {
63+ return fmt.Sprintf("%.1fs", d.Seconds())
64+ }
65+ d = d.Round(time.Second)
66+ return fmt.Sprintf("%dm%02ds", int(d.Minutes()), int(d.Seconds())%60)
67+}
added internal/tui/stopwatch_test.go +82 -0
new file mode 100644
@@ -0,0 +1,82 @@
1+package tui
2+
3+import (
4+ "strings"
5+ "testing"
6+ "time"
7+
8+ "mm/internal/config"
9+)
10+
11+// The label is empty before the first completion, ticks while one runs,
12+// and keeps the final duration once it has settled.
13+func TestStopwatchTicksThenKeepsTheLastDuration(t *testing.T) {
14+ now := time.Unix(0, 0)
15+ s := stopwatch{now: func() time.Time { return now }}
16+ if got := s.label(); got != "" {
17+ t.Errorf("before any completion: %q", got)
18+ }
19+ s.start()
20+ now = now.Add(1200 * time.Millisecond)
21+ if got := s.label(); got != "⏱ 1.2s" {
22+ t.Errorf("running: %q", got)
23+ }
24+ now = now.Add(3 * time.Second)
25+ s.stop()
26+ now = now.Add(time.Hour)
27+ if got := s.label(); got != "⏱ 4.2s" {
28+ t.Errorf("settled: %q", got)
29+ }
30+ s.stop() // nothing running: unchanged
31+ if got := s.label(); got != "⏱ 4.2s" {
32+ t.Errorf("second stop: %q", got)
33+ }
34+}
35+
36+func TestFormatDurationUsesMinutesFromOneMinute(t *testing.T) {
37+ for d, want := range map[time.Duration]string{
38+ 0: "0.0s",
39+ 59900 * time.Millisecond: "59.9s",
40+ time.Minute: "1m00s",
41+ 125 * time.Second: "2m05s",
42+ } {
43+ if got := formatDuration(d); got != want {
44+ t.Errorf("formatDuration(%v) = %q, want %q", d, got, want)
45+ }
46+ }
47+}
48+
49+// A turn starts the stopwatch and settling it stops it: the status bar then
50+// leads with the turn's duration.
51+func TestATurnIsTimedInTheStatusBar(t *testing.T) {
52+ m := &fakeModel{hold: make(chan struct{}), answer: "done"}
53+ r, _ := newTestRunner(t, m)
54+ if strings.Contains(r.status(), "⏱") {
55+ t.Errorf("timed before any turn: %q", r.status())
56+ }
57+ now := time.Unix(0, 0)
58+ r.watch.now = func() time.Time { return now }
59+ r.submit(promptOf("q"))
60+ now = now.Add(2500 * time.Millisecond)
61+ if got := r.status(); !strings.HasPrefix(got, "⏱ 2.5s · ") {
62+ t.Errorf("running: %q", got)
63+ }
64+ close(m.hold)
65+ waitIdle(t, r)
66+ now = now.Add(time.Minute)
67+ if got := r.status(); !strings.HasPrefix(got, "⏱ 2.5s · 2 msgs") {
68+ t.Errorf("settled: %q", got)
69+ }
70+}
71+
72+// `showCompletionTime: false` keeps the status bar to its counters, even
73+// after a timed turn.
74+func TestShowCompletionTimeOffHidesTheDuration(t *testing.T) {
75+ r, _ := newTestRunner(t, &fakeModel{answer: "done"})
76+ config.Cfg.ShowCompletionTime = false
77+ r.submit(promptOf("q"))
78+ waitIdle(t, r)
79+ if got := r.status(); strings.Contains(got, "⏱") || got != "2 msgs · 0 tools" {
80+ t.Errorf("status %q", got)
81+ }
82+}
new file mode 100644
@@ -0,0 +1,82 @@
1+package tui
2+
3+import (
4+ "strings"
5+ "testing"
6+ "time"
7+
8+ "mm/internal/config"
9+)
10+
11+// The label is empty before the first completion, ticks while one runs,
12+// and keeps the final duration once it has settled.
13+func TestStopwatchTicksThenKeepsTheLastDuration(t *testing.T) {
14+ now := time.Unix(0, 0)
15+ s := stopwatch{now: func() time.Time { return now }}
16+ if got := s.label(); got != "" {
17+ t.Errorf("before any completion: %q", got)
18+ }
19+ s.start()
20+ now = now.Add(1200 * time.Millisecond)
21+ if got := s.label(); got != "⏱ 1.2s" {
22+ t.Errorf("running: %q", got)
23+ }
24+ now = now.Add(3 * time.Second)
25+ s.stop()
26+ now = now.Add(time.Hour)
27+ if got := s.label(); got != "⏱ 4.2s" {
28+ t.Errorf("settled: %q", got)
29+ }
30+ s.stop() // nothing running: unchanged
31+ if got := s.label(); got != "⏱ 4.2s" {
32+ t.Errorf("second stop: %q", got)
33+ }
34+}
35+
36+func TestFormatDurationUsesMinutesFromOneMinute(t *testing.T) {
37+ for d, want := range map[time.Duration]string{
38+ 0: "0.0s",
39+ 59900 * time.Millisecond: "59.9s",
40+ time.Minute: "1m00s",
41+ 125 * time.Second: "2m05s",
42+ } {
43+ if got := formatDuration(d); got != want {
44+ t.Errorf("formatDuration(%v) = %q, want %q", d, got, want)
45+ }
46+ }
47+}
48+
49+// A turn starts the stopwatch and settling it stops it: the status bar then
50+// leads with the turn's duration.
51+func TestATurnIsTimedInTheStatusBar(t *testing.T) {
52+ m := &fakeModel{hold: make(chan struct{}), answer: "done"}
53+ r, _ := newTestRunner(t, m)
54+ if strings.Contains(r.status(), "⏱") {
55+ t.Errorf("timed before any turn: %q", r.status())
56+ }
57+ now := time.Unix(0, 0)
58+ r.watch.now = func() time.Time { return now }
59+ r.submit(promptOf("q"))
60+ now = now.Add(2500 * time.Millisecond)
61+ if got := r.status(); !strings.HasPrefix(got, "⏱ 2.5s · ") {
62+ t.Errorf("running: %q", got)
63+ }
64+ close(m.hold)
65+ waitIdle(t, r)
66+ now = now.Add(time.Minute)
67+ if got := r.status(); !strings.HasPrefix(got, "⏱ 2.5s · 2 msgs") {
68+ t.Errorf("settled: %q", got)
69+ }
70+}
71+
72+// `showCompletionTime: false` keeps the status bar to its counters, even
73+// after a timed turn.
74+func TestShowCompletionTimeOffHidesTheDuration(t *testing.T) {
75+ r, _ := newTestRunner(t, &fakeModel{answer: "done"})
76+ config.Cfg.ShowCompletionTime = false
77+ r.submit(promptOf("q"))
78+ waitIdle(t, r)
79+ if got := r.status(); strings.Contains(got, "⏱") || got != "2 msgs · 0 tools" {
80+ t.Errorf("status %q", got)
81+ }
82+}
modified internal/tui/turn.go +15 -4
@@ -147,6 +147,9 @@ type runner struct {
147147 // struggles is what the turn reported about the model struggling
148148 // (ticket 153), for the status bar; cleared when a question is sent.
149149 struggles struggles
150+
151+ // watch times the completion in progress for the status bar (ticket 158).
152+ watch stopwatch
150153 }
151154
152155 func newRunner(opts Options, t *Transcript, wake func()) *runner {
@@ -199,6 +202,7 @@ func (r *runner) startTurn(p Prompt) {
199202 r.cancel = cancel
200203 r.cancelled.Store(false)
201204 r.busy.Store("thinking")
205+ r.watch.start()
202206 r.streamed.Store(false)
203207 msgs := append([]*ai.Message(nil), r.messages...)
204208 // Hooks that ran outside a turn (an /ask) are not this turn's.
@@ -290,6 +294,7 @@ func (r *runner) settle() {
290294 return
291295 }
292296 r.busy.Store("")
297+ r.watch.stop()
293298 r.cancel = nil
294299 r.cancelled.Store(false)
295300 r.runQueued()
@@ -455,11 +460,17 @@ func (r *runner) header() string {
455460 return b.String()
456461 }
457462
458-// status is the counters on the right of the bottom bar. The version sits in
459-// front of them when there is room; drawStatus decides, since only it knows
460-// how wide the window is.
463+// status is the counters on the right of the bottom bar, led by the
464+// duration of the completion in progress or of the last one (ticket 158).
465+// `showCompletionTime: false` leaves the counters alone. The version sits in
466+// front of them when there is room; drawStatus decides,
467+// since only it knows how wide the window is.
461468 func (r *runner) status() string {
462- return fmt.Sprintf("%d msgs · %d tools", session.Forgotten(r.messages), r.t.Tools())
469+ counters := fmt.Sprintf("%d msgs · %d tools", session.Forgotten(r.messages), r.t.Tools())
470+ if d := r.watch.label(); d != "" && config.Cfg.ShowCompletionTime {
471+ return d + " · " + counters
472+ }
473+ return counters
463474 }
464475
465476 // --- permissions ------------------------------------------------------------------
@@ -147,6 +147,9 @@ type runner struct {
147 // struggles is what the turn reported about the model struggling147 // struggles is what the turn reported about the model struggling
148 // (ticket 153), for the status bar; cleared when a question is sent.148 // (ticket 153), for the status bar; cleared when a question is sent.
149 struggles struggles149 struggles struggles
150+
151+ // watch times the completion in progress for the status bar (ticket 158).
152+ watch stopwatch
150 }153 }
151 154
152 func newRunner(opts Options, t *Transcript, wake func()) *runner {155 func newRunner(opts Options, t *Transcript, wake func()) *runner {
@@ -199,6 +202,7 @@ func (r *runner) startTurn(p Prompt) {
199 r.cancel = cancel202 r.cancel = cancel
200 r.cancelled.Store(false)203 r.cancelled.Store(false)
201 r.busy.Store("thinking")204 r.busy.Store("thinking")
205+ r.watch.start()
202 r.streamed.Store(false)206 r.streamed.Store(false)
203 msgs := append([]*ai.Message(nil), r.messages...)207 msgs := append([]*ai.Message(nil), r.messages...)
204 // Hooks that ran outside a turn (an /ask) are not this turn's.208 // Hooks that ran outside a turn (an /ask) are not this turn's.
@@ -290,6 +294,7 @@ func (r *runner) settle() {
290 return294 return
291 }295 }
292 r.busy.Store("")296 r.busy.Store("")
297+ r.watch.stop()
293 r.cancel = nil298 r.cancel = nil
294 r.cancelled.Store(false)299 r.cancelled.Store(false)
295 r.runQueued()300 r.runQueued()
@@ -455,11 +460,17 @@ func (r *runner) header() string {
455 return b.String()460 return b.String()
456 }461 }
457 462
458-// status is the counters on the right of the bottom bar. The version sits in463+// status is the counters on the right of the bottom bar, led by the
459-// front of them when there is room; drawStatus decides, since only it knows464+// duration of the completion in progress or of the last one (ticket 158).
460-// how wide the window is.465+// `showCompletionTime: false` leaves the counters alone. The version sits in
466+// front of them when there is room; drawStatus decides,
467+// since only it knows how wide the window is.
461 func (r *runner) status() string {468 func (r *runner) status() string {
462- return fmt.Sprintf("%d msgs · %d tools", session.Forgotten(r.messages), r.t.Tools())469+ counters := fmt.Sprintf("%d msgs · %d tools", session.Forgotten(r.messages), r.t.Tools())
470+ if d := r.watch.label(); d != "" && config.Cfg.ShowCompletionTime {
471+ return d + " · " + counters
472+ }
473+ return counters
463 }474 }
464 475
465 // --- permissions ------------------------------------------------------------------476 // --- permissions ------------------------------------------------------------------