@punkcode/cli 0.1.16 → 0.1.18

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.
@@ -0,0 +1,785 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PunkConnection,
4
+ clearAuth,
5
+ createPunkStore,
6
+ initQrSession,
7
+ loadAuth,
8
+ refreshIdToken,
9
+ saveAuth,
10
+ signIn,
11
+ usePunkStore,
12
+ version
13
+ } from "./chunk-NW32U73H.js";
14
+
15
+ // src/ui/App.tsx
16
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
17
+
18
+ // src/ui/LoginDialog.tsx
19
+ import { useState, useEffect, useCallback } from "react";
20
+ import { Box, Text, useInput } from "ink";
21
+ import { TextInput, Spinner } from "@inkjs/ui";
22
+
23
+ // src/ui/theme.ts
24
+ var theme = {
25
+ // Accent colors
26
+ primary: "cyan",
27
+ accent: "yellow",
28
+ // Status colors
29
+ success: "green",
30
+ error: "red",
31
+ warning: "yellow",
32
+ // Structural
33
+ border: "gray",
34
+ sectionTitle: "cyan",
35
+ // Logo
36
+ logo: "yellow"
37
+ };
38
+
39
+ // src/ui/LoginDialog.tsx
40
+ import { jsx, jsxs } from "react/jsx-runtime";
41
+ var QR_COLORS = ["cyan", "magenta", "greenBright", "yellowBright", "cyanBright", "magentaBright"];
42
+ function LoginDialog({ store, server }) {
43
+ const [mode, setMode] = useState("qr");
44
+ const [error, setError] = useState(null);
45
+ const [qrString, setQrString] = useState("");
46
+ const [colorIdx, setColorIdx] = useState(0);
47
+ const [email, setEmail] = useState("");
48
+ const [polling, setPolling] = useState(false);
49
+ const [welcomeEmail, setWelcomeEmail] = useState("");
50
+ useEffect(() => {
51
+ if (mode !== "success") return;
52
+ const timer = setTimeout(() => {
53
+ store.getState().setScreen("connecting");
54
+ }, 2500);
55
+ return () => clearTimeout(timer);
56
+ }, [mode, store]);
57
+ useEffect(() => {
58
+ if (mode !== "qr") return;
59
+ const timer = setInterval(() => {
60
+ setColorIdx((i) => (i + 1) % QR_COLORS.length);
61
+ }, 1e3);
62
+ return () => clearInterval(timer);
63
+ }, [mode]);
64
+ const startQr = useCallback(async () => {
65
+ setMode("qr");
66
+ setError(null);
67
+ setPolling(false);
68
+ try {
69
+ const session = await initQrSession(server);
70
+ setQrString(session.qrString);
71
+ setPolling(true);
72
+ const ac = new AbortController();
73
+ for await (const result of session.poll(ac.signal)) {
74
+ if (result.status === "pending") continue;
75
+ if (result.status === "expired") {
76
+ setError("QR code expired. Retrying...");
77
+ setPolling(false);
78
+ setTimeout(() => startQr(), 1e3);
79
+ return;
80
+ }
81
+ if (result.status === "confirmed") {
82
+ handleConfirmed(result);
83
+ return;
84
+ }
85
+ }
86
+ } catch (err) {
87
+ setError(err instanceof Error ? err.message : "QR login failed");
88
+ setPolling(false);
89
+ }
90
+ }, [server, store]);
91
+ useEffect(() => {
92
+ startQr();
93
+ }, [startQr]);
94
+ const handleConfirmed = (result) => {
95
+ saveAuth({
96
+ idToken: result.idToken,
97
+ refreshToken: result.refreshToken,
98
+ expiresAt: Date.now() + parseInt(result.expiresIn, 10) * 1e3,
99
+ email: result.email,
100
+ uid: result.uid
101
+ });
102
+ store.getState().setAuth({ email: result.email, uid: result.uid });
103
+ store.getState().addActivity({ icon: "\u2713", message: `Logged in as ${result.email}` });
104
+ setWelcomeEmail(result.email);
105
+ setMode("success");
106
+ };
107
+ useInput((input, key) => {
108
+ if (mode === "qr" && input === "e") {
109
+ setMode("email-input");
110
+ setError(null);
111
+ }
112
+ if ((mode === "email-input" || mode === "password-input") && key.escape) {
113
+ setMode("qr");
114
+ setEmail("");
115
+ startQr();
116
+ }
117
+ }, { isActive: mode === "qr" || mode === "email-input" || mode === "password-input" });
118
+ const handleEmailSubmit = (value) => {
119
+ setEmail(value);
120
+ setMode("password-input");
121
+ };
122
+ const handlePasswordSubmit = async (password) => {
123
+ setMode("authenticating");
124
+ setError(null);
125
+ try {
126
+ const auth = await signIn(email, password);
127
+ store.getState().setAuth({ email: auth.email, uid: auth.uid });
128
+ store.getState().addActivity({ icon: "\u2713", message: `Logged in as ${auth.email}` });
129
+ setWelcomeEmail(auth.email);
130
+ setMode("success");
131
+ } catch (err) {
132
+ setError(err instanceof Error ? err.message : "Login failed");
133
+ setMode("email-input");
134
+ setEmail("");
135
+ }
136
+ };
137
+ return /* @__PURE__ */ jsxs(
138
+ Box,
139
+ {
140
+ flexDirection: "column",
141
+ paddingX: 2,
142
+ children: [
143
+ error && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.error, children: [
144
+ "\u2717",
145
+ " ",
146
+ error
147
+ ] }) }),
148
+ mode === "qr" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
149
+ /* @__PURE__ */ jsx(Text, { children: "Scan with the Punk app to connect:" }),
150
+ /* @__PURE__ */ jsx(Text, { children: " " }),
151
+ qrString ? /* @__PURE__ */ jsx(Text, { color: QR_COLORS[colorIdx], children: qrString }) : /* @__PURE__ */ jsx(Spinner, { label: "Generating QR code..." }),
152
+ /* @__PURE__ */ jsx(Text, { children: " " }),
153
+ polling && /* @__PURE__ */ jsx(Spinner, { label: "Waiting for confirmation..." }),
154
+ /* @__PURE__ */ jsx(Text, { children: " " }),
155
+ /* @__PURE__ */ jsxs(Text, { children: [
156
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, bold: true, children: "e" }),
157
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " email login" })
158
+ ] })
159
+ ] }),
160
+ mode === "email-input" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
161
+ /* @__PURE__ */ jsx(Text, { children: "Email:" }),
162
+ /* @__PURE__ */ jsx(TextInput, { placeholder: "you@example.com", onSubmit: handleEmailSubmit }),
163
+ /* @__PURE__ */ jsx(Text, { children: " " }),
164
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc to go back to QR" })
165
+ ] }),
166
+ mode === "password-input" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
167
+ /* @__PURE__ */ jsxs(Text, { children: [
168
+ "Password for ",
169
+ email,
170
+ ":"
171
+ ] }),
172
+ /* @__PURE__ */ jsx(PasswordInput, { onSubmit: handlePasswordSubmit }),
173
+ /* @__PURE__ */ jsx(Text, { children: " " }),
174
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc to go back to QR" })
175
+ ] }),
176
+ mode === "authenticating" && /* @__PURE__ */ jsx(Box, { justifyContent: "center", children: /* @__PURE__ */ jsx(Spinner, { label: "Authenticating..." }) }),
177
+ mode === "success" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
178
+ /* @__PURE__ */ jsx(Text, { children: " " }),
179
+ /* @__PURE__ */ jsxs(Text, { color: theme.success, bold: true, children: [
180
+ "\u2714",
181
+ " Welcome!"
182
+ ] }),
183
+ /* @__PURE__ */ jsx(Text, { children: " " }),
184
+ /* @__PURE__ */ jsx(Text, { children: welcomeEmail }),
185
+ /* @__PURE__ */ jsx(Text, { children: " " }),
186
+ /* @__PURE__ */ jsx(Spinner, { label: "Connecting..." }),
187
+ /* @__PURE__ */ jsx(Text, { children: " " })
188
+ ] })
189
+ ]
190
+ }
191
+ );
192
+ }
193
+ function PasswordInput({ onSubmit }) {
194
+ const [value, setValue] = useState("");
195
+ useInput((input, key) => {
196
+ if (key.return) {
197
+ onSubmit(value);
198
+ } else if (key.backspace || key.delete) {
199
+ setValue((v) => v.slice(0, -1));
200
+ } else if (input && !key.ctrl && !key.meta) {
201
+ setValue((v) => v + input);
202
+ }
203
+ });
204
+ return /* @__PURE__ */ jsxs(Text, { children: [
205
+ "*".repeat(value.length),
206
+ value.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter password" }) : ""
207
+ ] });
208
+ }
209
+
210
+ // src/ui/Dashboard.tsx
211
+ import { useState as useState2, useEffect as useEffect2, useRef } from "react";
212
+ import { Box as Box2, Text as Text2, useApp, useInput as useInput2, useStdout } from "ink";
213
+ import { Badge } from "@inkjs/ui";
214
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
215
+ function getColumns(width) {
216
+ if (width >= 120) return { id: 12, project: 20, status: 12, started: 12, model: 10, effort: 8, showExtras: true };
217
+ if (width >= 100) return { id: 10, project: 18, status: 12, started: 10, model: 10, effort: 8, showExtras: true };
218
+ if (width >= 80) return { id: 10, project: 16, status: 10, started: 10, model: 0, effort: 0, showExtras: false };
219
+ return { id: 8, project: 12, status: 10, started: 8, model: 0, effort: 0, showExtras: false };
220
+ }
221
+ var LOGO = [
222
+ "\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
223
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
224
+ "\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 ",
225
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 ",
226
+ "\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588"
227
+ ];
228
+ function Dashboard({ store, connection, overlay }) {
229
+ const { exit } = useApp();
230
+ const { stdout } = useStdout();
231
+ const termWidth = stdout?.columns ?? 80;
232
+ const termHeight = stdout?.rows ?? 24;
233
+ const connectionStatus = usePunkStore(store, (s) => s.connection);
234
+ const connectionError = usePunkStore(store, (s) => s.connectionError);
235
+ const deviceInfo = usePunkStore(store, (s) => s.deviceInfo);
236
+ const sessions = usePunkStore(store, (s) => s.sessions);
237
+ const activityLog = usePunkStore(store, (s) => s.activityLog);
238
+ const server = usePunkStore(store, (s) => s.server);
239
+ const connectedAt = usePunkStore(store, (s) => s.connectedAt);
240
+ const auth = usePunkStore(store, (s) => s.auth);
241
+ const sessionProjects = usePunkStore(store, (s) => s.sessionProjects);
242
+ const [focusMode, setFocusMode] = useState2(false);
243
+ const [focusedSessionIdx, setFocusedSessionIdx] = useState2(0);
244
+ const knownSessionIds = Object.keys(sessionProjects);
245
+ const focusedSessionId = focusMode ? knownSessionIds[focusedSessionIdx] : null;
246
+ const hasMultipleSessions = knownSessionIds.length > 1;
247
+ const filteredActivity = focusedSessionId ? activityLog.filter((e) => e.sessionId === focusedSessionId) : activityLog;
248
+ const [now, setNow] = useState2(Date.now());
249
+ useEffect2(() => {
250
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
251
+ return () => clearInterval(timer);
252
+ }, []);
253
+ const [selectedIdx, setSelectedIdx] = useState2(-1);
254
+ const [userScrolled, setUserScrolled] = useState2(false);
255
+ const prevLogLength = useRef(filteredActivity.length);
256
+ useEffect2(() => {
257
+ if (filteredActivity.length > prevLogLength.current && !userScrolled) {
258
+ setSelectedIdx(filteredActivity.length - 1);
259
+ }
260
+ prevLogLength.current = filteredActivity.length;
261
+ }, [filteredActivity.length, userScrolled]);
262
+ useEffect2(() => {
263
+ setSelectedIdx(filteredActivity.length - 1);
264
+ setUserScrolled(false);
265
+ }, [focusedSessionId]);
266
+ const [detailEntry, setDetailEntry] = useState2(null);
267
+ const effectiveIdx = selectedIdx === -1 ? filteredActivity.length - 1 : selectedIdx;
268
+ useInput2((input, key) => {
269
+ if (input === "q" || input === "c" && key.ctrl) {
270
+ connection?.disconnect();
271
+ exit();
272
+ }
273
+ if (input === "r") connection?.reconnect();
274
+ if (input === "c" && !key.ctrl) {
275
+ store.getState().clearActivity();
276
+ setSelectedIdx(-1);
277
+ setUserScrolled(false);
278
+ }
279
+ if (input === "l") {
280
+ connection?.disconnect();
281
+ clearAuth();
282
+ store.getState().setAuth(null);
283
+ store.getState().setDisconnected("logged out");
284
+ store.getState().addActivity({ icon: "\u25CF", message: "Logged out" });
285
+ store.getState().setScreen("login");
286
+ }
287
+ if (input === "g" && knownSessionIds.length > 0) {
288
+ setFocusMode((f) => !f);
289
+ setFocusedSessionIdx(0);
290
+ }
291
+ if (focusMode && key.leftArrow) {
292
+ setFocusedSessionIdx((i) => (i - 1 + knownSessionIds.length) % knownSessionIds.length);
293
+ }
294
+ if (focusMode && key.rightArrow) {
295
+ setFocusedSessionIdx((i) => (i + 1) % knownSessionIds.length);
296
+ }
297
+ if (key.return && !detailEntry && effectiveIdx >= 0 && effectiveIdx < filteredActivity.length) {
298
+ setDetailEntry(filteredActivity[effectiveIdx]);
299
+ }
300
+ if ((key.escape || key.return) && detailEntry) {
301
+ setDetailEntry(null);
302
+ }
303
+ if (!detailEntry && key.upArrow && filteredActivity.length > 0) {
304
+ setUserScrolled(true);
305
+ setSelectedIdx((prev) => Math.max(0, (prev === -1 ? filteredActivity.length - 1 : prev) - 1));
306
+ }
307
+ if (!detailEntry && key.downArrow && filteredActivity.length > 0) {
308
+ const next = selectedIdx + 1;
309
+ if (next >= filteredActivity.length - 1) {
310
+ setUserScrolled(false);
311
+ setSelectedIdx(filteredActivity.length - 1);
312
+ } else {
313
+ setSelectedIdx(next);
314
+ }
315
+ }
316
+ });
317
+ const sessionList = Object.values(sessions);
318
+ const uptime = connectedAt ? formatDuration(now - connectedAt) : "-";
319
+ const boxWidth = termWidth - 2;
320
+ const col = getColumns(termWidth);
321
+ const headerHeight = 8;
322
+ const sessionHeight = Math.max(sessionList.length, 1) + 5;
323
+ const footerHeight = 1;
324
+ const visibleCount = Math.max(termHeight - headerHeight - sessionHeight - footerHeight - 4, 4);
325
+ const tabBarHeight = focusMode ? 1 : 0;
326
+ const adjustedVisibleCount = visibleCount - tabBarHeight;
327
+ let viewStart = Math.max(0, filteredActivity.length - adjustedVisibleCount);
328
+ if (effectiveIdx < viewStart) viewStart = effectiveIdx;
329
+ if (effectiveIdx >= viewStart + adjustedVisibleCount) viewStart = effectiveIdx - adjustedVisibleCount + 1;
330
+ viewStart = Math.max(0, viewStart);
331
+ const visibleActivity = filteredActivity.slice(viewStart, viewStart + adjustedVisibleCount);
332
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termWidth, height: termHeight, children: [
333
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", justifyContent: "space-between", width: termWidth - 2, paddingX: 1, children: [
334
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
335
+ auth && /* @__PURE__ */ jsxs2(Box2, { children: [
336
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "User: ".padEnd(12) }),
337
+ /* @__PURE__ */ jsx2(Text2, { children: auth.email })
338
+ ] }),
339
+ /* @__PURE__ */ jsxs2(Box2, { children: [
340
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Server: ".padEnd(12) }),
341
+ /* @__PURE__ */ jsx2(Text2, { children: stripProtocol(server) })
342
+ ] }),
343
+ /* @__PURE__ */ jsxs2(Box2, { children: [
344
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: ".padEnd(12) }),
345
+ /* @__PURE__ */ jsx2(StatusIndicator, { status: connectionStatus, error: connectionError })
346
+ ] }),
347
+ /* @__PURE__ */ jsxs2(Box2, { children: [
348
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Uptime: ".padEnd(12) }),
349
+ /* @__PURE__ */ jsx2(Text2, { children: uptime })
350
+ ] }),
351
+ deviceInfo && /* @__PURE__ */ jsxs2(Box2, { children: [
352
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Device: ".padEnd(12) }),
353
+ /* @__PURE__ */ jsx2(Text2, { children: deviceInfo.name })
354
+ ] }),
355
+ termWidth >= 80 && deviceInfo && /* @__PURE__ */ jsxs2(Box2, { children: [
356
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Platform: ".padEnd(12) }),
357
+ /* @__PURE__ */ jsxs2(Text2, { children: [
358
+ deviceInfo.platform,
359
+ " ",
360
+ deviceInfo.arch,
361
+ " ",
362
+ deviceInfo.memoryGB,
363
+ "GB"
364
+ ] })
365
+ ] }),
366
+ termWidth >= 80 && deviceInfo?.battery && /* @__PURE__ */ jsxs2(Box2, { children: [
367
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Battery: ".padEnd(12) }),
368
+ /* @__PURE__ */ jsxs2(Text2, { children: [
369
+ deviceInfo.battery.level,
370
+ "%",
371
+ deviceInfo.battery.charging ? " \u26A1" : ""
372
+ ] })
373
+ ] }),
374
+ termWidth >= 80 && deviceInfo?.claudeCodeVersion && /* @__PURE__ */ jsxs2(Box2, { children: [
375
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Claude: ".padEnd(12) }),
376
+ /* @__PURE__ */ jsx2(Text2, { color: "#E07B39", children: deviceInfo.claudeCodeVersion })
377
+ ] }),
378
+ termWidth >= 80 && deviceInfo && deviceInfo.platform === "darwin" && /* @__PURE__ */ jsxs2(Box2, { children: [
379
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Sleep: ".padEnd(12) }),
380
+ /* @__PURE__ */ jsx2(Text2, { color: theme.success, children: "caffeinate active" })
381
+ ] })
382
+ ] }),
383
+ termWidth >= 80 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: termWidth >= 100 ? /* @__PURE__ */ jsxs2(Fragment, { children: [
384
+ /* @__PURE__ */ jsxs2(Box2, { children: [
385
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<q> ".padEnd(10) }),
386
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Quit" })
387
+ ] }),
388
+ /* @__PURE__ */ jsxs2(Box2, { children: [
389
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<r> ".padEnd(10) }),
390
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reconnect" })
391
+ ] }),
392
+ /* @__PURE__ */ jsxs2(Box2, { children: [
393
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<c> ".padEnd(10) }),
394
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Clear Activity" })
395
+ ] }),
396
+ /* @__PURE__ */ jsxs2(Box2, { children: [
397
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<g> ".padEnd(10) }),
398
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Group Activity" })
399
+ ] }),
400
+ /* @__PURE__ */ jsxs2(Box2, { children: [
401
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<l> ".padEnd(10) }),
402
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Logout" })
403
+ ] }),
404
+ /* @__PURE__ */ jsxs2(Box2, { children: [
405
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<enter> ".padEnd(10) }),
406
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Activity Details" })
407
+ ] })
408
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
409
+ /* @__PURE__ */ jsxs2(Text2, { children: [
410
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "q" }),
411
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " quit " }),
412
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "r" }),
413
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " reconnect" })
414
+ ] }),
415
+ /* @__PURE__ */ jsxs2(Text2, { children: [
416
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "c" }),
417
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " clear " }),
418
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "g" }),
419
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " group" })
420
+ ] }),
421
+ /* @__PURE__ */ jsxs2(Text2, { children: [
422
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "l" }),
423
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " logout " }),
424
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "enter" }),
425
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " details" })
426
+ ] })
427
+ ] }) }),
428
+ termWidth >= 120 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", alignItems: "flex-end", justifyContent: "center", children: LOGO.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: theme.logo, children: line }, i)) })
429
+ ] }),
430
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginX: 1, marginTop: 1, children: [
431
+ buildTopBorderJsx(`Sessions (${sessionList.length})`, boxWidth),
432
+ /* @__PURE__ */ jsxs2(
433
+ Box2,
434
+ {
435
+ flexDirection: "column",
436
+ borderStyle: "single",
437
+ borderColor: theme.border,
438
+ borderTop: false,
439
+ paddingX: 1,
440
+ paddingY: 1,
441
+ children: [
442
+ /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
443
+ "ID".padEnd(col.id),
444
+ "PROJECT".padEnd(col.project),
445
+ "STATUS".padEnd(col.status),
446
+ "STARTED".padEnd(col.started),
447
+ col.showExtras ? "MODEL".padEnd(col.model) + "EFFORT" : ""
448
+ ] }) }),
449
+ sessionList.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
450
+ "\u25CB",
451
+ " Waiting for sessions from the Punk app..."
452
+ ] }) : sessionList.map((session) => /* @__PURE__ */ jsx2(SessionRow, { session, now, col }, session.requestId))
453
+ ]
454
+ }
455
+ )
456
+ ] }),
457
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginX: 1, flexGrow: 1, children: [
458
+ buildTopBorderJsx(
459
+ overlay ? "Login" : focusMode && focusedSessionId ? `Activity: ${sessionProjects[focusedSessionId]?.project ?? focusedSessionId.slice(0, 8)}` : filteredActivity.length > adjustedVisibleCount ? `Activity (${effectiveIdx + 1}/${filteredActivity.length})` : "Activity",
460
+ boxWidth
461
+ ),
462
+ /* @__PURE__ */ jsx2(
463
+ Box2,
464
+ {
465
+ flexDirection: "column",
466
+ borderStyle: "single",
467
+ borderColor: theme.border,
468
+ borderTop: false,
469
+ paddingX: 1,
470
+ flexGrow: 1,
471
+ alignItems: overlay ? "center" : void 0,
472
+ justifyContent: overlay ? "center" : void 0,
473
+ children: overlay ?? (detailEntry ? (
474
+ /* Detail view */
475
+ /* @__PURE__ */ jsx2(DetailView, { entry: detailEntry, sessionProjects })
476
+ ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
477
+ focusMode && knownSessionIds.length > 0 && /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, gap: 1, children: [
478
+ knownSessionIds.map((sid, i) => {
479
+ const sp = sessionProjects[sid];
480
+ const isFocused = i === focusedSessionIdx;
481
+ return /* @__PURE__ */ jsx2(Text2, { children: isFocused ? /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
482
+ "[",
483
+ sp?.project ?? sid.slice(0, 8),
484
+ ":",
485
+ sid.slice(0, 8),
486
+ "]"
487
+ ] }) : /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
488
+ " ",
489
+ sp?.project ?? sid.slice(0, 8),
490
+ ":",
491
+ sid.slice(0, 8),
492
+ " "
493
+ ] }) }, sid);
494
+ }),
495
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
496
+ " ",
497
+ "\u2190",
498
+ "/",
499
+ "\u2192",
500
+ " switch g all"
501
+ ] })
502
+ ] }),
503
+ visibleActivity.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Waiting for events..." }) : visibleActivity.map((entry, i) => /* @__PURE__ */ jsx2(
504
+ ActivityRow,
505
+ {
506
+ entry,
507
+ termWidth,
508
+ isSelected: viewStart + i === effectiveIdx,
509
+ sessionProjects: hasMultipleSessions && !focusMode ? sessionProjects : void 0
510
+ },
511
+ `${entry.timestamp}-${i}`
512
+ ))
513
+ ] }))
514
+ }
515
+ )
516
+ ] }),
517
+ /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, justifyContent: "space-between", width: termWidth - 2, children: [
518
+ /* @__PURE__ */ jsx2(Box2, { gap: 2, children: detailEntry ? /* @__PURE__ */ jsx2(Fragment, { children: /* @__PURE__ */ jsxs2(Text2, { children: [
519
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "esc" }),
520
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " close" })
521
+ ] }) }) : overlay ? /* @__PURE__ */ jsxs2(Fragment, { children: [
522
+ /* @__PURE__ */ jsxs2(Text2, { children: [
523
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "e" }),
524
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " email" })
525
+ ] }),
526
+ /* @__PURE__ */ jsxs2(Text2, { children: [
527
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "q" }),
528
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " quit" })
529
+ ] })
530
+ ] }) : focusMode ? /* @__PURE__ */ jsxs2(Fragment, { children: [
531
+ /* @__PURE__ */ jsxs2(Text2, { children: [
532
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "\u2190\u2192" }),
533
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " switch" })
534
+ ] }),
535
+ /* @__PURE__ */ jsxs2(Text2, { children: [
536
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "g" }),
537
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " all" })
538
+ ] }),
539
+ /* @__PURE__ */ jsxs2(Text2, { children: [
540
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "\u2191\u2193" }),
541
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " scroll" })
542
+ ] }),
543
+ /* @__PURE__ */ jsxs2(Text2, { children: [
544
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "enter" }),
545
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " details" })
546
+ ] }),
547
+ /* @__PURE__ */ jsxs2(Text2, { children: [
548
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "q" }),
549
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " quit" })
550
+ ] })
551
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
552
+ /* @__PURE__ */ jsxs2(Text2, { children: [
553
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "\u2191\u2193" }),
554
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " scroll" })
555
+ ] }),
556
+ /* @__PURE__ */ jsxs2(Text2, { children: [
557
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "enter" }),
558
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " details" })
559
+ ] }),
560
+ /* @__PURE__ */ jsxs2(Text2, { children: [
561
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "g" }),
562
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " group" })
563
+ ] }),
564
+ /* @__PURE__ */ jsxs2(Text2, { children: [
565
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "c" }),
566
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " clear" })
567
+ ] }),
568
+ /* @__PURE__ */ jsxs2(Text2, { children: [
569
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "r" }),
570
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " reconnect" })
571
+ ] }),
572
+ /* @__PURE__ */ jsxs2(Text2, { children: [
573
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "l" }),
574
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " logout" })
575
+ ] }),
576
+ /* @__PURE__ */ jsxs2(Text2, { children: [
577
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "q" }),
578
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " quit" })
579
+ ] })
580
+ ] }) }),
581
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
582
+ "punk v",
583
+ version
584
+ ] })
585
+ ] })
586
+ ] });
587
+ }
588
+ function StatusIndicator({ status, error }) {
589
+ if (status === "connected") {
590
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.success, bold: true, children: [
591
+ "\u25CF",
592
+ " Connected"
593
+ ] });
594
+ }
595
+ if (status === "connecting") {
596
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
597
+ "\u25CF",
598
+ " Connecting..."
599
+ ] });
600
+ }
601
+ if (status === "error") {
602
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
603
+ "\u25CF",
604
+ " ",
605
+ error ?? "Error"
606
+ ] });
607
+ }
608
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
609
+ "\u25CF",
610
+ " Disconnected"
611
+ ] });
612
+ }
613
+ function SessionRow({ session, now, col }) {
614
+ const id = session.requestId.slice(0, col.id - 2);
615
+ const project = session.workingDirectory ? session.workingDirectory.split("/").pop() ?? "" : "";
616
+ const started = formatDuration(now - session.startedAt);
617
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
618
+ /* @__PURE__ */ jsxs2(Text2, { children: [
619
+ id.padEnd(col.id),
620
+ project.padEnd(col.project)
621
+ ] }),
622
+ /* @__PURE__ */ jsx2(Box2, { width: col.status, children: session.status === "running" ? /* @__PURE__ */ jsx2(Badge, { color: "green", children: "RUNNING" }) : session.status === "error" ? /* @__PURE__ */ jsx2(Badge, { color: "red", children: "ERROR" }) : /* @__PURE__ */ jsx2(Badge, { color: "yellow", children: "IDLE" }) }),
623
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: started.padEnd(col.started) }),
624
+ col.showExtras && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: (session.model ?? "").padEnd(col.model) }),
625
+ col.showExtras && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: session.effort ?? "" })
626
+ ] });
627
+ }
628
+ function DetailView({ entry, sessionProjects }) {
629
+ const time = new Date(entry.timestamp).toLocaleTimeString("en-US", {
630
+ hour12: false,
631
+ hour: "2-digit",
632
+ minute: "2-digit",
633
+ second: "2-digit"
634
+ });
635
+ const date = new Date(entry.timestamp).toLocaleDateString("en-US", {
636
+ year: "numeric",
637
+ month: "short",
638
+ day: "numeric"
639
+ });
640
+ const sp = entry.sessionId ? sessionProjects[entry.sessionId] : null;
641
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingY: 1, children: [
642
+ /* @__PURE__ */ jsx2(Box2, { gap: 1, children: /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
643
+ entry.icon,
644
+ " ",
645
+ entry.message
646
+ ] }) }),
647
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
648
+ /* @__PURE__ */ jsxs2(Box2, { children: [
649
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Time: ".padEnd(12) }),
650
+ /* @__PURE__ */ jsxs2(Text2, { children: [
651
+ date,
652
+ " ",
653
+ time
654
+ ] })
655
+ ] }),
656
+ entry.sessionId && /* @__PURE__ */ jsxs2(Box2, { children: [
657
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Session: ".padEnd(12) }),
658
+ /* @__PURE__ */ jsx2(Text2, { children: entry.sessionId })
659
+ ] }),
660
+ sp && /* @__PURE__ */ jsxs2(Box2, { children: [
661
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Project: ".padEnd(12) }),
662
+ /* @__PURE__ */ jsx2(Text2, { children: sp.project })
663
+ ] }),
664
+ entry.data && Object.entries(entry.data).map(([key, value]) => {
665
+ if (value == null) return null;
666
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
667
+ if (!strValue) return null;
668
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
669
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: (key + ":").padEnd(12) }),
670
+ /* @__PURE__ */ jsx2(Text2, { children: strValue })
671
+ ] }, key);
672
+ })
673
+ ] });
674
+ }
675
+ function ActivityRow({ entry, termWidth, isSelected, sessionProjects }) {
676
+ const time = new Date(entry.timestamp).toLocaleTimeString("en-US", {
677
+ hour12: false,
678
+ hour: "2-digit",
679
+ minute: "2-digit",
680
+ second: "2-digit"
681
+ });
682
+ const iconColor = entry.icon === "\u2713" ? theme.success : entry.icon === "\u2717" ? theme.error : entry.icon === "\u25B6" ? theme.primary : entry.icon === "\u27F3" ? theme.warning : "white";
683
+ const sessionStr = entry.sessionId ? entry.sessionId.slice(0, 8) : "";
684
+ const detailStr = entry.detail ?? "";
685
+ const marker = isSelected ? "\u25B8 " : " ";
686
+ const sp = sessionProjects && entry.sessionId ? sessionProjects[entry.sessionId] : null;
687
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
688
+ /* @__PURE__ */ jsx2(Text2, { color: isSelected ? theme.primary : void 0, bold: isSelected, children: marker }),
689
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: !isSelected, children: [
690
+ time,
691
+ " "
692
+ ] }),
693
+ sp && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: `${sp.project.slice(0, 8)}:${sessionStr}`.padEnd(18) }),
694
+ /* @__PURE__ */ jsxs2(Text2, { color: iconColor, bold: true, children: [
695
+ entry.icon,
696
+ " "
697
+ ] }),
698
+ /* @__PURE__ */ jsx2(Text2, { bold: isSelected, children: entry.message }),
699
+ !sp && termWidth >= 80 && sessionStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: !isSelected, children: [
700
+ " ",
701
+ sessionStr
702
+ ] }),
703
+ termWidth >= 80 && detailStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: !isSelected, children: [
704
+ " ",
705
+ detailStr
706
+ ] })
707
+ ] });
708
+ }
709
+ function buildTopBorderJsx(title, width, borderColor) {
710
+ const prefix = "\u250C\u2500 ";
711
+ const suffix = " ";
712
+ const end = "\u2510";
713
+ const used = prefix.length + title.length + suffix.length + end.length;
714
+ const fill = Math.max(width - used, 0);
715
+ const lineColor = borderColor ?? theme.border;
716
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
717
+ /* @__PURE__ */ jsx2(Text2, { color: lineColor, children: prefix }),
718
+ /* @__PURE__ */ jsx2(Text2, { color: theme.sectionTitle, bold: true, children: title }),
719
+ /* @__PURE__ */ jsxs2(Text2, { color: lineColor, children: [
720
+ suffix,
721
+ "\u2500".repeat(fill),
722
+ end
723
+ ] })
724
+ ] });
725
+ }
726
+ function formatDuration(ms) {
727
+ const seconds = Math.floor(ms / 1e3);
728
+ if (seconds < 60) return `${seconds}s`;
729
+ const minutes = Math.floor(seconds / 60);
730
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
731
+ const hours = Math.floor(minutes / 60);
732
+ return `${hours}h ${minutes % 60}m`;
733
+ }
734
+ function stripProtocol(url) {
735
+ return url.replace(/^https?:\/\//, "");
736
+ }
737
+
738
+ // src/ui/App.tsx
739
+ import { jsx as jsx3 } from "react/jsx-runtime";
740
+ function App({ server, idToken, options }) {
741
+ const [store] = useState3(() => {
742
+ const s = createPunkStore(server);
743
+ const auth = loadAuth();
744
+ if (auth || idToken) {
745
+ if (auth) s.getState().setAuth({ email: auth.email, uid: auth.uid });
746
+ s.getState().setScreen("connecting");
747
+ }
748
+ return s;
749
+ });
750
+ const connectionRef = useRef2(null);
751
+ const screen = usePunkStore(store, (s) => s.screen);
752
+ useEffect3(() => {
753
+ if (screen !== "connecting") return;
754
+ if (connectionRef.current) {
755
+ connectionRef.current.disconnect();
756
+ connectionRef.current = null;
757
+ }
758
+ const tokenPromise = idToken ? Promise.resolve(idToken) : refreshIdToken();
759
+ tokenPromise.then((token) => {
760
+ connectionRef.current = new PunkConnection(server, token, options, store);
761
+ store.getState().setScreen("dashboard");
762
+ }).catch((err) => {
763
+ store.getState().setError(err instanceof Error ? err.message : String(err));
764
+ store.getState().addActivity({ icon: "\u2717", message: "Auth failed, please log in" });
765
+ store.getState().setScreen("login");
766
+ });
767
+ }, [screen, server, idToken, options, store]);
768
+ useEffect3(() => {
769
+ return () => {
770
+ connectionRef.current?.disconnect();
771
+ };
772
+ }, []);
773
+ const loginOverlay = screen === "login" ? /* @__PURE__ */ jsx3(LoginDialog, { store, server }) : void 0;
774
+ return /* @__PURE__ */ jsx3(
775
+ Dashboard,
776
+ {
777
+ store,
778
+ connection: connectionRef.current,
779
+ overlay: loginOverlay
780
+ }
781
+ );
782
+ }
783
+ export {
784
+ App
785
+ };