🛟 Updated.
d973158 parent: a7ddac6 modified
.tickets/issues/0158-calculate-the-duration-of-a-completion.yaml +71 -4 | @@ -6,9 +6,76 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | 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 | |
| 11 | 13 | 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 | |
| 14 | 81 | comments: [] |
| @@ -6,9 +6,76 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-25T18:15:39.756Z | 8 | createdAt: 2026-10-25T18:15:39.756Z |
| 9 | -updatedAt: 2026-10-25T18:15:39.756Z | 9 | +updatedAt: 2026-10-25T22:40:00.000Z |
| 10 | -labels: [] | 10 | +labels: |
| 11 | + - enhancement | ||
| 12 | + - To be tested | ||
| 11 | milestone: v1.0.1 | 13 | 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 | ||
| 8 | 8 | |---|---|---|---| |
| 9 | 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 | 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 | 15 | ## Run |
| 16 | 16 | |
| @@ -23,7 +23,7 @@ cd demos/coding-helpers | ||
| 23 | 23 | 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 | 28 | ## 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 | 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 | 85 | ## Known limits |
| 66 | 86 | |
| 67 | 87 | - 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 | ## Run | 15 | ## Run |
| 16 | 16 | ||
| @@ -23,7 +23,7 @@ cd demos/coding-helpers | |||
| 23 | mm # reads ./agent.yaml | 23 | 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 | ## Use | 28 | ## 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 limits | 85 | ## 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 | ||
| 8 | 8 | |---|---|---|---| |
| 9 | 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 | 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 | 15 | ## Lancer |
| 16 | 16 | |
| @@ -23,7 +23,7 @@ cd demos/coding-helpers | ||
| 23 | 23 | 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 | 28 | ## 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 | 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 | 85 | ## Limites connues |
| 66 | 86 | |
| 67 | 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. |
| @@ -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 | ## Lancer | 15 | ## Lancer |
| 16 | 16 | ||
| @@ -23,7 +23,7 @@ cd demos/coding-helpers | |||
| 23 | mm # lit ./agent.yaml | 23 | 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 | ## Utiliser | 28 | ## 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 connues | 85 | ## 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 @@ | ||
| 14 | 14 | # sends the conversation along (`with_context`), so the Rust specialist |
| 15 | 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 | 19 | # 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 | 28 | # llmman pull hf.co/openbmb/MiniCPM5-2B-GGUF:Q4_K_M |
| 23 | 29 | # llmman pull huggingface.co/jetbrains/mellum2-12b-a2.5b-thinking-gguf-q4_k_m:Q4_K_M |
| @@ -58,6 +64,22 @@ sampling: | ||
| 58 | 64 | chat_template_kwargs: |
| 59 | 65 | 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 | 83 | # During ask_agent every event of the specialist counts as a sign of life; |
| 62 | 84 | # a 12B thinking model may reason a while before its first word. |
| 63 | 85 | watchdogTimeout: 5m |
| @@ -67,6 +89,7 @@ firstTokenTimeout: 3m | ||
| 67 | 89 | maxTurns: 3 |
| 68 | 90 | maxOutput: 20000 |
| 69 | 91 | displayCommands: true |
| 92 | +displayHooks: true | |
| 70 | 93 | |
| 71 | 94 | yolo: true |
| 72 | 95 | bashTool: false |
| @@ -14,10 +14,16 @@ | |||
| 14 | # sends the conversation along (`with_context`), so the Rust specialist | 14 | # 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 GB | 19 | # 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_M | 28 | # 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_M | 29 | # 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: false | 65 | 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: 5m | 85 | watchdogTimeout: 5m |
| @@ -67,6 +89,7 @@ firstTokenTimeout: 3m | |||
| 67 | maxTurns: 3 | 89 | maxTurns: 3 |
| 68 | maxOutput: 20000 | 90 | maxOutput: 20000 |
| 69 | displayCommands: true | 91 | displayCommands: true |
| 92 | +displayHooks: true | ||
| 70 | 93 | ||
| 71 | yolo: true | 94 | yolo: true |
| 72 | bashTool: false | 95 | 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 | ||
| 14 | 14 | |
| 15 | 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 | 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 | |
| @@ -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/ | ||
| 118 | 118 | | Banner | Not printed on stdout; shown as the first line of the conversation. | |
| 119 | 119 | | Spinner | Disabled; the rule under the conversation animates instead. | |
| 120 | 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 | 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 | 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 | 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 | 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 | 127 | | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). | |
| 128 | 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 | 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 | 132 | ## Prompt mode specifics |
| @@ -179,7 +180,7 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers | ||
| 179 | 180 | |
| 180 | 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 | 184 | | Line mode, `-p` | One line `⚠ <signal>` / `✖ <signal>` on stdout when it happens: yellow or red on a terminal, plain otherwise. | |
| 184 | 185 | | ACP | A paragraph `*⚠ mm: <signal>*` in the agent's message when it happens; not kept in the history. | |
| 185 | 186 | | `-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 specifics | 132 | ## 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 @@ | ||
| 18 | 18 | | `displayCommands` | bool | `false` | Print the numbered list of commands and file operations after each answer. | |
| 19 | 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 | 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 | 22 | | `system` | string | built-in prompt | The system prompt, first message of every history. | |
| 22 | 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 | 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. | |
| @@ -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 | ||
| 14 | 14 | |
| 15 | 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 | 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 | |
| @@ -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/ | ||
| 118 | 118 | | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. | |
| 119 | 119 | | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. | |
| 120 | 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 | 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 | 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 | 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 | 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 | 127 | | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). | |
| 128 | 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 | 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 | 132 | ## 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 | 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 | 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 | 185 | | ACP | Un paragraphe `*⚠ mm: <signal>*` dans le message de l'agent au moment où il arrive ; pas gardé dans l'historique. | |
| 185 | 186 | | `-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 invite | 132 | ## 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 @@ | ||
| 18 | 18 | | `displayCommands` | booléen | `false` | Affiche la liste numérotée des commandes et opérations sur fichiers après chaque réponse. | |
| 19 | 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 | 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 | 22 | | `system` | chaîne | prompt intégré | Le prompt système, premier message de chaque historique. | |
| 22 | 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 | 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. | |
| @@ -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 { | ||
| 82 | 82 | // while a model thinks (see internal/engine/reasoning.go). |
| 83 | 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 | 90 | // System: the system prompt — what the agent is, and what it may do. |
| 86 | 91 | System string `yaml:"system"` |
| 87 | 92 | |
| @@ -585,12 +590,15 @@ var Cfg = Config{ | ||
| 585 | 590 | // needs. `displayCommands: true` in the YAML turns it on. |
| 586 | 591 | DisplayCommands: false, |
| 587 | 592 | 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, | |
| 594 | 602 | // FetchContentTool stays false (the zero value): see its field comment. |
| 595 | 603 | System: `You are a coding agent working in a terminal. |
| 596 | 604 | 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) { | ||
| 27 | 27 | r.cancel = cancel |
| 28 | 28 | r.cancelled.Store(false) |
| 29 | 29 | r.busy.Store("asking " + req.Agent) |
| 30 | + r.watch.start() | |
| 30 | 31 | r.streamed.Store(false) |
| 31 | 32 | msgs := append([]*ai.Message(nil), r.messages...) |
| 32 | 33 | go func() { |
| @@ -27,6 +27,7 @@ func (r *runner) startAsk(p Prompt, req command.AskRequest) { | |||
| 27 | r.cancel = cancel | 27 | 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 { | ||
| 147 | 147 | // struggles is what the turn reported about the model struggling |
| 148 | 148 | // (ticket 153), for the status bar; cleared when a question is sent. |
| 149 | 149 | struggles struggles |
| 150 | + | |
| 151 | + // watch times the completion in progress for the status bar (ticket 158). | |
| 152 | + watch stopwatch | |
| 150 | 153 | } |
| 151 | 154 | |
| 152 | 155 | func newRunner(opts Options, t *Transcript, wake func()) *runner { |
| @@ -199,6 +202,7 @@ func (r *runner) startTurn(p Prompt) { | ||
| 199 | 202 | r.cancel = cancel |
| 200 | 203 | r.cancelled.Store(false) |
| 201 | 204 | r.busy.Store("thinking") |
| 205 | + r.watch.start() | |
| 202 | 206 | r.streamed.Store(false) |
| 203 | 207 | msgs := append([]*ai.Message(nil), r.messages...) |
| 204 | 208 | // Hooks that ran outside a turn (an /ask) are not this turn's. |
| @@ -290,6 +294,7 @@ func (r *runner) settle() { | ||
| 290 | 294 | return |
| 291 | 295 | } |
| 292 | 296 | r.busy.Store("") |
| 297 | + r.watch.stop() | |
| 293 | 298 | r.cancel = nil |
| 294 | 299 | r.cancelled.Store(false) |
| 295 | 300 | r.runQueued() |
| @@ -455,11 +460,17 @@ func (r *runner) header() string { | ||
| 455 | 460 | return b.String() |
| 456 | 461 | } |
| 457 | 462 | |
| 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. | |
| 461 | 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 | 476 | // --- permissions ------------------------------------------------------------------ |
| @@ -147,6 +147,9 @@ type runner struct { | |||
| 147 | // struggles is what the turn reported about the model struggling | 147 | // 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 struggles | 149 | 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 = cancel | 202 | 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 | return | 294 | return |
| 291 | } | 295 | } |
| 292 | r.busy.Store("") | 296 | r.busy.Store("") |
| 297 | + r.watch.stop() | ||
| 293 | r.cancel = nil | 298 | 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 in | 463 | +// 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 knows | 464 | +// 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 ------------------------------------------------------------------ |