@rigkit/provider-freestyle 0.2.3 → 0.2.4
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.md +9 -3
- package/package.json +8 -5
- package/src/host-auth.test.ts +375 -0
- package/src/host-auth.ts +591 -0
- package/src/index.ts +37 -59
- package/src/provider.test.ts +171 -113
- package/src/provider.ts +106 -380
- package/src/terminal-session.test.ts +42 -2
- package/src/terminal-session.ts +633 -308
- package/src/version.ts +1 -1
package/src/terminal-session.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type { ProviderInteractionSession } from "@rigkit/engine";
|
|
|
4
4
|
export type FreestyleTerminalSessionRequest = {
|
|
5
5
|
title: string;
|
|
6
6
|
command: string;
|
|
7
|
+
displayCommand?: string;
|
|
8
|
+
startupInput?: string;
|
|
7
9
|
remoteCommand?: string;
|
|
10
|
+
canFinishWhileRunning?: boolean;
|
|
8
11
|
instructions?: string;
|
|
9
12
|
nodePath?: string;
|
|
10
13
|
};
|
|
@@ -44,7 +47,10 @@ export function createFreestyleTerminalSession(
|
|
|
44
47
|
let fail!: (error: Error) => void;
|
|
45
48
|
const sockets = new Set<ServerWebSocket<SocketData>>();
|
|
46
49
|
const outputBuffer: string[] = [];
|
|
47
|
-
const
|
|
50
|
+
const startupCommand = request.startupInput ?? request.remoteCommand;
|
|
51
|
+
const startupInput = startupCommand ? ensureTrailingNewline(startupCommand) : undefined;
|
|
52
|
+
const displayCommand = request.displayCommand ?? request.remoteCommand ?? request.command;
|
|
53
|
+
const canFinishWhileRunning = canFinishWhileProcessRuns(request, startupInput);
|
|
48
54
|
|
|
49
55
|
const completed = new Promise<FreestyleTerminalSessionResult>((resolve, reject) => {
|
|
50
56
|
complete = resolve;
|
|
@@ -127,7 +133,7 @@ export function createFreestyleTerminalSession(
|
|
|
127
133
|
broadcast({
|
|
128
134
|
type: "status",
|
|
129
135
|
status: "Connected",
|
|
130
|
-
canFinish:
|
|
136
|
+
canFinish: canFinishWhileRunning,
|
|
131
137
|
});
|
|
132
138
|
|
|
133
139
|
proc = Bun.spawn(["sh", "-lc", `exec ${request.command}`], {
|
|
@@ -194,7 +200,7 @@ export function createFreestyleTerminalSession(
|
|
|
194
200
|
remoteCommandStarted = true;
|
|
195
201
|
broadcast({
|
|
196
202
|
type: "status",
|
|
197
|
-
status: `Running ${
|
|
203
|
+
status: `Running ${displayCommand}`,
|
|
198
204
|
canFinish: true,
|
|
199
205
|
});
|
|
200
206
|
}
|
|
@@ -250,10 +256,10 @@ export function createFreestyleTerminalSession(
|
|
|
250
256
|
return;
|
|
251
257
|
}
|
|
252
258
|
if (remoteCommandStarted) {
|
|
253
|
-
send(ws, { type: "status", status: `Running ${
|
|
259
|
+
send(ws, { type: "status", status: `Running ${displayCommand}`, canFinish: true });
|
|
254
260
|
return;
|
|
255
261
|
}
|
|
256
|
-
send(ws, { type: "status", status: proc ? "Connected" : "Starting", canFinish:
|
|
262
|
+
send(ws, { type: "status", status: proc ? "Connected" : "Starting", canFinish: canFinishWhileRunning });
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
function broadcast(message: ServerMessage): void {
|
|
@@ -314,177 +320,322 @@ function renderInteractionPage(
|
|
|
314
320
|
options: { completed?: boolean; startupInput?: string } = {},
|
|
315
321
|
): string {
|
|
316
322
|
const completed = options.completed ?? false;
|
|
317
|
-
const
|
|
318
|
-
const
|
|
323
|
+
const command = request.displayCommand ?? request.remoteCommand ?? request.command;
|
|
324
|
+
const node = request.nodePath ?? "provider";
|
|
325
|
+
const instructions = request.instructions ?? "";
|
|
326
|
+
|
|
327
|
+
const escapedDocTitle = escapeHtml(completed ? "Interactive task completed" : request.title);
|
|
319
328
|
const escapedLabel = escapeHtml(request.title);
|
|
320
|
-
const
|
|
321
|
-
const
|
|
329
|
+
const escapedCommand = escapeHtml(command);
|
|
330
|
+
const escapedInstructions = instructions ? escapeHtml(instructions) : "";
|
|
331
|
+
|
|
332
|
+
const titleLit = javaScriptLiteral(request.title);
|
|
333
|
+
const instructionsLit = javaScriptLiteral(instructions);
|
|
334
|
+
const nodeLit = javaScriptLiteral(node);
|
|
322
335
|
const startupInputLiteral = javaScriptLiteral(options.startupInput ?? null);
|
|
336
|
+
const canFinishWhileRunningLiteral = javaScriptLiteral(canFinishWhileProcessRuns(request, options.startupInput));
|
|
337
|
+
const initialCompletedLiteral = completed ? "true" : "false";
|
|
323
338
|
|
|
324
339
|
return `<!doctype html>
|
|
325
340
|
<html lang="en">
|
|
326
341
|
<head>
|
|
327
342
|
<meta charset="utf-8">
|
|
328
343
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
329
|
-
<title>${
|
|
344
|
+
<title>${escapedDocTitle}</title>
|
|
330
345
|
<style>
|
|
331
346
|
:root {
|
|
332
|
-
color-scheme:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
color-scheme: light;
|
|
348
|
+
--bg: #efece5;
|
|
349
|
+
--surface: #ffffff;
|
|
350
|
+
--fg: #0a0a0a;
|
|
351
|
+
--muted: #5a5a5a;
|
|
352
|
+
--dim: #8e8a80;
|
|
353
|
+
--border: #d8d2c5;
|
|
354
|
+
--border-strong: #b8b0a0;
|
|
355
|
+
--accent: #2d4df5;
|
|
356
|
+
--accent-soft: #e8ecff;
|
|
357
|
+
--ok: #0f9d58;
|
|
358
|
+
--err: #d93025;
|
|
359
|
+
--term-bg: #faf8f2;
|
|
360
|
+
--term-fg: #1a1a1a;
|
|
361
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
362
|
+
--sans: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
363
|
+
font-family: var(--sans);
|
|
364
|
+
color: var(--fg);
|
|
365
|
+
background: var(--bg);
|
|
336
366
|
}
|
|
367
|
+
* { box-sizing: border-box; }
|
|
337
368
|
body {
|
|
338
369
|
margin: 0;
|
|
339
|
-
height: 100vh;
|
|
340
|
-
padding: 24px;
|
|
341
|
-
display: grid;
|
|
342
|
-
place-items: center;
|
|
343
|
-
box-sizing: border-box;
|
|
370
|
+
min-height: 100vh;
|
|
344
371
|
overflow: hidden;
|
|
345
|
-
background:
|
|
346
|
-
|
|
347
|
-
|
|
372
|
+
background: var(--bg);
|
|
373
|
+
-webkit-font-smoothing: antialiased;
|
|
374
|
+
-moz-osx-font-smoothing: grayscale;
|
|
348
375
|
}
|
|
349
|
-
|
|
350
|
-
width: min(1120px, 100%);
|
|
351
|
-
height: min(760px, calc(100vh - 48px));
|
|
352
|
-
min-height: 420px;
|
|
376
|
+
#app {
|
|
353
377
|
display: grid;
|
|
354
|
-
grid-template-rows: auto
|
|
355
|
-
|
|
356
|
-
border: 1px solid #2b2b2f;
|
|
357
|
-
border-radius: 8px;
|
|
358
|
-
background: #0b0f14;
|
|
359
|
-
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
|
|
378
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
379
|
+
height: 100vh;
|
|
360
380
|
}
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
background: linear-gradient(#1c1c20, #17171a);
|
|
369
|
-
box-sizing: border-box;
|
|
381
|
+
.noscript-fallback {
|
|
382
|
+
padding: 32px;
|
|
383
|
+
color: var(--muted);
|
|
384
|
+
font-size: 14px;
|
|
385
|
+
line-height: 1.6;
|
|
386
|
+
max-width: 640px;
|
|
387
|
+
margin: 0 auto;
|
|
370
388
|
}
|
|
371
|
-
.
|
|
389
|
+
.app-header {
|
|
372
390
|
display: flex;
|
|
373
|
-
|
|
374
|
-
|
|
391
|
+
align-items: center;
|
|
392
|
+
padding: 18px 24px 14px;
|
|
375
393
|
}
|
|
376
|
-
.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
|
|
394
|
+
.brand {
|
|
395
|
+
display: inline-flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 10px;
|
|
398
|
+
color: var(--fg);
|
|
382
399
|
}
|
|
383
|
-
.
|
|
384
|
-
|
|
400
|
+
.brand-mark { width: 22px; height: 22px; color: var(--fg); flex: 0 0 auto; }
|
|
401
|
+
.brand-mark svg { width: 100%; height: 100%; display: block; }
|
|
402
|
+
.brand-wordmark {
|
|
403
|
+
font-family: var(--mono);
|
|
404
|
+
font-size: 15px;
|
|
405
|
+
font-weight: 500;
|
|
406
|
+
letter-spacing: -0.01em;
|
|
407
|
+
color: var(--fg);
|
|
385
408
|
}
|
|
386
|
-
.
|
|
387
|
-
|
|
409
|
+
.brand-node {
|
|
410
|
+
margin-left: 14px;
|
|
411
|
+
padding-left: 14px;
|
|
412
|
+
border-left: 1px solid var(--border);
|
|
413
|
+
color: var(--muted);
|
|
414
|
+
font-family: var(--mono);
|
|
415
|
+
font-size: 13px;
|
|
388
416
|
}
|
|
389
|
-
.
|
|
390
|
-
|
|
417
|
+
.workspace {
|
|
418
|
+
display: grid;
|
|
419
|
+
grid-template-columns: minmax(360px, 480px) minmax(0, 1fr);
|
|
420
|
+
gap: 22px;
|
|
421
|
+
padding: 0 24px 24px;
|
|
422
|
+
min-height: 0;
|
|
423
|
+
height: 100%;
|
|
391
424
|
}
|
|
392
|
-
.
|
|
425
|
+
.instructions-pane {
|
|
426
|
+
display: flex;
|
|
427
|
+
flex-direction: column;
|
|
428
|
+
gap: 20px;
|
|
393
429
|
min-width: 0;
|
|
394
|
-
|
|
430
|
+
padding: 18px 22px 22px;
|
|
431
|
+
overflow: auto;
|
|
432
|
+
user-select: text;
|
|
395
433
|
}
|
|
396
|
-
.
|
|
397
|
-
margin: 0
|
|
398
|
-
|
|
399
|
-
|
|
434
|
+
.eyebrow {
|
|
435
|
+
margin: 0;
|
|
436
|
+
align-self: flex-start;
|
|
437
|
+
display: inline-flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
padding: 6px 12px;
|
|
440
|
+
border: 1.5px solid var(--accent);
|
|
441
|
+
border-radius: 8px;
|
|
442
|
+
color: var(--accent);
|
|
443
|
+
font-size: 11px;
|
|
444
|
+
font-weight: 600;
|
|
445
|
+
letter-spacing: 0.12em;
|
|
446
|
+
text-transform: uppercase;
|
|
400
447
|
}
|
|
401
|
-
|
|
448
|
+
.task-title {
|
|
402
449
|
margin: 0;
|
|
450
|
+
font-size: 40px;
|
|
451
|
+
font-weight: 800;
|
|
452
|
+
letter-spacing: -0.035em;
|
|
453
|
+
line-height: 1.02;
|
|
454
|
+
color: var(--fg);
|
|
455
|
+
}
|
|
456
|
+
.instruction-text {
|
|
457
|
+
margin: 0;
|
|
458
|
+
white-space: pre-wrap;
|
|
459
|
+
color: #2a2a2a;
|
|
460
|
+
font-size: 15px;
|
|
461
|
+
line-height: 1.55;
|
|
462
|
+
}
|
|
463
|
+
.instruction-steps {
|
|
464
|
+
margin: 0;
|
|
465
|
+
padding: 0;
|
|
466
|
+
list-style: none;
|
|
467
|
+
counter-reset: step;
|
|
468
|
+
display: flex;
|
|
469
|
+
flex-direction: column;
|
|
470
|
+
gap: 10px;
|
|
471
|
+
}
|
|
472
|
+
.instruction-steps li {
|
|
473
|
+
counter-increment: step;
|
|
474
|
+
position: relative;
|
|
475
|
+
padding: 2px 0 2px 34px;
|
|
476
|
+
color: #2a2a2a;
|
|
477
|
+
font-size: 15px;
|
|
478
|
+
line-height: 1.5;
|
|
479
|
+
}
|
|
480
|
+
.instruction-steps li::before {
|
|
481
|
+
content: counter(step);
|
|
482
|
+
position: absolute;
|
|
483
|
+
left: 0;
|
|
484
|
+
top: 1px;
|
|
485
|
+
width: 22px;
|
|
486
|
+
height: 22px;
|
|
487
|
+
display: grid;
|
|
488
|
+
place-items: center;
|
|
489
|
+
border-radius: 999px;
|
|
490
|
+
border: 1.5px solid var(--accent);
|
|
491
|
+
color: var(--accent);
|
|
492
|
+
font-family: var(--mono);
|
|
493
|
+
font-size: 11px;
|
|
494
|
+
font-weight: 600;
|
|
495
|
+
line-height: 1;
|
|
496
|
+
}
|
|
497
|
+
.instructions-cta {
|
|
498
|
+
margin-top: auto;
|
|
499
|
+
padding-top: 12px;
|
|
500
|
+
display: flex;
|
|
501
|
+
flex-direction: column;
|
|
502
|
+
gap: 12px;
|
|
503
|
+
}
|
|
504
|
+
.primary-button {
|
|
505
|
+
display: inline-flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
justify-content: center;
|
|
508
|
+
gap: 10px;
|
|
509
|
+
width: 100%;
|
|
510
|
+
border: 0;
|
|
511
|
+
border-radius: 10px;
|
|
512
|
+
padding: 14px 18px;
|
|
513
|
+
font: inherit;
|
|
403
514
|
font-size: 14px;
|
|
404
|
-
line-height: 1.25;
|
|
405
515
|
font-weight: 600;
|
|
406
|
-
letter-spacing: 0;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
516
|
+
letter-spacing: -0.005em;
|
|
517
|
+
cursor: pointer;
|
|
518
|
+
color: #ffffff;
|
|
519
|
+
background: var(--fg);
|
|
520
|
+
transition: transform 0.12s ease, background 0.12s ease, opacity 0.12s ease;
|
|
410
521
|
}
|
|
411
|
-
.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
522
|
+
.primary-button:hover:not(:disabled) {
|
|
523
|
+
transform: translateY(-1px);
|
|
524
|
+
background: #1f1f1f;
|
|
525
|
+
}
|
|
526
|
+
.primary-button:active:not(:disabled) {
|
|
527
|
+
transform: translateY(0);
|
|
528
|
+
}
|
|
529
|
+
.primary-button:disabled {
|
|
530
|
+
cursor: not-allowed;
|
|
531
|
+
color: var(--dim);
|
|
532
|
+
background: var(--border);
|
|
417
533
|
}
|
|
418
|
-
.
|
|
534
|
+
.primary-button .check { width: 16px; height: 16px; display: inline-grid; place-items: center; }
|
|
535
|
+
.primary-button .check svg {
|
|
536
|
+
width: 16px;
|
|
537
|
+
height: 16px;
|
|
538
|
+
fill: none;
|
|
539
|
+
stroke: currentColor;
|
|
540
|
+
stroke-width: 2.4;
|
|
541
|
+
stroke-linecap: round;
|
|
542
|
+
stroke-linejoin: round;
|
|
543
|
+
}
|
|
544
|
+
.cta-hint {
|
|
419
545
|
margin: 0;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
546
|
+
color: var(--muted);
|
|
547
|
+
font-size: 12.5px;
|
|
548
|
+
line-height: 1.5;
|
|
549
|
+
text-align: center;
|
|
550
|
+
}
|
|
551
|
+
.right-pane {
|
|
552
|
+
position: relative;
|
|
553
|
+
min-width: 0;
|
|
554
|
+
min-height: 0;
|
|
555
|
+
}
|
|
556
|
+
.terminal-window {
|
|
557
|
+
position: absolute;
|
|
558
|
+
inset: 0;
|
|
559
|
+
display: grid;
|
|
560
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
561
|
+
overflow: hidden;
|
|
562
|
+
border: 1px solid var(--border);
|
|
563
|
+
border-radius: 10px;
|
|
564
|
+
background: var(--term-bg);
|
|
565
|
+
}
|
|
566
|
+
.term-titlebar {
|
|
567
|
+
display: flex;
|
|
568
|
+
align-items: center;
|
|
569
|
+
gap: 10px;
|
|
570
|
+
padding: 10px 14px;
|
|
571
|
+
border-bottom: 1px solid var(--border);
|
|
572
|
+
background: #f2efe7;
|
|
573
|
+
}
|
|
574
|
+
.term-titlebar-icon {
|
|
575
|
+
width: 12px;
|
|
576
|
+
height: 12px;
|
|
577
|
+
color: var(--muted);
|
|
578
|
+
flex: 0 0 auto;
|
|
579
|
+
}
|
|
580
|
+
.term-titlebar-icon svg { width: 100%; height: 100%; display: block; }
|
|
581
|
+
.term-titlebar-label {
|
|
582
|
+
color: var(--muted);
|
|
583
|
+
font-family: var(--mono);
|
|
425
584
|
font-size: 12px;
|
|
426
|
-
overflow-wrap: anywhere;
|
|
427
585
|
}
|
|
428
586
|
.terminal-shell {
|
|
587
|
+
position: relative;
|
|
429
588
|
min-height: 0;
|
|
430
589
|
height: 100%;
|
|
431
|
-
|
|
590
|
+
background: var(--term-bg);
|
|
432
591
|
overflow: hidden;
|
|
433
|
-
background: #0b0f14;
|
|
434
592
|
user-select: text;
|
|
435
593
|
}
|
|
436
|
-
|
|
594
|
+
.term-host {
|
|
437
595
|
position: absolute;
|
|
438
596
|
inset: 0;
|
|
439
|
-
border-radius: 0;
|
|
440
|
-
box-shadow: none;
|
|
441
597
|
user-select: text;
|
|
442
|
-
--term-bg: #
|
|
443
|
-
--term-fg: #
|
|
444
|
-
--term-cursor: #
|
|
445
|
-
--term-font-family:
|
|
598
|
+
--term-bg: #faf8f2;
|
|
599
|
+
--term-fg: #1a1a1a;
|
|
600
|
+
--term-cursor: #2d4df5;
|
|
601
|
+
--term-font-family: var(--mono);
|
|
446
602
|
--term-font-size: 13px;
|
|
447
603
|
--term-row-height: 17px;
|
|
448
|
-
--term-color-0: #
|
|
449
|
-
--term-color-1: #
|
|
450
|
-
--term-color-2: #
|
|
451
|
-
--term-color-3: #
|
|
452
|
-
--term-color-4: #
|
|
453
|
-
--term-color-5: #
|
|
454
|
-
--term-color-6: #
|
|
455
|
-
--term-color-7: #
|
|
456
|
-
--term-color-8: #
|
|
457
|
-
--term-color-9: #
|
|
458
|
-
--term-color-10: #
|
|
459
|
-
--term-color-11: #
|
|
460
|
-
--term-color-12: #
|
|
461
|
-
--term-color-13: #
|
|
462
|
-
--term-color-14: #
|
|
463
|
-
--term-color-15: #
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
#fallback {
|
|
604
|
+
--term-color-0: #0a0a0a;
|
|
605
|
+
--term-color-1: #c93250;
|
|
606
|
+
--term-color-2: #1f8b4c;
|
|
607
|
+
--term-color-3: #a17500;
|
|
608
|
+
--term-color-4: #2d4df5;
|
|
609
|
+
--term-color-5: #8e3eff;
|
|
610
|
+
--term-color-6: #0a7783;
|
|
611
|
+
--term-color-7: #5a5a5a;
|
|
612
|
+
--term-color-8: #6a6a6a;
|
|
613
|
+
--term-color-9: #b81e3a;
|
|
614
|
+
--term-color-10: #176a3a;
|
|
615
|
+
--term-color-11: #7a5800;
|
|
616
|
+
--term-color-12: #1a3ad9;
|
|
617
|
+
--term-color-13: #7128df;
|
|
618
|
+
--term-color-14: #06606a;
|
|
619
|
+
--term-color-15: #0a0a0a;
|
|
620
|
+
}
|
|
621
|
+
.term-host:not(.ready) { visibility: hidden; }
|
|
622
|
+
.term-fallback {
|
|
469
623
|
position: absolute;
|
|
470
624
|
inset: 0;
|
|
471
625
|
z-index: 1;
|
|
472
|
-
box-sizing: border-box;
|
|
473
626
|
margin: 0;
|
|
474
|
-
padding:
|
|
627
|
+
padding: 16px;
|
|
475
628
|
overflow: auto;
|
|
476
629
|
white-space: pre-wrap;
|
|
477
630
|
overflow-wrap: anywhere;
|
|
478
|
-
background:
|
|
479
|
-
color:
|
|
631
|
+
background: var(--term-bg);
|
|
632
|
+
color: var(--fg);
|
|
480
633
|
user-select: text;
|
|
481
|
-
font-family:
|
|
634
|
+
font-family: var(--mono);
|
|
482
635
|
font-size: 13px;
|
|
483
|
-
line-height: 1.
|
|
484
|
-
}
|
|
485
|
-
#fallback.hidden {
|
|
486
|
-
display: none;
|
|
636
|
+
line-height: 1.4;
|
|
487
637
|
}
|
|
638
|
+
.term-fallback.hidden { display: none; }
|
|
488
639
|
.wterm {
|
|
489
640
|
position: relative;
|
|
490
641
|
background: var(--term-bg);
|
|
@@ -492,17 +643,12 @@ function renderInteractionPage(
|
|
|
492
643
|
font-family: var(--term-font-family);
|
|
493
644
|
font-size: var(--term-font-size);
|
|
494
645
|
line-height: 1.2;
|
|
495
|
-
padding:
|
|
646
|
+
padding: 14px 16px;
|
|
496
647
|
outline: none;
|
|
497
648
|
overflow: auto;
|
|
498
649
|
user-select: text;
|
|
499
650
|
}
|
|
500
|
-
.term-grid {
|
|
501
|
-
display: block;
|
|
502
|
-
white-space: pre;
|
|
503
|
-
contain: layout paint style;
|
|
504
|
-
user-select: text;
|
|
505
|
-
}
|
|
651
|
+
.term-grid { display: block; white-space: pre; contain: layout paint style; user-select: text; }
|
|
506
652
|
.term-row {
|
|
507
653
|
display: block;
|
|
508
654
|
height: var(--term-row-height);
|
|
@@ -515,222 +661,140 @@ function renderInteractionPage(
|
|
|
515
661
|
vertical-align: top;
|
|
516
662
|
user-select: text;
|
|
517
663
|
}
|
|
518
|
-
.term-block {
|
|
519
|
-
|
|
520
|
-
|
|
664
|
+
.term-block { width: 1ch; overflow: hidden; }
|
|
665
|
+
.term-cursor { outline: 1px solid var(--term-cursor); outline-offset: -1px; }
|
|
666
|
+
.wterm.focused .term-cursor { background: var(--term-cursor); color: #ffffff; outline: none; }
|
|
667
|
+
.success-pane {
|
|
668
|
+
position: absolute;
|
|
669
|
+
inset: 0;
|
|
670
|
+
display: grid;
|
|
671
|
+
place-items: center;
|
|
672
|
+
padding: 24px;
|
|
673
|
+
animation: fadeUp 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
521
674
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
675
|
+
@keyframes fadeUp {
|
|
676
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
677
|
+
to { opacity: 1; transform: translateY(0); }
|
|
525
678
|
}
|
|
526
|
-
.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
679
|
+
.success-card {
|
|
680
|
+
width: min(420px, 100%);
|
|
681
|
+
padding: 32px 28px 28px;
|
|
682
|
+
border-radius: 12px;
|
|
683
|
+
border: 1px solid var(--border);
|
|
684
|
+
background: var(--surface);
|
|
685
|
+
text-align: center;
|
|
530
686
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
687
|
+
.success-icon {
|
|
688
|
+
width: 52px;
|
|
689
|
+
height: 52px;
|
|
690
|
+
margin: 0 auto 18px;
|
|
691
|
+
display: grid;
|
|
692
|
+
place-items: center;
|
|
693
|
+
border-radius: 999px;
|
|
694
|
+
border: 2px solid var(--accent);
|
|
695
|
+
color: var(--accent);
|
|
538
696
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
697
|
+
.success-icon svg {
|
|
698
|
+
width: 24px;
|
|
699
|
+
height: 24px;
|
|
700
|
+
fill: none;
|
|
701
|
+
stroke: currentColor;
|
|
702
|
+
stroke-width: 2.6;
|
|
703
|
+
stroke-linecap: round;
|
|
704
|
+
stroke-linejoin: round;
|
|
544
705
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
cursor: pointer;
|
|
552
|
-
font: inherit;
|
|
553
|
-
font-size: 12px;
|
|
554
|
-
font-weight: 600;
|
|
555
|
-
padding: 8px 12px;
|
|
706
|
+
.success-title {
|
|
707
|
+
margin: 0 0 8px;
|
|
708
|
+
font-size: 26px;
|
|
709
|
+
font-weight: 800;
|
|
710
|
+
letter-spacing: -0.025em;
|
|
711
|
+
color: var(--fg);
|
|
556
712
|
}
|
|
557
|
-
|
|
558
|
-
|
|
713
|
+
.success-message {
|
|
714
|
+
margin: 0;
|
|
715
|
+
color: var(--muted);
|
|
716
|
+
font-size: 14px;
|
|
717
|
+
line-height: 1.55;
|
|
559
718
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
719
|
+
@media (max-width: 880px) {
|
|
720
|
+
body { overflow: auto; }
|
|
721
|
+
#app { height: auto; min-height: 100vh; }
|
|
722
|
+
.workspace { grid-template-columns: 1fr; padding: 0 16px 16px; gap: 16px; }
|
|
723
|
+
.right-pane { height: min(640px, 70vh); }
|
|
724
|
+
.task-title { font-size: 32px; }
|
|
563
725
|
}
|
|
564
|
-
@media (max-width:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
.
|
|
569
|
-
height: 100vh;
|
|
570
|
-
min-height: 100vh;
|
|
571
|
-
border: 0;
|
|
572
|
-
border-radius: 0;
|
|
573
|
-
}
|
|
574
|
-
.instructions {
|
|
575
|
-
display: none;
|
|
576
|
-
}
|
|
726
|
+
@media (max-width: 540px) {
|
|
727
|
+
.app-header { padding: 14px 16px 10px; }
|
|
728
|
+
.brand-node { display: none; }
|
|
729
|
+
.task-title { font-size: 28px; }
|
|
730
|
+
.instructions-pane { padding: 12px 16px 16px; }
|
|
577
731
|
}
|
|
578
732
|
</style>
|
|
579
733
|
</head>
|
|
580
734
|
<body>
|
|
581
|
-
<
|
|
582
|
-
<
|
|
583
|
-
<div class="
|
|
584
|
-
<span class="light red"></span>
|
|
585
|
-
<span class="light yellow"></span>
|
|
586
|
-
<span class="light green"></span>
|
|
587
|
-
</div>
|
|
588
|
-
<div class="title-copy">
|
|
589
|
-
<p class="meta">rigkit node ${escapedNode}</p>
|
|
735
|
+
<div id="app">
|
|
736
|
+
<noscript>
|
|
737
|
+
<div class="noscript-fallback">
|
|
590
738
|
<h1>${escapedLabel}</h1>
|
|
591
|
-
${escapedInstructions ? `<p
|
|
739
|
+
${escapedInstructions ? `<p>${escapedInstructions}</p>` : ""}
|
|
740
|
+
<pre>$ ${escapedCommand}</pre>
|
|
741
|
+
<p>This interactive task requires JavaScript to run a terminal in your browser.</p>
|
|
592
742
|
</div>
|
|
593
|
-
</
|
|
594
|
-
|
|
595
|
-
<main class="terminal-shell" aria-label="Interactive terminal">
|
|
596
|
-
<pre id="fallback">Starting terminal...\n</pre>
|
|
597
|
-
<div id="terminal"></div>
|
|
598
|
-
</main>
|
|
599
|
-
<footer>
|
|
600
|
-
<span id="status">${completed ? "Done. You can close this page now." : "Starting terminal"}</span>
|
|
601
|
-
<button id="finish" type="button" disabled>Finished</button>
|
|
602
|
-
</footer>
|
|
603
|
-
</section>
|
|
743
|
+
</noscript>
|
|
744
|
+
</div>
|
|
604
745
|
<script type="module">
|
|
746
|
+
import * as React from "https://esm.sh/react@18.3.1";
|
|
747
|
+
import { createRoot } from "https://esm.sh/react-dom@18.3.1/client";
|
|
748
|
+
|
|
749
|
+
const h = React.createElement;
|
|
750
|
+
const F = React.Fragment;
|
|
751
|
+
const { useState, useEffect, useRef, useCallback, useMemo } = React;
|
|
752
|
+
|
|
753
|
+
const TASK_TITLE = ${titleLit};
|
|
754
|
+
const TASK_INSTRUCTIONS = ${instructionsLit};
|
|
755
|
+
const NODE_PATH = ${nodeLit};
|
|
756
|
+
const startupInput = ${startupInputLiteral};
|
|
757
|
+
const canFinishWhileRunning = ${canFinishWhileRunningLiteral};
|
|
758
|
+
const INITIAL_COMPLETED = ${initialCompletedLiteral};
|
|
605
759
|
const token = new URLSearchParams(location.search).get("token") || "";
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
terminalUrl.searchParams.set("token", token);
|
|
609
|
-
const statusEl = document.getElementById("status");
|
|
610
|
-
const finishEl = document.getElementById("finish");
|
|
611
|
-
const terminalEl = document.getElementById("terminal");
|
|
612
|
-
const fallbackEl = document.getElementById("fallback");
|
|
613
|
-
const outputBacklog = [];
|
|
614
|
-
let socket;
|
|
760
|
+
|
|
761
|
+
let terminalEl = null;
|
|
615
762
|
let term;
|
|
616
763
|
let termReady = false;
|
|
617
|
-
|
|
764
|
+
let socket;
|
|
618
765
|
let startupSent = false;
|
|
619
766
|
let startupIdleTimer;
|
|
620
767
|
let startupMaxTimer;
|
|
768
|
+
const outputBacklog = [];
|
|
769
|
+
const listeners = {
|
|
770
|
+
onStatus: null,
|
|
771
|
+
onOutput: null,
|
|
772
|
+
onClose: null,
|
|
773
|
+
};
|
|
621
774
|
|
|
622
775
|
function sendTerminalInput(data) {
|
|
623
|
-
if (!data || socket
|
|
776
|
+
if (!data || !socket || socket.readyState !== WebSocket.OPEN) return;
|
|
624
777
|
socket.send(JSON.stringify({ type: "input", data }));
|
|
625
778
|
}
|
|
626
779
|
|
|
627
|
-
function setStatus(text, canFinish = false) {
|
|
628
|
-
statusEl.textContent = text;
|
|
629
|
-
finishEl.disabled = !canFinish;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function appendFallback(data) {
|
|
633
|
-
fallbackEl.textContent += data;
|
|
634
|
-
fallbackEl.scrollTop = fallbackEl.scrollHeight;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
780
|
function sendStartupInput() {
|
|
638
|
-
if (!startupInput || startupSent || socket.readyState !== WebSocket.OPEN) return;
|
|
781
|
+
if (!startupInput || startupSent || !socket || socket.readyState !== WebSocket.OPEN) return;
|
|
639
782
|
startupSent = true;
|
|
640
783
|
clearTimeout(startupIdleTimer);
|
|
641
784
|
clearTimeout(startupMaxTimer);
|
|
642
785
|
sendTerminalInput(startupInput);
|
|
643
786
|
}
|
|
644
787
|
|
|
645
|
-
function scheduleStartupInput(delay
|
|
646
|
-
if (!startupInput || startupSent || socket.readyState !== WebSocket.OPEN) return;
|
|
788
|
+
function scheduleStartupInput(delay) {
|
|
789
|
+
if (!startupInput || startupSent || !socket || socket.readyState !== WebSocket.OPEN) return;
|
|
647
790
|
clearTimeout(startupIdleTimer);
|
|
648
|
-
startupIdleTimer = setTimeout(sendStartupInput, delay);
|
|
649
|
-
startupMaxTimer
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
socket = new WebSocket(terminalUrl);
|
|
653
|
-
socket.addEventListener("open", () => {
|
|
654
|
-
setStatus("Connected");
|
|
655
|
-
scheduleStartupInput(700);
|
|
656
|
-
});
|
|
657
|
-
socket.addEventListener("message", (event) => {
|
|
658
|
-
const message = JSON.parse(event.data);
|
|
659
|
-
if (message.type === "output") {
|
|
660
|
-
outputBacklog.push(message.data);
|
|
661
|
-
if (termReady) {
|
|
662
|
-
term.write(message.data);
|
|
663
|
-
} else {
|
|
664
|
-
appendFallback(message.data);
|
|
665
|
-
}
|
|
666
|
-
scheduleStartupInput();
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
if (message.type === "status") {
|
|
670
|
-
setStatus(message.status, Boolean(message.canFinish));
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
socket.addEventListener("close", () => {
|
|
674
|
-
if (finishEl.disabled) setStatus("Terminal connection closed");
|
|
675
|
-
});
|
|
676
|
-
finishEl.addEventListener("click", () => {
|
|
677
|
-
if (socket?.readyState === WebSocket.OPEN) {
|
|
678
|
-
socket.send(JSON.stringify({ type: "finish" }));
|
|
679
|
-
} else {
|
|
680
|
-
fetch("/complete?token=" + encodeURIComponent(token), { method: "POST" }).catch(() => {});
|
|
681
|
-
}
|
|
682
|
-
setStatus("Finishing");
|
|
683
|
-
finishEl.disabled = true;
|
|
684
|
-
});
|
|
685
|
-
document.addEventListener("keydown", (event) => {
|
|
686
|
-
if (event.defaultPrevented || isTextEditingTarget(event.target)) return;
|
|
687
|
-
const data = keyEventToTerminalInput(event);
|
|
688
|
-
if (!data) return;
|
|
689
|
-
event.preventDefault();
|
|
690
|
-
event.stopImmediatePropagation();
|
|
691
|
-
term?.focus();
|
|
692
|
-
sendTerminalInput(data);
|
|
693
|
-
}, { capture: true });
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
const [{ WTerm }, { GhosttyCore }] = await Promise.all([
|
|
697
|
-
import("https://esm.sh/@wterm/dom@0.3.0?bundle"),
|
|
698
|
-
import("https://esm.sh/@wterm/ghostty@0.3.0?bundle"),
|
|
699
|
-
]);
|
|
700
|
-
const core = await GhosttyCore.load({
|
|
701
|
-
wasmPath: "https://esm.sh/@wterm/ghostty@0.3.0/wasm/ghostty-vt.wasm",
|
|
702
|
-
});
|
|
703
|
-
term = new WTerm(terminalEl, {
|
|
704
|
-
core,
|
|
705
|
-
cols: 100,
|
|
706
|
-
rows: 28,
|
|
707
|
-
autoResize: true,
|
|
708
|
-
cursorBlink: true,
|
|
709
|
-
onData(data) {
|
|
710
|
-
sendTerminalInput(data);
|
|
711
|
-
},
|
|
712
|
-
onResize(cols, rows) {
|
|
713
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
714
|
-
socket.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
715
|
-
}
|
|
716
|
-
},
|
|
717
|
-
});
|
|
718
|
-
await term.init();
|
|
719
|
-
for (const chunk of outputBacklog) term.write(chunk);
|
|
720
|
-
termReady = true;
|
|
721
|
-
terminalEl.classList.add("ready");
|
|
722
|
-
fallbackEl.classList.add("hidden");
|
|
723
|
-
term.focus();
|
|
724
|
-
} catch (error) {
|
|
725
|
-
console.error(error);
|
|
726
|
-
appendFallback("\\nUnable to load the libghostty renderer. Output will continue here.\\n");
|
|
727
|
-
setStatus("Renderer unavailable. Command output is shown in fallback mode.", !startupInput || startupSent);
|
|
791
|
+
startupIdleTimer = setTimeout(sendStartupInput, delay || 350);
|
|
792
|
+
startupMaxTimer = startupMaxTimer || setTimeout(sendStartupInput, 1500);
|
|
728
793
|
}
|
|
729
794
|
|
|
730
795
|
function isTextEditingTarget(target) {
|
|
731
796
|
if (!(target instanceof Element)) return false;
|
|
732
|
-
if (terminalEl.contains(target)) return false;
|
|
733
|
-
if (target === finishEl) return true;
|
|
797
|
+
if (terminalEl && terminalEl.contains(target)) return false;
|
|
734
798
|
return Boolean(target.closest("textarea, input, select, button, [contenteditable=''], [contenteditable='true']"));
|
|
735
799
|
}
|
|
736
800
|
|
|
@@ -797,18 +861,279 @@ function renderInteractionPage(
|
|
|
797
861
|
|
|
798
862
|
return null;
|
|
799
863
|
}
|
|
864
|
+
|
|
865
|
+
document.addEventListener("keydown", (event) => {
|
|
866
|
+
if (event.defaultPrevented || isTextEditingTarget(event.target)) return;
|
|
867
|
+
const data = keyEventToTerminalInput(event);
|
|
868
|
+
if (!data) return;
|
|
869
|
+
event.preventDefault();
|
|
870
|
+
event.stopImmediatePropagation();
|
|
871
|
+
term?.focus();
|
|
872
|
+
sendTerminalInput(data);
|
|
873
|
+
}, { capture: true });
|
|
874
|
+
|
|
875
|
+
function setupSocket() {
|
|
876
|
+
const terminalUrl = new URL("/terminal", location.href);
|
|
877
|
+
terminalUrl.protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
878
|
+
terminalUrl.searchParams.set("token", token);
|
|
879
|
+
socket = new WebSocket(terminalUrl);
|
|
880
|
+
socket.addEventListener("open", () => {
|
|
881
|
+
listeners.onStatus && listeners.onStatus("Connected", canFinishWhileRunning);
|
|
882
|
+
scheduleStartupInput(700);
|
|
883
|
+
});
|
|
884
|
+
socket.addEventListener("message", (event) => {
|
|
885
|
+
const message = JSON.parse(event.data);
|
|
886
|
+
if (message.type === "output") {
|
|
887
|
+
outputBacklog.push(message.data);
|
|
888
|
+
if (termReady) {
|
|
889
|
+
term.write(message.data);
|
|
890
|
+
} else if (listeners.onOutput) {
|
|
891
|
+
listeners.onOutput(message.data);
|
|
892
|
+
}
|
|
893
|
+
scheduleStartupInput();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (message.type === "status") {
|
|
897
|
+
listeners.onStatus && listeners.onStatus(message.status, Boolean(message.canFinish));
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
socket.addEventListener("close", () => {
|
|
901
|
+
listeners.onClose && listeners.onClose();
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function classifyStatus(text, canFinish) {
|
|
906
|
+
const lower = text.toLowerCase();
|
|
907
|
+
if (lower.includes("done.") || lower.startsWith("task complete")) return "done";
|
|
908
|
+
if (lower.includes("error") || lower.includes("unavailable") || /exited [^0]/.test(lower)) return "error";
|
|
909
|
+
if (canFinish) return "ready";
|
|
910
|
+
return "working";
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function parseSteps(text) {
|
|
914
|
+
if (!text) return [];
|
|
915
|
+
const trimmed = text.trim();
|
|
916
|
+
if (!trimmed) return [];
|
|
917
|
+
const lines = trimmed.split(/\\r?\\n/).map((l) => l.trim()).filter(Boolean);
|
|
918
|
+
if (lines.length <= 1) return [];
|
|
919
|
+
return lines.map((line) => line.replace(/^([0-9]+[.)]\\s+|[-*•]\\s+)/, ""));
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function CheckIcon() {
|
|
923
|
+
return h("svg", { viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
924
|
+
h("polyline", { points: "4 12 10 18 20 6", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function CloudIcon() {
|
|
929
|
+
return h("svg", { viewBox: "0 0 32 32", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" },
|
|
930
|
+
h("path", { d: "M22 21H10.5a4.5 4.5 0 0 1-.45-8.97A6.5 6.5 0 0 1 22.86 13H23a4 4 0 0 1 0 8h-1" }),
|
|
931
|
+
h("path", { d: "M11.5 24v3" }),
|
|
932
|
+
h("path", { d: "M16 25v3" }),
|
|
933
|
+
h("path", { d: "M20.5 24v3" })
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function TerminalIcon() {
|
|
938
|
+
return h("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" },
|
|
939
|
+
h("polyline", { points: "4 5 7 8 4 11" }),
|
|
940
|
+
h("line", { x1: "8.5", y1: "11", x2: "12", y2: "11" })
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function Header(props) {
|
|
945
|
+
return h("header", { className: "app-header" },
|
|
946
|
+
h("div", { className: "brand" },
|
|
947
|
+
h("span", { className: "brand-mark", "aria-hidden": "true" }, h(CloudIcon, null)),
|
|
948
|
+
h("span", { className: "brand-wordmark" }, "freestyle.sh"),
|
|
949
|
+
h("span", { className: "brand-node" }, props.node),
|
|
950
|
+
),
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function InstructionsPane(props) {
|
|
955
|
+
const steps = useMemo(() => parseSteps(props.instructions), [props.instructions]);
|
|
956
|
+
const showSteps = steps.length > 1;
|
|
957
|
+
const buttonDisabled = !props.canFinish || props.done;
|
|
958
|
+
return h("section", { className: "instructions-pane", "aria-label": "Task instructions" },
|
|
959
|
+
h("p", { className: "eyebrow" }, "Interactive task"),
|
|
960
|
+
h("h1", { className: "task-title" }, props.title),
|
|
961
|
+
props.instructions
|
|
962
|
+
? (showSteps
|
|
963
|
+
? h("ol", { className: "instruction-steps" },
|
|
964
|
+
steps.map((step, i) => h("li", { key: i }, step))
|
|
965
|
+
)
|
|
966
|
+
: h("p", { className: "instruction-text" }, props.instructions))
|
|
967
|
+
: null,
|
|
968
|
+
h("div", { className: "instructions-cta" },
|
|
969
|
+
h("button", {
|
|
970
|
+
type: "button",
|
|
971
|
+
className: "primary-button",
|
|
972
|
+
disabled: buttonDisabled,
|
|
973
|
+
onClick: props.onFinish,
|
|
974
|
+
},
|
|
975
|
+
h("span", { className: "check" }, h(CheckIcon, null)),
|
|
976
|
+
h("span", null, "Complete task"),
|
|
977
|
+
),
|
|
978
|
+
h("p", { className: "cta-hint" },
|
|
979
|
+
props.done
|
|
980
|
+
? "Task complete — you can close this tab."
|
|
981
|
+
: (props.canFinish
|
|
982
|
+
? "When the command above has finished in the terminal, click here to continue."
|
|
983
|
+
: "Run the command in the terminal — this button will activate when the task is ready to finish.")
|
|
984
|
+
),
|
|
985
|
+
),
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function TerminalChrome() {
|
|
990
|
+
const hostRef = useRef(null);
|
|
991
|
+
const fallbackRef = useRef(null);
|
|
992
|
+
|
|
993
|
+
useEffect(() => {
|
|
994
|
+
terminalEl = hostRef.current;
|
|
995
|
+
if (fallbackRef.current) {
|
|
996
|
+
for (const chunk of outputBacklog) {
|
|
997
|
+
fallbackRef.current.textContent += chunk;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
listeners.onOutput = (data) => {
|
|
1001
|
+
if (!fallbackRef.current) return;
|
|
1002
|
+
fallbackRef.current.textContent += data;
|
|
1003
|
+
fallbackRef.current.scrollTop = fallbackRef.current.scrollHeight;
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
let cancelled = false;
|
|
1007
|
+
(async () => {
|
|
1008
|
+
try {
|
|
1009
|
+
const [{ WTerm }, { GhosttyCore }] = await Promise.all([
|
|
1010
|
+
import("https://esm.sh/@wterm/dom@0.3.0?bundle"),
|
|
1011
|
+
import("https://esm.sh/@wterm/ghostty@0.3.0?bundle"),
|
|
1012
|
+
]);
|
|
1013
|
+
if (cancelled || !hostRef.current) return;
|
|
1014
|
+
const core = await GhosttyCore.load({
|
|
1015
|
+
wasmPath: "https://esm.sh/@wterm/ghostty@0.3.0/wasm/ghostty-vt.wasm",
|
|
1016
|
+
});
|
|
1017
|
+
if (cancelled || !hostRef.current) return;
|
|
1018
|
+
term = new WTerm(hostRef.current, {
|
|
1019
|
+
core,
|
|
1020
|
+
cols: 100,
|
|
1021
|
+
rows: 28,
|
|
1022
|
+
autoResize: true,
|
|
1023
|
+
cursorBlink: true,
|
|
1024
|
+
onData(data) { sendTerminalInput(data); },
|
|
1025
|
+
onResize(cols, rows) {
|
|
1026
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
1027
|
+
socket.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
await term.init();
|
|
1032
|
+
for (const chunk of outputBacklog) term.write(chunk);
|
|
1033
|
+
termReady = true;
|
|
1034
|
+
hostRef.current.classList.add("ready");
|
|
1035
|
+
fallbackRef.current && fallbackRef.current.classList.add("hidden");
|
|
1036
|
+
term.focus();
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
console.error(error);
|
|
1039
|
+
if (fallbackRef.current) {
|
|
1040
|
+
fallbackRef.current.textContent += "\\nUnable to load the libghostty renderer. Output will continue here.\\n";
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
})();
|
|
1044
|
+
|
|
1045
|
+
return () => { cancelled = true; };
|
|
1046
|
+
}, []);
|
|
1047
|
+
|
|
1048
|
+
return h(F, null,
|
|
1049
|
+
h("div", { className: "term-titlebar" },
|
|
1050
|
+
h("span", { className: "term-titlebar-icon", "aria-hidden": "true" }, h(TerminalIcon, null)),
|
|
1051
|
+
h("span", { className: "term-titlebar-label" }, NODE_PATH + " · terminal"),
|
|
1052
|
+
),
|
|
1053
|
+
h("div", { className: "terminal-shell" },
|
|
1054
|
+
h("pre", { ref: fallbackRef, className: "term-fallback" }, "Starting terminal...\\n"),
|
|
1055
|
+
h("div", { ref: hostRef, className: "term-host" }),
|
|
1056
|
+
),
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function SuccessPane() {
|
|
1061
|
+
return h("div", { className: "success-pane" },
|
|
1062
|
+
h("div", { className: "success-card", role: "status", "aria-live": "polite" },
|
|
1063
|
+
h("div", { className: "success-icon" },
|
|
1064
|
+
h(CheckIcon, null)
|
|
1065
|
+
),
|
|
1066
|
+
h("h2", { className: "success-title" }, "Task complete"),
|
|
1067
|
+
h("p", { className: "success-message" }, "You can close this tab — Rigkit will pick up from here."),
|
|
1068
|
+
),
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function App() {
|
|
1073
|
+
const [canFinish, setCanFinish] = useState(false);
|
|
1074
|
+
const [done, setDone] = useState(INITIAL_COMPLETED);
|
|
1075
|
+
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
listeners.onStatus = (text, canFinishVal) => {
|
|
1078
|
+
setCanFinish(canFinishVal);
|
|
1079
|
+
if (classifyStatus(text, canFinishVal) === "done") setDone(true);
|
|
1080
|
+
};
|
|
1081
|
+
if (!INITIAL_COMPLETED) setupSocket();
|
|
1082
|
+
return () => {
|
|
1083
|
+
listeners.onStatus = null;
|
|
1084
|
+
listeners.onClose = null;
|
|
1085
|
+
};
|
|
1086
|
+
}, []);
|
|
1087
|
+
|
|
1088
|
+
const handleFinish = useCallback(() => {
|
|
1089
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
1090
|
+
socket.send(JSON.stringify({ type: "finish" }));
|
|
1091
|
+
} else {
|
|
1092
|
+
fetch("/complete?token=" + encodeURIComponent(token), { method: "POST" }).catch(() => {});
|
|
1093
|
+
}
|
|
1094
|
+
setCanFinish(false);
|
|
1095
|
+
setDone(true);
|
|
1096
|
+
}, []);
|
|
1097
|
+
|
|
1098
|
+
return h(F, null,
|
|
1099
|
+
h(Header, { node: NODE_PATH }),
|
|
1100
|
+
h("main", { className: "workspace" },
|
|
1101
|
+
h(InstructionsPane, {
|
|
1102
|
+
title: TASK_TITLE,
|
|
1103
|
+
instructions: TASK_INSTRUCTIONS,
|
|
1104
|
+
canFinish: canFinish,
|
|
1105
|
+
done: done,
|
|
1106
|
+
onFinish: handleFinish,
|
|
1107
|
+
}),
|
|
1108
|
+
h("div", { className: "right-pane" },
|
|
1109
|
+
!done
|
|
1110
|
+
? h("div", { className: "terminal-window" }, h(TerminalChrome, null))
|
|
1111
|
+
: h(SuccessPane, null)
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
createRoot(document.getElementById("app")).render(h(App));
|
|
800
1118
|
</script>
|
|
801
1119
|
</body>
|
|
802
1120
|
</html>`;
|
|
803
1121
|
}
|
|
804
1122
|
|
|
805
|
-
function javaScriptLiteral(value: string | null): string {
|
|
1123
|
+
function javaScriptLiteral(value: string | boolean | null): string {
|
|
806
1124
|
return JSON.stringify(value)
|
|
807
1125
|
.replaceAll("<", "\\u003c")
|
|
808
1126
|
.replaceAll(">", "\\u003e")
|
|
809
1127
|
.replaceAll("&", "\\u0026")
|
|
810
|
-
.replaceAll("
|
|
811
|
-
.replaceAll("
|
|
1128
|
+
.replaceAll("
", "\\u2028")
|
|
1129
|
+
.replaceAll("
", "\\u2029");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function canFinishWhileProcessRuns(
|
|
1133
|
+
request: FreestyleTerminalSessionRequest,
|
|
1134
|
+
startupInput: string | undefined,
|
|
1135
|
+
): boolean {
|
|
1136
|
+
return request.canFinishWhileRunning ?? (!request.displayCommand && !startupInput);
|
|
812
1137
|
}
|
|
813
1138
|
|
|
814
1139
|
function escapeHtml(value: string): string {
|