@frockbot/plugin-shell 0.0.0 → 0.1.0

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.
Files changed (73) hide show
  1. package/frockbot.json +68 -0
  2. package/package.json +87 -6
  3. package/src/agent.test.ts +372 -0
  4. package/src/agent.ts +335 -0
  5. package/src/approvals.test.ts +224 -0
  6. package/src/approvals.ts +530 -0
  7. package/src/backend-assignment.test.ts +161 -0
  8. package/src/backend-assignment.ts +274 -0
  9. package/src/backend-authoring.test.ts +518 -0
  10. package/src/backend-authoring.ts +531 -0
  11. package/src/backend-bot-identity.test.ts +215 -0
  12. package/src/backend-completion.test.ts +289 -0
  13. package/src/backend-completion.ts +95 -0
  14. package/src/backend-composition.ts +242 -0
  15. package/src/backend-computer.ts +76 -0
  16. package/src/backend-configuration.test.ts +1757 -0
  17. package/src/backend-contracts.test.ts +189 -0
  18. package/src/backend-contracts.ts +44 -0
  19. package/src/backend-debug.test.ts +202 -0
  20. package/src/backend-execution.ts +55 -0
  21. package/src/backend-flock.ts +96 -0
  22. package/src/backend-image.test.ts +115 -0
  23. package/src/backend-image.ts +180 -0
  24. package/src/backend-isolate.test.ts +238 -0
  25. package/src/backend-isolate.ts +409 -0
  26. package/src/backend-machine.ts +144 -0
  27. package/src/backend-memory.ts +89 -0
  28. package/src/backend-recovery-integration.test.ts +1575 -0
  29. package/src/backend-recovery.ts +106 -0
  30. package/src/backend-routines.ts +375 -0
  31. package/src/backend-runner.ts +251 -0
  32. package/src/backend-skills.test.ts +126 -0
  33. package/src/backend-skills.ts +198 -0
  34. package/src/backend-stop.test.ts +356 -0
  35. package/src/backend-subagents.ts +459 -0
  36. package/src/backend.ts +6035 -0
  37. package/src/client/FrockBotApp.vue +1026 -0
  38. package/src/client/SendPayloadView.vue +337 -0
  39. package/src/client/composer-draft.test.ts +31 -0
  40. package/src/client/composer-draft.ts +35 -0
  41. package/src/client/cordis-client-shim.d.ts +15 -0
  42. package/src/client/index.test.ts +2548 -0
  43. package/src/client/index.ts +2346 -0
  44. package/src/client/model-presentation.test.ts +35 -0
  45. package/src/client/model-presentation.ts +19 -0
  46. package/src/client/notify.test.ts +89 -0
  47. package/src/client/notify.ts +101 -0
  48. package/src/client/skill-invocation.test.ts +143 -0
  49. package/src/client/skill-invocation.ts +175 -0
  50. package/src/client/styles.css +1043 -0
  51. package/src/composition-views.ts +118 -0
  52. package/src/debug-protocol.test.ts +80 -0
  53. package/src/debug-protocol.ts +165 -0
  54. package/src/env.d.ts +10 -0
  55. package/src/history.test.ts +163 -0
  56. package/src/history.ts +108 -0
  57. package/src/host.ts +20 -0
  58. package/src/index.ts +2 -0
  59. package/src/manifest.ts +3 -0
  60. package/src/run-cursor.ts +28 -0
  61. package/src/run-protocol.test.ts +1281 -0
  62. package/src/run-protocol.ts +1417 -0
  63. package/src/settings-links.test.ts +106 -0
  64. package/src/settings-links.ts +289 -0
  65. package/src/shared.ts +338 -0
  66. package/src/skill-protocol.ts +117 -0
  67. package/src/terminal-records.test.ts +217 -0
  68. package/src/terminal-records.ts +150 -0
  69. package/src/unread.test.ts +362 -0
  70. package/src/unread.ts +675 -0
  71. package/tsconfig.json +18 -0
  72. package/vite.config.ts +32 -0
  73. package/README.md +0 -3
@@ -0,0 +1,1043 @@
1
+ .frockbot-root {
2
+ position: fixed;
3
+ z-index: 1000;
4
+ inset: 0;
5
+ width: 100%;
6
+ height: 100%;
7
+
8
+ /*
9
+ * `clip`, not `hidden`. A closed drawer is parked outside the window, and
10
+ * `hidden` makes this element a scroll container that can still be scrolled
11
+ * programmatically: focusing a field inside the composer scrolls the parked
12
+ * drawer into view and takes the whole layout sideways with it. `clip` has
13
+ * the clipping without the scroll port, so there is nothing to scroll. The
14
+ * `hidden` above it is the fallback for a renderer without `clip`.
15
+ */
16
+ overflow: hidden;
17
+ overflow: clip;
18
+ font-size: var(--frock-text-base);
19
+ line-height: var(--frock-leading-normal);
20
+ }
21
+
22
+ /*
23
+ * Three columns: flock sidebar, workspace, right panel. The panel column is
24
+ * always present and animates between its width and zero so the toggle feels
25
+ * like a drawer rather than a relayout, and so panel plugins keep their state.
26
+ */
27
+ .app-shell {
28
+ --panel-current: 0px;
29
+
30
+ /* A hosted surface needs more room than a panel plugin's summary column. */
31
+ --panel-surface-width: 360px;
32
+ position: relative;
33
+ display: grid;
34
+ grid-template-columns:
35
+ var(--frock-sidebar-width) minmax(0, 1fr)
36
+ var(--panel-current);
37
+ width: 100%;
38
+ height: 100%;
39
+ min-height: 0;
40
+ background: var(--frock-surface-window);
41
+ transition: grid-template-columns var(--frock-motion-panel);
42
+ }
43
+
44
+ .app-shell.panel-open {
45
+ --panel-current: var(--frock-panel-width);
46
+ }
47
+
48
+ .app-shell.panel-open.panel-surface {
49
+ --panel-current: var(--panel-surface-width);
50
+ }
51
+
52
+ /* Sidebar */
53
+
54
+ .sidebar {
55
+ position: relative;
56
+ display: flex;
57
+ min-height: 0;
58
+ flex-direction: column;
59
+ padding: 0 10px 10px;
60
+ border-right: 1px solid var(--frock-border);
61
+ background: var(--frock-surface-sidebar);
62
+ }
63
+
64
+ .brand {
65
+ display: flex;
66
+ height: var(--frock-titlebar-height);
67
+ flex: 0 0 auto;
68
+ align-items: center;
69
+ padding: 0 8px;
70
+ -webkit-app-region: drag;
71
+ }
72
+
73
+ .mac-desktop .brand {
74
+ padding-left: 72px;
75
+ }
76
+
77
+ .brand-mark {
78
+ color: var(--frock-text);
79
+ font-family: var(--frock-font-display);
80
+ font-size: var(--frock-text-xl);
81
+ letter-spacing: var(--frock-tracking-display);
82
+ }
83
+
84
+ .sidebar-top {
85
+ display: flex;
86
+ flex: 0 0 auto;
87
+ align-items: center;
88
+ gap: 6px;
89
+ margin: 0 2px 6px;
90
+ -webkit-app-region: no-drag;
91
+ }
92
+
93
+ .sidebar-top > *:first-child {
94
+ min-width: 0;
95
+ flex: 1 1 auto;
96
+ }
97
+
98
+ .bot-list {
99
+ min-height: 0;
100
+ flex: 1 1 auto;
101
+ overflow: auto;
102
+ padding-top: 4px;
103
+ scrollbar-color: var(--frock-scrollbar) transparent;
104
+ scrollbar-width: thin;
105
+ }
106
+
107
+ .sidebar-computer {
108
+ flex: 0 0 auto;
109
+ padding: 8px 2px 10px;
110
+ border-top: 1px solid var(--frock-border);
111
+ }
112
+
113
+ .sidebar-bottom {
114
+ display: grid;
115
+ gap: 2px;
116
+ margin-top: 0;
117
+ padding-top: 10px;
118
+ border-top: 1px solid var(--frock-border);
119
+ }
120
+
121
+ /* Workspace */
122
+
123
+ .workspace {
124
+ position: relative;
125
+ min-width: 0;
126
+ min-height: 0;
127
+ background: var(--frock-surface);
128
+ }
129
+
130
+ .topbar {
131
+ display: flex;
132
+ height: var(--frock-titlebar-height);
133
+ align-items: center;
134
+ gap: 12px;
135
+ padding: 0 64px 0 18px;
136
+ border-bottom: 1px solid var(--frock-border);
137
+ -webkit-app-region: drag;
138
+ }
139
+
140
+ .topbar > :last-child:not(.workspace-title) {
141
+ flex: 0 0 auto;
142
+ -webkit-app-region: no-drag;
143
+ }
144
+
145
+ .bot-identity {
146
+ display: grid;
147
+ flex: 0 0 auto;
148
+ place-items: center;
149
+ -webkit-app-region: no-drag;
150
+ }
151
+
152
+ .workspace-title {
153
+ display: flex;
154
+ min-width: 0;
155
+ flex: 1;
156
+ flex-direction: column;
157
+ line-height: var(--frock-leading-tight);
158
+ }
159
+
160
+ .workspace-title strong {
161
+ overflow: hidden;
162
+ color: var(--frock-text);
163
+ font-size: var(--frock-text-lg);
164
+ font-weight: 600;
165
+ white-space: nowrap;
166
+ text-overflow: ellipsis;
167
+ }
168
+
169
+ .workspace-title small {
170
+ overflow: hidden;
171
+ margin-top: 3px;
172
+ color: var(--frock-text-muted);
173
+ font-size: var(--frock-text-sm);
174
+ white-space: nowrap;
175
+ text-overflow: ellipsis;
176
+ }
177
+
178
+ .model-setup-link {
179
+ width: max-content;
180
+ max-width: 100%;
181
+ margin-top: 3px;
182
+ overflow: hidden;
183
+ border: 0;
184
+ padding: 0;
185
+ color: var(--frock-action-primary);
186
+ background: transparent;
187
+ font: inherit;
188
+ font-size: var(--frock-text-sm);
189
+ text-align: left;
190
+ text-overflow: ellipsis;
191
+ white-space: nowrap;
192
+ cursor: pointer;
193
+ }
194
+
195
+ .model-setup-link:hover {
196
+ color: var(--frock-action-primary-hover);
197
+ }
198
+
199
+ /*
200
+ * The panel collapse control is pinned to the window's trailing edge. Feature
201
+ * actions live in the panel's own header.
202
+ */
203
+ .window-actions {
204
+ position: absolute;
205
+ z-index: 20;
206
+ top: 0;
207
+ right: 0;
208
+ display: flex;
209
+ height: var(--frock-titlebar-height);
210
+ align-items: center;
211
+ gap: 2px;
212
+ padding: 0 12px;
213
+ -webkit-app-region: no-drag;
214
+ }
215
+
216
+ /* Thread */
217
+
218
+ .thread {
219
+ position: absolute;
220
+ top: var(--frock-titlebar-height);
221
+ right: 0;
222
+ bottom: 84px;
223
+ left: 0;
224
+ overflow-y: auto;
225
+ padding: 16px 20px 24px;
226
+ background-color: var(--frock-surface);
227
+ background-image: var(--frock-thread-gradient);
228
+ scrollbar-color: var(--frock-scrollbar) transparent;
229
+ scrollbar-width: thin;
230
+ }
231
+
232
+ .empty-thread {
233
+ display: grid;
234
+ min-height: 100%;
235
+ place-content: center;
236
+ justify-items: center;
237
+ padding: 30px;
238
+ color: var(--frock-text-muted);
239
+ text-align: center;
240
+ animation: frock-fade-in var(--frock-motion-enter) both;
241
+ }
242
+
243
+ .empty-mark {
244
+ display: grid;
245
+ width: 44px;
246
+ height: 44px;
247
+ place-items: center;
248
+ margin-bottom: 14px;
249
+ border-radius: 14px;
250
+ color: var(--frock-action-primary-hover);
251
+ background: var(--frock-surface-accent);
252
+ }
253
+
254
+ .empty-thread h1 {
255
+ margin: 0;
256
+ color: var(--frock-text);
257
+ font-family: var(--frock-font-display);
258
+ font-size: var(--frock-text-2xl);
259
+ font-weight: 400;
260
+ letter-spacing: var(--frock-tracking-display);
261
+ }
262
+
263
+ .empty-thread p {
264
+ max-width: 340px;
265
+ margin: 6px 0 0;
266
+ color: var(--frock-text-muted);
267
+ font-size: var(--frock-text-md);
268
+ line-height: var(--frock-leading-normal);
269
+ }
270
+
271
+ .message {
272
+ display: flex;
273
+ width: 100%;
274
+ flex-direction: column;
275
+ margin-top: 6px;
276
+ animation: frock-rise-in var(--frock-motion-enter) both;
277
+ }
278
+
279
+ .message-user {
280
+ align-items: flex-end;
281
+ margin-top: 18px;
282
+ }
283
+
284
+ /* A Session announcement: the conversation narrating itself, not a party. */
285
+ .message-system {
286
+ align-items: center;
287
+ margin-top: 14px;
288
+ }
289
+
290
+ .message-system-line {
291
+ margin: 0;
292
+ padding: 4px 10px;
293
+ border-radius: var(--frock-radius-control);
294
+ background: var(--frock-surface-subtle);
295
+ color: var(--frock-text-muted);
296
+ font-size: var(--frock-text-xs);
297
+ text-align: center;
298
+ }
299
+
300
+ /* An assistant Turn is its avatar and, once there is text, its bubble. */
301
+ .message-assistant {
302
+ flex-direction: row;
303
+ align-items: flex-start;
304
+ gap: 8px;
305
+ }
306
+
307
+ .bot-avatar {
308
+ position: relative;
309
+ display: grid;
310
+ width: var(--frock-avatar-sm);
311
+ height: var(--frock-avatar-sm);
312
+ flex: 0 0 auto;
313
+ place-items: center;
314
+ margin-top: 2px;
315
+ }
316
+
317
+ .bot-avatar-fallback {
318
+ display: grid;
319
+ width: var(--frock-avatar-sm);
320
+ height: var(--frock-avatar-sm);
321
+ place-items: center;
322
+ border-radius: 9px;
323
+ color: var(--frock-action-primary-hover);
324
+ background: var(--frock-surface-accent);
325
+ }
326
+
327
+ /*
328
+ * A Package that fills `frockbot.bot-avatar` renders beside this tile, which
329
+ * then stops being the only child and steps aside.
330
+ */
331
+ .bot-avatar-fallback:not(:only-child) {
332
+ display: none;
333
+ }
334
+
335
+ /*
336
+ * The avatar carries the running state: a fast breath with a halo while the
337
+ * Turn has produced nothing yet, a slower one once text is arriving. The
338
+ * reduced-motion block at the end of this file stops both.
339
+ */
340
+ .bot-avatar-live {
341
+ animation: frock-breathe 2600ms ease-in-out infinite;
342
+ }
343
+
344
+ .bot-avatar-live::after {
345
+ position: absolute;
346
+ border-radius: 12px;
347
+ animation: frock-halo 2600ms ease-in-out infinite;
348
+ box-shadow: 0 0 0 2px var(--frock-surface-accent);
349
+ content: "";
350
+ inset: -4px;
351
+ opacity: 0;
352
+ pointer-events: none;
353
+ }
354
+
355
+ .bot-avatar-waiting,
356
+ .bot-avatar-waiting::after {
357
+ animation-duration: 1600ms;
358
+ }
359
+
360
+ @keyframes frock-breathe {
361
+ 0%,
362
+ 100% {
363
+ transform: scale(1);
364
+ }
365
+
366
+ 50% {
367
+ transform: scale(1.07);
368
+ }
369
+ }
370
+
371
+ @keyframes frock-halo {
372
+ 0%,
373
+ 100% {
374
+ opacity: 0;
375
+ transform: scale(0.94);
376
+ }
377
+
378
+ 50% {
379
+ opacity: 1;
380
+ transform: scale(1.06);
381
+ }
382
+ }
383
+
384
+ .message-bubble {
385
+ width: max-content;
386
+ max-width: min(640px, 84%);
387
+ padding: 10px 14px;
388
+ border: 1px solid var(--frock-border);
389
+ border-radius: 18px 18px 18px 6px;
390
+ color: var(--frock-text);
391
+ background: var(--frock-surface-raised);
392
+ box-shadow: var(--frock-shadow-card);
393
+ font-size: var(--frock-text-md);
394
+ line-height: var(--frock-leading-normal);
395
+ white-space: pre-wrap;
396
+ overflow-wrap: anywhere;
397
+ animation: frock-rise-in var(--frock-motion-enter) both;
398
+ }
399
+
400
+ .message-user .message-bubble {
401
+ max-width: min(640px, 84%);
402
+ border-color: var(--frock-action-primary);
403
+ border-radius: 18px 18px 6px 18px;
404
+ color: var(--frock-on-accent);
405
+ background: var(--frock-action-primary);
406
+ }
407
+
408
+ /*
409
+ * User-facing sends. They stack under the Bot's avatar in the same column the
410
+ * text bubble occupies, so a Turn that only sent a widget still reads as the
411
+ * Bot speaking.
412
+ */
413
+
414
+ .message-sends {
415
+ display: flex;
416
+ flex-direction: column;
417
+ gap: 8px;
418
+ width: min(640px, 84%);
419
+ }
420
+
421
+ /*
422
+ * Dispatched subagents. A chip is deliberately quiet — a subagent is work the
423
+ * Bot delegated, not something it said — and opens in place to the summary the
424
+ * child handed back. The child's own transcript is never here.
425
+ */
426
+
427
+ .message-tasks {
428
+ display: flex;
429
+ flex-direction: column;
430
+ gap: 6px;
431
+ width: min(640px, 84%);
432
+ }
433
+
434
+ .task-chip {
435
+ display: flex;
436
+ flex-wrap: wrap;
437
+ gap: 8px;
438
+ align-items: baseline;
439
+ padding: 8px 10px;
440
+ font: inherit;
441
+ color: var(--frock-text);
442
+ text-align: left;
443
+ cursor: pointer;
444
+ background: var(--frock-surface);
445
+ border: 1px solid var(--frock-border);
446
+ border-radius: 10px;
447
+ }
448
+
449
+ .task-chip-type {
450
+ font-weight: 600;
451
+ }
452
+
453
+ .task-chip-description {
454
+ flex: 1 1 auto;
455
+ }
456
+
457
+ .task-chip-status,
458
+ .task-chip-model {
459
+ color: var(--frock-text-muted);
460
+ font-size: var(--frock-text-xs);
461
+ }
462
+
463
+ .task-chip-completed .task-chip-status {
464
+ color: var(--frock-text);
465
+ }
466
+
467
+ .task-chip-failed .task-chip-status,
468
+ .task-chip-stopped .task-chip-status {
469
+ color: var(--frock-danger-text);
470
+ }
471
+
472
+ .task-chip-summary {
473
+ flex: 1 0 100%;
474
+ color: var(--frock-text-muted);
475
+ white-space: pre-wrap;
476
+ }
477
+
478
+ .task-chip-stop {
479
+ flex: 0 0 auto;
480
+ padding: 2px 8px;
481
+ color: var(--frock-danger-text);
482
+ font-size: var(--frock-text-xs);
483
+ cursor: pointer;
484
+ border: 1px solid var(--frock-danger-border);
485
+ border-radius: 8px;
486
+ }
487
+
488
+ /* Jump to latest */
489
+
490
+ .jump-latest {
491
+ position: absolute;
492
+ z-index: 6;
493
+ right: 24px;
494
+ bottom: 92px;
495
+ color: var(--frock-text);
496
+ background: var(--frock-surface-raised);
497
+ box-shadow: var(--frock-shadow-card);
498
+ }
499
+
500
+ /* Status banner */
501
+
502
+ .error-banner {
503
+ position: absolute;
504
+ right: 20px;
505
+ bottom: 84px;
506
+ left: 20px;
507
+ z-index: 5;
508
+ display: flex;
509
+ min-height: 38px;
510
+ align-items: center;
511
+ justify-content: space-between;
512
+ gap: 12px;
513
+ padding: 7px 12px;
514
+ border: 1px solid var(--frock-danger-border);
515
+ border-radius: 12px;
516
+ color: var(--frock-danger-text);
517
+ background: var(--frock-danger-surface);
518
+ box-shadow: var(--frock-shadow-card);
519
+ font-size: var(--frock-text-sm);
520
+ }
521
+
522
+ .error-banner button {
523
+ flex: 0 0 auto;
524
+ padding: 5px 10px;
525
+ border-radius: 999px;
526
+ color: var(--frock-on-accent);
527
+ background: var(--frock-danger-text);
528
+ font-weight: 600;
529
+ cursor: pointer;
530
+ }
531
+
532
+ .banner-enter-active,
533
+ .banner-leave-active {
534
+ transition:
535
+ opacity var(--frock-motion-fast),
536
+ transform var(--frock-motion-fast);
537
+ }
538
+
539
+ .banner-enter-from,
540
+ .banner-leave-to {
541
+ opacity: 0;
542
+ transform: translateY(6px);
543
+ }
544
+
545
+ /* Composer */
546
+
547
+ .composer {
548
+ position: absolute;
549
+ right: 20px;
550
+ bottom: 18px;
551
+ left: 20px;
552
+ display: flex;
553
+ min-height: 52px;
554
+ align-items: flex-end;
555
+ gap: 8px;
556
+ padding: 8px 8px 8px 18px;
557
+ border: 1px solid var(--frock-border-strong);
558
+ border-radius: 26px;
559
+ background: var(--frock-surface-raised);
560
+ box-shadow: var(--frock-shadow-composer);
561
+ transition:
562
+ border-color var(--frock-motion-fast),
563
+ box-shadow var(--frock-motion-fast);
564
+ }
565
+
566
+ .composer:focus-within {
567
+ border-color: var(--frock-border-strong);
568
+ box-shadow:
569
+ var(--frock-shadow-composer),
570
+ 0 0 0 3px var(--frock-focus-ring);
571
+ }
572
+
573
+ .composer-model-setup {
574
+ min-width: 0;
575
+ flex: 1;
576
+ align-self: center;
577
+ }
578
+
579
+ .composer textarea {
580
+ box-sizing: border-box;
581
+ width: 100%;
582
+ min-width: 0;
583
+ min-height: 36px;
584
+ max-height: 120px;
585
+ flex: 1;
586
+ align-self: stretch;
587
+ resize: none;
588
+ border: 0;
589
+ outline: 0;
590
+ padding: 8px 0;
591
+ color: var(--frock-text);
592
+ background: transparent;
593
+ font-size: var(--frock-text-md);
594
+ line-height: var(--frock-leading-normal);
595
+ text-align: left;
596
+ }
597
+
598
+ .composer textarea::placeholder {
599
+ color: var(--frock-text-subtle);
600
+ }
601
+
602
+ .composer textarea:disabled {
603
+ color: var(--frock-text-muted);
604
+ }
605
+
606
+ /*
607
+ * Skill invocation. The popover floats above the composer rather than pushing
608
+ * it, so the message the User is writing never moves under their caret; the
609
+ * chips sit inside the composer with the textarea because they are part of the
610
+ * message being composed, not a separate control.
611
+ */
612
+
613
+ .composer-body {
614
+ display: flex;
615
+ width: 100%;
616
+ min-width: 0;
617
+ flex: 1;
618
+ flex-direction: column;
619
+ align-self: stretch;
620
+ gap: 6px;
621
+ }
622
+
623
+ .skill-chips {
624
+ display: flex;
625
+ margin: 0;
626
+ padding: 0;
627
+ flex-wrap: wrap;
628
+ gap: 6px;
629
+ list-style: none;
630
+ }
631
+
632
+ .skill-chip {
633
+ display: inline-flex;
634
+ align-items: center;
635
+ gap: 4px;
636
+ padding: 2px 6px 2px 8px;
637
+ border: 1px solid var(--frock-accent-border);
638
+ border-radius: var(--frock-radius-control);
639
+ color: var(--frock-accent-text);
640
+ background: var(--frock-accent-surface);
641
+ cursor: pointer;
642
+ font-size: var(--frock-text-xs);
643
+ line-height: var(--frock-leading-tight);
644
+ }
645
+
646
+ .skill-chip:hover {
647
+ border-color: var(--frock-border-strong);
648
+ }
649
+
650
+ .skill-chip-name {
651
+ max-width: 160px;
652
+ overflow: hidden;
653
+ text-overflow: ellipsis;
654
+ white-space: nowrap;
655
+ }
656
+
657
+ .skill-popover {
658
+ position: absolute;
659
+ right: 0;
660
+ bottom: calc(100% + 8px);
661
+ left: 0;
662
+ z-index: var(--frock-layer-menu);
663
+ max-height: 240px;
664
+ margin: 0;
665
+ padding: 4px;
666
+ overflow-y: auto;
667
+ border: 1px solid var(--frock-border);
668
+ border-radius: var(--frock-radius-card);
669
+ background: var(--frock-surface-raised);
670
+ box-shadow: var(--frock-shadow-floating);
671
+ list-style: none;
672
+ }
673
+
674
+ .skill-option {
675
+ display: grid;
676
+ padding: 6px 8px;
677
+ border-radius: var(--frock-radius-control);
678
+ cursor: pointer;
679
+ grid-template-columns: auto 1fr;
680
+ gap: 2px 8px;
681
+ }
682
+
683
+ .skill-option-active {
684
+ background: var(--frock-fill-hover);
685
+ }
686
+
687
+ .skill-option-name {
688
+ color: var(--frock-text);
689
+ font-size: var(--frock-text-base);
690
+ line-height: var(--frock-leading-tight);
691
+ }
692
+
693
+ .skill-option-ref {
694
+ color: var(--frock-text-subtle);
695
+ font-family: var(--frock-font-mono);
696
+ font-size: var(--frock-text-xs);
697
+ line-height: var(--frock-leading-tight);
698
+ }
699
+
700
+ .skill-option-description {
701
+ overflow: hidden;
702
+ color: var(--frock-text-muted);
703
+ font-size: var(--frock-text-xs);
704
+ grid-column: 1 / -1;
705
+ line-height: var(--frock-leading-snug);
706
+ text-overflow: ellipsis;
707
+ white-space: nowrap;
708
+ }
709
+
710
+ .composer-busy .stop-button {
711
+ background: var(--frock-danger-text);
712
+ }
713
+
714
+ /* Right panel */
715
+
716
+ .right-panel {
717
+ min-width: 0;
718
+ overflow: hidden;
719
+ border-left: 1px solid var(--frock-border);
720
+ background: var(--frock-surface-sidebar);
721
+ }
722
+
723
+ .right-panel-stack {
724
+ position: relative;
725
+ width: 100%;
726
+ height: 100%;
727
+ }
728
+
729
+ .right-panel-content {
730
+ position: absolute;
731
+ inset: 0;
732
+ display: flex;
733
+ width: var(--frock-panel-width);
734
+ min-height: 0;
735
+ flex-direction: column;
736
+ }
737
+
738
+ .right-panel-header {
739
+ display: flex;
740
+ height: var(--frock-titlebar-height);
741
+ flex: 0 0 auto;
742
+ align-items: center;
743
+ justify-content: flex-end;
744
+ padding: 0 52px 0 12px;
745
+ border-bottom: 1px solid var(--frock-border);
746
+ }
747
+
748
+ .right-panel-body {
749
+ min-height: 0;
750
+ flex: 1;
751
+ overflow-y: auto;
752
+ padding: 16px;
753
+ scrollbar-color: var(--frock-scrollbar) transparent;
754
+ scrollbar-width: thin;
755
+ }
756
+
757
+ .panel-surface-view {
758
+ position: absolute;
759
+ inset: 0;
760
+ display: flex;
761
+ width: var(--panel-surface-width);
762
+ min-height: 0;
763
+ flex-direction: column;
764
+ background: var(--frock-surface);
765
+ }
766
+
767
+ /*
768
+ * The title and back control belong to the surface header; only the generic
769
+ * collapse control floats over its trailing edge.
770
+ */
771
+ .panel-surface-header {
772
+ display: flex;
773
+ height: var(--frock-titlebar-height);
774
+ flex: 0 0 auto;
775
+ align-items: center;
776
+ gap: 8px;
777
+ padding: 0 52px 0 10px;
778
+ border-bottom: 1px solid var(--frock-border);
779
+ }
780
+
781
+ .panel-surface-header h2 {
782
+ overflow: hidden;
783
+ margin: 0;
784
+ color: var(--frock-text);
785
+ font-size: var(--frock-text-md);
786
+ font-weight: 600;
787
+ white-space: nowrap;
788
+ text-overflow: ellipsis;
789
+ }
790
+
791
+ .panel-surface-content {
792
+ min-height: 0;
793
+ flex: 1;
794
+ overflow-y: auto;
795
+ padding: 16px;
796
+ scrollbar-color: var(--frock-scrollbar) transparent;
797
+ scrollbar-width: thin;
798
+ }
799
+
800
+ .panel-swap-enter-active,
801
+ .panel-swap-leave-active {
802
+ transition:
803
+ opacity var(--frock-motion-panel),
804
+ transform var(--frock-motion-panel);
805
+ }
806
+
807
+ .panel-swap-enter-from,
808
+ .panel-swap-leave-to {
809
+ opacity: 0;
810
+ transform: translateX(12px);
811
+ }
812
+
813
+ /* Responsive */
814
+
815
+ @media (max-width: 980px) {
816
+ .app-shell.panel-open {
817
+ --panel-current: 0px;
818
+ }
819
+
820
+ .app-shell.panel-surface .right-panel {
821
+ width: var(--panel-surface-width);
822
+ }
823
+
824
+ /*
825
+ * A parked panel is hidden as well as off-canvas: it is not something to
826
+ * tab into, read out, or hit-test, and `visibility` is the one property that
827
+ * says all three and still animates.
828
+ */
829
+ .right-panel {
830
+ position: absolute;
831
+ z-index: 15;
832
+ top: 0;
833
+ right: 0;
834
+ bottom: 0;
835
+ width: var(--frock-panel-width);
836
+ box-shadow: var(--frock-shadow-right-panel);
837
+ transform: translateX(100%);
838
+ visibility: hidden;
839
+ transition:
840
+ transform var(--frock-motion-panel),
841
+ visibility var(--frock-motion-panel);
842
+ }
843
+
844
+ .app-shell.panel-open .right-panel {
845
+ transform: translateX(0);
846
+ visibility: visible;
847
+ }
848
+ }
849
+
850
+ /*
851
+ * Phone.
852
+ *
853
+ * A hand-held viewport has room for exactly one column, so the shell stops
854
+ * being three columns and becomes the conversation with two drawers over it:
855
+ * the Bot list from the leading edge, the right panel from the trailing one.
856
+ * Squeezing all three instead is what the 190px sidebar above used to do, and
857
+ * at 390px it left the conversation about a hundred points wide with every
858
+ * label clipped mid-word.
859
+ *
860
+ * Only one drawer is ever open: opening either closes the other, in the
861
+ * component, so this layer never has to reason about them overlapping.
862
+ */
863
+ @media (max-width: 640px) {
864
+ .frockbot-root {
865
+ height: var(--frock-viewport-height);
866
+ }
867
+
868
+ .app-shell,
869
+ .app-shell.panel-open,
870
+ .app-shell.panel-open.panel-surface {
871
+ --panel-surface-width: 100%;
872
+ grid-template-columns: minmax(0, 1fr);
873
+ }
874
+
875
+ /*
876
+ * Off canvas and inert. `visibility` rather than transform alone so a closed
877
+ * drawer is hidden to assistive technology and to a hit test, and so it
878
+ * still animates: the property is transitioned, not switched.
879
+ */
880
+ .sidebar {
881
+ position: absolute;
882
+ z-index: 25;
883
+ top: 0;
884
+ bottom: 0;
885
+ left: 0;
886
+ width: var(--frock-drawer-width);
887
+ /* `calc` so the declaration is a length the checker can type; `env` alone
888
+ is not one it knows. */
889
+ padding-top: calc(var(--frock-safe-top) + 0px);
890
+ box-shadow: var(--frock-shadow-panel);
891
+ transform: translateX(-100%);
892
+ visibility: hidden;
893
+ transition:
894
+ transform var(--frock-motion-panel),
895
+ visibility var(--frock-motion-panel);
896
+ }
897
+
898
+ .app-shell.nav-open .sidebar {
899
+ transform: translateX(0);
900
+ visibility: visible;
901
+ }
902
+
903
+ /* The drawers dim the conversation they cover, and the dimmed area closes them. */
904
+ /*
905
+ * Under both drawers and over the workspace: the sidebar sits at 25 and the
906
+ * right panel at 15, so one scrim serves whichever is open.
907
+ */
908
+ .nav-scrim {
909
+ position: absolute;
910
+ z-index: 14;
911
+ inset: 0;
912
+ border: 0;
913
+ padding: 0;
914
+ background: var(--frock-overlay-tint);
915
+ cursor: pointer;
916
+ }
917
+
918
+ .scrim-enter-active,
919
+ .scrim-leave-active {
920
+ transition: opacity var(--frock-motion-panel);
921
+ }
922
+
923
+ .scrim-enter-from,
924
+ .scrim-leave-to {
925
+ opacity: 0;
926
+ }
927
+
928
+ /* The right panel is a drawer too, and on a phone it takes the whole width. */
929
+ .right-panel,
930
+ .app-shell.panel-surface .right-panel {
931
+ width: 100%;
932
+ }
933
+
934
+ .right-panel-content,
935
+ .panel-surface-view {
936
+ width: 100%;
937
+ }
938
+
939
+ /* There is no window chrome to drag and no traffic lights to clear. */
940
+ .brand {
941
+ padding-left: 0;
942
+ }
943
+
944
+ .mac-desktop .brand {
945
+ padding-left: 0;
946
+ }
947
+
948
+ .topbar {
949
+ gap: 8px;
950
+ padding: 0 100px 0 4px;
951
+ }
952
+
953
+ .brand-mark {
954
+ font-size: var(--frock-text-lg);
955
+ }
956
+
957
+ .window-actions {
958
+ padding: 0 8px;
959
+ }
960
+
961
+ .nav-toggle {
962
+ flex: 0 0 auto;
963
+ }
964
+
965
+ /*
966
+ * The conversation. Its horizontal room is the whole window now, so the
967
+ * gutters shrink and a bubble may use nearly all of what is left; the
968
+ * vertical anchors all clear the home indicator.
969
+ */
970
+ .thread {
971
+ bottom: calc(76px + var(--frock-safe-bottom));
972
+ padding: 12px 12px 20px;
973
+ }
974
+
975
+ .empty-thread {
976
+ padding: 20px;
977
+ }
978
+
979
+ .message-bubble,
980
+ .message-user .message-bubble {
981
+ max-width: 92%;
982
+ }
983
+
984
+ .message-sends,
985
+ .message-tasks {
986
+ width: 92%;
987
+ }
988
+
989
+ .message-attachments img {
990
+ max-width: 100%;
991
+ }
992
+
993
+ .error-banner {
994
+ right: 10px;
995
+ bottom: calc(76px + var(--frock-safe-bottom));
996
+ left: 10px;
997
+ }
998
+
999
+ .jump-latest {
1000
+ right: 14px;
1001
+ bottom: calc(84px + var(--frock-safe-bottom));
1002
+ }
1003
+
1004
+ .composer {
1005
+ right: 10px;
1006
+ bottom: calc(10px + var(--frock-safe-bottom));
1007
+ left: 10px;
1008
+ padding: 6px 6px 6px 14px;
1009
+ }
1010
+
1011
+ .composer textarea {
1012
+ max-height: 96px;
1013
+ font-size: var(--frock-text-input);
1014
+ }
1015
+
1016
+ .skill-chip-name {
1017
+ max-width: 120px;
1018
+ }
1019
+ }
1020
+
1021
+ @media (prefers-reduced-motion: reduce) {
1022
+ *,
1023
+ *::before,
1024
+ *::after {
1025
+ animation-duration: 0.01ms !important;
1026
+ animation-iteration-count: 1 !important;
1027
+ scroll-behavior: auto !important;
1028
+ }
1029
+ }
1030
+
1031
+ .message-attachments {
1032
+ display: flex;
1033
+ flex-wrap: wrap;
1034
+ gap: var(--frockbot-space-2, 0.5rem);
1035
+ margin-top: var(--frockbot-space-2, 0.5rem);
1036
+ }
1037
+
1038
+ .message-attachments img {
1039
+ border: 1px solid var(--frock-border);
1040
+ border-radius: var(--frockbot-radius-small, 4px);
1041
+ display: block;
1042
+ max-width: 20rem;
1043
+ }