tycono 0.3.20 → 0.3.22-beta.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/package.json
CHANGED
|
@@ -237,10 +237,33 @@ class WaveMultiplexer {
|
|
|
237
237
|
this.registerSession(waveId, execution);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
/** Remove completed wave sessions from memory
|
|
240
|
+
/** Remove completed wave sessions from memory.
|
|
241
|
+
* Keep SSE clients registered — they persist until connection closes.
|
|
242
|
+
* When the wave restarts (new directive), registerSession will resubscribe them. */
|
|
241
243
|
cleanupWave(waveId: string): void {
|
|
242
244
|
this.waveSessions.delete(waveId);
|
|
243
|
-
|
|
245
|
+
|
|
246
|
+
// Don't delete clients — TUI SSE connections stay open across directives.
|
|
247
|
+
// Just unsubscribe dead session listeners to prevent stale callbacks.
|
|
248
|
+
const clients = this.clients.get(waveId);
|
|
249
|
+
if (clients) {
|
|
250
|
+
// Remove disconnected clients
|
|
251
|
+
for (const client of clients) {
|
|
252
|
+
if (client.closed || client.res.destroyed || client.res.writableEnded) {
|
|
253
|
+
clearInterval(client.heartbeat);
|
|
254
|
+
clients.delete(client);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Unsubscribe old session listeners (execution is done)
|
|
258
|
+
for (const [, attached] of client.attachedSessions) {
|
|
259
|
+
attached.unsubscribe();
|
|
260
|
+
}
|
|
261
|
+
client.attachedSessions.clear();
|
|
262
|
+
}
|
|
263
|
+
if (clients.size === 0) {
|
|
264
|
+
this.clients.delete(waveId);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
244
267
|
}
|
|
245
268
|
|
|
246
269
|
detach(waveId: string, client: WaveStreamClient): void {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Shows full output: text, tools, thinking, dispatch — no aggressive truncation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useState, useCallback, useRef } from 'react';
|
|
8
|
+
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
|
9
9
|
import { Box, Text, Static, useInput } from 'ink';
|
|
10
10
|
import TextInput from 'ink-text-input';
|
|
11
11
|
import type { SSEEvent, ActiveSessionInfo } from '../api';
|
|
@@ -306,28 +306,58 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
306
306
|
const [quickBarIndex, setQuickBarIndex] = useState(0);
|
|
307
307
|
const [acIndex, setAcIndex] = useState(0);
|
|
308
308
|
|
|
309
|
-
// Convert events to stream lines
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
309
|
+
// Convert events to stream lines — memoized to prevent <Static> duplication.
|
|
310
|
+
// Without useMemo, every re-render calls summarizeEvent → new lineCounter IDs
|
|
311
|
+
// → <Static> treats them as new items → duplicate output in terminal scrollback.
|
|
312
|
+
const prevEventsLenRef = useRef(0);
|
|
313
|
+
const eventLinesRef = useRef<StreamLine[]>([]);
|
|
314
|
+
|
|
315
|
+
useMemo(() => {
|
|
316
|
+
// Reset if events array was cleared (wave switch)
|
|
317
|
+
if (events.length < prevEventsLenRef.current) {
|
|
318
|
+
eventLinesRef.current = [];
|
|
319
|
+
prevEventsLenRef.current = 0;
|
|
318
320
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
321
|
+
|
|
322
|
+
// Only process newly added events (events array grows monotonically)
|
|
323
|
+
const startIdx = prevEventsLenRef.current;
|
|
324
|
+
if (startIdx >= events.length) return;
|
|
325
|
+
|
|
326
|
+
const seenDone = new Set<string>();
|
|
327
|
+
// Re-scan for msg:done dedup across ALL events (need full context)
|
|
328
|
+
const doneIndices = new Map<string, number>();
|
|
329
|
+
for (let i = 0; i < events.length; i++) {
|
|
330
|
+
if (events[i].type === 'msg:done') {
|
|
331
|
+
doneIndices.set(events[i].roleId, i);
|
|
332
|
+
}
|
|
327
333
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
334
|
+
|
|
335
|
+
for (let i = startIdx; i < events.length; i++) {
|
|
336
|
+
const event = events[i];
|
|
337
|
+
// Skip consecutive thinking from same role
|
|
338
|
+
if (event.type === 'thinking' && i + 1 < events.length) {
|
|
339
|
+
const next = events[i + 1];
|
|
340
|
+
if (next.type === 'thinking' && next.roleId === event.roleId) continue;
|
|
341
|
+
}
|
|
342
|
+
// Dedup msg:done — only show last one per role
|
|
343
|
+
if (event.type === 'msg:done') {
|
|
344
|
+
const key = `${event.roleId}:done`;
|
|
345
|
+
if (seenDone.has(key)) continue;
|
|
346
|
+
if (doneIndices.get(event.roleId) !== i) continue; // Not the last one
|
|
347
|
+
seenDone.add(key);
|
|
348
|
+
}
|
|
349
|
+
const line = summarizeEvent(event, allRoleIds);
|
|
350
|
+
if (line) eventLinesRef.current.push(line);
|
|
351
|
+
}
|
|
352
|
+
prevEventsLenRef.current = events.length;
|
|
353
|
+
|
|
354
|
+
// Cap to prevent unbounded growth
|
|
355
|
+
if (eventLinesRef.current.length > 60) {
|
|
356
|
+
eventLinesRef.current = eventLinesRef.current.slice(-60);
|
|
357
|
+
}
|
|
358
|
+
}, [events, allRoleIds]);
|
|
359
|
+
|
|
360
|
+
const eventLines = eventLinesRef.current;
|
|
331
361
|
|
|
332
362
|
// Merge all lines sorted by ID (chronological) — prevents user input from being sliced off
|
|
333
363
|
const allLines = [...userInputs, ...systemMessages, ...eventLines]
|