@jossuealcala/madre 0.5.1 → 0.6.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.
- package/CHANGELOG.md +95 -0
- package/package.json +1 -1
- package/public/abduction.js +384 -0
- package/public/app.js +1174 -78
- package/public/assets/abduction-cat-blink.png +0 -0
- package/public/assets/abduction-cat-control-left.png +0 -0
- package/public/assets/abduction-cat-control-right.png +0 -0
- package/public/assets/abduction-cat.png +0 -0
- package/public/es.js +182 -16
- package/public/index.html +48 -3
- package/public/routing.js +74 -0
- package/public/styles.css +390 -22
- package/src/choices.mjs +64 -0
- package/src/file-ops.mjs +45 -0
- package/src/modules/ash-policy.mjs +67 -0
- package/src/modules/ash.mjs +1 -1
- package/src/modules/choices.mjs +34 -0
- package/src/modules/index.mjs +2 -1
- package/src/modules/playwright.mjs +5 -1
- package/src/modules/ripley.mjs +13 -1
- package/src/ports.mjs +71 -0
- package/src/room/economy.mjs +83 -1
- package/src/room/prompt.mjs +13 -1
- package/src/room.mjs +91 -9
- package/src/server.mjs +127 -7
package/public/styles.css
CHANGED
|
@@ -140,13 +140,13 @@ body {
|
|
|
140
140
|
.bar, .onboarding, .composer, .safety, .thread-inner { width: var(--column); margin-left: auto; margin-right: auto; }
|
|
141
141
|
/* The bar is the ship's console: wider than the thread so the project root, the crew and the alerts share one line. */
|
|
142
142
|
.shell > .bar { width: min(1280px, 100% - 40px); }
|
|
143
|
-
.shell > .bar { grid-row: 1; } .shell > .onboarding { grid-row: 2; } .shell > .thread { grid-row: 3; } .shell > .composer { grid-row: 4; } .shell > .safety { grid-row: 5; }
|
|
143
|
+
.shell > .bar { grid-row: 1; } .shell > .onboarding { grid-row: 2; } .shell > .thread { grid-row: 3; } .shell > .composer { grid-row: 4; } .shell > .safety { max-width: min(100%, 150ch); margin-inline: auto; grid-row: 5; }
|
|
144
144
|
.shell > .bar, .shell > .onboarding, .shell > .thread, .shell > .composer, .shell > .safety, .shell > .tree, .shell > .chats-button { grid-column: 1; }
|
|
145
145
|
.visually-hidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
146
146
|
|
|
147
147
|
/* ================= bar ================= */
|
|
148
148
|
.bar {
|
|
149
|
-
display: flex; align-items: center; gap: 18px;
|
|
149
|
+
display: flex; align-items: center; gap: 18px; min-width: 0;
|
|
150
150
|
padding: 18px 0 14px;
|
|
151
151
|
border-bottom: 1px solid var(--line);
|
|
152
152
|
font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
|
|
@@ -189,7 +189,10 @@ body {
|
|
|
189
189
|
.train-steps li.note { list-style: none; margin-left: -18px; }
|
|
190
190
|
.train-step-label { font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--text-3); }
|
|
191
191
|
.project::before { content: "/ "; }
|
|
192
|
-
|
|
192
|
+
/* 3px of ring on each side plus air between them: anything under 10px and the neighbours clip
|
|
193
|
+
each other's arc. The padding is the same 3px, so the row's box contains the rings it draws —
|
|
194
|
+
without it the first and last ring hang outside and collide with whatever sits beside them. */
|
|
195
|
+
.agents { display: flex; gap: 12px; padding: 0 3px; margin-left: 0; flex: none; }
|
|
193
196
|
.connection { display: inline-flex; flex: 0 0 auto; white-space: nowrap; align-items: center; gap: 6px; color: var(--text-3); text-transform: uppercase; font-size: 10px; }
|
|
194
197
|
.connection-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); }
|
|
195
198
|
.connection[data-state="live"] .connection-dot { background: var(--phosphor); box-shadow: 0 0 10px var(--phosphor); }
|
|
@@ -201,16 +204,22 @@ body {
|
|
|
201
204
|
--size: 28px; --pct: 0;
|
|
202
205
|
position: relative; display: inline-grid; place-items: center;
|
|
203
206
|
width: var(--size); height: var(--size); border-radius: 50%;
|
|
204
|
-
color: var(--agent); background: color-mix(in srgb, var(--agent)
|
|
207
|
+
color: var(--agent); background: color-mix(in srgb, var(--agent) 17%, transparent);
|
|
205
208
|
flex: none;
|
|
206
209
|
}
|
|
207
210
|
.avatar svg { width: 58%; height: 58%; display: block; }
|
|
211
|
+
/* The ring is the agent's quota, drawn around it. Two things were wrong with it: it sits 3px
|
|
212
|
+
outside the avatar's own box, so with a 6px gap the rings of neighbours overlapped and each
|
|
213
|
+
arc came out flattened where the next one cut into it — the "cut bubbles" on screen. And the
|
|
214
|
+
unused part of the arc was drawn in --line-2, a full-strength grey that outweighed the colour
|
|
215
|
+
it was supposed to frame, so every agent read as a grey circle with a tint inside.
|
|
216
|
+
|
|
217
|
+
Now the track is a whisper and the used arc is the only thing with weight. */
|
|
208
218
|
.avatar.ring::before {
|
|
209
219
|
content: ""; position: absolute; inset: -3px; border-radius: 50%;
|
|
210
|
-
background: conic-gradient(var(--agent) calc(var(--pct) * 1%), var(--
|
|
211
|
-
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% -
|
|
212
|
-
mask: radial-gradient(farthest-side, transparent calc(100% -
|
|
213
|
-
opacity: .9;
|
|
220
|
+
background: conic-gradient(var(--agent) calc(var(--pct) * 1%), color-mix(in srgb, var(--text-3) 22%, transparent) 0);
|
|
221
|
+
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
|
|
222
|
+
mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
|
|
214
223
|
}
|
|
215
224
|
.avatar.ring.hot::before { background: conic-gradient(var(--crit) calc(var(--pct) * 1%), var(--line-2) 0); }
|
|
216
225
|
.avatar.offline { color: var(--text-3); background: transparent; border: 1px dashed var(--line-2); }
|
|
@@ -304,19 +313,94 @@ code { font-family: var(--mono); font-size: .9em; color: var(--phosphor); backgr
|
|
|
304
313
|
.row.thinking .tokens { font-family: var(--mono); font-size: 10px; letter-spacing: .08em; color: var(--text-3); margin-left: 8px; opacity: .75; }
|
|
305
314
|
/* What a reply cost, on the same line as everything else above the bubble: no pill, no wrap. */
|
|
306
315
|
.who .spent { flex: none; margin-left: auto; padding: 0; border: 0; background: none; font-family: var(--mono); font-size: 10px; letter-spacing: .04em; color: var(--text-3); white-space: nowrap; }
|
|
316
|
+
.who .spent.ash { color: var(--brew); }
|
|
317
|
+
.stamp .uncharged { color: var(--brew); }
|
|
318
|
+
.ash-observed { margin-top: 12px; }
|
|
319
|
+
.economy-headline { font-family: var(--mono); font-size: clamp(38px, 5vw, 56px); line-height: 1.1; letter-spacing: -.01em; color: var(--ph); margin: 2px 0 12px; }
|
|
320
|
+
/* The panel is a screen, not a column. Sections pair up once there is room for two, and the
|
|
321
|
+
ones that carry a table or a full-width strip keep the whole width to themselves. */
|
|
322
|
+
#economy-body { display: grid; grid-template-columns: 1fr; gap: 0 56px; align-content: start; }
|
|
323
|
+
@media (min-width: 1180px) { #economy-body { grid-template-columns: 1fr 1fr; } #economy-body > .wide, #economy-body > .economy-strip { grid-column: 1 / -1; } }
|
|
324
|
+
.economy .card-block { display: grid; gap: 10px; align-content: start; }
|
|
325
|
+
.economy .card-block { margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--ph-faint); }
|
|
326
|
+
.economy .card-block:first-of-type { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
327
|
+
|
|
328
|
+
/* The room in six figures, read across before anything is read down. */
|
|
329
|
+
.economy-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px 32px; margin: 4px 0 8px; padding-bottom: 26px; border-bottom: 1px solid var(--ph-dim); }
|
|
330
|
+
.economy-stat { display: grid; gap: 5px; align-content: start; min-width: 0; }
|
|
331
|
+
.economy-stat b { font-family: var(--mono); font-size: 22px; font-weight: 600; line-height: 1.1; color: var(--ph); }
|
|
332
|
+
.economy-stat span { font-family: var(--mono); font-size: 8.5px; letter-spacing: .18em; color: var(--ph-dim); }
|
|
333
|
+
|
|
334
|
+
/* A table is read by looking one row up, so the grid stays recessive and the numbers line up. */
|
|
335
|
+
.economy-table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 11px; }
|
|
336
|
+
.economy-table th, .economy-table td { padding: 7px 10px; text-align: right; border-bottom: 1px solid var(--ph-faint); white-space: nowrap; }
|
|
337
|
+
.economy-table thead th { font-size: 8.5px; font-weight: 400; letter-spacing: .18em; color: var(--ph-dim); border-bottom-color: var(--ph-dim); }
|
|
338
|
+
.economy-table tbody th { text-align: left; font-weight: 600; letter-spacing: .14em; color: var(--ph-dim); }
|
|
339
|
+
.economy-table td { color: var(--ph); }
|
|
340
|
+
.economy-table tbody tr:last-child th, .economy-table tbody tr:last-child td { border-bottom: 0; }
|
|
341
|
+
.economy .card-block h5 { margin: 0; font-size: 10px; font-weight: 400; letter-spacing: .28em; color: var(--ph-dim); }
|
|
342
|
+
.economy .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 230px)); justify-content: start; gap: 24px 40px; }
|
|
343
|
+
.economy .metric { display: grid; gap: 6px; align-content: start; min-width: 0; }
|
|
344
|
+
.economy .metric b { font-family: var(--mono); font-size: 26px; font-weight: 600; line-height: 1.1; color: var(--ph); }
|
|
345
|
+
.economy .metric span { font-family: var(--mono); font-size: 9px; line-height: 1.5; letter-spacing: .18em; color: var(--ph-dim); }
|
|
346
|
+
/* Prose is read, so it gets a measure and a size: a 60-character column, sentence case, never the
|
|
347
|
+
full width of a panel at heading scale. */
|
|
348
|
+
.economy .note { margin: 8px 0 0; max-width: 640px; font-size: 11px; line-height: 1.85; letter-spacing: .06em; color: var(--ph-dim); text-shadow: none; }
|
|
349
|
+
.economy .ash-block { grid-template-columns: 120px minmax(80px, 560px) auto; gap: 16px; }
|
|
350
|
+
.economy .ash-block b, .economy .ash-block span { font-size: 10px; }
|
|
351
|
+
.economy .ash-block i { height: 7px; }
|
|
352
|
+
|
|
353
|
+
/* One agent, two bars, one scale: the shorter bar is the shorter answer. */
|
|
354
|
+
.ash-compare { display: grid; gap: 20px; }
|
|
355
|
+
.ash-agent { display: grid; gap: 6px; }
|
|
356
|
+
.ash-agent > b { font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .2em; color: var(--ph-dim); text-shadow: none; }
|
|
357
|
+
.ash-bar { display: grid; grid-template-columns: 76px minmax(80px, 560px) auto; gap: 14px; align-items: center; }
|
|
358
|
+
.ash-bar .ash-side { font-family: var(--mono); font-size: 10px; letter-spacing: .12em; color: var(--ph-dim); text-shadow: none; }
|
|
359
|
+
.ash-bar .ash-figure { font-family: var(--mono); font-size: 10px; letter-spacing: .08em; color: var(--ph-dim); white-space: nowrap; text-shadow: none; }
|
|
360
|
+
.ash-bar i { height: 9px; background: var(--ph-faint); position: relative; }
|
|
361
|
+
.ash-bar i::after { content: ''; position: absolute; inset: 0 auto 0 0; width: var(--fill, 0%); background: color-mix(in srgb, var(--ph) 30%, transparent); }
|
|
362
|
+
.ash-bar.on i::after { background: var(--ph); }
|
|
363
|
+
.ash-bar.on .ash-figure, .ash-bar.on .ash-side { color: var(--ph); }
|
|
364
|
+
.ash-verdict { margin: 4px 0 0; font-family: var(--mono); font-size: 10px; line-height: 1.5; letter-spacing: .06em; color: var(--ph); text-shadow: none; }
|
|
365
|
+
.ash-verdict.noise { color: var(--ph-dim); }
|
|
366
|
+
.ash-waiting { margin: 0; font-family: var(--mono); font-size: 10px; letter-spacing: .08em; color: var(--ph-dim); text-shadow: none; }
|
|
367
|
+
@media (max-width: 720px) { .ash-bar { grid-template-columns: 1fr; } .ash-bar i { display: none; } }
|
|
368
|
+
.ash-observed h5 { margin: 0 0 6px; }
|
|
307
369
|
.row.thinking .elapsed.late { color: var(--warn); }
|
|
308
370
|
.row.thinking i { width: 7px; height: 7px; border-radius: 50%; background: var(--agent, var(--text-2)); animation: bounce 1.2s infinite; opacity: .5; }
|
|
309
371
|
.row.thinking i:nth-child(2) { animation-delay: .18s; } .row.thinking i:nth-child(3) { animation-delay: .36s; }
|
|
310
372
|
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: .35; } 40% { transform: translateY(-4px); opacity: 1; } }
|
|
311
373
|
|
|
312
374
|
/* system lines: handoffs, sentinel, failures — MOTHER speaks in small caps */
|
|
375
|
+
/* A line of the room talking about itself. Three things keep it orderly:
|
|
376
|
+
|
|
377
|
+
· a measure. Running the full column turns one sentence into a ragged staircase, and these are
|
|
378
|
+
read at a glance or not at all.
|
|
379
|
+
· balanced wrapping, so two lines come out the same length instead of a long one and a word.
|
|
380
|
+
· the label on its own line. CONTROL, MEMORY, MODULES name what follows; leaving them inline
|
|
381
|
+
let the break land anywhere and the eye lost which part was the heading. */
|
|
313
382
|
.system {
|
|
314
|
-
|
|
315
|
-
|
|
383
|
+
/* A definite width, not shrink-to-fit. Sized to its content, the box is as wide as its widest
|
|
384
|
+
line, so the heading centred in one box and the body wrapped inside another — which is why
|
|
385
|
+
the agent's name never lined up with the sentence under it. */
|
|
386
|
+
align-self: center; text-align: center; width: min(92%, 58ch);
|
|
387
|
+
/* `pretty`, not `balance`: balance evens the line lengths and in doing so kept pushing the
|
|
388
|
+
agent's name onto a line of its own, away from the sentence it begins. `pretty` only avoids
|
|
389
|
+
orphans, which is the actual problem these rows had. */
|
|
390
|
+
overflow-wrap: anywhere; text-wrap: pretty;
|
|
316
391
|
margin: 22px 0 8px;
|
|
317
392
|
font-family: var(--mono); font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
|
|
318
|
-
color: var(--text-3); line-height: 1.
|
|
393
|
+
color: var(--text-3); line-height: 1.7;
|
|
319
394
|
}
|
|
395
|
+
.system > b:first-child { display: block; margin-bottom: 3px; color: var(--text-3); letter-spacing: .2em; }
|
|
396
|
+
/* Everything after the heading is one sentence and has to wrap as one: a code chip that becomes
|
|
397
|
+
a block breaks the line in front of it and leaves the agent's name stranded. */
|
|
398
|
+
.system code { display: inline-block; vertical-align: baseline; }
|
|
399
|
+
/* `.who` belongs to a message's header, where it is a flex row with a left margin. Inside a
|
|
400
|
+
system line it is a word in a sentence, and inheriting that rule is what put every agent's
|
|
401
|
+
name alone on a line, indented, away from the clause it opens. */
|
|
402
|
+
.system .who { display: inline; margin: 0; white-space: nowrap; }
|
|
403
|
+
.system .who { color: var(--agent, var(--text-2)); }
|
|
320
404
|
.system b { font-weight: 600; color: var(--text-2); }
|
|
321
405
|
.system .from { color: var(--from, var(--text-2)); } .system .to { color: var(--to, var(--text-2)); }
|
|
322
406
|
.system.warn { color: var(--warn); }
|
|
@@ -565,11 +649,31 @@ pre.core-egress-log { margin: 0; max-height: 34vh; overflow: auto; padding: 10px
|
|
|
565
649
|
.memory-hint .more { font-family: var(--mono); font-size: 9.5px; color: var(--text-3); }
|
|
566
650
|
/* Memory used is not memory saved: saving is an event, using happens on most turns. Same shape so
|
|
567
651
|
there is one thing to learn, quieter so it never competes with the answer it helped write. */
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
.memory-hint.used
|
|
652
|
+
/* What the archive handed this turn: one indicator per line under its heading.
|
|
653
|
+
Legibility first — opacity on the whole group used to fade text that was already pale, and the
|
|
654
|
+
five kind colours belong to NOSTROMO's dark map, so here the colour is a dot and the text is
|
|
655
|
+
the room's own. */
|
|
656
|
+
.memory-hint.used { flex-direction: column; align-items: flex-start; margin-top: 4px; gap: 5px; animation: none; }
|
|
657
|
+
.memory-hint.used .lead { font-size: 9px; letter-spacing: .14em; color: var(--text-3); }
|
|
658
|
+
.memory-hint.used .pill {
|
|
659
|
+
gap: 7px; max-width: min(340px, 100%); padding: 3px 10px 3px 8px;
|
|
660
|
+
border: 1px solid color-mix(in srgb, var(--text-3) 26%, transparent);
|
|
661
|
+
background: color-mix(in srgb, var(--text-3) 7%, transparent);
|
|
662
|
+
color: var(--text-2); font-size: 11px; line-height: 1.45;
|
|
663
|
+
animation: none; box-shadow: none;
|
|
664
|
+
}
|
|
665
|
+
.memory-hint.used .pill .dot {
|
|
666
|
+
flex: none; width: 7px; height: 7px; border-radius: 999px; background: var(--kind);
|
|
667
|
+
/* The ring is what keeps a near-white kind visible on a near-white page. */
|
|
668
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-3) 45%, transparent);
|
|
669
|
+
}
|
|
670
|
+
.memory-hint.used .pill .what { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
671
|
+
.memory-hint.used .pill:hover { background: color-mix(in srgb, var(--text-3) 14%, transparent); color: var(--text); border-color: color-mix(in srgb, var(--text-3) 42%, transparent); }
|
|
672
|
+
/* Carried along rather than matched: the dot is hollow, the way an indicator says "inherited". */
|
|
673
|
+
.memory-hint.used .pill.carried .dot { background: transparent; box-shadow: inset 0 0 0 2px var(--kind), 0 0 0 1px color-mix(in srgb, var(--text-3) 45%, transparent); }
|
|
674
|
+
.memory-hint.used .last { display: flex; align-items: center; gap: 6px; max-width: 100%; }
|
|
675
|
+
.memory-hint.used .last .pill { min-width: 0; }
|
|
676
|
+
.memory-hint.used .more { font-size: 9.5px; }
|
|
573
677
|
@keyframes hint-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
574
678
|
@keyframes hint-glow { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--kind) 55%, transparent); } 40% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--kind) 0%, transparent), 0 0 18px color-mix(in srgb, var(--kind) 45%, transparent); } 100% { box-shadow: 0 0 0 0 transparent; } }
|
|
575
679
|
@media (prefers-reduced-motion: reduce) { .memory-hint, .memory-hint .pill { animation: none; } }
|
|
@@ -625,7 +729,13 @@ textarea::placeholder { color: var(--text-3); }
|
|
|
625
729
|
.field button[type="submit"]:hover { filter: brightness(1.06); }
|
|
626
730
|
.field button[type="submit"]:disabled { opacity: .4; cursor: default; box-shadow: none; }
|
|
627
731
|
.pick-label .pick-note { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-family: var(--mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); white-space: nowrap; }
|
|
628
|
-
.safety { margin:
|
|
732
|
+
.safety { max-width: min(100%, 150ch); margin: 8px auto 4px; color: var(--text-3); font-family: var(--mono); font-size: 8.5px; line-height: 1.5; letter-spacing: .06em; text-transform: uppercase; text-align: center; opacity: .85; }
|
|
733
|
+
/* The signature, in the corner it belongs in: last in the page's order, and out of the way of
|
|
734
|
+
everything that is actually used. */
|
|
735
|
+
.colophon { position: fixed; right: 14px; bottom: 10px; z-index: 2; font-family: var(--mono); font-size: 8.5px; letter-spacing: .1em; color: var(--text-3); opacity: .5; pointer-events: none; user-select: none; transition: opacity .2s; }
|
|
736
|
+
.colophon:hover { opacity: .9; }
|
|
737
|
+
@media (max-width: 720px) { .colophon { display: none; } }
|
|
738
|
+
.shell.with-browser .colophon { display: none; }
|
|
629
739
|
|
|
630
740
|
/* toast */
|
|
631
741
|
.toast { position: fixed; left: var(--composer-center, 50%); bottom: calc(100vh - var(--composer-top, 70vh) + 22px); transform: translateX(-50%); text-align: center; padding: 9px 14px; animation: toast-in .22s ease; z-index: 40; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); background: var(--panel); color: var(--danger); font-size: 13px; box-shadow: var(--shadow); max-width: min(560px, calc(100% - 32px)); }
|
|
@@ -667,7 +777,9 @@ textarea::placeholder { color: var(--text-3); }
|
|
|
667
777
|
|
|
668
778
|
.lang-button { font-family: var(--mono); font-size: 9.5px; font-weight: 600; letter-spacing: .16em; padding: 0 9px; }
|
|
669
779
|
.mother-glyph { width: 8px; height: 8px; border: 1px solid currentColor; }
|
|
670
|
-
|
|
780
|
+
/* Every count in the bar wears the same green. Red on a counter reads as an alarm, and the
|
|
781
|
+
number of conditions MU/TH/UR is holding is not one. */
|
|
782
|
+
.mother-count { min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; display: inline-grid; place-items: center; background: var(--phosphor); color: var(--phosphor-ink); font-size: 9px; letter-spacing: 0; }
|
|
671
783
|
|
|
672
784
|
dialog.mother { border: 0; padding: 0; margin: 0; width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; background: transparent; color: var(--brew); }
|
|
673
785
|
dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(6px); }
|
|
@@ -1047,6 +1159,156 @@ dialog.override[open] { display: grid; place-items: center; }
|
|
|
1047
1159
|
.bubble img.inline { max-width: 100%; max-height: 420px; border-radius: 12px; display: block; margin: 6px 0; cursor: zoom-in; }
|
|
1048
1160
|
.bubble a.file-link { color: var(--agent, var(--phosphor)); text-decoration: none; border-bottom: 1px dashed currentColor; font-family: var(--mono); font-size: .92em; cursor: pointer; }
|
|
1049
1161
|
dialog.viewer { border: 0; padding: 0; width: min(1100px, 96vw); max-width: 96vw; height: min(88vh, 900px); background: var(--panel); color: var(--text); border-radius: 16px; box-shadow: var(--shadow); }
|
|
1162
|
+
/* With the browser docked the conversation lives in half a screen, and a bar built for a wide one
|
|
1163
|
+
piles its pieces on top of each other.
|
|
1164
|
+
|
|
1165
|
+
The breakpoints are the BAR'S OWN WIDTH, not the window's. That distinction is the whole fix:
|
|
1166
|
+
the window stays 2000px wide while the column shrinks to 500, so every @media rule written
|
|
1167
|
+
against the viewport sat there never firing while the pieces overlapped. A container query asks
|
|
1168
|
+
the element how much room IT has, which is the only question that was ever relevant.
|
|
1169
|
+
|
|
1170
|
+
And a flex child with min-width:auto refuses to shrink below its content and pushes its
|
|
1171
|
+
neighbour out of the row instead. That is what put MADRE on top of the agent dots. */
|
|
1172
|
+
.bar { container-type: inline-size; container-name: bar; }
|
|
1173
|
+
/* Only what can truncate is allowed to shrink. A button given min-width:0 shrinks below its own
|
|
1174
|
+
label and lets it spill past the border onto the next button — which looks exactly like two
|
|
1175
|
+
controls overlapping, because they are. Buttons hold their size and lose their words instead,
|
|
1176
|
+
at the widths below. */
|
|
1177
|
+
.bar > * { flex: none; }
|
|
1178
|
+
/* Everything but the names holds its size. The names are the only thing in the bar that can lose
|
|
1179
|
+
characters without losing meaning, so they are the only thing allowed to give — and the group
|
|
1180
|
+
that holds them has to be allowed to give too, or its children never get the chance. */
|
|
1181
|
+
.bar > .bar-left { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
1182
|
+
/* The project's name holds; the room's title is the one that gives. A project is where you are
|
|
1183
|
+
and there is one of them; a room title is a sentence somebody typed and there are seven. */
|
|
1184
|
+
.bar .project { flex: none; white-space: nowrap; }
|
|
1185
|
+
.bar .chat-here { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1186
|
+
/* No overflow:hidden here. The quota ring is drawn 3px outside each avatar's own box, so hiding
|
|
1187
|
+
the overflow of the row that holds them is exactly what shaved a flat edge off every sphere. */
|
|
1188
|
+
.bar .agents { flex-wrap: nowrap; }
|
|
1189
|
+
|
|
1190
|
+
/* The order of loss. Nothing disappears: everything that had a word keeps its glyph, so the bar
|
|
1191
|
+
stops being a row of labels and becomes a row of buttons. What a glyph cannot carry — the name
|
|
1192
|
+
of the project, the name of the room — is what falls last.
|
|
1193
|
+
|
|
1194
|
+
The crew is the exception that needs markup: five spheres cannot be compacted by CSS into one
|
|
1195
|
+
button, so there are two elements and the container query picks which one is on screen. */
|
|
1196
|
+
.bar-icon { display: inline-flex; align-items: center; justify-content: center; gap: 5px; width: auto; padding: 0 8px 0 6px; height: 28px; padding: 0; border-radius: 8px; border: 1px solid var(--line-2); background: transparent; color: var(--text-2); cursor: pointer; }
|
|
1197
|
+
.bar-icon svg { width: 17px; height: 17px; }
|
|
1198
|
+
.bar-icon:hover { color: var(--phosphor); border-color: var(--phosphor); }
|
|
1199
|
+
.bar-count { min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; display: inline-grid; place-items: center; background: var(--phosphor); color: var(--phosphor-ink); font-family: var(--mono); font-size: 9px; letter-spacing: 0; }
|
|
1200
|
+
.bar-glyph { display: none; }
|
|
1201
|
+
.agents-compact { display: none; }
|
|
1202
|
+
|
|
1203
|
+
@container bar (max-width: 1040px) {
|
|
1204
|
+
.bar { gap: 9px; }
|
|
1205
|
+
/* The dot already says live in colour; the word only repeats it. */
|
|
1206
|
+
.connection-label { display: none; }
|
|
1207
|
+
.brand-word { display: none; }
|
|
1208
|
+
}
|
|
1209
|
+
/* The crew collapses before the names do. Five spheres are 178px; at the width where the bar is
|
|
1210
|
+
tight, giving those back is what buys the project its name — and a name is information a glyph
|
|
1211
|
+
cannot carry, while five spheres say the same thing one button says with a number. */
|
|
1212
|
+
@container bar (max-width: 980px) {
|
|
1213
|
+
/* The crew becomes one button: how many are ready, and the whole list in its title. */
|
|
1214
|
+
.agents { display: none; }
|
|
1215
|
+
.agents-compact { display: inline-flex; }
|
|
1216
|
+
/* A label on a button whose glyph already names it is a label spending room it does not own. */
|
|
1217
|
+
/* The word goes and a real glyph takes its place: a puzzle piece for the modules, a pulse for
|
|
1218
|
+
MU/TH/UR. The decorative mark each button already carried is not an icon — it is a bullet —
|
|
1219
|
+
so leaving only that left two empty outlines on screen. */
|
|
1220
|
+
/* Icon and number side by side, inside the button. Pulling the badge out with position:absolute
|
|
1221
|
+
made it escape 16-23px past the right edge into an 18px gap, which is exactly the collision
|
|
1222
|
+
that was on screen. A count belongs inside the thing it counts. */
|
|
1223
|
+
.mother-button { width: auto; height: 28px; padding: 0 9px 0 7px; gap: 5px; font-size: 0; justify-content: center; }
|
|
1224
|
+
.mother-button .mother-glyph { display: none; }
|
|
1225
|
+
.mother-button .bar-glyph { display: block; width: 17px; height: 17px; }
|
|
1226
|
+
.mother-button .modules-count, .mother-button .mother-count { font-size: 9px; }
|
|
1227
|
+
.stop-all { width: 30px; height: 28px; padding: 0; gap: 0; font-size: 0; justify-content: center; }
|
|
1228
|
+
.stop-all .stop-all-glyph { font-size: 12px; }
|
|
1229
|
+
}
|
|
1230
|
+
@container bar (max-width: 640px) {
|
|
1231
|
+
/* The room's name is the first thing that cannot be drawn, so it becomes the mark that says
|
|
1232
|
+
there is one: three dots, with the whole title still in the tooltip. */
|
|
1233
|
+
.chat-here { font-size: 0; }
|
|
1234
|
+
.chat-here::after { content: '…'; font-size: 11px; letter-spacing: .1em; }
|
|
1235
|
+
}
|
|
1236
|
+
@container bar (max-width: 460px) {
|
|
1237
|
+
.project:not(.chat-here) { font-size: 0; }
|
|
1238
|
+
.project:not(.chat-here)::after { content: '/'; font-size: 11px; }
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/* The composer has the same disease: its parts have to be allowed to give. */
|
|
1242
|
+
.composer .field { min-width: 0; }
|
|
1243
|
+
.composer .editor { min-width: 0; }
|
|
1244
|
+
.composer .crew-label { flex: none; }
|
|
1245
|
+
|
|
1246
|
+
/* The browser docks beside the conversation; it does not take the screen from it. The point of
|
|
1247
|
+
looking at a server while a room full of agents works on it is seeing both, so the thread keeps
|
|
1248
|
+
its column and simply narrows. One rule does it: the shell grows a second column, and every
|
|
1249
|
+
existing child is already pinned to the first. */
|
|
1250
|
+
.shell.with-browser { grid-template-columns: minmax(0, 1fr) var(--dock, 46vw); }
|
|
1251
|
+
.shell.with-browser > .browser-dock { grid-column: 2; grid-row: 1 / -1; }
|
|
1252
|
+
/* No height of its own: it already spans every row of the shell, and asking for 100dvh on top of
|
|
1253
|
+
that made the grid satisfy it by starving the rows in the other column — which is how the
|
|
1254
|
+
conversations panel ended up two pixels tall and the thread lost its end. */
|
|
1255
|
+
.browser-dock { position: relative; display: grid; grid-template-rows: auto auto minmax(0, 1fr); min-width: 0; min-height: 0; background: var(--panel); border-left: 1px solid var(--line); }
|
|
1256
|
+
/* The dock is a fixed grid track, so anything inside it that refuses to shrink pushes the whole
|
|
1257
|
+
page wider and hands the human a horizontal scrollbar for a layout that was supposed to fit.
|
|
1258
|
+
Its own container query decides what the header can afford. */
|
|
1259
|
+
.browser-dock { container-type: inline-size; container-name: dock; overflow: hidden; }
|
|
1260
|
+
.browser-dock .viewer-head { padding: 10px 12px; min-width: 0; }
|
|
1261
|
+
.browser-dock .viewer-head > * { flex: none; }
|
|
1262
|
+
.browser-dock .browser-where { flex: 1 1 auto; min-width: 40px; }
|
|
1263
|
+
.browser-dock .viewer-body { height: auto; min-height: 0; }
|
|
1264
|
+
.browser-tabs { min-width: 0; }
|
|
1265
|
+
@container dock (max-width: 620px) {
|
|
1266
|
+
/* The widths are a convenience; the address is not. */
|
|
1267
|
+
.browser-dock .viewer-sizes { display: none; }
|
|
1268
|
+
}
|
|
1269
|
+
@container dock (max-width: 460px) {
|
|
1270
|
+
.browser-dock #browser-close { font-size: 0; padding: 6px 8px; }
|
|
1271
|
+
.browser-dock #browser-close::after { content: '×'; font-size: 13px; }
|
|
1272
|
+
.browser-dock #browser-servers { padding: 0 7px; }
|
|
1273
|
+
.browser-dock #browser-servers > span { display: none; }
|
|
1274
|
+
}
|
|
1275
|
+
/* The edge between the two: wide enough to grab, invisible until wanted. */
|
|
1276
|
+
.dock-grip { position: absolute; left: -3px; top: 0; bottom: 0; width: 8px; cursor: ew-resize; z-index: 5; }
|
|
1277
|
+
.dock-grip::after { content: ''; position: absolute; left: 3px; top: 50%; transform: translateY(-50%); width: 2px; height: 48px; border-radius: 999px; background: var(--line-2); opacity: 0; transition: opacity .15s; }
|
|
1278
|
+
.dock-grip:hover::after, .shell.dragging-dock .dock-grip::after { opacity: 1; background: var(--phosphor); }
|
|
1279
|
+
.shell.dragging-dock .browser-dock iframe { pointer-events: none; }
|
|
1280
|
+
@media (max-width: 900px) { .shell.with-browser { grid-template-columns: 1fr; } .shell.with-browser > .browser-dock { grid-column: 1; grid-row: 3 / 4; height: auto; border-left: 0; border-top: 1px solid var(--line); } }
|
|
1281
|
+
|
|
1282
|
+
/* The browser is its own window on the rail, beside the conversations. */
|
|
1283
|
+
|
|
1284
|
+
.browser-head { flex-wrap: nowrap; }
|
|
1285
|
+
.browser-head .viewer-address { flex: 1; max-width: none; }
|
|
1286
|
+
.browser-tabs { display: flex; align-items: stretch; gap: 4px; padding: 10px 12px; border-top: 1px solid var(--line); overflow-x: auto; scrollbar-width: thin; }
|
|
1287
|
+
.browser-tab { display: inline-flex; align-items: center; gap: 8px; max-width: 220px; padding: 6px 10px; border: 1px solid var(--line-2); border-radius: 8px; background: transparent; color: var(--text-3); font-family: var(--mono); font-size: 11px; cursor: pointer; }
|
|
1288
|
+
.browser-tab .tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1289
|
+
.browser-tab .tab-close { opacity: .45; font-size: 13px; line-height: 1; }
|
|
1290
|
+
.browser-tab .tab-close:hover { opacity: 1; color: var(--danger); }
|
|
1291
|
+
.browser-tab.on { color: var(--phosphor); border-color: var(--phosphor); background: color-mix(in srgb, var(--phosphor) 10%, transparent); }
|
|
1292
|
+
.browser-newtab { flex: none; width: 28px; border: 1px dashed var(--line-2); border-radius: 8px; background: transparent; color: var(--text-3); font-size: 14px; cursor: pointer; }
|
|
1293
|
+
.browser-newtab:hover { color: var(--phosphor); border-color: var(--phosphor); }
|
|
1294
|
+
|
|
1295
|
+
/* Browsing a live server is not reading a file: the page under the glass was built for a window,
|
|
1296
|
+
so it gets one. Full bleed, no rounded corners pretending this is a card, and a stage the frame
|
|
1297
|
+
can be made narrower inside — because checking a layout at phone width is most of what a human
|
|
1298
|
+
opens their own app to do. */
|
|
1299
|
+
dialog.viewer.browsing { width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; }
|
|
1300
|
+
.viewer-body.browsing { padding: 0; display: grid; grid-template-rows: minmax(0, 1fr); place-items: stretch; background: var(--bg); overflow: hidden; }
|
|
1301
|
+
.viewer-stage { position: relative; display: grid; grid-template-rows: minmax(0, 1fr); justify-content: center; width: 100%; min-height: 0; overflow: hidden; }
|
|
1302
|
+
.viewer-stage iframe.ripley { width: var(--stage, 100%); max-width: 100%; height: 100%; min-height: 0; border-radius: 0; box-shadow: none; }
|
|
1303
|
+
.viewer-stage[data-sized="true"] iframe.ripley { box-shadow: 0 0 0 1px var(--line-2); }
|
|
1304
|
+
/* The edge you pull. Wide enough to grab, invisible until it matters. */
|
|
1305
|
+
.stage-grip { position: absolute; top: 0; bottom: 0; width: 14px; cursor: ew-resize; display: grid; place-items: center; }
|
|
1306
|
+
.stage-grip::after { content: ''; width: 3px; height: 46px; border-radius: 999px; background: var(--line-2); opacity: 0; transition: opacity .15s; }
|
|
1307
|
+
.stage-grip:hover::after, .viewer-stage.dragging .stage-grip::after { opacity: 1; background: var(--phosphor); }
|
|
1308
|
+
.viewer-stage.dragging iframe.ripley { pointer-events: none; }
|
|
1309
|
+
.viewer-sizes { display: inline-flex; gap: 2px; border: 1px solid var(--line-2); border-radius: 999px; padding: 2px; }
|
|
1310
|
+
.viewer-sizes button { border: 0; background: transparent; color: var(--text-3); font-family: var(--mono); font-size: 9.5px; letter-spacing: .08em; padding: 4px 9px; border-radius: 999px; cursor: pointer; }
|
|
1311
|
+
.viewer-sizes button[aria-pressed="true"] { background: color-mix(in srgb, var(--phosphor) 18%, transparent); color: var(--phosphor); }
|
|
1050
1312
|
dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px); }
|
|
1051
1313
|
.viewer-head { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--line); font-family: var(--mono); font-size: 11px; }
|
|
1052
1314
|
.viewer-path { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
@@ -1355,10 +1617,93 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1355
1617
|
.reply-quote .said { min-width: 0; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
1356
1618
|
.reply-quote .drop { flex: none; margin-left: auto; width: 18px; height: 18px; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--text-3); font-size: 14px; line-height: 1; cursor: pointer; }
|
|
1357
1619
|
.reply-quote .drop:hover { background: color-mix(in srgb, var(--text-3) 14%, transparent); color: var(--text); }
|
|
1358
|
-
|
|
1359
|
-
|
|
1620
|
+
/* What the project already settled, at the head of the conversation: the same indicator language
|
|
1621
|
+
as the memory chips, so the room has one way of showing a note and not three. */
|
|
1622
|
+
.settled { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 12px 14px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
1623
|
+
.settled-lead { font-family: var(--mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
|
|
1624
|
+
.settled-note { display: inline-flex; align-items: center; gap: 7px; max-width: min(380px, 100%); padding: 3px 10px 3px 8px; border: 1px solid color-mix(in srgb, var(--text-3) 26%, transparent); background: color-mix(in srgb, var(--text-3) 7%, transparent); color: var(--text-2); font: inherit; font-size: 11px; line-height: 1.45; border-radius: 999px; cursor: pointer; text-align: left; }
|
|
1625
|
+
.settled-note .dot { flex: none; width: 7px; height: 7px; border-radius: 999px; background: #ffdc3c; box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-3) 45%, transparent); }
|
|
1626
|
+
.settled-note .what { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1627
|
+
.settled-note:hover { background: color-mix(in srgb, var(--text-3) 14%, transparent); color: var(--text); }
|
|
1628
|
+
|
|
1629
|
+
/* Where to go on the server on screen: the host is printed, the path is typed. */
|
|
1630
|
+
.viewer-address { display: flex; align-items: center; gap: 0; min-width: 0; flex: 1; max-width: 520px; border: 1px solid var(--line-2); border-radius: 999px; padding: 0 4px 0 11px; background: color-mix(in srgb, var(--text-3) 6%, transparent); }
|
|
1631
|
+
.viewer-address .host { flex: none; font-family: var(--mono); font-size: 10.5px; letter-spacing: .04em; color: var(--text-3); }
|
|
1632
|
+
.viewer-address input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--text); font-family: var(--mono); font-size: 11.5px; padding: 6px 8px; }
|
|
1633
|
+
.viewer-address input::placeholder { color: var(--text-3); }
|
|
1634
|
+
|
|
1635
|
+
/* Which server is on screen. A label, not a field: the panel opens what the SERVERS list offers
|
|
1636
|
+
and nothing else, so there is nothing here to type. */
|
|
1637
|
+
.browser-where { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 11px; letter-spacing: .04em; color: var(--text-2); padding: 0 4px; }
|
|
1638
|
+
|
|
1639
|
+
/* A page that will not be framed, explained where the page would have been. */
|
|
1640
|
+
.browser-refused { display: grid; align-content: center; justify-items: center; gap: 12px; height: 100%; padding: 32px; text-align: center; }
|
|
1641
|
+
.browser-refused h4 { margin: 0; font-family: var(--mono); font-size: 13px; letter-spacing: .1em; color: var(--text); }
|
|
1642
|
+
.browser-refused p { margin: 0; max-width: 46ch; font-size: 12px; line-height: 1.7; color: var(--text-2); }
|
|
1643
|
+
.browser-refused-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
|
|
1644
|
+
.browser-refused button { border: 1px solid var(--line-2); background: transparent; color: var(--text); font-family: var(--mono); font-size: 10px; letter-spacing: .12em; padding: 8px 14px; border-radius: 999px; cursor: pointer; }
|
|
1645
|
+
.browser-refused .browser-go { border-color: var(--phosphor); color: var(--phosphor); }
|
|
1646
|
+
.browser-refused button:hover { background: color-mix(in srgb, var(--phosphor) 12%, transparent); }
|
|
1647
|
+
|
|
1648
|
+
/* The servers answering on this computer, listed from RIPLEY's bar. */
|
|
1649
|
+
/* The one control in the header that opens something, so it is built like the size pills rather
|
|
1650
|
+
than like the arrows: a round track, a lamp that says whether anything is listening, and a
|
|
1651
|
+
caret that turns when the list is down. */
|
|
1652
|
+
.viewer-nav .nav.ports { width: auto; gap: 7px; display: inline-flex; align-items: center; justify-content: center; padding: 0 9px 0 8px; border-radius: 999px; font-family: var(--mono); font-size: 9.5px; letter-spacing: .12em; color: var(--text-2); }
|
|
1653
|
+
.ports-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--text-3); flex: none; transition: background .2s, box-shadow .2s; }
|
|
1654
|
+
.nav.ports[data-live="yes"] .ports-dot { background: var(--phosphor); box-shadow: 0 0 6px var(--phosphor); }
|
|
1655
|
+
.ports-caret { width: 0; height: 0; flex: none; border-left: 3.5px solid transparent; border-right: 3.5px solid transparent; border-top: 4px solid currentColor; opacity: .55; transition: transform .18s, opacity .18s; }
|
|
1656
|
+
.nav.ports[aria-expanded="true"] { background: color-mix(in srgb, var(--phosphor) 14%, transparent); border-color: color-mix(in srgb, var(--phosphor) 45%, transparent); color: var(--phosphor); }
|
|
1657
|
+
.nav.ports[aria-expanded="true"] .ports-caret { transform: rotate(180deg); opacity: 1; }
|
|
1658
|
+
/* Keyboard focus is worth a ring; a mouse click is not. Without this the clicked button keeps a
|
|
1659
|
+
heavy dark box the arrows beside it never wear. */
|
|
1660
|
+
.viewer-nav .nav:focus { outline: none; }
|
|
1661
|
+
.viewer-nav .nav:focus-visible { outline: 2px solid var(--phosphor); outline-offset: 2px; }
|
|
1662
|
+
.ports-menu { position: absolute; top: 46px; left: 12px; z-index: 40; min-width: 0; width: min(420px, calc(100cqw - 24px)); max-width: none; border: 1px solid var(--line-2); background: var(--panel); border-radius: 12px; box-shadow: 0 14px 40px rgba(0, 0, 0, .35); padding: 6px; display: grid; gap: 2px; }
|
|
1663
|
+
.ports-row { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: baseline; padding: 7px 10px; border: 0; border-radius: 8px; background: transparent; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
|
|
1664
|
+
.ports-row b { font-family: var(--mono); font-size: 12.5px; color: var(--phosphor); }
|
|
1665
|
+
.ports-row span { font-size: 11.5px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1666
|
+
.ports-row:hover { background: color-mix(in srgb, var(--phosphor) 12%, transparent); }
|
|
1667
|
+
.ports-note { padding: 6px 10px 7px; font-family: var(--mono); font-size: 9px; line-height: 1.6; letter-spacing: .1em; color: var(--text-3); }
|
|
1668
|
+
|
|
1669
|
+
/* The way up, beside the agent asking to climb it. Quiet: it is a door, not an alarm. */
|
|
1670
|
+
.mode-ask { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 8px 12px 0; padding: 7px 12px; border: 1px dashed color-mix(in srgb, var(--warn, #e2a022) 46%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--warn, #e2a022) 6%, transparent); }
|
|
1671
|
+
.mode-what { flex: 1; min-width: 0; font-family: var(--mono); font-size: 10.5px; letter-spacing: .06em; color: var(--text-2); }
|
|
1672
|
+
.mode-go { flex: none; border: 1px solid color-mix(in srgb, var(--warn, #e2a022) 60%, transparent); background: transparent; color: var(--warn, #e2a022); font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .12em; padding: 5px 12px; border-radius: 7px; cursor: pointer; }
|
|
1673
|
+
.mode-go:hover { background: color-mix(in srgb, var(--warn, #e2a022) 16%, transparent); }
|
|
1674
|
+
|
|
1675
|
+
/* A decision handed back, in the agent's colour: read at a glance, answered with one press. */
|
|
1676
|
+
.choice { margin: 10px 12px 2px; padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--agent, var(--text-3)) 32%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--agent, var(--text-3)) 5%, transparent); }
|
|
1677
|
+
.choice-q { margin: 0 0 10px; font-size: 13px; line-height: 1.5; color: var(--text); }
|
|
1678
|
+
.choice-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
1679
|
+
.choice-option { border: 1px solid color-mix(in srgb, var(--agent, var(--text-3)) 45%, transparent); background: var(--panel); color: var(--text); font: inherit; font-size: 12.5px; line-height: 1.4; text-align: left; padding: 8px 14px; border-radius: 999px; cursor: pointer; transition: background .15s, border-color .15s; }
|
|
1680
|
+
.choice-option:hover { background: color-mix(in srgb, var(--agent, var(--text-3)) 14%, transparent); border-color: var(--agent, var(--text-3)); }
|
|
1681
|
+
|
|
1682
|
+
/* What the room still has no answer to. Quieter than the free-turn offer above it: that one is
|
|
1683
|
+
about to save money, this one is only wondering. */
|
|
1684
|
+
/* The label on its own line over a single row of pills. Wrapping put the third question on a
|
|
1685
|
+
second line and walked the composer down the page; a sideways rail keeps the height fixed
|
|
1686
|
+
however many the room is carrying. */
|
|
1687
|
+
.openq { display: grid; justify-items: center; gap: 5px; margin: 0 0 8px; min-width: 0; }
|
|
1688
|
+
.openq-lead { font-family: var(--mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
|
|
1689
|
+
.openq-rail { display: flex; gap: 6px; max-width: 100%; overflow-x: auto; scrollbar-width: none; padding: 1px 2px; }
|
|
1690
|
+
.openq-rail::-webkit-scrollbar { display: none; }
|
|
1691
|
+
.openq-rail > * { flex: 0 0 auto; }
|
|
1692
|
+
.openq-ask { max-width: min(340px, 100%); padding: 4px 11px; border: 1px solid color-mix(in srgb, var(--text-3) 24%, transparent); background: transparent; color: var(--text-2); font: inherit; font-size: 11px; line-height: 1.45; border-radius: 999px; cursor: pointer; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1693
|
+
.openq-ask:hover { background: color-mix(in srgb, var(--text-3) 10%, transparent); color: var(--text); border-color: color-mix(in srgb, var(--text-3) 40%, transparent); }
|
|
1694
|
+
|
|
1695
|
+
/* The free-turn offer: one line above the field, never in the way of typing. */
|
|
1696
|
+
.free-offer { display: flex; align-items: center; gap: 12px; margin: 0 0 8px; padding: 8px 12px; border: 1px dashed var(--brew); border-radius: 10px; background: color-mix(in srgb, var(--brew) 7%, transparent); }
|
|
1697
|
+
.free-offer .free-what { flex: 1; min-width: 0; font-family: var(--mono); font-size: 11px; line-height: 1.5; letter-spacing: .04em; color: var(--text-2); }
|
|
1698
|
+
.free-offer .free-go { flex: none; border: 1px solid var(--brew); background: transparent; color: var(--brew); font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .12em; padding: 6px 12px; border-radius: 7px; cursor: pointer; }
|
|
1699
|
+
.free-offer .free-go:hover { background: color-mix(in srgb, var(--brew) 16%, transparent); }
|
|
1700
|
+
@media (max-width: 720px) { .free-offer { flex-wrap: wrap; } }
|
|
1701
|
+
.slash-menu { position: absolute; left: -12px; bottom: calc(100% + 14px); z-index: 30; min-width: 320px; max-width: min(580px, 92vw); border: 1px solid var(--line-2); background: var(--panel); border-radius: 14px; box-shadow: 0 14px 40px rgba(0, 0, 0, .35); padding: 6px; display: grid; gap: 2px; }
|
|
1702
|
+
.slash-menu .item { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 10px; align-items: baseline; padding: 8px 10px; border-radius: 9px; cursor: pointer; border: 0; background: transparent; color: var(--text); text-align: left; font: inherit; }
|
|
1360
1703
|
.slash-menu .item .key { font-family: var(--mono); font-size: 12.5px; color: var(--agent, var(--phosphor)); font-weight: 600; white-space: nowrap; }
|
|
1361
|
-
.slash-menu .item .what { font-size: 12.5px; color: var(--text-2);
|
|
1704
|
+
.slash-menu .item .what { font-size: 12.5px; line-height: 1.45; color: var(--text-2); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
|
|
1705
|
+
/* The argument syntax on its own line, under both, free to wrap. */
|
|
1706
|
+
.slash-menu .item .args { grid-column: 1 / -1; font-family: var(--mono); font-size: 11px; line-height: 1.5; color: var(--text-3); overflow-wrap: anywhere; }
|
|
1362
1707
|
.slash-menu .item.off .key { color: var(--text-3); text-decoration: line-through; }
|
|
1363
1708
|
.slash-menu .item[aria-selected="true"], .slash-menu .item:hover { background: color-mix(in srgb, var(--agent, var(--phosphor)) 14%, transparent); }
|
|
1364
1709
|
.slash-menu .hint { padding: 4px 10px 6px; font-family: var(--mono); font-size: 10px; letter-spacing: .12em; color: var(--text-3); }
|
|
@@ -1395,8 +1740,31 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1395
1740
|
.chats-button:active { transform: translateY(1px); }
|
|
1396
1741
|
@media (max-width: 1100px) { .chats-button { display: none; } }
|
|
1397
1742
|
|
|
1398
|
-
.tree { grid-row: 3; justify-self: end; align-self: stretch; z-index: 5; width: clamp(240px, calc((100% - var(--column)) / 2 - 36px), 420px); margin: 18px 20px 10px 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--line-2); border-radius: 18px; background: var(--panel); box-shadow: 0 10px 30px rgba(0, 0, 0, .06); overflow: hidden; animation: tree-in .22s ease; }
|
|
1743
|
+
.tree { position: relative; grid-row: 3; justify-self: end; align-self: stretch; z-index: 5; width: clamp(240px, calc((100% - var(--column)) / 2 - 36px), 420px); margin: 18px 20px 10px 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--line-2); border-radius: 18px; background: var(--panel); box-shadow: 0 10px 30px rgba(0, 0, 0, .06); overflow: hidden; animation: tree-in .22s ease; }
|
|
1744
|
+
/* The finder's own row of tools: a search, and what is waiting to be pasted. */
|
|
1745
|
+
.tree { grid-template-rows: auto auto auto minmax(0, 1fr); grid-template-columns: minmax(0, 1fr); }
|
|
1746
|
+
.tree-search { padding: 8px 12px 6px; }
|
|
1747
|
+
.tree-search input { width: 100%; border: 1px solid var(--line-2); border-radius: 8px; background: color-mix(in srgb, var(--text-3) 6%, transparent); color: var(--text); font-family: var(--mono); font-size: 11px; padding: 6px 10px; outline: 0; }
|
|
1748
|
+
.tree-search input:focus { border-color: var(--phosphor); }
|
|
1749
|
+
.tree-clip { display: flex; min-width: 0; align-items: center; gap: 8px; margin: 0 12px 6px; padding: 6px 10px; border: 1px dashed var(--phosphor); border-radius: 8px; font-family: var(--mono); font-size: 10px; color: var(--text-2); }
|
|
1750
|
+
.tree-clip .what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1751
|
+
.tree-clip button { border: 0; background: transparent; color: var(--phosphor); font: inherit; font-size: 10px; letter-spacing: .1em; cursor: pointer; }
|
|
1752
|
+
.tree-ask { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
|
|
1753
|
+
.tree-ask input { flex: 1; min-width: 0; border: 0; border-bottom: 1px solid var(--line-2); background: transparent; color: var(--text); font: inherit; font-size: 11px; padding: 2px 1px; outline: none; }
|
|
1754
|
+
.tree-ask input:focus { border-bottom-color: var(--phosphor); }
|
|
1755
|
+
.tree-ask .go { color: var(--phosphor); }
|
|
1756
|
+
.tree-menu { position: absolute; z-index: 20; min-width: 170px; border: 1px solid var(--line-2); border-radius: 10px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.35); padding: 5px; display: grid; gap: 1px; }
|
|
1757
|
+
.tree-menu button { border: 0; background: transparent; color: var(--text); font: inherit; font-size: 12px; text-align: left; padding: 7px 10px; border-radius: 7px; cursor: pointer; }
|
|
1758
|
+
.tree-menu button:hover { background: color-mix(in srgb, var(--phosphor) 12%, transparent); }
|
|
1759
|
+
.tree-menu .guarded { color: var(--warn); }
|
|
1760
|
+
.tree-menu .note { padding: 5px 10px 6px; font-family: var(--mono); font-size: 8.5px; letter-spacing: .1em; color: var(--text-3); }
|
|
1761
|
+
|
|
1399
1762
|
@keyframes tree-in { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
|
|
1763
|
+
/* Conversations open where their handle is. The files panel is anchored right because its button
|
|
1764
|
+
lives in the bar's right end; this one's handle sits at the left of the thread, so a panel that
|
|
1765
|
+
appeared on the far side — behind the browser, once there is one — read as not opening at all. */
|
|
1766
|
+
.chats { justify-self: start; margin-left: 20px; margin-right: 0; z-index: 7; animation: chats-in .22s ease; }
|
|
1767
|
+
@keyframes chats-in { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }
|
|
1400
1768
|
.tree-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 12px 10px 16px; border-bottom: 1px solid var(--line); font-family: var(--mono); font-size: 10px; letter-spacing: .18em; color: var(--text-2); }
|
|
1401
1769
|
.tree-title { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
|
|
1402
1770
|
.tree-root { color: var(--text-3); letter-spacing: .06em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
package/src/choices.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// An agent asking the human to choose.
|
|
2
|
+
//
|
|
3
|
+
// The room already has one fenced channel, ```pulse, and it puts other agents to work. This is
|
|
4
|
+
// its inert sibling: ```pulse-ask carries a question and the options an agent sees, and running
|
|
5
|
+
// it does nothing at all. MADRE renders the options under the answer; pressing one writes that
|
|
6
|
+
// option into the composer as the human's next message. Nothing is sent, nothing is decided and
|
|
7
|
+
// no turn is spent until the human presses.
|
|
8
|
+
//
|
|
9
|
+
// Separate from the delegation parser on purpose. That one is load-bearing — it starts real
|
|
10
|
+
// turns with real permissions — and a question is not a step. Teaching it a second grammar would
|
|
11
|
+
// put a parser that can spend money in the path of a parser that cannot.
|
|
12
|
+
//
|
|
13
|
+
// ```pulse-ask
|
|
14
|
+
// Where should the exported dataset live?
|
|
15
|
+
// - Beside the room log, with the rest of the archive
|
|
16
|
+
// - In the project, so it travels with the repository
|
|
17
|
+
// - Somewhere the human names each time
|
|
18
|
+
// ```
|
|
19
|
+
//
|
|
20
|
+
// Two options are a yes/no, which a sentence already handles; the block earns its place from
|
|
21
|
+
// three. Above six it is a list, not a choice, and the room says so by refusing it.
|
|
22
|
+
|
|
23
|
+
export const MIN_OPTIONS = 3;
|
|
24
|
+
export const MAX_OPTIONS = 6;
|
|
25
|
+
const MAX_QUESTION = 200;
|
|
26
|
+
const MAX_OPTION = 120;
|
|
27
|
+
|
|
28
|
+
// The last ```pulse-ask block of a reply, if it closes the reply. Returns null for anything
|
|
29
|
+
// else: a block quoted mid-answer is an example, and an example is not a question.
|
|
30
|
+
export function parseChoice(text) {
|
|
31
|
+
const source = String(text ?? '');
|
|
32
|
+
const blocks = [...source.matchAll(/```pulse-ask\s*\n([\s\S]*?)```/gi)];
|
|
33
|
+
if (!blocks.length) return null;
|
|
34
|
+
const last = blocks.at(-1);
|
|
35
|
+
if (source.slice(last.index + last[0].length).trim()) return null;
|
|
36
|
+
|
|
37
|
+
const lines = last[1].split('\n').map((line) => line.trim()).filter(Boolean);
|
|
38
|
+
if (!lines.length) return null;
|
|
39
|
+
const question = lines[0].startsWith('-') ? null : lines[0].slice(0, MAX_QUESTION);
|
|
40
|
+
const options = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const line of lines.slice(question ? 1 : 0)) {
|
|
43
|
+
const match = line.match(/^[-*•]\s+(.+)$/);
|
|
44
|
+
if (!match) continue;
|
|
45
|
+
const label = match[1].trim().slice(0, MAX_OPTION);
|
|
46
|
+
// The same option twice is a mistake, and a choice that offers it twice reads as rigged.
|
|
47
|
+
const key = label.toLowerCase();
|
|
48
|
+
if (!label || seen.has(key)) continue;
|
|
49
|
+
seen.add(key);
|
|
50
|
+
options.push(label);
|
|
51
|
+
}
|
|
52
|
+
if (!question) return null;
|
|
53
|
+
if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) return null;
|
|
54
|
+
return { question, options };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// What the reply reads as once the block has been lifted out of it: the room shows the question
|
|
58
|
+
// as bubbles, so leaving the raw fence in the text would say everything twice.
|
|
59
|
+
export function withoutChoice(text) {
|
|
60
|
+
const source = String(text ?? '');
|
|
61
|
+
if (!parseChoice(source)) return source;
|
|
62
|
+
const last = [...source.matchAll(/```pulse-ask\s*\n[\s\S]*?```/gi)].at(-1);
|
|
63
|
+
return source.slice(0, last.index).trimEnd();
|
|
64
|
+
}
|