clideck 1.25.7 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/activity.js CHANGED
@@ -56,6 +56,8 @@ function isActive(id) {
56
56
  return s ? (Date.now() - s.lastOutAt < 2000) : false;
57
57
  }
58
58
 
59
+ function lastOutputAt(id) { return stream[id]?.lastOutAt || 0; }
60
+
59
61
  function clear(id) { delete net[id]; delete stream[id]; }
60
62
 
61
- module.exports = { start, stop, trackIn, trackOut, isActive, clear };
63
+ module.exports = { start, stop, trackIn, trackOut, isActive, lastOutputAt, clear };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.25.7",
3
+ "version": "1.26.0",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
package/sessions.js CHANGED
@@ -104,14 +104,29 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
104
104
 
105
105
  term.onExit(() => {
106
106
  // Skip cleanup if this PTY was replaced by a restart
107
- if (sessions.get(id)?.pty !== term) return;
107
+ const s = sessions.get(id);
108
+ if (s?.pty !== term) return;
108
109
  activity.clear(id);
109
110
  telemetry.clear(id);
110
111
  opencodeBridge.clear(id);
111
- transcript.clear(id);
112
112
  plugins.clearStatus(id);
113
+ // If resumable and token captured, move to resumable list (keep transcript for search)
114
+ if (cmd.canResume && cmd.resumeCommand && s.sessionToken) {
115
+ resumable.push({
116
+ id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
117
+ themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
118
+ lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
119
+ savedAt: new Date().toISOString(),
120
+ });
121
+ console.log(`Session ${id.slice(0, 8)}: moved to resumable on exit (token: ${s.sessionToken.slice(0, 12)}…)`);
122
+ } else {
123
+ transcript.clear(id);
124
+ }
113
125
  sessions.delete(id);
114
126
  broadcast({ type: 'closed', id });
127
+ if (cmd.canResume && s.sessionToken) {
128
+ broadcast({ type: 'sessions.resumable', list: getResumable() });
129
+ }
115
130
  });
116
131
 
117
132
  return null;
@@ -2,8 +2,10 @@
2
2
  // CLI agents export telemetry events here; we capture agent session IDs
3
3
  // (for resume) and detect whether telemetry is configured (setup prompts).
4
4
 
5
+ const ioActivity = require('./activity');
5
6
  const activity = new Map(); // sessionId → has received events
6
7
  const pendingSetup = new Map(); // sessionId → timer (waiting for first event)
8
+ const pendingIdle = new Map(); // sessionId → timer (api_request → confirm idle after output silence)
7
9
  let broadcastFn = null;
8
10
  let sessionsFn = null;
9
11
 
@@ -69,36 +71,42 @@ function handleLogs(req, res) {
69
71
  const attrs = parseAttrs(lr.attributes);
70
72
 
71
73
  const eventName = attrs['event.name'];
74
+ // Debug telemetry logs — uncomment as needed, do not delete
75
+ // if (serviceName === 'claude-code' && eventName) console.log(`[telemetry:claude] ${eventName}`);
72
76
  // if (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName}`);
73
77
  // if (serviceName === 'gemini-cli' && eventName) console.log(`[telemetry:gemini] ${eventName}`);
74
78
 
75
79
  // Telemetry-based status
76
80
  const startEvents = new Set(['user_prompt', 'gemini_cli.user_prompt', 'codex.user_prompt']);
77
81
  if (startEvents.has(eventName)) {
82
+ cancelPendingIdle(resolvedId);
78
83
  broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
79
84
  }
80
- // Claude: telemetry-only status. user_prompt/any event working, api_request idle.
85
+ // Claude: telemetry-only status. api_requestpending idle (confirm after 1s output silence, expire after 6s).
81
86
  if (serviceName === 'claude-code' && eventName) {
82
87
  if (eventName === 'api_request') {
83
- broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
88
+ startPendingIdle(resolvedId);
84
89
  } else if (eventName !== 'user_prompt') {
90
+ cancelPendingIdle(resolvedId);
85
91
  broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
86
92
  }
87
93
  }
88
- // Codex: telemetry-only status. codex.user_prompt/any event working, codex.sse_eventidle.
94
+ // Codex: codex.sse_eventpending idle (2s PTY silence), other events working.
89
95
  if (serviceName === 'codex_cli_rs' && eventName) {
90
96
  if (eventName === 'codex.sse_event') {
91
- broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
97
+ startPendingIdle(resolvedId);
92
98
  } else if (eventName !== 'codex.user_prompt') {
99
+ cancelPendingIdle(resolvedId);
93
100
  broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
94
101
  }
95
102
  }
96
- // Gemini: telemetry-only status. Whitelisted events working, api_response (role=main) → idle.
103
+ // Gemini: api_response (role=main) pending idle (2s PTY silence), whitelisted events working.
97
104
  if (serviceName === 'gemini-cli' && eventName) {
98
105
  if (eventName === 'gemini_cli.api_response' && attrs['role'] === 'main') {
99
- broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
106
+ startPendingIdle(resolvedId);
100
107
  } else if (eventName === 'gemini_cli.api_request' || eventName === 'gemini_cli.model_routing'
101
108
  || (eventName === 'gemini_cli.api_response' && attrs['role'] !== 'main')) {
109
+ cancelPendingIdle(resolvedId);
102
110
  broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
103
111
  }
104
112
  }
@@ -142,8 +150,27 @@ function cancelPendingSetup(sessionId) {
142
150
  }
143
151
  }
144
152
 
153
+ // Pending idle: starts a check loop. Confirm idle after 2s of PTY output silence.
154
+ function startPendingIdle(id) {
155
+ cancelPendingIdle(id);
156
+ const started = Date.now();
157
+ const check = setInterval(() => {
158
+ if (Date.now() - Math.max(started, ioActivity.lastOutputAt(id)) >= 2000) {
159
+ cancelPendingIdle(id);
160
+ broadcastFn?.({ type: 'session.status', id, working: false, source: 'telemetry' });
161
+ }
162
+ }, 250);
163
+ pendingIdle.set(id, check);
164
+ }
165
+
166
+ function cancelPendingIdle(id) {
167
+ const timer = pendingIdle.get(id);
168
+ if (timer) { clearInterval(timer); pendingIdle.delete(id); }
169
+ }
170
+
145
171
  function clear(id) {
146
172
  activity.delete(id);
173
+ cancelPendingIdle(id);
147
174
  const pending = pendingSetup.get(id);
148
175
  if (pending) { clearTimeout(pending.timer); pendingSetup.delete(id); }
149
176
  }
package/workplan.txt CHANGED
@@ -1,28 +1,43 @@
1
- CliDeck — Future Tasks
1
+ CliDeck — Work Plan
2
2
  ======================
3
3
 
4
- ## Structured menu events
5
-
6
- Move interactive menu detection from mobile-side parsing to CliDeck desktop.
7
- Detect menus server-side, broadcast as structured data, render on both desktop and mobile.
8
-
9
- Why:
10
- - Single source of truth detect once, consume everywhere
11
- - Enables desktop sidebar preview ("please make a choice")
12
- - Opens the door to raw ANSI-based detection (mobile never sees PTY bytes)
13
- - Fixes stuck "working" status when agent shows a menu: no telemetry event fires for
14
- "waiting for user input", so CliDeck never flips to idle and never sends the notification.
15
- Menu detection can double as an idle signal — if a menu is detected, force status to idle.
16
-
17
- Steps:
18
- 1. Add menu detection in CliDeck where screen parsing already happens
19
- 2. Store per-session menu state (choices, selected, footer)
20
- 3. Detect menu appearance and disappearance, broadcast session.menu events
21
- 4. Show "make a choice" preview in desktop session list
22
- 5. Daemon forwards session.menu to mobile
23
- 6. Remove mobile-side menu parsing, render from structured state instead
24
-
25
- Notes:
26
- - Detection heuristic stays the same (footer + numbered lines) unless ANSI-based approach is pursued
27
- - Currently mobile detection works fine with regex fix + "Esc to cancel" footer check
28
- - No agent-side telemetry event exists for menus — heuristic is the only option for now
4
+ ## Structured menu events — DONE
5
+
6
+ Moved interactive menu detection from mobile-side parsing to CliDeck desktop.
7
+ Menus detected server-side, broadcast as structured data, rendered on both desktop and mobile.
8
+
9
+ Implementation:
10
+ - transcript.js: detectMenu(lines, presetId)walks upward from footer/esc anchor,
11
+ collects sequential numbered choices, requires agent-specific marker (❯/›/●) on at least one line.
12
+ Handles footerless Gemini menus via (esc) in choice text.
13
+ - handlers.js: after terminal.buffer capture, runs detectMenu, broadcasts session.menu
14
+ with structured choices. Forces idle when menu detected. Deduplicates via _menuKey on session.
15
+ - sessions.js: list() includes menu state for daemon reconnect.
16
+ - daemon.js: forwards session.menu to mobile, caches per-session menu state,
17
+ sends on subscribe, clears on working status.
18
+ - mobile index.html: renderMenu(choices) replaces 28-line updateChoices parser.
19
+ Menu state comes from session.menu event, no local parsing.
20
+
21
+ ## Pending idle for Claude DONE
22
+
23
+ Claude's api_request no longer flips to idle immediately. Instead:
24
+ - api_request → starts pending idle window (polls every 250ms)
25
+ - Confirm idle: 1s of no PTY output (measured from max(started, lastOutputAt))
26
+ - Cancel: any new telemetry event, or 6s timeout
27
+ - activity.js: exposes lastOutputAt(id) timestamp of last PTY output per session
28
+ - telemetry-receiver.js: startPendingIdle/cancelPendingIdle manage the window
29
+
30
+ Why: api_request fires between tool calls mid-work, not just at end-of-turn.
31
+ Immediate idle caused false status flickers. The 1s output silence confirms
32
+ the agent is truly waiting for user input.
33
+
34
+ ## Future: desktop sidebar menu preview
35
+
36
+ Show "please make a choice" or choice labels in the desktop session list
37
+ when a menu is active. Infrastructure is ready (session.menu broadcast exists).
38
+
39
+ ## Future: OpenCode menu support
40
+
41
+ OpenCode not yet in MENU_MARKERS. Need to determine:
42
+ - Does OpenCode expose menu events via bridge plugin API?
43
+ - If not, what marker character does OpenCode use for interactive menus?