@rigkit/provider-freestyle 0.2.2 → 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.
@@ -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 startupInput = request.remoteCommand ? ensureTrailingNewline(request.remoteCommand) : undefined;
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: false,
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 ${request.remoteCommand}`,
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 ${request.remoteCommand}`, canFinish: true });
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: !request.remoteCommand });
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 escapedTitle = escapeHtml(completed ? "Interactive task completed" : request.title);
318
- const escapedNode = escapeHtml(request.nodePath ?? "provider");
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 escapedInstructions = request.instructions ? escapeHtml(request.instructions) : "";
321
- const escapedCommand = escapeHtml(request.remoteCommand ?? request.command);
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>${escapedTitle}</title>
344
+ <title>${escapedDocTitle}</title>
330
345
  <style>
331
346
  :root {
332
- color-scheme: dark;
333
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
334
- background: #0a0a0a;
335
- color: #f5f5f5;
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
- radial-gradient(circle at 18% 0%, rgba(82, 82, 91, 0.16), transparent 28%),
347
- #0a0a0a;
372
+ background: var(--bg);
373
+ -webkit-font-smoothing: antialiased;
374
+ -moz-osx-font-smoothing: grayscale;
348
375
  }
349
- .terminal-window {
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 auto minmax(0, 1fr) auto;
355
- overflow: hidden;
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
- .titlebar {
362
- min-height: 50px;
363
- display: flex;
364
- align-items: center;
365
- gap: 14px;
366
- padding: 11px 14px;
367
- border-bottom: 1px solid #27272a;
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
- .lights {
389
+ .app-header {
372
390
  display: flex;
373
- gap: 7px;
374
- flex: 0 0 auto;
391
+ align-items: center;
392
+ padding: 18px 24px 14px;
375
393
  }
376
- .light {
377
- width: 11px;
378
- height: 11px;
379
- border-radius: 999px;
380
- background: #3f3f46;
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
- .light.red {
384
- background: #ff5f57;
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
- .light.yellow {
387
- background: #febc2e;
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
- .light.green {
390
- background: #28c840;
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
- .title-copy {
425
+ .instructions-pane {
426
+ display: flex;
427
+ flex-direction: column;
428
+ gap: 20px;
393
429
  min-width: 0;
394
- flex: 1;
430
+ padding: 18px 22px 22px;
431
+ overflow: auto;
432
+ user-select: text;
395
433
  }
396
- .meta {
397
- margin: 0 0 3px;
398
- color: #a1a1aa;
399
- font-size: 12px;
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
- h1 {
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
- white-space: nowrap;
408
- overflow: hidden;
409
- text-overflow: ellipsis;
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
- .instructions {
412
- margin: 4px 0 0;
413
- white-space: pre-wrap;
414
- color: #a1a1aa;
415
- line-height: 1.35;
416
- font-size: 12px;
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
- .command {
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
- padding: 8px 12px;
421
- border-bottom: 1px solid #1f2937;
422
- background: #0f1720;
423
- color: #7dd3fc;
424
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
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
- position: relative;
590
+ background: var(--term-bg);
432
591
  overflow: hidden;
433
- background: #0b0f14;
434
592
  user-select: text;
435
593
  }
436
- #terminal {
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: #0b0f14;
443
- --term-fg: #e5e7eb;
444
- --term-cursor: #f8fafc;
445
- --term-font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
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: #1f2937;
449
- --term-color-1: #ef4444;
450
- --term-color-2: #22c55e;
451
- --term-color-3: #eab308;
452
- --term-color-4: #38bdf8;
453
- --term-color-5: #a78bfa;
454
- --term-color-6: #2dd4bf;
455
- --term-color-7: #e5e7eb;
456
- --term-color-8: #6b7280;
457
- --term-color-9: #f87171;
458
- --term-color-10: #4ade80;
459
- --term-color-11: #facc15;
460
- --term-color-12: #7dd3fc;
461
- --term-color-13: #c4b5fd;
462
- --term-color-14: #5eead4;
463
- --term-color-15: #ffffff;
464
- }
465
- #terminal:not(.ready) {
466
- visibility: hidden;
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: 14px;
627
+ padding: 16px;
475
628
  overflow: auto;
476
629
  white-space: pre-wrap;
477
630
  overflow-wrap: anywhere;
478
- background: #0b0f14;
479
- color: #e5e7eb;
631
+ background: var(--term-bg);
632
+ color: var(--fg);
480
633
  user-select: text;
481
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
634
+ font-family: var(--mono);
482
635
  font-size: 13px;
483
- line-height: 1.35;
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: 12px;
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
- width: 1ch;
520
- overflow: hidden;
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
- .term-cursor {
523
- outline: 1px solid var(--term-cursor);
524
- outline-offset: -1px;
675
+ @keyframes fadeUp {
676
+ from { opacity: 0; transform: translateY(8px); }
677
+ to { opacity: 1; transform: translateY(0); }
525
678
  }
526
- .wterm.focused .term-cursor {
527
- background: var(--term-cursor);
528
- color: var(--term-bg);
529
- outline: none;
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
- footer {
532
- display: flex;
533
- align-items: center;
534
- gap: 14px;
535
- padding: 11px 14px;
536
- border-top: 1px solid #27272a;
537
- background: #17171a;
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
- #status {
540
- flex: 1;
541
- min-width: 0;
542
- color: #cbd5e1;
543
- font-size: 12px;
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
- button {
546
- border: 0;
547
- border-radius: 6px;
548
- background: #f5f5f5;
549
- color: #111111;
550
- min-width: 82px;
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
- button:hover:not(:disabled) {
558
- background: #ffffff;
713
+ .success-message {
714
+ margin: 0;
715
+ color: var(--muted);
716
+ font-size: 14px;
717
+ line-height: 1.55;
559
718
  }
560
- button:disabled {
561
- cursor: not-allowed;
562
- opacity: 0.45;
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: 720px) {
565
- body {
566
- padding: 0;
567
- }
568
- .terminal-window {
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
- <section class="terminal-window">
582
- <header class="titlebar">
583
- <div class="lights" aria-hidden="true">
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 class="instructions">${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
- </header>
594
- <p class="command">$ ${escapedCommand}</p>
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
- const terminalUrl = new URL("/terminal", location.href);
607
- terminalUrl.protocol = location.protocol === "https:" ? "wss:" : "ws:";
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
- const startupInput = ${startupInputLiteral};
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?.readyState !== WebSocket.OPEN) return;
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 = 350) {
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 ??= setTimeout(sendStartupInput, 1500);
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("\u2028", "\\u2028")
811
- .replaceAll("\u2029", "\\u2029");
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 {