clideck 1.25.8 → 1.26.2
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/README.md +17 -4
- package/activity.js +15 -1
- package/config.js +74 -1
- package/handlers.js +14 -3
- package/package.json +2 -1
- package/plugin-loader.js +128 -11
- package/plugins/autopilot/clideck-plugin.json +52 -0
- package/plugins/autopilot/client.js +84 -0
- package/plugins/autopilot/index.js +797 -0
- package/plugins/autopilot/prompt.md +68 -0
- package/public/index.html +11 -3
- package/public/js/app.js +73 -6
- package/public/js/creator.js +72 -6
- package/public/js/nav.js +2 -2
- package/public/js/prompts.js +1 -1
- package/public/js/roles.js +112 -0
- package/public/js/state.js +2 -0
- package/public/js/terminals.js +219 -2
- package/public/js/toast.js +28 -9
- package/public/tailwind.css +1 -1
- package/server.js +7 -4
- package/sessions.js +89 -6
- package/telemetry-receiver.js +76 -41
- package/transcript.js +15 -3
package/sessions.js
CHANGED
|
@@ -29,7 +29,11 @@ function addBroadcastListener(fn) { broadcastListeners.push(fn); }
|
|
|
29
29
|
function broadcast(msg) {
|
|
30
30
|
const raw = JSON.stringify(msg);
|
|
31
31
|
for (const c of clients) if (c.readyState === 1) c.send(raw);
|
|
32
|
-
if (msg.type === 'session.status')
|
|
32
|
+
if (msg.type === 'session.status') {
|
|
33
|
+
const s = sessions.get(msg.id);
|
|
34
|
+
if (s) s.working = !!msg.working;
|
|
35
|
+
plugins.notifyStatus(msg.id, msg.working, msg.source);
|
|
36
|
+
}
|
|
33
37
|
for (const fn of broadcastListeners) try { fn(msg); } catch {}
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -74,7 +78,7 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
74
78
|
const sessionIdRe = cmd.sessionIdPattern ? new RegExp(cmd.sessionIdPattern, 'i') : null;
|
|
75
79
|
const bin = binName(cmd.command);
|
|
76
80
|
const preset = PRESETS.find(p => binName(p.command) === bin);
|
|
77
|
-
const session = { name, themeId, commandId, cwd, pty: term, chunks: [], chunksSize: 0, sessionToken: savedToken || null, projectId: projectId || null, presetId: preset?.presetId || 'shell' };
|
|
81
|
+
const session = { name, themeId, commandId, cwd, pty: term, chunks: [], chunksSize: 0, sessionToken: savedToken || null, projectId: projectId || null, presetId: preset?.presetId || 'shell', working: undefined };
|
|
78
82
|
sessions.set(id, session);
|
|
79
83
|
|
|
80
84
|
// Watch for telemetry — if config isn't set up, frontend will prompt
|
|
@@ -82,6 +86,18 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
82
86
|
if (preset?.bridge === 'opencode') opencodeBridge.watchSession(id, cwd);
|
|
83
87
|
|
|
84
88
|
term.onData((data) => {
|
|
89
|
+
// Inject role prompt once after agent starts producing output
|
|
90
|
+
if (session.pendingRolePrompt && !session._rolePromptTimer) {
|
|
91
|
+
session._rolePromptTimer = setTimeout(() => {
|
|
92
|
+
if (session.pendingRolePrompt) {
|
|
93
|
+
term.write(session.pendingRolePrompt);
|
|
94
|
+
setTimeout(() => term.write('\r'), 150);
|
|
95
|
+
console.log(`Session ${id.slice(0, 8)}: injected role prompt`);
|
|
96
|
+
delete session.pendingRolePrompt;
|
|
97
|
+
delete session._rolePromptTimer;
|
|
98
|
+
}
|
|
99
|
+
}, 3000);
|
|
100
|
+
}
|
|
85
101
|
session.chunks.push(data);
|
|
86
102
|
session.chunksSize += data.length;
|
|
87
103
|
while (session.chunksSize > MAX_BUFFER && session.chunks.length > 1) {
|
|
@@ -104,14 +120,30 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
104
120
|
|
|
105
121
|
term.onExit(() => {
|
|
106
122
|
// Skip cleanup if this PTY was replaced by a restart
|
|
107
|
-
|
|
123
|
+
const s = sessions.get(id);
|
|
124
|
+
if (s?.pty !== term) return;
|
|
108
125
|
activity.clear(id);
|
|
109
126
|
telemetry.clear(id);
|
|
110
127
|
opencodeBridge.clear(id);
|
|
111
|
-
transcript.clear(id);
|
|
112
128
|
plugins.clearStatus(id);
|
|
129
|
+
// If resumable and token captured, move to resumable list (keep transcript for search)
|
|
130
|
+
if (!s.ephemeral && cmd.canResume && cmd.resumeCommand && s.sessionToken) {
|
|
131
|
+
resumable.push({
|
|
132
|
+
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
133
|
+
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
134
|
+
roleName: s.roleName || null,
|
|
135
|
+
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
136
|
+
savedAt: new Date().toISOString(),
|
|
137
|
+
});
|
|
138
|
+
console.log(`Session ${id.slice(0, 8)}: moved to resumable on exit (token: ${s.sessionToken.slice(0, 12)}…)`);
|
|
139
|
+
} else {
|
|
140
|
+
transcript.clear(id);
|
|
141
|
+
}
|
|
113
142
|
sessions.delete(id);
|
|
114
143
|
broadcast({ type: 'closed', id });
|
|
144
|
+
if (!s.ephemeral && cmd.canResume && s.sessionToken) {
|
|
145
|
+
broadcast({ type: 'sessions.resumable', list: getResumable() });
|
|
146
|
+
}
|
|
115
147
|
});
|
|
116
148
|
|
|
117
149
|
return null;
|
|
@@ -137,6 +169,18 @@ function create(msg, ws, cfg) {
|
|
|
137
169
|
return;
|
|
138
170
|
}
|
|
139
171
|
|
|
172
|
+
// If a role was selected, store identity on session and queue prompt injection
|
|
173
|
+
if (msg.roleId) {
|
|
174
|
+
const role = (cfg.roles || []).find(r => r.id === msg.roleId);
|
|
175
|
+
if (role) {
|
|
176
|
+
const s = sessions.get(id);
|
|
177
|
+
if (s) {
|
|
178
|
+
s.roleName = role.name;
|
|
179
|
+
if (role.instructions) s.pendingRolePrompt = role.instructions;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
const createdPresetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
|
|
141
185
|
const installId = msg.installId || undefined;
|
|
142
186
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId: createdPresetId, projectId, installId });
|
|
@@ -149,6 +193,33 @@ function create(msg, ws, cfg) {
|
|
|
149
193
|
}
|
|
150
194
|
}
|
|
151
195
|
|
|
196
|
+
// --- Programmatic session creation (for plugins / internal use) ---
|
|
197
|
+
|
|
198
|
+
function createProgrammatic(opts, cfg) {
|
|
199
|
+
const id = crypto.randomUUID();
|
|
200
|
+
let cmd;
|
|
201
|
+
if (opts.presetId) cmd = cfg.commands.find(c => c.presetId === opts.presetId);
|
|
202
|
+
else if (opts.commandId) cmd = cfg.commands.find(c => c.id === opts.commandId);
|
|
203
|
+
if (!cmd) return { error: 'Command not found' };
|
|
204
|
+
|
|
205
|
+
const parts = parseCommand(cmd.command);
|
|
206
|
+
const cwd = resolveValidDir(opts.cwd || cmd.defaultPath || cfg.defaultPath);
|
|
207
|
+
const themeId = opts.themeId || cfg.defaultTheme || 'default';
|
|
208
|
+
const name = opts.name || cmd.label;
|
|
209
|
+
const projectId = opts.projectId || null;
|
|
210
|
+
|
|
211
|
+
const err = spawnSession(id, cmd, parts, cwd, name, themeId, cmd.id, null, projectId);
|
|
212
|
+
if (err) return { error: err.message };
|
|
213
|
+
|
|
214
|
+
const s = sessions.get(id);
|
|
215
|
+
if (s && opts.roleName) s.roleName = opts.roleName;
|
|
216
|
+
if (s && opts.ephemeral) s.ephemeral = true;
|
|
217
|
+
|
|
218
|
+
const presetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
|
|
219
|
+
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId, projectId });
|
|
220
|
+
return { id };
|
|
221
|
+
}
|
|
222
|
+
|
|
152
223
|
// --- Resume a persisted session ---
|
|
153
224
|
|
|
154
225
|
function resume(msg, ws, cfg) {
|
|
@@ -185,7 +256,11 @@ function resume(msg, ws, cfg) {
|
|
|
185
256
|
return;
|
|
186
257
|
}
|
|
187
258
|
|
|
188
|
-
|
|
259
|
+
const s = sessions.get(id);
|
|
260
|
+
if (s) {
|
|
261
|
+
if (saved.muted) s.muted = true;
|
|
262
|
+
if (saved.roleName) s.roleName = saved.roleName;
|
|
263
|
+
}
|
|
189
264
|
|
|
190
265
|
// Remove from resumable list and notify all clients
|
|
191
266
|
resumable = resumable.filter(s => s.id !== id);
|
|
@@ -202,6 +277,11 @@ function input(msg) {
|
|
|
202
277
|
activity.trackIn(msg.id, data.length);
|
|
203
278
|
transcript.trackInput(msg.id, data);
|
|
204
279
|
sessions.get(msg.id)?.pty.write(data);
|
|
280
|
+
if (data === '\x1b') {
|
|
281
|
+
const s = sessions.get(msg.id);
|
|
282
|
+
console.log(`[esc] session=${msg.id.slice(0,8)} working=${s?.working}`);
|
|
283
|
+
if (s?.working) telemetry.startEscIdle(msg.id);
|
|
284
|
+
}
|
|
205
285
|
}
|
|
206
286
|
function resize(msg) { sessions.get(msg.id)?.pty.resize(msg.cols, msg.rows); }
|
|
207
287
|
|
|
@@ -280,6 +360,7 @@ function restart(msg, ws, cfg) {
|
|
|
280
360
|
function list() {
|
|
281
361
|
return [...sessions].map(([id, s]) => ({
|
|
282
362
|
id, name: s.name, themeId: s.themeId, commandId: s.commandId, presetId: s.presetId || 'shell', projectId: s.projectId, muted: !!s.muted,
|
|
363
|
+
roleName: s.roleName || null,
|
|
283
364
|
// Last preview text for sidebar display on reconnect
|
|
284
365
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
285
366
|
menu: s._menuKey ? JSON.parse(s._menuKey) : undefined,
|
|
@@ -325,6 +406,7 @@ function saveSessions(cfg) {
|
|
|
325
406
|
let skippedNoToken = 0;
|
|
326
407
|
const live = [...sessions]
|
|
327
408
|
.filter(([, s]) => {
|
|
409
|
+
if (s.ephemeral) return false;
|
|
328
410
|
const cmd = cfg.commands.find(c => c.id === s.commandId);
|
|
329
411
|
if (!cmd?.canResume || !cmd.resumeCommand) return false;
|
|
330
412
|
// If resume needs a session ID, we must have captured one
|
|
@@ -337,6 +419,7 @@ function saveSessions(cfg) {
|
|
|
337
419
|
.map(([id, s]) => ({
|
|
338
420
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
339
421
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
422
|
+
roleName: s.roleName || null,
|
|
340
423
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
341
424
|
savedAt: new Date().toISOString(),
|
|
342
425
|
}));
|
|
@@ -387,7 +470,7 @@ function shutdown(cfg) {
|
|
|
387
470
|
|
|
388
471
|
module.exports = {
|
|
389
472
|
clients, broadcast, addBroadcastListener, getSessions: () => sessions,
|
|
390
|
-
create, resume, restart, input, resize, rename, setTheme, setMute, setProject, setPreview, close,
|
|
473
|
+
create, createProgrammatic, resume, restart, input, resize, rename, setTheme, setMute, setProject, setPreview, close,
|
|
391
474
|
list, getResumable, sendBuffers,
|
|
392
475
|
loadSessions, startAutoSave, shutdown,
|
|
393
476
|
};
|
package/telemetry-receiver.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
const ioActivity = require('./activity');
|
|
6
6
|
const activity = new Map(); // sessionId → has received events
|
|
7
7
|
const pendingSetup = new Map(); // sessionId → timer (waiting for first event)
|
|
8
|
-
const pendingIdle = new Map(); // sessionId → timer (
|
|
8
|
+
const pendingIdle = new Map(); // sessionId → timer (PTY silence → idle)
|
|
9
|
+
const escPendingIdle = new Map(); // sessionId → timer (Esc interrupt → confirm idle after output silence)
|
|
10
|
+
const escSuppressUntil = new Map(); // sessionId → ts (briefly ignore telemetry reassertions after Esc)
|
|
9
11
|
let broadcastFn = null;
|
|
10
12
|
let sessionsFn = null;
|
|
11
13
|
|
|
@@ -65,47 +67,22 @@ function handleLogs(req, res) {
|
|
|
65
67
|
if (firstEvent) console.log(`Telemetry: first event from ${agent} (${resolvedId.slice(0, 8)})`);
|
|
66
68
|
|
|
67
69
|
// Process each log record — capture session ID for resume
|
|
68
|
-
let captured = false;
|
|
69
70
|
for (const sl of rl.scopeLogs || []) {
|
|
70
71
|
for (const lr of sl.logRecords || []) {
|
|
71
72
|
const attrs = parseAttrs(lr.attributes);
|
|
72
|
-
|
|
73
73
|
const eventName = attrs['event.name'];
|
|
74
|
+
|
|
74
75
|
// Debug telemetry logs — uncomment as needed, do not delete
|
|
75
76
|
// if (serviceName === 'claude-code' && eventName) console.log(`[telemetry:claude] ${eventName}`);
|
|
76
|
-
// if (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName}`);
|
|
77
|
+
// if (serviceName === 'codex_cli_rs' && eventName) console.log(`[telemetry:codex] ${eventName} session=${resolvedId.slice(0,8)} working=${sessionsFn?.()?.get(resolvedId)?.working}`);
|
|
77
78
|
// if (serviceName === 'gemini-cli' && eventName) console.log(`[telemetry:gemini] ${eventName}`);
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
+
// Status: user_prompt → working + start PTY silence monitor for idle
|
|
80
81
|
const startEvents = new Set(['user_prompt', 'gemini_cli.user_prompt', 'codex.user_prompt']);
|
|
81
82
|
if (startEvents.has(eventName)) {
|
|
83
|
+
cancelPendingIdle(resolvedId);
|
|
82
84
|
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
83
|
-
|
|
84
|
-
// Claude: telemetry-only status. api_request → pending idle (confirm after 1s output silence, expire after 6s).
|
|
85
|
-
if (serviceName === 'claude-code' && eventName) {
|
|
86
|
-
if (eventName === 'api_request') {
|
|
87
|
-
startPendingIdle(resolvedId);
|
|
88
|
-
} else if (eventName !== 'user_prompt') {
|
|
89
|
-
cancelPendingIdle(resolvedId);
|
|
90
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Codex: telemetry-only status. codex.user_prompt/any event → working, codex.sse_event → idle.
|
|
94
|
-
if (serviceName === 'codex_cli_rs' && eventName) {
|
|
95
|
-
if (eventName === 'codex.sse_event') {
|
|
96
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
|
|
97
|
-
} else if (eventName !== 'codex.user_prompt') {
|
|
98
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Gemini: telemetry-only status. Whitelisted events → working, api_response (role=main) → idle.
|
|
102
|
-
if (serviceName === 'gemini-cli' && eventName) {
|
|
103
|
-
if (eventName === 'gemini_cli.api_response' && attrs['role'] === 'main') {
|
|
104
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: false, source: 'telemetry' });
|
|
105
|
-
} else if (eventName === 'gemini_cli.api_request' || eventName === 'gemini_cli.model_routing'
|
|
106
|
-
|| (eventName === 'gemini_cli.api_response' && attrs['role'] !== 'main')) {
|
|
107
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
108
|
-
}
|
|
85
|
+
startPendingIdle(resolvedId, serviceName);
|
|
109
86
|
}
|
|
110
87
|
|
|
111
88
|
const agentSessionId = attrs['session.id'] || attrs['conversation.id'];
|
|
@@ -115,7 +92,6 @@ function handleLogs(req, res) {
|
|
|
115
92
|
if (!sess.sessionToken || dominated) {
|
|
116
93
|
sess.sessionToken = agentSessionId;
|
|
117
94
|
console.log(`Telemetry: captured session ID ${agentSessionId} for ${agent} (${resolvedId.slice(0, 8)})`);
|
|
118
|
-
captured = true;
|
|
119
95
|
}
|
|
120
96
|
}
|
|
121
97
|
}
|
|
@@ -147,16 +123,44 @@ function cancelPendingSetup(sessionId) {
|
|
|
147
123
|
}
|
|
148
124
|
}
|
|
149
125
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
126
|
+
// PTY activity monitor: 2s silent → idle, 2s active or user_prompt → working.
|
|
127
|
+
// Agent working indicators in PTY output.
|
|
128
|
+
const CLAUDE_WORKING_RE = /[✳✽✢✻·]|Working…|thinking/;
|
|
129
|
+
const CODEX_WORKING_RE = /Working|•/;
|
|
130
|
+
const GEMINI_WORKING_RE = /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/;
|
|
131
|
+
|
|
132
|
+
function startPendingIdle(id, agent) {
|
|
133
|
+
if (pendingIdle.has(id)) return; // already monitoring
|
|
134
|
+
const isClaude = agent === 'claude-code';
|
|
135
|
+
const isCodex = agent === 'codex_cli_rs';
|
|
136
|
+
const isGemini = agent === 'gemini-cli';
|
|
137
|
+
let isIdle = false;
|
|
138
|
+
let activeStart = 0;
|
|
154
139
|
const check = setInterval(() => {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
if
|
|
158
|
-
|
|
140
|
+
const lastOut = ioActivity.lastOutputAt(id);
|
|
141
|
+
const lastIn = ioActivity.lastInputAt(id);
|
|
142
|
+
// Ignore echo: if last output is within 100ms of last input, treat as silent
|
|
143
|
+
const agentOut = (lastIn && lastOut - lastIn >= 0 && lastOut - lastIn < 100) ? 0 : lastOut;
|
|
144
|
+
let silent = (Date.now() - agentOut) >= 2000;
|
|
145
|
+
// Agent override: if recent output has spinner/working chars, not silent
|
|
146
|
+
if (silent && (Date.now() - lastOut) < 2000) {
|
|
147
|
+
const chunk = ioActivity.lastChunk(id);
|
|
148
|
+
if (isClaude && CLAUDE_WORKING_RE.test(chunk)) silent = false;
|
|
149
|
+
if (isCodex && CODEX_WORKING_RE.test(chunk)) silent = false;
|
|
150
|
+
if (isGemini && GEMINI_WORKING_RE.test(chunk)) silent = false;
|
|
151
|
+
}
|
|
152
|
+
if (silent && !isIdle) {
|
|
153
|
+
isIdle = true;
|
|
154
|
+
activeStart = 0;
|
|
159
155
|
broadcastFn?.({ type: 'session.status', id, working: false, source: 'telemetry' });
|
|
156
|
+
} else if (!silent && isIdle) {
|
|
157
|
+
if (!activeStart) activeStart = Date.now();
|
|
158
|
+
if (Date.now() - activeStart >= 2000) {
|
|
159
|
+
isIdle = false;
|
|
160
|
+
broadcastFn?.({ type: 'session.status', id, working: true, source: 'telemetry' });
|
|
161
|
+
}
|
|
162
|
+
} else if (!silent) {
|
|
163
|
+
activeStart = 0;
|
|
160
164
|
}
|
|
161
165
|
}, 250);
|
|
162
166
|
pendingIdle.set(id, check);
|
|
@@ -167,9 +171,40 @@ function cancelPendingIdle(id) {
|
|
|
167
171
|
if (timer) { clearInterval(timer); pendingIdle.delete(id); }
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
function startEscIdle(id) {
|
|
175
|
+
cancelEscIdle(id);
|
|
176
|
+
const started = Date.now();
|
|
177
|
+
const ignoreUntil = started + 500;
|
|
178
|
+
console.log(`[escIdle] start session=${id.slice(0,8)}`);
|
|
179
|
+
const check = setInterval(() => {
|
|
180
|
+
const lastOut = ioActivity.lastOutputAt(id);
|
|
181
|
+
const silence = Date.now() - Math.max(ignoreUntil, lastOut);
|
|
182
|
+
const elapsed = Date.now() - started;
|
|
183
|
+
if (elapsed > 10000) {
|
|
184
|
+
console.log(`[escIdle] timeout session=${id.slice(0,8)} silence=${silence}ms`);
|
|
185
|
+
cancelEscIdle(id);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (silence >= 2000) {
|
|
189
|
+
console.log(`[escIdle] idle session=${id.slice(0,8)} silence=${silence}ms`);
|
|
190
|
+
escSuppressUntil.set(id, Date.now() + 2000);
|
|
191
|
+
cancelEscIdle(id);
|
|
192
|
+
broadcastFn?.({ type: 'session.status', id, working: false, source: 'esc' });
|
|
193
|
+
}
|
|
194
|
+
}, 250);
|
|
195
|
+
escPendingIdle.set(id, check);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cancelEscIdle(id) {
|
|
199
|
+
const timer = escPendingIdle.get(id);
|
|
200
|
+
if (timer) { clearInterval(timer); escPendingIdle.delete(id); }
|
|
201
|
+
}
|
|
202
|
+
|
|
170
203
|
function clear(id) {
|
|
171
204
|
activity.delete(id);
|
|
172
205
|
cancelPendingIdle(id);
|
|
206
|
+
cancelEscIdle(id);
|
|
207
|
+
escSuppressUntil.delete(id);
|
|
173
208
|
const pending = pendingSetup.get(id);
|
|
174
209
|
if (pending) { clearTimeout(pending.timer); pendingSetup.delete(id); }
|
|
175
210
|
}
|
|
@@ -179,4 +214,4 @@ function hasEvents(id) {
|
|
|
179
214
|
return activity.has(id);
|
|
180
215
|
}
|
|
181
216
|
|
|
182
|
-
module.exports = { init, handleLogs, clear, hasEvents, watchSession };
|
|
217
|
+
module.exports = { init, handleLogs, clear, hasEvents, watchSession, startPendingIdle, startEscIdle };
|
package/transcript.js
CHANGED
|
@@ -254,15 +254,27 @@ function anchorParse(id, lines) {
|
|
|
254
254
|
return turns;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// For consecutive agent turns, keep only the last one (strips tool output, keeps conversational response).
|
|
258
|
+
function collapseAgentTurns(turns) {
|
|
259
|
+
if (!turns?.length) return turns;
|
|
260
|
+
const result = [];
|
|
261
|
+
for (let i = 0; i < turns.length; i++) {
|
|
262
|
+
if (turns[i].role === 'agent' && i + 1 < turns.length && turns[i + 1].role === 'agent') continue;
|
|
263
|
+
result.push(turns[i]);
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
257
268
|
// Parse .screen into structured turns — use agent-specific parser if available, else anchor fallback.
|
|
258
|
-
|
|
269
|
+
// opts.raw: if true, preserve trailing user turn (needed by autopilot for freshness checks).
|
|
270
|
+
function getScreenTurns(id, agent, opts) {
|
|
259
271
|
const screen = getScreen(id);
|
|
260
272
|
if (!screen) return null;
|
|
261
273
|
const lines = screen.split('\n');
|
|
262
274
|
const parser = agentParsers[agent];
|
|
263
|
-
const turns = parser ? parser(lines, id) : anchorParse(id, lines);
|
|
275
|
+
const turns = collapseAgentTurns(parser ? parser(lines, id) : anchorParse(id, lines));
|
|
264
276
|
// Drop trailing user turn — it's the empty prompt or unanswered input
|
|
265
|
-
if (turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
|
|
277
|
+
if (!opts?.raw && turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
|
|
266
278
|
return turns?.length >= 2 ? turns : null;
|
|
267
279
|
}
|
|
268
280
|
|