Files
Loki/internal/loki/code_mode.go
T
Claude ef11454a21 API interne d'un tour de discussion tenu par Rust
Ce qui décrit un tour reste au Go le temps d'être porté (lot 7) ; la
façade le demande ici :
- /turn/start : pièces jointes et vision (userMessageContent), capacités
  demandées et celles du tour (rôle du mode Code figé), nom du modèle,
  puce « mode Code » (une fois par discussion) ;
- /turn/view : la tête de turnView sans PROJ_SNAPSHOT (prompt du preset,
  consignes du dépôt, contexte du projet), le préambule d'InjectSkills et
  les outils du tour, MCP compris ; 409 sous PROJ_SNAPSHOT ;
- /turn/done : notification Web Push de fin de tour ;
- /conv/active : la discussion active des outils suit la façade ;
- /conv/dropped : fichiers, travaux, critères, mode et résultats d'outils
  d'une discussion supprimée.

injectPrefix et codeHintDue sortent d'InjectSkills et de maybeCodeHint,
sans changement de comportement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-15 10:48:38 +00:00

190 lines
5.8 KiB
Go

package loki
// code_mode.go — le mode Chat/Code d'une discussion. Bascule EXPLICITE
// (sélecteur dans le pied du composeur), persistée par discussion ; en mode
// chat, une détection côté serveur peut SUGGÉRER la bascule (puce ignorable
// dans l'UI) mais ne l'impose jamais — un faux positif coûterait des tours de
// planification/vérification sur un simple bonjour.
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
func modeKey(convID string) string { return "mode:" + convID }
func hintKey(convID string) string { return "hinted:" + convID }
// convCodeMode : la discussion active est-elle en mode code ?
func convCodeMode() bool { return getStr(bkChat, modeKey(convEnsureActive())) == "code" }
func setConvMode(convID, mode string) error {
if mode != "code" {
mode = "" // chat = défaut = clé absente
}
return putStr(bkChat, modeKey(convID), mode)
}
// dropConvMode : appelé à la suppression d'une discussion.
func dropConvMode(convID string) {
_ = putStr(bkChat, modeKey(convID), "")
_ = putStr(bkChat, hintKey(convID), "")
}
// --- Détection « ça ressemble à du code » ------------------------------------
// Côté serveur, AUCUN token : ni consigne dans le prompt, ni appel modèle.
// Un indice suffit ; la puce de l'UI est gratuite à ignorer.
var codeHintRes = []*regexp.Regexp{
// Un bloc de code clôturé.
regexp.MustCompile("```"),
// Un chemin de fichier source.
regexp.MustCompile(`(?i)\b[\w./\\-]+\.(go|py|js|jsx|ts|tsx|rs|java|kt|c|h|cpp|hpp|cs|rb|php|swift|sh|ps1|sql|html|css|scss|vue|svelte|toml|yaml|yml|json|dockerfile|makefile)\b`),
// Verbes / vocabulaire de tâche de code.
regexp.MustCompile(`(?i)\b(corrige|implémente|implemente|refactor|refactorise|débogue|debug|compile|stack ?trace|segfault|exception|traceback|unit ?test|pull ?request|merge|commit|clone)\b`),
regexp.MustCompile(`(?i)\b(bug|erreur de compilation|tests? (échoue|cassé)|fonction|classe|module|api rest|endpoint)\b`),
}
// looksLikeCode dit si un message utilisateur ressemble à une tâche de code.
// Le dépôt git déjà cloné dans la discussion est aussi un indice fort.
func looksLikeCode(text string) bool {
if strings.TrimSpace(text) == "" {
return false
}
for _, re := range codeHintRes {
if re.MatchString(text) {
return true
}
}
return false
}
// convHasRepo : un dépôt git vit déjà dans le dossier de la discussion.
func convHasRepo() bool {
ws := agentCwd()
if _, err := os.Stat(filepath.Join(ws, ".git")); err == nil {
return true
}
entries, err := os.ReadDir(ws)
if err != nil {
return false
}
for _, e := range entries {
if !e.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(ws, e.Name(), ".git")); err == nil {
return true
}
}
return false
}
// maybeCodeHint émet la puce « passer en mode Code ? » si le message s'y
// prête. Une seule fois par discussion : une suggestion répétée est du bruit.
func maybeCodeHint(c *Conversation, epoch int, text string) {
if codeHintDue(text) {
c.appendDelta(epoch, map[string]any{"code_hint": true})
}
}
// codeHintDue dit si la puce doit apparaître pour ce message, et la marque
// alors comme montrée pour la discussion active (une fois par discussion).
func codeHintDue(text string) bool {
if convCodeMode() || !agentEnabled() {
return false
}
id := convEnsureActive()
if getBool(bkChat, hintKey(id)) {
return false
}
if !looksLikeCode(text) && !convHasRepo() {
return false
}
_ = putBool(bkChat, hintKey(id), true)
return true
}
// --- API ---------------------------------------------------------------------
// handleChatMode — GET : {mode, criteria} de la discussion active ;
// POST {"mode":"code"|"chat"} : bascule (et pousse le nouvel état aux abonnés).
func handleChatMode(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var body struct {
Mode string `json:"mode"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
if body.Mode != "code" && body.Mode != "chat" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "mode inconnu (chat|code)"})
return
}
id := convEnsureActive()
if err := setConvMode(id, body.Mode); err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
// Pousse la bascule à tous les appareils abonnés (le sélecteur doit
// suivre, comme le fil lui-même).
conv.mu.Lock()
epoch := conv.epoch
conv.mu.Unlock()
conv.appendDelta(epoch, map[string]any{"mode": body.Mode})
sendJSON(w, 200, map[string]any{"ok": true, "mode": body.Mode})
return
}
mode := "chat"
if convCodeMode() {
mode = "code"
}
sendJSON(w, 200, map[string]any{
"ok": true,
"mode": mode,
"criteria": critList(convEnsureActive()),
})
}
// handleChatCriteria — POST : édition manuelle des critères depuis l'UI.
// {"criteria":[{"id":1,"text":"…","status":"pending"},…]} remplace la liste.
func handleChatCriteria(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
sendJSON(w, 405, map[string]any{"ok": false, "error": "POST attendu"})
return
}
var body struct {
Criteria []Criterion `json:"criteria"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
var clean []Criterion
next := 1
for _, c := range body.Criteria {
if strings.TrimSpace(c.Text) == "" || len(clean) >= critMaxCount {
continue
}
if c.ID <= 0 {
c.ID = next
}
if c.ID >= next {
next = c.ID + 1
}
switch c.Status {
case "pending", "passed", "failed":
default:
c.Status = "pending"
}
clean = append(clean, c)
}
critSave(convEnsureActive(), clean)
critNotify(clean)
sendJSON(w, 200, map[string]any{"ok": true, "criteria": clean})
}