@punkcode/cli 0.1.16 → 0.1.17

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,566 @@
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-5W6T4TEF.js";
14
+
15
+ // src/ui/App.tsx
16
+ import { useEffect as useEffect3, useRef, 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 } 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 [now, setNow] = useState2(Date.now());
242
+ useEffect2(() => {
243
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
244
+ return () => clearInterval(timer);
245
+ }, []);
246
+ useInput2((input, key) => {
247
+ if (input === "q" || input === "c" && key.ctrl) {
248
+ connection?.disconnect();
249
+ exit();
250
+ }
251
+ if (input === "r") connection?.reconnect();
252
+ if (input === "c" && !key.ctrl) store.getState().clearActivity();
253
+ if (input === "l") {
254
+ connection?.disconnect();
255
+ clearAuth();
256
+ store.getState().setAuth(null);
257
+ store.getState().setDisconnected("logged out");
258
+ store.getState().addActivity({ icon: "\u25CF", message: "Logged out" });
259
+ store.getState().setScreen("login");
260
+ }
261
+ });
262
+ const sessionList = Object.values(sessions);
263
+ const uptime = connectedAt ? formatDuration(now - connectedAt) : "-";
264
+ const boxWidth = termWidth - 2;
265
+ const col = getColumns(termWidth);
266
+ const headerHeight = 8;
267
+ const sessionHeight = Math.max(sessionList.length, 1) + 5;
268
+ const footerHeight = 1;
269
+ const activityHeight = Math.max(termHeight - headerHeight - sessionHeight - footerHeight - 4, 4);
270
+ const visibleActivity = activityLog.slice(-activityHeight);
271
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termWidth, height: termHeight, children: [
272
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", justifyContent: "space-between", width: termWidth - 2, paddingX: 1, children: [
273
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
274
+ auth && /* @__PURE__ */ jsxs2(Box2, { children: [
275
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "User: ".padEnd(12) }),
276
+ /* @__PURE__ */ jsx2(Text2, { children: auth.email })
277
+ ] }),
278
+ /* @__PURE__ */ jsxs2(Box2, { children: [
279
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Server: ".padEnd(12) }),
280
+ /* @__PURE__ */ jsx2(Text2, { children: stripProtocol(server) })
281
+ ] }),
282
+ /* @__PURE__ */ jsxs2(Box2, { children: [
283
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: ".padEnd(12) }),
284
+ /* @__PURE__ */ jsx2(StatusIndicator, { status: connectionStatus, error: connectionError })
285
+ ] }),
286
+ /* @__PURE__ */ jsxs2(Box2, { children: [
287
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Uptime: ".padEnd(12) }),
288
+ /* @__PURE__ */ jsx2(Text2, { children: uptime })
289
+ ] }),
290
+ deviceInfo && /* @__PURE__ */ jsxs2(Box2, { children: [
291
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Device: ".padEnd(12) }),
292
+ /* @__PURE__ */ jsx2(Text2, { children: deviceInfo.name })
293
+ ] }),
294
+ termWidth >= 80 && deviceInfo && /* @__PURE__ */ jsxs2(Box2, { children: [
295
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Platform: ".padEnd(12) }),
296
+ /* @__PURE__ */ jsxs2(Text2, { children: [
297
+ deviceInfo.platform,
298
+ " ",
299
+ deviceInfo.arch,
300
+ " ",
301
+ deviceInfo.memoryGB,
302
+ "GB"
303
+ ] })
304
+ ] }),
305
+ termWidth >= 80 && deviceInfo?.battery && /* @__PURE__ */ jsxs2(Box2, { children: [
306
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Battery: ".padEnd(12) }),
307
+ /* @__PURE__ */ jsxs2(Text2, { children: [
308
+ deviceInfo.battery.level,
309
+ "%",
310
+ deviceInfo.battery.charging ? " \u26A1" : ""
311
+ ] })
312
+ ] }),
313
+ termWidth >= 80 && deviceInfo?.claudeCodeVersion && /* @__PURE__ */ jsxs2(Box2, { children: [
314
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Claude: ".padEnd(12) }),
315
+ /* @__PURE__ */ jsx2(Text2, { color: "#E07B39", children: deviceInfo.claudeCodeVersion })
316
+ ] }),
317
+ termWidth >= 80 && deviceInfo && deviceInfo.platform === "darwin" && /* @__PURE__ */ jsxs2(Box2, { children: [
318
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Sleep: ".padEnd(12) }),
319
+ /* @__PURE__ */ jsx2(Text2, { color: theme.success, children: "caffeinate active" })
320
+ ] })
321
+ ] }),
322
+ termWidth >= 80 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: termWidth >= 100 ? /* @__PURE__ */ jsxs2(Fragment, { children: [
323
+ /* @__PURE__ */ jsxs2(Box2, { children: [
324
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<q> ".padEnd(10) }),
325
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Quit" })
326
+ ] }),
327
+ /* @__PURE__ */ jsxs2(Box2, { children: [
328
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<r> ".padEnd(10) }),
329
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reconnect" })
330
+ ] }),
331
+ /* @__PURE__ */ jsxs2(Box2, { children: [
332
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<c> ".padEnd(10) }),
333
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Clear Log" })
334
+ ] }),
335
+ /* @__PURE__ */ jsxs2(Box2, { children: [
336
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<l> ".padEnd(10) }),
337
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Logout" })
338
+ ] }),
339
+ /* @__PURE__ */ jsxs2(Box2, { children: [
340
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "<ctrl-c>".padEnd(10) }),
341
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Force Quit" })
342
+ ] })
343
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
344
+ /* @__PURE__ */ jsxs2(Text2, { children: [
345
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "q" }),
346
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " quit " }),
347
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "r" }),
348
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " reconnect" })
349
+ ] }),
350
+ /* @__PURE__ */ jsxs2(Text2, { children: [
351
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "c" }),
352
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " clear " }),
353
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: "l" }),
354
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " logout" })
355
+ ] })
356
+ ] }) }),
357
+ 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)) })
358
+ ] }),
359
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginX: 1, marginTop: 1, children: [
360
+ buildTopBorderJsx(`Sessions (${sessionList.length})`, boxWidth),
361
+ /* @__PURE__ */ jsxs2(
362
+ Box2,
363
+ {
364
+ flexDirection: "column",
365
+ borderStyle: "single",
366
+ borderColor: theme.border,
367
+ borderTop: false,
368
+ paddingX: 1,
369
+ paddingY: 1,
370
+ children: [
371
+ /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
372
+ "ID".padEnd(col.id),
373
+ "PROJECT".padEnd(col.project),
374
+ "STATUS".padEnd(col.status),
375
+ "STARTED".padEnd(col.started),
376
+ col.showExtras ? "MODEL".padEnd(col.model) + "EFFORT" : ""
377
+ ] }) }),
378
+ sessionList.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
379
+ "\u25CB",
380
+ " Waiting for sessions from the Punk app..."
381
+ ] }) : sessionList.map((session) => /* @__PURE__ */ jsx2(SessionRow, { session, now, col }, session.requestId))
382
+ ]
383
+ }
384
+ )
385
+ ] }),
386
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginX: 1, flexGrow: 1, children: [
387
+ buildTopBorderJsx(overlay ? "Login" : "Activity", boxWidth),
388
+ /* @__PURE__ */ jsx2(
389
+ Box2,
390
+ {
391
+ flexDirection: "column",
392
+ borderStyle: "single",
393
+ borderColor: theme.border,
394
+ borderTop: false,
395
+ paddingX: 1,
396
+ flexGrow: 1,
397
+ alignItems: overlay ? "center" : void 0,
398
+ justifyContent: overlay ? "center" : void 0,
399
+ children: overlay ?? (visibleActivity.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Waiting for events..." }) : visibleActivity.map((entry, i) => /* @__PURE__ */ jsx2(ActivityRow, { entry, termWidth }, `${entry.timestamp}-${i}`)))
400
+ }
401
+ )
402
+ ] }),
403
+ /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, justifyContent: "space-between", width: termWidth - 2, children: [
404
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
405
+ "punk v",
406
+ version
407
+ ] }),
408
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
409
+ sessionList.length,
410
+ " session",
411
+ sessionList.length !== 1 ? "s" : "",
412
+ " ",
413
+ "\xB7",
414
+ " ",
415
+ stripProtocol(server)
416
+ ] })
417
+ ] })
418
+ ] });
419
+ }
420
+ function StatusIndicator({ status, error }) {
421
+ if (status === "connected") {
422
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.success, bold: true, children: [
423
+ "\u25CF",
424
+ " Connected"
425
+ ] });
426
+ }
427
+ if (status === "connecting") {
428
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.warning, children: [
429
+ "\u25CF",
430
+ " Connecting..."
431
+ ] });
432
+ }
433
+ if (status === "error") {
434
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
435
+ "\u25CF",
436
+ " ",
437
+ error ?? "Error"
438
+ ] });
439
+ }
440
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.error, children: [
441
+ "\u25CF",
442
+ " Disconnected"
443
+ ] });
444
+ }
445
+ function SessionRow({ session, now, col }) {
446
+ const id = session.requestId.slice(0, col.id - 2);
447
+ const project = session.workingDirectory ? session.workingDirectory.split("/").pop() ?? "" : "";
448
+ const started = formatDuration(now - session.startedAt);
449
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
450
+ /* @__PURE__ */ jsxs2(Text2, { children: [
451
+ id.padEnd(col.id),
452
+ project.padEnd(col.project)
453
+ ] }),
454
+ /* @__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" }) }),
455
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: started.padEnd(col.started) }),
456
+ col.showExtras && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: (session.model ?? "").padEnd(col.model) }),
457
+ col.showExtras && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: session.effort ?? "" })
458
+ ] });
459
+ }
460
+ function ActivityRow({ entry, termWidth }) {
461
+ const time = new Date(entry.timestamp).toLocaleTimeString("en-US", {
462
+ hour12: false,
463
+ hour: "2-digit",
464
+ minute: "2-digit",
465
+ second: "2-digit"
466
+ });
467
+ const iconColor = entry.icon === "\u2713" ? theme.success : entry.icon === "\u2717" ? theme.error : entry.icon === "\u25B6" ? theme.primary : entry.icon === "\u27F3" ? theme.warning : "white";
468
+ const sessionStr = entry.sessionId ? entry.sessionId.slice(0, 8) : "";
469
+ const detailStr = entry.detail ?? "";
470
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
471
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
472
+ time,
473
+ " "
474
+ ] }),
475
+ /* @__PURE__ */ jsxs2(Text2, { color: iconColor, bold: true, children: [
476
+ entry.icon,
477
+ " "
478
+ ] }),
479
+ /* @__PURE__ */ jsx2(Text2, { children: entry.message }),
480
+ termWidth >= 80 && sessionStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
481
+ " ",
482
+ sessionStr
483
+ ] }),
484
+ termWidth >= 80 && detailStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
485
+ " ",
486
+ detailStr
487
+ ] })
488
+ ] });
489
+ }
490
+ function buildTopBorderJsx(title, width, borderColor) {
491
+ const prefix = "\u250C\u2500 ";
492
+ const suffix = " ";
493
+ const end = "\u2510";
494
+ const used = prefix.length + title.length + suffix.length + end.length;
495
+ const fill = Math.max(width - used, 0);
496
+ const lineColor = borderColor ?? theme.border;
497
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
498
+ /* @__PURE__ */ jsx2(Text2, { color: lineColor, children: prefix }),
499
+ /* @__PURE__ */ jsx2(Text2, { color: theme.sectionTitle, bold: true, children: title }),
500
+ /* @__PURE__ */ jsxs2(Text2, { color: lineColor, children: [
501
+ suffix,
502
+ "\u2500".repeat(fill),
503
+ end
504
+ ] })
505
+ ] });
506
+ }
507
+ function formatDuration(ms) {
508
+ const seconds = Math.floor(ms / 1e3);
509
+ if (seconds < 60) return `${seconds}s`;
510
+ const minutes = Math.floor(seconds / 60);
511
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
512
+ const hours = Math.floor(minutes / 60);
513
+ return `${hours}h ${minutes % 60}m`;
514
+ }
515
+ function stripProtocol(url) {
516
+ return url.replace(/^https?:\/\//, "");
517
+ }
518
+
519
+ // src/ui/App.tsx
520
+ import { jsx as jsx3 } from "react/jsx-runtime";
521
+ function App({ server, idToken, options }) {
522
+ const [store] = useState3(() => {
523
+ const s = createPunkStore(server);
524
+ const auth = loadAuth();
525
+ if (auth || idToken) {
526
+ if (auth) s.getState().setAuth({ email: auth.email, uid: auth.uid });
527
+ s.getState().setScreen("connecting");
528
+ }
529
+ return s;
530
+ });
531
+ const connectionRef = useRef(null);
532
+ const screen = usePunkStore(store, (s) => s.screen);
533
+ useEffect3(() => {
534
+ if (screen !== "connecting") return;
535
+ if (connectionRef.current) {
536
+ connectionRef.current.disconnect();
537
+ connectionRef.current = null;
538
+ }
539
+ const tokenPromise = idToken ? Promise.resolve(idToken) : refreshIdToken();
540
+ tokenPromise.then((token) => {
541
+ connectionRef.current = new PunkConnection(server, token, options, store);
542
+ store.getState().setScreen("dashboard");
543
+ }).catch((err) => {
544
+ store.getState().setError(err instanceof Error ? err.message : String(err));
545
+ store.getState().addActivity({ icon: "\u2717", message: "Auth failed, please log in" });
546
+ store.getState().setScreen("login");
547
+ });
548
+ }, [screen, server, idToken, options, store]);
549
+ useEffect3(() => {
550
+ return () => {
551
+ connectionRef.current?.disconnect();
552
+ };
553
+ }, []);
554
+ const loginOverlay = screen === "login" ? /* @__PURE__ */ jsx3(LoginDialog, { store, server }) : void 0;
555
+ return /* @__PURE__ */ jsx3(
556
+ Dashboard,
557
+ {
558
+ store,
559
+ connection: connectionRef.current,
560
+ overlay: loginOverlay
561
+ }
562
+ );
563
+ }
564
+ export {
565
+ App
566
+ };