Skip to content

Commit 6abe698

Browse files
committed
- Scan terminal output for "esc to " pattern to detect busy state
- Strip ANSI escape sequences and lowercase on-the-fly for matching - Debounce idle transition (500ms) to avoid flickering - Send agent-status:busy/idle control messages to mobile - Document update-required and mobile-info control messages in CLAUDE.md
1 parent bc280f6 commit 6abe698

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ make major # X.0.0
4545
| Fichier | Role |
4646
|---------|------|
4747
| `main.go` | Entry point, parsing flags, demarrage PTY, boucle I/O |
48-
| `types.go` | Struct `Daemon`, `Message`, constantes ANSI |
48+
| `types.go` | Struct `Daemon`, `Message`, constantes ANSI, `MinAppVersion` |
4949
| `constants.go` | Timeouts, buffer sizes, permissions |
5050
| `agents.go` | Detection agents IA (claude, gemini, codex), selection |
5151
| `encryption.go` | AES-256-GCM pour chiffrement donnees session |
@@ -59,7 +59,7 @@ make major # X.0.0
5959
| `ssh.go` | Detection SSH, installation cles authorized_keys |
6060
| `commands_session.go` | Commande /qr (affichage QR pairing) |
6161
| `commands_pairing.go` | Affichage QR pairing depuis daemon |
62-
| `commands_info.go` | Envoi info CLI au mobile |
62+
| `commands_info.go` | Envoi info CLI au mobile, handleMobileInfo (version check) |
6363
| `commands_terminal.go` | Traitement messages controle (resize, file-upload) |
6464
| `commands_upload.go` | Upload fichiers (simple et chunked) |
6565
| `utils.go` | Utilitaires (openFile cross-platform) |
@@ -81,6 +81,7 @@ Les messages de controle passent par le canal data avec prefix `\x00CTRL:`.
8181
| `file-upload-ack` | `file-upload-ack:uploadId:chunkIndex` | Confirmation chunk recu |
8282
| `file-upload-result` | `file-upload-result:success\|error:path\|msg` | Resultat upload |
8383
| `ssh-setup-result` | `ssh-setup-result:success\|error:message` | Resultat installation cle SSH |
84+
| `update-required` | `update-required:{"min_app_version":"X.Y.Z","cli_version":"...","message":"..."}` | Demande au mobile de se mettre a jour |
8485

8586
### Recus (Mobile -> CLI)
8687

@@ -93,6 +94,7 @@ Les messages de controle passent par le canal data avec prefix `\x00CTRL:`.
9394
| `file-upload-start` | `file-upload-start:uploadId:filename:totalChunks:totalSize` | Debut upload chunked |
9495
| `file-upload-chunk` | `file-upload-chunk:uploadId:chunkIndex:dataBase64` | Chunk upload |
9596
| `file-upload-cancel` | `file-upload-cancel:uploadId` | Annule upload chunked |
97+
| `mobile-info` | `mobile-info:{"app_version":"X.Y.Z"}` | Version mobile, CLI compare avec `MinAppVersion` |
9698

9799
---
98100

agent_status.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"time"
6+
)
7+
8+
// Agent busy/idle detection via PTY output scanning.
9+
// Detects "esc to " pattern (case-insensitive) in terminal output
10+
// to determine if the agent is busy (thinking/processing).
11+
12+
const (
13+
// AgentIdleDebounce is the delay before declaring idle after pattern disappears
14+
AgentIdleDebounce = 500 * time.Millisecond
15+
// agentStatusBufSize is the rolling buffer size for pattern scanning
16+
agentStatusBufSize = 256
17+
)
18+
19+
// busyPattern is the lowercase pattern to detect in PTY output
20+
var busyPattern = []byte("esc to ")
21+
22+
// initAgentStatus initializes the agent status detection fields
23+
func (d *Daemon) initAgentStatus() {
24+
d.agentStatusBuf = make([]byte, 0, agentStatusBufSize)
25+
}
26+
27+
// scanAgentStatus scans PTY output for the busy pattern.
28+
// Called from startPTYReader on every read — must be fast.
29+
func (d *Daemon) scanAgentStatus(data []byte) {
30+
// Append printable chars (lowercased) to rolling buffer, skip ANSI sequences
31+
inEscape := d.agentStatusInEscape
32+
buf := d.agentStatusBuf
33+
34+
for _, b := range data {
35+
if inEscape {
36+
// Inside ANSI escape: wait for terminating letter
37+
if (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') {
38+
inEscape = false
39+
}
40+
continue
41+
}
42+
if b == 0x1b {
43+
inEscape = true
44+
continue
45+
}
46+
// Only keep printable ASCII
47+
if b >= 0x20 && b <= 0x7e {
48+
// Lowercase on the fly
49+
if b >= 'A' && b <= 'Z' {
50+
b += 0x20
51+
}
52+
buf = append(buf, b)
53+
}
54+
}
55+
d.agentStatusInEscape = inEscape
56+
57+
// Trim buffer to max size (keep tail)
58+
if len(buf) > agentStatusBufSize {
59+
buf = buf[len(buf)-agentStatusBufSize:]
60+
}
61+
d.agentStatusBuf = buf
62+
63+
// Check for pattern
64+
found := bytes.Contains(buf, busyPattern)
65+
66+
if found {
67+
// Agent is busy — notify immediately if state changed
68+
if d.agentIdleTimer != nil {
69+
d.agentIdleTimer.Stop()
70+
d.agentIdleTimer = nil
71+
}
72+
if !d.agentBusy {
73+
d.agentBusy = true
74+
d.sendControlMessage("agent-status:busy")
75+
}
76+
} else if d.agentBusy {
77+
// Pattern gone — debounce before declaring idle
78+
if d.agentIdleTimer == nil {
79+
d.agentIdleTimer = time.AfterFunc(AgentIdleDebounce, func() {
80+
d.agentBusy = false
81+
d.agentIdleTimer = nil
82+
d.sendControlMessage("agent-status:idle")
83+
})
84+
}
85+
}
86+
}

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ func createDaemon(session, token, relay, command, workDir string, agentType Agen
238238
log.Fatal("Failed to initialize encryption:", err)
239239
}
240240

241+
// Initialize agent busy/idle detection
242+
daemon.initAgentStatus()
243+
241244
return daemon
242245
}
243246

@@ -337,6 +340,7 @@ func startPTYReader(daemon *Daemon) {
337340
return
338341
}
339342

343+
daemon.scanAgentStatus(buf[:n])
340344
os.Stdout.Write(buf[:n])
341345
daemon.sendToMobile(buf[:n])
342346
}

types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ type Daemon struct {
100100
// Context for cancelling ping goroutine
101101
pingCtx context.Context
102102
pingCancel context.CancelFunc
103+
104+
// Agent busy/idle detection
105+
agentBusy bool
106+
agentIdleTimer *time.Timer
107+
agentStatusBuf []byte
108+
agentStatusInEscape bool
103109
}
104110

105111
// Message types for WebSocket communication

0 commit comments

Comments
 (0)