bots-garden/sidekickspublic Fork
⑂ b5a5d88
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

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

Download ZIP

🛟 Updated.

k33g committed 2026-11-01T08:00:54+01:00 Browse files
b5a5d88 parent: 69b852b
modified .tickets/issues/0051-ajouter-un-bouton-pour-decharger-le-modele-de-la.yaml +84 -5
@@ -1,13 +1,92 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 51
3-title: Ajouter un bouton pour decharger le modele de la mémoire (quand c'est possible)
3+title: Ajouter un bouton pour décharger le modèle de la mémoire (quand l'agent le permet)
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-31T07:10:06.981Z
9-updatedAt: 2026-10-31T07:10:06.981Z
10-labels: []
11-body: ""
12-tasks: []
9+updatedAt: 2026-11-01T06:58:53.000Z
10+labels:
11+ - kind::feature
12+ - area::acp
13+ - area::ui
14+ - citizen-ux
15+epic: Agents
16+related:
17+ - 61
18+body: |
19+ Libérer la mémoire prise par le modèle d'un agent sans l'arrêter, depuis l'interface, quand la
20+ pression mémoire (#61) monte.
21+
22+ ## Principe : c'est l'agent qui expose la capacité
23+ Sidekicks ne parle pas au serveur de modèles (llmman, Ollama…) : il ne sait pas quel modèle
24+ un agent utilise, ni où il est servi. C'est l'agent qui sait se décharger. mm le fait déjà :
25+ il annonce la commande `unload` (avec `compact` et `new`) dans `available_commands_update`,
26+ juste après `session/new` ([#24](.tickets/issues/0024-commandes-slash-exposees-par-les-agents.yaml)),
27+ et vérifie `/api/ps` avant d'envoyer `keep_alive: 0` (il ne charge jamais un modèle pour le décharger).
28+
29+ ACP n'a pas de méthode standard pour ça. Convention retenue : **un agent qui annonce une
30+ commande nommée `unload` sait décharger son modèle**. Sidekicks connaît déjà les commandes de
31+ chaque agent (`ChannelService.Commands(agent)`, gardées dans le store, demandées par `Prepare`).
32+ Plus tard, si ACP ou mm proposent une méthode d'extension (`_sidekicks/unload` ou une capacité
33+ dans `initialize`), elle pourra remplacer la commande sans changer l'interface.
34+
35+ ## Comportement attendu
36+ - Un bouton **⏏ Décharger le modèle** pour un agent qui a annoncé `unload`, dans les actions de
37+ la liste Agents et dans l'en-tête de la vue de l'agent ; absent pour les autres agents.
38+ - Actif seulement quand l'agent tourne et ne répond pas (prêt) ; grisé pendant une réponse.
39+ Un agent arrêté n'a rien à décharger depuis Sidekicks (c'est le rôle d'`afterStop`).
40+ - Le déchargement **ne laisse pas de trace dans les conversations** : Sidekicks ouvre une
41+ session à part sur la connexion de l'agent, y envoie `/unload`, puis l'oublie. Ce que l'agent
42+ répond va dans son **Log**.
43+ - Le modèle est rechargé tout seul au message suivant (rien à faire côté Sidekicks).
44+ - La pression mémoire (#61) doit baisser dans les secondes qui suivent.
45+
46+ ## Critères d'acceptation
47+ - `agents` / `channels` : une méthode `Unload(agent)` qui refuse un agent arrêté, occupé, ou sans
48+ commande `unload` (erreurs explicites), sinon envoie `/unload` dans une session dédiée.
49+ - Service `AgentService.Unload(name)` et bindings ; `AgentService.List()` dit si l'agent sait se
50+ décharger (`canUnload`).
51+ - Interface : bouton, état grisé, erreur traduite ; textes EN + FR.
52+ - Tests Go avec un faux agent qui annonce (ou non) `unload` ; tests frontend.
53+ - Docs EN + FR : guide « Surveiller la pression mémoire » (que faire quand elle monte),
54+ référence `AgentService`, explication mémoire et modèles locaux (la convention `unload`).
55+
56+ ## À trancher
57+ - Mettre aussi le bouton dans le panneau Log, à côté de la pression mémoire (pour l'agent choisi) ?
58+ - Proposer un **Tout décharger** (tous les agents qui le permettent) quand la pression est élevée ?
59+tasks:
60+ - id: 1
61+ title: "Unload(agent) : session dédiée, envoi de /unload, refus si arrêté, occupé ou sans la commande"
62+ state: open
63+ priority: medium
64+ author:
65+ name: k33g
66+ email: ph.charriere@gmail.com
67+ createdAt: 2026-11-01T06:48:09.000Z
68+ - id: 2
69+ title: "AgentService.Unload et canUnload dans List(), bindings"
70+ state: open
71+ priority: medium
72+ author:
73+ name: k33g
74+ email: ph.charriere@gmail.com
75+ createdAt: 2026-11-01T06:48:09.000Z
76+ - id: 3
77+ title: "Interface : bouton ⏏ dans la liste Agents et la vue de l'agent"
78+ state: open
79+ priority: medium
80+ author:
81+ name: k33g
82+ email: ph.charriere@gmail.com
83+ createdAt: 2026-11-01T06:48:09.000Z
84+ - id: 4
85+ title: Tests et documentation EN + FR
86+ state: open
87+ priority: low
88+ author:
89+ name: k33g
90+ email: ph.charriere@gmail.com
91+ createdAt: 2026-11-01T06:48:09.000Z
1392 comments: []
@@ -1,13 +1,92 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 512 id: 51
3-title: Ajouter un bouton pour decharger le modele de la mémoire (quand c'est possible)3+title: Ajouter un bouton pour décharger le modèle de la mémoire (quand l'agent le permet)
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-31T07:10:06.981Z8 createdAt: 2026-10-31T07:10:06.981Z
9-updatedAt: 2026-10-31T07:10:06.981Z9+updatedAt: 2026-11-01T06:58:53.000Z
10-labels: []10+labels:
11-body: ""11+ - kind::feature
12-tasks: []12+ - area::acp
13+ - area::ui
14+ - citizen-ux
15+epic: Agents
16+related:
17+ - 61
18+body: |
19+ Libérer la mémoire prise par le modèle d'un agent sans l'arrêter, depuis l'interface, quand la
20+ pression mémoire (#61) monte.
21+
22+ ## Principe : c'est l'agent qui expose la capacité
23+ Sidekicks ne parle pas au serveur de modèles (llmman, Ollama…) : il ne sait pas quel modèle
24+ un agent utilise, ni où il est servi. C'est l'agent qui sait se décharger. mm le fait déjà :
25+ il annonce la commande `unload` (avec `compact` et `new`) dans `available_commands_update`,
26+ juste après `session/new` ([#24](.tickets/issues/0024-commandes-slash-exposees-par-les-agents.yaml)),
27+ et vérifie `/api/ps` avant d'envoyer `keep_alive: 0` (il ne charge jamais un modèle pour le décharger).
28+
29+ ACP n'a pas de méthode standard pour ça. Convention retenue : **un agent qui annonce une
30+ commande nommée `unload` sait décharger son modèle**. Sidekicks connaît déjà les commandes de
31+ chaque agent (`ChannelService.Commands(agent)`, gardées dans le store, demandées par `Prepare`).
32+ Plus tard, si ACP ou mm proposent une méthode d'extension (`_sidekicks/unload` ou une capacité
33+ dans `initialize`), elle pourra remplacer la commande sans changer l'interface.
34+
35+ ## Comportement attendu
36+ - Un bouton **⏏ Décharger le modèle** pour un agent qui a annoncé `unload`, dans les actions de
37+ la liste Agents et dans l'en-tête de la vue de l'agent ; absent pour les autres agents.
38+ - Actif seulement quand l'agent tourne et ne répond pas (prêt) ; grisé pendant une réponse.
39+ Un agent arrêté n'a rien à décharger depuis Sidekicks (c'est le rôle d'`afterStop`).
40+ - Le déchargement **ne laisse pas de trace dans les conversations** : Sidekicks ouvre une
41+ session à part sur la connexion de l'agent, y envoie `/unload`, puis l'oublie. Ce que l'agent
42+ répond va dans son **Log**.
43+ - Le modèle est rechargé tout seul au message suivant (rien à faire côté Sidekicks).
44+ - La pression mémoire (#61) doit baisser dans les secondes qui suivent.
45+
46+ ## Critères d'acceptation
47+ - `agents` / `channels` : une méthode `Unload(agent)` qui refuse un agent arrêté, occupé, ou sans
48+ commande `unload` (erreurs explicites), sinon envoie `/unload` dans une session dédiée.
49+ - Service `AgentService.Unload(name)` et bindings ; `AgentService.List()` dit si l'agent sait se
50+ décharger (`canUnload`).
51+ - Interface : bouton, état grisé, erreur traduite ; textes EN + FR.
52+ - Tests Go avec un faux agent qui annonce (ou non) `unload` ; tests frontend.
53+ - Docs EN + FR : guide « Surveiller la pression mémoire » (que faire quand elle monte),
54+ référence `AgentService`, explication mémoire et modèles locaux (la convention `unload`).
55+
56+ ## À trancher
57+ - Mettre aussi le bouton dans le panneau Log, à côté de la pression mémoire (pour l'agent choisi) ?
58+ - Proposer un **Tout décharger** (tous les agents qui le permettent) quand la pression est élevée ?
59+tasks:
60+ - id: 1
61+ title: "Unload(agent) : session dédiée, envoi de /unload, refus si arrêté, occupé ou sans la commande"
62+ state: open
63+ priority: medium
64+ author:
65+ name: k33g
66+ email: ph.charriere@gmail.com
67+ createdAt: 2026-11-01T06:48:09.000Z
68+ - id: 2
69+ title: "AgentService.Unload et canUnload dans List(), bindings"
70+ state: open
71+ priority: medium
72+ author:
73+ name: k33g
74+ email: ph.charriere@gmail.com
75+ createdAt: 2026-11-01T06:48:09.000Z
76+ - id: 3
77+ title: "Interface : bouton ⏏ dans la liste Agents et la vue de l'agent"
78+ state: open
79+ priority: medium
80+ author:
81+ name: k33g
82+ email: ph.charriere@gmail.com
83+ createdAt: 2026-11-01T06:48:09.000Z
84+ - id: 4
85+ title: Tests et documentation EN + FR
86+ state: open
87+ priority: low
88+ author:
89+ name: k33g
90+ email: ph.charriere@gmail.com
91+ createdAt: 2026-11-01T06:48:09.000Z
13 comments: []92 comments: []
modified .tickets/issues/0056-preparer-le-compte-apple-developer-certificat-de.yaml +17 -7
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 56
33 title: "Préparer le compte Apple Developer : certificat Developer ID et profil de notarisation"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z
9+updatedAt: 2026-10-31T22:25:00.000Z
1010 labels:
1111 - kind::chore
1212 - area::infra
@@ -25,42 +25,52 @@ body: |
2525 tasks:
2626 - id: 1
2727 title: "Relever le Team ID"
28- state: open
28+ state: closed
2929 priority: high
3030 author:
3131 name: k33g
3232 email: ph.charriere@gmail.com
3333 createdAt: 2026-10-31T11:03:40.000Z
34+ updatedAt: 2026-10-31T22:25:00.000Z
35+ closedAt: 2026-10-31T22:25:00.000Z
3436 - id: 2
3537 title: "Créer le certificat Developer ID Application"
36- state: open
38+ state: closed
3739 priority: high
3840 author:
3941 name: k33g
4042 email: ph.charriere@gmail.com
4143 createdAt: 2026-10-31T11:03:40.000Z
44+ updatedAt: 2026-10-31T22:25:00.000Z
45+ closedAt: 2026-10-31T22:25:00.000Z
4246 - id: 3
4347 title: "Sauvegarder le certificat et sa clé (.p12)"
44- state: open
48+ state: closed
4549 priority: high
4650 author:
4751 name: k33g
4852 email: ph.charriere@gmail.com
4953 createdAt: 2026-10-31T11:03:40.000Z
54+ updatedAt: 2026-10-31T22:25:00.000Z
55+ closedAt: 2026-10-31T22:25:00.000Z
5056 - id: 4
5157 title: "Créer le mot de passe d'application et le profil notarytool"
52- state: open
58+ state: closed
5359 priority: high
5460 author:
5561 name: k33g
5662 email: ph.charriere@gmail.com
5763 createdAt: 2026-10-31T11:03:40.000Z
64+ updatedAt: 2026-10-31T22:25:00.000Z
65+ closedAt: 2026-10-31T22:25:00.000Z
5866 - id: 5
5967 title: "Définir SIDEKICKS_SIGN_IDENTITY et SIDEKICKS_NOTARY_PROFILE"
60- state: open
68+ state: closed
6169 priority: medium
6270 author:
6371 name: k33g
6472 email: ph.charriere@gmail.com
6573 createdAt: 2026-10-31T11:03:40.000Z
74+ updatedAt: 2026-10-31T22:25:00.000Z
75+ closedAt: 2026-10-31T22:25:00.000Z
6676 comments: []
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 562 id: 56
3 title: "Préparer le compte Apple Developer : certificat Developer ID et profil de notarisation"3 title: "Préparer le compte Apple Developer : certificat Developer ID et profil de notarisation"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-31T11:03:40.000Z8 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z9+updatedAt: 2026-10-31T22:25:00.000Z
10 labels:10 labels:
11 - kind::chore11 - kind::chore
12 - area::infra12 - area::infra
@@ -25,42 +25,52 @@ body: |
25 tasks:25 tasks:
26 - id: 126 - id: 1
27 title: "Relever le Team ID"27 title: "Relever le Team ID"
28- state: open28+ state: closed
29 priority: high29 priority: high
30 author:30 author:
31 name: k33g31 name: k33g
32 email: ph.charriere@gmail.com32 email: ph.charriere@gmail.com
33 createdAt: 2026-10-31T11:03:40.000Z33 createdAt: 2026-10-31T11:03:40.000Z
34+ updatedAt: 2026-10-31T22:25:00.000Z
35+ closedAt: 2026-10-31T22:25:00.000Z
34 - id: 236 - id: 2
35 title: "Créer le certificat Developer ID Application"37 title: "Créer le certificat Developer ID Application"
36- state: open38+ state: closed
37 priority: high39 priority: high
38 author:40 author:
39 name: k33g41 name: k33g
40 email: ph.charriere@gmail.com42 email: ph.charriere@gmail.com
41 createdAt: 2026-10-31T11:03:40.000Z43 createdAt: 2026-10-31T11:03:40.000Z
44+ updatedAt: 2026-10-31T22:25:00.000Z
45+ closedAt: 2026-10-31T22:25:00.000Z
42 - id: 346 - id: 3
43 title: "Sauvegarder le certificat et sa clé (.p12)"47 title: "Sauvegarder le certificat et sa clé (.p12)"
44- state: open48+ state: closed
45 priority: high49 priority: high
46 author:50 author:
47 name: k33g51 name: k33g
48 email: ph.charriere@gmail.com52 email: ph.charriere@gmail.com
49 createdAt: 2026-10-31T11:03:40.000Z53 createdAt: 2026-10-31T11:03:40.000Z
54+ updatedAt: 2026-10-31T22:25:00.000Z
55+ closedAt: 2026-10-31T22:25:00.000Z
50 - id: 456 - id: 4
51 title: "Créer le mot de passe d'application et le profil notarytool"57 title: "Créer le mot de passe d'application et le profil notarytool"
52- state: open58+ state: closed
53 priority: high59 priority: high
54 author:60 author:
55 name: k33g61 name: k33g
56 email: ph.charriere@gmail.com62 email: ph.charriere@gmail.com
57 createdAt: 2026-10-31T11:03:40.000Z63 createdAt: 2026-10-31T11:03:40.000Z
64+ updatedAt: 2026-10-31T22:25:00.000Z
65+ closedAt: 2026-10-31T22:25:00.000Z
58 - id: 566 - id: 5
59 title: "Définir SIDEKICKS_SIGN_IDENTITY et SIDEKICKS_NOTARY_PROFILE"67 title: "Définir SIDEKICKS_SIGN_IDENTITY et SIDEKICKS_NOTARY_PROFILE"
60- state: open68+ state: closed
61 priority: medium69 priority: medium
62 author:70 author:
63 name: k33g71 name: k33g
64 email: ph.charriere@gmail.com72 email: ph.charriere@gmail.com
65 createdAt: 2026-10-31T11:03:40.000Z73 createdAt: 2026-10-31T11:03:40.000Z
74+ updatedAt: 2026-10-31T22:25:00.000Z
75+ closedAt: 2026-10-31T22:25:00.000Z
66 comments: []76 comments: []
modified .tickets/issues/0057-signer-notariser-et-publier-les-paquets-macos-de.yaml +20 -8
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 57
33 title: "Signer, notariser et publier les paquets macOS de la 1.0.0 (v1.0.0-macos)"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z
9+updatedAt: 2026-10-31T22:25:00.000Z
1010 labels:
1111 - kind::chore
1212 - area::infra
@@ -28,50 +28,62 @@ body: |
2828 tasks:
2929 - id: 1
3030 title: "Signer et notariser sidekicks.app"
31- state: open
31+ state: closed
3232 priority: high
3333 author:
3434 name: k33g
3535 email: ph.charriere@gmail.com
3636 createdAt: 2026-10-31T11:03:40.000Z
37+ updatedAt: 2026-10-31T22:25:00.000Z
38+ closedAt: 2026-10-31T22:25:00.000Z
3739 - id: 2
3840 title: "Refaire, signer et notariser le .dmg"
39- state: open
41+ state: closed
4042 priority: high
4143 author:
4244 name: k33g
4345 email: ph.charriere@gmail.com
4446 createdAt: 2026-10-31T11:03:40.000Z
47+ updatedAt: 2026-10-31T22:25:00.000Z
48+ closedAt: 2026-10-31T22:25:00.000Z
4549 - id: 3
4650 title: "Refaire le .zip et SHA256SUMS"
47- state: open
51+ state: closed
4852 priority: high
4953 author:
5054 name: k33g
5155 email: ph.charriere@gmail.com
5256 createdAt: 2026-10-31T11:03:40.000Z
57+ updatedAt: 2026-10-31T22:25:00.000Z
58+ closedAt: 2026-10-31T22:25:00.000Z
5359 - id: 4
5460 title: "Vérifier sur un autre Mac et tester l'app (runtime renforcé)"
55- state: open
61+ state: closed
5662 priority: high
5763 author:
5864 name: k33g
5965 email: ph.charriere@gmail.com
6066 createdAt: 2026-10-31T11:03:40.000Z
67+ updatedAt: 2026-10-31T22:25:00.000Z
68+ closedAt: 2026-10-31T22:25:00.000Z
6169 - id: 5
6270 title: "Publier la release v1.0.0-macos"
63- state: open
71+ state: closed
6472 priority: high
6573 author:
6674 name: k33g
6775 email: ph.charriere@gmail.com
6876 createdAt: 2026-10-31T11:03:40.000Z
77+ updatedAt: 2026-10-31T22:25:00.000Z
78+ closedAt: 2026-10-31T22:25:00.000Z
6979 - id: 6
7080 title: "Option : faire accepter les tags v<version>-<suffixe> au workflow Release"
71- state: open
81+ state: closed
7282 priority: low
7383 author:
7484 name: k33g
7585 email: ph.charriere@gmail.com
7686 createdAt: 2026-10-31T11:03:40.000Z
87+ updatedAt: 2026-10-31T22:25:00.000Z
88+ closedAt: 2026-10-31T22:25:00.000Z
7789 comments: []
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 572 id: 57
3 title: "Signer, notariser et publier les paquets macOS de la 1.0.0 (v1.0.0-macos)"3 title: "Signer, notariser et publier les paquets macOS de la 1.0.0 (v1.0.0-macos)"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-31T11:03:40.000Z8 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z9+updatedAt: 2026-10-31T22:25:00.000Z
10 labels:10 labels:
11 - kind::chore11 - kind::chore
12 - area::infra12 - area::infra
@@ -28,50 +28,62 @@ body: |
28 tasks:28 tasks:
29 - id: 129 - id: 1
30 title: "Signer et notariser sidekicks.app"30 title: "Signer et notariser sidekicks.app"
31- state: open31+ state: closed
32 priority: high32 priority: high
33 author:33 author:
34 name: k33g34 name: k33g
35 email: ph.charriere@gmail.com35 email: ph.charriere@gmail.com
36 createdAt: 2026-10-31T11:03:40.000Z36 createdAt: 2026-10-31T11:03:40.000Z
37+ updatedAt: 2026-10-31T22:25:00.000Z
38+ closedAt: 2026-10-31T22:25:00.000Z
37 - id: 239 - id: 2
38 title: "Refaire, signer et notariser le .dmg"40 title: "Refaire, signer et notariser le .dmg"
39- state: open41+ state: closed
40 priority: high42 priority: high
41 author:43 author:
42 name: k33g44 name: k33g
43 email: ph.charriere@gmail.com45 email: ph.charriere@gmail.com
44 createdAt: 2026-10-31T11:03:40.000Z46 createdAt: 2026-10-31T11:03:40.000Z
47+ updatedAt: 2026-10-31T22:25:00.000Z
48+ closedAt: 2026-10-31T22:25:00.000Z
45 - id: 349 - id: 3
46 title: "Refaire le .zip et SHA256SUMS"50 title: "Refaire le .zip et SHA256SUMS"
47- state: open51+ state: closed
48 priority: high52 priority: high
49 author:53 author:
50 name: k33g54 name: k33g
51 email: ph.charriere@gmail.com55 email: ph.charriere@gmail.com
52 createdAt: 2026-10-31T11:03:40.000Z56 createdAt: 2026-10-31T11:03:40.000Z
57+ updatedAt: 2026-10-31T22:25:00.000Z
58+ closedAt: 2026-10-31T22:25:00.000Z
53 - id: 459 - id: 4
54 title: "Vérifier sur un autre Mac et tester l'app (runtime renforcé)"60 title: "Vérifier sur un autre Mac et tester l'app (runtime renforcé)"
55- state: open61+ state: closed
56 priority: high62 priority: high
57 author:63 author:
58 name: k33g64 name: k33g
59 email: ph.charriere@gmail.com65 email: ph.charriere@gmail.com
60 createdAt: 2026-10-31T11:03:40.000Z66 createdAt: 2026-10-31T11:03:40.000Z
67+ updatedAt: 2026-10-31T22:25:00.000Z
68+ closedAt: 2026-10-31T22:25:00.000Z
61 - id: 569 - id: 5
62 title: "Publier la release v1.0.0-macos"70 title: "Publier la release v1.0.0-macos"
63- state: open71+ state: closed
64 priority: high72 priority: high
65 author:73 author:
66 name: k33g74 name: k33g
67 email: ph.charriere@gmail.com75 email: ph.charriere@gmail.com
68 createdAt: 2026-10-31T11:03:40.000Z76 createdAt: 2026-10-31T11:03:40.000Z
77+ updatedAt: 2026-10-31T22:25:00.000Z
78+ closedAt: 2026-10-31T22:25:00.000Z
69 - id: 679 - id: 6
70 title: "Option : faire accepter les tags v<version>-<suffixe> au workflow Release"80 title: "Option : faire accepter les tags v<version>-<suffixe> au workflow Release"
71- state: open81+ state: closed
72 priority: low82 priority: low
73 author:83 author:
74 name: k33g84 name: k33g
75 email: ph.charriere@gmail.com85 email: ph.charriere@gmail.com
76 createdAt: 2026-10-31T11:03:40.000Z86 createdAt: 2026-10-31T11:03:40.000Z
87+ updatedAt: 2026-10-31T22:25:00.000Z
88+ closedAt: 2026-10-31T22:25:00.000Z
77 comments: []89 comments: []
modified .tickets/issues/0058-automatiser-la-signature-et-la-notarisation-dans.yaml +10 -3
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 58
33 title: "Automatiser la signature et la notarisation dans make release-build"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z
9+updatedAt: 2026-10-31T22:25:00.000Z
1010 labels:
1111 - kind::feature
1212 - area::infra
@@ -57,4 +57,11 @@ tasks:
5757 name: k33g
5858 email: ph.charriere@gmail.com
5959 createdAt: 2026-10-31T11:03:40.000Z
60-comments: []
60+comments:
61+ - id: 1
62+ author:
63+ name: k33g
64+ email: ph.charriere@gmail.com
65+ createdAt: 2026-10-31T22:25:00.000Z
66+ body: |
67+ Fermé avec la milestone v1.0.0-macos sans avoir été fait : les tâches restent ouvertes. À rouvrir (ou à recréer) dans une prochaine milestone.
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 582 id: 58
3 title: "Automatiser la signature et la notarisation dans make release-build"3 title: "Automatiser la signature et la notarisation dans make release-build"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-31T11:03:40.000Z8 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z9+updatedAt: 2026-10-31T22:25:00.000Z
10 labels:10 labels:
11 - kind::feature11 - kind::feature
12 - area::infra12 - area::infra
@@ -57,4 +57,11 @@ tasks:
57 name: k33g57 name: k33g
58 email: ph.charriere@gmail.com58 email: ph.charriere@gmail.com
59 createdAt: 2026-10-31T11:03:40.000Z59 createdAt: 2026-10-31T11:03:40.000Z
60-comments: []60+comments:
61+ - id: 1
62+ author:
63+ name: k33g
64+ email: ph.charriere@gmail.com
65+ createdAt: 2026-10-31T22:25:00.000Z
66+ body: |
67+ Fermé avec la milestone v1.0.0-macos sans avoir été fait : les tâches restent ouvertes. À rouvrir (ou à recréer) dans une prochaine milestone.
modified .tickets/issues/0059-guide-d-installation-macos-plus-d-ouvrir-quand-m.yaml +10 -3
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 59
33 title: "Guide d'installation macOS : plus d'« Ouvrir quand même » pour une release signée"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z
9+updatedAt: 2026-10-31T22:25:00.000Z
1010 labels:
1111 - documentation
1212 - citizen-ux
@@ -27,4 +27,11 @@ tasks:
2727 name: k33g
2828 email: ph.charriere@gmail.com
2929 createdAt: 2026-10-31T11:03:40.000Z
30-comments: []
30+comments:
31+ - id: 1
32+ author:
33+ name: k33g
34+ email: ph.charriere@gmail.com
35+ createdAt: 2026-10-31T22:25:00.000Z
36+ body: |
37+ Fermé avec la milestone v1.0.0-macos sans avoir été fait : les tâches restent ouvertes. À rouvrir (ou à recréer) dans une prochaine milestone.
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 592 id: 59
3 title: "Guide d'installation macOS : plus d'« Ouvrir quand même » pour une release signée"3 title: "Guide d'installation macOS : plus d'« Ouvrir quand même » pour une release signée"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-31T11:03:40.000Z8 createdAt: 2026-10-31T11:03:40.000Z
9-updatedAt: 2026-10-31T11:03:40.000Z9+updatedAt: 2026-10-31T22:25:00.000Z
10 labels:10 labels:
11 - documentation11 - documentation
12 - citizen-ux12 - citizen-ux
@@ -27,4 +27,11 @@ tasks:
27 name: k33g27 name: k33g
28 email: ph.charriere@gmail.com28 email: ph.charriere@gmail.com
29 createdAt: 2026-10-31T11:03:40.000Z29 createdAt: 2026-10-31T11:03:40.000Z
30-comments: []30+comments:
31+ - id: 1
32+ author:
33+ name: k33g
34+ email: ph.charriere@gmail.com
35+ createdAt: 2026-10-31T22:25:00.000Z
36+ body: |
37+ Fermé avec la milestone v1.0.0-macos sans avoir été fait : les tâches restent ouvertes. À rouvrir (ou à recréer) dans une prochaine milestone.
added .tickets/issues/0060-supprimer-la-liste-des-messages-directs.yaml +64 -0
new file mode 100644
@@ -0,0 +1,64 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 60
3+title: Supprimer la liste des messages directs
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-31T22:25:00.000Z
9+updatedAt: 2026-10-31T22:36:11.000Z
10+labels:
11+ - kind::feature
12+ - area::ui
13+ - citizen-ux
14+milestone: "M6 · Better UI"
15+epic: GUI
16+body: |
17+ La section **Messages directs** du panel Channels
18+ ([ChannelsPanel.svelte](frontend/src/lib/panels/ChannelsPanel.svelte)) fait double emploi
19+ avec la liste **Agents** ([AgentsPanel.svelte](frontend/src/lib/panels/AgentsPanel.svelte)) :
20+ cliquer sur un agent ouvre déjà la conversation directe avec lui (`openDirect`), et l'agent
21+ de la conversation affichée y est surligné.
22+
23+ ## Critères d'acceptation
24+ - Le panel Channels ne montre plus que les channels (plus de titre « Messages directs », ni de message « aucune conversation directe »).
25+ - La liste Agents reste le seul point d'entrée des conversations directes ; l'agent de la conversation affichée est surligné.
26+ - Rien n'est perdu : les conversations directes et leur historique restent dans le store, et se rouvrent depuis la liste Agents.
27+ - Ce qui ne servait qu'à cette liste est retiré (`visibleDirects` si plus utilisé, clés i18n EN + FR), sans casser la palette de commandes ni les raccourcis.
28+ - Les tests frontend et la documentation EN + FR (tutoriel, guides, captures/wireframes qui montrent la section) sont mis à jour.
29+tasks:
30+ - id: 1
31+ title: Retirer la section du panel Channels et le code devenu inutile
32+ state: closed
33+ priority: medium
34+ author:
35+ name: k33g
36+ email: ph.charriere@gmail.com
37+ createdAt: 2026-10-31T22:25:00.000Z
38+ updatedAt: 2026-10-31T22:30:04.000Z
39+ closedAt: 2026-10-31T22:30:04.000Z
40+ - id: 2
41+ title: Mettre à jour les tests et la documentation EN + FR
42+ state: closed
43+ priority: medium
44+ author:
45+ name: k33g
46+ email: ph.charriere@gmail.com
47+ createdAt: 2026-10-31T22:25:00.000Z
48+ updatedAt: 2026-10-31T22:30:04.000Z
49+ closedAt: 2026-10-31T22:30:04.000Z
50+comments:
51+ - id: 1
52+ author:
53+ name: k33g
54+ email: ph.charriere@gmail.com
55+ createdAt: 2026-10-31T22:30:04.000Z
56+ body: |
57+ Fait : la section « Messages directs » est retirée du panel Channels
58+ ([ChannelsPanel.svelte](frontend/src/lib/panels/ChannelsPanel.svelte)). Pour ne pas perdre
59+ l'information qu'elle donnait, la liste Agents
60+ ([AgentsPanel.svelte](frontend/src/lib/panels/AgentsPanel.svelte)) affiche maintenant le nombre
61+ de réponses non lues de la conversation directe à côté de l'agent (masqué au survol, où
62+ apparaissent les boutons). `visibleDirects` est remplacé par `directOf` (testé), les clés
63+ `channels.section.direct` et `channels.noDirect` sont retirées. Docs EN + FR mises à jour
64+ (gérer les channels, tutoriel premier agent, ADR 0006). `make check` vert (118 tests frontend).
new file mode 100644
@@ -0,0 +1,64 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 60
3+title: Supprimer la liste des messages directs
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-31T22:25:00.000Z
9+updatedAt: 2026-10-31T22:36:11.000Z
10+labels:
11+ - kind::feature
12+ - area::ui
13+ - citizen-ux
14+milestone: "M6 · Better UI"
15+epic: GUI
16+body: |
17+ La section **Messages directs** du panel Channels
18+ ([ChannelsPanel.svelte](frontend/src/lib/panels/ChannelsPanel.svelte)) fait double emploi
19+ avec la liste **Agents** ([AgentsPanel.svelte](frontend/src/lib/panels/AgentsPanel.svelte)) :
20+ cliquer sur un agent ouvre déjà la conversation directe avec lui (`openDirect`), et l'agent
21+ de la conversation affichée y est surligné.
22+
23+ ## Critères d'acceptation
24+ - Le panel Channels ne montre plus que les channels (plus de titre « Messages directs », ni de message « aucune conversation directe »).
25+ - La liste Agents reste le seul point d'entrée des conversations directes ; l'agent de la conversation affichée est surligné.
26+ - Rien n'est perdu : les conversations directes et leur historique restent dans le store, et se rouvrent depuis la liste Agents.
27+ - Ce qui ne servait qu'à cette liste est retiré (`visibleDirects` si plus utilisé, clés i18n EN + FR), sans casser la palette de commandes ni les raccourcis.
28+ - Les tests frontend et la documentation EN + FR (tutoriel, guides, captures/wireframes qui montrent la section) sont mis à jour.
29+tasks:
30+ - id: 1
31+ title: Retirer la section du panel Channels et le code devenu inutile
32+ state: closed
33+ priority: medium
34+ author:
35+ name: k33g
36+ email: ph.charriere@gmail.com
37+ createdAt: 2026-10-31T22:25:00.000Z
38+ updatedAt: 2026-10-31T22:30:04.000Z
39+ closedAt: 2026-10-31T22:30:04.000Z
40+ - id: 2
41+ title: Mettre à jour les tests et la documentation EN + FR
42+ state: closed
43+ priority: medium
44+ author:
45+ name: k33g
46+ email: ph.charriere@gmail.com
47+ createdAt: 2026-10-31T22:25:00.000Z
48+ updatedAt: 2026-10-31T22:30:04.000Z
49+ closedAt: 2026-10-31T22:30:04.000Z
50+comments:
51+ - id: 1
52+ author:
53+ name: k33g
54+ email: ph.charriere@gmail.com
55+ createdAt: 2026-10-31T22:30:04.000Z
56+ body: |
57+ Fait : la section « Messages directs » est retirée du panel Channels
58+ ([ChannelsPanel.svelte](frontend/src/lib/panels/ChannelsPanel.svelte)). Pour ne pas perdre
59+ l'information qu'elle donnait, la liste Agents
60+ ([AgentsPanel.svelte](frontend/src/lib/panels/AgentsPanel.svelte)) affiche maintenant le nombre
61+ de réponses non lues de la conversation directe à côté de l'agent (masqué au survol, où
62+ apparaissent les boutons). `visibleDirects` est remplacé par `directOf` (testé), les clés
63+ `channels.section.direct` et `channels.noDirect` sont retirées. Docs EN + FR mises à jour
64+ (gérer les channels, tutoriel premier agent, ADR 0006). `make check` vert (118 tests frontend).
added .tickets/issues/0061-afficher-la-pression-memoire.yaml +84 -0
new file mode 100644
@@ -0,0 +1,84 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 61
3+title: Afficher la pression mémoire
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-31T22:25:00.000Z
9+updatedAt: 2026-10-31T22:51:01.000Z
10+labels:
11+ - kind::feature
12+ - area::ui
13+ - citizen-ux
14+milestone: "M6 · Better UI"
15+epic: GUI
16+related:
17+ - 51
18+body: |
19+ Les agents et leurs modèles locaux consomment beaucoup de mémoire (MacBook Air 32 Go : au-delà
20+ de 2 agents, la memory pressure peut arrêter la machine, voir #7 et #11). Afficher la pression
21+ mémoire de la machine **au même endroit que les logs** (panel du bas,
22+ [LogsPanel.svelte](frontend/src/lib/panels/LogsPanel.svelte)), pour voir quand il faut arrêter
23+ un agent ou décharger un modèle (#51).
24+
25+ ## Critères d'acceptation
26+ - Le panel des logs montre la pression mémoire (normale / élevée / critique, avec la couleur de Moniteur d'activité) et la mémoire utilisée / totale, rafraîchies régulièrement (quelques secondes) sans coût notable.
27+ - macOS : le niveau vient du système (`kern.memorystatus_vm_pressure_level` ou équivalent). Windows (et Linux en dev) : une approximation à partir de la mémoire disponible, documentée.
28+ - Si la mesure n'est pas disponible, rien ne s'affiche (pas d'erreur).
29+ - Un service Go (`internal/…`, testé) fournit la mesure ; l'interface la reçoit par un événement ou un appel lié.
30+ - Textes EN + FR ; documentation (référence de l'interface, guide) mise à jour.
31+tasks:
32+ - id: 1
33+ title: Mesurer la pression mémoire (macOS, Windows, Linux) dans un paquet Go testé
34+ state: closed
35+ priority: medium
36+ author:
37+ name: k33g
38+ email: ph.charriere@gmail.com
39+ createdAt: 2026-10-31T22:25:00.000Z
40+ updatedAt: 2026-10-31T22:40:25.000Z
41+ closedAt: 2026-10-31T22:40:25.000Z
42+ - id: 2
43+ title: L'afficher dans le panel des logs
44+ state: closed
45+ priority: medium
46+ author:
47+ name: k33g
48+ email: ph.charriere@gmail.com
49+ createdAt: 2026-10-31T22:25:00.000Z
50+ updatedAt: 2026-10-31T22:40:25.000Z
51+ closedAt: 2026-10-31T22:40:25.000Z
52+ - id: 3
53+ title: Documentation EN + FR
54+ state: closed
55+ priority: low
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-31T22:25:00.000Z
60+ updatedAt: 2026-10-31T22:40:25.000Z
61+ closedAt: 2026-10-31T22:40:25.000Z
62+comments:
63+ - id: 1
64+ author:
65+ name: k33g
66+ email: ph.charriere@gmail.com
67+ createdAt: 2026-10-31T22:40:25.000Z
68+ body: |
69+ Fait.
70+ - Go : paquet [internal/memory](internal/memory/memory.go). macOS : niveau de
71+ `kern.memorystatus_vm_pressure_level` (1/2/4), total `hw.memsize`, utilisée comptée comme
72+ Moniteur d'activité à partir de `vm_stat` (anonymes − purgeables + résidente + compressée).
73+ Linux : `/proc/meminfo`. Windows : `GlobalMemoryStatusEx`. Linux et Windows : niveau déduit
74+ de la mémoire disponible (élevée < 20 %, critique < 10 %). Pas de cgo ; `golang.org/x/sys`
75+ passe en dépendance directe. Tests des analyses et des niveaux.
76+ - Service `SystemService.Memory()` ([system.go](internal/app/system.go)) : un statut vide si la
77+ mesure échoue ; bindings régénérés.
78+ - Interface : [MemoryPressure.svelte](frontend/src/lib/panels/MemoryPressure.svelte) en haut à
79+ droite du panneau Log (point coloré, niveau, barre, « 18,2 / 32,0 Go utilisés », aide au
80+ survol), mesuré toutes les 5 s seulement quand le panneau est déplié et la fenêtre visible.
81+ Helpers testés dans `frontend/src/lib/memory.ts`. Textes EN + FR.
82+ - Docs : guide « Surveiller la pression mémoire » / « Watch the memory pressure », référence
83+ `SystemService`, README.
84+ `make check` vert (121 tests frontend) ; lint Go aussi vérifié pour GOOS=darwin et windows.
new file mode 100644
@@ -0,0 +1,84 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 61
3+title: Afficher la pression mémoire
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-31T22:25:00.000Z
9+updatedAt: 2026-10-31T22:51:01.000Z
10+labels:
11+ - kind::feature
12+ - area::ui
13+ - citizen-ux
14+milestone: "M6 · Better UI"
15+epic: GUI
16+related:
17+ - 51
18+body: |
19+ Les agents et leurs modèles locaux consomment beaucoup de mémoire (MacBook Air 32 Go : au-delà
20+ de 2 agents, la memory pressure peut arrêter la machine, voir #7 et #11). Afficher la pression
21+ mémoire de la machine **au même endroit que les logs** (panel du bas,
22+ [LogsPanel.svelte](frontend/src/lib/panels/LogsPanel.svelte)), pour voir quand il faut arrêter
23+ un agent ou décharger un modèle (#51).
24+
25+ ## Critères d'acceptation
26+ - Le panel des logs montre la pression mémoire (normale / élevée / critique, avec la couleur de Moniteur d'activité) et la mémoire utilisée / totale, rafraîchies régulièrement (quelques secondes) sans coût notable.
27+ - macOS : le niveau vient du système (`kern.memorystatus_vm_pressure_level` ou équivalent). Windows (et Linux en dev) : une approximation à partir de la mémoire disponible, documentée.
28+ - Si la mesure n'est pas disponible, rien ne s'affiche (pas d'erreur).
29+ - Un service Go (`internal/…`, testé) fournit la mesure ; l'interface la reçoit par un événement ou un appel lié.
30+ - Textes EN + FR ; documentation (référence de l'interface, guide) mise à jour.
31+tasks:
32+ - id: 1
33+ title: Mesurer la pression mémoire (macOS, Windows, Linux) dans un paquet Go testé
34+ state: closed
35+ priority: medium
36+ author:
37+ name: k33g
38+ email: ph.charriere@gmail.com
39+ createdAt: 2026-10-31T22:25:00.000Z
40+ updatedAt: 2026-10-31T22:40:25.000Z
41+ closedAt: 2026-10-31T22:40:25.000Z
42+ - id: 2
43+ title: L'afficher dans le panel des logs
44+ state: closed
45+ priority: medium
46+ author:
47+ name: k33g
48+ email: ph.charriere@gmail.com
49+ createdAt: 2026-10-31T22:25:00.000Z
50+ updatedAt: 2026-10-31T22:40:25.000Z
51+ closedAt: 2026-10-31T22:40:25.000Z
52+ - id: 3
53+ title: Documentation EN + FR
54+ state: closed
55+ priority: low
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-31T22:25:00.000Z
60+ updatedAt: 2026-10-31T22:40:25.000Z
61+ closedAt: 2026-10-31T22:40:25.000Z
62+comments:
63+ - id: 1
64+ author:
65+ name: k33g
66+ email: ph.charriere@gmail.com
67+ createdAt: 2026-10-31T22:40:25.000Z
68+ body: |
69+ Fait.
70+ - Go : paquet [internal/memory](internal/memory/memory.go). macOS : niveau de
71+ `kern.memorystatus_vm_pressure_level` (1/2/4), total `hw.memsize`, utilisée comptée comme
72+ Moniteur d'activité à partir de `vm_stat` (anonymes − purgeables + résidente + compressée).
73+ Linux : `/proc/meminfo`. Windows : `GlobalMemoryStatusEx`. Linux et Windows : niveau déduit
74+ de la mémoire disponible (élevée < 20 %, critique < 10 %). Pas de cgo ; `golang.org/x/sys`
75+ passe en dépendance directe. Tests des analyses et des niveaux.
76+ - Service `SystemService.Memory()` ([system.go](internal/app/system.go)) : un statut vide si la
77+ mesure échoue ; bindings régénérés.
78+ - Interface : [MemoryPressure.svelte](frontend/src/lib/panels/MemoryPressure.svelte) en haut à
79+ droite du panneau Log (point coloré, niveau, barre, « 18,2 / 32,0 Go utilisés », aide au
80+ survol), mesuré toutes les 5 s seulement quand le panneau est déplié et la fenêtre visible.
81+ Helpers testés dans `frontend/src/lib/memory.ts`. Textes EN + FR.
82+ - Docs : guide « Surveiller la pression mémoire » / « Watch the memory pressure », référence
83+ `SystemService`, README.
84+ `make check` vert (121 tests frontend) ; lint Go aussi vérifié pour GOOS=darwin et windows.
added .tickets/issues/0062-vider-l-historique-d-une-conversation-directe-ou.yaml +112 -0
new file mode 100644
@@ -0,0 +1,112 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 62
3+title: Vider l'historique d'une conversation directe ou d'un channel
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-31T22:51:13.000Z
9+updatedAt: 2026-11-01T06:36:00.000Z
10+labels:
11+ - kind::feature
12+ - area::ui
13+ - area::persistence
14+ - citizen-ux
15+milestone: "M6 · Better UI"
16+epic: GUI
17+related:
18+ - 60
19+body: |
20+ Pouvoir repartir d'une conversation vide sans supprimer le channel ni l'agent :
21+ - **conversation directe** : vider son historique (depuis l'en-tête de la conversation, ou la
22+ liste Agents, qui ouvre les conversations directes depuis #60) ;
23+ - **channel** : vider son historique en gardant son nom, sa description et ses membres
24+ (aujourd'hui on ne peut que le supprimer, ce qui efface tout).
25+
26+ Aujourd'hui seule la Danger zone (#52) efface tous les historiques d'un coup.
27+
28+ ## Critères d'acceptation
29+ - Une action **Vider l'historique** sur une conversation directe et sur un channel, avec une
30+ confirmation (« Effacer tous les messages de #channel ? Ses membres restent. »).
31+ - Les messages sont effacés du store Pebble (`internal/store` : supprimer les clés des messages du
32+ channel, garder le channel et ses membres) ; la conversation affichée se vide, le compteur de
33+ non lus repasse à 0.
34+ - Les agents repartent de zéro aussi : leurs sessions ACP du channel sont oubliées (nouvelle session
35+ au prochain message) et le contexte « non vu » est remis à zéro, pour qu'ils ne se souviennent
36+ pas de ce qui a été effacé.
37+ - Refusé (ou proposé après arrêt) si un agent est en train de répondre dans ce channel.
38+ - Un événement `channels:changed` (ou dédié) prévient les autres vues.
39+ - Tests Go (store, channels, service) et frontend ; textes EN + FR ; docs EN + FR (gérer les
40+ channels, référence `ChannelService`).
41+tasks:
42+ - id: 1
43+ title: "Store : effacer les messages d'un channel (garder channel et membres)"
44+ state: closed
45+ priority: medium
46+ author:
47+ name: k33g
48+ email: ph.charriere@gmail.com
49+ createdAt: 2026-10-31T22:51:13.000Z
50+ updatedAt: 2026-11-01T05:38:59.000Z
51+ closedAt: 2026-11-01T05:38:59.000Z
52+ - id: 2
53+ title: "Channels : oublier les sessions ACP et le contexte non vu, refuser pendant une réponse"
54+ state: closed
55+ priority: medium
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-31T22:51:13.000Z
60+ updatedAt: 2026-11-01T05:38:59.000Z
61+ closedAt: 2026-11-01T05:38:59.000Z
62+ - id: 3
63+ title: "Service ChannelService.Clear(id) et bindings"
64+ state: closed
65+ priority: medium
66+ author:
67+ name: k33g
68+ email: ph.charriere@gmail.com
69+ createdAt: 2026-10-31T22:51:13.000Z
70+ updatedAt: 2026-11-01T05:38:59.000Z
71+ closedAt: 2026-11-01T05:38:59.000Z
72+ - id: 4
73+ title: "Interface : action Vider l'historique (channel et conversation directe) avec confirmation"
74+ state: closed
75+ priority: medium
76+ author:
77+ name: k33g
78+ email: ph.charriere@gmail.com
79+ createdAt: 2026-10-31T22:51:13.000Z
80+ updatedAt: 2026-11-01T05:38:59.000Z
81+ closedAt: 2026-11-01T05:38:59.000Z
82+ - id: 5
83+ title: Tests et documentation EN + FR
84+ state: closed
85+ priority: low
86+ author:
87+ name: k33g
88+ email: ph.charriere@gmail.com
89+ createdAt: 2026-10-31T22:51:13.000Z
90+ updatedAt: 2026-11-01T05:38:59.000Z
91+ closedAt: 2026-11-01T05:38:59.000Z
92+comments:
93+ - id: 1
94+ author:
95+ name: k33g
96+ email: ph.charriere@gmail.com
97+ createdAt: 2026-11-01T05:38:59.000Z
98+ body: |
99+ Fait.
100+ - Store : `ClearMessages(id)` efface les clés `msg/<id>/` (channel, nom et membres gardés).
101+ - Channels : `Manager.Clear(id)` refuse avec `ErrBusy` si le channel répond ou a des messages
102+ en attente ; sinon efface les messages, vide `sessionId` / `lastSeen` des membres (et oublie
103+ leur connexion) : le tour suivant ouvre une nouvelle session, sans le passé effacé ; usage
104+ remis à zéro ; émet `channels:changed`.
105+ - Service `ChannelService.Clear(id)` et bindings.
106+ - Interface : bouton 🗑 « Vider l'historique » à droite de l'en-tête de la conversation
107+ (channel et conversation directe), visible dès qu'il y a des messages, avec confirmation
108+ Effacer / Annuler ; l'erreur « un agent répond » est traduite (`error.channelBusy`).
109+ - Tests : store, channels (refus pendant une réponse, membres gardés, nouvelle session qui ne
110+ connaît pas le passé), erreurs frontend. Docs EN + FR : gérer les channels, référence
111+ `ChannelService`.
112+ `make check` vert (122 tests frontend).
new file mode 100644
@@ -0,0 +1,112 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 62
3+title: Vider l'historique d'une conversation directe ou d'un channel
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-31T22:51:13.000Z
9+updatedAt: 2026-11-01T06:36:00.000Z
10+labels:
11+ - kind::feature
12+ - area::ui
13+ - area::persistence
14+ - citizen-ux
15+milestone: "M6 · Better UI"
16+epic: GUI
17+related:
18+ - 60
19+body: |
20+ Pouvoir repartir d'une conversation vide sans supprimer le channel ni l'agent :
21+ - **conversation directe** : vider son historique (depuis l'en-tête de la conversation, ou la
22+ liste Agents, qui ouvre les conversations directes depuis #60) ;
23+ - **channel** : vider son historique en gardant son nom, sa description et ses membres
24+ (aujourd'hui on ne peut que le supprimer, ce qui efface tout).
25+
26+ Aujourd'hui seule la Danger zone (#52) efface tous les historiques d'un coup.
27+
28+ ## Critères d'acceptation
29+ - Une action **Vider l'historique** sur une conversation directe et sur un channel, avec une
30+ confirmation (« Effacer tous les messages de #channel ? Ses membres restent. »).
31+ - Les messages sont effacés du store Pebble (`internal/store` : supprimer les clés des messages du
32+ channel, garder le channel et ses membres) ; la conversation affichée se vide, le compteur de
33+ non lus repasse à 0.
34+ - Les agents repartent de zéro aussi : leurs sessions ACP du channel sont oubliées (nouvelle session
35+ au prochain message) et le contexte « non vu » est remis à zéro, pour qu'ils ne se souviennent
36+ pas de ce qui a été effacé.
37+ - Refusé (ou proposé après arrêt) si un agent est en train de répondre dans ce channel.
38+ - Un événement `channels:changed` (ou dédié) prévient les autres vues.
39+ - Tests Go (store, channels, service) et frontend ; textes EN + FR ; docs EN + FR (gérer les
40+ channels, référence `ChannelService`).
41+tasks:
42+ - id: 1
43+ title: "Store : effacer les messages d'un channel (garder channel et membres)"
44+ state: closed
45+ priority: medium
46+ author:
47+ name: k33g
48+ email: ph.charriere@gmail.com
49+ createdAt: 2026-10-31T22:51:13.000Z
50+ updatedAt: 2026-11-01T05:38:59.000Z
51+ closedAt: 2026-11-01T05:38:59.000Z
52+ - id: 2
53+ title: "Channels : oublier les sessions ACP et le contexte non vu, refuser pendant une réponse"
54+ state: closed
55+ priority: medium
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-31T22:51:13.000Z
60+ updatedAt: 2026-11-01T05:38:59.000Z
61+ closedAt: 2026-11-01T05:38:59.000Z
62+ - id: 3
63+ title: "Service ChannelService.Clear(id) et bindings"
64+ state: closed
65+ priority: medium
66+ author:
67+ name: k33g
68+ email: ph.charriere@gmail.com
69+ createdAt: 2026-10-31T22:51:13.000Z
70+ updatedAt: 2026-11-01T05:38:59.000Z
71+ closedAt: 2026-11-01T05:38:59.000Z
72+ - id: 4
73+ title: "Interface : action Vider l'historique (channel et conversation directe) avec confirmation"
74+ state: closed
75+ priority: medium
76+ author:
77+ name: k33g
78+ email: ph.charriere@gmail.com
79+ createdAt: 2026-10-31T22:51:13.000Z
80+ updatedAt: 2026-11-01T05:38:59.000Z
81+ closedAt: 2026-11-01T05:38:59.000Z
82+ - id: 5
83+ title: Tests et documentation EN + FR
84+ state: closed
85+ priority: low
86+ author:
87+ name: k33g
88+ email: ph.charriere@gmail.com
89+ createdAt: 2026-10-31T22:51:13.000Z
90+ updatedAt: 2026-11-01T05:38:59.000Z
91+ closedAt: 2026-11-01T05:38:59.000Z
92+comments:
93+ - id: 1
94+ author:
95+ name: k33g
96+ email: ph.charriere@gmail.com
97+ createdAt: 2026-11-01T05:38:59.000Z
98+ body: |
99+ Fait.
100+ - Store : `ClearMessages(id)` efface les clés `msg/<id>/` (channel, nom et membres gardés).
101+ - Channels : `Manager.Clear(id)` refuse avec `ErrBusy` si le channel répond ou a des messages
102+ en attente ; sinon efface les messages, vide `sessionId` / `lastSeen` des membres (et oublie
103+ leur connexion) : le tour suivant ouvre une nouvelle session, sans le passé effacé ; usage
104+ remis à zéro ; émet `channels:changed`.
105+ - Service `ChannelService.Clear(id)` et bindings.
106+ - Interface : bouton 🗑 « Vider l'historique » à droite de l'en-tête de la conversation
107+ (channel et conversation directe), visible dès qu'il y a des messages, avec confirmation
108+ Effacer / Annuler ; l'erreur « un agent répond » est traduite (`error.channelBusy`).
109+ - Tests : store, channels (refus pendant une réponse, membres gardés, nouvelle session qui ne
110+ connaît pas le passé), erreurs frontend. Docs EN + FR : gérer les channels, référence
111+ `ChannelService`.
112+ `make check` vert (122 tests frontend).
added .tickets/issues/0063-la-liste-de-l-ne-defile-pas-avec-les-fleches.yaml +44 -0
new file mode 100644
@@ -0,0 +1,44 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 63
3+title: "La liste de @ ne défile pas avec les flèches"
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-11-01T06:48:57.000Z
9+updatedAt: 2026-11-01T06:54:37.000Z
10+labels:
11+ - bug
12+ - kind::bug
13+ - area::ui
14+ - citizen-ux
15+milestone: "M6 · Better UI"
16+epic: GUI
17+body: |
18+ Quand on tape `@` dans le champ de message, la liste des agents ne montre que les premiers :
19+ en descendant avec ↓, la sélection sort de la partie visible et la liste ne défile pas, on ne
20+ voit pas les autres agents.
21+
22+ Cause probable : la popup du [Composer](frontend/src/lib/chat/Composer.svelte) a une hauteur
23+ maximale (`max-height: 240px; overflow-y: auto`) mais l'élément sélectionné n'est pas ramené
24+ dans la partie visible quand il change. Les listes de `/` (commandes) et de `$` (fichiers)
25+ utilisent la même popup et ont sans doute le même problème.
26+
27+ ## Critères d'acceptation
28+ - Avec ↓ / ↑, l'élément sélectionné reste toujours visible : la liste défile (`scrollIntoView({ block: 'nearest' })`).
29+ - Tous les agents sont atteignables au clavier, et à la souris avec la molette ou la barre de défilement.
30+ - Même comportement pour les listes de `/` et de `$`.
31+ - Vérifié avec une configuration d'une quinzaine d'agents (`agents/` en a 14).
32+tasks: []
33+comments:
34+ - id: 1
35+ author:
36+ name: k33g
37+ email: ph.charriere@gmail.com
38+ createdAt: 2026-11-01T06:49:52.000Z
39+ body: |
40+ Corrigé dans [Composer.svelte](frontend/src/lib/chat/Composer.svelte) : une action
41+ `shownWhen` ramène l'élément sélectionné dans la partie visible
42+ (`scrollIntoView({ block: 'nearest' })`) à chaque déplacement, y compris quand ↓ repart du
43+ début ou ↑ de la fin. Une seule popup sert `@`, `/` et `$` : les trois listes sont corrigées.
44+ `make check` vert.
new file mode 100644
@@ -0,0 +1,44 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 63
3+title: "La liste de @ ne défile pas avec les flèches"
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-11-01T06:48:57.000Z
9+updatedAt: 2026-11-01T06:54:37.000Z
10+labels:
11+ - bug
12+ - kind::bug
13+ - area::ui
14+ - citizen-ux
15+milestone: "M6 · Better UI"
16+epic: GUI
17+body: |
18+ Quand on tape `@` dans le champ de message, la liste des agents ne montre que les premiers :
19+ en descendant avec ↓, la sélection sort de la partie visible et la liste ne défile pas, on ne
20+ voit pas les autres agents.
21+
22+ Cause probable : la popup du [Composer](frontend/src/lib/chat/Composer.svelte) a une hauteur
23+ maximale (`max-height: 240px; overflow-y: auto`) mais l'élément sélectionné n'est pas ramené
24+ dans la partie visible quand il change. Les listes de `/` (commandes) et de `$` (fichiers)
25+ utilisent la même popup et ont sans doute le même problème.
26+
27+ ## Critères d'acceptation
28+ - Avec ↓ / ↑, l'élément sélectionné reste toujours visible : la liste défile (`scrollIntoView({ block: 'nearest' })`).
29+ - Tous les agents sont atteignables au clavier, et à la souris avec la molette ou la barre de défilement.
30+ - Même comportement pour les listes de `/` et de `$`.
31+ - Vérifié avec une configuration d'une quinzaine d'agents (`agents/` en a 14).
32+tasks: []
33+comments:
34+ - id: 1
35+ author:
36+ name: k33g
37+ email: ph.charriere@gmail.com
38+ createdAt: 2026-11-01T06:49:52.000Z
39+ body: |
40+ Corrigé dans [Composer.svelte](frontend/src/lib/chat/Composer.svelte) : une action
41+ `shownWhen` ramène l'élément sélectionné dans la partie visible
42+ (`scrollIntoView({ block: 'nearest' })`) à chaque déplacement, y compris quand ↓ repart du
43+ début ou ↑ de la fin. Une seule popup sert `@`, `/` et `$` : les trois listes sont corrigées.
44+ `make check` vert.
added .tickets/issues/0064-publier-la-version-1-1-0.yaml +102 -0
new file mode 100644
@@ -0,0 +1,102 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 64
3+title: Publier la version 1.1.0
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-11-01T06:59:54.000Z
9+updatedAt: 2026-11-01T06:59:54.000Z
10+labels:
11+ - kind::chore
12+ - area::infra
13+milestone: "v1.1.0 · Release"
14+epic: Qualité & Release
15+related:
16+ - 58
17+body: |
18+ Publier Sidekicks 1.1.0 avec [◆ M6 · Better UI](.tickets/milestones.yaml#M6%20%C2%B7%20Better%20UI) :
19+ - [#60 — Supprimer la liste des messages directs](.tickets/issues/0060-supprimer-la-liste-des-messages-directs.yaml)
20+ - [#61 — Afficher la pression mémoire](.tickets/issues/0061-afficher-la-pression-memoire.yaml)
21+ - [#62 — Vider l'historique d'une conversation directe ou d'un channel](.tickets/issues/0062-vider-l-historique-d-une-conversation-directe-ou.yaml)
22+ - [#63 — La liste de @ ne défile pas avec les flèches](.tickets/issues/0063-la-liste-de-l-ne-defile-pas-avec-les-fleches.yaml)
23+
24+ En suivant [RELEASING.fr.md](RELEASING.fr.md). C'est la première release « normale » depuis la
25+ signature : tant que #58 (automatisation) n'est pas fait, les paquets macOS sont signés et
26+ notarisés à la main avec la partie B de
27+ [Signer et notariser sous macOS](docs/fr/how-to/signer-et-notariser-sous-macos.md),
28+ **kDrive en pause**.
29+
30+ ## Critères d'acceptation
31+ - `info.version` vaut `1.1.0`, les fichiers de build sont régénérés et `LSMinimumSystemVersion` vaut `13.0.0`.
32+ - `docs/release-notes/v1.1.0.md` et `v1.1.0.fr.md` décrivent les nouveautés de M6.
33+ - `make verify` est vert, tout est commité et poussé sur `main`.
34+ - Le `.dmg` et le `.zip` macOS sont signés, notarisés et agrafés (`spctl` : `accepted` · `Notarized Developer ID`) ; `SHA256SUMS` est recalculé.
35+ - Le tag `v1.1.0` est poussé, la release est publiée avec ses 5 fichiers, et le `.dmg` téléchargé correspond à `SHA256SUMS` et s'ouvre sans « Ouvrir quand même ».
36+ - `git fetch --tags` fait en local.
37+tasks:
38+ - id: 1
39+ title: "Fixer la version 1.1.0 et régénérer les fichiers de build (LSMinimumSystemVersion 13.0.0)"
40+ state: open
41+ priority: high
42+ author:
43+ name: k33g
44+ email: ph.charriere@gmail.com
45+ createdAt: 2026-11-01T06:59:54.000Z
46+ - id: 2
47+ title: "Écrire les notes de release v1.1.0 (EN + FR)"
48+ state: open
49+ priority: high
50+ author:
51+ name: k33g
52+ email: ph.charriere@gmail.com
53+ createdAt: 2026-11-01T06:59:54.000Z
54+ - id: 3
55+ title: "make verify, commit, push sur main"
56+ state: open
57+ priority: high
58+ author:
59+ name: k33g
60+ email: ph.charriere@gmail.com
61+ createdAt: 2026-11-01T06:59:54.000Z
62+ - id: 4
63+ title: "make release-build (kDrive en pause)"
64+ state: open
65+ priority: high
66+ author:
67+ name: k33g
68+ email: ph.charriere@gmail.com
69+ createdAt: 2026-11-01T06:59:54.000Z
70+ - id: 5
71+ title: "Signer, notariser et agrafer l'app et le .dmg, refaire le .zip et SHA256SUMS (guide, partie B)"
72+ state: open
73+ priority: high
74+ author:
75+ name: k33g
76+ email: ph.charriere@gmail.com
77+ createdAt: 2026-11-01T06:59:54.000Z
78+ - id: 6
79+ title: "Essayer le .dmg sur un autre Mac ou un autre compte"
80+ state: open
81+ priority: medium
82+ author:
83+ name: k33g
84+ email: ph.charriere@gmail.com
85+ createdAt: 2026-11-01T06:59:54.000Z
86+ - id: 7
87+ title: "Tagger v1.1.0, publier la release, vérifier le .dmg téléchargé"
88+ state: open
89+ priority: high
90+ author:
91+ name: k33g
92+ email: ph.charriere@gmail.com
93+ createdAt: 2026-11-01T06:59:54.000Z
94+ - id: 8
95+ title: "git fetch --tags"
96+ state: open
97+ priority: low
98+ author:
99+ name: k33g
100+ email: ph.charriere@gmail.com
101+ createdAt: 2026-11-01T06:59:54.000Z
102+comments: []
new file mode 100644
@@ -0,0 +1,102 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 64
3+title: Publier la version 1.1.0
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-11-01T06:59:54.000Z
9+updatedAt: 2026-11-01T06:59:54.000Z
10+labels:
11+ - kind::chore
12+ - area::infra
13+milestone: "v1.1.0 · Release"
14+epic: Qualité & Release
15+related:
16+ - 58
17+body: |
18+ Publier Sidekicks 1.1.0 avec [◆ M6 · Better UI](.tickets/milestones.yaml#M6%20%C2%B7%20Better%20UI) :
19+ - [#60 — Supprimer la liste des messages directs](.tickets/issues/0060-supprimer-la-liste-des-messages-directs.yaml)
20+ - [#61 — Afficher la pression mémoire](.tickets/issues/0061-afficher-la-pression-memoire.yaml)
21+ - [#62 — Vider l'historique d'une conversation directe ou d'un channel](.tickets/issues/0062-vider-l-historique-d-une-conversation-directe-ou.yaml)
22+ - [#63 — La liste de @ ne défile pas avec les flèches](.tickets/issues/0063-la-liste-de-l-ne-defile-pas-avec-les-fleches.yaml)
23+
24+ En suivant [RELEASING.fr.md](RELEASING.fr.md). C'est la première release « normale » depuis la
25+ signature : tant que #58 (automatisation) n'est pas fait, les paquets macOS sont signés et
26+ notarisés à la main avec la partie B de
27+ [Signer et notariser sous macOS](docs/fr/how-to/signer-et-notariser-sous-macos.md),
28+ **kDrive en pause**.
29+
30+ ## Critères d'acceptation
31+ - `info.version` vaut `1.1.0`, les fichiers de build sont régénérés et `LSMinimumSystemVersion` vaut `13.0.0`.
32+ - `docs/release-notes/v1.1.0.md` et `v1.1.0.fr.md` décrivent les nouveautés de M6.
33+ - `make verify` est vert, tout est commité et poussé sur `main`.
34+ - Le `.dmg` et le `.zip` macOS sont signés, notarisés et agrafés (`spctl` : `accepted` · `Notarized Developer ID`) ; `SHA256SUMS` est recalculé.
35+ - Le tag `v1.1.0` est poussé, la release est publiée avec ses 5 fichiers, et le `.dmg` téléchargé correspond à `SHA256SUMS` et s'ouvre sans « Ouvrir quand même ».
36+ - `git fetch --tags` fait en local.
37+tasks:
38+ - id: 1
39+ title: "Fixer la version 1.1.0 et régénérer les fichiers de build (LSMinimumSystemVersion 13.0.0)"
40+ state: open
41+ priority: high
42+ author:
43+ name: k33g
44+ email: ph.charriere@gmail.com
45+ createdAt: 2026-11-01T06:59:54.000Z
46+ - id: 2
47+ title: "Écrire les notes de release v1.1.0 (EN + FR)"
48+ state: open
49+ priority: high
50+ author:
51+ name: k33g
52+ email: ph.charriere@gmail.com
53+ createdAt: 2026-11-01T06:59:54.000Z
54+ - id: 3
55+ title: "make verify, commit, push sur main"
56+ state: open
57+ priority: high
58+ author:
59+ name: k33g
60+ email: ph.charriere@gmail.com
61+ createdAt: 2026-11-01T06:59:54.000Z
62+ - id: 4
63+ title: "make release-build (kDrive en pause)"
64+ state: open
65+ priority: high
66+ author:
67+ name: k33g
68+ email: ph.charriere@gmail.com
69+ createdAt: 2026-11-01T06:59:54.000Z
70+ - id: 5
71+ title: "Signer, notariser et agrafer l'app et le .dmg, refaire le .zip et SHA256SUMS (guide, partie B)"
72+ state: open
73+ priority: high
74+ author:
75+ name: k33g
76+ email: ph.charriere@gmail.com
77+ createdAt: 2026-11-01T06:59:54.000Z
78+ - id: 6
79+ title: "Essayer le .dmg sur un autre Mac ou un autre compte"
80+ state: open
81+ priority: medium
82+ author:
83+ name: k33g
84+ email: ph.charriere@gmail.com
85+ createdAt: 2026-11-01T06:59:54.000Z
86+ - id: 7
87+ title: "Tagger v1.1.0, publier la release, vérifier le .dmg téléchargé"
88+ state: open
89+ priority: high
90+ author:
91+ name: k33g
92+ email: ph.charriere@gmail.com
93+ createdAt: 2026-11-01T06:59:54.000Z
94+ - id: 8
95+ title: "git fetch --tags"
96+ state: open
97+ priority: low
98+ author:
99+ name: k33g
100+ email: ph.charriere@gmail.com
101+ createdAt: 2026-11-01T06:59:54.000Z
102+comments: []
modified .tickets/milestones.yaml +7 -1
@@ -31,5 +31,11 @@ milestones:
3131 state: closed
3232 description: "Version, vérifications, packaging (macOS, Windows), tag v1.0.0 et publication de la release, avec ce qu'apportent M5 et les milestones précédentes."
3333 - title: "v1.0.0-macos · Signature et notarisation"
34- state: open
34+ state: closed
3535 description: "Avec le compte Apple Developer payant : certificat Developer ID, notarisation, paquets macOS de la 1.0.0 signés et notarisés (plus d'« Ouvrir quand même »), puis automatisation pour les releases suivantes."
36+ - title: "M6 · Better UI"
37+ state: closed
38+ description: "Une interface plus simple : pas de liste en double pour les conversations directes, et la pression mémoire de la machine visible."
39+ - title: "v1.1.0 · Release"
40+ state: open
41+ description: "Version 1.1.0 avec ce qu'apporte M6 · Better UI (pression mémoire, vider un historique, plus de liste de messages directs, liste @ qui défile) : version, notes de release, vérifications, paquets macOS signés et notarisés et Windows, tag v1.1.0 et publication."
@@ -31,5 +31,11 @@ milestones:
31 state: closed31 state: closed
32 description: "Version, vérifications, packaging (macOS, Windows), tag v1.0.0 et publication de la release, avec ce qu'apportent M5 et les milestones précédentes."32 description: "Version, vérifications, packaging (macOS, Windows), tag v1.0.0 et publication de la release, avec ce qu'apportent M5 et les milestones précédentes."
33 - title: "v1.0.0-macos · Signature et notarisation"33 - title: "v1.0.0-macos · Signature et notarisation"
34- state: open34+ state: closed
35 description: "Avec le compte Apple Developer payant : certificat Developer ID, notarisation, paquets macOS de la 1.0.0 signés et notarisés (plus d'« Ouvrir quand même »), puis automatisation pour les releases suivantes."35 description: "Avec le compte Apple Developer payant : certificat Developer ID, notarisation, paquets macOS de la 1.0.0 signés et notarisés (plus d'« Ouvrir quand même »), puis automatisation pour les releases suivantes."
36+ - title: "M6 · Better UI"
37+ state: closed
38+ description: "Une interface plus simple : pas de liste en double pour les conversations directes, et la pression mémoire de la machine visible."
39+ - title: "v1.1.0 · Release"
40+ state: open
41+ description: "Version 1.1.0 avec ce qu'apporte M6 · Better UI (pression mémoire, vider un historique, plus de liste de messages directs, liste @ qui défile) : version, notes de release, vérifications, paquets macOS signés et notarisés et Windows, tag v1.1.0 et publication."
modified README.md +1 -0
@@ -52,6 +52,7 @@ Details: [How to develop the application](docs/en/how-to/develop-the-application
5252 | `internal/acp` | ACP v1 client over stdio (wraps `coder/acp-go-sdk`) |
5353 | `internal/config` | Configuration file: load, validate, locate, create, save, watch |
5454 | `internal/events` | Events sent from Go to the interface |
55+| `internal/memory` | The memory pressure of the computer, shown in the Log panel |
5556 | `internal/shellpath` | The `PATH` of the user's shell, for an application opened from the Finder |
5657 | `schema` | JSON Schema of the configuration file |
5758 | `frontend` | Svelte 5 interface: panels, shell, state |
@@ -52,6 +52,7 @@ Details: [How to develop the application](docs/en/how-to/develop-the-application
52 | `internal/acp` | ACP v1 client over stdio (wraps `coder/acp-go-sdk`) |52 | `internal/acp` | ACP v1 client over stdio (wraps `coder/acp-go-sdk`) |
53 | `internal/config` | Configuration file: load, validate, locate, create, save, watch |53 | `internal/config` | Configuration file: load, validate, locate, create, save, watch |
54 | `internal/events` | Events sent from Go to the interface |54 | `internal/events` | Events sent from Go to the interface |
55+| `internal/memory` | The memory pressure of the computer, shown in the Log panel |
55 | `internal/shellpath` | The `PATH` of the user's shell, for an application opened from the Finder |56 | `internal/shellpath` | The `PATH` of the user's shell, for an application opened from the Finder |
56 | `schema` | JSON Schema of the configuration file |57 | `schema` | JSON Schema of the configuration file |
57 | `frontend` | Svelte 5 interface: panels, shell, state |58 | `frontend` | Svelte 5 interface: panels, shell, state |
added agents/khan/agent.yaml +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+# Khan — a coding agent: reads and changes the project, runs commands.
2+#
3+# Written by `mm init`. Every value says why it has that value; change
4+# them as you watch the agent work (guides/agent-settings-for-small-models.md).
5+#
6+# model family: gpt-oss
7+# usage: developer
8+# machine: small — a laptop, models up to about 8B
9+# model facts: read from llmman: native window 131072 tokens; reasoning switch reasoning_effort (default on)
10+#
11+# Where the numbers come from (the tag before their comment):
12+# [measured] measured in a demo of mm — re-measure after a model or quant change
13+# [guide] the settings guide's row for this machine
14+# [rule] a rule of thumb, not specific to this model
15+#
16+# mm -config <this file> or mm -tui <this file>
17+
18+provider: llmman
19+# The model. Chosen for it (family gpt-oss): sampling (temperature, top_p,
20+# max_tokens) and sampling.chat_template_kwargs (reasoning_effort). Change the
21+# model and redo them together — running mm init again is the quick way.
22+model: "hf.co/unsloth/gpt-oss-20b-GGUF:Q4_K_M"
23+# No baseUrl: llmman's defaults apply. For the record:
24+# baseUrl: "http://127.0.0.1:17434/v1"
25+# fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer
26+
27+# 0 = mm asks the server for the window it serves (the model's native window
28+# is 131072 tokens; the server may serve less)
29+contextWindow: 0
30+# your answer: tools run without asking
31+yolo: true
32+# usage developer
33+bashTool: true
34+editTools: true
35+planTool: true
36+askUserTool: true
37+# no skills: add a skills/ folder and set skillsDir to use some (see
38+# docs/en/how-to/add-a-skill.md)
39+skillsDir: no-skills
40+# usage developer
41+maxTurns: 40
42+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
43+maxOutput: 8000
44+firstTokenTimeout: 3m
45+# [guide] aligned on bashTimeout, as the settings guide says when bash is on
46+# (section 3)
47+watchdogTimeout: 30s
48+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
49+bashTimeout: 30s
50+
51+sampling:
52+ # [measured] family gpt-oss: demos/codex-style (gpt-oss 20B: 1.0 / 1.0,
53+ # harmony format, reasoning_effort low | medium | high, default medium) —
54+ # the model always reasons: never a low temperature
55+ temperature: 1.0
56+ top_p: 1.0
57+ # [rule] reasoning on: room for the thoughts plus a whole tool call
58+ max_tokens: 16384
59+ # one tool call per reply: small models run a check before the change it
60+ # checks has landed otherwise
61+ parallel_tool_calls: false
62+ chat_template_kwargs:
63+ # one of medium (read from the chat template); a value the template does
64+ # not know may make every request fail
65+ reasoning_effort: medium
66+
67+context:
68+ # compress the history before the window is full (settings guide, every row)
69+ enabled: true
70+ # mm's default: 75 % of the window the server serves, a quarter left for the
71+ # next question and its tool outputs
72+ threshold: 75
73+
74+agentsMd:
75+ # An AGENTS.md in the project directory is added to the system prompt.
76+ enabled: true
77+
78+acp:
79+ name: Khan
80+ title: "Khan (developer)"
81+
82+system: |
83+ You are Khan, a coding agent working in a terminal, in the user's project.
84+ You have a bash tool, file tools (read_file, write_file, edit_file) and a
85+ plan tool.
86+ Read before you change: look at the files and run the commands that tell
87+ you how the project works. Make the smallest change that does the job,
88+ then run the tests or the build to check it.
89+ Never state the output of a command you did not run in this answer.
90+ When a task has several steps, make a plan and update it as you go.
91+
92+# Next: skills (docs/en/how-to/add-a-skill.md), MCP servers (docs/en/tutorials/mcp-getting-started.md),
93+# hooks (docs/en/how-to/guard-the-agent-with-hooks.md), sessions (docs/en/how-to/keep-sessions.md).
new file mode 100644
@@ -0,0 +1,93 @@
1+# Khan — a coding agent: reads and changes the project, runs commands.
2+#
3+# Written by `mm init`. Every value says why it has that value; change
4+# them as you watch the agent work (guides/agent-settings-for-small-models.md).
5+#
6+# model family: gpt-oss
7+# usage: developer
8+# machine: small — a laptop, models up to about 8B
9+# model facts: read from llmman: native window 131072 tokens; reasoning switch reasoning_effort (default on)
10+#
11+# Where the numbers come from (the tag before their comment):
12+# [measured] measured in a demo of mm — re-measure after a model or quant change
13+# [guide] the settings guide's row for this machine
14+# [rule] a rule of thumb, not specific to this model
15+#
16+# mm -config <this file> or mm -tui <this file>
17+
18+provider: llmman
19+# The model. Chosen for it (family gpt-oss): sampling (temperature, top_p,
20+# max_tokens) and sampling.chat_template_kwargs (reasoning_effort). Change the
21+# model and redo them together — running mm init again is the quick way.
22+model: "hf.co/unsloth/gpt-oss-20b-GGUF:Q4_K_M"
23+# No baseUrl: llmman's defaults apply. For the record:
24+# baseUrl: "http://127.0.0.1:17434/v1"
25+# fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer
26+
27+# 0 = mm asks the server for the window it serves (the model's native window
28+# is 131072 tokens; the server may serve less)
29+contextWindow: 0
30+# your answer: tools run without asking
31+yolo: true
32+# usage developer
33+bashTool: true
34+editTools: true
35+planTool: true
36+askUserTool: true
37+# no skills: add a skills/ folder and set skillsDir to use some (see
38+# docs/en/how-to/add-a-skill.md)
39+skillsDir: no-skills
40+# usage developer
41+maxTurns: 40
42+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
43+maxOutput: 8000
44+firstTokenTimeout: 3m
45+# [guide] aligned on bashTimeout, as the settings guide says when bash is on
46+# (section 3)
47+watchdogTimeout: 30s
48+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
49+bashTimeout: 30s
50+
51+sampling:
52+ # [measured] family gpt-oss: demos/codex-style (gpt-oss 20B: 1.0 / 1.0,
53+ # harmony format, reasoning_effort low | medium | high, default medium) —
54+ # the model always reasons: never a low temperature
55+ temperature: 1.0
56+ top_p: 1.0
57+ # [rule] reasoning on: room for the thoughts plus a whole tool call
58+ max_tokens: 16384
59+ # one tool call per reply: small models run a check before the change it
60+ # checks has landed otherwise
61+ parallel_tool_calls: false
62+ chat_template_kwargs:
63+ # one of medium (read from the chat template); a value the template does
64+ # not know may make every request fail
65+ reasoning_effort: medium
66+
67+context:
68+ # compress the history before the window is full (settings guide, every row)
69+ enabled: true
70+ # mm's default: 75 % of the window the server serves, a quarter left for the
71+ # next question and its tool outputs
72+ threshold: 75
73+
74+agentsMd:
75+ # An AGENTS.md in the project directory is added to the system prompt.
76+ enabled: true
77+
78+acp:
79+ name: Khan
80+ title: "Khan (developer)"
81+
82+system: |
83+ You are Khan, a coding agent working in a terminal, in the user's project.
84+ You have a bash tool, file tools (read_file, write_file, edit_file) and a
85+ plan tool.
86+ Read before you change: look at the files and run the commands that tell
87+ you how the project works. Make the smallest change that does the job,
88+ then run the tests or the build to check it.
89+ Never state the output of a command you did not run in this answer.
90+ When a task has several steps, make a plan and update it as you go.
91+
92+# Next: skills (docs/en/how-to/add-a-skill.md), MCP servers (docs/en/tutorials/mcp-getting-started.md),
93+# hooks (docs/en/how-to/guard-the-agent-with-hooks.md), sessions (docs/en/how-to/keep-sessions.md).
added agents/khan/gpt-oss-20b +0 -0
new file mode 100644
new file mode 100644
added agents/kyle/LFM2-24B-A2B +0 -0
new file mode 100644
new file mode 100644
added agents/kyle/agent.yaml +92 -0
new file mode 100644
@@ -0,0 +1,92 @@
1+# Kyle — a coding agent: reads and changes the project, runs commands.
2+#
3+# Written by `mm init`. Every value says why it has that value; change
4+# them as you watch the agent work (guides/agent-settings-for-small-models.md).
5+#
6+# model family: other
7+# usage: developer
8+# machine: small — a laptop, models up to about 8B
9+# model facts: read from llmman: native window 128000 tokens
10+#
11+# Where the numbers come from (the tag before their comment):
12+# [guide] the settings guide's row for this machine
13+# [rule] a rule of thumb, not specific to this model
14+#
15+# mm -config <this file> or mm -tui <this file>
16+
17+provider: llmman
18+# The model. Chosen for it (family other): sampling (temperature, top_p,
19+# max_tokens). Change the model and redo them together — running mm init
20+# again is the quick way.
21+model: "hf.co/LiquidAI/LFM2-24B-A2B-GGUF:Q4_K_M"
22+# No baseUrl: llmman's defaults apply. For the record:
23+# baseUrl: "http://127.0.0.1:17434/v1"
24+# fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer
25+
26+# 0 = mm asks the server for the window it serves (the model's native window
27+# is 128000 tokens; the server may serve less)
28+contextWindow: 0
29+# your answer: tools run without asking
30+yolo: true
31+# usage developer
32+bashTool: true
33+editTools: true
34+planTool: true
35+askUserTool: true
36+# no skills: add a skills/ folder and set skillsDir to use some (see
37+# docs/en/how-to/add-a-skill.md)
38+skillsDir: no-skills
39+# usage developer
40+maxTurns: 40
41+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
42+maxOutput: 8000
43+firstTokenTimeout: 3m
44+# [guide] aligned on bashTimeout, as the settings guide says when bash is on
45+# (section 3)
46+watchdogTimeout: 30s
47+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
48+bashTimeout: 30s
49+
50+sampling:
51+ # [rule] precise temperature for a developer agent without reasoning (family
52+ # other)
53+ temperature: 0.2
54+ # [rule] family other: skills/create-agent rule when nothing is published:
55+ # 0.7 / 0.9, 0.2 for a code agent without reasoning
56+ top_p: 0.9
57+ # [rule] room for an answer or a whole file in one tool call
58+ max_tokens: 4096
59+ # one tool call per reply: small models run a check before the change it
60+ # checks has landed otherwise
61+ parallel_tool_calls: false
62+
63+context:
64+ # compress the history before the window is full (settings guide, every row)
65+ enabled: true
66+ # mm's default: 75 % of the window the server serves, a quarter left for the
67+ # next question and its tool outputs
68+ threshold: 75
69+
70+agentsMd:
71+ # An AGENTS.md in the project directory is added to the system prompt.
72+ enabled: true
73+
74+acp:
75+ name: Kyle
76+ title: "Kyle (developer)"
77+
78+system: |
79+ You are Kyle, a coding agent working in a terminal, in the user's project.
80+ You have a bash tool, file tools (read_file, write_file, edit_file) and a
81+ plan tool.
82+ Read before you change: look at the files and run the commands that tell
83+ you how the project works. Make the smallest change that does the job,
84+ then run the tests or the build to check it.
85+ Never state the output of a command you did not run in this answer.
86+ When a task has several steps, make a plan and update it as you go.
87+
88+# The family of this model is not in mm's table: the sampling is a generic
89+# starting point — check the model card.
90+
91+# Next: skills (docs/en/how-to/add-a-skill.md), MCP servers (docs/en/tutorials/mcp-getting-started.md),
92+# hooks (docs/en/how-to/guard-the-agent-with-hooks.md), sessions (docs/en/how-to/keep-sessions.md).
new file mode 100644
@@ -0,0 +1,92 @@
1+# Kyle — a coding agent: reads and changes the project, runs commands.
2+#
3+# Written by `mm init`. Every value says why it has that value; change
4+# them as you watch the agent work (guides/agent-settings-for-small-models.md).
5+#
6+# model family: other
7+# usage: developer
8+# machine: small — a laptop, models up to about 8B
9+# model facts: read from llmman: native window 128000 tokens
10+#
11+# Where the numbers come from (the tag before their comment):
12+# [guide] the settings guide's row for this machine
13+# [rule] a rule of thumb, not specific to this model
14+#
15+# mm -config <this file> or mm -tui <this file>
16+
17+provider: llmman
18+# The model. Chosen for it (family other): sampling (temperature, top_p,
19+# max_tokens). Change the model and redo them together — running mm init
20+# again is the quick way.
21+model: "hf.co/LiquidAI/LFM2-24B-A2B-GGUF:Q4_K_M"
22+# No baseUrl: llmman's defaults apply. For the record:
23+# baseUrl: "http://127.0.0.1:17434/v1"
24+# fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer
25+
26+# 0 = mm asks the server for the window it serves (the model's native window
27+# is 128000 tokens; the server may serve less)
28+contextWindow: 0
29+# your answer: tools run without asking
30+yolo: true
31+# usage developer
32+bashTool: true
33+editTools: true
34+planTool: true
35+askUserTool: true
36+# no skills: add a skills/ folder and set skillsDir to use some (see
37+# docs/en/how-to/add-a-skill.md)
38+skillsDir: no-skills
39+# usage developer
40+maxTurns: 40
41+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
42+maxOutput: 8000
43+firstTokenTimeout: 3m
44+# [guide] aligned on bashTimeout, as the settings guide says when bash is on
45+# (section 3)
46+watchdogTimeout: 30s
47+# [guide] settings guide, row "Small model (≤ 8B), short tasks"
48+bashTimeout: 30s
49+
50+sampling:
51+ # [rule] precise temperature for a developer agent without reasoning (family
52+ # other)
53+ temperature: 0.2
54+ # [rule] family other: skills/create-agent rule when nothing is published:
55+ # 0.7 / 0.9, 0.2 for a code agent without reasoning
56+ top_p: 0.9
57+ # [rule] room for an answer or a whole file in one tool call
58+ max_tokens: 4096
59+ # one tool call per reply: small models run a check before the change it
60+ # checks has landed otherwise
61+ parallel_tool_calls: false
62+
63+context:
64+ # compress the history before the window is full (settings guide, every row)
65+ enabled: true
66+ # mm's default: 75 % of the window the server serves, a quarter left for the
67+ # next question and its tool outputs
68+ threshold: 75
69+
70+agentsMd:
71+ # An AGENTS.md in the project directory is added to the system prompt.
72+ enabled: true
73+
74+acp:
75+ name: Kyle
76+ title: "Kyle (developer)"
77+
78+system: |
79+ You are Kyle, a coding agent working in a terminal, in the user's project.
80+ You have a bash tool, file tools (read_file, write_file, edit_file) and a
81+ plan tool.
82+ Read before you change: look at the files and run the commands that tell
83+ you how the project works. Make the smallest change that does the job,
84+ then run the tests or the build to check it.
85+ Never state the output of a command you did not run in this answer.
86+ When a task has several steps, make a plan and update it as you go.
87+
88+# The family of this model is not in mm's table: the sampling is a generic
89+# starting point — check the model card.
90+
91+# Next: skills (docs/en/how-to/add-a-skill.md), MCP servers (docs/en/tutorials/mcp-getting-started.md),
92+# hooks (docs/en/how-to/guard-the-agent-with-hooks.md), sessions (docs/en/how-to/keep-sessions.md).
modified agents/riker/agent.yaml +18 -23
@@ -3,32 +3,32 @@
33 # Written by `mm init`. Every value says why it has that value; change
44 # them as you watch the agent work (guides/agent-settings-for-small-models.md).
55 #
6-# model family: qwen
6+# model family: mellum
77 # usage: developer
88 # machine: small — a laptop, models up to about 8B
9-# model facts: read from llmman: native window 262144 tokens; reasoning switch enable_thinking (default off)
9+# model facts: read from llmman: native window 131072 tokens
1010 #
1111 # Where the numbers come from (the tag before their comment):
12-# [card] from the model card or the GGUF, not measured — re-read the new card
12+# [measured] measured in a demo of mm — re-measure after a model or quant change
1313 # [guide] the settings guide's row for this machine
1414 # [rule] a rule of thumb, not specific to this model
1515 #
1616 # mm -config <this file> or mm -tui <this file>
1717
1818 provider: llmman
19-# The model. Chosen for it (family qwen): sampling (temperature, top_p, top_k,
20-# max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the
21-# model and redo them together — running mm init again is the quick way.
22-model: "hf.co/unsloth/Qwen3.5-2B-GGUF:Q4_K_M"
19+# The model. Chosen for it (family mellum): sampling (temperature, top_p,
20+# max_tokens). Change the model and redo them together — running mm init
21+# again is the quick way.
22+model: "hf.co/JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M"
2323 # No baseUrl: llmman's defaults apply. For the record:
2424 # baseUrl: "http://127.0.0.1:17434/v1"
2525 # fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer
2626
2727 # 0 = mm asks the server for the window it serves (the model's native window
28-# is 262144 tokens; the server may serve less)
28+# is 131072 tokens; the server may serve less)
2929 contextWindow: 0
30-# your answer: every command and file change waits for your answer
31-yolo: false
30+# your answer: tools run without asking
31+yolo: true
3232 # usage developer
3333 bashTool: true
3434 editTools: true
@@ -49,22 +49,17 @@ watchdogTimeout: 30s
4949 bashTimeout: 30s
5050
5151 sampling:
52- # [card] family qwen: Qwen model cards (thinking 0.6 / 0.95 / 20,
53- # non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B,
54- # thinking off (demos/docs-search, memory-search, plans)
55- temperature: 0.7
56- top_p: 0.8
57- top_k: 20
58- # [rule] room for a long answer plus a whole tool call
59- max_tokens: 16384
52+ # [rule] precise temperature for a developer agent without reasoning (family
53+ # mellum)
54+ temperature: 0.0
55+ # [measured] family mellum: demos/little-coder (Mellum2 12B Instruct,
56+ # measured 0.2 / 0.9); agent.dmr.yaml (0.0 / 0.9)
57+ top_p: 0.9
58+ # [rule] room for an answer or a whole file in one tool call
59+ max_tokens: 4096
6060 # one tool call per reply: small models run a check before the change it
6161 # checks has landed otherwise
6262 parallel_tool_calls: false
63- chat_template_kwargs:
64- # reasoning off (the template's default): with it on, Qwen3.5 2B often
65- # writes its final answer in the reasoning after a tool call and returns
66- # an empty answer ("mm: empty answer"); measured 2/2 on, 3/3 fine off
67- enable_thinking: false
6863
6964 context:
7065 # compress the history before the window is full (settings guide, every row)
@@ -3,32 +3,32 @@
3 # Written by `mm init`. Every value says why it has that value; change3 # Written by `mm init`. Every value says why it has that value; change
4 # them as you watch the agent work (guides/agent-settings-for-small-models.md).4 # them as you watch the agent work (guides/agent-settings-for-small-models.md).
5 #5 #
6-# model family: qwen6+# model family: mellum
7 # usage: developer7 # usage: developer
8 # machine: small — a laptop, models up to about 8B8 # machine: small — a laptop, models up to about 8B
9-# model facts: read from llmman: native window 262144 tokens; reasoning switch enable_thinking (default off)9+# model facts: read from llmman: native window 131072 tokens
10 #10 #
11 # Where the numbers come from (the tag before their comment):11 # Where the numbers come from (the tag before their comment):
12-# [card] from the model card or the GGUF, not measured — re-read the new card12+# [measured] measured in a demo of mm — re-measure after a model or quant change
13 # [guide] the settings guide's row for this machine13 # [guide] the settings guide's row for this machine
14 # [rule] a rule of thumb, not specific to this model14 # [rule] a rule of thumb, not specific to this model
15 #15 #
16 # mm -config <this file> or mm -tui <this file>16 # mm -config <this file> or mm -tui <this file>
17 17
18 provider: llmman18 provider: llmman
19-# The model. Chosen for it (family qwen): sampling (temperature, top_p, top_k,19+# The model. Chosen for it (family mellum): sampling (temperature, top_p,
20-# max_tokens) and sampling.chat_template_kwargs (enable_thinking). Change the20+# max_tokens). Change the model and redo them together — running mm init
21-# model and redo them together — running mm init again is the quick way.21+# again is the quick way.
22-model: "hf.co/unsloth/Qwen3.5-2B-GGUF:Q4_K_M"22+model: "hf.co/JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M"
23 # No baseUrl: llmman's defaults apply. For the record:23 # No baseUrl: llmman's defaults apply. For the record:
24 # baseUrl: "http://127.0.0.1:17434/v1"24 # baseUrl: "http://127.0.0.1:17434/v1"
25 # fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer25 # fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer
26 26
27 # 0 = mm asks the server for the window it serves (the model's native window27 # 0 = mm asks the server for the window it serves (the model's native window
28-# is 262144 tokens; the server may serve less)28+# is 131072 tokens; the server may serve less)
29 contextWindow: 029 contextWindow: 0
30-# your answer: every command and file change waits for your answer30+# your answer: tools run without asking
31-yolo: false31+yolo: true
32 # usage developer32 # usage developer
33 bashTool: true33 bashTool: true
34 editTools: true34 editTools: true
@@ -49,22 +49,17 @@ watchdogTimeout: 30s
49 bashTimeout: 30s49 bashTimeout: 30s
50 50
51 sampling:51 sampling:
52- # [card] family qwen: Qwen model cards (thinking 0.6 / 0.95 / 20,52+ # [rule] precise temperature for a developer agent without reasoning (family
53- # non-thinking 0.7 / 0.8 / 20); precise 0.0 measured with Qwen3.5 4B,53+ # mellum)
54- # thinking off (demos/docs-search, memory-search, plans)54+ temperature: 0.0
55- temperature: 0.755+ # [measured] family mellum: demos/little-coder (Mellum2 12B Instruct,
56- top_p: 0.856+ # measured 0.2 / 0.9); agent.dmr.yaml (0.0 / 0.9)
57- top_k: 2057+ top_p: 0.9
58- # [rule] room for a long answer plus a whole tool call58+ # [rule] room for an answer or a whole file in one tool call
59- max_tokens: 1638459+ max_tokens: 4096
60 # one tool call per reply: small models run a check before the change it60 # one tool call per reply: small models run a check before the change it
61 # checks has landed otherwise61 # checks has landed otherwise
62 parallel_tool_calls: false62 parallel_tool_calls: false
63- chat_template_kwargs:
64- # reasoning off (the template's default): with it on, Qwen3.5 2B often
65- # writes its final answer in the reasoning after a tool call and returns
66- # an empty answer ("mm: empty answer"); measured 2/2 on, 3/3 fine off
67- enable_thinking: false
68 63
69 context:64 context:
70 # compress the history before the window is full (settings guide, every row)65 # compress the history before the window is full (settings guide, every row)
modified docs/adr/0006-channels.md +1 -1
@@ -13,7 +13,7 @@ Milestone M2 kept one conversation per agent, in memory. Milestone M3 adds what
1313
1414 ### 1. Channels and direct conversations are one thing
1515
16-`internal/channels` replaces `internal/conversations`. A direct conversation (clicking an agent) is a channel with one member and no name, flagged `direct` with the agent's name; the Channels panel lists it under "Direct messages". The same code streams, queues, asks permissions and stores both. Mentions are plain text in a direct conversation: its agent always answers.
16+`internal/channels` replaces `internal/conversations`. A direct conversation (clicking an agent) is a channel with one member and no name, flagged `direct` with the agent's name; it opens from the Agents panel, which shows its unread answers next to the agent (the Channels panel no longer lists it, issue #60). The same code streams, queues, asks permissions and stores both. Mentions are plain text in a direct conversation: its agent always answers.
1717
1818 ### 2. Who answers
1919
@@ -13,7 +13,7 @@ Milestone M2 kept one conversation per agent, in memory. Milestone M3 adds what
13 13
14 ### 1. Channels and direct conversations are one thing14 ### 1. Channels and direct conversations are one thing
15 15
16-`internal/channels` replaces `internal/conversations`. A direct conversation (clicking an agent) is a channel with one member and no name, flagged `direct` with the agent's name; the Channels panel lists it under "Direct messages". The same code streams, queues, asks permissions and stores both. Mentions are plain text in a direct conversation: its agent always answers.16+`internal/channels` replaces `internal/conversations`. A direct conversation (clicking an agent) is a channel with one member and no name, flagged `direct` with the agent's name; it opens from the Agents panel, which shows its unread answers next to the agent (the Channels panel no longer lists it, issue #60). The same code streams, queues, asks permissions and stores both. Mentions are plain text in a direct conversation: its agent always answers.
17 17
18 ### 2. Who answers18 ### 2. Who answers
19 19
modified docs/en/README.md +1 -1
@@ -7,7 +7,7 @@ This documentation follows [Diátaxis](https://diataxis.fr): pick the section th
77 | You want to… | Section | Pages |
88 | --- | --- | --- |
99 | **learn** by doing | Tutorials | [Your first agent, without writing any configuration](tutorials/your-first-agent.md) · [A channel with two agents](tutorials/first-channel.md) · [Your first conversation with an agent](tutorials/first-conversation.md) · [Your first launch of Sidekicks](tutorials/first-launch.md) · [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |
10-| **do** a specific task | How-to guides | [Install Sidekicks](how-to/install-sidekicks.md) · [Add an agent](how-to/add-an-agent.md) · [Work with an agent's files and terminal](how-to/work-with-an-agents-files.md) · [Run an agent in a Docker container](how-to/run-an-agent-in-docker.md) · [Run an agent in an sbx sandbox](how-to/run-an-agent-in-a-sandbox.md) · [Join files to a message](how-to/join-files-to-a-message.md) · [Manage channels and their agents](how-to/manage-channels.md) · [Edit or switch the configuration](how-to/edit-or-switch-the-configuration.md) · [Reset Sidekicks](how-to/reset-sidekicks.md) · [Develop the application](how-to/develop-the-application.md) · [Run the tests](how-to/run-the-tests.md) · [Check a change before merging it](how-to/check-before-merging.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Run the end-to-end scenarios (v0.1)](how-to/run-the-end-to-end-scenarios.md) · [Publish a release](how-to/publish-a-release.md) · [Sign and notarize for macOS](how-to/sign-and-notarize-on-macos.md) · [Test issue #52 on macOS](how-to/test-issue-52-on-macos.md) · [Test issues #49 and #50 on macOS](how-to/test-issues-49-and-50-on-macos.md) · [Test issue #47 on macOS](how-to/test-issue-47-on-macos.md) · [Test milestone M4 on macOS](how-to/test-milestone-m4-on-macos.md) · [Test milestone M3 on macOS](how-to/test-milestone-m3-on-macos.md) · [Test milestone M2 on macOS](how-to/test-milestone-m2-on-macos.md) · [Test milestone M1 on macOS](how-to/test-milestone-m1-on-macos.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |
10+| **do** a specific task | How-to guides | [Install Sidekicks](how-to/install-sidekicks.md) · [Add an agent](how-to/add-an-agent.md) · [Work with an agent's files and terminal](how-to/work-with-an-agents-files.md) · [Run an agent in a Docker container](how-to/run-an-agent-in-docker.md) · [Run an agent in an sbx sandbox](how-to/run-an-agent-in-a-sandbox.md) · [Join files to a message](how-to/join-files-to-a-message.md) · [Manage channels and their agents](how-to/manage-channels.md) · [Edit or switch the configuration](how-to/edit-or-switch-the-configuration.md) · [Reset Sidekicks](how-to/reset-sidekicks.md) · [Watch the memory pressure](how-to/watch-the-memory-pressure.md) · [Develop the application](how-to/develop-the-application.md) · [Run the tests](how-to/run-the-tests.md) · [Check a change before merging it](how-to/check-before-merging.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Run the end-to-end scenarios (v0.1)](how-to/run-the-end-to-end-scenarios.md) · [Publish a release](how-to/publish-a-release.md) · [Sign and notarize for macOS](how-to/sign-and-notarize-on-macos.md) · [Test issue #52 on macOS](how-to/test-issue-52-on-macos.md) · [Test issues #49 and #50 on macOS](how-to/test-issues-49-and-50-on-macos.md) · [Test issue #47 on macOS](how-to/test-issue-47-on-macos.md) · [Test milestone M4 on macOS](how-to/test-milestone-m4-on-macos.md) · [Test milestone M3 on macOS](how-to/test-milestone-m3-on-macos.md) · [Test milestone M2 on macOS](how-to/test-milestone-m2-on-macos.md) · [Test milestone M1 on macOS](how-to/test-milestone-m1-on-macos.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |
1111 | **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [Keyboard shortcuts](reference/keyboard-shortcuts.md) · [Agents, channels and events](reference/agents-and-events.md) · [Commands](reference/commands.md) · [scripts/agents.sh](reference/agents-script.md) |
1212 | **understand** why | Explanation | [Workspaces and terminals](explanation/workspaces-and-terminals.md) · [Channels and what each agent knows](explanation/channels-and-context.md) · [Permissions and trust](explanation/permissions.md) · [Why ACP](explanation/why-acp.md) · [The interface stack](explanation/ui-stack.md) · [Architecture](explanation/architecture.md) · [Memory and local models](explanation/memory-and-local-models.md) |
1313
@@ -7,7 +7,7 @@ This documentation follows [Diátaxis](https://diataxis.fr): pick the section th
7 | You want to… | Section | Pages |7 | You want to… | Section | Pages |
8 | --- | --- | --- |8 | --- | --- | --- |
9 | **learn** by doing | Tutorials | [Your first agent, without writing any configuration](tutorials/your-first-agent.md) · [A channel with two agents](tutorials/first-channel.md) · [Your first conversation with an agent](tutorials/first-conversation.md) · [Your first launch of Sidekicks](tutorials/first-launch.md) · [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |9 | **learn** by doing | Tutorials | [Your first agent, without writing any configuration](tutorials/your-first-agent.md) · [A channel with two agents](tutorials/first-channel.md) · [Your first conversation with an agent](tutorials/first-conversation.md) · [Your first launch of Sidekicks](tutorials/first-launch.md) · [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |
10-| **do** a specific task | How-to guides | [Install Sidekicks](how-to/install-sidekicks.md) · [Add an agent](how-to/add-an-agent.md) · [Work with an agent's files and terminal](how-to/work-with-an-agents-files.md) · [Run an agent in a Docker container](how-to/run-an-agent-in-docker.md) · [Run an agent in an sbx sandbox](how-to/run-an-agent-in-a-sandbox.md) · [Join files to a message](how-to/join-files-to-a-message.md) · [Manage channels and their agents](how-to/manage-channels.md) · [Edit or switch the configuration](how-to/edit-or-switch-the-configuration.md) · [Reset Sidekicks](how-to/reset-sidekicks.md) · [Develop the application](how-to/develop-the-application.md) · [Run the tests](how-to/run-the-tests.md) · [Check a change before merging it](how-to/check-before-merging.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Run the end-to-end scenarios (v0.1)](how-to/run-the-end-to-end-scenarios.md) · [Publish a release](how-to/publish-a-release.md) · [Sign and notarize for macOS](how-to/sign-and-notarize-on-macos.md) · [Test issue #52 on macOS](how-to/test-issue-52-on-macos.md) · [Test issues #49 and #50 on macOS](how-to/test-issues-49-and-50-on-macos.md) · [Test issue #47 on macOS](how-to/test-issue-47-on-macos.md) · [Test milestone M4 on macOS](how-to/test-milestone-m4-on-macos.md) · [Test milestone M3 on macOS](how-to/test-milestone-m3-on-macos.md) · [Test milestone M2 on macOS](how-to/test-milestone-m2-on-macos.md) · [Test milestone M1 on macOS](how-to/test-milestone-m1-on-macos.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |10+| **do** a specific task | How-to guides | [Install Sidekicks](how-to/install-sidekicks.md) · [Add an agent](how-to/add-an-agent.md) · [Work with an agent's files and terminal](how-to/work-with-an-agents-files.md) · [Run an agent in a Docker container](how-to/run-an-agent-in-docker.md) · [Run an agent in an sbx sandbox](how-to/run-an-agent-in-a-sandbox.md) · [Join files to a message](how-to/join-files-to-a-message.md) · [Manage channels and their agents](how-to/manage-channels.md) · [Edit or switch the configuration](how-to/edit-or-switch-the-configuration.md) · [Reset Sidekicks](how-to/reset-sidekicks.md) · [Watch the memory pressure](how-to/watch-the-memory-pressure.md) · [Develop the application](how-to/develop-the-application.md) · [Run the tests](how-to/run-the-tests.md) · [Check a change before merging it](how-to/check-before-merging.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Run the end-to-end scenarios (v0.1)](how-to/run-the-end-to-end-scenarios.md) · [Publish a release](how-to/publish-a-release.md) · [Sign and notarize for macOS](how-to/sign-and-notarize-on-macos.md) · [Test issue #52 on macOS](how-to/test-issue-52-on-macos.md) · [Test issues #49 and #50 on macOS](how-to/test-issues-49-and-50-on-macos.md) · [Test issue #47 on macOS](how-to/test-issue-47-on-macos.md) · [Test milestone M4 on macOS](how-to/test-milestone-m4-on-macos.md) · [Test milestone M3 on macOS](how-to/test-milestone-m3-on-macos.md) · [Test milestone M2 on macOS](how-to/test-milestone-m2-on-macos.md) · [Test milestone M1 on macOS](how-to/test-milestone-m1-on-macos.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |
11 | **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [Keyboard shortcuts](reference/keyboard-shortcuts.md) · [Agents, channels and events](reference/agents-and-events.md) · [Commands](reference/commands.md) · [scripts/agents.sh](reference/agents-script.md) |11 | **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [Keyboard shortcuts](reference/keyboard-shortcuts.md) · [Agents, channels and events](reference/agents-and-events.md) · [Commands](reference/commands.md) · [scripts/agents.sh](reference/agents-script.md) |
12 | **understand** why | Explanation | [Workspaces and terminals](explanation/workspaces-and-terminals.md) · [Channels and what each agent knows](explanation/channels-and-context.md) · [Permissions and trust](explanation/permissions.md) · [Why ACP](explanation/why-acp.md) · [The interface stack](explanation/ui-stack.md) · [Architecture](explanation/architecture.md) · [Memory and local models](explanation/memory-and-local-models.md) |12 | **understand** why | Explanation | [Workspaces and terminals](explanation/workspaces-and-terminals.md) · [Channels and what each agent knows](explanation/channels-and-context.md) · [Permissions and trust](explanation/permissions.md) · [Why ACP](explanation/why-acp.md) · [The interface stack](explanation/ui-stack.md) · [Architecture](explanation/architecture.md) · [Memory and local models](explanation/memory-and-local-models.md) |
13 13
modified docs/en/how-to/manage-channels.md +15 -3
@@ -1,6 +1,6 @@
11 # How to manage channels and their agents
22
3-This guide shows how to create, rename and delete channels, choose the agents in them, and talk with an agent directly. It assumes Sidekicks runs with a configuration that has agents.
3+This guide shows how to create, rename and delete channels, choose the agents in them, talk with an agent directly, and clear a conversation's history. It assumes Sidekicks runs with a configuration that has agents.
44
55 ## Create a channel
66
@@ -24,7 +24,19 @@ Hover the channel in the list:
2424
2525 ## Talk with one agent directly
2626
27-Click an agent in the **Agents** panel. Its direct conversation opens, and appears under **Direct messages** in the Channels panel. Every message goes to that agent; `@` mentions are plain text there. When the agent is removed from the configuration, its direct conversation is hidden but kept: it comes back if the agent is added again.
27+Click an agent in the **Agents** panel. Its direct conversation opens; the agent stays highlighted while it is shown, and a counter next to it gives the answers you have not read yet. Every message goes to that agent; `@` mentions are plain text there. When the agent is removed from the configuration, its direct conversation is hidden but kept: it comes back if the agent is added again.
28+
29+## Clear the history of a channel or a direct conversation
30+
31+To start again from an empty conversation without deleting the channel:
32+
33+1. Open the channel, or the direct conversation (click the agent in **Agents**).
34+2. Click **🗑** (*Clear the history*) at the right of the conversation's header. It shows once the conversation has messages.
35+3. Confirm with **Erase**.
36+
37+Every message is erased, for good. A channel keeps its name, description and members. The agents start a new conversation: they do not remember what was erased, and their context gauge goes back to zero.
38+
39+While an agent is answering, the history cannot be cleared: stop it (**■**) or wait for the end of its answer, then click **🗑** again.
2840
2941 ## Address the right agent
3042
@@ -38,7 +50,7 @@ Click an agent in the **Agents** panel. Its direct conversation opens, and appea
3850 ## Variants
3951
4052 - **Channels from the configuration file**: channels declared under `channels:` are created at startup if they do not exist yet, with their agents; afterwards they are ordinary channels. Removing them from the file deletes nothing ([configuration reference](../reference/configuration.md)).
41-- **Start again from an empty history**: quit Sidekicks and delete the folder `sidekicks/data/<…>` of your configuration (its location is in the [reference](../reference/agents-and-events.md#history)). The channels declared in the file come back at the next launch, empty.
53+- **Start again from an empty history, for every channel at once**: use the **Danger zone** ([Reset Sidekicks](reset-sidekicks.md)), or quit Sidekicks and delete the folder `sidekicks/data/<…>` of your configuration (its location is in the [reference](../reference/agents-and-events.md#history)). The channels declared in the file come back at the next launch, empty.
4254 - **"History unavailable" banner**: another Sidekicks has the same configuration file open. This one keeps working, but forgets everything when it quits; close the other one and relaunch.
4355
4456 ## See also
@@ -1,6 +1,6 @@
1 # How to manage channels and their agents1 # How to manage channels and their agents
2 2
3-This guide shows how to create, rename and delete channels, choose the agents in them, and talk with an agent directly. It assumes Sidekicks runs with a configuration that has agents.3+This guide shows how to create, rename and delete channels, choose the agents in them, talk with an agent directly, and clear a conversation's history. It assumes Sidekicks runs with a configuration that has agents.
4 4
5 ## Create a channel5 ## Create a channel
6 6
@@ -24,7 +24,19 @@ Hover the channel in the list:
24 24
25 ## Talk with one agent directly25 ## Talk with one agent directly
26 26
27-Click an agent in the **Agents** panel. Its direct conversation opens, and appears under **Direct messages** in the Channels panel. Every message goes to that agent; `@` mentions are plain text there. When the agent is removed from the configuration, its direct conversation is hidden but kept: it comes back if the agent is added again.27+Click an agent in the **Agents** panel. Its direct conversation opens; the agent stays highlighted while it is shown, and a counter next to it gives the answers you have not read yet. Every message goes to that agent; `@` mentions are plain text there. When the agent is removed from the configuration, its direct conversation is hidden but kept: it comes back if the agent is added again.
28+
29+## Clear the history of a channel or a direct conversation
30+
31+To start again from an empty conversation without deleting the channel:
32+
33+1. Open the channel, or the direct conversation (click the agent in **Agents**).
34+2. Click **🗑** (*Clear the history*) at the right of the conversation's header. It shows once the conversation has messages.
35+3. Confirm with **Erase**.
36+
37+Every message is erased, for good. A channel keeps its name, description and members. The agents start a new conversation: they do not remember what was erased, and their context gauge goes back to zero.
38+
39+While an agent is answering, the history cannot be cleared: stop it (**■**) or wait for the end of its answer, then click **🗑** again.
28 40
29 ## Address the right agent41 ## Address the right agent
30 42
@@ -38,7 +50,7 @@ Click an agent in the **Agents** panel. Its direct conversation opens, and appea
38 ## Variants50 ## Variants
39 51
40 - **Channels from the configuration file**: channels declared under `channels:` are created at startup if they do not exist yet, with their agents; afterwards they are ordinary channels. Removing them from the file deletes nothing ([configuration reference](../reference/configuration.md)).52 - **Channels from the configuration file**: channels declared under `channels:` are created at startup if they do not exist yet, with their agents; afterwards they are ordinary channels. Removing them from the file deletes nothing ([configuration reference](../reference/configuration.md)).
41-- **Start again from an empty history**: quit Sidekicks and delete the folder `sidekicks/data/<…>` of your configuration (its location is in the [reference](../reference/agents-and-events.md#history)). The channels declared in the file come back at the next launch, empty.53+- **Start again from an empty history, for every channel at once**: use the **Danger zone** ([Reset Sidekicks](reset-sidekicks.md)), or quit Sidekicks and delete the folder `sidekicks/data/<…>` of your configuration (its location is in the [reference](../reference/agents-and-events.md#history)). The channels declared in the file come back at the next launch, empty.
42 - **"History unavailable" banner**: another Sidekicks has the same configuration file open. This one keeps working, but forgets everything when it quits; close the other one and relaunch.54 - **"History unavailable" banner**: another Sidekicks has the same configuration file open. This one keeps working, but forgets everything when it quits; close the other one and relaunch.
43 55
44 ## See also56 ## See also
added docs/en/how-to/watch-the-memory-pressure.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to watch the memory pressure
2+
3+Local models and agents take a lot of memory. When the computer runs short of it, it slows down, and on a Mac with little memory it can stop applications. Sidekicks shows how much the memory is under pressure, next to the agents' logs.
4+
5+1. Click **Log** in the status bar (bottom right). The panel opens at the bottom.
6+2. At the top right of the panel, read the line **Memory pressure:**
7+ - a dot and a level: **normal** (green), **high** (yellow) or **critical** (red);
8+ - a bar and the memory used, e.g. `18.2 / 32.0 GB used`.
9+
10+ Hover over the level for a reminder of what to do. The line is refreshed every 5 seconds while the panel is open.
11+3. When the level is **high** or **critical**, free memory:
12+ - stop an agent you do not need: **■** next to it in the **Agents** panel;
13+ - close the applications you do not use.
14+
15+## What the figures mean
16+
17+- **On macOS**, the level is the one the system gives, the same as the *Memory Pressure* graph of Activity Monitor. The memory used is counted as Activity Monitor does: app memory, wired memory and compressed memory. The memory "cached" by macOS is not counted: it is given back as soon as an application needs it.
18+- **On Windows** (and Linux), the level is worked out from the memory still available: **high** under 20 %, **critical** under 10 %.
19+- If the memory cannot be measured, the line is not shown.
20+
21+## See also
22+
23+- [Reference: services called by the interface](../reference/agents-and-events.md#services-called-by-the-interface) (`SystemService.Memory`)
24+- [Work with an agent's files and terminal](work-with-an-agents-files.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# How to watch the memory pressure
2+
3+Local models and agents take a lot of memory. When the computer runs short of it, it slows down, and on a Mac with little memory it can stop applications. Sidekicks shows how much the memory is under pressure, next to the agents' logs.
4+
5+1. Click **Log** in the status bar (bottom right). The panel opens at the bottom.
6+2. At the top right of the panel, read the line **Memory pressure:**
7+ - a dot and a level: **normal** (green), **high** (yellow) or **critical** (red);
8+ - a bar and the memory used, e.g. `18.2 / 32.0 GB used`.
9+
10+ Hover over the level for a reminder of what to do. The line is refreshed every 5 seconds while the panel is open.
11+3. When the level is **high** or **critical**, free memory:
12+ - stop an agent you do not need: **■** next to it in the **Agents** panel;
13+ - close the applications you do not use.
14+
15+## What the figures mean
16+
17+- **On macOS**, the level is the one the system gives, the same as the *Memory Pressure* graph of Activity Monitor. The memory used is counted as Activity Monitor does: app memory, wired memory and compressed memory. The memory "cached" by macOS is not counted: it is given back as soon as an application needs it.
18+- **On Windows** (and Linux), the level is worked out from the memory still available: **high** under 20 %, **critical** under 10 %.
19+- If the memory cannot be measured, the line is not shown.
20+
21+## See also
22+
23+- [Reference: services called by the interface](../reference/agents-and-events.md#services-called-by-the-interface) (`SystemService.Memory`)
24+- [Work with an agent's files and terminal](work-with-an-agents-files.md)
modified docs/en/reference/agents-and-events.md +2 -0
@@ -51,6 +51,7 @@ The Go names and types are in `internal/events`; the TypeScript constants and ty
5151 | | `StopSandbox(name)`, `RemoveSandbox(name)` | Stops the agent, then runs `runtime.lifecycle.stop` or `remove` (container or sandbox); the output goes to the agent's log. |
5252 | | `Logs(name)` | The last 200 stderr lines of the agent. |
5353 | `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |
54+| `SystemService` | `Memory()` | `level` (`normal`, `warning`, `critical`), `used` and `total` in bytes; all empty (`total` 0) where the memory cannot be measured. macOS: the level of `kern.memorystatus_vm_pressure_level`, used = app + wired + compressed memory (`vm_stat`), as in Activity Monitor. Linux and Windows: worked out from the available memory, `warning` under 20 %, `critical` under 10 %. The Log panel calls it every 5 seconds while it is open (issue #61). |
5455
5556 ## Channels (milestones M2 and M3)
5657
@@ -130,6 +131,7 @@ A tool call has `id, title, kind` (`read, edit, delete, move, search, execute, t
130131 | `Create(name, description)` | Creates a channel. A taken or empty name is an error. |
131132 | `Rename(id, name, description)` | Renames a channel (not a direct conversation). |
132133 | `Delete(id)` | Stops its answers, then deletes the channel and its whole history. |
134+| `Clear(id)` | Erases every message of a channel or direct conversation and keeps its name, description and members. The members' sessions are forgotten (`sessionId` and `lastSeen` emptied): their next turn opens a new session, which knows nothing of what was erased; the usage is reset. Refused while the channel answers or has queued messages (`an agent is answering in this channel…`). Sends `channels:changed`. |
133135 | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invites or removes an agent; the channel shows `agent-joined` or `agent-left`. |
134136 | `Direct(agent)` | The direct conversation with `agent`, created the first time. |
135137 | `Send(id, text)` | Adds a user message and returns its id; the agents that answer are started if needed; the message is queued if the channel is answering. Empty text and unknown channels are errors. Each `$path` joins a file of an agent's folder (see [Joined files](#joined-files)); more than 10 is an error. |
@@ -51,6 +51,7 @@ The Go names and types are in `internal/events`; the TypeScript constants and ty
51 | | `StopSandbox(name)`, `RemoveSandbox(name)` | Stops the agent, then runs `runtime.lifecycle.stop` or `remove` (container or sandbox); the output goes to the agent's log. |51 | | `StopSandbox(name)`, `RemoveSandbox(name)` | Stops the agent, then runs `runtime.lifecycle.stop` or `remove` (container or sandbox); the output goes to the agent's log. |
52 | | `Logs(name)` | The last 200 stderr lines of the agent. |52 | | `Logs(name)` | The last 200 stderr lines of the agent. |
53 | `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |53 | `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |
54+| `SystemService` | `Memory()` | `level` (`normal`, `warning`, `critical`), `used` and `total` in bytes; all empty (`total` 0) where the memory cannot be measured. macOS: the level of `kern.memorystatus_vm_pressure_level`, used = app + wired + compressed memory (`vm_stat`), as in Activity Monitor. Linux and Windows: worked out from the available memory, `warning` under 20 %, `critical` under 10 %. The Log panel calls it every 5 seconds while it is open (issue #61). |
54 55
55 ## Channels (milestones M2 and M3)56 ## Channels (milestones M2 and M3)
56 57
@@ -130,6 +131,7 @@ A tool call has `id, title, kind` (`read, edit, delete, move, search, execute, t
130 | `Create(name, description)` | Creates a channel. A taken or empty name is an error. |131 | `Create(name, description)` | Creates a channel. A taken or empty name is an error. |
131 | `Rename(id, name, description)` | Renames a channel (not a direct conversation). |132 | `Rename(id, name, description)` | Renames a channel (not a direct conversation). |
132 | `Delete(id)` | Stops its answers, then deletes the channel and its whole history. |133 | `Delete(id)` | Stops its answers, then deletes the channel and its whole history. |
134+| `Clear(id)` | Erases every message of a channel or direct conversation and keeps its name, description and members. The members' sessions are forgotten (`sessionId` and `lastSeen` emptied): their next turn opens a new session, which knows nothing of what was erased; the usage is reset. Refused while the channel answers or has queued messages (`an agent is answering in this channel…`). Sends `channels:changed`. |
133 | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invites or removes an agent; the channel shows `agent-joined` or `agent-left`. |135 | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invites or removes an agent; the channel shows `agent-joined` or `agent-left`. |
134 | `Direct(agent)` | The direct conversation with `agent`, created the first time. |136 | `Direct(agent)` | The direct conversation with `agent`, created the first time. |
135 | `Send(id, text)` | Adds a user message and returns its id; the agents that answer are started if needed; the message is queued if the channel is answering. Empty text and unknown channels are errors. Each `$path` joins a file of an agent's folder (see [Joined files](#joined-files)); more than 10 is an error. |137 | `Send(id, text)` | Adds a user message and returns its id; the agents that answer are started if needed; the message is queued if the channel is answering. Empty text and unknown channels are errors. Each `$path` joins a file of an agent's folder (see [Joined files](#joined-files)); more than 10 is an error. |
modified docs/en/tutorials/your-first-agent.md +1 -1
@@ -36,7 +36,7 @@ Leave **On this computer** selected. Fill in:
3636
3737 Click **Add the agent**.
3838
39-You should see the assistant close, **🤖 Alice (assistant)** appear under **AGENTS** and under **DIRECT MESSAGES**, and her conversation open in the centre with "Write your first message to Alice (assistant)."
39+You should see the assistant close, **🤖 Alice (assistant)** appear under **AGENTS**, and her conversation open in the centre with "Write your first message to Alice (assistant)."
4040
4141 ## Step 4 — Talk with Alice
4242
@@ -36,7 +36,7 @@ Leave **On this computer** selected. Fill in:
36 36
37 Click **Add the agent**.37 Click **Add the agent**.
38 38
39-You should see the assistant close, **🤖 Alice (assistant)** appear under **AGENTS** and under **DIRECT MESSAGES**, and her conversation open in the centre with "Write your first message to Alice (assistant)."39+You should see the assistant close, **🤖 Alice (assistant)** appear under **AGENTS**, and her conversation open in the centre with "Write your first message to Alice (assistant)."
40 40
41 ## Step 4 — Talk with Alice41 ## Step 4 — Talk with Alice
42 42
modified docs/fr/README.md +1 -1
@@ -7,7 +7,7 @@ Cette documentation suit la méthode [Diátaxis](https://diataxis.fr) : choisiss
77 | Vous voulez… | Section | Pages |
88 | --- | --- | --- |
99 | **apprendre** en pratiquant | Tutoriels | [Votre premier agent, sans écrire de configuration](tutorials/premier-agent.md) · [Un channel avec deux agents](tutorials/premier-channel.md) · [Votre première conversation avec un agent](tutorials/premiere-conversation.md) · [Votre premier lancement de Sidekicks](tutorials/premier-lancement.md) · [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |
10-| **réaliser** une tâche précise | Guides pratiques | [Installer Sidekicks](how-to/installer-sidekicks.md) · [Ajouter un agent](how-to/ajouter-un-agent.md) · [Travailler avec les fichiers et le terminal d'un agent](how-to/fichiers-et-terminal-d-un-agent.md) · [Lancer un agent dans un conteneur Docker](how-to/lancer-un-agent-dans-docker.md) · [Lancer un agent dans une sandbox sbx](how-to/lancer-un-agent-dans-une-sandbox.md) · [Joindre des fichiers à un message](how-to/joindre-des-fichiers-a-un-message.md) · [Gérer les channels et leurs agents](how-to/gerer-les-channels.md) · [Modifier ou changer la configuration](how-to/modifier-ou-changer-la-configuration.md) · [Repartir de zéro](how-to/repartir-de-zero.md) · [Développer l'application](how-to/developper-l-application.md) · [Lancer les tests](how-to/lancer-les-tests.md) · [Vérifier une modification avant de la fusionner](how-to/verifier-avant-de-fusionner.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Dérouler les scénarios de bout en bout (v0.1)](how-to/derouler-les-scenarios-de-bout-en-bout.md) · [Publier une release](how-to/publier-une-release.md) · [Signer et notariser pour macOS](how-to/signer-et-notariser-sous-macos.md) · [Tester l'issue #52 sous macOS](how-to/tester-l-issue-52-sous-macos.md) · [Tester les issues #49 et #50 sous macOS](how-to/tester-les-issues-49-et-50-sous-macos.md) · [Tester l'issue #47 sous macOS](how-to/tester-l-issue-47-sous-macos.md) · [Tester la milestone M4 sous macOS](how-to/tester-la-milestone-m4-sous-macos.md) · [Tester la milestone M3 sous macOS](how-to/tester-la-milestone-m3-sous-macos.md) · [Tester la milestone M2 sous macOS](how-to/tester-la-milestone-m2-sous-macos.md) · [Tester la milestone M1 sous macOS](how-to/tester-la-milestone-m1-sous-macos.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |
10+| **réaliser** une tâche précise | Guides pratiques | [Installer Sidekicks](how-to/installer-sidekicks.md) · [Ajouter un agent](how-to/ajouter-un-agent.md) · [Travailler avec les fichiers et le terminal d'un agent](how-to/fichiers-et-terminal-d-un-agent.md) · [Lancer un agent dans un conteneur Docker](how-to/lancer-un-agent-dans-docker.md) · [Lancer un agent dans une sandbox sbx](how-to/lancer-un-agent-dans-une-sandbox.md) · [Joindre des fichiers à un message](how-to/joindre-des-fichiers-a-un-message.md) · [Gérer les channels et leurs agents](how-to/gerer-les-channels.md) · [Modifier ou changer la configuration](how-to/modifier-ou-changer-la-configuration.md) · [Repartir de zéro](how-to/repartir-de-zero.md) · [Surveiller la pression mémoire](how-to/surveiller-la-pression-memoire.md) · [Développer l'application](how-to/developper-l-application.md) · [Lancer les tests](how-to/lancer-les-tests.md) · [Vérifier une modification avant de la fusionner](how-to/verifier-avant-de-fusionner.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Dérouler les scénarios de bout en bout (v0.1)](how-to/derouler-les-scenarios-de-bout-en-bout.md) · [Publier une release](how-to/publier-une-release.md) · [Signer et notariser pour macOS](how-to/signer-et-notariser-sous-macos.md) · [Tester l'issue #52 sous macOS](how-to/tester-l-issue-52-sous-macos.md) · [Tester les issues #49 et #50 sous macOS](how-to/tester-les-issues-49-et-50-sous-macos.md) · [Tester l'issue #47 sous macOS](how-to/tester-l-issue-47-sous-macos.md) · [Tester la milestone M4 sous macOS](how-to/tester-la-milestone-m4-sous-macos.md) · [Tester la milestone M3 sous macOS](how-to/tester-la-milestone-m3-sous-macos.md) · [Tester la milestone M2 sous macOS](how-to/tester-la-milestone-m2-sous-macos.md) · [Tester la milestone M1 sous macOS](how-to/tester-la-milestone-m1-sous-macos.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |
1111 | **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [Raccourcis clavier](reference/raccourcis-clavier.md) · [Agents, channels et événements](reference/agents-et-evenements.md) · [Commandes](reference/commandes.md) · [scripts/agents.sh](reference/script-agents.md) |
1212 | **comprendre** le pourquoi | Explications | [Workspaces et terminaux](explanation/workspaces-et-terminaux.md) · [Les channels et ce que sait chaque agent](explanation/channels-et-contexte.md) · [Permissions et confiance](explanation/permissions-et-confiance.md) · [Pourquoi ACP](explanation/pourquoi-acp.md) · [La stack de l'interface](explanation/stack-interface.md) · [Architecture](explanation/architecture.md) · [Mémoire et modèles locaux](explanation/memoire-et-modeles-locaux.md) |
1313
@@ -7,7 +7,7 @@ Cette documentation suit la méthode [Diátaxis](https://diataxis.fr) : choisiss
7 | Vous voulez… | Section | Pages |7 | Vous voulez… | Section | Pages |
8 | --- | --- | --- |8 | --- | --- | --- |
9 | **apprendre** en pratiquant | Tutoriels | [Votre premier agent, sans écrire de configuration](tutorials/premier-agent.md) · [Un channel avec deux agents](tutorials/premier-channel.md) · [Votre première conversation avec un agent](tutorials/premiere-conversation.md) · [Votre premier lancement de Sidekicks](tutorials/premier-lancement.md) · [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |9 | **apprendre** en pratiquant | Tutoriels | [Votre premier agent, sans écrire de configuration](tutorials/premier-agent.md) · [Un channel avec deux agents](tutorials/premier-channel.md) · [Votre première conversation avec un agent](tutorials/premiere-conversation.md) · [Votre premier lancement de Sidekicks](tutorials/premier-lancement.md) · [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |
10-| **réaliser** une tâche précise | Guides pratiques | [Installer Sidekicks](how-to/installer-sidekicks.md) · [Ajouter un agent](how-to/ajouter-un-agent.md) · [Travailler avec les fichiers et le terminal d'un agent](how-to/fichiers-et-terminal-d-un-agent.md) · [Lancer un agent dans un conteneur Docker](how-to/lancer-un-agent-dans-docker.md) · [Lancer un agent dans une sandbox sbx](how-to/lancer-un-agent-dans-une-sandbox.md) · [Joindre des fichiers à un message](how-to/joindre-des-fichiers-a-un-message.md) · [Gérer les channels et leurs agents](how-to/gerer-les-channels.md) · [Modifier ou changer la configuration](how-to/modifier-ou-changer-la-configuration.md) · [Repartir de zéro](how-to/repartir-de-zero.md) · [Développer l'application](how-to/developper-l-application.md) · [Lancer les tests](how-to/lancer-les-tests.md) · [Vérifier une modification avant de la fusionner](how-to/verifier-avant-de-fusionner.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Dérouler les scénarios de bout en bout (v0.1)](how-to/derouler-les-scenarios-de-bout-en-bout.md) · [Publier une release](how-to/publier-une-release.md) · [Signer et notariser pour macOS](how-to/signer-et-notariser-sous-macos.md) · [Tester l'issue #52 sous macOS](how-to/tester-l-issue-52-sous-macos.md) · [Tester les issues #49 et #50 sous macOS](how-to/tester-les-issues-49-et-50-sous-macos.md) · [Tester l'issue #47 sous macOS](how-to/tester-l-issue-47-sous-macos.md) · [Tester la milestone M4 sous macOS](how-to/tester-la-milestone-m4-sous-macos.md) · [Tester la milestone M3 sous macOS](how-to/tester-la-milestone-m3-sous-macos.md) · [Tester la milestone M2 sous macOS](how-to/tester-la-milestone-m2-sous-macos.md) · [Tester la milestone M1 sous macOS](how-to/tester-la-milestone-m1-sous-macos.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |10+| **réaliser** une tâche précise | Guides pratiques | [Installer Sidekicks](how-to/installer-sidekicks.md) · [Ajouter un agent](how-to/ajouter-un-agent.md) · [Travailler avec les fichiers et le terminal d'un agent](how-to/fichiers-et-terminal-d-un-agent.md) · [Lancer un agent dans un conteneur Docker](how-to/lancer-un-agent-dans-docker.md) · [Lancer un agent dans une sandbox sbx](how-to/lancer-un-agent-dans-une-sandbox.md) · [Joindre des fichiers à un message](how-to/joindre-des-fichiers-a-un-message.md) · [Gérer les channels et leurs agents](how-to/gerer-les-channels.md) · [Modifier ou changer la configuration](how-to/modifier-ou-changer-la-configuration.md) · [Repartir de zéro](how-to/repartir-de-zero.md) · [Surveiller la pression mémoire](how-to/surveiller-la-pression-memoire.md) · [Développer l'application](how-to/developper-l-application.md) · [Lancer les tests](how-to/lancer-les-tests.md) · [Vérifier une modification avant de la fusionner](how-to/verifier-avant-de-fusionner.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Dérouler les scénarios de bout en bout (v0.1)](how-to/derouler-les-scenarios-de-bout-en-bout.md) · [Publier une release](how-to/publier-une-release.md) · [Signer et notariser pour macOS](how-to/signer-et-notariser-sous-macos.md) · [Tester l'issue #52 sous macOS](how-to/tester-l-issue-52-sous-macos.md) · [Tester les issues #49 et #50 sous macOS](how-to/tester-les-issues-49-et-50-sous-macos.md) · [Tester l'issue #47 sous macOS](how-to/tester-l-issue-47-sous-macos.md) · [Tester la milestone M4 sous macOS](how-to/tester-la-milestone-m4-sous-macos.md) · [Tester la milestone M3 sous macOS](how-to/tester-la-milestone-m3-sous-macos.md) · [Tester la milestone M2 sous macOS](how-to/tester-la-milestone-m2-sous-macos.md) · [Tester la milestone M1 sous macOS](how-to/tester-la-milestone-m1-sous-macos.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |
11 | **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [Raccourcis clavier](reference/raccourcis-clavier.md) · [Agents, channels et événements](reference/agents-et-evenements.md) · [Commandes](reference/commandes.md) · [scripts/agents.sh](reference/script-agents.md) |11 | **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [Raccourcis clavier](reference/raccourcis-clavier.md) · [Agents, channels et événements](reference/agents-et-evenements.md) · [Commandes](reference/commandes.md) · [scripts/agents.sh](reference/script-agents.md) |
12 | **comprendre** le pourquoi | Explications | [Workspaces et terminaux](explanation/workspaces-et-terminaux.md) · [Les channels et ce que sait chaque agent](explanation/channels-et-contexte.md) · [Permissions et confiance](explanation/permissions-et-confiance.md) · [Pourquoi ACP](explanation/pourquoi-acp.md) · [La stack de l'interface](explanation/stack-interface.md) · [Architecture](explanation/architecture.md) · [Mémoire et modèles locaux](explanation/memoire-et-modeles-locaux.md) |12 | **comprendre** le pourquoi | Explications | [Workspaces et terminaux](explanation/workspaces-et-terminaux.md) · [Les channels et ce que sait chaque agent](explanation/channels-et-contexte.md) · [Permissions et confiance](explanation/permissions-et-confiance.md) · [Pourquoi ACP](explanation/pourquoi-acp.md) · [La stack de l'interface](explanation/stack-interface.md) · [Architecture](explanation/architecture.md) · [Mémoire et modèles locaux](explanation/memoire-et-modeles-locaux.md) |
13 13
modified docs/fr/how-to/gerer-les-channels.md +15 -3
@@ -1,6 +1,6 @@
11 # Gérer les channels et leurs agents
22
3-Ce guide montre comment créer, renommer et supprimer des channels, choisir les agents qui y participent, et parler directement à un agent. Il suppose que Sidekicks tourne avec une configuration qui contient des agents. Les libellés sont ceux de l'interface en anglais, sa langue par défaut.
3+Ce guide montre comment créer, renommer et supprimer des channels, choisir les agents qui y participent, parler directement à un agent, et vider l'historique d'une conversation. Il suppose que Sidekicks tourne avec une configuration qui contient des agents. Les libellés sont ceux de l'interface en anglais, sa langue par défaut.
44
55 ## Créer un channel
66
@@ -24,7 +24,19 @@ Survolez le channel dans la liste :
2424
2525 ## Parler directement à un agent
2626
27-Cliquez sur un agent dans le panneau **Agents**. Sa conversation directe s'ouvre, et apparaît sous **Direct messages** dans le panneau Channels. Chaque message va à cet agent ; les mentions `@` y sont du simple texte. Quand l'agent est retiré de la configuration, sa conversation directe est masquée mais gardée : elle revient si l'agent est de nouveau ajouté.
27+Cliquez sur un agent dans le panneau **Agents**. Sa conversation directe s'ouvre ; l'agent reste surligné tant qu'elle est affichée, et un compteur à côté de lui donne les réponses que vous n'avez pas encore lues. Chaque message va à cet agent ; les mentions `@` y sont du simple texte. Quand l'agent est retiré de la configuration, sa conversation directe est masquée mais gardée : elle revient si l'agent est de nouveau ajouté.
28+
29+## Vider l'historique d'un channel ou d'une conversation directe
30+
31+Pour repartir d'une conversation vide sans supprimer le channel :
32+
33+1. Ouvrez le channel, ou la conversation directe (cliquez sur l'agent dans **Agents**).
34+2. Cliquez sur **🗑** (*Clear the history*, « Vider l'historique » en français) à droite de l'en-tête de la conversation. Il apparaît dès que la conversation a des messages.
35+3. Confirmez avec **Erase** (« Effacer »).
36+
37+Tous les messages sont effacés, définitivement. Un channel garde son nom, sa description et ses membres. Les agents repartent sur une nouvelle conversation : ils ne se souviennent pas de ce qui a été effacé, et leur jauge de contexte revient à zéro.
38+
39+Pendant qu'un agent répond, l'historique ne peut pas être vidé : arrêtez-le (**■**) ou attendez la fin de sa réponse, puis cliquez de nouveau sur **🗑**.
2840
2941 ## S'adresser au bon agent
3042
@@ -38,7 +50,7 @@ Cliquez sur un agent dans le panneau **Agents**. Sa conversation directe s'ouvre
3850 ## Variantes
3951
4052 - **Channels du fichier de configuration** : les channels déclarés sous `channels:` sont créés au démarrage s'ils n'existent pas encore, avec leurs agents ; ensuite ce sont des channels ordinaires. Les retirer du fichier ne supprime rien ([référence de la configuration](../reference/configuration.md)).
41-- **Repartir d'un historique vide** : quittez Sidekicks et supprimez le dossier `sidekicks/data/<…>` de votre configuration (son emplacement est dans la [référence](../reference/agents-et-evenements.md#historique)). Les channels déclarés dans le fichier reviennent, vides, au lancement suivant.
53+- **Repartir d'un historique vide, pour tous les channels d'un coup** : utilisez la **Danger zone** ([Repartir de zéro](repartir-de-zero.md)), ou quittez Sidekicks et supprimez le dossier `sidekicks/data/<…>` de votre configuration (son emplacement est dans la [référence](../reference/agents-et-evenements.md#historique)). Les channels déclarés dans le fichier reviennent, vides, au lancement suivant.
4254 - **Bandeau « History unavailable »** : un autre Sidekicks a ouvert le même fichier de configuration. Celui-ci continue de fonctionner, mais oublie tout en quittant ; fermez l'autre et relancez.
4355
4456 ## Voir aussi
@@ -1,6 +1,6 @@
1 # Gérer les channels et leurs agents1 # Gérer les channels et leurs agents
2 2
3-Ce guide montre comment créer, renommer et supprimer des channels, choisir les agents qui y participent, et parler directement à un agent. Il suppose que Sidekicks tourne avec une configuration qui contient des agents. Les libellés sont ceux de l'interface en anglais, sa langue par défaut.3+Ce guide montre comment créer, renommer et supprimer des channels, choisir les agents qui y participent, parler directement à un agent, et vider l'historique d'une conversation. Il suppose que Sidekicks tourne avec une configuration qui contient des agents. Les libellés sont ceux de l'interface en anglais, sa langue par défaut.
4 4
5 ## Créer un channel5 ## Créer un channel
6 6
@@ -24,7 +24,19 @@ Survolez le channel dans la liste :
24 24
25 ## Parler directement à un agent25 ## Parler directement à un agent
26 26
27-Cliquez sur un agent dans le panneau **Agents**. Sa conversation directe s'ouvre, et apparaît sous **Direct messages** dans le panneau Channels. Chaque message va à cet agent ; les mentions `@` y sont du simple texte. Quand l'agent est retiré de la configuration, sa conversation directe est masquée mais gardée : elle revient si l'agent est de nouveau ajouté.27+Cliquez sur un agent dans le panneau **Agents**. Sa conversation directe s'ouvre ; l'agent reste surligné tant qu'elle est affichée, et un compteur à côté de lui donne les réponses que vous n'avez pas encore lues. Chaque message va à cet agent ; les mentions `@` y sont du simple texte. Quand l'agent est retiré de la configuration, sa conversation directe est masquée mais gardée : elle revient si l'agent est de nouveau ajouté.
28+
29+## Vider l'historique d'un channel ou d'une conversation directe
30+
31+Pour repartir d'une conversation vide sans supprimer le channel :
32+
33+1. Ouvrez le channel, ou la conversation directe (cliquez sur l'agent dans **Agents**).
34+2. Cliquez sur **🗑** (*Clear the history*, « Vider l'historique » en français) à droite de l'en-tête de la conversation. Il apparaît dès que la conversation a des messages.
35+3. Confirmez avec **Erase** (« Effacer »).
36+
37+Tous les messages sont effacés, définitivement. Un channel garde son nom, sa description et ses membres. Les agents repartent sur une nouvelle conversation : ils ne se souviennent pas de ce qui a été effacé, et leur jauge de contexte revient à zéro.
38+
39+Pendant qu'un agent répond, l'historique ne peut pas être vidé : arrêtez-le (**■**) ou attendez la fin de sa réponse, puis cliquez de nouveau sur **🗑**.
28 40
29 ## S'adresser au bon agent41 ## S'adresser au bon agent
30 42
@@ -38,7 +50,7 @@ Cliquez sur un agent dans le panneau **Agents**. Sa conversation directe s'ouvre
38 ## Variantes50 ## Variantes
39 51
40 - **Channels du fichier de configuration** : les channels déclarés sous `channels:` sont créés au démarrage s'ils n'existent pas encore, avec leurs agents ; ensuite ce sont des channels ordinaires. Les retirer du fichier ne supprime rien ([référence de la configuration](../reference/configuration.md)).52 - **Channels du fichier de configuration** : les channels déclarés sous `channels:` sont créés au démarrage s'ils n'existent pas encore, avec leurs agents ; ensuite ce sont des channels ordinaires. Les retirer du fichier ne supprime rien ([référence de la configuration](../reference/configuration.md)).
41-- **Repartir d'un historique vide** : quittez Sidekicks et supprimez le dossier `sidekicks/data/<…>` de votre configuration (son emplacement est dans la [référence](../reference/agents-et-evenements.md#historique)). Les channels déclarés dans le fichier reviennent, vides, au lancement suivant.53+- **Repartir d'un historique vide, pour tous les channels d'un coup** : utilisez la **Danger zone** ([Repartir de zéro](repartir-de-zero.md)), ou quittez Sidekicks et supprimez le dossier `sidekicks/data/<…>` de votre configuration (son emplacement est dans la [référence](../reference/agents-et-evenements.md#historique)). Les channels déclarés dans le fichier reviennent, vides, au lancement suivant.
42 - **Bandeau « History unavailable »** : un autre Sidekicks a ouvert le même fichier de configuration. Celui-ci continue de fonctionner, mais oublie tout en quittant ; fermez l'autre et relancez.54 - **Bandeau « History unavailable »** : un autre Sidekicks a ouvert le même fichier de configuration. Celui-ci continue de fonctionner, mais oublie tout en quittant ; fermez l'autre et relancez.
43 55
44 ## Voir aussi56 ## Voir aussi
added docs/fr/how-to/surveiller-la-pression-memoire.md +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+# Surveiller la pression mémoire
2+
3+Les modèles locaux et les agents prennent beaucoup de mémoire. Quand l'ordinateur en manque, il ralentit, et sur un Mac avec peu de mémoire il peut arrêter des applications. Sidekicks affiche la pression sur la mémoire, à côté des journaux des agents.
4+
5+1. Cliquez sur **Log** dans la barre d'état (en bas à droite). Le panneau s'ouvre en bas.
6+2. En haut à droite du panneau, lisez la ligne **Pression mémoire :**
7+ - un point et un niveau : **normale** (vert), **élevée** (jaune) ou **critique** (rouge) ;
8+ - une barre et la mémoire utilisée, par exemple `18,2 / 32,0 Go utilisés`.
9+
10+ Survolez le niveau pour un rappel de ce qu'il faut faire. La ligne est mise à jour toutes les 5 secondes tant que le panneau est ouvert.
11+3. Quand le niveau est **élevée** ou **critique**, libérez de la mémoire :
12+ - arrêtez un agent dont vous n'avez pas besoin : **■** à côté de lui dans le panneau **Agents** ;
13+ - fermez les applications dont vous ne vous servez pas.
14+
15+## Ce que veulent dire les chiffres
16+
17+- **Sous macOS**, le niveau est celui que donne le système, le même que le graphique *Pression de la mémoire* de Moniteur d'activité. La mémoire utilisée est comptée comme dans Moniteur d'activité : mémoire des apps, mémoire résidente et mémoire compressée. La mémoire « en cache » de macOS n'est pas comptée : elle est rendue dès qu'une application en a besoin.
18+- **Sous Windows** (et Linux), le niveau est déduit de la mémoire encore disponible : **élevée** sous 20 %, **critique** sous 10 %.
19+- Si la mémoire ne peut pas être mesurée, la ligne n'est pas affichée.
20+
21+## Voir aussi
22+
23+- [Référence : services appelés par l'interface](../reference/agents-et-evenements.md#services-appelés-par-linterface) (`SystemService.Memory`)
24+- [Travailler avec les fichiers et le terminal d'un agent](fichiers-et-terminal-d-un-agent.md)
new file mode 100644
@@ -0,0 +1,24 @@
1+# Surveiller la pression mémoire
2+
3+Les modèles locaux et les agents prennent beaucoup de mémoire. Quand l'ordinateur en manque, il ralentit, et sur un Mac avec peu de mémoire il peut arrêter des applications. Sidekicks affiche la pression sur la mémoire, à côté des journaux des agents.
4+
5+1. Cliquez sur **Log** dans la barre d'état (en bas à droite). Le panneau s'ouvre en bas.
6+2. En haut à droite du panneau, lisez la ligne **Pression mémoire :**
7+ - un point et un niveau : **normale** (vert), **élevée** (jaune) ou **critique** (rouge) ;
8+ - une barre et la mémoire utilisée, par exemple `18,2 / 32,0 Go utilisés`.
9+
10+ Survolez le niveau pour un rappel de ce qu'il faut faire. La ligne est mise à jour toutes les 5 secondes tant que le panneau est ouvert.
11+3. Quand le niveau est **élevée** ou **critique**, libérez de la mémoire :
12+ - arrêtez un agent dont vous n'avez pas besoin : **■** à côté de lui dans le panneau **Agents** ;
13+ - fermez les applications dont vous ne vous servez pas.
14+
15+## Ce que veulent dire les chiffres
16+
17+- **Sous macOS**, le niveau est celui que donne le système, le même que le graphique *Pression de la mémoire* de Moniteur d'activité. La mémoire utilisée est comptée comme dans Moniteur d'activité : mémoire des apps, mémoire résidente et mémoire compressée. La mémoire « en cache » de macOS n'est pas comptée : elle est rendue dès qu'une application en a besoin.
18+- **Sous Windows** (et Linux), le niveau est déduit de la mémoire encore disponible : **élevée** sous 20 %, **critique** sous 10 %.
19+- Si la mémoire ne peut pas être mesurée, la ligne n'est pas affichée.
20+
21+## Voir aussi
22+
23+- [Référence : services appelés par l'interface](../reference/agents-et-evenements.md#services-appelés-par-linterface) (`SystemService.Memory`)
24+- [Travailler avec les fichiers et le terminal d'un agent](fichiers-et-terminal-d-un-agent.md)
modified docs/fr/reference/agents-et-evenements.md +2 -0
@@ -51,6 +51,7 @@ Les noms et types Go sont dans `internal/events` ; les constantes et types TypeS
5151 | | `StopSandbox(name)`, `RemoveSandbox(name)` | Arrête l'agent, puis lance `runtime.lifecycle.stop` ou `remove` (conteneur ou sandbox) ; la sortie va dans le journal de l'agent. |
5252 | | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. |
5353 | `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |
54+| `SystemService` | `Memory()` | `level` (`normal`, `warning`, `critical`), `used` et `total` en octets ; tout vide (`total` à 0) là où la mémoire ne peut pas être mesurée. macOS : le niveau de `kern.memorystatus_vm_pressure_level`, utilisée = mémoire des apps + résidente + compressée (`vm_stat`), comme dans Moniteur d'activité. Linux et Windows : déduit de la mémoire disponible, `warning` sous 20 %, `critical` sous 10 %. Le panneau Log l'appelle toutes les 5 secondes tant qu'il est ouvert (issue #61). |
5455
5556 ## Channels (milestones M2 et M3)
5657
@@ -130,6 +131,7 @@ Un appel d'outil a `id, title, kind` (`read, edit, delete, move, search, execute
130131 | `Create(name, description)` | Crée un channel. Un nom déjà pris ou vide est une erreur. |
131132 | `Rename(id, name, description)` | Renomme un channel (pas une conversation directe). |
132133 | `Delete(id)` | Arrête ses réponses, puis supprime le channel et tout son historique. |
134+| `Clear(id)` | Efface tous les messages d'un channel ou d'une conversation directe et garde son nom, sa description et ses membres. Les sessions des membres sont oubliées (`sessionId` et `lastSeen` vidés) : leur prochain tour ouvre une nouvelle session, qui ne sait rien de ce qui a été effacé ; l'usage est remis à zéro. Refusé tant que le channel répond ou a des messages en attente (`an agent is answering in this channel…`). Envoie `channels:changed`. |
133135 | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invite ou retire un agent ; le channel affiche `agent-joined` ou `agent-left`. |
134136 | `Direct(agent)` | La conversation directe avec `agent`, créée la première fois. |
135137 | `Send(id, text)` | Ajoute un message de l'utilisateur et renvoie son id ; les agents qui répondent sont démarrés si besoin ; le message attend si le channel répond déjà. Un texte vide ou un channel inconnu sont des erreurs. Chaque `$chemin` joint un fichier du dossier d'un agent (voir [Fichiers joints](#fichiers-joints)) ; plus de 10 est une erreur. |
@@ -51,6 +51,7 @@ Les noms et types Go sont dans `internal/events` ; les constantes et types TypeS
51 | | `StopSandbox(name)`, `RemoveSandbox(name)` | Arrête l'agent, puis lance `runtime.lifecycle.stop` ou `remove` (conteneur ou sandbox) ; la sortie va dans le journal de l'agent. |51 | | `StopSandbox(name)`, `RemoveSandbox(name)` | Arrête l'agent, puis lance `runtime.lifecycle.stop` ou `remove` (conteneur ou sandbox) ; la sortie va dans le journal de l'agent. |
52 | | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. |52 | | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. |
53 | `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |53 | `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |
54+| `SystemService` | `Memory()` | `level` (`normal`, `warning`, `critical`), `used` et `total` en octets ; tout vide (`total` à 0) là où la mémoire ne peut pas être mesurée. macOS : le niveau de `kern.memorystatus_vm_pressure_level`, utilisée = mémoire des apps + résidente + compressée (`vm_stat`), comme dans Moniteur d'activité. Linux et Windows : déduit de la mémoire disponible, `warning` sous 20 %, `critical` sous 10 %. Le panneau Log l'appelle toutes les 5 secondes tant qu'il est ouvert (issue #61). |
54 55
55 ## Channels (milestones M2 et M3)56 ## Channels (milestones M2 et M3)
56 57
@@ -130,6 +131,7 @@ Un appel d'outil a `id, title, kind` (`read, edit, delete, move, search, execute
130 | `Create(name, description)` | Crée un channel. Un nom déjà pris ou vide est une erreur. |131 | `Create(name, description)` | Crée un channel. Un nom déjà pris ou vide est une erreur. |
131 | `Rename(id, name, description)` | Renomme un channel (pas une conversation directe). |132 | `Rename(id, name, description)` | Renomme un channel (pas une conversation directe). |
132 | `Delete(id)` | Arrête ses réponses, puis supprime le channel et tout son historique. |133 | `Delete(id)` | Arrête ses réponses, puis supprime le channel et tout son historique. |
134+| `Clear(id)` | Efface tous les messages d'un channel ou d'une conversation directe et garde son nom, sa description et ses membres. Les sessions des membres sont oubliées (`sessionId` et `lastSeen` vidés) : leur prochain tour ouvre une nouvelle session, qui ne sait rien de ce qui a été effacé ; l'usage est remis à zéro. Refusé tant que le channel répond ou a des messages en attente (`an agent is answering in this channel…`). Envoie `channels:changed`. |
133 | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invite ou retire un agent ; le channel affiche `agent-joined` ou `agent-left`. |135 | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invite ou retire un agent ; le channel affiche `agent-joined` ou `agent-left`. |
134 | `Direct(agent)` | La conversation directe avec `agent`, créée la première fois. |136 | `Direct(agent)` | La conversation directe avec `agent`, créée la première fois. |
135 | `Send(id, text)` | Ajoute un message de l'utilisateur et renvoie son id ; les agents qui répondent sont démarrés si besoin ; le message attend si le channel répond déjà. Un texte vide ou un channel inconnu sont des erreurs. Chaque `$chemin` joint un fichier du dossier d'un agent (voir [Fichiers joints](#fichiers-joints)) ; plus de 10 est une erreur. |137 | `Send(id, text)` | Ajoute un message de l'utilisateur et renvoie son id ; les agents qui répondent sont démarrés si besoin ; le message attend si le channel répond déjà. Un texte vide ou un channel inconnu sont des erreurs. Chaque `$chemin` joint un fichier du dossier d'un agent (voir [Fichiers joints](#fichiers-joints)) ; plus de 10 est une erreur. |
modified docs/fr/tutorials/premier-agent.md +1 -1
@@ -38,7 +38,7 @@ Laissez **Sur cet ordinateur** sélectionné. Remplissez :
3838
3939 Cliquez sur **Ajouter l'agent**.
4040
41-Vous devez voir l'assistant se fermer, **🤖 Alice (assistante)** apparaître sous **AGENTS** et sous **MESSAGES DIRECTS**, et sa conversation s'ouvrir au centre avec « Écrivez votre premier message à Alice (assistante). »
41+Vous devez voir l'assistant se fermer, **🤖 Alice (assistante)** apparaître sous **AGENTS**, et sa conversation s'ouvrir au centre avec « Écrivez votre premier message à Alice (assistante). »
4242
4343 ## Étape 4 — Discuter avec Alice
4444
@@ -38,7 +38,7 @@ Laissez **Sur cet ordinateur** sélectionné. Remplissez :
38 38
39 Cliquez sur **Ajouter l'agent**.39 Cliquez sur **Ajouter l'agent**.
40 40
41-Vous devez voir l'assistant se fermer, **🤖 Alice (assistante)** apparaître sous **AGENTS** et sous **MESSAGES DIRECTS**, et sa conversation s'ouvrir au centre avec « Écrivez votre premier message à Alice (assistante). »41+Vous devez voir l'assistant se fermer, **🤖 Alice (assistante)** apparaître sous **AGENTS**, et sa conversation s'ouvrir au centre avec « Écrivez votre premier message à Alice (assistante). »
42 42
43 ## Étape 4 — Discuter avec Alice43 ## Étape 4 — Discuter avec Alice
44 44
modified frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/channelservice.ts +8 -0
@@ -46,6 +46,14 @@ export function Cancel(id: string): $CancellablePromise<void> {
4646 return $Call.ByID(1498855258, id);
4747 }
4848
49+/**
50+ * Clear deletes the messages of a channel or direct conversation and keeps
51+ * its members; the agents start a new session (issue #62).
52+ */
53+export function Clear(id: string): $CancellablePromise<void> {
54+ return $Call.ByID(3078585069, id);
55+}
56+
4957 /**
5058 * Commands returns the slash commands an agent offers.
5159 */
@@ -46,6 +46,14 @@ export function Cancel(id: string): $CancellablePromise<void> {
46 return $Call.ByID(1498855258, id);46 return $Call.ByID(1498855258, id);
47 }47 }
48 48
49+/**
50+ * Clear deletes the messages of a channel or direct conversation and keeps
51+ * its members; the agents start a new session (issue #62).
52+ */
53+export function Clear(id: string): $CancellablePromise<void> {
54+ return $Call.ByID(3078585069, id);
55+}
56+
49 /**57 /**
50 * Commands returns the slash commands an agent offers.58 * Commands returns the slash commands an agent offers.
51 */59 */
modified frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/index.ts +2 -0
@@ -4,12 +4,14 @@
44 import * as AgentService from "./agentservice.js";
55 import * as ChannelService from "./channelservice.js";
66 import * as ConfigService from "./configservice.js";
7+import * as SystemService from "./systemservice.js";
78 import * as TerminalService from "./terminalservice.js";
89 import * as WorkspaceService from "./workspaceservice.js";
910 export {
1011 AgentService,
1112 ChannelService,
1213 ConfigService,
14+ SystemService,
1315 TerminalService,
1416 WorkspaceService
1517 };
@@ -4,12 +4,14 @@
4 import * as AgentService from "./agentservice.js";4 import * as AgentService from "./agentservice.js";
5 import * as ChannelService from "./channelservice.js";5 import * as ChannelService from "./channelservice.js";
6 import * as ConfigService from "./configservice.js";6 import * as ConfigService from "./configservice.js";
7+import * as SystemService from "./systemservice.js";
7 import * as TerminalService from "./terminalservice.js";8 import * as TerminalService from "./terminalservice.js";
8 import * as WorkspaceService from "./workspaceservice.js";9 import * as WorkspaceService from "./workspaceservice.js";
9 export {10 export {
10 AgentService,11 AgentService,
11 ChannelService,12 ChannelService,
12 ConfigService,13 ConfigService,
14+ SystemService,
13 TerminalService,15 TerminalService,
14 WorkspaceService16 WorkspaceService
15 };17 };
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/systemservice.ts +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * SystemService tells the interface about the computer Sidekicks runs on.
6+ * @module
7+ */
8+
9+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
10+// @ts-ignore: Unused imports
11+import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
12+
13+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
14+// @ts-ignore: Unused imports
15+import * as memory$0 from "../memory/models.js";
16+
17+/**
18+ * Memory returns the memory pressure and the used and total memory (issue
19+ * #61). Where it cannot be measured, Total is 0 and the interface shows
20+ * nothing.
21+ */
22+export function Memory(): $CancellablePromise<memory$0.Status> {
23+ return $Call.ByID(3344638405);
24+}
new file mode 100644
@@ -0,0 +1,24 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * SystemService tells the interface about the computer Sidekicks runs on.
6+ * @module
7+ */
8+
9+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
10+// @ts-ignore: Unused imports
11+import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
12+
13+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
14+// @ts-ignore: Unused imports
15+import * as memory$0 from "../memory/models.js";
16+
17+/**
18+ * Memory returns the memory pressure and the used and total memory (issue
19+ * #61). Where it cannot be measured, Total is 0 and the interface shows
20+ * nothing.
21+ */
22+export function Memory(): $CancellablePromise<memory$0.Status> {
23+ return $Call.ByID(3344638405);
24+}
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/memory/index.ts +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+export {
5+ Level
6+} from "./models.js";
7+
8+export type {
9+ Status
10+} from "./models.js";
new file mode 100644
@@ -0,0 +1,10 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+export {
5+ Level
6+} from "./models.js";
7+
8+export type {
9+ Status
10+} from "./models.js";
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/memory/models.ts +38 -0
new file mode 100644
@@ -0,0 +1,38 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * Level is how much the memory is under pressure.
6+ */
7+export enum Level {
8+ /**
9+ * The Go zero value for the underlying type of the enum.
10+ */
11+ $zero = "",
12+
13+ /**
14+ * The levels, with the colours of Activity Monitor.
15+ *
16+ * green
17+ */
18+ Normal = "normal",
19+
20+ /**
21+ * yellow
22+ */
23+ Warning = "warning",
24+
25+ /**
26+ * red
27+ */
28+ Critical = "critical",
29+};
30+
31+/**
32+ * Status is the memory of the computer at one moment, in bytes.
33+ */
34+export interface Status {
35+ "level": Level;
36+ "used": number;
37+ "total": number;
38+}
new file mode 100644
@@ -0,0 +1,38 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * Level is how much the memory is under pressure.
6+ */
7+export enum Level {
8+ /**
9+ * The Go zero value for the underlying type of the enum.
10+ */
11+ $zero = "",
12+
13+ /**
14+ * The levels, with the colours of Activity Monitor.
15+ *
16+ * green
17+ */
18+ Normal = "normal",
19+
20+ /**
21+ * yellow
22+ */
23+ Warning = "warning",
24+
25+ /**
26+ * red
27+ */
28+ Critical = "critical",
29+};
30+
31+/**
32+ * Status is the memory of the computer at one moment, in bytes.
33+ */
34+export interface Status {
35+ "level": Level;
36+ "used": number;
37+ "total": number;
38+}
modified frontend/src/lib/chat/Composer.svelte +9 -0
@@ -81,6 +81,14 @@
8181 trigger = null
8282 }
8383
84+ // Issue #63: the selected suggestion is scrolled into view, so that the
85+ // arrows reach every agent, command or file of a long list.
86+ function shownWhen(element: HTMLElement, selected: boolean) {
87+ const show = (on: boolean) => { if (on) element.scrollIntoView?.({ block: 'nearest' }) }
88+ show(selected)
89+ return { update: show }
90+ }
91+
8492 // Keys for the popup when it has suggestions; true when the key was used.
8593 function popupKey(event: KeyboardEvent): boolean {
8694 const items = suggestions?.items ?? []
@@ -153,6 +161,7 @@
153161 role="option"
154162 aria-selected={index === active}
155163 class:active={index === active}
164+ use:shownWhen={index === active}
156165 onmousedown={(event) => { event.preventDefault(); choose(suggestion) }}
157166 >
158167 <span class="label">{#if suggestion.agent}<AgentAvatar icon={suggestion.agent.icon} title={suggestion.agent.title} name={suggestion.agent.name} size={16} /> {/if}{suggestion.label}</span>
@@ -81,6 +81,14 @@
81 trigger = null81 trigger = null
82 }82 }
83 83
84+ // Issue #63: the selected suggestion is scrolled into view, so that the
85+ // arrows reach every agent, command or file of a long list.
86+ function shownWhen(element: HTMLElement, selected: boolean) {
87+ const show = (on: boolean) => { if (on) element.scrollIntoView?.({ block: 'nearest' }) }
88+ show(selected)
89+ return { update: show }
90+ }
91+
84 // Keys for the popup when it has suggestions; true when the key was used.92 // Keys for the popup when it has suggestions; true when the key was used.
85 function popupKey(event: KeyboardEvent): boolean {93 function popupKey(event: KeyboardEvent): boolean {
86 const items = suggestions?.items ?? []94 const items = suggestions?.items ?? []
@@ -153,6 +161,7 @@
153 role="option"161 role="option"
154 aria-selected={index === active}162 aria-selected={index === active}
155 class:active={index === active}163 class:active={index === active}
164+ use:shownWhen={index === active}
156 onmousedown={(event) => { event.preventDefault(); choose(suggestion) }}165 onmousedown={(event) => { event.preventDefault(); choose(suggestion) }}
157 >166 >
158 <span class="label">{#if suggestion.agent}<AgentAvatar icon={suggestion.agent.icon} title={suggestion.agent.title} name={suggestion.agent.name} size={16} /> {/if}{suggestion.label}</span>167 <span class="label">{#if suggestion.agent}<AgentAvatar icon={suggestion.agent.icon} title={suggestion.agent.title} name={suggestion.agent.name} size={16} /> {/if}{suggestion.label}</span>
modified frontend/src/lib/conversation.test.ts +6 -5
@@ -3,7 +3,7 @@
33 import { describe, expect, it } from 'vitest'
44 import type { Channel, Message } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'
55 import { Role } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'
6-import { activityOf, addPermission, answerText, visibleDirects, applyDelta, applyMessage, emptyConversation, prependMessages, removePermission, usagePercent } from './conversation'
6+import { activityOf, addPermission, answerText, directOf, applyDelta, applyMessage, emptyConversation, prependMessages, removePermission, usagePercent } from './conversation'
77
88 const general: Channel = { id: 'c1', name: 'general', description: '', direct: '', members: ['alice'] }
99
@@ -80,10 +80,11 @@ describe('conversation events', () => {
8080 expect(activityOf({ ...message('m-3'), role: Role.RoleUser })).toBeNull()
8181 })
8282
83- it('lists the direct conversations of the configured agents only', () => {
84- const direct = (agent: string): Channel => ({ id: agent, name: '', description: '', direct: agent, members: [agent] })
85- const channels = [general, direct('alice'), direct('boxed')]
86- expect(visibleDirects(channels, ['alice', 'bob']).map((channel) => channel.direct)).toEqual(['alice'])
83+ it('finds the direct conversation of an agent', () => {
84+ const direct = (agent: string): Channel => ({ id: `d-${agent}`, name: '', description: '', direct: agent, members: [agent] })
85+ const channels = [general, direct('alice')]
86+ expect(directOf(channels, 'alice')?.id).toBe('d-alice')
87+ expect(directOf(channels, 'bob')).toBeUndefined()
8788 })
8889 })
8990
@@ -3,7 +3,7 @@
3 import { describe, expect, it } from 'vitest'3 import { describe, expect, it } from 'vitest'
4 import type { Channel, Message } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'4 import type { Channel, Message } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'
5 import { Role } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'5 import { Role } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'
6-import { activityOf, addPermission, answerText, visibleDirects, applyDelta, applyMessage, emptyConversation, prependMessages, removePermission, usagePercent } from './conversation'6+import { activityOf, addPermission, answerText, directOf, applyDelta, applyMessage, emptyConversation, prependMessages, removePermission, usagePercent } from './conversation'
7 7
8 const general: Channel = { id: 'c1', name: 'general', description: '', direct: '', members: ['alice'] }8 const general: Channel = { id: 'c1', name: 'general', description: '', direct: '', members: ['alice'] }
9 9
@@ -80,10 +80,11 @@ describe('conversation events', () => {
80 expect(activityOf({ ...message('m-3'), role: Role.RoleUser })).toBeNull()80 expect(activityOf({ ...message('m-3'), role: Role.RoleUser })).toBeNull()
81 })81 })
82 82
83- it('lists the direct conversations of the configured agents only', () => {83+ it('finds the direct conversation of an agent', () => {
84- const direct = (agent: string): Channel => ({ id: agent, name: '', description: '', direct: agent, members: [agent] })84+ const direct = (agent: string): Channel => ({ id: `d-${agent}`, name: '', description: '', direct: agent, members: [agent] })
85- const channels = [general, direct('alice'), direct('boxed')]85+ const channels = [general, direct('alice')]
86- expect(visibleDirects(channels, ['alice', 'bob']).map((channel) => channel.direct)).toEqual(['alice'])86+ expect(directOf(channels, 'alice')?.id).toBe('d-alice')
87+ expect(directOf(channels, 'bob')).toBeUndefined()
87 })88 })
88 })89 })
89 90
modified frontend/src/lib/conversation.ts +5 -6
@@ -90,15 +90,14 @@ export function activityOf(message: Message): Activity | null {
9090 }
9191
9292 /**
93- * The direct conversations to list: those whose agent is still in the
94- * configuration. The others stay in the history and come back with their
95- * agent.
93+ * The direct conversation with an agent, if there is one yet: the Agents
94+ * panel shows its unread messages next to the agent (issue #60).
9695 *
9796 * @example
98- * visibleDirects(channels, ['alice', 'bob'])
97+ * directOf(channels, 'alice')?.id
9998 */
100-export function visibleDirects(channels: Channel[], agents: string[]): Channel[] {
101- return channels.filter((channel) => channel.direct && agents.includes(channel.direct))
99+export function directOf(channels: Channel[], agent: string): Channel | undefined {
100+ return channels.find((channel) => channel.direct === agent)
102101 }
103102
104103 /**
@@ -90,15 +90,14 @@ export function activityOf(message: Message): Activity | null {
90 }90 }
91 91
92 /**92 /**
93- * The direct conversations to list: those whose agent is still in the93+ * The direct conversation with an agent, if there is one yet: the Agents
94- * configuration. The others stay in the history and come back with their94+ * panel shows its unread messages next to the agent (issue #60).
95- * agent.
96 *95 *
97 * @example96 * @example
98- * visibleDirects(channels, ['alice', 'bob'])97+ * directOf(channels, 'alice')?.id
99 */98 */
100-export function visibleDirects(channels: Channel[], agents: string[]): Channel[] {99+export function directOf(channels: Channel[], agent: string): Channel | undefined {
101- return channels.filter((channel) => channel.direct && agents.includes(channel.direct))100+ return channels.find((channel) => channel.direct === agent)
102 }101 }
103 102
104 /**103 /**
modified frontend/src/lib/errors.test.ts +4 -0
@@ -28,6 +28,10 @@ describe('explain', () => {
2828 expect(explain('unsupported ACP protocol version: the agent speaks version 2')?.key).toBe('error.protocol')
2929 })
3030
31+ it('recognises a history that cannot be cleared during an answer', () => {
32+ expect(explain('an agent is answering in this channel: stop it or wait for the end of its answer')?.key).toBe('error.channelBusy')
33+ })
34+
3135 it('leaves unknown errors alone', () => {
3236 expect(explain('something odd happened')).toBeNull()
3337 expect(explain('')).toBeNull()
@@ -28,6 +28,10 @@ describe('explain', () => {
28 expect(explain('unsupported ACP protocol version: the agent speaks version 2')?.key).toBe('error.protocol')28 expect(explain('unsupported ACP protocol version: the agent speaks version 2')?.key).toBe('error.protocol')
29 })29 })
30 30
31+ it('recognises a history that cannot be cleared during an answer', () => {
32+ expect(explain('an agent is answering in this channel: stop it or wait for the end of its answer')?.key).toBe('error.channelBusy')
33+ })
34+
31 it('leaves unknown errors alone', () => {35 it('leaves unknown errors alone', () => {
32 expect(explain('something odd happened')).toBeNull()36 expect(explain('something odd happened')).toBeNull()
33 expect(explain('')).toBeNull()37 expect(explain('')).toBeNull()
modified frontend/src/lib/errors.ts +1 -0
@@ -35,6 +35,7 @@ const rules: Rule[] = [
3535 { pattern: /the agent stopped/, key: 'error.agentStopped' },
3636 { pattern: /unknown agent:?\s*(\S*)/, key: 'error.unknownAgent', groups: ['agent'] },
3737 { pattern: /the history was written by a newer Sidekicks/, key: 'error.newerHistory' },
38+ { pattern: /an agent is answering in this channel/, key: 'error.channelBusy' },
3839 ]
3940
4041 /**
@@ -35,6 +35,7 @@ const rules: Rule[] = [
35 { pattern: /the agent stopped/, key: 'error.agentStopped' },35 { pattern: /the agent stopped/, key: 'error.agentStopped' },
36 { pattern: /unknown agent:?\s*(\S*)/, key: 'error.unknownAgent', groups: ['agent'] },36 { pattern: /unknown agent:?\s*(\S*)/, key: 'error.unknownAgent', groups: ['agent'] },
37 { pattern: /the history was written by a newer Sidekicks/, key: 'error.newerHistory' },37 { pattern: /the history was written by a newer Sidekicks/, key: 'error.newerHistory' },
38+ { pattern: /an agent is answering in this channel/, key: 'error.channelBusy' },
38 ]39 ]
39 40
40 /**41 /**
modified frontend/src/lib/i18n/messages.ts +24 -4
@@ -27,8 +27,6 @@ export const en = {
2727
2828 // channels
2929 'channels.empty': 'No channel yet.',
30- 'channels.section.direct': 'Direct messages',
31- 'channels.noDirect': 'Click an agent to talk with it directly.',
3230 'channels.new': 'New channel',
3331 'channels.namePlaceholder': 'Channel name',
3432 'channels.create': 'Create',
@@ -43,6 +41,12 @@ export const en = {
4341 // log
4442 'logs.of': 'Log of',
4543 'logs.empty': 'Nothing yet: the log fills up when the agent starts.',
44+ 'memory.pressure': 'Memory pressure:',
45+ 'memory.level.normal': 'normal',
46+ 'memory.level.warning': 'high',
47+ 'memory.level.critical': 'critical',
48+ 'memory.used': '{used} / {total} GB used',
49+ 'memory.help': 'How much the memory of this computer is under pressure, as in Activity Monitor. When it is high or critical, stop an agent you do not need or unload its model.',
4650
4751 // status bar
4852 'status.agents.one': '{count} active agent',
@@ -66,6 +70,11 @@ export const en = {
6670 'conversation.invite': '+ Invite',
6771 'conversation.inviteLabel': 'Invite an agent into the channel',
6872 'conversation.removeMember': 'Remove {agent} from the channel',
73+ 'conversation.clear': 'Clear the history',
74+ 'conversation.clearConfirmChannel': 'Erase every message of #{channel}? Its members stay, and start a new conversation.',
75+ 'conversation.clearConfirmDirect': 'Erase every message with {agent}? {agent} starts a new conversation and will not remember this one.',
76+ 'conversation.clearYes': 'Erase',
77+ 'conversation.clearNo': 'Cancel',
6978 'conversation.historyProblem': 'History unavailable ({problem}): this session will be forgotten when Sidekicks quits.',
7079
7180 // composer
@@ -149,6 +158,7 @@ export const en = {
149158 'error.agentStopped': 'The agent stopped unexpectedly. Its log (📂, then Log) tells why.',
150159 'error.unknownAgent': 'The agent "{agent}" is no longer in the configuration.',
151160 'error.newerHistory': 'The history was written by a newer Sidekicks. Update Sidekicks to read it.',
161+ 'error.channelBusy': 'An agent is answering here: stop it (■) or wait for the end of its answer, then clear the history.',
152162 'code.copy': 'Copy',
153163 'code.copied': 'Copied ✓',
154164 'message.copy': 'Copy the answer',
@@ -364,8 +374,6 @@ export const fr: Record<MessageKey, string> = {
364374 'agent.open': 'Ouvrir la conversation avec {agent}',
365375
366376 'channels.empty': 'Aucun channel pour le moment.',
367- 'channels.section.direct': 'Messages directs',
368- 'channels.noDirect': 'Cliquez sur un agent pour lui parler directement.',
369377 'channels.new': 'Nouveau channel',
370378 'channels.namePlaceholder': 'Nom du channel',
371379 'channels.create': 'Créer',
@@ -379,6 +387,12 @@ export const fr: Record<MessageKey, string> = {
379387
380388 'logs.of': 'Journal de',
381389 'logs.empty': "Rien pour l'instant : le journal se remplit quand l'agent démarre.",
390+ 'memory.pressure': 'Pression mémoire :',
391+ 'memory.level.normal': 'normale',
392+ 'memory.level.warning': 'élevée',
393+ 'memory.level.critical': 'critique',
394+ 'memory.used': '{used} / {total} Go utilisés',
395+ 'memory.help': "La pression sur la mémoire de cet ordinateur, comme dans Moniteur d'activité. Quand elle est élevée ou critique, arrêtez un agent dont vous n'avez pas besoin ou déchargez son modèle.",
382396
383397 'status.agents.one': '{count} agent actif',
384398 'status.agents.other': '{count} agents actifs',
@@ -400,6 +414,11 @@ export const fr: Record<MessageKey, string> = {
400414 'conversation.invite': '+ Inviter',
401415 'conversation.inviteLabel': 'Inviter un agent dans le channel',
402416 'conversation.removeMember': 'Retirer {agent} du channel',
417+ 'conversation.clear': "Vider l'historique",
418+ 'conversation.clearConfirmChannel': 'Effacer tous les messages de #{channel} ? Ses membres restent, et repartent sur une nouvelle conversation.',
419+ 'conversation.clearConfirmDirect': "Effacer tous les messages avec {agent} ? {agent} repart sur une nouvelle conversation et ne se souviendra pas de celle-ci.",
420+ 'conversation.clearYes': 'Effacer',
421+ 'conversation.clearNo': 'Annuler',
403422 'conversation.historyProblem': "Historique indisponible ({problem}) : cette session sera oubliée à la fermeture de Sidekicks.",
404423
405424 'composer.label': 'Votre message',
@@ -478,6 +497,7 @@ export const fr: Record<MessageKey, string> = {
478497 'error.agentStopped': "L'agent s'est arrêté de façon inattendue. Son journal (📂, puis Journal) en donne la raison.",
479498 'error.unknownAgent': "L'agent « {agent} » n'est plus dans la configuration.",
480499 'error.newerHistory': "L'historique a été écrit par un Sidekicks plus récent. Mettez Sidekicks à jour pour le lire.",
500+ 'error.channelBusy': "Un agent répond ici : arrêtez-le (■) ou attendez la fin de sa réponse, puis videz l'historique.",
481501 'code.copy': 'Copier',
482502 'code.copied': 'Copié ✓',
483503 'message.copy': 'Copier la réponse',
@@ -27,8 +27,6 @@ export const en = {
27 27
28 // channels28 // channels
29 'channels.empty': 'No channel yet.',29 'channels.empty': 'No channel yet.',
30- 'channels.section.direct': 'Direct messages',
31- 'channels.noDirect': 'Click an agent to talk with it directly.',
32 'channels.new': 'New channel',30 'channels.new': 'New channel',
33 'channels.namePlaceholder': 'Channel name',31 'channels.namePlaceholder': 'Channel name',
34 'channels.create': 'Create',32 'channels.create': 'Create',
@@ -43,6 +41,12 @@ export const en = {
43 // log41 // log
44 'logs.of': 'Log of',42 'logs.of': 'Log of',
45 'logs.empty': 'Nothing yet: the log fills up when the agent starts.',43 'logs.empty': 'Nothing yet: the log fills up when the agent starts.',
44+ 'memory.pressure': 'Memory pressure:',
45+ 'memory.level.normal': 'normal',
46+ 'memory.level.warning': 'high',
47+ 'memory.level.critical': 'critical',
48+ 'memory.used': '{used} / {total} GB used',
49+ 'memory.help': 'How much the memory of this computer is under pressure, as in Activity Monitor. When it is high or critical, stop an agent you do not need or unload its model.',
46 50
47 // status bar51 // status bar
48 'status.agents.one': '{count} active agent',52 'status.agents.one': '{count} active agent',
@@ -66,6 +70,11 @@ export const en = {
66 'conversation.invite': '+ Invite',70 'conversation.invite': '+ Invite',
67 'conversation.inviteLabel': 'Invite an agent into the channel',71 'conversation.inviteLabel': 'Invite an agent into the channel',
68 'conversation.removeMember': 'Remove {agent} from the channel',72 'conversation.removeMember': 'Remove {agent} from the channel',
73+ 'conversation.clear': 'Clear the history',
74+ 'conversation.clearConfirmChannel': 'Erase every message of #{channel}? Its members stay, and start a new conversation.',
75+ 'conversation.clearConfirmDirect': 'Erase every message with {agent}? {agent} starts a new conversation and will not remember this one.',
76+ 'conversation.clearYes': 'Erase',
77+ 'conversation.clearNo': 'Cancel',
69 'conversation.historyProblem': 'History unavailable ({problem}): this session will be forgotten when Sidekicks quits.',78 'conversation.historyProblem': 'History unavailable ({problem}): this session will be forgotten when Sidekicks quits.',
70 79
71 // composer80 // composer
@@ -149,6 +158,7 @@ export const en = {
149 'error.agentStopped': 'The agent stopped unexpectedly. Its log (📂, then Log) tells why.',158 'error.agentStopped': 'The agent stopped unexpectedly. Its log (📂, then Log) tells why.',
150 'error.unknownAgent': 'The agent "{agent}" is no longer in the configuration.',159 'error.unknownAgent': 'The agent "{agent}" is no longer in the configuration.',
151 'error.newerHistory': 'The history was written by a newer Sidekicks. Update Sidekicks to read it.',160 'error.newerHistory': 'The history was written by a newer Sidekicks. Update Sidekicks to read it.',
161+ 'error.channelBusy': 'An agent is answering here: stop it (■) or wait for the end of its answer, then clear the history.',
152 'code.copy': 'Copy',162 'code.copy': 'Copy',
153 'code.copied': 'Copied ✓',163 'code.copied': 'Copied ✓',
154 'message.copy': 'Copy the answer',164 'message.copy': 'Copy the answer',
@@ -364,8 +374,6 @@ export const fr: Record<MessageKey, string> = {
364 'agent.open': 'Ouvrir la conversation avec {agent}',374 'agent.open': 'Ouvrir la conversation avec {agent}',
365 375
366 'channels.empty': 'Aucun channel pour le moment.',376 'channels.empty': 'Aucun channel pour le moment.',
367- 'channels.section.direct': 'Messages directs',
368- 'channels.noDirect': 'Cliquez sur un agent pour lui parler directement.',
369 'channels.new': 'Nouveau channel',377 'channels.new': 'Nouveau channel',
370 'channels.namePlaceholder': 'Nom du channel',378 'channels.namePlaceholder': 'Nom du channel',
371 'channels.create': 'Créer',379 'channels.create': 'Créer',
@@ -379,6 +387,12 @@ export const fr: Record<MessageKey, string> = {
379 387
380 'logs.of': 'Journal de',388 'logs.of': 'Journal de',
381 'logs.empty': "Rien pour l'instant : le journal se remplit quand l'agent démarre.",389 'logs.empty': "Rien pour l'instant : le journal se remplit quand l'agent démarre.",
390+ 'memory.pressure': 'Pression mémoire :',
391+ 'memory.level.normal': 'normale',
392+ 'memory.level.warning': 'élevée',
393+ 'memory.level.critical': 'critique',
394+ 'memory.used': '{used} / {total} Go utilisés',
395+ 'memory.help': "La pression sur la mémoire de cet ordinateur, comme dans Moniteur d'activité. Quand elle est élevée ou critique, arrêtez un agent dont vous n'avez pas besoin ou déchargez son modèle.",
382 396
383 'status.agents.one': '{count} agent actif',397 'status.agents.one': '{count} agent actif',
384 'status.agents.other': '{count} agents actifs',398 'status.agents.other': '{count} agents actifs',
@@ -400,6 +414,11 @@ export const fr: Record<MessageKey, string> = {
400 'conversation.invite': '+ Inviter',414 'conversation.invite': '+ Inviter',
401 'conversation.inviteLabel': 'Inviter un agent dans le channel',415 'conversation.inviteLabel': 'Inviter un agent dans le channel',
402 'conversation.removeMember': 'Retirer {agent} du channel',416 'conversation.removeMember': 'Retirer {agent} du channel',
417+ 'conversation.clear': "Vider l'historique",
418+ 'conversation.clearConfirmChannel': 'Effacer tous les messages de #{channel} ? Ses membres restent, et repartent sur une nouvelle conversation.',
419+ 'conversation.clearConfirmDirect': "Effacer tous les messages avec {agent} ? {agent} repart sur une nouvelle conversation et ne se souviendra pas de celle-ci.",
420+ 'conversation.clearYes': 'Effacer',
421+ 'conversation.clearNo': 'Annuler',
403 'conversation.historyProblem': "Historique indisponible ({problem}) : cette session sera oubliée à la fermeture de Sidekicks.",422 'conversation.historyProblem': "Historique indisponible ({problem}) : cette session sera oubliée à la fermeture de Sidekicks.",
404 423
405 'composer.label': 'Votre message',424 'composer.label': 'Votre message',
@@ -478,6 +497,7 @@ export const fr: Record<MessageKey, string> = {
478 'error.agentStopped': "L'agent s'est arrêté de façon inattendue. Son journal (📂, puis Journal) en donne la raison.",497 'error.agentStopped': "L'agent s'est arrêté de façon inattendue. Son journal (📂, puis Journal) en donne la raison.",
479 'error.unknownAgent': "L'agent « {agent} » n'est plus dans la configuration.",498 'error.unknownAgent': "L'agent « {agent} » n'est plus dans la configuration.",
480 'error.newerHistory': "L'historique a été écrit par un Sidekicks plus récent. Mettez Sidekicks à jour pour le lire.",499 'error.newerHistory': "L'historique a été écrit par un Sidekicks plus récent. Mettez Sidekicks à jour pour le lire.",
500+ 'error.channelBusy': "Un agent répond ici : arrêtez-le (■) ou attendez la fin de sa réponse, puis videz l'historique.",
481 'code.copy': 'Copier',501 'code.copy': 'Copier',
482 'code.copied': 'Copié ✓',502 'code.copied': 'Copié ✓',
483 'message.copy': 'Copier la réponse',503 'message.copy': 'Copier la réponse',
added frontend/src/lib/memory.test.ts +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it } from 'vitest'
4+import { Level } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/memory'
5+import { gigabytes, measured, usedPercent } from './memory'
6+
7+describe('memory', () => {
8+ it('writes gigabytes with one decimal, in the language of the interface', () => {
9+ expect(gigabytes(18.25 * 2 ** 30, 'en')).toBe('18.3')
10+ expect(gigabytes(18.25 * 2 ** 30, 'fr')).toBe('18,3')
11+ expect(gigabytes(32 * 2 ** 30, 'en')).toBe('32.0')
12+ })
13+ it('gives the share in use', () => {
14+ expect(usedPercent({ level: Level.Normal, used: 8, total: 32 })).toBe(25)
15+ expect(usedPercent({ level: Level.Normal, used: 40, total: 32 })).toBe(100)
16+ expect(usedPercent({ level: Level.$zero, used: 0, total: 0 })).toBe(0)
17+ })
18+ it('shows nothing without a measure', () => {
19+ expect(measured(undefined)).toBe(false)
20+ expect(measured({ level: Level.$zero, used: 0, total: 0 })).toBe(false)
21+ expect(measured({ level: Level.Warning, used: 20, total: 32 })).toBe(true)
22+ })
23+})
new file mode 100644
@@ -0,0 +1,23 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it } from 'vitest'
4+import { Level } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/memory'
5+import { gigabytes, measured, usedPercent } from './memory'
6+
7+describe('memory', () => {
8+ it('writes gigabytes with one decimal, in the language of the interface', () => {
9+ expect(gigabytes(18.25 * 2 ** 30, 'en')).toBe('18.3')
10+ expect(gigabytes(18.25 * 2 ** 30, 'fr')).toBe('18,3')
11+ expect(gigabytes(32 * 2 ** 30, 'en')).toBe('32.0')
12+ })
13+ it('gives the share in use', () => {
14+ expect(usedPercent({ level: Level.Normal, used: 8, total: 32 })).toBe(25)
15+ expect(usedPercent({ level: Level.Normal, used: 40, total: 32 })).toBe(100)
16+ expect(usedPercent({ level: Level.$zero, used: 0, total: 0 })).toBe(0)
17+ })
18+ it('shows nothing without a measure', () => {
19+ expect(measured(undefined)).toBe(false)
20+ expect(measured({ level: Level.$zero, used: 0, total: 0 })).toBe(false)
21+ expect(measured({ level: Level.Warning, used: 20, total: 32 })).toBe(true)
22+ })
23+})
added frontend/src/lib/memory.ts +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The memory of the computer, shown in the Log panel (issue #61).
4+
5+import type { Status } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/memory'
6+
7+/** How often the memory is measured, in milliseconds. */
8+export const memoryInterval = 5000
9+
10+/**
11+ * Bytes in gigabytes, with one decimal in the language of the interface.
12+ *
13+ * @example
14+ * gigabytes(19_542_000_000, 'fr') // "18,2"
15+ */
16+export function gigabytes(bytes: number, language: string): string {
17+ return (bytes / 2 ** 30).toLocaleString(language, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
18+}
19+
20+/**
21+ * The share of the memory in use, from 0 to 100; 0 when it is not known.
22+ *
23+ * @example
24+ * usedPercent({ level: 'normal', used: 8, total: 32 }) // 25
25+ */
26+export function usedPercent(status: Status): number {
27+ return status.total > 0 ? Math.min(100, Math.round((status.used / status.total) * 100)) : 0
28+}
29+
30+/** Whether there is a measure to show: none where the system gives none. */
31+export function measured(status: Status | undefined): status is Status {
32+ return !!status && status.total > 0 && !!status.level
33+}
new file mode 100644
@@ -0,0 +1,33 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The memory of the computer, shown in the Log panel (issue #61).
4+
5+import type { Status } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/memory'
6+
7+/** How often the memory is measured, in milliseconds. */
8+export const memoryInterval = 5000
9+
10+/**
11+ * Bytes in gigabytes, with one decimal in the language of the interface.
12+ *
13+ * @example
14+ * gigabytes(19_542_000_000, 'fr') // "18,2"
15+ */
16+export function gigabytes(bytes: number, language: string): string {
17+ return (bytes / 2 ** 30).toLocaleString(language, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
18+}
19+
20+/**
21+ * The share of the memory in use, from 0 to 100; 0 when it is not known.
22+ *
23+ * @example
24+ * usedPercent({ level: 'normal', used: 8, total: 32 }) // 25
25+ */
26+export function usedPercent(status: Status): number {
27+ return status.total > 0 ? Math.min(100, Math.round((status.used / status.total) * 100)) : 0
28+}
29+
30+/** Whether there is a measure to show: none where the system gives none. */
31+export function measured(status: Status | undefined): status is Status {
32+ return !!status && status.total > 0 && !!status.level
33+}
modified frontend/src/lib/panels/AgentsPanel.svelte +13 -0
@@ -6,9 +6,17 @@
66 import AgentAvatar from '../agent/AgentAvatar.svelte'
77 import { AgentService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
88 import { act, appState, channelOf, openAssistant, openDirect, openWorkspace } from '../state.svelte'
9+ import { directOf } from '../conversation'
910
1011 // The agent of the direct conversation shown, to highlight it.
1112 const directAgent = $derived(channelOf(appState.selectedChannel)?.direct ?? '')
13+
14+ // The messages of an agent's direct conversation not read yet: this list
15+ // is where direct conversations open (issue #60).
16+ function unreadOf(agent: string): number {
17+ const direct = directOf(appState.channels, agent)
18+ return direct ? appState.unread[direct.id] ?? 0 : 0
19+ }
1220 </script>
1321
1422 {#if appState.agents.length === 0}
@@ -19,6 +27,9 @@
1927 <li title={agent.description || agent.title} class:selected={appState.workspaceAgent ? appState.workspaceAgent === agent.name : directAgent === agent.name}>
2028 <span class="dot {agent.state}" aria-hidden="true"></span>
2129 <button class="name" onclick={() => openDirect(agent.name)} aria-label={t('agent.open', { agent: agent.title })}><AgentAvatar icon={agent.icon} title={agent.title} name={agent.name} /> {agent.title}</button>
30+ {#if unreadOf(agent.name)}
31+ <span class="badge" title={t('channels.unread', { count: unreadOf(agent.name) })}>{unreadOf(agent.name)}</span>
32+ {/if}
2233 <span class="state">{hasMessage(`agent.state.${agent.state}`) ? t(`agent.state.${agent.state}` as MessageKey) : agent.state}{agent.outdated ? t('agent.outdated') : ''}</span>
2334 <span class="actions">
2435 <button onclick={() => openWorkspace(agent.name)} aria-label={t('workspace.open', { agent: agent.title })} title={t('workspace.open', { agent: agent.title })}>📂</button>
@@ -52,9 +63,11 @@
5263 .dot.busy, .dot.starting { background: var(--warn); }
5364 .dot.error { background: var(--danger); }
5465 .name { grid-column: 2; grid-row: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66+ .badge { grid-column: 3; grid-row: 1; justify-self: end; background: var(--accent); color: #fff; border-radius: 9px; font-size: 11px; padding: 0 6px; line-height: 17px; }
5567 .state { grid-column: 2; grid-row: 2; color: var(--muted); font-size: 12px; }
5668 .actions { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; opacity: 0; }
5769 li:hover .actions, li:focus-within .actions { opacity: 1; }
70+ li:hover .badge, li:focus-within .badge { visibility: hidden; }
5871 .actions button { padding: 1px 7px; font-size: 11px; }
5972 .failure { grid-column: 2 / span 2; grid-row: 3; margin: 2px 0 0; color: var(--danger); font-size: 12px; white-space: pre-wrap; }
6073 .empty { color: var(--muted); padding: 0 8px; }
@@ -6,9 +6,17 @@
6 import AgentAvatar from '../agent/AgentAvatar.svelte'6 import AgentAvatar from '../agent/AgentAvatar.svelte'
7 import { AgentService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'7 import { AgentService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
8 import { act, appState, channelOf, openAssistant, openDirect, openWorkspace } from '../state.svelte'8 import { act, appState, channelOf, openAssistant, openDirect, openWorkspace } from '../state.svelte'
9+ import { directOf } from '../conversation'
9 10
10 // The agent of the direct conversation shown, to highlight it.11 // The agent of the direct conversation shown, to highlight it.
11 const directAgent = $derived(channelOf(appState.selectedChannel)?.direct ?? '')12 const directAgent = $derived(channelOf(appState.selectedChannel)?.direct ?? '')
13+
14+ // The messages of an agent's direct conversation not read yet: this list
15+ // is where direct conversations open (issue #60).
16+ function unreadOf(agent: string): number {
17+ const direct = directOf(appState.channels, agent)
18+ return direct ? appState.unread[direct.id] ?? 0 : 0
19+ }
12 </script>20 </script>
13 21
14 {#if appState.agents.length === 0}22 {#if appState.agents.length === 0}
@@ -19,6 +27,9 @@
19 <li title={agent.description || agent.title} class:selected={appState.workspaceAgent ? appState.workspaceAgent === agent.name : directAgent === agent.name}>27 <li title={agent.description || agent.title} class:selected={appState.workspaceAgent ? appState.workspaceAgent === agent.name : directAgent === agent.name}>
20 <span class="dot {agent.state}" aria-hidden="true"></span>28 <span class="dot {agent.state}" aria-hidden="true"></span>
21 <button class="name" onclick={() => openDirect(agent.name)} aria-label={t('agent.open', { agent: agent.title })}><AgentAvatar icon={agent.icon} title={agent.title} name={agent.name} /> {agent.title}</button>29 <button class="name" onclick={() => openDirect(agent.name)} aria-label={t('agent.open', { agent: agent.title })}><AgentAvatar icon={agent.icon} title={agent.title} name={agent.name} /> {agent.title}</button>
30+ {#if unreadOf(agent.name)}
31+ <span class="badge" title={t('channels.unread', { count: unreadOf(agent.name) })}>{unreadOf(agent.name)}</span>
32+ {/if}
22 <span class="state">{hasMessage(`agent.state.${agent.state}`) ? t(`agent.state.${agent.state}` as MessageKey) : agent.state}{agent.outdated ? t('agent.outdated') : ''}</span>33 <span class="state">{hasMessage(`agent.state.${agent.state}`) ? t(`agent.state.${agent.state}` as MessageKey) : agent.state}{agent.outdated ? t('agent.outdated') : ''}</span>
23 <span class="actions">34 <span class="actions">
24 <button onclick={() => openWorkspace(agent.name)} aria-label={t('workspace.open', { agent: agent.title })} title={t('workspace.open', { agent: agent.title })}>📂</button>35 <button onclick={() => openWorkspace(agent.name)} aria-label={t('workspace.open', { agent: agent.title })} title={t('workspace.open', { agent: agent.title })}>📂</button>
@@ -52,9 +63,11 @@
52 .dot.busy, .dot.starting { background: var(--warn); }63 .dot.busy, .dot.starting { background: var(--warn); }
53 .dot.error { background: var(--danger); }64 .dot.error { background: var(--danger); }
54 .name { grid-column: 2; grid-row: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }65 .name { grid-column: 2; grid-row: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66+ .badge { grid-column: 3; grid-row: 1; justify-self: end; background: var(--accent); color: #fff; border-radius: 9px; font-size: 11px; padding: 0 6px; line-height: 17px; }
55 .state { grid-column: 2; grid-row: 2; color: var(--muted); font-size: 12px; }67 .state { grid-column: 2; grid-row: 2; color: var(--muted); font-size: 12px; }
56 .actions { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; opacity: 0; }68 .actions { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; opacity: 0; }
57 li:hover .actions, li:focus-within .actions { opacity: 1; }69 li:hover .actions, li:focus-within .actions { opacity: 1; }
70+ li:hover .badge, li:focus-within .badge { visibility: hidden; }
58 .actions button { padding: 1px 7px; font-size: 11px; }71 .actions button { padding: 1px 7px; font-size: 11px; }
59 .failure { grid-column: 2 / span 2; grid-row: 3; margin: 2px 0 0; color: var(--danger); font-size: 12px; white-space: pre-wrap; }72 .failure { grid-column: 2 / span 2; grid-row: 3; margin: 2px 0 0; color: var(--danger); font-size: 12px; white-space: pre-wrap; }
60 .empty { color: var(--muted); padding: 0 8px; }73 .empty { color: var(--muted); padding: 0 8px; }
modified frontend/src/lib/panels/ChannelsPanel.svelte +2 -32
@@ -3,15 +3,11 @@
33 import { t } from '../i18n/index.svelte'
44 import { appState, createChannel, deleteChannel, renameChannel, selectChannel } from '../state.svelte'
55 import ActionError from '../chat/ActionError.svelte'
6- import AgentAvatar from '../agent/AgentAvatar.svelte'
7- import { visibleDirects } from '../conversation'
86 import type { Channel } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'
97
10- // Issue #21: the channels, created, renamed and deleted here; the direct
11- // conversations below them.
8+ // Issue #21: the channels, created, renamed and deleted here. The direct
9+ // conversations open from the Agents panel (issue #60).
1210 const channels = $derived(appState.channels.filter((channel) => !channel.direct))
13- // A removed agent's conversation is hidden, kept in the history.
14- const directs = $derived(visibleDirects(appState.channels, appState.agents.map((agent) => agent.name)))
1511
1612 let creating = $state(false)
1713 // ⌘N, or "New channel" in the palette.
@@ -55,10 +51,6 @@
5551 else if (event.key === 'Escape') escape()
5652 }
5753
58- function agentOf(name: string) {
59- return appState.agents.find((agent) => agent.name === name)
60- }
61-
6254 // Focuses an input as soon as it appears.
6355 function autofocus(element: HTMLInputElement) {
6456 element.focus()
@@ -122,29 +114,7 @@
122114 <button class="add" onclick={() => { appState.actionError = ''; creating = true }}>+ {t('channels.new')}</button>
123115 {/if}
124116
125-<div class="section">
126- <span class="heading">{t('channels.section.direct')}</span>
127-</div>
128-{#if directs.length === 0}
129- <p class="empty">{t('channels.noDirect')}</p>
130-{/if}
131-<ul>
132- {#each directs as channel (channel.id)}
133- {@const agent = agentOf(channel.direct)}
134- <li class:active={appState.selectedChannel === channel.id}>
135- <button class="name" onclick={() => selectChannel(channel.id)}>
136- <AgentAvatar icon={agent?.icon} title={agent?.title ?? channel.direct} name={channel.direct} /> {agent?.title ?? channel.direct}
137- </button>
138- {#if appState.unread[channel.id]}
139- <span class="badge" title={t('channels.unread', { count: appState.unread[channel.id] })}>{appState.unread[channel.id]}</span>
140- {/if}
141- </li>
142- {/each}
143-</ul>
144-
145117 <style>
146- .section { display: flex; align-items: center; justify-content: space-between; padding: 14px 8px 4px; }
147- .heading { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
148118 .add { margin: 4px 8px 0; border: none; background: none; padding: 2px 0; color: var(--muted); font-size: 12px; }
149119 .add:hover { color: var(--accent); }
150120 .form { display: flex; gap: 4px; padding: 0 8px 6px; }
@@ -3,15 +3,11 @@
3 import { t } from '../i18n/index.svelte'3 import { t } from '../i18n/index.svelte'
4 import { appState, createChannel, deleteChannel, renameChannel, selectChannel } from '../state.svelte'4 import { appState, createChannel, deleteChannel, renameChannel, selectChannel } from '../state.svelte'
5 import ActionError from '../chat/ActionError.svelte'5 import ActionError from '../chat/ActionError.svelte'
6- import AgentAvatar from '../agent/AgentAvatar.svelte'
7- import { visibleDirects } from '../conversation'
8 import type { Channel } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'6 import type { Channel } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels'
9 7
10- // Issue #21: the channels, created, renamed and deleted here; the direct8+ // Issue #21: the channels, created, renamed and deleted here. The direct
11- // conversations below them.9+ // conversations open from the Agents panel (issue #60).
12 const channels = $derived(appState.channels.filter((channel) => !channel.direct))10 const channels = $derived(appState.channels.filter((channel) => !channel.direct))
13- // A removed agent's conversation is hidden, kept in the history.
14- const directs = $derived(visibleDirects(appState.channels, appState.agents.map((agent) => agent.name)))
15 11
16 let creating = $state(false)12 let creating = $state(false)
17 // ⌘N, or "New channel" in the palette.13 // ⌘N, or "New channel" in the palette.
@@ -55,10 +51,6 @@
55 else if (event.key === 'Escape') escape()51 else if (event.key === 'Escape') escape()
56 }52 }
57 53
58- function agentOf(name: string) {
59- return appState.agents.find((agent) => agent.name === name)
60- }
61-
62 // Focuses an input as soon as it appears.54 // Focuses an input as soon as it appears.
63 function autofocus(element: HTMLInputElement) {55 function autofocus(element: HTMLInputElement) {
64 element.focus()56 element.focus()
@@ -122,29 +114,7 @@
122 <button class="add" onclick={() => { appState.actionError = ''; creating = true }}>+ {t('channels.new')}</button>114 <button class="add" onclick={() => { appState.actionError = ''; creating = true }}>+ {t('channels.new')}</button>
123 {/if}115 {/if}
124 116
125-<div class="section">
126- <span class="heading">{t('channels.section.direct')}</span>
127-</div>
128-{#if directs.length === 0}
129- <p class="empty">{t('channels.noDirect')}</p>
130-{/if}
131-<ul>
132- {#each directs as channel (channel.id)}
133- {@const agent = agentOf(channel.direct)}
134- <li class:active={appState.selectedChannel === channel.id}>
135- <button class="name" onclick={() => selectChannel(channel.id)}>
136- <AgentAvatar icon={agent?.icon} title={agent?.title ?? channel.direct} name={channel.direct} /> {agent?.title ?? channel.direct}
137- </button>
138- {#if appState.unread[channel.id]}
139- <span class="badge" title={t('channels.unread', { count: appState.unread[channel.id] })}>{appState.unread[channel.id]}</span>
140- {/if}
141- </li>
142- {/each}
143-</ul>
144-
145 <style>117 <style>
146- .section { display: flex; align-items: center; justify-content: space-between; padding: 14px 8px 4px; }
147- .heading { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
148 .add { margin: 4px 8px 0; border: none; background: none; padding: 2px 0; color: var(--muted); font-size: 12px; }118 .add { margin: 4px 8px 0; border: none; background: none; padding: 2px 0; color: var(--muted); font-size: 12px; }
149 .add:hover { color: var(--accent); }119 .add:hover { color: var(--accent); }
150 .form { display: flex; gap: 4px; padding: 0 8px 6px; }120 .form { display: flex; gap: 4px; padding: 0 8px 6px; }
modified frontend/src/lib/panels/ConversationPanel.svelte +21 -1
@@ -14,7 +14,7 @@
1414 import Logo from '../layout/Logo.svelte'
1515 import { isNearBottom, type Trigger } from '../composer'
1616 import { usagePercent } from '../conversation'
17- import { act, addMember, agentTitle, appState, cancel, channelOf, loadOlder, openAssistant, prepareCommands, removeMember, send } from '../state.svelte'
17+ import { act, addMember, agentTitle, appState, cancel, channelOf, clearHistory, loadOlder, openAssistant, prepareCommands, removeMember, send } from '../state.svelte'
1818 import { ConfigService, WorkspaceService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
1919 import type { FilesDroppedPayload } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/events'
2020 import { FilesDropped } from '../../../bindings/events'
@@ -39,6 +39,8 @@
3939 return conversation?.busy && last?.role === 'agent' && !last.done ? trailingFailures(last.parts ?? []) : 0
4040 })
4141 const stuckAgent = $derived(agentTitle(conversation?.answering ?? ''))
42+ // Issue #62: the channel whose history waits for a confirmation to be erased.
43+ let clearing = $state('')
4244
4345 function infoOf(name: string): AgentInfo {
4446 const agent = appState.agents.find((candidate) => candidate.name === name)
@@ -240,6 +242,17 @@
240242 {#if percent !== null}
241243 <span class="usage" title={t('conversation.usageHint')}>{t('conversation.usage', { percent })}</span>
242244 {/if}
245+ {#if (conversation.messages ?? []).length > 0}
246+ <button class="clear" class:alone={percent === null} onclick={() => { appState.actionError = ''; clearing = channel.id }} aria-label={t('conversation.clear')} title={t('conversation.clear')}>🗑</button>
247+ {/if}
248+ {#if clearing === channel.id}
249+ {@const question = channel.direct ? t('conversation.clearConfirmDirect', { agent: title }) : t('conversation.clearConfirmChannel', { channel: channel.name })}
250+ <div class="confirm" role="alertdialog" aria-label={question}>
251+ <p>{question}</p>
252+ <button class="danger" onclick={() => { clearing = ''; void clearHistory(channel.id) }}>{t('conversation.clearYes')}</button>
253+ <button onclick={() => (clearing = '')}>{t('conversation.clearNo')}</button>
254+ </div>
255+ {/if}
243256 </header>
244257 <div class="messages" bind:this={list} onscroll={onScroll}>
245258 {#if conversation.more}
@@ -333,6 +346,13 @@
333346 .busy { color: var(--warn); font-size: 12px; font-style: italic; display: inline-flex; align-items: center; gap: 6px; }
334347 .pending { color: var(--muted); font-size: 13px; font-style: italic; display: flex; align-items: center; gap: 8px; margin: 0; }
335348 .usage { margin-left: auto; color: var(--muted); font-size: 12px; border: 1px solid var(--border); border-radius: 10px; padding: 1px 10px; }
349+ .clear { border: none; background: none; padding: 0 4px; font-size: 13px; opacity: 0.6; }
350+ .clear:hover { opacity: 1; }
351+ .clear.alone { margin-left: auto; }
352+ .confirm { flex-basis: 100%; padding: 6px 10px; border: 1px solid var(--danger); border-radius: 6px; font-size: 12px; }
353+ .confirm p { margin: 0 0 6px; }
354+ .confirm button { font-size: 12px; margin-right: 4px; }
355+ .danger { color: #fff; background: var(--danger); border-color: var(--danger); }
336356 .messages { flex: 1; overflow-y: auto; padding: 16px 18px; display: flex; flex-direction: column; gap: 14px; }
337357 .older { align-self: center; font-size: 12px; }
338358 .empty { color: var(--muted); margin: auto; text-align: center; max-width: 420px; }
@@ -14,7 +14,7 @@
14 import Logo from '../layout/Logo.svelte'14 import Logo from '../layout/Logo.svelte'
15 import { isNearBottom, type Trigger } from '../composer'15 import { isNearBottom, type Trigger } from '../composer'
16 import { usagePercent } from '../conversation'16 import { usagePercent } from '../conversation'
17- import { act, addMember, agentTitle, appState, cancel, channelOf, loadOlder, openAssistant, prepareCommands, removeMember, send } from '../state.svelte'17+ import { act, addMember, agentTitle, appState, cancel, channelOf, clearHistory, loadOlder, openAssistant, prepareCommands, removeMember, send } from '../state.svelte'
18 import { ConfigService, WorkspaceService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'18 import { ConfigService, WorkspaceService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
19 import type { FilesDroppedPayload } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/events'19 import type { FilesDroppedPayload } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/events'
20 import { FilesDropped } from '../../../bindings/events'20 import { FilesDropped } from '../../../bindings/events'
@@ -39,6 +39,8 @@
39 return conversation?.busy && last?.role === 'agent' && !last.done ? trailingFailures(last.parts ?? []) : 039 return conversation?.busy && last?.role === 'agent' && !last.done ? trailingFailures(last.parts ?? []) : 0
40 })40 })
41 const stuckAgent = $derived(agentTitle(conversation?.answering ?? ''))41 const stuckAgent = $derived(agentTitle(conversation?.answering ?? ''))
42+ // Issue #62: the channel whose history waits for a confirmation to be erased.
43+ let clearing = $state('')
42 44
43 function infoOf(name: string): AgentInfo {45 function infoOf(name: string): AgentInfo {
44 const agent = appState.agents.find((candidate) => candidate.name === name)46 const agent = appState.agents.find((candidate) => candidate.name === name)
@@ -240,6 +242,17 @@
240 {#if percent !== null}242 {#if percent !== null}
241 <span class="usage" title={t('conversation.usageHint')}>{t('conversation.usage', { percent })}</span>243 <span class="usage" title={t('conversation.usageHint')}>{t('conversation.usage', { percent })}</span>
242 {/if}244 {/if}
245+ {#if (conversation.messages ?? []).length > 0}
246+ <button class="clear" class:alone={percent === null} onclick={() => { appState.actionError = ''; clearing = channel.id }} aria-label={t('conversation.clear')} title={t('conversation.clear')}>🗑</button>
247+ {/if}
248+ {#if clearing === channel.id}
249+ {@const question = channel.direct ? t('conversation.clearConfirmDirect', { agent: title }) : t('conversation.clearConfirmChannel', { channel: channel.name })}
250+ <div class="confirm" role="alertdialog" aria-label={question}>
251+ <p>{question}</p>
252+ <button class="danger" onclick={() => { clearing = ''; void clearHistory(channel.id) }}>{t('conversation.clearYes')}</button>
253+ <button onclick={() => (clearing = '')}>{t('conversation.clearNo')}</button>
254+ </div>
255+ {/if}
243 </header>256 </header>
244 <div class="messages" bind:this={list} onscroll={onScroll}>257 <div class="messages" bind:this={list} onscroll={onScroll}>
245 {#if conversation.more}258 {#if conversation.more}
@@ -333,6 +346,13 @@
333 .busy { color: var(--warn); font-size: 12px; font-style: italic; display: inline-flex; align-items: center; gap: 6px; }346 .busy { color: var(--warn); font-size: 12px; font-style: italic; display: inline-flex; align-items: center; gap: 6px; }
334 .pending { color: var(--muted); font-size: 13px; font-style: italic; display: flex; align-items: center; gap: 8px; margin: 0; }347 .pending { color: var(--muted); font-size: 13px; font-style: italic; display: flex; align-items: center; gap: 8px; margin: 0; }
335 .usage { margin-left: auto; color: var(--muted); font-size: 12px; border: 1px solid var(--border); border-radius: 10px; padding: 1px 10px; }348 .usage { margin-left: auto; color: var(--muted); font-size: 12px; border: 1px solid var(--border); border-radius: 10px; padding: 1px 10px; }
349+ .clear { border: none; background: none; padding: 0 4px; font-size: 13px; opacity: 0.6; }
350+ .clear:hover { opacity: 1; }
351+ .clear.alone { margin-left: auto; }
352+ .confirm { flex-basis: 100%; padding: 6px 10px; border: 1px solid var(--danger); border-radius: 6px; font-size: 12px; }
353+ .confirm p { margin: 0 0 6px; }
354+ .confirm button { font-size: 12px; margin-right: 4px; }
355+ .danger { color: #fff; background: var(--danger); border-color: var(--danger); }
336 .messages { flex: 1; overflow-y: auto; padding: 16px 18px; display: flex; flex-direction: column; gap: 14px; }356 .messages { flex: 1; overflow-y: auto; padding: 16px 18px; display: flex; flex-direction: column; gap: 14px; }
337 .older { align-self: center; font-size: 12px; }357 .older { align-self: center; font-size: 12px; }
338 .empty { color: var(--muted); margin: auto; text-align: center; max-width: 420px; }358 .empty { color: var(--muted); margin: auto; text-align: center; max-width: 420px; }
modified frontend/src/lib/panels/LogsPanel.svelte +14 -9
@@ -3,25 +3,30 @@
33 import { t } from '../i18n/index.svelte'
44 import { appState } from '../state.svelte'
55 import AgentLog from '../agent/AgentLog.svelte'
6+ import MemoryPressure from './MemoryPressure.svelte'
67
78 let selected = $state('')
89 const agentName = $derived(selected || appState.agents[0]?.name || '')
910 </script>
1011
1112 <div class="logs">
12- <label>
13- {t('logs.of')}
14- <select value={agentName} onchange={(event) => (selected = event.currentTarget.value)}>
15- {#each appState.agents as agent (agent.name)}
16- <option value={agent.name}>{agent.title}</option>
17- {/each}
18- </select>
19- </label>
13+ <div class="bar">
14+ <label>
15+ {t('logs.of')}
16+ <select value={agentName} onchange={(event) => (selected = event.currentTarget.value)}>
17+ {#each appState.agents as agent (agent.name)}
18+ <option value={agent.name}>{agent.title}</option>
19+ {/each}
20+ </select>
21+ </label>
22+ <MemoryPressure />
23+ </div>
2024 <AgentLog agent={agentName} />
2125 </div>
2226
2327 <style>
2428 .logs { height: 100%; display: flex; flex-direction: column; }
25- label { padding: 0 8px 6px; color: var(--muted); font-size: 12px; }
29+ .bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 12px; padding: 0 8px 6px; }
30+ label { color: var(--muted); font-size: 12px; }
2631 select { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; }
2732 </style>
@@ -3,25 +3,30 @@
3 import { t } from '../i18n/index.svelte'3 import { t } from '../i18n/index.svelte'
4 import { appState } from '../state.svelte'4 import { appState } from '../state.svelte'
5 import AgentLog from '../agent/AgentLog.svelte'5 import AgentLog from '../agent/AgentLog.svelte'
6+ import MemoryPressure from './MemoryPressure.svelte'
6 7
7 let selected = $state('')8 let selected = $state('')
8 const agentName = $derived(selected || appState.agents[0]?.name || '')9 const agentName = $derived(selected || appState.agents[0]?.name || '')
9 </script>10 </script>
10 11
11 <div class="logs">12 <div class="logs">
12- <label>13+ <div class="bar">
13- {t('logs.of')}14+ <label>
14- <select value={agentName} onchange={(event) => (selected = event.currentTarget.value)}>15+ {t('logs.of')}
15- {#each appState.agents as agent (agent.name)}16+ <select value={agentName} onchange={(event) => (selected = event.currentTarget.value)}>
16- <option value={agent.name}>{agent.title}</option>17+ {#each appState.agents as agent (agent.name)}
17- {/each}18+ <option value={agent.name}>{agent.title}</option>
18- </select>19+ {/each}
19- </label>20+ </select>
21+ </label>
22+ <MemoryPressure />
23+ </div>
20 <AgentLog agent={agentName} />24 <AgentLog agent={agentName} />
21 </div>25 </div>
22 26
23 <style>27 <style>
24 .logs { height: 100%; display: flex; flex-direction: column; }28 .logs { height: 100%; display: flex; flex-direction: column; }
25- label { padding: 0 8px 6px; color: var(--muted); font-size: 12px; }29+ .bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 12px; padding: 0 8px 6px; }
30+ label { color: var(--muted); font-size: 12px; }
26 select { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; }31 select { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; }
27 </style>32 </style>
added frontend/src/lib/panels/MemoryPressure.svelte +56 -0
new file mode 100644
@@ -0,0 +1,56 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ // Issue #61: the memory pressure of the computer, next to the logs, to see
4+ // when an agent should be stopped or a model unloaded.
5+ import { onMount } from 'svelte'
6+ import { locale, t, type MessageKey } from '../i18n/index.svelte'
7+ import { SystemService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
8+ import type { Status } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/memory'
9+ import { gigabytes, measured, memoryInterval, usedPercent } from '../memory'
10+
11+ let status = $state<Status>()
12+ let element = $state<HTMLElement>()
13+
14+ onMount(() => {
15+ // Measured only while it can be seen: not when the dock of the Log panel
16+ // is folded (it then has no height) nor when the window is hidden.
17+ async function measure() {
18+ if (document.hidden || element?.closest('.dock')?.clientHeight === 0) return
19+ try {
20+ status = await SystemService.Memory()
21+ } catch {
22+ status = undefined
23+ }
24+ }
25+ void measure()
26+ const timer = setInterval(measure, memoryInterval)
27+ return () => clearInterval(timer)
28+ })
29+</script>
30+
31+<span class="memory" bind:this={element}>
32+ {#if measured(status)}
33+ {@const used = gigabytes(status.used, locale.language)}
34+ {@const total = gigabytes(status.total, locale.language)}
35+ <span class="level {status.level}" title={t('memory.help')}>
36+ <span class="dot" aria-hidden="true"></span>
37+ {t('memory.pressure')} {t(`memory.level.${status.level}` as MessageKey)}
38+ </span>
39+ <span class="bar" role="meter" aria-label={t('memory.used', { used, total })} aria-valuemin="0" aria-valuemax="100" aria-valuenow={usedPercent(status)}>
40+ <span class="fill {status.level}" style:width="{usedPercent(status)}%"></span>
41+ </span>
42+ <span class="used">{t('memory.used', { used, total })}</span>
43+ {/if}
44+</span>
45+
46+<style>
47+ .memory { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; white-space: nowrap; }
48+ .level { display: inline-flex; align-items: center; gap: 5px; }
49+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
50+ .normal .dot, .fill.normal { background: var(--ok); }
51+ .warning .dot, .fill.warning { background: var(--warn); }
52+ .critical .dot, .fill.critical { background: var(--danger); }
53+ .level.critical { color: var(--danger); font-weight: 600; }
54+ .bar { width: 80px; height: 6px; border-radius: 3px; background: var(--panel-2); border: 1px solid var(--border); overflow: hidden; }
55+ .fill { display: block; height: 100%; }
56+</style>
new file mode 100644
@@ -0,0 +1,56 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ // Issue #61: the memory pressure of the computer, next to the logs, to see
4+ // when an agent should be stopped or a model unloaded.
5+ import { onMount } from 'svelte'
6+ import { locale, t, type MessageKey } from '../i18n/index.svelte'
7+ import { SystemService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
8+ import type { Status } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/memory'
9+ import { gigabytes, measured, memoryInterval, usedPercent } from '../memory'
10+
11+ let status = $state<Status>()
12+ let element = $state<HTMLElement>()
13+
14+ onMount(() => {
15+ // Measured only while it can be seen: not when the dock of the Log panel
16+ // is folded (it then has no height) nor when the window is hidden.
17+ async function measure() {
18+ if (document.hidden || element?.closest('.dock')?.clientHeight === 0) return
19+ try {
20+ status = await SystemService.Memory()
21+ } catch {
22+ status = undefined
23+ }
24+ }
25+ void measure()
26+ const timer = setInterval(measure, memoryInterval)
27+ return () => clearInterval(timer)
28+ })
29+</script>
30+
31+<span class="memory" bind:this={element}>
32+ {#if measured(status)}
33+ {@const used = gigabytes(status.used, locale.language)}
34+ {@const total = gigabytes(status.total, locale.language)}
35+ <span class="level {status.level}" title={t('memory.help')}>
36+ <span class="dot" aria-hidden="true"></span>
37+ {t('memory.pressure')} {t(`memory.level.${status.level}` as MessageKey)}
38+ </span>
39+ <span class="bar" role="meter" aria-label={t('memory.used', { used, total })} aria-valuemin="0" aria-valuemax="100" aria-valuenow={usedPercent(status)}>
40+ <span class="fill {status.level}" style:width="{usedPercent(status)}%"></span>
41+ </span>
42+ <span class="used">{t('memory.used', { used, total })}</span>
43+ {/if}
44+</span>
45+
46+<style>
47+ .memory { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; white-space: nowrap; }
48+ .level { display: inline-flex; align-items: center; gap: 5px; }
49+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
50+ .normal .dot, .fill.normal { background: var(--ok); }
51+ .warning .dot, .fill.warning { background: var(--warn); }
52+ .critical .dot, .fill.critical { background: var(--danger); }
53+ .level.critical { color: var(--danger); font-weight: 600; }
54+ .bar { width: 80px; height: 6px; border-radius: 3px; background: var(--panel-2); border: 1px solid var(--border); overflow: hidden; }
55+ .fill { display: block; height: 100%; }
56+</style>
modified frontend/src/lib/state.svelte.ts +13 -0
@@ -287,6 +287,19 @@ export function deleteChannel(id: string) {
287287 })
288288 }
289289
290+/**
291+ * Erases the messages of a channel or direct conversation (issue #62): its
292+ * members stay and start a new session. Refused while an agent answers.
293+ */
294+export function clearHistory(id: string) {
295+ return act(async () => {
296+ await ChannelService.Clear(id)
297+ appState.unread[id] = 0
298+ const conversation = await ChannelService.Conversation(id)
299+ if (conversation) appState.conversations[id] = conversation
300+ })
301+}
302+
290303 export function addMember(id: string, agent: string) {
291304 return act(async () => {
292305 await ChannelService.AddMember(id, agent)
@@ -287,6 +287,19 @@ export function deleteChannel(id: string) {
287 })287 })
288 }288 }
289 289
290+/**
291+ * Erases the messages of a channel or direct conversation (issue #62): its
292+ * members stay and start a new session. Refused while an agent answers.
293+ */
294+export function clearHistory(id: string) {
295+ return act(async () => {
296+ await ChannelService.Clear(id)
297+ appState.unread[id] = 0
298+ const conversation = await ChannelService.Conversation(id)
299+ if (conversation) appState.conversations[id] = conversation
300+ })
301+}
302+
290 export function addMember(id: string, agent: string) {303 export function addMember(id: string, agent: string) {
291 return act(async () => {304 return act(async () => {
292 await ChannelService.AddMember(id, agent)305 await ChannelService.AddMember(id, agent)
modified go.mod +1 -1
@@ -10,6 +10,7 @@ require (
1010 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
1111 github.com/wailsapp/wails/v3 v3.0.0
1212 go.yaml.in/yaml/v3 v3.0.5
13+ golang.org/x/sys v0.48.0
1314 golang.org/x/text v0.42.0
1415 )
1516
@@ -47,6 +48,5 @@ require (
4748 github.com/prometheus/procfs v0.10.1 // indirect
4849 github.com/rogpeppe/go-internal v1.9.0 // indirect
4950 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
50- golang.org/x/sys v0.48.0 // indirect
5151 google.golang.org/protobuf v1.33.0 // indirect
5252 )
@@ -10,6 +10,7 @@ require (
10 github.com/santhosh-tekuri/jsonschema/v6 v6.0.310 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
11 github.com/wailsapp/wails/v3 v3.0.011 github.com/wailsapp/wails/v3 v3.0.0
12 go.yaml.in/yaml/v3 v3.0.512 go.yaml.in/yaml/v3 v3.0.5
13+ golang.org/x/sys v0.48.0
13 golang.org/x/text v0.42.014 golang.org/x/text v0.42.0
14 )15 )
15 16
@@ -47,6 +48,5 @@ require (
47 github.com/prometheus/procfs v0.10.1 // indirect48 github.com/prometheus/procfs v0.10.1 // indirect
48 github.com/rogpeppe/go-internal v1.9.0 // indirect49 github.com/rogpeppe/go-internal v1.9.0 // indirect
49 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect50 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
50- golang.org/x/sys v0.48.0 // indirect
51 google.golang.org/protobuf v1.33.0 // indirect51 google.golang.org/protobuf v1.33.0 // indirect
52 )52 )
modified internal/app/services.go +6 -0
@@ -74,6 +74,12 @@ func (s *ChannelService) Delete(id string) error {
7474 return s.Core.Channels().Delete(id)
7575 }
7676
77+// Clear deletes the messages of a channel or direct conversation and keeps
78+// its members; the agents start a new session (issue #62).
79+func (s *ChannelService) Clear(id string) error {
80+ return s.Core.Channels().Clear(id)
81+}
82+
7783 // AddMember invites an agent into a channel.
7884 func (s *ChannelService) AddMember(id, agent string) error {
7985 return s.Core.Channels().AddMember(id, agent)
@@ -74,6 +74,12 @@ func (s *ChannelService) Delete(id string) error {
74 return s.Core.Channels().Delete(id)74 return s.Core.Channels().Delete(id)
75 }75 }
76 76
77+// Clear deletes the messages of a channel or direct conversation and keeps
78+// its members; the agents start a new session (issue #62).
79+func (s *ChannelService) Clear(id string) error {
80+ return s.Core.Channels().Clear(id)
81+}
82+
77 // AddMember invites an agent into a channel.83 // AddMember invites an agent into a channel.
78 func (s *ChannelService) AddMember(id, agent string) error {84 func (s *ChannelService) AddMember(id, agent string) error {
79 return s.Core.Channels().AddMember(id, agent)85 return s.Core.Channels().AddMember(id, agent)
added internal/app/system.go +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app
4+
5+import "git.rickub.com/bots-garden/sidekicks/internal/memory"
6+
7+// SystemService tells the interface about the computer Sidekicks runs on.
8+type SystemService struct{}
9+
10+// Memory returns the memory pressure and the used and total memory (issue
11+// #61). Where it cannot be measured, Total is 0 and the interface shows
12+// nothing.
13+func (s *SystemService) Memory() memory.Status {
14+ status, err := memory.Read()
15+ if err != nil {
16+ return memory.Status{}
17+ }
18+ return status
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app
4+
5+import "git.rickub.com/bots-garden/sidekicks/internal/memory"
6+
7+// SystemService tells the interface about the computer Sidekicks runs on.
8+type SystemService struct{}
9+
10+// Memory returns the memory pressure and the used and total memory (issue
11+// #61). Where it cannot be measured, Total is 0 and the interface shows
12+// nothing.
13+func (s *SystemService) Memory() memory.Status {
14+ status, err := memory.Read()
15+ if err != nil {
16+ return memory.Status{}
17+ }
18+ return status
19+}
added internal/app/system_test.go +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app_test
4+
5+import (
6+ "testing"
7+
8+ "git.rickub.com/bots-garden/sidekicks/internal/app"
9+)
10+
11+func TestMemoryIsMeasuredOrLeftEmpty(t *testing.T) {
12+ status := (&app.SystemService{}).Memory()
13+ if status.Total == 0 {
14+ if status.Level != "" || status.Used != 0 {
15+ t.Fatalf("a missing measure is empty: %+v", status)
16+ }
17+ return
18+ }
19+ if status.Used > status.Total || status.Level == "" {
20+ t.Fatalf("got %+v", status)
21+ }
22+}
new file mode 100644
@@ -0,0 +1,22 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app_test
4+
5+import (
6+ "testing"
7+
8+ "git.rickub.com/bots-garden/sidekicks/internal/app"
9+)
10+
11+func TestMemoryIsMeasuredOrLeftEmpty(t *testing.T) {
12+ status := (&app.SystemService{}).Memory()
13+ if status.Total == 0 {
14+ if status.Level != "" || status.Used != 0 {
15+ t.Fatalf("a missing measure is empty: %+v", status)
16+ }
17+ return
18+ }
19+ if status.Used > status.Total || status.Level == "" {
20+ t.Fatalf("got %+v", status)
21+ }
22+}
modified internal/channels/channels.go +33 -0
@@ -77,6 +77,7 @@ var (
7777 ErrUnknownAgent = errors.New("unknown agent")
7878 ErrUnknownChannel = errors.New("unknown channel")
7979 ErrInvalidName = errors.New("a channel name has 1 to 64 characters")
80+ ErrBusy = errors.New("an agent is answering in this channel: stop it or wait for the end of its answer")
8081 )
8182
8283 // Part is a piece of a message: text, reasoning, a tool call or a plan.
@@ -445,6 +446,38 @@ func (m *Manager) Delete(id string) error {
445446 return nil
446447 }
447448
449+// Clear deletes every message of a channel or direct conversation and keeps
450+// its name, description and members (issue #62). The members start over
451+// too: their sessions are forgotten, so that their next turn opens a new
452+// one and they cannot remember what was erased. It is refused while the
453+// channel answers or has messages waiting (ErrBusy).
454+func (m *Manager) Clear(id string) error {
455+ m.mu.Lock()
456+ defer m.mu.Unlock()
457+ ch, err := m.channel(id)
458+ if err != nil {
459+ return err
460+ }
461+ if ch.busy || len(ch.queue) > 0 {
462+ return ErrBusy
463+ }
464+ if err := m.db.ClearMessages(id); err != nil {
465+ return err
466+ }
467+ for _, member := range ch.members {
468+ member.conn, member.fresh = nil, false
469+ member.SessionID, member.LastSeen = "", ""
470+ if err := m.db.PutMember(member.Membership); err != nil {
471+ return err
472+ }
473+ }
474+ ch.messages, ch.loaded, ch.more = nil, true, false
475+ ch.usage = map[string]acp.Usage{}
476+ ch.stopped, ch.rejected = false, 0
477+ m.emitChannels()
478+ return nil
479+}
480+
448481 // AddMember invites agent into a channel; the channel says so.
449482 func (m *Manager) AddMember(id, agent string) error {
450483 if _, ok := m.agents.Definition(agent); !ok {
@@ -77,6 +77,7 @@ var (
77 ErrUnknownAgent = errors.New("unknown agent")77 ErrUnknownAgent = errors.New("unknown agent")
78 ErrUnknownChannel = errors.New("unknown channel")78 ErrUnknownChannel = errors.New("unknown channel")
79 ErrInvalidName = errors.New("a channel name has 1 to 64 characters")79 ErrInvalidName = errors.New("a channel name has 1 to 64 characters")
80+ ErrBusy = errors.New("an agent is answering in this channel: stop it or wait for the end of its answer")
80 )81 )
81 82
82 // Part is a piece of a message: text, reasoning, a tool call or a plan.83 // Part is a piece of a message: text, reasoning, a tool call or a plan.
@@ -445,6 +446,38 @@ func (m *Manager) Delete(id string) error {
445 return nil446 return nil
446 }447 }
447 448
449+// Clear deletes every message of a channel or direct conversation and keeps
450+// its name, description and members (issue #62). The members start over
451+// too: their sessions are forgotten, so that their next turn opens a new
452+// one and they cannot remember what was erased. It is refused while the
453+// channel answers or has messages waiting (ErrBusy).
454+func (m *Manager) Clear(id string) error {
455+ m.mu.Lock()
456+ defer m.mu.Unlock()
457+ ch, err := m.channel(id)
458+ if err != nil {
459+ return err
460+ }
461+ if ch.busy || len(ch.queue) > 0 {
462+ return ErrBusy
463+ }
464+ if err := m.db.ClearMessages(id); err != nil {
465+ return err
466+ }
467+ for _, member := range ch.members {
468+ member.conn, member.fresh = nil, false
469+ member.SessionID, member.LastSeen = "", ""
470+ if err := m.db.PutMember(member.Membership); err != nil {
471+ return err
472+ }
473+ }
474+ ch.messages, ch.loaded, ch.more = nil, true, false
475+ ch.usage = map[string]acp.Usage{}
476+ ch.stopped, ch.rejected = false, 0
477+ m.emitChannels()
478+ return nil
479+}
480+
448 // AddMember invites agent into a channel; the channel says so.481 // AddMember invites agent into a channel; the channel says so.
449 func (m *Manager) AddMember(id, agent string) error {482 func (m *Manager) AddMember(id, agent string) error {
450 if _, ok := m.agents.Definition(agent); !ok {483 if _, ok := m.agents.Definition(agent); !ok {
modified internal/channels/channels_test.go +43 -0
@@ -49,6 +49,49 @@ func TestChannelsAreCreatedRenamedAndDeleted(t *testing.T) {
4949 }
5050 }
5151
52+func TestClearingAChannelStartsItsAgentsOver(t *testing.T) {
53+ release := make(chan struct{})
54+ f := setup(t, map[string]script{
55+ "slow": func(context.Context, acp.SessionHandler) (acp.TurnResult, error) {
56+ <-release
57+ return acp.TurnResult{StopReason: "end_turn"}, nil
58+ },
59+ })
60+ general := f.channel(t, "general", "alice")
61+ alice := f.source.conn("alice")
62+ f.send(t, general.ID, "remember 42")
63+ f.idle(t, general.ID, 3)
64+ f.send(t, general.ID, "slow")
65+ waitFor(t, func() bool { return len(alice.sent()) == 2 })
66+ if err := f.manager.Clear(general.ID); !errors.Is(err, channels.ErrBusy) {
67+ t.Fatalf("refused during an answer: %v", err)
68+ }
69+ close(release)
70+ f.idle(t, general.ID, 5)
71+ if err := f.manager.Clear(general.ID); err != nil {
72+ t.Fatal(err)
73+ }
74+ view, _ := f.manager.Conversation(general.ID)
75+ if len(view.Messages) != 0 || view.More || fmt.Sprint(view.Channel.Members) != "[alice]" || view.Channel.Name != "general" {
76+ t.Fatalf("cleared, members kept: %+v", view)
77+ }
78+ if stored, _, _ := f.db.Messages(general.ID, "", 10); len(stored) != 0 {
79+ t.Fatal("the history is erased")
80+ }
81+ if members, _ := f.db.Members(general.ID); len(members) != 1 || members[0].SessionID != "" || members[0].LastSeen != "" {
82+ t.Fatalf("the session is forgotten: %+v", members)
83+ }
84+ f.send(t, general.ID, "what did I say?")
85+ f.idle(t, general.ID, 2)
86+ sent := alice.sent()
87+ if alice.sessionCount() != 2 || strings.Contains(sent[len(sent)-1], "42") {
88+ t.Fatalf("a new session that knows nothing: %d sessions, last prompt %q", alice.sessionCount(), sent[len(sent)-1])
89+ }
90+ if err := f.manager.Clear("nope"); !errors.Is(err, channels.ErrUnknownChannel) {
91+ t.Fatalf("unknown channel: %v", err)
92+ }
93+}
94+
5295 func TestMembersJoinAndLeaveVisibly(t *testing.T) {
5396 f := setup(t, nil)
5497 general := f.channel(t, "general", "alice", "bob")
@@ -49,6 +49,49 @@ func TestChannelsAreCreatedRenamedAndDeleted(t *testing.T) {
49 }49 }
50 }50 }
51 51
52+func TestClearingAChannelStartsItsAgentsOver(t *testing.T) {
53+ release := make(chan struct{})
54+ f := setup(t, map[string]script{
55+ "slow": func(context.Context, acp.SessionHandler) (acp.TurnResult, error) {
56+ <-release
57+ return acp.TurnResult{StopReason: "end_turn"}, nil
58+ },
59+ })
60+ general := f.channel(t, "general", "alice")
61+ alice := f.source.conn("alice")
62+ f.send(t, general.ID, "remember 42")
63+ f.idle(t, general.ID, 3)
64+ f.send(t, general.ID, "slow")
65+ waitFor(t, func() bool { return len(alice.sent()) == 2 })
66+ if err := f.manager.Clear(general.ID); !errors.Is(err, channels.ErrBusy) {
67+ t.Fatalf("refused during an answer: %v", err)
68+ }
69+ close(release)
70+ f.idle(t, general.ID, 5)
71+ if err := f.manager.Clear(general.ID); err != nil {
72+ t.Fatal(err)
73+ }
74+ view, _ := f.manager.Conversation(general.ID)
75+ if len(view.Messages) != 0 || view.More || fmt.Sprint(view.Channel.Members) != "[alice]" || view.Channel.Name != "general" {
76+ t.Fatalf("cleared, members kept: %+v", view)
77+ }
78+ if stored, _, _ := f.db.Messages(general.ID, "", 10); len(stored) != 0 {
79+ t.Fatal("the history is erased")
80+ }
81+ if members, _ := f.db.Members(general.ID); len(members) != 1 || members[0].SessionID != "" || members[0].LastSeen != "" {
82+ t.Fatalf("the session is forgotten: %+v", members)
83+ }
84+ f.send(t, general.ID, "what did I say?")
85+ f.idle(t, general.ID, 2)
86+ sent := alice.sent()
87+ if alice.sessionCount() != 2 || strings.Contains(sent[len(sent)-1], "42") {
88+ t.Fatalf("a new session that knows nothing: %d sessions, last prompt %q", alice.sessionCount(), sent[len(sent)-1])
89+ }
90+ if err := f.manager.Clear("nope"); !errors.Is(err, channels.ErrUnknownChannel) {
91+ t.Fatalf("unknown channel: %v", err)
92+ }
93+}
94+
52 func TestMembersJoinAndLeaveVisibly(t *testing.T) {95 func TestMembersJoinAndLeaveVisibly(t *testing.T) {
53 f := setup(t, nil)96 f := setup(t, nil)
54 general := f.channel(t, "general", "alice", "bob")97 general := f.channel(t, "general", "alice", "bob")
added internal/memory/memory.go +123 -0
new file mode 100644
@@ -0,0 +1,123 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package memory tells how much the computer's memory is under pressure
4+// (issue #61): local models and agents take a lot of it, and past a point
5+// the computer slows down or stops them.
6+//
7+// status, err := memory.Read()
8+// if err == nil { fmt.Println(status.Level, status.Used, status.Total) }
9+//
10+// On macOS the level is the one the system gives (the "memory pressure" of
11+// Activity Monitor) and the used memory is counted like Activity Monitor
12+// does: app memory, wired memory and compressed memory. Elsewhere the level
13+// is worked out from the memory still available (see LevelOf).
14+package memory
15+
16+import (
17+ "bufio"
18+ "errors"
19+ "fmt"
20+ "regexp"
21+ "strconv"
22+ "strings"
23+)
24+
25+// Level is how much the memory is under pressure.
26+type Level string
27+
28+// The levels, with the colours of Activity Monitor.
29+const (
30+ Normal Level = "normal" // green
31+ Warning Level = "warning" // yellow
32+ Critical Level = "critical" // red
33+)
34+
35+// Status is the memory of the computer at one moment, in bytes.
36+type Status struct {
37+ Level Level `json:"level"`
38+ Used uint64 `json:"used"`
39+ Total uint64 `json:"total"`
40+}
41+
42+// ErrUnavailable is returned where the memory cannot be measured.
43+var ErrUnavailable = errors.New("the memory cannot be measured on this system")
44+
45+// Read measures the memory of the computer now.
46+func Read() (Status, error) {
47+ return read()
48+}
49+
50+// LevelOf works out a level from the memory still available, where the
51+// system gives none: under 10 % available is critical, under 20 % a warning.
52+func LevelOf(available, total uint64) Level {
53+ switch {
54+ case total == 0 || available*10 < total:
55+ return Critical
56+ case available*5 < total:
57+ return Warning
58+ }
59+ return Normal
60+}
61+
62+// DarwinLevel turns the value of the sysctl kern.memorystatus_vm_pressure_level
63+// (1 normal, 2 warning, 4 critical) into a level.
64+func DarwinLevel(value uint32) Level {
65+ switch value {
66+ case 2:
67+ return Warning
68+ case 4:
69+ return Critical
70+ }
71+ return Normal
72+}
73+
74+// ParseMeminfo reads the total and available memory, in bytes, from the
75+// content of /proc/meminfo (Linux).
76+func ParseMeminfo(text string) (total, available uint64, err error) {
77+ values := map[string]uint64{}
78+ scanner := bufio.NewScanner(strings.NewReader(text))
79+ for scanner.Scan() {
80+ key, rest, found := strings.Cut(scanner.Text(), ":")
81+ fields := strings.Fields(rest)
82+ if !found || len(fields) == 0 {
83+ continue
84+ }
85+ if value, err := strconv.ParseUint(fields[0], 10, 64); err == nil {
86+ values[key] = value * 1024 // kB
87+ }
88+ }
89+ total, hasTotal := values["MemTotal"]
90+ available, hasAvailable := values["MemAvailable"]
91+ if !hasTotal || !hasAvailable {
92+ return 0, 0, fmt.Errorf("%w: no MemTotal or MemAvailable in /proc/meminfo", ErrUnavailable)
93+ }
94+ return total, available, nil
95+}
96+
97+var (
98+ vmStatPageSize = regexp.MustCompile(`page size of (\d+) bytes`)
99+ vmStatLine = regexp.MustCompile(`^"?([^:"]+)"?:\s+(\d+)\.?$`)
100+)
101+
102+// ParseVMStat counts the used memory, in bytes, from what vm_stat prints
103+// (macOS), as Activity Monitor does: app memory (anonymous pages that are
104+// not purgeable), wired memory and memory occupied by the compressor.
105+func ParseVMStat(text string) (uint64, error) {
106+ match := vmStatPageSize.FindStringSubmatch(text)
107+ if match == nil {
108+ return 0, fmt.Errorf("%w: no page size in the output of vm_stat", ErrUnavailable)
109+ }
110+ pageSize, _ := strconv.ParseUint(match[1], 10, 64)
111+ pages := map[string]uint64{}
112+ for _, line := range strings.Split(text, "\n") {
113+ if found := vmStatLine.FindStringSubmatch(strings.TrimSpace(line)); found != nil {
114+ pages[found[1]], _ = strconv.ParseUint(found[2], 10, 64)
115+ }
116+ }
117+ anonymous, hasAnonymous := pages["Anonymous pages"]
118+ if !hasAnonymous {
119+ return 0, fmt.Errorf("%w: no anonymous pages in the output of vm_stat", ErrUnavailable)
120+ }
121+ app := anonymous - min(anonymous, pages["Pages purgeable"])
122+ return (app + pages["Pages wired down"] + pages["Pages occupied by compressor"]) * pageSize, nil
123+}
new file mode 100644
@@ -0,0 +1,123 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package memory tells how much the computer's memory is under pressure
4+// (issue #61): local models and agents take a lot of it, and past a point
5+// the computer slows down or stops them.
6+//
7+// status, err := memory.Read()
8+// if err == nil { fmt.Println(status.Level, status.Used, status.Total) }
9+//
10+// On macOS the level is the one the system gives (the "memory pressure" of
11+// Activity Monitor) and the used memory is counted like Activity Monitor
12+// does: app memory, wired memory and compressed memory. Elsewhere the level
13+// is worked out from the memory still available (see LevelOf).
14+package memory
15+
16+import (
17+ "bufio"
18+ "errors"
19+ "fmt"
20+ "regexp"
21+ "strconv"
22+ "strings"
23+)
24+
25+// Level is how much the memory is under pressure.
26+type Level string
27+
28+// The levels, with the colours of Activity Monitor.
29+const (
30+ Normal Level = "normal" // green
31+ Warning Level = "warning" // yellow
32+ Critical Level = "critical" // red
33+)
34+
35+// Status is the memory of the computer at one moment, in bytes.
36+type Status struct {
37+ Level Level `json:"level"`
38+ Used uint64 `json:"used"`
39+ Total uint64 `json:"total"`
40+}
41+
42+// ErrUnavailable is returned where the memory cannot be measured.
43+var ErrUnavailable = errors.New("the memory cannot be measured on this system")
44+
45+// Read measures the memory of the computer now.
46+func Read() (Status, error) {
47+ return read()
48+}
49+
50+// LevelOf works out a level from the memory still available, where the
51+// system gives none: under 10 % available is critical, under 20 % a warning.
52+func LevelOf(available, total uint64) Level {
53+ switch {
54+ case total == 0 || available*10 < total:
55+ return Critical
56+ case available*5 < total:
57+ return Warning
58+ }
59+ return Normal
60+}
61+
62+// DarwinLevel turns the value of the sysctl kern.memorystatus_vm_pressure_level
63+// (1 normal, 2 warning, 4 critical) into a level.
64+func DarwinLevel(value uint32) Level {
65+ switch value {
66+ case 2:
67+ return Warning
68+ case 4:
69+ return Critical
70+ }
71+ return Normal
72+}
73+
74+// ParseMeminfo reads the total and available memory, in bytes, from the
75+// content of /proc/meminfo (Linux).
76+func ParseMeminfo(text string) (total, available uint64, err error) {
77+ values := map[string]uint64{}
78+ scanner := bufio.NewScanner(strings.NewReader(text))
79+ for scanner.Scan() {
80+ key, rest, found := strings.Cut(scanner.Text(), ":")
81+ fields := strings.Fields(rest)
82+ if !found || len(fields) == 0 {
83+ continue
84+ }
85+ if value, err := strconv.ParseUint(fields[0], 10, 64); err == nil {
86+ values[key] = value * 1024 // kB
87+ }
88+ }
89+ total, hasTotal := values["MemTotal"]
90+ available, hasAvailable := values["MemAvailable"]
91+ if !hasTotal || !hasAvailable {
92+ return 0, 0, fmt.Errorf("%w: no MemTotal or MemAvailable in /proc/meminfo", ErrUnavailable)
93+ }
94+ return total, available, nil
95+}
96+
97+var (
98+ vmStatPageSize = regexp.MustCompile(`page size of (\d+) bytes`)
99+ vmStatLine = regexp.MustCompile(`^"?([^:"]+)"?:\s+(\d+)\.?$`)
100+)
101+
102+// ParseVMStat counts the used memory, in bytes, from what vm_stat prints
103+// (macOS), as Activity Monitor does: app memory (anonymous pages that are
104+// not purgeable), wired memory and memory occupied by the compressor.
105+func ParseVMStat(text string) (uint64, error) {
106+ match := vmStatPageSize.FindStringSubmatch(text)
107+ if match == nil {
108+ return 0, fmt.Errorf("%w: no page size in the output of vm_stat", ErrUnavailable)
109+ }
110+ pageSize, _ := strconv.ParseUint(match[1], 10, 64)
111+ pages := map[string]uint64{}
112+ for _, line := range strings.Split(text, "\n") {
113+ if found := vmStatLine.FindStringSubmatch(strings.TrimSpace(line)); found != nil {
114+ pages[found[1]], _ = strconv.ParseUint(found[2], 10, 64)
115+ }
116+ }
117+ anonymous, hasAnonymous := pages["Anonymous pages"]
118+ if !hasAnonymous {
119+ return 0, fmt.Errorf("%w: no anonymous pages in the output of vm_stat", ErrUnavailable)
120+ }
121+ app := anonymous - min(anonymous, pages["Pages purgeable"])
122+ return (app + pages["Pages wired down"] + pages["Pages occupied by compressor"]) * pageSize, nil
123+}
added internal/memory/memory_darwin.go +30 -0
new file mode 100644
@@ -0,0 +1,30 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory
4+
5+import (
6+ "fmt"
7+ "os/exec"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+func read() (Status, error) {
13+ total, err := unix.SysctlUint64("hw.memsize")
14+ if err != nil {
15+ return Status{}, fmt.Errorf("%w: hw.memsize: %w", ErrUnavailable, err)
16+ }
17+ level, err := unix.SysctlUint32("kern.memorystatus_vm_pressure_level")
18+ if err != nil {
19+ return Status{}, fmt.Errorf("%w: kern.memorystatus_vm_pressure_level: %w", ErrUnavailable, err)
20+ }
21+ output, err := exec.Command("/usr/bin/vm_stat").Output()
22+ if err != nil {
23+ return Status{}, fmt.Errorf("%w: vm_stat: %w", ErrUnavailable, err)
24+ }
25+ used, err := ParseVMStat(string(output))
26+ if err != nil {
27+ return Status{}, err
28+ }
29+ return Status{Level: DarwinLevel(level), Used: min(used, total), Total: total}, nil
30+}
new file mode 100644
@@ -0,0 +1,30 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory
4+
5+import (
6+ "fmt"
7+ "os/exec"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+func read() (Status, error) {
13+ total, err := unix.SysctlUint64("hw.memsize")
14+ if err != nil {
15+ return Status{}, fmt.Errorf("%w: hw.memsize: %w", ErrUnavailable, err)
16+ }
17+ level, err := unix.SysctlUint32("kern.memorystatus_vm_pressure_level")
18+ if err != nil {
19+ return Status{}, fmt.Errorf("%w: kern.memorystatus_vm_pressure_level: %w", ErrUnavailable, err)
20+ }
21+ output, err := exec.Command("/usr/bin/vm_stat").Output()
22+ if err != nil {
23+ return Status{}, fmt.Errorf("%w: vm_stat: %w", ErrUnavailable, err)
24+ }
25+ used, err := ParseVMStat(string(output))
26+ if err != nil {
27+ return Status{}, err
28+ }
29+ return Status{Level: DarwinLevel(level), Used: min(used, total), Total: total}, nil
30+}
added internal/memory/memory_linux.go +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory
4+
5+import (
6+ "fmt"
7+ "os"
8+)
9+
10+func read() (Status, error) {
11+ content, err := os.ReadFile("/proc/meminfo")
12+ if err != nil {
13+ return Status{}, fmt.Errorf("%w: %w", ErrUnavailable, err)
14+ }
15+ total, available, err := ParseMeminfo(string(content))
16+ if err != nil {
17+ return Status{}, err
18+ }
19+ return Status{Level: LevelOf(available, total), Used: total - min(available, total), Total: total}, nil
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory
4+
5+import (
6+ "fmt"
7+ "os"
8+)
9+
10+func read() (Status, error) {
11+ content, err := os.ReadFile("/proc/meminfo")
12+ if err != nil {
13+ return Status{}, fmt.Errorf("%w: %w", ErrUnavailable, err)
14+ }
15+ total, available, err := ParseMeminfo(string(content))
16+ if err != nil {
17+ return Status{}, err
18+ }
19+ return Status{Level: LevelOf(available, total), Used: total - min(available, total), Total: total}, nil
20+}
added internal/memory/memory_other.go +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build !darwin && !linux && !windows
4+
5+package memory
6+
7+func read() (Status, error) {
8+ return Status{}, ErrUnavailable
9+}
new file mode 100644
@@ -0,0 +1,9 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build !darwin && !linux && !windows
4+
5+package memory
6+
7+func read() (Status, error) {
8+ return Status{}, ErrUnavailable
9+}
added internal/memory/memory_test.go +82 -0
new file mode 100644
@@ -0,0 +1,82 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory_test
4+
5+import (
6+ "errors"
7+ "testing"
8+
9+ "git.rickub.com/bots-garden/sidekicks/internal/memory"
10+)
11+
12+func TestLevelOfFollowsTheAvailableMemory(t *testing.T) {
13+ const total = 32 << 30
14+ cases := map[uint64]memory.Level{
15+ 16 << 30: memory.Normal,
16+ 7 << 30: memory.Normal,
17+ 6 << 30: memory.Warning,
18+ 3 << 30: memory.Critical,
19+ 0: memory.Critical,
20+ }
21+ for available, want := range cases {
22+ if got := memory.LevelOf(available, total); got != want {
23+ t.Errorf("%d GB available: got %s, want %s", available>>30, got, want)
24+ }
25+ }
26+ if memory.LevelOf(0, 0) != memory.Critical {
27+ t.Error("no total is not a normal level")
28+ }
29+}
30+
31+func TestDarwinLevel(t *testing.T) {
32+ for value, want := range map[uint32]memory.Level{1: memory.Normal, 2: memory.Warning, 4: memory.Critical, 0: memory.Normal} {
33+ if got := memory.DarwinLevel(value); got != want {
34+ t.Errorf("%d: got %s, want %s", value, got, want)
35+ }
36+ }
37+}
38+
39+func TestParseMeminfo(t *testing.T) {
40+ total, available, err := memory.ParseMeminfo("MemTotal: 32000000 kB\nMemFree: 1000000 kB\nMemAvailable: 8000000 kB\nHugePages_Total: 0\n")
41+ if err != nil || total != 32000000*1024 || available != 8000000*1024 {
42+ t.Fatalf("got %d, %d, %v", total, available, err)
43+ }
44+ if _, _, err := memory.ParseMeminfo("MemTotal: 1 kB\n"); !errors.Is(err, memory.ErrUnavailable) {
45+ t.Fatalf("no MemAvailable: got %v", err)
46+ }
47+}
48+
49+const vmStat = `Mach Virtual Memory Statistics: (page size of 16384 bytes)
50+Pages free: 12345.
51+Pages active: 500000.
52+Pages inactive: 480000.
53+Pages wired down: 200000.
54+Pages purgeable: 10000.
55+"Translation faults": 123456789.
56+Anonymous pages: 700000.
57+Pages occupied by compressor: 100000.
58+`
59+
60+func TestParseVMStatCountsLikeActivityMonitor(t *testing.T) {
61+ used, err := memory.ParseVMStat(vmStat)
62+ want := uint64(700000-10000+200000+100000) * 16384
63+ if err != nil || used != want {
64+ t.Fatalf("got %d, %v; want %d", used, err, want)
65+ }
66+ if _, err := memory.ParseVMStat("Pages free: 1."); !errors.Is(err, memory.ErrUnavailable) {
67+ t.Fatalf("no page size: got %v", err)
68+ }
69+ if _, err := memory.ParseVMStat("(page size of 4096 bytes)\nPages free: 1."); !errors.Is(err, memory.ErrUnavailable) {
70+ t.Fatalf("no anonymous pages: got %v", err)
71+ }
72+}
73+
74+func TestReadMeasuresThisComputer(t *testing.T) {
75+ status, err := memory.Read()
76+ if errors.Is(err, memory.ErrUnavailable) {
77+ t.Skip(err)
78+ }
79+ if err != nil || status.Total == 0 || status.Used > status.Total || status.Level == "" {
80+ t.Fatalf("got %+v, %v", status, err)
81+ }
82+}
new file mode 100644
@@ -0,0 +1,82 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory_test
4+
5+import (
6+ "errors"
7+ "testing"
8+
9+ "git.rickub.com/bots-garden/sidekicks/internal/memory"
10+)
11+
12+func TestLevelOfFollowsTheAvailableMemory(t *testing.T) {
13+ const total = 32 << 30
14+ cases := map[uint64]memory.Level{
15+ 16 << 30: memory.Normal,
16+ 7 << 30: memory.Normal,
17+ 6 << 30: memory.Warning,
18+ 3 << 30: memory.Critical,
19+ 0: memory.Critical,
20+ }
21+ for available, want := range cases {
22+ if got := memory.LevelOf(available, total); got != want {
23+ t.Errorf("%d GB available: got %s, want %s", available>>30, got, want)
24+ }
25+ }
26+ if memory.LevelOf(0, 0) != memory.Critical {
27+ t.Error("no total is not a normal level")
28+ }
29+}
30+
31+func TestDarwinLevel(t *testing.T) {
32+ for value, want := range map[uint32]memory.Level{1: memory.Normal, 2: memory.Warning, 4: memory.Critical, 0: memory.Normal} {
33+ if got := memory.DarwinLevel(value); got != want {
34+ t.Errorf("%d: got %s, want %s", value, got, want)
35+ }
36+ }
37+}
38+
39+func TestParseMeminfo(t *testing.T) {
40+ total, available, err := memory.ParseMeminfo("MemTotal: 32000000 kB\nMemFree: 1000000 kB\nMemAvailable: 8000000 kB\nHugePages_Total: 0\n")
41+ if err != nil || total != 32000000*1024 || available != 8000000*1024 {
42+ t.Fatalf("got %d, %d, %v", total, available, err)
43+ }
44+ if _, _, err := memory.ParseMeminfo("MemTotal: 1 kB\n"); !errors.Is(err, memory.ErrUnavailable) {
45+ t.Fatalf("no MemAvailable: got %v", err)
46+ }
47+}
48+
49+const vmStat = `Mach Virtual Memory Statistics: (page size of 16384 bytes)
50+Pages free: 12345.
51+Pages active: 500000.
52+Pages inactive: 480000.
53+Pages wired down: 200000.
54+Pages purgeable: 10000.
55+"Translation faults": 123456789.
56+Anonymous pages: 700000.
57+Pages occupied by compressor: 100000.
58+`
59+
60+func TestParseVMStatCountsLikeActivityMonitor(t *testing.T) {
61+ used, err := memory.ParseVMStat(vmStat)
62+ want := uint64(700000-10000+200000+100000) * 16384
63+ if err != nil || used != want {
64+ t.Fatalf("got %d, %v; want %d", used, err, want)
65+ }
66+ if _, err := memory.ParseVMStat("Pages free: 1."); !errors.Is(err, memory.ErrUnavailable) {
67+ t.Fatalf("no page size: got %v", err)
68+ }
69+ if _, err := memory.ParseVMStat("(page size of 4096 bytes)\nPages free: 1."); !errors.Is(err, memory.ErrUnavailable) {
70+ t.Fatalf("no anonymous pages: got %v", err)
71+ }
72+}
73+
74+func TestReadMeasuresThisComputer(t *testing.T) {
75+ status, err := memory.Read()
76+ if errors.Is(err, memory.ErrUnavailable) {
77+ t.Skip(err)
78+ }
79+ if err != nil || status.Total == 0 || status.Used > status.Total || status.Level == "" {
80+ t.Fatalf("got %+v, %v", status, err)
81+ }
82+}
added internal/memory/memory_windows.go +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory
4+
5+import (
6+ "fmt"
7+ "unsafe"
8+
9+ "golang.org/x/sys/windows"
10+)
11+
12+// memoryStatusEx is the MEMORYSTATUSEX structure of the Windows API.
13+type memoryStatusEx struct {
14+ Length uint32
15+ MemoryLoad uint32
16+ TotalPhys uint64
17+ AvailPhys uint64
18+ TotalPageFile uint64
19+ AvailPageFile uint64
20+ TotalVirtual uint64
21+ AvailVirtual uint64
22+ AvailExtendedVirtual uint64
23+}
24+
25+var globalMemoryStatusEx = windows.NewLazySystemDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
26+
27+func read() (Status, error) {
28+ status := memoryStatusEx{}
29+ status.Length = uint32(unsafe.Sizeof(status))
30+ if ok, _, err := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&status))); ok == 0 {
31+ return Status{}, fmt.Errorf("%w: GlobalMemoryStatusEx: %w", ErrUnavailable, err)
32+ }
33+ total, available := status.TotalPhys, status.AvailPhys
34+ return Status{Level: LevelOf(available, total), Used: total - min(available, total), Total: total}, nil
35+}
new file mode 100644
@@ -0,0 +1,35 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package memory
4+
5+import (
6+ "fmt"
7+ "unsafe"
8+
9+ "golang.org/x/sys/windows"
10+)
11+
12+// memoryStatusEx is the MEMORYSTATUSEX structure of the Windows API.
13+type memoryStatusEx struct {
14+ Length uint32
15+ MemoryLoad uint32
16+ TotalPhys uint64
17+ AvailPhys uint64
18+ TotalPageFile uint64
19+ AvailPageFile uint64
20+ TotalVirtual uint64
21+ AvailVirtual uint64
22+ AvailExtendedVirtual uint64
23+}
24+
25+var globalMemoryStatusEx = windows.NewLazySystemDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
26+
27+func read() (Status, error) {
28+ status := memoryStatusEx{}
29+ status.Length = uint32(unsafe.Sizeof(status))
30+ if ok, _, err := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&status))); ok == 0 {
31+ return Status{}, fmt.Errorf("%w: GlobalMemoryStatusEx: %w", ErrUnavailable, err)
32+ }
33+ total, available := status.TotalPhys, status.AvailPhys
34+ return Status{Level: LevelOf(available, total), Used: total - min(available, total), Total: total}, nil
35+}
modified internal/store/store.go +11 -0
@@ -341,6 +341,17 @@ func (s *Store) DeleteChannel(id string) error {
341341 return batch.Commit(pebble.Sync)
342342 }
343343
344+// ClearMessages deletes every message of a channel and keeps the channel
345+// and its members (issue #62).
346+func (s *Store) ClearMessages(id string) error {
347+ if err := s.enter(); err != nil {
348+ return err
349+ }
350+ defer s.leave()
351+ prefix := "msg/" + id + "/"
352+ return s.db.DeleteRange([]byte(prefix), upperBound(prefix), pebble.Sync)
353+}
354+
344355 // ---------------------------------------------------------------- members
345356
346357 // Members returns the members of a channel, in the order they joined.
@@ -341,6 +341,17 @@ func (s *Store) DeleteChannel(id string) error {
341 return batch.Commit(pebble.Sync)341 return batch.Commit(pebble.Sync)
342 }342 }
343 343
344+// ClearMessages deletes every message of a channel and keeps the channel
345+// and its members (issue #62).
346+func (s *Store) ClearMessages(id string) error {
347+ if err := s.enter(); err != nil {
348+ return err
349+ }
350+ defer s.leave()
351+ prefix := "msg/" + id + "/"
352+ return s.db.DeleteRange([]byte(prefix), upperBound(prefix), pebble.Sync)
353+}
354+
344 // ---------------------------------------------------------------- members355 // ---------------------------------------------------------------- members
345 356
346 // Members returns the members of a channel, in the order they joined.357 // Members returns the members of a channel, in the order they joined.
modified internal/store/store_test.go +26 -0
@@ -91,6 +91,32 @@ func TestDeletingAChannelDeletesItsMembersMessagesAndName(t *testing.T) {
9191 }
9292 }
9393
94+func TestClearingAChannelKeepsItAndItsMembers(t *testing.T) {
95+ db := memory(t)
96+ general := store.Channel{ID: store.NewID(), Name: "team"}
97+ other := store.Channel{ID: store.NewID(), Name: "other"}
98+ _ = db.CreateChannel(general)
99+ _ = db.CreateChannel(other)
100+ _ = db.PutMember(store.Membership{ChannelID: general.ID, Agent: "alice"})
101+ _ = db.PutMessage(general.ID, store.NewID(), []byte("hello"))
102+ _ = db.PutMessage(other.ID, store.NewID(), []byte("kept"))
103+ if err := db.ClearMessages(general.ID); err != nil {
104+ t.Fatal(err)
105+ }
106+ if messages, _, _ := db.Messages(general.ID, "", 10); len(messages) != 0 {
107+ t.Fatalf("cleared: %q", messages)
108+ }
109+ if found, err := db.Channel(general.ID); err != nil || found.Name != "team" {
110+ t.Fatalf("the channel stays: %+v %v", found, err)
111+ }
112+ if members, _ := db.Members(general.ID); len(members) != 1 {
113+ t.Fatalf("its members stay: %+v", members)
114+ }
115+ if messages, _, _ := db.Messages(other.ID, "", 10); len(messages) != 1 {
116+ t.Fatal("another channel keeps its messages")
117+ }
118+}
119+
94120 func TestADirectChannelIsFoundByItsAgentAndHasNoName(t *testing.T) {
95121 db := memory(t)
96122 direct := store.Channel{ID: store.NewID(), Direct: "alice"}
@@ -91,6 +91,32 @@ func TestDeletingAChannelDeletesItsMembersMessagesAndName(t *testing.T) {
91 }91 }
92 }92 }
93 93
94+func TestClearingAChannelKeepsItAndItsMembers(t *testing.T) {
95+ db := memory(t)
96+ general := store.Channel{ID: store.NewID(), Name: "team"}
97+ other := store.Channel{ID: store.NewID(), Name: "other"}
98+ _ = db.CreateChannel(general)
99+ _ = db.CreateChannel(other)
100+ _ = db.PutMember(store.Membership{ChannelID: general.ID, Agent: "alice"})
101+ _ = db.PutMessage(general.ID, store.NewID(), []byte("hello"))
102+ _ = db.PutMessage(other.ID, store.NewID(), []byte("kept"))
103+ if err := db.ClearMessages(general.ID); err != nil {
104+ t.Fatal(err)
105+ }
106+ if messages, _, _ := db.Messages(general.ID, "", 10); len(messages) != 0 {
107+ t.Fatalf("cleared: %q", messages)
108+ }
109+ if found, err := db.Channel(general.ID); err != nil || found.Name != "team" {
110+ t.Fatalf("the channel stays: %+v %v", found, err)
111+ }
112+ if members, _ := db.Members(general.ID); len(members) != 1 {
113+ t.Fatalf("its members stay: %+v", members)
114+ }
115+ if messages, _, _ := db.Messages(other.ID, "", 10); len(messages) != 1 {
116+ t.Fatal("another channel keeps its messages")
117+ }
118+}
119+
94 func TestADirectChannelIsFoundByItsAgentAndHasNoName(t *testing.T) {120 func TestADirectChannelIsFoundByItsAgentAndHasNoName(t *testing.T) {
95 db := memory(t)121 db := memory(t)
96 direct := store.Channel{ID: store.NewID(), Direct: "alice"}122 direct := store.Channel{ID: store.NewID(), Direct: "alice"}
modified main.go +1 -0
@@ -133,6 +133,7 @@ func main() {
133133 application.NewService(&app.ChannelService{Core: core}),
134134 application.NewService(&app.WorkspaceService{Core: core}),
135135 application.NewService(&app.TerminalService{Core: core}),
136+ application.NewService(&app.SystemService{}),
136137 },
137138 Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
138139 Mac: application.MacOptions{ApplicationShouldTerminateAfterLastWindowClosed: true},
@@ -133,6 +133,7 @@ func main() {
133 application.NewService(&app.ChannelService{Core: core}),133 application.NewService(&app.ChannelService{Core: core}),
134 application.NewService(&app.WorkspaceService{Core: core}),134 application.NewService(&app.WorkspaceService{Core: core}),
135 application.NewService(&app.TerminalService{Core: core}),135 application.NewService(&app.TerminalService{Core: core}),
136+ application.NewService(&app.SystemService{}),
136 },137 },
137 Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},138 Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
138 Mac: application.MacOptions{ApplicationShouldTerminateAfterLastWindowClosed: true},139 Mac: application.MacOptions{ApplicationShouldTerminateAfterLastWindowClosed: true},