@online5880/opensession 0.1.0 → 0.1.3
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.ko.md +133 -0
- package/README.md +137 -30
- package/package.json +23 -20
- package/site/index.html +35 -0
- package/sql/schema.sql +38 -27
- package/src/automation.js +197 -0
- package/src/cli.js +1317 -172
- package/src/config.js +118 -32
- package/src/hook-server.js +194 -0
- package/src/idempotency.js +66 -0
- package/src/metrics.js +110 -0
- package/src/supabase.js +309 -119
- package/src/tui.js +159 -0
- package/src/viewer.js +708 -0
- package/test/cli-compatibility.test.js +63 -0
- package/test/config-secrets.test.js +47 -0
- package/test/idempotency.test.js +30 -0
- package/test/supabase-append-event.test.js +133 -0
package/src/supabase.js
CHANGED
|
@@ -1,119 +1,309 @@
|
|
|
1
|
-
import { createClient } from '@supabase/supabase-js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
throw
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
4
|
+
const BASE_BACKOFF_MS = 120;
|
|
5
|
+
const MAX_BACKOFF_MS = 1500;
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
setTimeout(resolve, ms);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function shouldRetry(error) {
|
|
14
|
+
if (!error) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const status = Number(error.status ?? error.statusCode ?? 0);
|
|
18
|
+
const code = String(error.code ?? '').toUpperCase();
|
|
19
|
+
const message = String(error.message ?? '').toLowerCase();
|
|
20
|
+
|
|
21
|
+
if (status >= 500 || status === 408 || status === 429) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'EAI_AGAIN') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (message.includes('network') || message.includes('timeout') || message.includes('fetch failed')) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isUniqueViolation(error) {
|
|
34
|
+
if (!error) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const code = String(error.code ?? '').toUpperCase();
|
|
38
|
+
const status = Number(error.status ?? error.statusCode ?? 0);
|
|
39
|
+
return code === '23505' || status === 409;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function withRetry(taskName, fn, attempts = DEFAULT_RETRY_ATTEMPTS) {
|
|
43
|
+
let attempt = 0;
|
|
44
|
+
let lastError = null;
|
|
45
|
+
while (attempt < attempts) {
|
|
46
|
+
attempt += 1;
|
|
47
|
+
try {
|
|
48
|
+
return await fn();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
lastError = error;
|
|
51
|
+
if (attempt >= attempts || !shouldRetry(error)) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const exponential = BASE_BACKOFF_MS * (2 ** (attempt - 1));
|
|
55
|
+
const jitter = Math.floor(Math.random() * 80);
|
|
56
|
+
const backoffMs = Math.min(MAX_BACKOFF_MS, exponential + jitter);
|
|
57
|
+
await sleep(backoffMs);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw lastError ?? new Error(`${taskName} failed`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getClient(config) {
|
|
65
|
+
if (!config.supabaseUrl || !config.supabaseAnonKey) {
|
|
66
|
+
throw new Error('Supabase is not configured. Run `opensession init` first.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return createClient(config.supabaseUrl, config.supabaseAnonKey, {
|
|
70
|
+
auth: { persistSession: false }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function ensureProject(client, projectKey, projectName) {
|
|
75
|
+
const { data: existing, error: lookupError } = await withRetry('ensureProject.lookup', () =>
|
|
76
|
+
client
|
|
77
|
+
.from('projects')
|
|
78
|
+
.select('id,project_key,name')
|
|
79
|
+
.eq('project_key', projectKey)
|
|
80
|
+
.maybeSingle()
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (lookupError) {
|
|
84
|
+
throw lookupError;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existing) {
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { data: created, error: insertError } = await withRetry('ensureProject.insert', () =>
|
|
92
|
+
client
|
|
93
|
+
.from('projects')
|
|
94
|
+
.insert({ project_key: projectKey, name: projectName ?? projectKey })
|
|
95
|
+
.select('id,project_key,name')
|
|
96
|
+
.single()
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (insertError) {
|
|
100
|
+
throw insertError;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return created;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function startSession(client, projectId, actor, options = {}) {
|
|
107
|
+
const operationId = options.operationId ?? `start-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
108
|
+
const { data: session, error: sessionError } = await withRetry('startSession.insert', () =>
|
|
109
|
+
client
|
|
110
|
+
.from('sessions')
|
|
111
|
+
.insert({ project_id: projectId, actor, status: 'active', started_at: new Date().toISOString() })
|
|
112
|
+
.select('id,project_id,actor,status,started_at')
|
|
113
|
+
.single()
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (sessionError) {
|
|
117
|
+
throw sessionError;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { error: eventError } = await withRetry('startSession.appendStartedEvent', () =>
|
|
121
|
+
client
|
|
122
|
+
.from('session_events')
|
|
123
|
+
.insert({
|
|
124
|
+
session_id: session.id,
|
|
125
|
+
type: 'started',
|
|
126
|
+
payload: {
|
|
127
|
+
actor,
|
|
128
|
+
idempotencyKey: `${operationId}:started`
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (eventError) {
|
|
134
|
+
throw eventError;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return session;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function appendEvent(client, sessionId, type, payload = {}, options = {}) {
|
|
141
|
+
const normalizedIdempotencyKey = typeof options.idempotencyKey === 'string' && options.idempotencyKey.trim().length > 0
|
|
142
|
+
? options.idempotencyKey.trim()
|
|
143
|
+
: null;
|
|
144
|
+
const nextPayload = normalizedIdempotencyKey ? { ...payload, idempotencyKey: normalizedIdempotencyKey } : payload;
|
|
145
|
+
const { data, error } = await withRetry('appendEvent.insert', () =>
|
|
146
|
+
client
|
|
147
|
+
.from('session_events')
|
|
148
|
+
.insert({
|
|
149
|
+
session_id: sessionId,
|
|
150
|
+
type,
|
|
151
|
+
idempotency_key: normalizedIdempotencyKey,
|
|
152
|
+
payload: nextPayload
|
|
153
|
+
})
|
|
154
|
+
.select('id,session_id,type,payload,created_at')
|
|
155
|
+
.single()
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (!error) {
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!normalizedIdempotencyKey || !isUniqueViolation(error)) {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { data: existing, error: findError } = await withRetry('appendEvent.findExistingAfterConflict', () =>
|
|
167
|
+
client
|
|
168
|
+
.from('session_events')
|
|
169
|
+
.select('id,session_id,type,payload,created_at')
|
|
170
|
+
.eq('session_id', sessionId)
|
|
171
|
+
.eq('type', type)
|
|
172
|
+
.eq('idempotency_key', normalizedIdempotencyKey)
|
|
173
|
+
.limit(1)
|
|
174
|
+
.maybeSingle()
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (findError) {
|
|
178
|
+
throw findError;
|
|
179
|
+
}
|
|
180
|
+
if (!existing) {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return existing;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function validateConnection(supabaseUrl, supabaseAnonKey) {
|
|
188
|
+
const client = createClient(supabaseUrl, supabaseAnonKey, {
|
|
189
|
+
auth: { persistSession: false }
|
|
190
|
+
});
|
|
191
|
+
const { error } = await withRetry('validateConnection', () =>
|
|
192
|
+
client.from('projects').select('id').limit(1)
|
|
193
|
+
);
|
|
194
|
+
if (error) {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function getSession(client, sessionId) {
|
|
200
|
+
const { data, error } = await withRetry('getSession', () =>
|
|
201
|
+
client
|
|
202
|
+
.from('sessions')
|
|
203
|
+
.select('id,project_id,actor,status,started_at,ended_at')
|
|
204
|
+
.eq('id', sessionId)
|
|
205
|
+
.maybeSingle()
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (error) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return data;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function listActiveSessions(client, projectId) {
|
|
216
|
+
const { data, error } = await withRetry('listActiveSessions', () =>
|
|
217
|
+
client
|
|
218
|
+
.from('sessions')
|
|
219
|
+
.select('id,project_id,actor,status,started_at,ended_at')
|
|
220
|
+
.eq('project_id', projectId)
|
|
221
|
+
.eq('status', 'active')
|
|
222
|
+
.order('started_at', { ascending: false })
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (error) {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return data;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function getSessionEvents(client, sessionId, limit = 50, options = {}) {
|
|
233
|
+
const ascending = options.ascending !== false;
|
|
234
|
+
const { data, error } = await withRetry('getSessionEvents', () =>
|
|
235
|
+
client
|
|
236
|
+
.from('session_events')
|
|
237
|
+
.select('id,session_id,type,payload,created_at')
|
|
238
|
+
.eq('session_id', sessionId)
|
|
239
|
+
.order('created_at', { ascending })
|
|
240
|
+
.limit(limit)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (error) {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return data;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function listSessionEvents(client, sessionIds, options = {}) {
|
|
251
|
+
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let query = client
|
|
256
|
+
.from('session_events')
|
|
257
|
+
.select('id,session_id,type,payload,created_at')
|
|
258
|
+
.in('session_id', sessionIds)
|
|
259
|
+
.order('created_at', { ascending: options.ascending !== false });
|
|
260
|
+
|
|
261
|
+
if (options.since) {
|
|
262
|
+
query = query.gte('created_at', options.since);
|
|
263
|
+
}
|
|
264
|
+
if (options.until) {
|
|
265
|
+
query = query.lt('created_at', options.until);
|
|
266
|
+
}
|
|
267
|
+
if (Number.isInteger(options.limit) && options.limit > 0) {
|
|
268
|
+
query = query.limit(options.limit);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { data, error } = await query;
|
|
272
|
+
if (error) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function listProjects(client, limit = 50) {
|
|
279
|
+
const { data, error } = await withRetry('listProjects', () =>
|
|
280
|
+
client
|
|
281
|
+
.from('projects')
|
|
282
|
+
.select('id,project_key,name,created_at')
|
|
283
|
+
.order('created_at', { ascending: false })
|
|
284
|
+
.limit(limit)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (error) {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return data;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function listSessions(client, projectId, limit = 100) {
|
|
295
|
+
const { data, error } = await withRetry('listSessions', () =>
|
|
296
|
+
client
|
|
297
|
+
.from('sessions')
|
|
298
|
+
.select('id,project_id,actor,status,started_at,ended_at')
|
|
299
|
+
.eq('project_id', projectId)
|
|
300
|
+
.order('started_at', { ascending: false })
|
|
301
|
+
.limit(limit)
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (error) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return data;
|
|
309
|
+
}
|
package/src/tui.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { listActiveSessions, getSessionEvents } from './supabase.js';
|
|
3
|
+
|
|
4
|
+
export async function startTui(client, options = {}) {
|
|
5
|
+
const screen = blessed.screen({
|
|
6
|
+
smartCSR: true,
|
|
7
|
+
title: 'OpenSession TUI Dashboard'
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const layout = blessed.layout({
|
|
11
|
+
parent: screen,
|
|
12
|
+
top: 0,
|
|
13
|
+
left: 0,
|
|
14
|
+
width: '100%',
|
|
15
|
+
height: '100%'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const header = blessed.box({
|
|
19
|
+
parent: layout,
|
|
20
|
+
top: 0,
|
|
21
|
+
left: 0,
|
|
22
|
+
width: '100%',
|
|
23
|
+
height: 3,
|
|
24
|
+
content: ' OpenSession TUI Dashboard ',
|
|
25
|
+
style: {
|
|
26
|
+
fg: 'white',
|
|
27
|
+
bg: 'blue',
|
|
28
|
+
bold: true
|
|
29
|
+
},
|
|
30
|
+
border: {
|
|
31
|
+
type: 'line'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const sessionList = blessed.list({
|
|
36
|
+
parent: layout,
|
|
37
|
+
top: 3,
|
|
38
|
+
left: 0,
|
|
39
|
+
width: '30%',
|
|
40
|
+
height: '100%-3',
|
|
41
|
+
label: ' Active Sessions ',
|
|
42
|
+
border: {
|
|
43
|
+
type: 'line'
|
|
44
|
+
},
|
|
45
|
+
style: {
|
|
46
|
+
selected: {
|
|
47
|
+
bg: 'magenta'
|
|
48
|
+
},
|
|
49
|
+
border: {
|
|
50
|
+
fg: 'cyan'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
keys: true,
|
|
54
|
+
mouse: true
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const eventLog = blessed.log({
|
|
58
|
+
parent: layout,
|
|
59
|
+
top: 3,
|
|
60
|
+
left: '30%',
|
|
61
|
+
width: '70%',
|
|
62
|
+
height: '100%-3',
|
|
63
|
+
label: ' Session Events ',
|
|
64
|
+
border: {
|
|
65
|
+
type: 'line'
|
|
66
|
+
},
|
|
67
|
+
style: {
|
|
68
|
+
border: {
|
|
69
|
+
fg: 'yellow'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
scrollable: true,
|
|
73
|
+
alwaysScroll: true,
|
|
74
|
+
mouse: true
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const footer = blessed.box({
|
|
78
|
+
parent: screen,
|
|
79
|
+
bottom: 0,
|
|
80
|
+
left: 0,
|
|
81
|
+
width: '100%',
|
|
82
|
+
height: 1,
|
|
83
|
+
content: ' [R] Refresh | [Q] Quit | [↑/↓] Select Session ',
|
|
84
|
+
style: {
|
|
85
|
+
fg: 'black',
|
|
86
|
+
bg: 'white'
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let sessions = [];
|
|
91
|
+
let selectedSessionId = null;
|
|
92
|
+
let refreshInterval = null;
|
|
93
|
+
|
|
94
|
+
async function refreshSessions() {
|
|
95
|
+
try {
|
|
96
|
+
header.setContent(` Loading sessions... `);
|
|
97
|
+
screen.render();
|
|
98
|
+
|
|
99
|
+
const activeSessions = await listActiveSessions(client);
|
|
100
|
+
sessions = activeSessions || [];
|
|
101
|
+
|
|
102
|
+
sessionList.setItems(sessions.map(s => `${s.actor} (${s.id.slice(0, 8)})`));
|
|
103
|
+
header.setContent(` OpenSession TUI Dashboard | Total: ${sessions.length} `);
|
|
104
|
+
screen.render();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
header.setContent(` Error: ${error.message} `);
|
|
107
|
+
screen.render();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function refreshEvents() {
|
|
112
|
+
if (!selectedSessionId) return;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const events = await getSessionEvents(client, selectedSessionId);
|
|
116
|
+
const currentScroll = eventLog.childBase;
|
|
117
|
+
|
|
118
|
+
eventLog.clear();
|
|
119
|
+
if (events && events.length > 0) {
|
|
120
|
+
events.forEach(e => {
|
|
121
|
+
eventLog.log(`[${new Date(e.created_at).toLocaleTimeString()}] ${e.type}: ${JSON.stringify(e.payload)}`);
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
eventLog.log(' No events found for this session.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Maintain scroll position if needed or scroll to bottom
|
|
128
|
+
eventLog.setScroll(currentScroll);
|
|
129
|
+
screen.render();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
eventLog.log(` Auto-refresh error: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
sessionList.on('select', async (item, index) => {
|
|
136
|
+
const session = sessions[index];
|
|
137
|
+
if (!session) return;
|
|
138
|
+
|
|
139
|
+
selectedSessionId = session.id;
|
|
140
|
+
eventLog.setContent(` Loading events for ${session.id}... \n`);
|
|
141
|
+
screen.render();
|
|
142
|
+
|
|
143
|
+
if (refreshInterval) clearInterval(refreshInterval);
|
|
144
|
+
|
|
145
|
+
await refreshEvents();
|
|
146
|
+
|
|
147
|
+
// Set up auto-refresh every 5 seconds for the selected session
|
|
148
|
+
refreshInterval = setInterval(() => refreshEvents(), 5000);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
screen.key(['escape', 'q', 'C-c'], () => {
|
|
152
|
+
if (refreshInterval) clearInterval(refreshInterval);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
155
|
+
screen.key(['r'], () => refreshSessions());
|
|
156
|
+
|
|
157
|
+
await refreshSessions();
|
|
158
|
+
screen.render();
|
|
159
|
+
}
|