clideck 1.26.0 → 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 +74 -6
- package/telemetry-receiver.js +75 -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) {
|
|
@@ -111,10 +127,11 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
111
127
|
opencodeBridge.clear(id);
|
|
112
128
|
plugins.clearStatus(id);
|
|
113
129
|
// If resumable and token captured, move to resumable list (keep transcript for search)
|
|
114
|
-
if (cmd.canResume && cmd.resumeCommand && s.sessionToken) {
|
|
130
|
+
if (!s.ephemeral && cmd.canResume && cmd.resumeCommand && s.sessionToken) {
|
|
115
131
|
resumable.push({
|
|
116
132
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
117
133
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
134
|
+
roleName: s.roleName || null,
|
|
118
135
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
119
136
|
savedAt: new Date().toISOString(),
|
|
120
137
|
});
|
|
@@ -124,7 +141,7 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
124
141
|
}
|
|
125
142
|
sessions.delete(id);
|
|
126
143
|
broadcast({ type: 'closed', id });
|
|
127
|
-
if (cmd.canResume && s.sessionToken) {
|
|
144
|
+
if (!s.ephemeral && cmd.canResume && s.sessionToken) {
|
|
128
145
|
broadcast({ type: 'sessions.resumable', list: getResumable() });
|
|
129
146
|
}
|
|
130
147
|
});
|
|
@@ -152,6 +169,18 @@ function create(msg, ws, cfg) {
|
|
|
152
169
|
return;
|
|
153
170
|
}
|
|
154
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
|
+
|
|
155
184
|
const createdPresetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
|
|
156
185
|
const installId = msg.installId || undefined;
|
|
157
186
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId: createdPresetId, projectId, installId });
|
|
@@ -164,6 +193,33 @@ function create(msg, ws, cfg) {
|
|
|
164
193
|
}
|
|
165
194
|
}
|
|
166
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
|
+
|
|
167
223
|
// --- Resume a persisted session ---
|
|
168
224
|
|
|
169
225
|
function resume(msg, ws, cfg) {
|
|
@@ -200,7 +256,11 @@ function resume(msg, ws, cfg) {
|
|
|
200
256
|
return;
|
|
201
257
|
}
|
|
202
258
|
|
|
203
|
-
|
|
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
|
+
}
|
|
204
264
|
|
|
205
265
|
// Remove from resumable list and notify all clients
|
|
206
266
|
resumable = resumable.filter(s => s.id !== id);
|
|
@@ -217,6 +277,11 @@ function input(msg) {
|
|
|
217
277
|
activity.trackIn(msg.id, data.length);
|
|
218
278
|
transcript.trackInput(msg.id, data);
|
|
219
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
|
+
}
|
|
220
285
|
}
|
|
221
286
|
function resize(msg) { sessions.get(msg.id)?.pty.resize(msg.cols, msg.rows); }
|
|
222
287
|
|
|
@@ -295,6 +360,7 @@ function restart(msg, ws, cfg) {
|
|
|
295
360
|
function list() {
|
|
296
361
|
return [...sessions].map(([id, s]) => ({
|
|
297
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,
|
|
298
364
|
// Last preview text for sidebar display on reconnect
|
|
299
365
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
300
366
|
menu: s._menuKey ? JSON.parse(s._menuKey) : undefined,
|
|
@@ -340,6 +406,7 @@ function saveSessions(cfg) {
|
|
|
340
406
|
let skippedNoToken = 0;
|
|
341
407
|
const live = [...sessions]
|
|
342
408
|
.filter(([, s]) => {
|
|
409
|
+
if (s.ephemeral) return false;
|
|
343
410
|
const cmd = cfg.commands.find(c => c.id === s.commandId);
|
|
344
411
|
if (!cmd?.canResume || !cmd.resumeCommand) return false;
|
|
345
412
|
// If resume needs a session ID, we must have captured one
|
|
@@ -352,6 +419,7 @@ function saveSessions(cfg) {
|
|
|
352
419
|
.map(([id, s]) => ({
|
|
353
420
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
354
421
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
422
|
+
roleName: s.roleName || null,
|
|
355
423
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
356
424
|
savedAt: new Date().toISOString(),
|
|
357
425
|
}));
|
|
@@ -402,7 +470,7 @@ function shutdown(cfg) {
|
|
|
402
470
|
|
|
403
471
|
module.exports = {
|
|
404
472
|
clients, broadcast, addBroadcastListener, getSessions: () => sessions,
|
|
405
|
-
create, resume, restart, input, resize, rename, setTheme, setMute, setProject, setPreview, close,
|
|
473
|
+
create, createProgrammatic, resume, restart, input, resize, rename, setTheme, setMute, setProject, setPreview, close,
|
|
406
474
|
list, getResumable, sendBuffers,
|
|
407
475
|
loadSessions, startAutoSave, shutdown,
|
|
408
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,50 +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)) {
|
|
82
83
|
cancelPendingIdle(resolvedId);
|
|
83
84
|
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
84
|
-
|
|
85
|
-
// Claude: telemetry-only status. api_request → pending idle (confirm after 1s output silence, expire after 6s).
|
|
86
|
-
if (serviceName === 'claude-code' && eventName) {
|
|
87
|
-
if (eventName === 'api_request') {
|
|
88
|
-
startPendingIdle(resolvedId);
|
|
89
|
-
} else if (eventName !== 'user_prompt') {
|
|
90
|
-
cancelPendingIdle(resolvedId);
|
|
91
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Codex: codex.sse_event → pending idle (2s PTY silence), other events → working.
|
|
95
|
-
if (serviceName === 'codex_cli_rs' && eventName) {
|
|
96
|
-
if (eventName === 'codex.sse_event') {
|
|
97
|
-
startPendingIdle(resolvedId);
|
|
98
|
-
} else if (eventName !== 'codex.user_prompt') {
|
|
99
|
-
cancelPendingIdle(resolvedId);
|
|
100
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Gemini: api_response (role=main) → pending idle (2s PTY silence), whitelisted events → working.
|
|
104
|
-
if (serviceName === 'gemini-cli' && eventName) {
|
|
105
|
-
if (eventName === 'gemini_cli.api_response' && attrs['role'] === 'main') {
|
|
106
|
-
startPendingIdle(resolvedId);
|
|
107
|
-
} else if (eventName === 'gemini_cli.api_request' || eventName === 'gemini_cli.model_routing'
|
|
108
|
-
|| (eventName === 'gemini_cli.api_response' && attrs['role'] !== 'main')) {
|
|
109
|
-
cancelPendingIdle(resolvedId);
|
|
110
|
-
broadcastFn?.({ type: 'session.status', id: resolvedId, working: true, source: 'telemetry' });
|
|
111
|
-
}
|
|
85
|
+
startPendingIdle(resolvedId, serviceName);
|
|
112
86
|
}
|
|
113
87
|
|
|
114
88
|
const agentSessionId = attrs['session.id'] || attrs['conversation.id'];
|
|
@@ -118,7 +92,6 @@ function handleLogs(req, res) {
|
|
|
118
92
|
if (!sess.sessionToken || dominated) {
|
|
119
93
|
sess.sessionToken = agentSessionId;
|
|
120
94
|
console.log(`Telemetry: captured session ID ${agentSessionId} for ${agent} (${resolvedId.slice(0, 8)})`);
|
|
121
|
-
captured = true;
|
|
122
95
|
}
|
|
123
96
|
}
|
|
124
97
|
}
|
|
@@ -150,14 +123,44 @@ function cancelPendingSetup(sessionId) {
|
|
|
150
123
|
}
|
|
151
124
|
}
|
|
152
125
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
139
|
const check = setInterval(() => {
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
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;
|
|
161
164
|
}
|
|
162
165
|
}, 250);
|
|
163
166
|
pendingIdle.set(id, check);
|
|
@@ -168,9 +171,40 @@ function cancelPendingIdle(id) {
|
|
|
168
171
|
if (timer) { clearInterval(timer); pendingIdle.delete(id); }
|
|
169
172
|
}
|
|
170
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
|
+
|
|
171
203
|
function clear(id) {
|
|
172
204
|
activity.delete(id);
|
|
173
205
|
cancelPendingIdle(id);
|
|
206
|
+
cancelEscIdle(id);
|
|
207
|
+
escSuppressUntil.delete(id);
|
|
174
208
|
const pending = pendingSetup.get(id);
|
|
175
209
|
if (pending) { clearTimeout(pending.timer); pendingSetup.delete(id); }
|
|
176
210
|
}
|
|
@@ -180,4 +214,4 @@ function hasEvents(id) {
|
|
|
180
214
|
return activity.has(id);
|
|
181
215
|
}
|
|
182
216
|
|
|
183
|
-
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
|
|