samagotchi 0.126.0 → 0.128.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -1
- data/docs/architecture.md +4 -0
- data/docs/cli.md +1 -1
- data/docs/configuration.md +2 -2
- data/docs/guardrails.md +5 -0
- data/docs/internals/participants.md +17 -7
- data/docs/sessions.md +3 -1
- data/docs/testing.md +3 -2
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +56 -0
- data/lib/samagotchi/guardrails/read_only_shell.rb +48 -1
- data/lib/samagotchi/session_manager.rb +5 -0
- data/lib/samagotchi/session_participants.rb +14 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +9 -5
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +6 -12
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +23 -35
- data/lib/samagotchi/terminal_ui/attached_loop.rb +10 -18
- data/lib/samagotchi/terminal_ui/own_turns.rb +81 -0
- data/lib/samagotchi/verify_anchor.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +12 -0
- data/lib/samagotchi/web/participant_access.rb +16 -1
- data/lib/samagotchi/web/participant_review.rb +4 -2
- data/lib/samagotchi/web/project_list.rb +52 -0
- data/lib/samagotchi/web/public/annotate_bar.js +14 -2
- data/lib/samagotchi/web/public/app.js +25 -4
- data/lib/samagotchi/web/public/composer.css +13 -1
- data/lib/samagotchi/web/public/index.html +35 -16
- data/lib/samagotchi/web/public/notify.js +4 -1
- data/lib/samagotchi/web/public/participant.html +33 -32
- data/lib/samagotchi/web/public/participant.js +16 -6
- data/lib/samagotchi/web/public/participant_model.js +3 -2
- data/lib/samagotchi/web/public/participant_names.js +2 -2
- data/lib/samagotchi/web/public/project_picker.js +115 -0
- data/lib/samagotchi/web/public/question_card.js +4 -1
- data/lib/samagotchi/web/public/restore.js +7 -4
- data/lib/samagotchi/web/public/scope.js +8 -10
- data/lib/samagotchi/web/public/send_turn.js +3 -1
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/stage_view.js +1 -1
- data/lib/samagotchi/web/public/stream_handlers.js +7 -3
- data/lib/samagotchi/web/public/verdict_card.js +18 -3
- data/lib/samagotchi/web/public/verdict_fresh.js +11 -0
- data/lib/samagotchi/web/public/verify_card.css +1 -0
- data/lib/samagotchi/web/public/verify_preset.js +6 -0
- data/lib/samagotchi/web/push/attention.rb +24 -5
- data/lib/samagotchi/web/verify_fresh_routes.rb +12 -7
- data/lib/samagotchi/worker.rb +7 -4
- metadata +4 -1
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
|
|
23
23
|
body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:radial-gradient(1100px 700px at 8% 0%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 70%),var(--bg);background-attachment:fixed;color:var(--text);height:100vh;display:flex;flex-direction:column}
|
|
24
24
|
/* Glass: the composer card, the strip cards, the all-sessions cards and the question cards (by selector, so app.js is untouched). */
|
|
25
|
-
|
|
25
|
+
.turn-stage,#topStrip .card,#allList .card,.bubble.question,.bubble.plugin-card{background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
|
|
26
26
|
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:var(--card-hover-border)}
|
|
27
27
|
#topStrip .card.active,#allList .card.active{background:color-mix(in srgb,var(--accent-bg) 75%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
|
|
28
28
|
/* A parent whose folded family holds the open session: a quieter mark than .active. */
|
|
@@ -30,9 +30,9 @@ body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetic
|
|
|
30
30
|
/* A session that waits on the user (a question, an approval, a card with actions): the warning colour of its question card, also when active or hovered. */
|
|
31
31
|
#topStrip .card.waiting,#allList .card.waiting{border-color:color-mix(in srgb,var(--warn) 75%,transparent)}
|
|
32
32
|
#topStrip .card.waiting:hover,#allList .card.waiting:hover{border-color:var(--warn)}
|
|
33
|
-
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
|
|
34
|
-
@media (prefers-reduced-transparency:reduce){
|
|
35
|
-
body.no-glass
|
|
33
|
+
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.turn-stage,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel)}.bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel))}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel))}}
|
|
34
|
+
@media (prefers-reduced-transparency:reduce){.turn-stage,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}}
|
|
35
|
+
body.no-glass .turn-stage,body.no-glass #topStrip .card,body.no-glass #allList .card,body.no-glass #allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
36
36
|
body.no-glass .bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
37
37
|
body.no-glass .bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
38
38
|
button,input,textarea,select{font:inherit}
|
|
@@ -566,8 +566,8 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
|
|
|
566
566
|
.bubble.output.markdown .code-wrap>pre{margin:0}
|
|
567
567
|
.code-wrap>.copy-btn{top:5px;right:5px}
|
|
568
568
|
@media (hover:none){.copy-btn{opacity:.55}}
|
|
569
|
-
/* The dock is the composer card: the text field, a toolbar row, the info bar as its footer. */
|
|
570
|
-
#dock{
|
|
569
|
+
/* The dock is the composer card (composer.css .composer-card): the text field, a toolbar row, the info bar as its footer. */
|
|
570
|
+
#dock{flex-shrink:0;margin:0 12px 12px}
|
|
571
571
|
/* In a session the card floats over the history, so the glass blurs it; the history's bottom padding follows the card (--dock-h, set from dock_reserve.js). */
|
|
572
572
|
/* The session panel lets the page's glow through (the history's own layers go clear), so the card and its footer read as glass over it, not as a flat box on black. */
|
|
573
573
|
body.has-session #center{background:color-mix(in srgb,var(--bg) 35%,transparent);border-color:var(--glass-border);position:relative}
|
|
@@ -576,9 +576,8 @@ body.has-session #stage,body.has-session #history{background:transparent}
|
|
|
576
576
|
body.no-glass.has-session #center{background:var(--bg);border-color:var(--border)}
|
|
577
577
|
body.has-session #dock{position:absolute;left:16px;right:16px;bottom:16px;margin:0;z-index:3}
|
|
578
578
|
body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
579
|
-
#dock:focus-within{border-color:color-mix(in srgb,var(--accent) 70%,transparent);box-shadow:0 0 0 3px var(--accent-ring),var(--glass-shadow)}
|
|
580
579
|
/* The delivery line (delivery_line.js): a session's row above the text field, always there so the composer never changes height: the key hint while idle; while a turn runs, where a message typed now goes in, then the cut and queue links. One quiet row, ellipsised; a phone gets the short text and chips. */
|
|
581
|
-
#deliveryLine{display:flex;align-items:center;gap:10px
|
|
580
|
+
#deliveryLine{display:flex;align-items:center;gap:10px}
|
|
582
581
|
#deliveryLine[hidden]{display:none}
|
|
583
582
|
#deliveryLine .delivery-where{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
584
583
|
#deliveryLine .delivery-where.idle{color:var(--text-faint)}
|
|
@@ -672,8 +671,6 @@ body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){dis
|
|
|
672
671
|
/* D2: its own glass card above the composer card (as a section of one card the composer's focus ring wrapped the turn too):
|
|
673
672
|
in the stage view #dock is a clear column holding the stage and .dock-card (the composer and its footer, stage_view.js). */
|
|
674
673
|
body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;border:0;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;gap:10px}
|
|
675
|
-
.dock-card{display:flex;flex-direction:column;border-radius:16px;position:relative;transition:border-color .12s ease,box-shadow .12s ease}
|
|
676
|
-
.dock-card:focus-within{border-color:color-mix(in srgb,var(--accent) 70%,transparent);box-shadow:0 0 0 3px var(--accent-ring),var(--glass-shadow)}
|
|
677
674
|
/* The live slots' heights (headline, tool row, trail); --ts-live-h is their running height with the two 8 px gaps. */
|
|
678
675
|
.turn-stage{--ts-head-h:42px;--ts-now-h:32px;--ts-trail-h:18px;--ts-live-h:calc(var(--ts-head-h) + var(--ts-now-h) + var(--ts-trail-h) + 16px)}
|
|
679
676
|
.turn-stage{display:flex;flex-direction:column;min-height:0;max-height:var(--stage-max,60vh);border-left:3px solid var(--accent);border-radius:14px;overflow:hidden}
|
|
@@ -995,7 +992,7 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
|
|
|
995
992
|
.ts-trail-item{max-width:24ch}
|
|
996
993
|
}
|
|
997
994
|
@media (prefers-reduced-motion:reduce){.turn-stage.enter,.ts-arrive,.ts-headline.fresh,.ts-tick.running,.ts-mark,.turn-work:not(.done) .activity-status.running::before{animation:none}.turn-stage.leaving,#center.reserve-easing #history{transition:none}}
|
|
998
|
-
#infoBar{display:flex;
|
|
995
|
+
#infoBar{display:flex;background:transparent;border-radius:0 0 16px 16px;color:var(--text-2);gap:10px;align-items:flex-start;flex-wrap:wrap;flex-shrink:0;transition:opacity 280ms ease}
|
|
999
996
|
#infoBar.visible{display:flex}
|
|
1000
997
|
/* The meta row keeps its room: with a plain flex:1 the bar's own buttons (continue, archive, stop session, delete)
|
|
1001
998
|
shrink it to a narrow column in a small window and its items wrap into five lines. With a 640px basis they share
|
|
@@ -1268,13 +1265,35 @@ body.has-session #newBtn .brand-new{display:inline}
|
|
|
1268
1265
|
/* The scope chip: "<project> · all", or "all projects" and where a new session starts (start page only). */
|
|
1269
1266
|
#scope{display:inline-flex;align-items:center;gap:8px;min-width:0;font-size:12px;color:var(--text-muted)}
|
|
1270
1267
|
#scope[hidden]{display:none}
|
|
1271
|
-
#scope .scope-name{flex-shrink:0;padding:2px 9px;border:1px solid var(--border-strong);border-radius:999px;color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40vw}
|
|
1268
|
+
#scope .scope-name{display:inline-flex;align-items:center;flex-shrink:0;padding:2px 9px;border:1px solid var(--border-strong);border-radius:999px;background:transparent;color:var(--text-2);font:inherit;line-height:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40vw;cursor:pointer;transition:none}
|
|
1269
|
+
#scope .scope-name:hover:not(:disabled){filter:none}
|
|
1272
1270
|
#scope .scope-name.error{color:var(--warn-text);border-color:var(--warn-border)}
|
|
1273
1271
|
#scope .scope-all{color:var(--text-muted);text-decoration:none}
|
|
1274
1272
|
#scope .scope-all:hover{color:var(--accent);text-decoration:underline}
|
|
1275
1273
|
#scope .scope-where{display:inline-flex;gap:5px;min-width:0;white-space:nowrap;font-size:11px}
|
|
1276
1274
|
/* The path cuts on the left, so its last folders stay in view. */
|
|
1277
1275
|
#scope .scope-where .path{overflow:hidden;text-overflow:ellipsis;direction:rtl;min-width:0;font-family:ui-monospace,monospace}
|
|
1276
|
+
#projectPicker{width:min(380px,calc(100vw - 32px))}
|
|
1277
|
+
.project-picker.sheet{border-radius:14px;padding:14px 16px}
|
|
1278
|
+
.project-picker .ctx-pop-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
1279
|
+
.project-picker .ctx-pop-head strong{font-family:inherit}
|
|
1280
|
+
.project-list{display:flex;flex-direction:column;gap:6px;margin:2px 0;padding:0}
|
|
1281
|
+
.project-row{display:flex;align-items:center;gap:12px;padding:8px 10px;border:1px solid transparent;border-radius:9px;background:var(--bg-inset);color:var(--text);text-decoration:none}
|
|
1282
|
+
.project-row:hover{background:var(--bg-hover);color:var(--text-hover)}
|
|
1283
|
+
.project-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
1284
|
+
.project-row[aria-current="page"]{border-color:var(--accent-border);background:var(--accent-bg)}
|
|
1285
|
+
.project-main{display:flex;flex:1;align-items:center;gap:7px;min-width:0;flex-wrap:wrap}
|
|
1286
|
+
.project-name{font-weight:600;overflow-wrap:anywhere}
|
|
1287
|
+
.project-root{flex-basis:100%;min-width:0;color:var(--text-muted);font:11px ui-monospace,monospace;overflow-wrap:anywhere}
|
|
1288
|
+
.project-meta{display:flex;flex-shrink:0;align-items:center;gap:7px;color:var(--text-muted);font-size:11px;white-space:nowrap}
|
|
1289
|
+
.project-default{padding:1px 6px;border-radius:999px;background:var(--bg-raised);color:var(--text-muted);font-size:10px}
|
|
1290
|
+
.project-check{color:var(--accent);font-weight:700}
|
|
1291
|
+
.project-hint{flex-basis:100%;color:var(--text-muted);font-size:12px}
|
|
1292
|
+
/* The divider is its own hairline above the row: a border-top on the rounded row curved up at both ends. */
|
|
1293
|
+
.project-all{position:relative;margin-top:12px;background:transparent}
|
|
1294
|
+
.project-all::before{content:"";position:absolute;left:-1px;right:-1px;top:-9px;height:1px;background:var(--border);pointer-events:none}
|
|
1295
|
+
.project-note{padding:5px 2px;color:var(--text-muted);font-size:12px}
|
|
1296
|
+
.project-error{color:var(--warn-text)}
|
|
1278
1297
|
body.has-session #scope .scope-where{display:none}
|
|
1279
1298
|
/* A phone has no room for both; the hidden strip's pill (count, live) wins. */
|
|
1280
1299
|
@media(max-width:600px){body.strip-hidden #scope .scope-where{display:none}}
|
|
@@ -1525,7 +1544,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
1525
1544
|
.card .dot.waiting,.family-row .dot.waiting{background:var(--warn);animation:attn-pulse 1.6s ease-in-out var(--pulse-delay,0s) infinite}
|
|
1526
1545
|
@keyframes attn-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--warn) 60%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--warn) 0%,transparent)}}
|
|
1527
1546
|
@media (prefers-reduced-motion:reduce){.card .dot.running,.card .dot.waiting,.family-row .dot.running,.family-row .dot.waiting{animation:none}}
|
|
1528
|
-
.card .attn,.family-row .attn{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn-bg);color:var(--warn-text);border:1px solid var(--warn-border)}
|
|
1547
|
+
.card .attn,.family-row .attn,.project-picker .attn{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn-bg);color:var(--warn-text);border:1px solid var(--warn-border)}
|
|
1529
1548
|
.card .stopped-badge,.family-row .stopped-badge{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn);color:var(--bg)}
|
|
1530
1549
|
.status.running{background:var(--ok-bg);color:var(--ok-text)}
|
|
1531
1550
|
.status.idle{background:var(--bg-raised)}
|
|
@@ -1569,7 +1588,7 @@ a{color:var(--accent)}
|
|
|
1569
1588
|
<div id="notice" role="status" hidden></div>
|
|
1570
1589
|
</div>
|
|
1571
1590
|
<div id="lightbox" hidden><img alt=""></div>
|
|
1572
|
-
<div id="dock">
|
|
1591
|
+
<div id="dock" class="composer-card">
|
|
1573
1592
|
<div id="initRow" role="status" aria-live="polite" hidden></div>
|
|
1574
1593
|
<div id="composeGrip" class="edge-grip" tabindex="0" aria-label="Message box" title="Drag to make the message box taller (double-click resets)"></div>
|
|
1575
1594
|
<div id="composer" style="position:relative">
|
|
@@ -1578,7 +1597,7 @@ a{color:var(--accent)}
|
|
|
1578
1597
|
<label class="model-search"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14 14" stroke-linecap="round"/></svg><input id="modelSearch" type="text" placeholder="Search models" autocomplete="off" spellcheck="false" role="combobox" aria-label="Search models" aria-autocomplete="list" aria-controls="modelList" aria-expanded="false"></label>
|
|
1579
1598
|
<div id="modelList" role="listbox" aria-label="Models"></div>
|
|
1580
1599
|
</div>
|
|
1581
|
-
<div id="deliveryLine" aria-live="off" hidden></div>
|
|
1600
|
+
<div id="deliveryLine" class="composer-line" aria-live="off" hidden></div>
|
|
1582
1601
|
<div class="compose-col">
|
|
1583
1602
|
<div id="chips" hidden></div>
|
|
1584
1603
|
<textarea id="prompt" placeholder="Ask chi anything…" aria-controls="commandList" aria-autocomplete="list" aria-expanded="false"></textarea>
|
|
@@ -1597,7 +1616,7 @@ a{color:var(--accent)}
|
|
|
1597
1616
|
</div>
|
|
1598
1617
|
</div>
|
|
1599
1618
|
</div>
|
|
1600
|
-
<div id="infoBar">
|
|
1619
|
+
<div id="infoBar" class="composer-card-foot">
|
|
1601
1620
|
<div class="info" id="infoText"></div>
|
|
1602
1621
|
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
1603
1622
|
<button class="ghost" id="infoCancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// title badge for what it returns, while the tab is not in front.
|
|
4
4
|
|
|
5
5
|
import { clip } from "./format.js";
|
|
6
|
+
import { isParticipantAuthor } from "./participant_model.js";
|
|
6
7
|
|
|
7
8
|
// The bell's state. OS notifications need a secure context: on chi web
|
|
8
9
|
// over http at a LAN address the API is missing or always denied, so the
|
|
@@ -163,7 +164,9 @@ export function attentionText(session, attention) {
|
|
|
163
164
|
const title = clip(name, 61);
|
|
164
165
|
const seconds = Math.round(Number(session?.last_turn?.seconds) || 0);
|
|
165
166
|
const verdict = session?.last_verdict;
|
|
166
|
-
|
|
167
|
+
// An owner's verdict has no participant author (none, "" or "owner").
|
|
168
|
+
const byParticipant = isParticipantAuthor(verdict?.by);
|
|
169
|
+
const who = verdict?.by_name || (byParticipant ? "A participant" : "You");
|
|
167
170
|
const verdictReason = clip(String(verdict?.reason || ""), VERDICT_REASON_MAX);
|
|
168
171
|
const verdictIcon = { confirmed: "✅", refuted: "❌", unclear: "❓" }[verdict?.verdict] || "";
|
|
169
172
|
const verdictBody = `${who}: ${verdictIcon} ${verdict?.verdict || ""} — ${verdictReason}`;
|
|
@@ -105,42 +105,43 @@ main{flex:1;min-height:0;overflow-y:auto;padding:16px 16px 8px}
|
|
|
105
105
|
.p-tool-one.error{color:var(--danger-text)}
|
|
106
106
|
.p-tag{font-size:11px;padding:0 6px;border-radius:999px;background:var(--bg-raised);color:var(--text-3)}
|
|
107
107
|
.p-waiting{align-self:flex-start;font-size:13px;color:var(--warn-text);background:var(--warn-bg);border:1px solid var(--warn-border);border-radius:999px;padding:3px 12px}
|
|
108
|
-
|
|
108
|
+
/* The dock is the owner's composer card (index.html's #dock): a glass card over the page's bottom edge, holding the
|
|
109
|
+
comment box (key hint, text field, usage footer); the queue and errors sit above it. */
|
|
110
|
+
.p-dock{padding:6px 16px calc(16px + env(safe-area-inset-bottom))}
|
|
109
111
|
.p-dock-in{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:8px}
|
|
110
112
|
.p-queue{display:flex;flex-direction:column;gap:4px;font-size:13px}
|
|
111
113
|
.p-queued{display:flex;align-items:center;gap:8px;color:var(--text-3)}
|
|
112
114
|
.p-queued span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
113
115
|
.p-queue-note{font-size:12.5px;color:var(--text-muted)}
|
|
114
116
|
.p-error{font-size:13px;color:var(--danger-text);background:var(--danger-bg-dim);border-radius:8px;padding:6px 10px}
|
|
115
|
-
/* The comment box: the shared composer (composer.css, composer_mount.js), as the owner's session
|
|
116
|
-
(participant.js sets body.has-session for a participant:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#composer:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
|
|
121
|
-
body.has-session #composer{padding:6px 6px 6px 10px}
|
|
117
|
+
/* The comment box: the shared composer in the shared card (composer.css, composer_mount.js), as the owner's session
|
|
118
|
+
composer lays it out (participant.js sets body.has-session for a participant): the key hint as the composer's line,
|
|
119
|
+
the text field and Send, the usage as the card's footer. Its grip sits on the card's top edge, as the owner's does;
|
|
120
|
+
folded, the card keeps its footer under the grip, the owner's info bar's way. No `/` list, delivery choices or model
|
|
121
|
+
picker here. */
|
|
122
122
|
#composer textarea{font:inherit;line-height:1.45;padding:2px 0}
|
|
123
123
|
#composer textarea:disabled{cursor:not-allowed}
|
|
124
124
|
#composer .actions-col #send{height:30px;min-height:30px;line-height:30px;padding:0 16px;font-weight:600}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/* The participant's own usage (participant_model.js usageItems: the fields their grant's exposure shows), one line
|
|
128
|
-
|
|
129
|
-
.p-foot{
|
|
130
|
-
.p-foot .p-
|
|
125
|
+
/* The key hint wraps to at most two lines (a narrow card cuts the second's end); the whole text is its title. */
|
|
126
|
+
.p-hint.composer-line{color:var(--text-faint);height:auto;max-height:36px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}
|
|
127
|
+
/* The participant's own usage (participant_model.js usageItems: the fields their grant's exposure shows), one line in
|
|
128
|
+
the card's footer, the end cut. Folded, it stays under the grip. */
|
|
129
|
+
.p-foot{min-width:0}
|
|
130
|
+
.p-foot:has(> .p-usage[hidden]){display:none}
|
|
131
|
+
/* Folded before the usage loads, the card keeps a footer's height under its grip, not a 2 px sliver. */
|
|
132
|
+
body.composer-folded .p-foot:has(> .p-usage[hidden]){display:block;min-height:18px}
|
|
131
133
|
.p-usage{display:flex;align-items:baseline;min-width:0;max-width:100%;font-size:12px;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
132
134
|
.p-usage span{flex:0 0 auto}
|
|
133
135
|
.p-usage span+span::before{content:"·";padding:0 6px;color:var(--text-faint)}
|
|
134
136
|
.p-usage span.model{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
135
137
|
.p-usage .warn{color:var(--warn-text)}
|
|
136
138
|
.p-usage .over{color:var(--danger-text);font-weight:600}
|
|
137
|
-
body.composer-folded .p-foot{
|
|
139
|
+
body.composer-folded .p-foot{border-top-color:transparent}
|
|
138
140
|
button{font:inherit;cursor:pointer;border-radius:8px;border:1px solid var(--border-strong);background:var(--bg-raised);color:var(--text);padding:6px 12px}
|
|
139
141
|
button:hover{background:var(--bg-hover)}
|
|
140
142
|
button.primary{background:var(--primary);border-color:var(--primary);color:var(--on-solid);height:42px;padding:0 18px}
|
|
141
143
|
button.primary:disabled{opacity:.5;cursor:default}
|
|
142
144
|
button.small{font-size:12px;padding:2px 8px}
|
|
143
|
-
.p-hint{font-size:12px;color:var(--text-faint)}
|
|
144
145
|
.p-ended{max-width:30rem;margin:18vh auto 0;padding:0 16px;text-align:center;display:flex;flex-direction:column;gap:10px}
|
|
145
146
|
.p-ended h1{font-size:20px;color:var(--text-bright)}
|
|
146
147
|
.p-ended p{color:var(--text-3)}
|
|
@@ -151,12 +152,10 @@ button.small{font-size:12px;padding:2px 8px}
|
|
|
151
152
|
.p-people .p-person:nth-child(n+4){display:none}
|
|
152
153
|
main{padding:12px 16px 6px}
|
|
153
154
|
.p-bubble{padding:9px 12px}
|
|
154
|
-
.p-hint,.p-note{display:none}
|
|
155
|
+
.p-hint.composer-line,.p-note{display:none}
|
|
155
156
|
}
|
|
156
157
|
/* Under 16px iOS Safari zooms the page on focus (and a Home Screen app never zooms back). */
|
|
157
158
|
@media (max-width:600px),(pointer:coarse){#composer textarea{font-size:16px}}
|
|
158
|
-
/* A phone's grip (composer.css: 26 px tall, the pill at 17 px): the pill on the dock's top border. */
|
|
159
|
-
@media (max-width:600px){.p-dock #composeGrip{margin:-29px auto 3px}}
|
|
160
159
|
</style>
|
|
161
160
|
</head>
|
|
162
161
|
<body>
|
|
@@ -185,25 +184,27 @@ button.small{font-size:12px;padding:2px 8px}
|
|
|
185
184
|
<div class="p-thread" id="thread"></div>
|
|
186
185
|
</main>
|
|
187
186
|
<footer class="p-dock" id="dock" hidden>
|
|
188
|
-
<div id="composeGrip" class="edge-grip" tabindex="0" aria-label="Comment box" title="Drag to make the comment box taller (double-click resets)"></div>
|
|
189
187
|
<div class="p-dock-in">
|
|
190
188
|
<div class="p-queue" id="queue"></div>
|
|
191
189
|
<div class="p-error" id="error" role="alert" hidden></div>
|
|
192
|
-
<div
|
|
193
|
-
<div class="
|
|
194
|
-
|
|
195
|
-
<
|
|
190
|
+
<div class="p-card composer-card">
|
|
191
|
+
<div id="composeGrip" class="edge-grip" tabindex="0" aria-label="Comment box" title="Drag to make the comment box taller (double-click resets)"></div>
|
|
192
|
+
<div id="composer">
|
|
193
|
+
<div class="p-hint composer-line" id="hint">⏎ sends, ⇧⏎ a new line, ↑ an earlier comment. The model answers in a read-only turn; the owner sees everything.</div>
|
|
194
|
+
<div class="compose-col">
|
|
195
|
+
<div id="chips" hidden></div>
|
|
196
|
+
<textarea id="comment" rows="1" placeholder="Comment or ask" aria-label="Comment or ask"></textarea>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="actions-col">
|
|
199
|
+
<button type="button" id="attachBtn" class="attach-btn" title="Add images" aria-label="Add images" hidden></button>
|
|
200
|
+
<input type="file" id="attachInput" accept="image/*" multiple hidden>
|
|
201
|
+
<div class="split"><button type="button" class="primary" id="send">Send</button></div>
|
|
202
|
+
</div>
|
|
196
203
|
</div>
|
|
197
|
-
<div class="
|
|
198
|
-
<
|
|
199
|
-
<input type="file" id="attachInput" accept="image/*" multiple hidden>
|
|
200
|
-
<div class="split"><button type="button" class="primary" id="send">Send</button></div>
|
|
204
|
+
<div class="p-foot composer-card-foot">
|
|
205
|
+
<div class="p-usage" id="usage" aria-label="Your usage" hidden></div>
|
|
201
206
|
</div>
|
|
202
207
|
</div>
|
|
203
|
-
<div class="p-foot">
|
|
204
|
-
<div class="p-usage" id="usage" aria-label="Your usage" hidden></div>
|
|
205
|
-
<div class="p-hint" id="hint">⏎ sends, ⇧⏎ a new line, ↑ an earlier comment. The model answers in a read-only turn; the owner sees everything.</div>
|
|
206
|
-
</div>
|
|
207
208
|
</div>
|
|
208
209
|
</footer>
|
|
209
210
|
</div>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// (spend vs cap, queue, link expiry, model). The owner's participant view
|
|
13
13
|
// mounts none.
|
|
14
14
|
import {
|
|
15
|
-
authorColor, participantAuthor, authorLabel, toolLine, refusalText, dequeueText, endedText, streamURL, reloads, endsLiveTurn, liveFrom, applyLive,
|
|
15
|
+
authorColor, participantAuthor, isParticipantAuthor, authorLabel, toolLine, refusalText, dequeueText, endedText, streamURL, reloads, endsLiveTurn, liveFrom, applyLive,
|
|
16
16
|
liveText, withToolSteps, pickSubject, subjectBody, subjectsKey, narrowView, retryDelay, retries, threadGaps, usageItems,
|
|
17
17
|
modeCopy,
|
|
18
18
|
} from "./participant_model.js";
|
|
@@ -22,10 +22,11 @@ import { createAuthLost, lostAnswer } from "./auth_lost.js";
|
|
|
22
22
|
import { REPLACE_SESSION_TEXT, fragmentCode, tradeFragmentCode } from "./token_fragment.js";
|
|
23
23
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
24
24
|
import { parsePresets } from "./annotate_presets.js";
|
|
25
|
-
import { VERIFY_PRESET, withVerify } from "./verify_preset.js";
|
|
25
|
+
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, withVerify } from "./verify_preset.js";
|
|
26
26
|
import { appendQuote } from "./annotations.js";
|
|
27
27
|
import { messageAuthorData, participantQuote, participantSource } from "./participant_annotate.js";
|
|
28
28
|
import { claimFromHash, selectionClaim, showClaim } from "./claim_link.js";
|
|
29
|
+
import { CHECK_NOT_ON_PAGE } from "./verdict_card.js";
|
|
29
30
|
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
30
31
|
import { mountComposer } from "./composer_mount.js";
|
|
31
32
|
import { PROMPT_LINE_PX } from "./composer_size.js";
|
|
@@ -234,6 +235,7 @@ function render() {
|
|
|
234
235
|
const copy = modeCopy(viewer.mode);
|
|
235
236
|
$("note").textContent = ownerView ? "" : copy.note;
|
|
236
237
|
$("hint").textContent = `⏎ sends, ⇧⏎ a new line, ↑ an earlier comment. ${copy.hint}`;
|
|
238
|
+
$("hint").title = $("hint").textContent; // at most two lines in the card: the whole text on hover
|
|
237
239
|
$("head").hidden = false;
|
|
238
240
|
$("main").hidden = false;
|
|
239
241
|
renderPlan();
|
|
@@ -342,7 +344,7 @@ function messageNode(message) {
|
|
|
342
344
|
const who = el("div", "p-who");
|
|
343
345
|
who.append(el("b", "", authorLabel(message)));
|
|
344
346
|
if (message.merged) who.append(el("span", "p-tag", "added mid-turn"));
|
|
345
|
-
if (message.author) who.append(el("span", "p-tag", modeCopy(message.participant_mode).tag));
|
|
347
|
+
if (isParticipantAuthor(message.author)) who.append(el("span", "p-tag", modeCopy(message.participant_mode).tag));
|
|
346
348
|
node.append(who);
|
|
347
349
|
const text = message.content + (message.images ? `\n[${message.images} image${message.images > 1 ? "s" : ""}]` : "");
|
|
348
350
|
node.append(el("div", "p-bubble", text));
|
|
@@ -412,13 +414,17 @@ function renderThread() {
|
|
|
412
414
|
const author = record.by || null;
|
|
413
415
|
const viewer = data.viewer || {};
|
|
414
416
|
let who = ownerView ? "You" : "Owner";
|
|
415
|
-
if (author
|
|
417
|
+
if (isParticipantAuthor(author)) {
|
|
416
418
|
if (!ownerView && author === participantAuthor(viewer.id)) who = "You";
|
|
417
419
|
else {
|
|
418
420
|
who = (data.participants || []).find((participant) => participantAuthor(participant.id) === author)?.name || "A participant";
|
|
419
421
|
}
|
|
420
422
|
}
|
|
421
|
-
placeVerdictWithFresh(thread, record, {
|
|
423
|
+
placeVerdictWithFresh(thread, record, {
|
|
424
|
+
who,
|
|
425
|
+
author,
|
|
426
|
+
onMissingCheck: () => showClaimNote(CHECK_NOT_ON_PAGE),
|
|
427
|
+
}, verifyFreshState());
|
|
422
428
|
}
|
|
423
429
|
if (atBottom || !thread.dataset.drawn) main.scrollTop = main.scrollHeight;
|
|
424
430
|
thread.dataset.drawn = "1";
|
|
@@ -430,10 +436,12 @@ function renderThread() {
|
|
|
430
436
|
// does, and no one here gets a link to the child's session.
|
|
431
437
|
function verifyFreshState() {
|
|
432
438
|
if (!Array.isArray(data.fresh_running)) data.fresh_running = [];
|
|
439
|
+
const unavailable = data.session?.verify_unavailable === true;
|
|
433
440
|
return {
|
|
434
441
|
verdicts: data.verdicts || [],
|
|
435
442
|
running: data.fresh_running,
|
|
436
|
-
canStart: !ownerView && !ended && !!data.viewer?.can_comment,
|
|
443
|
+
canStart: !unavailable && !ownerView && !ended && !!data.viewer?.can_comment,
|
|
444
|
+
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
437
445
|
start: async (of) => {
|
|
438
446
|
const { ok, body } = await api(freshURL(sessionId, of), {
|
|
439
447
|
method: "POST", body: "{}", headers: { "Content-Type": "application/json" },
|
|
@@ -649,6 +657,8 @@ for (const button of $("viewSeg").querySelectorAll("button")) {
|
|
|
649
657
|
// while this participant can't comment.
|
|
650
658
|
const annotateBar = createAnnotateBar({
|
|
651
659
|
presets: withVerify(parsePresets(body.annotatePresets)),
|
|
660
|
+
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
661
|
+
&& data?.session?.verify_unavailable === true ? VERIFY_UNAVAILABLE : null,
|
|
652
662
|
canAnnotate: () => !ownerView && !ended && !!data?.viewer?.can_comment,
|
|
653
663
|
roots: () => [$("planDoc"), $("thread")],
|
|
654
664
|
isLive: (root) => !!root.closest?.(".p-msg.live"),
|
|
@@ -9,6 +9,7 @@ import { fullTime, gapText } from "./format.js";
|
|
|
9
9
|
// (Steer.author_for / PARTICIPANT_PREFIX): the one place the web spells it.
|
|
10
10
|
export const PARTICIPANT_PREFIX = "p:";
|
|
11
11
|
export const participantAuthor = (id) => `${PARTICIPANT_PREFIX}${id}`;
|
|
12
|
+
export const isParticipantAuthor = (by) => typeof by === "string" && by.startsWith(PARTICIPANT_PREFIX);
|
|
12
13
|
|
|
13
14
|
// Six author colours (tokens.css --author-1..6), one per participant id,
|
|
14
15
|
// the same on every page that shows them (the owner's badges too).
|
|
@@ -22,13 +23,13 @@ export function authorSlot(author) {
|
|
|
22
23
|
|
|
23
24
|
// The CSS colour of an author: a participant's slot, else the owner's.
|
|
24
25
|
export function authorColor(author) {
|
|
25
|
-
return author ? `var(--author-${authorSlot(author)})` : "var(--author-owner)";
|
|
26
|
+
return isParticipantAuthor(author) ? `var(--author-${authorSlot(author)})` : "var(--author-owner)";
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// Who wrote a user message or started a turn, as the page names them.
|
|
29
30
|
export function authorLabel(item) {
|
|
30
31
|
if (item?.mine) return "You";
|
|
31
|
-
if (item?.author) return item.author_name || "A participant";
|
|
32
|
+
if (isParticipantAuthor(item?.author)) return item.author_name || "A participant";
|
|
32
33
|
if (item?.automatic) return "Automatic";
|
|
33
34
|
return "Owner";
|
|
34
35
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// prompt's own author_name (a saved message, turn_started) comes first; a
|
|
5
5
|
// badge drawn before the list came, with neither, says UNNAMED until
|
|
6
6
|
// nameUnnamedBadges names it.
|
|
7
|
-
import { authorColor, participantAuthor } from "./participant_model.js";
|
|
7
|
+
import { authorColor, isParticipantAuthor, participantAuthor } from "./participant_model.js";
|
|
8
8
|
|
|
9
9
|
export const UNNAMED = "a participant";
|
|
10
10
|
|
|
@@ -28,7 +28,7 @@ export function participantName(author) {
|
|
|
28
28
|
// +mode+: the turn's participant_mode stamp ("review", "pair"; the
|
|
29
29
|
// message's, or turn_started's), kept as mode: when known.
|
|
30
30
|
export function authorBadge(author, name = null, mode = null) {
|
|
31
|
-
if (!author
|
|
31
|
+
if (!isParticipantAuthor(author)) return null;
|
|
32
32
|
const known = name || participantName(author);
|
|
33
33
|
return { author, name: known || UNNAMED, color: authorColor(author), ...(known ? {} : { unnamed: true }), ...(mode ? { mode } : {}) };
|
|
34
34
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The project picker opened by the top-bar scope chip. The server supplies
|
|
2
|
+
// the project roots and their current/default marks; links carry the view
|
|
3
|
+
// without carrying a session hash into another project's list.
|
|
4
|
+
|
|
5
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
6
|
+
import { allScopeHref, projectScopeHref } from "./scope.js";
|
|
7
|
+
import { createChipPopover } from "./chip_popover.js";
|
|
8
|
+
|
|
9
|
+
function homeHref(search) {
|
|
10
|
+
const view = new URLSearchParams(search || "").get("view");
|
|
11
|
+
return view ? `/?view=${encodeURIComponent(view)}` : "/";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sessionCount(count) {
|
|
15
|
+
const n = Number(count) || 0;
|
|
16
|
+
return `${n} session${n === 1 ? "" : "s"}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function projectRow(row, search) {
|
|
20
|
+
return html`<a class=${`project-row${row.default ? " default" : ""}${row.current ? " current" : ""}`}
|
|
21
|
+
href=${row.default ? homeHref(search) : projectScopeHref(row.dir, "", search)}
|
|
22
|
+
aria-current=${row.current ? "page" : nothing}>
|
|
23
|
+
<span class="project-main"><span class="project-name">${row.name}</span
|
|
24
|
+
>${row.default ? html`<span class="project-default">default</span>` : nothing
|
|
25
|
+
}<span class="project-root">${row.root_label}</span></span>
|
|
26
|
+
<span class="project-meta">${Number(row.waiting) > 0 ? html`<span class="attn">${Number(row.waiting)} waiting</span>` : nothing
|
|
27
|
+
}<span class="project-count">${sessionCount(row.sessions)}</span>${row.current ? html`<span class="project-check" aria-hidden="true">✓</span>` : nothing}</span>
|
|
28
|
+
</a>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// +rows+ is the API's ordered list. When there is no project scope, the
|
|
32
|
+
// all-projects row is current; a bad ?dir= has no current project to mark.
|
|
33
|
+
export function pickerTemplate({ rows = [], loading = false, error = "", search = "", currentDir = null,
|
|
34
|
+
allCurrent = false } = {}) {
|
|
35
|
+
const allHref = allScopeHref("", currentDir, search);
|
|
36
|
+
return html`<div class="ctx-pop-head"><strong>Projects</strong><button type="button" class="ctx-pop-close"
|
|
37
|
+
data-act="close" aria-label="Close">×</button></div>
|
|
38
|
+
${loading ? html`<div class="project-note" role="status">Loading projects…</div>` : nothing}
|
|
39
|
+
${error ? html`<div class="project-note project-error" role="alert">${error}</div>` : nothing}
|
|
40
|
+
<nav class="project-list" aria-label="Projects">${rows.map((row) => projectRow(row, search))}
|
|
41
|
+
<a id="projectAll" class=${`project-row project-all${allCurrent ? " current" : ""}`}
|
|
42
|
+
href=${allHref} aria-current=${allCurrent ? "page" : nothing}>
|
|
43
|
+
<span class="project-main"><span class="project-name">All projects</span
|
|
44
|
+
><span class="project-hint">Every session</span></span>
|
|
45
|
+
${allCurrent ? html`<span class="project-check" aria-hidden="true">✓</span>` : nothing}
|
|
46
|
+
</a>
|
|
47
|
+
</nav>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// +scopeError+ returns the page's current folder error, if any. +where+
|
|
51
|
+
// supplies the live query string so links retain ?view= without a hash.
|
|
52
|
+
export function createProjectPicker({ el, scope, scopeError = () => null, api, where = () => ({ search: "" }),
|
|
53
|
+
doc = globalThis.document, win = globalThis.window }) {
|
|
54
|
+
let rows = [];
|
|
55
|
+
let loading = false;
|
|
56
|
+
let error = "";
|
|
57
|
+
let loads = 0;
|
|
58
|
+
|
|
59
|
+
const currentDir = () => scopeError() ? null : scope.dir || null;
|
|
60
|
+
const paint = (focus = false) => {
|
|
61
|
+
const failedScope = !!scopeError();
|
|
62
|
+
render(pickerTemplate({
|
|
63
|
+
rows, loading, error, search: where().search, currentDir: currentDir(),
|
|
64
|
+
allCurrent: !scope.dir && !failedScope,
|
|
65
|
+
}), shell.pop);
|
|
66
|
+
if (focus) {
|
|
67
|
+
const target = shell.pop.querySelector('.project-row[aria-current="page"]') ||
|
|
68
|
+
shell.pop.querySelector(".project-row");
|
|
69
|
+
target?.focus();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async function load(focus = false) {
|
|
74
|
+
const turn = ++loads;
|
|
75
|
+
loading = true;
|
|
76
|
+
error = "";
|
|
77
|
+
if (shell.isOpen()) {
|
|
78
|
+
paint();
|
|
79
|
+
shell.replace();
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// A dead ?dir (the folder-not-found chip) is left out: the list is the way out of it.
|
|
83
|
+
const dir = currentDir();
|
|
84
|
+
const answer = await api(`/api/projects${dir ? `?dir=${encodeURIComponent(dir)}` : ""}`);
|
|
85
|
+
if (!Array.isArray(answer)) throw new Error("the server returned an invalid project list");
|
|
86
|
+
if (turn !== loads) return;
|
|
87
|
+
rows = answer;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (turn !== loads) return;
|
|
90
|
+
error = `Can't read projects: ${e?.message || e}`;
|
|
91
|
+
rows = [];
|
|
92
|
+
}
|
|
93
|
+
if (turn !== loads) return;
|
|
94
|
+
loading = false;
|
|
95
|
+
if (shell.isOpen()) {
|
|
96
|
+
paint(focus);
|
|
97
|
+
shell.replace();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const shell = createChipPopover({
|
|
102
|
+
id: "projectPicker", className: "ctx-popover project-picker", width: 380, el, anchor: "#scopePick",
|
|
103
|
+
summary: () => ({}), sheet: true, label: "Projects", doc, win,
|
|
104
|
+
show: () => {
|
|
105
|
+
rows = [];
|
|
106
|
+
loading = true;
|
|
107
|
+
error = "";
|
|
108
|
+
paint();
|
|
109
|
+
void load(true);
|
|
110
|
+
},
|
|
111
|
+
onClose: () => { loads += 1; },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return { pop: shell.pop, isOpen: shell.isOpen, load };
|
|
115
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// command or paths, where and why. Its options end in "Deny"; the answer's
|
|
4
4
|
// last option denies. A page that doesn't know approvals still shows the
|
|
5
5
|
// plain question text, which says the same.
|
|
6
|
+
import { isParticipantAuthor, PARTICIPANT_PREFIX } from "./participant_model.js";
|
|
6
7
|
|
|
7
8
|
export function isApproval(pq) {
|
|
8
9
|
return !!pq && pq.kind === "approval";
|
|
@@ -68,7 +69,9 @@ export function relayView(pq) {
|
|
|
68
69
|
export function approvalAsker(pq) {
|
|
69
70
|
const a = (pq && pq.approval) || {};
|
|
70
71
|
if (!a.author) return null;
|
|
71
|
-
|
|
72
|
+
const author = String(a.author);
|
|
73
|
+
return a.author_name ? String(a.author_name)
|
|
74
|
+
: isParticipantAuthor(author) ? author.slice(PARTICIPANT_PREFIX.length) : author;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// @return {null | {tool, what, isCommand, where, why, paths?, preview?, delegate?, asker?}};
|
|
@@ -7,18 +7,21 @@ import { clientLabel, isOwn, withImages } from "./turn_events.js";
|
|
|
7
7
|
// What a prompt_restored (the worker rolled a failed turn back and handed its
|
|
8
8
|
// prompt back) does in this tab: `refill` is the text for an empty composer,
|
|
9
9
|
// only for a prompt this page sent (`sentIds`, its acks' enqueued_ids), so a
|
|
10
|
-
// replay after a reload never refills stale text
|
|
10
|
+
// replay after a reload never refills stale text, with a Verify turn's claim
|
|
11
|
+
// (`verify`) so a resend still records a verdict; `own`/`label` mark the
|
|
11
12
|
// failed bubble shown again after the resync.
|
|
12
13
|
export function restoreAction(event, { myId, sentIds, composerEmpty }) {
|
|
13
14
|
const origin = event?.origin || {};
|
|
14
15
|
const own = isOwn(origin.client_id, myId);
|
|
15
16
|
const sent = own && !!origin.enqueued_id && sentIds.has(origin.enqueued_id);
|
|
16
|
-
// The images go back as chips with the text.
|
|
17
|
+
// The images go back as chips with the text, and its Verify claim.
|
|
18
|
+
const back = sent && composerEmpty;
|
|
17
19
|
return withImages({
|
|
18
|
-
refill:
|
|
20
|
+
refill: back ? event.prompt : null,
|
|
19
21
|
own,
|
|
20
22
|
label: own ? null : clientLabel(origin.client_id),
|
|
21
|
-
|
|
23
|
+
...(back && event.verify ? { verify: event.verify } : {}),
|
|
24
|
+
}, back ? event.images : null);
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
// A turn that fails fast can have its prompt_restored come over the stream
|
|
@@ -49,24 +49,22 @@ export function cardFolder(workingDirectory, projectName) {
|
|
|
49
49
|
return folder === projectName ? "" : folder;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// The header's
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// () => { hash, search } (the page's location, read again at a click: the
|
|
58
|
-
// links follow an open session or the all-sessions view).
|
|
52
|
+
// The header's project chip opens the picker. The "all" link remains the
|
|
53
|
+
// direct way to every session from a project view. +scope+: { dir,
|
|
54
|
+
// projectName, projectDir, serverDir, backDir, backName }; +error+: why
|
|
55
|
+
// the ?dir= folder can't be shown; +where+: () => { hash, search } (the
|
|
56
|
+
// page's location, read again at a click).
|
|
59
57
|
export function scopeTemplate(scope, { error = null, where = () => ({ hash: "", search: "" }) } = {}) {
|
|
60
58
|
const at = where();
|
|
61
59
|
const allHref = () => allScopeHref(where().hash, scope.dir, where().search);
|
|
62
60
|
const all = html`<a class="scope-all" href=${allScopeHref(at.hash, scope.dir, at.search)} title=${"Sessions of every project"} @click=${(e) => { e.currentTarget.href = allHref(); }}>all</a>`;
|
|
63
|
-
if (error) return html`<
|
|
64
|
-
if (scope.projectName) return html`<
|
|
61
|
+
if (error) return html`<button id="scopePick" class="scope-name error" type="button" aria-haspopup="dialog" title=${error}>folder not found</button>${all}`;
|
|
62
|
+
if (scope.projectName) return html`<button id="scopePick" class="scope-name" type="button" aria-haspopup="dialog" title=${`Sessions of ${scope.projectDir}`}>${scope.projectName}</button>${all}`;
|
|
65
63
|
const dir = scope.dir || scope.serverDir;
|
|
66
64
|
const backHref = () => projectScopeHref(scope.backDir, where().hash, where().search);
|
|
67
65
|
const back = scope.backName
|
|
68
66
|
? html`<a class="scope-all scope-back" href=${projectScopeHref(scope.backDir, at.hash, at.search)} title=${`Sessions of ${scope.backName} only`} @click=${(e) => { e.currentTarget.href = backHref(); }}>← ${scope.backName}</a>`
|
|
69
67
|
: nothing;
|
|
70
68
|
const newSession = dir ? html`<span class="scope-where" title=${`New sessions start in ${dir}`}>new session in<span class="path"><bdi>${dir}</bdi></span></span>` : nothing;
|
|
71
|
-
return html`<
|
|
69
|
+
return html`<button id="scopePick" class="scope-name" type="button" aria-haspopup="dialog" title="Sessions of every project">all projects</button>${back}${newSession}`;
|
|
72
70
|
}
|
|
@@ -29,7 +29,7 @@ import { composerAfterAck, dropEarlyRestores, restoreOnAck } from "./restore.js"
|
|
|
29
29
|
// +alert+: the browser's.
|
|
30
30
|
export function createSendTurn({
|
|
31
31
|
session, state, running, clientId, sentIds, earlyRestores, api, dir, startPage, takeDelivery, composer,
|
|
32
|
-
verify = () => null, clearVerify = () => {},
|
|
32
|
+
verify = () => null, clearVerify = () => {}, restoreVerify = () => {},
|
|
33
33
|
historyEl, turnDom, makeUserBubble, bubbleById, addCommandBubble,
|
|
34
34
|
addCreatedSession, select, showNotice, updateInfoBar, updateComposerMode, followWokenWorker, refreshHistory,
|
|
35
35
|
leaveGoneSession, runCommand, toast, alert = (text) => globalThis.alert(text),
|
|
@@ -158,6 +158,8 @@ export function createSendTurn({
|
|
|
158
158
|
const restored = restoreOnAck(ack?.enqueued_id, earlyRestores, { myId: clientId, sentIds });
|
|
159
159
|
// A restored line must be seen: the composer opens for this visit.
|
|
160
160
|
if (restored?.refill && sentTo === session()) composer.openForVisit();
|
|
161
|
+
// Its Verify claim too, so the resend still records a verdict.
|
|
162
|
+
if (restored?.refill && restored.verify && sentTo === session()) restoreVerify(restored.verify);
|
|
161
163
|
const before = promptEl.value;
|
|
162
164
|
promptEl.value = composerAfterAck({ sent: line, current: before, refill: restored?.refill || "" });
|
|
163
165
|
// A line back from sentTo is a draft of that session, wherever the page is now.
|