tycono 0.3.24-beta.1 → 0.3.25-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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.24-beta.1",
3
+ "version": "0.3.25-beta.0",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/app.tsx CHANGED
@@ -14,7 +14,8 @@
14
14
  import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
15
15
  import { Box, Text, useApp, useInput } from 'ink';
16
16
  import { StatusBar } from './components/StatusBar';
17
- import { CommandMode, type StreamLine } from './components/CommandMode';
17
+ import { CommandMode, summarizeEvent, type StreamLine } from './components/CommandMode';
18
+ import type { SSEEvent } from './api';
18
19
  import { PanelMode } from './components/PanelMode';
19
20
  import { SetupWizard } from './components/SetupWizard';
20
21
  import { useApi } from './hooks/useApi';
@@ -234,6 +235,54 @@ export const App: React.FC = () => {
234
235
  // User input lines — lifted from CommandMode to survive Panel mode switches
235
236
  const [userInputs, setUserInputs] = useState<StreamLine[]>([]);
236
237
 
238
+ // Event line processing — lifted from CommandMode to survive Panel unmount/remount.
239
+ // Without this, every Tab→Escape cycle resets refs → reprocesses all events → <Static> duplicates.
240
+ const prevEventsLenRef = useRef(0);
241
+ const eventLinesRef = useRef<StreamLine[]>([]);
242
+
243
+ useMemo(() => {
244
+ const events = sse.events;
245
+ // Reset if events array was cleared (wave switch)
246
+ if (events.length < prevEventsLenRef.current) {
247
+ eventLinesRef.current = [];
248
+ prevEventsLenRef.current = 0;
249
+ }
250
+
251
+ const startIdx = prevEventsLenRef.current;
252
+ if (startIdx >= events.length) return;
253
+
254
+ const seenDone = new Set<string>();
255
+ const doneIndices = new Map<string, number>();
256
+ for (let i = 0; i < events.length; i++) {
257
+ if (events[i].type === 'msg:done') {
258
+ doneIndices.set(events[i].roleId, i);
259
+ }
260
+ }
261
+
262
+ for (let i = startIdx; i < events.length; i++) {
263
+ const event = events[i];
264
+ if (event.type === 'thinking' && i + 1 < events.length) {
265
+ const next = events[i + 1];
266
+ if (next.type === 'thinking' && next.roleId === event.roleId) continue;
267
+ }
268
+ if (event.type === 'msg:done') {
269
+ const key = `${event.roleId}:done`;
270
+ if (seenDone.has(key)) continue;
271
+ if (doneIndices.get(event.roleId) !== i) continue;
272
+ seenDone.add(key);
273
+ }
274
+ const line = summarizeEvent(event, flatRoleIds);
275
+ if (line) eventLinesRef.current.push(line);
276
+ }
277
+ prevEventsLenRef.current = events.length;
278
+
279
+ if (eventLinesRef.current.length > 80) {
280
+ eventLinesRef.current = eventLinesRef.current.slice(-80);
281
+ }
282
+ }, [sse.events, flatRoleIds]);
283
+
284
+ const eventLines = eventLinesRef.current;
285
+
237
286
  // Preset selection state (for /new without args)
238
287
  const [pendingPresetSelect, setPendingPresetSelect] = useState<PresetSummary[] | null>(null);
239
288
  const selectedPresetRef = useRef<string | null>(null);
@@ -730,8 +779,7 @@ export const App: React.FC = () => {
730
779
  return (
731
780
  <Box flexDirection="column">
732
781
  <CommandMode
733
- events={sse.events}
734
- allRoleIds={flatRoleIds}
782
+ eventLines={eventLines}
735
783
  systemMessages={systemMessages}
736
784
  userInputs={userInputs}
737
785
  onUserInput={(line) => setUserInputs(prev => [...prev.slice(-10), line])}
@@ -25,8 +25,7 @@ export interface StreamLine {
25
25
  }
26
26
 
27
27
  interface CommandModeProps {
28
- events: SSEEvent[];
29
- allRoleIds: string[];
28
+ eventLines: StreamLine[];
30
29
  systemMessages: StreamLine[];
31
30
  userInputs: StreamLine[];
32
31
  onUserInput: (line: StreamLine) => void;
@@ -293,8 +292,7 @@ const COMMANDS: Array<{ cmd: string; desc: string }> = [
293
292
  ];
294
293
 
295
294
  export const CommandMode: React.FC<CommandModeProps> = ({
296
- events,
297
- allRoleIds,
295
+ eventLines,
298
296
  systemMessages,
299
297
  userInputs,
300
298
  onUserInput,
@@ -309,59 +307,6 @@ export const CommandMode: React.FC<CommandModeProps> = ({
309
307
  const [quickBarIndex, setQuickBarIndex] = useState(0);
310
308
  const [acIndex, setAcIndex] = useState(0);
311
309
 
312
- // Convert events to stream lines — memoized to prevent <Static> duplication.
313
- // Without useMemo, every re-render calls summarizeEvent → new lineCounter IDs
314
- // → <Static> treats them as new items → duplicate output in terminal scrollback.
315
- const prevEventsLenRef = useRef(0);
316
- const eventLinesRef = useRef<StreamLine[]>([]);
317
-
318
- useMemo(() => {
319
- // Reset if events array was cleared (wave switch)
320
- if (events.length < prevEventsLenRef.current) {
321
- eventLinesRef.current = [];
322
- prevEventsLenRef.current = 0;
323
- }
324
-
325
- // Only process newly added events (events array grows monotonically)
326
- const startIdx = prevEventsLenRef.current;
327
- if (startIdx >= events.length) return;
328
-
329
- const seenDone = new Set<string>();
330
- // Re-scan for msg:done dedup across ALL events (need full context)
331
- const doneIndices = new Map<string, number>();
332
- for (let i = 0; i < events.length; i++) {
333
- if (events[i].type === 'msg:done') {
334
- doneIndices.set(events[i].roleId, i);
335
- }
336
- }
337
-
338
- for (let i = startIdx; i < events.length; i++) {
339
- const event = events[i];
340
- // Skip consecutive thinking from same role
341
- if (event.type === 'thinking' && i + 1 < events.length) {
342
- const next = events[i + 1];
343
- if (next.type === 'thinking' && next.roleId === event.roleId) continue;
344
- }
345
- // Dedup msg:done — only show last one per role
346
- if (event.type === 'msg:done') {
347
- const key = `${event.roleId}:done`;
348
- if (seenDone.has(key)) continue;
349
- if (doneIndices.get(event.roleId) !== i) continue; // Not the last one
350
- seenDone.add(key);
351
- }
352
- const line = summarizeEvent(event, allRoleIds);
353
- if (line) eventLinesRef.current.push(line);
354
- }
355
- prevEventsLenRef.current = events.length;
356
-
357
- // Cap to prevent unbounded growth
358
- if (eventLinesRef.current.length > 60) {
359
- eventLinesRef.current = eventLinesRef.current.slice(-60);
360
- }
361
- }, [events, allRoleIds]);
362
-
363
- const eventLines = eventLinesRef.current;
364
-
365
310
  // Merge lines: system + events go to scrollback, user inputs are pinned in live area.
366
311
  // Without pinning, long responses push ━━ > lines into Static scrollback → invisible.
367
312
  const scrollableLines = [...systemMessages, ...eventLines]