tycono 0.1.96-beta.2 → 0.1.96-beta.21

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.
@@ -1,11 +1,13 @@
1
1
  /**
2
- * useSSE — subscribe to wave SSE stream
2
+ * useSSE — subscribe to wave SSE stream with auto-reconnect
3
3
  */
4
4
 
5
5
  import { useState, useEffect, useRef, useCallback } from 'react';
6
6
  import { subscribeToWaveStream, type SSEEvent, type SSEConnection } from '../api';
7
7
 
8
8
  const MAX_EVENTS = 500;
9
+ const RECONNECT_DELAY_MS = 3000;
10
+ const MAX_RECONNECT_DELAY_MS = 15000;
9
11
 
10
12
  export interface SSEState {
11
13
  events: SSEEvent[];
@@ -18,9 +20,13 @@ export function useSSE(waveId: string | null): SSEState {
18
20
  const [streamStatus, setStreamStatus] = useState<'idle' | 'streaming' | 'done' | 'error'>('idle');
19
21
  const connRef = useRef<SSEConnection | null>(null);
20
22
  const waveIdRef = useRef<string | null>(null);
23
+ const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24
+ const reconnectAttemptRef = useRef(0);
25
+ const maxSeqRef = useRef(0);
21
26
 
22
27
  const clearEvents = useCallback(() => {
23
28
  setEvents([]);
29
+ maxSeqRef.current = 0;
24
30
  }, []);
25
31
 
26
32
  useEffect(() => {
@@ -29,6 +35,11 @@ export function useSSE(waveId: string | null): SSEState {
29
35
  connRef.current?.close();
30
36
  connRef.current = null;
31
37
  waveIdRef.current = waveId;
38
+ reconnectAttemptRef.current = 0;
39
+ if (reconnectTimerRef.current) {
40
+ clearTimeout(reconnectTimerRef.current);
41
+ reconnectTimerRef.current = null;
42
+ }
32
43
  }
33
44
 
34
45
  if (!waveId) {
@@ -36,32 +47,65 @@ export function useSSE(waveId: string | null): SSEState {
36
47
  return;
37
48
  }
38
49
 
39
- setStreamStatus('streaming');
40
- setEvents([]);
50
+ const connect = (fromSeq: number) => {
51
+ setStreamStatus('streaming');
52
+
53
+ // Only clear events on first connect (not reconnects)
54
+ if (fromSeq === 0) {
55
+ setEvents([]);
56
+ maxSeqRef.current = 0;
57
+ }
58
+
59
+ const conn = subscribeToWaveStream(
60
+ waveId,
61
+ (event) => {
62
+ // Track max seq for reconnect resume
63
+ if (event.seq !== undefined && event.seq > maxSeqRef.current) {
64
+ maxSeqRef.current = event.seq;
65
+ }
66
+ reconnectAttemptRef.current = 0; // Reset on successful event
67
+
68
+ setEvents((prev) => {
69
+ const next = [...prev, event];
70
+ return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
71
+ });
72
+ },
73
+ (reason) => {
74
+ if (reason === 'done') {
75
+ setStreamStatus('done');
76
+ } else {
77
+ // Disconnected or error → auto-reconnect
78
+ const attempt = reconnectAttemptRef.current++;
79
+ const delay = Math.min(
80
+ RECONNECT_DELAY_MS * Math.pow(1.5, attempt),
81
+ MAX_RECONNECT_DELAY_MS,
82
+ );
83
+
84
+ setStreamStatus('streaming'); // Keep showing streaming during reconnect
41
85
 
42
- const conn = subscribeToWaveStream(
43
- waveId,
44
- (event) => {
45
- setEvents((prev) => {
46
- const next = [...prev, event];
47
- return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
48
- });
49
- },
50
- (reason) => {
51
- if (reason === 'done') {
52
- setStreamStatus('done');
53
- } else if (reason === 'error') {
54
- setStreamStatus('error');
55
- } else {
56
- setStreamStatus('done');
57
- }
58
- },
59
- );
60
-
61
- connRef.current = conn;
86
+ reconnectTimerRef.current = setTimeout(() => {
87
+ reconnectTimerRef.current = null;
88
+ if (waveIdRef.current === waveId) {
89
+ connect(maxSeqRef.current);
90
+ }
91
+ }, delay);
92
+ }
93
+ },
94
+ fromSeq,
95
+ );
96
+
97
+ connRef.current = conn;
98
+ };
99
+
100
+ connect(0);
62
101
 
63
102
  return () => {
64
- conn.close();
103
+ connRef.current?.close();
104
+ connRef.current = null;
105
+ if (reconnectTimerRef.current) {
106
+ clearTimeout(reconnectTimerRef.current);
107
+ reconnectTimerRef.current = null;
108
+ }
65
109
  };
66
110
  }, [waveId]);
67
111
 
package/src/tui/store.ts CHANGED
@@ -97,3 +97,15 @@ export function buildOrgTree(roles: RoleInfo[], statuses: Record<string, string>
97
97
 
98
98
  return roots;
99
99
  }
100
+
101
+ /** Flatten org tree into visual top-to-bottom order of role IDs */
102
+ export function flattenOrgRoleIds(nodes: OrgNode[]): string[] {
103
+ const result: string[] = [];
104
+ for (const node of nodes) {
105
+ result.push(node.role.id);
106
+ if (node.children.length > 0) {
107
+ result.push(...flattenOrgRoleIds(node.children));
108
+ }
109
+ }
110
+ return result;
111
+ }
@@ -1,32 +0,0 @@
1
- /**
2
- * CommandInput — bottom bar with command input and shortcut hints
3
- */
4
-
5
- import React from 'react';
6
- import { Box, Text } from 'ink';
7
-
8
- interface CommandInputProps {
9
- focused: boolean;
10
- waveStatus: 'idle' | 'running' | 'done';
11
- dialog: string;
12
- }
13
-
14
- export const CommandInput: React.FC<CommandInputProps> = ({ focused, waveStatus, dialog }) => {
15
- if (dialog !== 'none') return null;
16
-
17
- return (
18
- <Box width="100%" paddingX={1} justifyContent="space-between">
19
- <Box>
20
- <Text color="green" bold>&gt; </Text>
21
- <Text color={focused ? 'white' : 'gray'}>
22
- {waveStatus === 'running' ? 'Wave running...' : 'Ready'}
23
- </Text>
24
- </Box>
25
- <Box>
26
- <Text color="gray" dimColor>
27
- [w]ave [?]help [q]uit [Tab]panel
28
- </Text>
29
- </Box>
30
- </Box>
31
- );
32
- };
@@ -1,51 +0,0 @@
1
- /**
2
- * HelpOverlay — keyboard shortcut reference
3
- */
4
-
5
- import React from 'react';
6
- import { Box, Text, useInput } from 'ink';
7
-
8
- interface HelpOverlayProps {
9
- onClose(): void;
10
- }
11
-
12
- const shortcuts = [
13
- ['w', 'Wave dispatch'],
14
- ['q', 'Quit'],
15
- ['?', 'Toggle help'],
16
- ['Tab', 'Cycle panels'],
17
- ['j/k', 'Navigate (in focused panel)'],
18
- ['Enter', 'Select'],
19
- ['Esc', 'Close dialog / deselect'],
20
- ] as const;
21
-
22
- export const HelpOverlay: React.FC<HelpOverlayProps> = ({ onClose }) => {
23
- useInput((input, key) => {
24
- if (input === '?' || input === 'q' || key.escape) {
25
- onClose();
26
- }
27
- });
28
-
29
- return (
30
- <Box
31
- flexDirection="column"
32
- borderStyle="round"
33
- borderColor="yellow"
34
- paddingX={2}
35
- paddingY={1}
36
- >
37
- <Text bold color="yellow">{'\u2500\u2500'} Keyboard Shortcuts {'\u2500\u2500'}</Text>
38
- <Box marginTop={1} flexDirection="column">
39
- {shortcuts.map(([key, desc]) => (
40
- <Box key={key}>
41
- <Text color="cyan" bold>{key.padEnd(8)}</Text>
42
- <Text color="white">{desc}</Text>
43
- </Box>
44
- ))}
45
- </Box>
46
- <Box marginTop={1}>
47
- <Text color="gray" dimColor>Press ? or Esc to close</Text>
48
- </Box>
49
- </Box>
50
- );
51
- };
@@ -1,74 +0,0 @@
1
- /**
2
- * SessionList — left bottom panel showing session history
3
- */
4
-
5
- import React from 'react';
6
- import { Box, Text } from 'ink';
7
- import type { SessionInfo } from '../api';
8
-
9
- interface SessionListProps {
10
- sessions: SessionInfo[];
11
- focused: boolean;
12
- selectedIndex: number;
13
- }
14
-
15
- function sourceIcon(source: string): string {
16
- switch (source) {
17
- case 'wave': return 'W';
18
- case 'dispatch': return 'D';
19
- case 'chat': return 'C';
20
- case 'consult': return 'Q';
21
- default: return '?';
22
- }
23
- }
24
-
25
- function statusSymbol(status: string): { icon: string; color: string } {
26
- switch (status) {
27
- case 'active':
28
- return { icon: '\u25CF', color: 'green' };
29
- case 'closed':
30
- return { icon: '\u2713', color: 'gray' };
31
- default:
32
- return { icon: '\u25CB', color: 'gray' };
33
- }
34
- }
35
-
36
- export const SessionList: React.FC<SessionListProps> = ({ sessions, focused, selectedIndex }) => {
37
- // Show most recent first, limit to 10
38
- const sorted = [...sessions]
39
- .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
40
- .slice(0, 10);
41
-
42
- return (
43
- <Box flexDirection="column" paddingX={1}>
44
- <Text bold color={focused ? 'cyan' : 'gray'}>
45
- {'\u2500\u2500'} Sessions ({sessions.length}) {'\u2500\u2500'}
46
- </Text>
47
- {sorted.length === 0 && (
48
- <Text color="gray" dimColor>No sessions yet</Text>
49
- )}
50
- {sorted.map((session, i) => {
51
- const isSelected = focused && i === selectedIndex;
52
- const { icon, color } = statusSymbol(session.status);
53
- const title = session.title || session.roleId;
54
- const src = sourceIcon(session.source);
55
-
56
- return (
57
- <Box key={session.id}>
58
- <Text color={color}>{icon}</Text>
59
- <Text> </Text>
60
- <Text color="gray">[{src}]</Text>
61
- <Text> </Text>
62
- <Text
63
- color={isSelected ? 'cyan' : 'white'}
64
- bold={isSelected}
65
- inverse={isSelected}
66
- >
67
- {title.length > 25 ? title.slice(0, 24) + '\u2026' : title}
68
- </Text>
69
- </Box>
70
- );
71
- })}
72
- </Box>
73
- );
74
- };
@@ -1,56 +0,0 @@
1
- /**
2
- * WaveDialog — modal for wave dispatch input
3
- */
4
-
5
- import React, { useState } from 'react';
6
- import { Box, Text, useInput } from 'ink';
7
- import TextInput from 'ink-text-input';
8
-
9
- interface WaveDialogProps {
10
- onSubmit(directive: string): void;
11
- onCancel(): void;
12
- }
13
-
14
- export const WaveDialog: React.FC<WaveDialogProps> = ({ onSubmit, onCancel }) => {
15
- const [directive, setDirective] = useState('');
16
-
17
- useInput((_input, key) => {
18
- if (key.escape) {
19
- onCancel();
20
- }
21
- });
22
-
23
- const handleSubmit = (value: string) => {
24
- const trimmed = value.trim();
25
- if (trimmed) {
26
- onSubmit(trimmed);
27
- }
28
- };
29
-
30
- return (
31
- <Box
32
- flexDirection="column"
33
- borderStyle="round"
34
- borderColor="cyan"
35
- paddingX={2}
36
- paddingY={1}
37
- >
38
- <Text bold color="cyan">{'\u2500\u2500'} Wave Dispatch {'\u2500\u2500'}</Text>
39
- <Box marginTop={1}>
40
- <Text color="white">Directive: </Text>
41
- </Box>
42
- <Box marginTop={0}>
43
- <Text color="green">&gt; </Text>
44
- <TextInput
45
- value={directive}
46
- onChange={setDirective}
47
- onSubmit={handleSubmit}
48
- placeholder="Type your wave directive..."
49
- />
50
- </Box>
51
- <Box marginTop={1}>
52
- <Text color="gray" dimColor>[Enter: Dispatch] [Esc: Cancel]</Text>
53
- </Box>
54
- </Box>
55
- );
56
- };
@@ -1,62 +0,0 @@
1
- /**
2
- * useKeyboard — global keyboard shortcuts for TUI
3
- */
4
-
5
- import { useInput } from 'ink';
6
-
7
- export interface KeyboardActions {
8
- onWave(): void;
9
- onQuit(): void;
10
- onHelp(): void;
11
- onTab(): void;
12
- onUp(): void;
13
- onDown(): void;
14
- onEnter(): void;
15
- onEscape(): void;
16
- }
17
-
18
- export function useKeyboard(actions: KeyboardActions, enabled: boolean): void {
19
- useInput((input, key) => {
20
- if (!enabled) return;
21
-
22
- if (input === 'w') {
23
- actions.onWave();
24
- return;
25
- }
26
-
27
- if (input === 'q') {
28
- actions.onQuit();
29
- return;
30
- }
31
-
32
- if (input === '?') {
33
- actions.onHelp();
34
- return;
35
- }
36
-
37
- if (key.tab) {
38
- actions.onTab();
39
- return;
40
- }
41
-
42
- if (key.upArrow || input === 'k') {
43
- actions.onUp();
44
- return;
45
- }
46
-
47
- if (key.downArrow || input === 'j') {
48
- actions.onDown();
49
- return;
50
- }
51
-
52
- if (key.return) {
53
- actions.onEnter();
54
- return;
55
- }
56
-
57
- if (key.escape) {
58
- actions.onEscape();
59
- return;
60
- }
61
- });
62
- }