🛟 Updated.
b5a5d88 parent: 69b852b modified
.tickets/issues/0051-ajouter-un-bouton-pour-decharger-le-modele-de-la.yaml +84 -5 | @@ -1,13 +1,92 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | 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 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | 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 | |
| 13 | 92 | 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: 51 | 2 | 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: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-31T07:10:06.981Z | 8 | createdAt: 2026-10-31T07:10:06.981Z |
| 9 | -updatedAt: 2026-10-31T07:10:06.981Z | 9 | +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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 56 |
| 3 | 3 | title: "Préparer le compte Apple Developer : certificat Developer ID et profil de notarisation" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | |
| 9 | +updatedAt: 2026-10-31T22:25:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::chore |
| 12 | 12 | - area::infra |
| @@ -25,42 +25,52 @@ body: | | ||
| 25 | 25 | tasks: |
| 26 | 26 | - id: 1 |
| 27 | 27 | title: "Relever le Team ID" |
| 28 | - state: open | |
| 28 | + state: closed | |
| 29 | 29 | priority: high |
| 30 | 30 | author: |
| 31 | 31 | name: k33g |
| 32 | 32 | email: ph.charriere@gmail.com |
| 33 | 33 | createdAt: 2026-10-31T11:03:40.000Z |
| 34 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 35 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 34 | 36 | - id: 2 |
| 35 | 37 | title: "Créer le certificat Developer ID Application" |
| 36 | - state: open | |
| 38 | + state: closed | |
| 37 | 39 | priority: high |
| 38 | 40 | author: |
| 39 | 41 | name: k33g |
| 40 | 42 | email: ph.charriere@gmail.com |
| 41 | 43 | createdAt: 2026-10-31T11:03:40.000Z |
| 44 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 45 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 42 | 46 | - id: 3 |
| 43 | 47 | title: "Sauvegarder le certificat et sa clé (.p12)" |
| 44 | - state: open | |
| 48 | + state: closed | |
| 45 | 49 | priority: high |
| 46 | 50 | author: |
| 47 | 51 | name: k33g |
| 48 | 52 | email: ph.charriere@gmail.com |
| 49 | 53 | createdAt: 2026-10-31T11:03:40.000Z |
| 54 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 55 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 50 | 56 | - id: 4 |
| 51 | 57 | title: "Créer le mot de passe d'application et le profil notarytool" |
| 52 | - state: open | |
| 58 | + state: closed | |
| 53 | 59 | priority: high |
| 54 | 60 | author: |
| 55 | 61 | name: k33g |
| 56 | 62 | email: ph.charriere@gmail.com |
| 57 | 63 | createdAt: 2026-10-31T11:03:40.000Z |
| 64 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 65 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 58 | 66 | - id: 5 |
| 59 | 67 | title: "Définir SIDEKICKS_SIGN_IDENTITY et SIDEKICKS_NOTARY_PROFILE" |
| 60 | - state: open | |
| 68 | + state: closed | |
| 61 | 69 | priority: medium |
| 62 | 70 | author: |
| 63 | 71 | name: k33g |
| 64 | 72 | email: ph.charriere@gmail.com |
| 65 | 73 | createdAt: 2026-10-31T11:03:40.000Z |
| 74 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 75 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 66 | 76 | 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: 56 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-31T11:03:40.000Z | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | 9 | +updatedAt: 2026-10-31T22:25:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::chore | 11 | - kind::chore |
| 12 | - area::infra | 12 | - area::infra |
| @@ -25,42 +25,52 @@ body: | | |||
| 25 | tasks: | 25 | tasks: |
| 26 | - id: 1 | 26 | - id: 1 |
| 27 | title: "Relever le Team ID" | 27 | title: "Relever le Team ID" |
| 28 | - state: open | 28 | + state: closed |
| 29 | priority: high | 29 | priority: high |
| 30 | author: | 30 | author: |
| 31 | name: k33g | 31 | name: k33g |
| 32 | email: ph.charriere@gmail.com | 32 | email: ph.charriere@gmail.com |
| 33 | createdAt: 2026-10-31T11:03:40.000Z | 33 | 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: 2 | 36 | - id: 2 |
| 35 | title: "Créer le certificat Developer ID Application" | 37 | title: "Créer le certificat Developer ID Application" |
| 36 | - state: open | 38 | + state: closed |
| 37 | priority: high | 39 | priority: high |
| 38 | author: | 40 | author: |
| 39 | name: k33g | 41 | name: k33g |
| 40 | email: ph.charriere@gmail.com | 42 | email: ph.charriere@gmail.com |
| 41 | createdAt: 2026-10-31T11:03:40.000Z | 43 | 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: 3 | 46 | - id: 3 |
| 43 | title: "Sauvegarder le certificat et sa clé (.p12)" | 47 | title: "Sauvegarder le certificat et sa clé (.p12)" |
| 44 | - state: open | 48 | + state: closed |
| 45 | priority: high | 49 | priority: high |
| 46 | author: | 50 | author: |
| 47 | name: k33g | 51 | name: k33g |
| 48 | email: ph.charriere@gmail.com | 52 | email: ph.charriere@gmail.com |
| 49 | createdAt: 2026-10-31T11:03:40.000Z | 53 | 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: 4 | 56 | - 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: open | 58 | + state: closed |
| 53 | priority: high | 59 | priority: high |
| 54 | author: | 60 | author: |
| 55 | name: k33g | 61 | name: k33g |
| 56 | email: ph.charriere@gmail.com | 62 | email: ph.charriere@gmail.com |
| 57 | createdAt: 2026-10-31T11:03:40.000Z | 63 | 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: 5 | 66 | - 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: open | 68 | + state: closed |
| 61 | priority: medium | 69 | priority: medium |
| 62 | author: | 70 | author: |
| 63 | name: k33g | 71 | name: k33g |
| 64 | email: ph.charriere@gmail.com | 72 | email: ph.charriere@gmail.com |
| 65 | createdAt: 2026-10-31T11:03:40.000Z | 73 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 57 |
| 3 | 3 | title: "Signer, notariser et publier les paquets macOS de la 1.0.0 (v1.0.0-macos)" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | |
| 9 | +updatedAt: 2026-10-31T22:25:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::chore |
| 12 | 12 | - area::infra |
| @@ -28,50 +28,62 @@ body: | | ||
| 28 | 28 | tasks: |
| 29 | 29 | - id: 1 |
| 30 | 30 | title: "Signer et notariser sidekicks.app" |
| 31 | - state: open | |
| 31 | + state: closed | |
| 32 | 32 | priority: high |
| 33 | 33 | author: |
| 34 | 34 | name: k33g |
| 35 | 35 | email: ph.charriere@gmail.com |
| 36 | 36 | createdAt: 2026-10-31T11:03:40.000Z |
| 37 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 38 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 37 | 39 | - id: 2 |
| 38 | 40 | title: "Refaire, signer et notariser le .dmg" |
| 39 | - state: open | |
| 41 | + state: closed | |
| 40 | 42 | priority: high |
| 41 | 43 | author: |
| 42 | 44 | name: k33g |
| 43 | 45 | email: ph.charriere@gmail.com |
| 44 | 46 | createdAt: 2026-10-31T11:03:40.000Z |
| 47 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 48 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 45 | 49 | - id: 3 |
| 46 | 50 | title: "Refaire le .zip et SHA256SUMS" |
| 47 | - state: open | |
| 51 | + state: closed | |
| 48 | 52 | priority: high |
| 49 | 53 | author: |
| 50 | 54 | name: k33g |
| 51 | 55 | email: ph.charriere@gmail.com |
| 52 | 56 | createdAt: 2026-10-31T11:03:40.000Z |
| 57 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 58 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 53 | 59 | - id: 4 |
| 54 | 60 | title: "Vérifier sur un autre Mac et tester l'app (runtime renforcé)" |
| 55 | - state: open | |
| 61 | + state: closed | |
| 56 | 62 | priority: high |
| 57 | 63 | author: |
| 58 | 64 | name: k33g |
| 59 | 65 | email: ph.charriere@gmail.com |
| 60 | 66 | createdAt: 2026-10-31T11:03:40.000Z |
| 67 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 68 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 61 | 69 | - id: 5 |
| 62 | 70 | title: "Publier la release v1.0.0-macos" |
| 63 | - state: open | |
| 71 | + state: closed | |
| 64 | 72 | priority: high |
| 65 | 73 | author: |
| 66 | 74 | name: k33g |
| 67 | 75 | email: ph.charriere@gmail.com |
| 68 | 76 | createdAt: 2026-10-31T11:03:40.000Z |
| 77 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 78 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 69 | 79 | - id: 6 |
| 70 | 80 | title: "Option : faire accepter les tags v<version>-<suffixe> au workflow Release" |
| 71 | - state: open | |
| 81 | + state: closed | |
| 72 | 82 | priority: low |
| 73 | 83 | author: |
| 74 | 84 | name: k33g |
| 75 | 85 | email: ph.charriere@gmail.com |
| 76 | 86 | createdAt: 2026-10-31T11:03:40.000Z |
| 87 | + updatedAt: 2026-10-31T22:25:00.000Z | |
| 88 | + closedAt: 2026-10-31T22:25:00.000Z | |
| 77 | 89 | 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: 57 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-31T11:03:40.000Z | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | 9 | +updatedAt: 2026-10-31T22:25:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::chore | 11 | - kind::chore |
| 12 | - area::infra | 12 | - area::infra |
| @@ -28,50 +28,62 @@ body: | | |||
| 28 | tasks: | 28 | tasks: |
| 29 | - id: 1 | 29 | - id: 1 |
| 30 | title: "Signer et notariser sidekicks.app" | 30 | title: "Signer et notariser sidekicks.app" |
| 31 | - state: open | 31 | + state: closed |
| 32 | priority: high | 32 | priority: high |
| 33 | author: | 33 | author: |
| 34 | name: k33g | 34 | name: k33g |
| 35 | email: ph.charriere@gmail.com | 35 | email: ph.charriere@gmail.com |
| 36 | createdAt: 2026-10-31T11:03:40.000Z | 36 | 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: 2 | 39 | - id: 2 |
| 38 | title: "Refaire, signer et notariser le .dmg" | 40 | title: "Refaire, signer et notariser le .dmg" |
| 39 | - state: open | 41 | + state: closed |
| 40 | priority: high | 42 | priority: high |
| 41 | author: | 43 | author: |
| 42 | name: k33g | 44 | name: k33g |
| 43 | email: ph.charriere@gmail.com | 45 | email: ph.charriere@gmail.com |
| 44 | createdAt: 2026-10-31T11:03:40.000Z | 46 | 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: 3 | 49 | - id: 3 |
| 46 | title: "Refaire le .zip et SHA256SUMS" | 50 | title: "Refaire le .zip et SHA256SUMS" |
| 47 | - state: open | 51 | + state: closed |
| 48 | priority: high | 52 | priority: high |
| 49 | author: | 53 | author: |
| 50 | name: k33g | 54 | name: k33g |
| 51 | email: ph.charriere@gmail.com | 55 | email: ph.charriere@gmail.com |
| 52 | createdAt: 2026-10-31T11:03:40.000Z | 56 | 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: 4 | 59 | - 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: open | 61 | + state: closed |
| 56 | priority: high | 62 | priority: high |
| 57 | author: | 63 | author: |
| 58 | name: k33g | 64 | name: k33g |
| 59 | email: ph.charriere@gmail.com | 65 | email: ph.charriere@gmail.com |
| 60 | createdAt: 2026-10-31T11:03:40.000Z | 66 | 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: 5 | 69 | - id: 5 |
| 62 | title: "Publier la release v1.0.0-macos" | 70 | title: "Publier la release v1.0.0-macos" |
| 63 | - state: open | 71 | + state: closed |
| 64 | priority: high | 72 | priority: high |
| 65 | author: | 73 | author: |
| 66 | name: k33g | 74 | name: k33g |
| 67 | email: ph.charriere@gmail.com | 75 | email: ph.charriere@gmail.com |
| 68 | createdAt: 2026-10-31T11:03:40.000Z | 76 | 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: 6 | 79 | - 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: open | 81 | + state: closed |
| 72 | priority: low | 82 | priority: low |
| 73 | author: | 83 | author: |
| 74 | name: k33g | 84 | name: k33g |
| 75 | email: ph.charriere@gmail.com | 85 | email: ph.charriere@gmail.com |
| 76 | createdAt: 2026-10-31T11:03:40.000Z | 86 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 58 |
| 3 | 3 | title: "Automatiser la signature et la notarisation dans make release-build" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | |
| 9 | +updatedAt: 2026-10-31T22:25:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | - area::infra |
| @@ -57,4 +57,11 @@ tasks: | ||
| 57 | 57 | name: k33g |
| 58 | 58 | email: ph.charriere@gmail.com |
| 59 | 59 | 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: 58 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-31T11:03:40.000Z | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | 9 | +updatedAt: 2026-10-31T22:25:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | - area::infra | 12 | - area::infra |
| @@ -57,4 +57,11 @@ tasks: | |||
| 57 | name: k33g | 57 | name: k33g |
| 58 | email: ph.charriere@gmail.com | 58 | email: ph.charriere@gmail.com |
| 59 | createdAt: 2026-10-31T11:03:40.000Z | 59 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 59 |
| 3 | 3 | title: "Guide d'installation macOS : plus d'« Ouvrir quand même » pour une release signée" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | |
| 9 | +updatedAt: 2026-10-31T22:25:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - documentation |
| 12 | 12 | - citizen-ux |
| @@ -27,4 +27,11 @@ tasks: | ||
| 27 | 27 | name: k33g |
| 28 | 28 | email: ph.charriere@gmail.com |
| 29 | 29 | 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: 59 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-31T11:03:40.000Z | 8 | createdAt: 2026-10-31T11:03:40.000Z |
| 9 | -updatedAt: 2026-10-31T11:03:40.000Z | 9 | +updatedAt: 2026-10-31T22:25:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - documentation | 11 | - documentation |
| 12 | - citizen-ux | 12 | - citizen-ux |
| @@ -27,4 +27,11 @@ tasks: | |||
| 27 | name: k33g | 27 | name: k33g |
| 28 | email: ph.charriere@gmail.com | 28 | email: ph.charriere@gmail.com |
| 29 | createdAt: 2026-10-31T11:03:40.000Z | 29 | 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: | ||
| 31 | 31 | state: closed |
| 32 | 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 | 33 | - title: "v1.0.0-macos · Signature et notarisation" |
| 34 | - state: open | |
| 34 | + state: closed | |
| 35 | 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." | |
| @@ -31,5 +31,11 @@ milestones: | |||
| 31 | state: closed | 31 | 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: open | 34 | + 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 | ||
| 52 | 52 | | `internal/acp` | ACP v1 client over stdio (wraps `coder/acp-go-sdk`) | |
| 53 | 53 | | `internal/config` | Configuration file: load, validate, locate, create, save, watch | |
| 54 | 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 | 56 | | `internal/shellpath` | The `PATH` of the user's shell, for an application opened from the Finder | |
| 56 | 57 | | `schema` | JSON Schema of the configuration file | |
| 57 | 58 | | `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 @@ | ||
| 3 | 3 | # Written by `mm init`. Every value says why it has that value; change |
| 4 | 4 | # them as you watch the agent work (guides/agent-settings-for-small-models.md). |
| 5 | 5 | # |
| 6 | -# model family: qwen | |
| 6 | +# model family: mellum | |
| 7 | 7 | # usage: developer |
| 8 | 8 | # 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 | 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 card | |
| 12 | +# [measured] measured in a demo of mm — re-measure after a model or quant change | |
| 13 | 13 | # [guide] the settings guide's row for this machine |
| 14 | 14 | # [rule] a rule of thumb, not specific to this model |
| 15 | 15 | # |
| 16 | 16 | # mm -config <this file> or mm -tui <this file> |
| 17 | 17 | |
| 18 | 18 | 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" | |
| 23 | 23 | # No baseUrl: llmman's defaults apply. For the record: |
| 24 | 24 | # baseUrl: "http://127.0.0.1:17434/v1" |
| 25 | 25 | # fallback: "http://host.docker.internal:17434/v1" # tried when baseUrl does not answer |
| 26 | 26 | |
| 27 | 27 | # 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 | 29 | 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 | |
| 32 | 32 | # usage developer |
| 33 | 33 | bashTool: true |
| 34 | 34 | editTools: true |
| @@ -49,22 +49,17 @@ watchdogTimeout: 30s | ||
| 49 | 49 | bashTimeout: 30s |
| 50 | 50 | |
| 51 | 51 | 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 | |
| 60 | 60 | # one tool call per reply: small models run a check before the change it |
| 61 | 61 | # checks has landed otherwise |
| 62 | 62 | 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 | 64 | context: |
| 70 | 65 | # 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; change | 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). | 4 | # them as you watch the agent work (guides/agent-settings-for-small-models.md). |
| 5 | # | 5 | # |
| 6 | -# model family: qwen | 6 | +# model family: mellum |
| 7 | # usage: developer | 7 | # usage: developer |
| 8 | # machine: small — a laptop, models up to about 8B | 8 | # 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 card | 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 | 13 | # [guide] the settings guide's row for this machine |
| 14 | # [rule] a rule of thumb, not specific to this model | 14 | # [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: llmman | 18 | 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 the | 20 | +# 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 answer | 25 | # 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 window | 27 | # 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: 0 | 29 | contextWindow: 0 |
| 30 | -# your answer: every command and file change waits for your answer | 30 | +# your answer: tools run without asking |
| 31 | -yolo: false | 31 | +yolo: true |
| 32 | # usage developer | 32 | # usage developer |
| 33 | bashTool: true | 33 | bashTool: true |
| 34 | editTools: true | 34 | editTools: true |
| @@ -49,22 +49,17 @@ watchdogTimeout: 30s | |||
| 49 | bashTimeout: 30s | 49 | 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.7 | 55 | + # [measured] family mellum: demos/little-coder (Mellum2 12B Instruct, |
| 56 | - top_p: 0.8 | 56 | + # measured 0.2 / 0.9); agent.dmr.yaml (0.0 / 0.9) |
| 57 | - top_k: 20 | 57 | + top_p: 0.9 |
| 58 | - # [rule] room for a long answer plus a whole tool call | 58 | + # [rule] room for an answer or a whole file in one tool call |
| 59 | - max_tokens: 16384 | 59 | + max_tokens: 4096 |
| 60 | # one tool call per reply: small models run a check before the change it | 60 | # one tool call per reply: small models run a check before the change it |
| 61 | # checks has landed otherwise | 61 | # checks has landed otherwise |
| 62 | parallel_tool_calls: false | 62 | 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 | ||
| 13 | 13 | |
| 14 | 14 | ### 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 | 18 | ### 2. Who answers |
| 19 | 19 | |
| @@ -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 thing | 14 | ### 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 answers | 18 | ### 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 | ||
| 7 | 7 | | You want to… | Section | Pages | |
| 8 | 8 | | --- | --- | --- | |
| 9 | 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 | 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 | 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 | |
| @@ -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 @@ | ||
| 1 | 1 | # 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 | 5 | ## Create a channel |
| 6 | 6 | |
| @@ -24,7 +24,19 @@ Hover the channel in the list: | ||
| 24 | 24 | |
| 25 | 25 | ## 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 | 41 | ## Address the right agent |
| 30 | 42 | |
| @@ -38,7 +50,7 @@ Click an agent in the **Agents** panel. Its direct conversation opens, and appea | ||
| 38 | 50 | ## Variants |
| 39 | 51 | |
| 40 | 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 | 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 | 56 | ## See also |
| @@ -1,6 +1,6 @@ | |||
| 1 | # How to manage channels and their agents | 1 | # 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 channel | 5 | ## Create a channel |
| 6 | 6 | ||
| @@ -24,7 +24,19 @@ Hover the channel in the list: | |||
| 24 | 24 | ||
| 25 | ## Talk with one agent directly | 25 | ## 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 agent | 41 | ## Address the right agent |
| 30 | 42 | ||
| @@ -38,7 +50,7 @@ Click an agent in the **Agents** panel. Its direct conversation opens, and appea | |||
| 38 | ## Variants | 50 | ## 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 also | 56 | ## 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 | ||
| 51 | 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 | 52 | | | `Logs(name)` | The last 200 stderr lines of the agent. | |
| 53 | 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 | 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 | 131 | | `Create(name, description)` | Creates a channel. A taken or empty name is an error. | |
| 131 | 132 | | `Rename(id, name, description)` | Renames a channel (not a direct conversation). | |
| 132 | 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 | 135 | | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invites or removes an agent; the channel shows `agent-joined` or `agent-left`. | |
| 134 | 136 | | `Direct(agent)` | The direct conversation with `agent`, created the first time. | |
| 135 | 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. | |
| @@ -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: | ||
| 36 | 36 | |
| 37 | 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 | 41 | ## Step 4 — Talk with Alice |
| 42 | 42 | |
| @@ -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 Alice | 41 | ## 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 | ||
| 7 | 7 | | Vous voulez… | Section | Pages | |
| 8 | 8 | | --- | --- | --- | |
| 9 | 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 | 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 | 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 | |
| @@ -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 @@ | ||
| 1 | 1 | # 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 | 5 | ## Créer un channel |
| 6 | 6 | |
| @@ -24,7 +24,19 @@ Survolez le channel dans la liste : | ||
| 24 | 24 | |
| 25 | 25 | ## 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 | 41 | ## 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 | 50 | ## Variantes |
| 39 | 51 | |
| 40 | 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 | 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 | 56 | ## Voir aussi |
| @@ -1,6 +1,6 @@ | |||
| 1 | # Gérer les channels et leurs agents | 1 | # 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 channel | 5 | ## Créer un channel |
| 6 | 6 | ||
| @@ -24,7 +24,19 @@ Survolez le channel dans la liste : | |||
| 24 | 24 | ||
| 25 | ## Parler directement à un agent | 25 | ## 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 agent | 41 | ## 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 | ## Variantes | 50 | ## 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 aussi | 56 | ## 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 | ||
| 51 | 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 | 52 | | | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. | |
| 53 | 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 | 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 | 131 | | `Create(name, description)` | Crée un channel. Un nom déjà pris ou vide est une erreur. | |
| 131 | 132 | | `Rename(id, name, description)` | Renomme un channel (pas une conversation directe). | |
| 132 | 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 | 135 | | `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invite ou retire un agent ; le channel affiche `agent-joined` ou `agent-left`. | |
| 134 | 136 | | `Direct(agent)` | La conversation directe avec `agent`, créée la première fois. | |
| 135 | 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. | |
| @@ -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 : | ||
| 38 | 38 | |
| 39 | 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 | 43 | ## Étape 4 — Discuter avec Alice |
| 44 | 44 | |
| @@ -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 Alice | 43 | ## É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> { | ||
| 46 | 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 | 58 | * Commands returns the slash commands an agent offers. |
| 51 | 59 | */ |
| @@ -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 @@ | ||
| 4 | 4 | import * as AgentService from "./agentservice.js"; |
| 5 | 5 | import * as ChannelService from "./channelservice.js"; |
| 6 | 6 | import * as ConfigService from "./configservice.js"; |
| 7 | +import * as SystemService from "./systemservice.js"; | |
| 7 | 8 | import * as TerminalService from "./terminalservice.js"; |
| 8 | 9 | import * as WorkspaceService from "./workspaceservice.js"; |
| 9 | 10 | export { |
| 10 | 11 | AgentService, |
| 11 | 12 | ChannelService, |
| 12 | 13 | ConfigService, |
| 14 | + SystemService, | |
| 13 | 15 | TerminalService, |
| 14 | 16 | WorkspaceService |
| 15 | 17 | }; |
| @@ -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 | WorkspaceService | 16 | 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 @@ | ||
| 81 | 81 | 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 | 92 | // Keys for the popup when it has suggestions; true when the key was used. |
| 85 | 93 | function popupKey(event: KeyboardEvent): boolean { |
| 86 | 94 | const items = suggestions?.items ?? [] |
| @@ -153,6 +161,7 @@ | ||
| 153 | 161 | role="option" |
| 154 | 162 | aria-selected={index === active} |
| 155 | 163 | class:active={index === active} |
| 164 | + use:shownWhen={index === active} | |
| 156 | 165 | onmousedown={(event) => { event.preventDefault(); choose(suggestion) }} |
| 157 | 166 | > |
| 158 | 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> |
| @@ -81,6 +81,14 @@ | |||
| 81 | trigger = null | 81 | 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 @@ | ||
| 3 | 3 | import { describe, expect, it } from 'vitest' |
| 4 | 4 | import type { Channel, Message } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/channels' |
| 5 | 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 | 8 | const general: Channel = { id: 'c1', name: 'general', description: '', direct: '', members: ['alice'] } |
| 9 | 9 | |
| @@ -80,10 +80,11 @@ describe('conversation events', () => { | ||
| 80 | 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', () => { | |
| 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() | |
| 87 | 88 | }) |
| 88 | 89 | }) |
| 89 | 90 | |
| @@ -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 { | ||
| 90 | 90 | } |
| 91 | 91 | |
| 92 | 92 | /** |
| 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). | |
| 96 | 95 | * |
| 97 | 96 | * @example |
| 98 | - * visibleDirects(channels, ['alice', 'bob']) | |
| 97 | + * directOf(channels, 'alice')?.id | |
| 99 | 98 | */ |
| 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) | |
| 102 | 101 | } |
| 103 | 102 | |
| 104 | 103 | /** |
| @@ -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 the | 93 | + * 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 their | 94 | + * panel shows its unread messages next to the agent (issue #60). |
| 95 | - * agent. | ||
| 96 | * | 95 | * |
| 97 | * @example | 96 | * @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', () => { | ||
| 28 | 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 | 35 | it('leaves unknown errors alone', () => { |
| 32 | 36 | expect(explain('something odd happened')).toBeNull() |
| 33 | 37 | 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[] = [ | ||
| 35 | 35 | { pattern: /the agent stopped/, key: 'error.agentStopped' }, |
| 36 | 36 | { pattern: /unknown agent:?\s*(\S*)/, key: 'error.unknownAgent', groups: ['agent'] }, |
| 37 | 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 | /** |
| @@ -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 = { | ||
| 27 | 27 | |
| 28 | 28 | // channels |
| 29 | 29 | 'channels.empty': 'No channel yet.', |
| 30 | - 'channels.section.direct': 'Direct messages', | |
| 31 | - 'channels.noDirect': 'Click an agent to talk with it directly.', | |
| 32 | 30 | 'channels.new': 'New channel', |
| 33 | 31 | 'channels.namePlaceholder': 'Channel name', |
| 34 | 32 | 'channels.create': 'Create', |
| @@ -43,6 +41,12 @@ export const en = { | ||
| 43 | 41 | // log |
| 44 | 42 | 'logs.of': 'Log of', |
| 45 | 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 | 51 | // status bar |
| 48 | 52 | 'status.agents.one': '{count} active agent', |
| @@ -66,6 +70,11 @@ export const en = { | ||
| 66 | 70 | 'conversation.invite': '+ Invite', |
| 67 | 71 | 'conversation.inviteLabel': 'Invite an agent into the channel', |
| 68 | 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 | 78 | 'conversation.historyProblem': 'History unavailable ({problem}): this session will be forgotten when Sidekicks quits.', |
| 70 | 79 | |
| 71 | 80 | // composer |
| @@ -149,6 +158,7 @@ export const en = { | ||
| 149 | 158 | 'error.agentStopped': 'The agent stopped unexpectedly. Its log (📂, then Log) tells why.', |
| 150 | 159 | 'error.unknownAgent': 'The agent "{agent}" is no longer in the configuration.', |
| 151 | 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 | 162 | 'code.copy': 'Copy', |
| 153 | 163 | 'code.copied': 'Copied ✓', |
| 154 | 164 | 'message.copy': 'Copy the answer', |
| @@ -364,8 +374,6 @@ export const fr: Record<MessageKey, string> = { | ||
| 364 | 374 | 'agent.open': 'Ouvrir la conversation avec {agent}', |
| 365 | 375 | |
| 366 | 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 | 377 | 'channels.new': 'Nouveau channel', |
| 370 | 378 | 'channels.namePlaceholder': 'Nom du channel', |
| 371 | 379 | 'channels.create': 'Créer', |
| @@ -379,6 +387,12 @@ export const fr: Record<MessageKey, string> = { | ||
| 379 | 387 | |
| 380 | 388 | 'logs.of': 'Journal de', |
| 381 | 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 | 397 | 'status.agents.one': '{count} agent actif', |
| 384 | 398 | 'status.agents.other': '{count} agents actifs', |
| @@ -400,6 +414,11 @@ export const fr: Record<MessageKey, string> = { | ||
| 400 | 414 | 'conversation.invite': '+ Inviter', |
| 401 | 415 | 'conversation.inviteLabel': 'Inviter un agent dans le channel', |
| 402 | 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 | 422 | 'conversation.historyProblem': "Historique indisponible ({problem}) : cette session sera oubliée à la fermeture de Sidekicks.", |
| 404 | 423 | |
| 405 | 424 | 'composer.label': 'Votre message', |
| @@ -478,6 +497,7 @@ export const fr: Record<MessageKey, string> = { | ||
| 478 | 497 | 'error.agentStopped': "L'agent s'est arrêté de façon inattendue. Son journal (📂, puis Journal) en donne la raison.", |
| 479 | 498 | 'error.unknownAgent': "L'agent « {agent} » n'est plus dans la configuration.", |
| 480 | 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 | 501 | 'code.copy': 'Copier', |
| 482 | 502 | 'code.copied': 'Copié ✓', |
| 483 | 503 | 'message.copy': 'Copier la réponse', |
| @@ -27,8 +27,6 @@ export const en = { | |||
| 27 | 27 | ||
| 28 | // channels | 28 | // 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 | // log | 41 | // 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 bar | 51 | // 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 | // composer | 80 | // 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 @@ | ||
| 6 | 6 | import AgentAvatar from '../agent/AgentAvatar.svelte' |
| 7 | 7 | import { AgentService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app' |
| 8 | 8 | import { act, appState, channelOf, openAssistant, openDirect, openWorkspace } from '../state.svelte' |
| 9 | + import { directOf } from '../conversation' | |
| 9 | 10 | |
| 10 | 11 | // The agent of the direct conversation shown, to highlight it. |
| 11 | 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 | 20 | </script> |
| 13 | 21 | |
| 14 | 22 | {#if appState.agents.length === 0} |
| @@ -19,6 +27,9 @@ | ||
| 19 | 27 | <li title={agent.description || agent.title} class:selected={appState.workspaceAgent ? appState.workspaceAgent === agent.name : directAgent === agent.name}> |
| 20 | 28 | <span class="dot {agent.state}" aria-hidden="true"></span> |
| 21 | 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 | 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 | 34 | <span class="actions"> |
| 24 | 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 | 63 | .dot.busy, .dot.starting { background: var(--warn); } |
| 53 | 64 | .dot.error { background: var(--danger); } |
| 54 | 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 | 67 | .state { grid-column: 2; grid-row: 2; color: var(--muted); font-size: 12px; } |
| 56 | 68 | .actions { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; opacity: 0; } |
| 57 | 69 | li:hover .actions, li:focus-within .actions { opacity: 1; } |
| 70 | + li:hover .badge, li:focus-within .badge { visibility: hidden; } | |
| 58 | 71 | .actions button { padding: 1px 7px; font-size: 11px; } |
| 59 | 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 | 73 | .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 @@ | ||
| 3 | 3 | import { t } from '../i18n/index.svelte' |
| 4 | 4 | import { appState, createChannel, deleteChannel, renameChannel, selectChannel } from '../state.svelte' |
| 5 | 5 | import ActionError from '../chat/ActionError.svelte' |
| 6 | - import AgentAvatar from '../agent/AgentAvatar.svelte' | |
| 7 | - import { visibleDirects } from '../conversation' | |
| 8 | 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 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). | |
| 12 | 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 | 12 | let creating = $state(false) |
| 17 | 13 | // ⌘N, or "New channel" in the palette. |
| @@ -55,10 +51,6 @@ | ||
| 55 | 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 | 54 | // Focuses an input as soon as it appears. |
| 63 | 55 | function autofocus(element: HTMLInputElement) { |
| 64 | 56 | element.focus() |
| @@ -122,29 +114,7 @@ | ||
| 122 | 114 | <button class="add" onclick={() => { appState.actionError = ''; creating = true }}>+ {t('channels.new')}</button> |
| 123 | 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 | 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 | 118 | .add { margin: 4px 8px 0; border: none; background: none; padding: 2px 0; color: var(--muted); font-size: 12px; } |
| 149 | 119 | .add:hover { color: var(--accent); } |
| 150 | 120 | .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 direct | 8 | + // 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 @@ | ||
| 14 | 14 | import Logo from '../layout/Logo.svelte' |
| 15 | 15 | import { isNearBottom, type Trigger } from '../composer' |
| 16 | 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 | 18 | import { ConfigService, WorkspaceService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app' |
| 19 | 19 | import type { FilesDroppedPayload } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/events' |
| 20 | 20 | import { FilesDropped } from '../../../bindings/events' |
| @@ -39,6 +39,8 @@ | ||
| 39 | 39 | return conversation?.busy && last?.role === 'agent' && !last.done ? trailingFailures(last.parts ?? []) : 0 |
| 40 | 40 | }) |
| 41 | 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 | 45 | function infoOf(name: string): AgentInfo { |
| 44 | 46 | const agent = appState.agents.find((candidate) => candidate.name === name) |
| @@ -240,6 +242,17 @@ | ||
| 240 | 242 | {#if percent !== null} |
| 241 | 243 | <span class="usage" title={t('conversation.usageHint')}>{t('conversation.usage', { percent })}</span> |
| 242 | 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 | 256 | </header> |
| 244 | 257 | <div class="messages" bind:this={list} onscroll={onScroll}> |
| 245 | 258 | {#if conversation.more} |
| @@ -333,6 +346,13 @@ | ||
| 333 | 346 | .busy { color: var(--warn); font-size: 12px; font-style: italic; display: inline-flex; align-items: center; gap: 6px; } |
| 334 | 347 | .pending { color: var(--muted); font-size: 13px; font-style: italic; display: flex; align-items: center; gap: 8px; margin: 0; } |
| 335 | 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 | 356 | .messages { flex: 1; overflow-y: auto; padding: 16px 18px; display: flex; flex-direction: column; gap: 14px; } |
| 337 | 357 | .older { align-self: center; font-size: 12px; } |
| 338 | 358 | .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 ?? []) : 0 | 39 | 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 @@ | ||
| 3 | 3 | import { t } from '../i18n/index.svelte' |
| 4 | 4 | import { appState } from '../state.svelte' |
| 5 | 5 | import AgentLog from '../agent/AgentLog.svelte' |
| 6 | + import MemoryPressure from './MemoryPressure.svelte' | |
| 6 | 7 | |
| 7 | 8 | let selected = $state('') |
| 8 | 9 | const agentName = $derived(selected || appState.agents[0]?.name || '') |
| 9 | 10 | </script> |
| 10 | 11 | |
| 11 | 12 | <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> | |
| 20 | 24 | <AgentLog agent={agentName} /> |
| 21 | 25 | </div> |
| 22 | 26 | |
| 23 | 27 | <style> |
| 24 | 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 | 31 | select { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; } |
| 27 | 32 | </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) { | ||
| 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 | 303 | export function addMember(id: string, agent: string) { |
| 291 | 304 | return act(async () => { |
| 292 | 305 | 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 ( | ||
| 10 | 10 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 |
| 11 | 11 | github.com/wailsapp/wails/v3 v3.0.0 |
| 12 | 12 | go.yaml.in/yaml/v3 v3.0.5 |
| 13 | + golang.org/x/sys v0.48.0 | |
| 13 | 14 | golang.org/x/text v0.42.0 |
| 14 | 15 | ) |
| 15 | 16 | |
| @@ -47,6 +48,5 @@ require ( | ||
| 47 | 48 | github.com/prometheus/procfs v0.10.1 // indirect |
| 48 | 49 | github.com/rogpeppe/go-internal v1.9.0 // indirect |
| 49 | 50 | golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect |
| 50 | - golang.org/x/sys v0.48.0 // indirect | |
| 51 | 51 | google.golang.org/protobuf v1.33.0 // indirect |
| 52 | 52 | ) |
| @@ -10,6 +10,7 @@ require ( | |||
| 10 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 | 10 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 |
| 11 | github.com/wailsapp/wails/v3 v3.0.0 | 11 | github.com/wailsapp/wails/v3 v3.0.0 |
| 12 | go.yaml.in/yaml/v3 v3.0.5 | 12 | go.yaml.in/yaml/v3 v3.0.5 |
| 13 | + golang.org/x/sys v0.48.0 | ||
| 13 | golang.org/x/text v0.42.0 | 14 | golang.org/x/text v0.42.0 |
| 14 | ) | 15 | ) |
| 15 | 16 | ||
| @@ -47,6 +48,5 @@ require ( | |||
| 47 | github.com/prometheus/procfs v0.10.1 // indirect | 48 | github.com/prometheus/procfs v0.10.1 // indirect |
| 48 | github.com/rogpeppe/go-internal v1.9.0 // indirect | 49 | github.com/rogpeppe/go-internal v1.9.0 // indirect |
| 49 | golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect | 50 | 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 // indirect | 51 | 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 { | ||
| 74 | 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 | 83 | // AddMember invites an agent into a channel. |
| 78 | 84 | func (s *ChannelService) AddMember(id, agent string) error { |
| 79 | 85 | 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 ( | ||
| 77 | 77 | ErrUnknownAgent = errors.New("unknown agent") |
| 78 | 78 | ErrUnknownChannel = errors.New("unknown channel") |
| 79 | 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 | 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 | 446 | 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 | 481 | // AddMember invites agent into a channel; the channel says so. |
| 449 | 482 | func (m *Manager) AddMember(id, agent string) error { |
| 450 | 483 | 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 nil | 446 | 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) { | ||
| 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 | 95 | func TestMembersJoinAndLeaveVisibly(t *testing.T) { |
| 53 | 96 | f := setup(t, nil) |
| 54 | 97 | 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 { | ||
| 341 | 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 | 355 | // ---------------------------------------------------------------- members |
| 345 | 356 | |
| 346 | 357 | // 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 | // ---------------------------------------------------------------- members | 355 | // ---------------------------------------------------------------- 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) { | ||
| 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 | 120 | func TestADirectChannelIsFoundByItsAgentAndHasNoName(t *testing.T) { |
| 95 | 121 | db := memory(t) |
| 96 | 122 | 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() { | ||
| 133 | 133 | application.NewService(&app.ChannelService{Core: core}), |
| 134 | 134 | application.NewService(&app.WorkspaceService{Core: core}), |
| 135 | 135 | application.NewService(&app.TerminalService{Core: core}), |
| 136 | + application.NewService(&app.SystemService{}), | |
| 136 | 137 | }, |
| 137 | 138 | Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)}, |
| 138 | 139 | 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}, |