samagotchi 0.170.0 → 0.171.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 +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -393,12 +393,15 @@ export function createFrameDrawer({
|
|
|
393
393
|
// limit what they hold (docs/internals/block-renderers.md, residual risk)
|
|
394
394
|
// @param drawers core drawers, asked before the bundle renderers
|
|
395
395
|
// The rest are seams for the specs.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
// +renderers+: false for a viewer without the bundles' renderers (viewer.js):
|
|
397
|
+
// the core +drawers+ only, and GET /api/block_renderers is never asked.
|
|
398
|
+
export function createBlockHost({ roots, sessionId, drawers = [], copy = copyText, watch = null, win = window, renderers = true,
|
|
399
|
+
...frameOpts }) {
|
|
400
|
+
const frames = renderers ? createFrameDrawer({ roots, win, ...frameOpts }) : null;
|
|
401
|
+
const watcher = createBlockWatch({ roots, sessionId, drawers: frames ? [...drawers, frames] : drawers, watch, copy, win });
|
|
399
402
|
// A system theme change redraws through the figure (needsDraw's theme check).
|
|
400
403
|
win.matchMedia?.("(prefers-color-scheme: dark)").addEventListener?.("change", () => {
|
|
401
404
|
for (const root of roots) watcher.scan(root);
|
|
402
405
|
});
|
|
403
|
-
return { scan: watcher.scan, dropFrames: frames.dropFrames };
|
|
406
|
+
return { scan: watcher.scan, dropFrames: frames ? frames.dropFrames : () => {} };
|
|
404
407
|
}
|
|
@@ -195,6 +195,14 @@ export function createBubbles({
|
|
|
195
195
|
return echoes.find((el) => el.dataset.userContent === text) || echoes[0] || null;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// A participant's own queued bubble for their turn's start (+mine+: no
|
|
199
|
+
// origin names its enqueued id), the oldest, preferring one with the same text.
|
|
200
|
+
function queuedOwnBubble(prompt) {
|
|
201
|
+
const queued = findInTurnRoots(".bubble.user.queued[data-own][data-enqueued-id]");
|
|
202
|
+
const text = normalize(prompt || "");
|
|
203
|
+
return queued.find((el) => el.dataset.userContent === text) || queued[0] || null;
|
|
204
|
+
}
|
|
205
|
+
|
|
198
206
|
// The running wake turn's id (a turn chi ran for reports), for its report's bubble.
|
|
199
207
|
let wakeTurnId = null;
|
|
200
208
|
|
|
@@ -229,7 +237,7 @@ export function createBubbles({
|
|
|
229
237
|
// Re-rendered from a snapshot: the echo is gone, the prompt still queued.
|
|
230
238
|
else makeUserBubble(op.prompt, { state: "queued", own: true, enqueuedId: op.enqueuedId, images: op.images, delivery: op.delivery });
|
|
231
239
|
} else if (op.op === "start") {
|
|
232
|
-
const bubble = bubbleById(op.enqueuedId) || untaggedOwnBubble(op.prompt);
|
|
240
|
+
const bubble = bubbleById(op.enqueuedId) || untaggedOwnBubble(op.prompt) || (op.mine ? queuedOwnBubble(op.prompt) : null);
|
|
233
241
|
// A resync (a new worker woken by this page's send) re-rendered the
|
|
234
242
|
// history and took the echo with it: draw the prompt again.
|
|
235
243
|
if (!bubble) started = makeUserBubble(op.prompt, { own: true, enqueuedId: op.enqueuedId, images: op.images });
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A snapshot's cards placed into a holder that is not the page (cards_offscreen.js):
|
|
2
|
+
// the opt-in reload check (reload_check.js) draws a handed-off turn offscreen and
|
|
3
|
+
// compares it with what the page has. A reload's render (turnHistoryNodes) doesn't
|
|
4
|
+
// run the placement (cards_place.js placeSnapshotCards), so the check needs its own.
|
|
5
|
+
//
|
|
6
|
+
// It is the real placer, not a copy: createCardsPlace, createBubbles and
|
|
7
|
+
// createQuestionCards over a detached holder, with every page hook made inert:
|
|
8
|
+
// no stage (turnDom answers "nothing holds it", and has no settleFold/settleWaiting
|
|
9
|
+
// — cards_place.js calls those with `?.`), no scroll, no hint, no info bar, no
|
|
10
|
+
// session id, no api. Nothing outside +holder+ is read or written; the page's
|
|
11
|
+
// questionCards, its turnDom and its appendToHistory are never touched.
|
|
12
|
+
import { createBubbles } from "./bubbles.js";
|
|
13
|
+
import { createCardsPlace } from "./cards_place.js";
|
|
14
|
+
import { createQuestionCards } from "./question_cards.js";
|
|
15
|
+
import { splitSnapshotCards } from "./card.js";
|
|
16
|
+
|
|
17
|
+
// +cards+ the read's snapshot (data.cards); +capabilities+ the page's, for the
|
|
18
|
+
// notices it shows elsewhere (splitSnapshotCards); +answers+ whether this viewer
|
|
19
|
+
// may answer cards at all (viewer.js answer_cards).
|
|
20
|
+
export function placeCardsOffscreen(holder, cards, { capabilities = null, answers = true, doc = globalThis.document } = {}) {
|
|
21
|
+
const shared = {
|
|
22
|
+
historyEl: holder,
|
|
23
|
+
findInTurnRoots: (selector) => [...holder.querySelectorAll(selector)],
|
|
24
|
+
isNearBottom: () => false,
|
|
25
|
+
selected: () => null,
|
|
26
|
+
clientId: null,
|
|
27
|
+
};
|
|
28
|
+
// No running turn here: nothing holds a piece, nothing takes a card or a steer.
|
|
29
|
+
const turnDom = { card: () => false, steer: () => false, holds: () => false };
|
|
30
|
+
const bubbles = createBubbles({
|
|
31
|
+
...shared,
|
|
32
|
+
appendToHistory: (el) => holder.appendChild(el),
|
|
33
|
+
turnDom,
|
|
34
|
+
shownTexts: { shows: () => false },
|
|
35
|
+
liveTurn: { timing: { el: null } },
|
|
36
|
+
});
|
|
37
|
+
const questionCards = createQuestionCards({
|
|
38
|
+
...shared,
|
|
39
|
+
sessionId: () => null,
|
|
40
|
+
place: (card) => { holder.appendChild(card); return false; },
|
|
41
|
+
scrollToEnd: () => {},
|
|
42
|
+
removeHintIfEmpty: () => {},
|
|
43
|
+
askingText: () => "",
|
|
44
|
+
renderMarkdown: () => Promise.resolve(null),
|
|
45
|
+
onPending: () => {},
|
|
46
|
+
onSettled: () => {},
|
|
47
|
+
answers,
|
|
48
|
+
doc,
|
|
49
|
+
// A card's button is inert here: a placed card is a comparison, not a live ask.
|
|
50
|
+
api: {
|
|
51
|
+
sendAnswer: () => Promise.reject(new Error("offscreen card")),
|
|
52
|
+
dismissQuestion: () => Promise.reject(new Error("offscreen card")),
|
|
53
|
+
sendCommand: () => Promise.reject(new Error("offscreen card")),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const place = createCardsPlace({
|
|
57
|
+
...shared,
|
|
58
|
+
turnDom,
|
|
59
|
+
questionCards,
|
|
60
|
+
bubbles,
|
|
61
|
+
followWokenWorker: () => {},
|
|
62
|
+
api: {
|
|
63
|
+
getSession: () => Promise.reject(new Error("offscreen card")),
|
|
64
|
+
sendCommand: () => Promise.reject(new Error("offscreen card")),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
place.placeSnapshotCards(splitSnapshotCards(cards, capabilities).placed, { before: null });
|
|
68
|
+
return holder;
|
|
69
|
+
}
|
|
@@ -60,7 +60,8 @@ function shownAnchor(anchor, doc) {
|
|
|
60
60
|
return standIn?.getClientRects().length ? standIn : anchor;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// id, className: the popover's; width: its widest (px
|
|
63
|
+
// id, className: the popover's; width: its widest (px, or a function of
|
|
64
|
+
// none read at each placing: the working set's grows while a row peeks); el: where the chip
|
|
64
65
|
// is drawn; anchor: the chip's selector; openers: the selector of every
|
|
65
66
|
// element that opens it (a click there is not a click outside; default the
|
|
66
67
|
// anchor's); summary(anchor): what the form is drawn from (null: nothing
|
|
@@ -72,7 +73,7 @@ function shownAnchor(anchor, doc) {
|
|
|
72
73
|
// dialog's accessible name (aria-label), none by default. onOpen(): after
|
|
73
74
|
// an open has shown and placed it (where focus can go into it: show runs
|
|
74
75
|
// while it is still hidden). sheetShare: the share of a phone's window the
|
|
75
|
-
// sheet takes at most (SHEET_SHARE). Always a modal dialog (modalDialog);
|
|
76
|
+
// sheet takes at most (SHEET_SHARE; a function of none is read at each placing). Always a modal dialog (modalDialog);
|
|
76
77
|
// the opener clicked (and the anchor it is placed at) carries aria-expanded
|
|
77
78
|
// "true" while it is open, "false" once closed: a lit-drawn chip writes
|
|
78
79
|
// aria-expanded="false" as a static attribute (lit leaves it to the shell).
|
|
@@ -106,7 +107,7 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
|
|
|
106
107
|
if (narrow) {
|
|
107
108
|
const vv = win.visualViewport;
|
|
108
109
|
const at = sheetPlace({ innerWidth: win.innerWidth, innerHeight: win.innerHeight },
|
|
109
|
-
{ share: sheetShare, viewport: vv ? { height: vv.height, offsetTop: vv.offsetTop } : null });
|
|
110
|
+
{ share: typeof sheetShare === "function" ? sheetShare() : sheetShare, viewport: vv ? { height: vv.height, offsetTop: vv.offsetTop } : null });
|
|
110
111
|
pop.style.width = `${at.width}px`;
|
|
111
112
|
pop.style.left = `${at.left}px`;
|
|
112
113
|
pop.style.top = "";
|
|
@@ -118,7 +119,7 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
|
|
|
118
119
|
pop.style.maxHeight = `${at.maxHeight}px`;
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
|
-
const width = Math.min(widest, win.innerWidth - 32);
|
|
122
|
+
const width = Math.min(typeof widest === "function" ? widest() : widest, win.innerWidth - 32);
|
|
122
123
|
pop.style.width = `${width}px`;
|
|
123
124
|
pop.style.maxHeight = "";
|
|
124
125
|
const at = popoverPlace(shownAnchor(anchor, doc).getBoundingClientRect(), { width, height: pop.offsetHeight,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A link to a claim (docs/internals/participants.md, Deep links): one turn
|
|
2
2
|
// of a session and a quote in it, on either page.
|
|
3
3
|
//
|
|
4
|
-
// participant page
|
|
4
|
+
// participant's page:<chi web>/#s=<id>&t=<turn8>&q=<quote>&p=<prefix>
|
|
5
5
|
// owner's page: #/s/<id>?t=<turn8>&q=<quote>&p=<prefix>
|
|
6
6
|
//
|
|
7
7
|
// t is the turn's id, its first 8 characters (a turn-opening message's
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// when there are none). URLSearchParams encodes both, so a link is
|
|
12
12
|
// URL-safe and a space is one "+".
|
|
13
13
|
//
|
|
14
|
-
// Normalized: whitespace collapsed, and what
|
|
14
|
+
// Normalized: whitespace collapsed, and what a participant's page shows
|
|
15
15
|
// instead (Samagotchi::Redact): secret shapes "[redacted]" (so a link
|
|
16
16
|
// never carries one, and matches the participant's redacted text), and on
|
|
17
17
|
// the owner's page the session's folder relative ("<cwd>/x" → "x",
|
|
@@ -85,7 +85,7 @@ function secretSpans(raw) {
|
|
|
85
85
|
return spans;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// The spans of +raw+ that read differently on
|
|
88
|
+
// The spans of +raw+ that read differently on a participant's page:
|
|
89
89
|
// [{start, end, text}], in order, not overlapping. Secrets first, then
|
|
90
90
|
// the session's folder (it is usually under home), as Redact.text.
|
|
91
91
|
function replacements(raw, { home = "", cwd = "" } = {}) {
|
|
@@ -191,7 +191,7 @@ export function claimParams({ turnId = "", q = "", p = "" } = {}) {
|
|
|
191
191
|
return params.toString();
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
//
|
|
194
|
+
// A participant's page's link: +base+ the participant link's origin
|
|
195
195
|
// ("https://chi.example/", "http://192.168.1.5:4567/"), no token.
|
|
196
196
|
export function participantClaimLink(base, sessionId, claim) {
|
|
197
197
|
const params = claimParams(claim);
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/* The composer (composer_mount.js): the message box, its image chips, grip and fold, its key hint and its toolbar row
|
|
2
|
-
(the image button, Send).
|
|
3
|
-
linked after tokens.css and before a page's own <style>, so a page's rules of the same weight win. The owner's
|
|
2
|
+
(the image button, Send). Linked by index.html after tokens.css and before a page's own <style>, so a page's rules of the same weight win. The owner's
|
|
4
3
|
own parts stay in index.html: the card's placing (#dock), the delivery line's links, the start page's picker and chips,
|
|
5
4
|
the / list, and the 16px text fields for iOS. */
|
|
6
5
|
/* The image button's icon mask (the stroke only shapes the mask; the icon takes currentColor); index.html's phone info
|
|
7
6
|
bar and all view's cards use it too. */
|
|
8
7
|
:root{--icon-toolkit:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.75' y='5.25' width='12.5' height='8' rx='1.5'/%3E%3Cpath d='M5.75 5.25v-1.5a1 1 0 0 1 1-1h2.5a1 1 0 0 1 1 1v1.5M1.75 8.75h12.5M7 8.75v1.5h2v-1.5'/%3E%3C/svg%3E");--icon-settings:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.4' stroke-linejoin='round'%3E%3Cpath d='M12.75 6.14L14.45 6.59L14.45 9.41L12.75 9.86L12.67 10.04L13.56 11.56L11.56 13.56L10.04 12.67L9.86 12.75L9.41 14.45L6.59 14.45L6.14 12.75L5.96 12.67L4.44 13.56L2.44 11.56L3.33 10.04L3.25 9.86L1.55 9.41L1.55 6.59L3.25 6.14L3.33 5.96L2.44 4.44L4.44 2.44L5.96 3.33L6.14 3.25L6.59 1.55L9.41 1.55L9.86 3.25L10.04 3.33L11.56 2.44L13.56 4.44L12.67 5.96Z'/%3E%3Ccircle cx='8' cy='8' r='2.1'/%3E%3C/svg%3E");--icon-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='10.5' rx='1.5'/%3E%3Ccircle cx='5.75' cy='6.25' r='1.1'/%3E%3Cpath d='m2.5 12.5 3.75-3.75 2.5 2.5 1.75-1.75 3 3'/%3E%3C/svg%3E")}
|
|
9
|
-
/* The composer card: the glass card
|
|
10
|
-
participant
|
|
11
|
-
|
|
12
|
-
participant's key hint). A page's own rules only place the card. */
|
|
8
|
+
/* The composer card: the glass card the composer sits in (#dock, the stage view's .dock-card), with its focus ring;
|
|
9
|
+
its footer (the info bar, a participant's bottom row) under a hairline; and the composer's line, the quiet row above
|
|
10
|
+
the text field (the delivery line). A page's own rules only place the card. */
|
|
13
11
|
.composer-card{display:flex;flex-direction:column;border-radius:16px;position:relative;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);transition:border-color .12s ease,box-shadow .12s ease}
|
|
14
12
|
.composer-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)}
|
|
15
13
|
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.composer-card{background:var(--bg-panel)}}
|
|
@@ -15,12 +15,15 @@ export const SEND_HINT = "Enter to send · Shift+Enter for a newline · ↑ hist
|
|
|
15
15
|
// start page), +owner+ its owner, +running+ whether its turn runs, +narrow+ a
|
|
16
16
|
// phone's width. While a turn runs, Send still sends: the prompt merges into
|
|
17
17
|
// the turn at its next step (steering); Cancel is its own button then.
|
|
18
|
+
// +steers+ false: a message sent now doesn't go into the running turn (a
|
|
19
|
+
// participant's into someone else's): the plain placeholder, the delivery
|
|
20
|
+
// line says where it goes.
|
|
18
21
|
// @return {placeholder, hint, button, disabled, empty, clearHistory,
|
|
19
22
|
// attachHidden, delivery}: +disabled+ undefined while a turn runs (the
|
|
20
23
|
// button keeps whatever a send in flight set); +empty+ the start page's
|
|
21
24
|
// empty state shown, +clearHistory+ #history emptied (the start page only);
|
|
22
25
|
// +delivery+ the delivery line's mode (delivery_line.js show).
|
|
23
|
-
export function composerMode({ selected, owner, running, narrow }) {
|
|
26
|
+
export function composerMode({ selected, owner, running, narrow, steers = true }) {
|
|
24
27
|
const session = !!selected;
|
|
25
28
|
const tui = session && owner === "tui";
|
|
26
29
|
const live = !!running && session;
|
|
@@ -34,7 +37,7 @@ export function composerMode({ selected, owner, running, narrow }) {
|
|
|
34
37
|
// composer is off, so is its row.
|
|
35
38
|
const delivery = !session || tui ? "off" : running ? "running" : "idle";
|
|
36
39
|
const base = { attachHidden, delivery, empty: false, clearHistory: false };
|
|
37
|
-
if (live) {
|
|
40
|
+
if (live && steers) {
|
|
38
41
|
return {
|
|
39
42
|
...base,
|
|
40
43
|
placeholder: pick("Running… a message goes in at the next step · Enter to send · Alt+Enter queues", "Steer the turn…"),
|
|
@@ -56,7 +59,7 @@ export function composerMode({ selected, owner, running, narrow }) {
|
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
// The keys are in the delivery line's row above.
|
|
59
|
-
return { ...base, placeholder: "Message chi…", hint: SEND_HINT, button: "Send", disabled: false };
|
|
62
|
+
return { ...base, placeholder: "Message chi…", hint: SEND_HINT, button: "Send", disabled: live ? undefined : false };
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
// The composer's mode drawn: update() is the store's `composer` redraw, and a
|
|
@@ -68,9 +71,10 @@ export function composerMode({ selected, owner, running, narrow }) {
|
|
|
68
71
|
// live_turn.js's (running). +startPage+: start_page.js's (restoreStartPick,
|
|
69
72
|
// syncModelPick, dropChoices). +deliveryControl+: delivery_line.js's.
|
|
70
73
|
// +fitPrompt()+,
|
|
71
|
-
// +placeToast()+: the page's. +win+: the window (matchMedia).
|
|
74
|
+
// +placeToast()+: the page's. +win+: the window (matchMedia). +steers()+:
|
|
75
|
+
// whether a message now goes into the running turn (composerMode's).
|
|
72
76
|
export function createComposerMode({
|
|
73
|
-
els, store, liveTurn, startPage, deliveryControl, fitPrompt, placeToast, win = globalThis,
|
|
77
|
+
els, store, liveTurn, startPage, deliveryControl, fitPrompt, placeToast, win = globalThis, steers = () => true,
|
|
74
78
|
}) {
|
|
75
79
|
const narrowScreen = win.matchMedia(NARROW_QUERY);
|
|
76
80
|
let hadSession = false;
|
|
@@ -89,8 +93,9 @@ export function createComposerMode({
|
|
|
89
93
|
if (store.selected) startPage.dropChoices();
|
|
90
94
|
const mode = composerMode({
|
|
91
95
|
selected: store.selected, owner: store.current.owner, running: liveTurn.running, narrow: narrowScreen.matches,
|
|
96
|
+
steers: steers(),
|
|
92
97
|
});
|
|
93
|
-
els.attach.hidden = mode.attachHidden;
|
|
98
|
+
if (els.attach) els.attach.hidden = mode.attachHidden;
|
|
94
99
|
deliveryControl.show(mode.delivery);
|
|
95
100
|
els.prompt.placeholder = mode.placeholder;
|
|
96
101
|
els.hint.textContent = mode.hint;
|
|
@@ -14,14 +14,14 @@ import { readStored } from "./storage.js";
|
|
|
14
14
|
//
|
|
15
15
|
// +els+: {composer (the card's box, where files drop), prompt (the text box),
|
|
16
16
|
// button (Send/Start), chips (the chips' row), attachBtn, attachInput (the
|
|
17
|
-
// hidden file picker), grip}.
|
|
17
|
+
// hidden file picker; both null: no image button, viewer.js), grip}.
|
|
18
18
|
// Hooks: +minPx()+ the empty box's height; +toast(text)+ why images stayed
|
|
19
19
|
// out; +onWrite()+ the composer wrote the text itself (a recalled entry);
|
|
20
20
|
// +loadHistory()+ a promise of the ↑/↓ entries (oldest first; none: no
|
|
21
21
|
// history); +send({queue})+ Enter (+queue+: with Alt/Option) or the button;
|
|
22
22
|
// +keyFirst(e)+ a key the page takes before the history's (true: taken);
|
|
23
23
|
// +escape()+ Esc, true when it let go of something. +imagesOff+: why this
|
|
24
|
-
// page takes no images (
|
|
24
|
+
// page takes no images (a participant's page, until its upload path): a
|
|
25
25
|
// paste, drop or pick of one toasts it and adds no chip; null takes them.
|
|
26
26
|
// +fold+: createComposerGrip's page hooks (session, waiting, allViewOpen,
|
|
27
27
|
// lightboxOpen, infoBarEl, typeAnywhere).
|
|
@@ -79,8 +79,8 @@ export function mountComposer({
|
|
|
79
79
|
});
|
|
80
80
|
// The image button: the file picker's images join the chips. Its value is
|
|
81
81
|
// cleared after each pick, so the same file can be picked again.
|
|
82
|
-
attachBtn
|
|
83
|
-
attachInput
|
|
82
|
+
attachBtn?.addEventListener("click", () => attachInput.click());
|
|
83
|
+
attachInput?.addEventListener("change", () => {
|
|
84
84
|
addImageFiles([...attachInput.files]);
|
|
85
85
|
attachInput.value = "";
|
|
86
86
|
});
|
|
@@ -124,10 +124,14 @@ export function ctxBarText(pct, windowSource = null) {
|
|
|
124
124
|
// page isn't told context.status is off, so "?" never means 0); after one,
|
|
125
125
|
// "Context window unknown" over the session's tokens and per-model rows with
|
|
126
126
|
// no window line (a window lookup that failed; a model list without a context
|
|
127
|
-
// length still falls back to context.window_tokens or chi's default)
|
|
127
|
+
// length still falls back to context.window_tokens or chi's default), or,
|
|
128
|
+
// when the window is known but no count has come yet, "No context count yet:
|
|
129
|
+
// the next request counts it". +window.afterSwitch+ (the info
|
|
128
130
|
// bar's; a /model whose new window didn't resolve, no request since:
|
|
129
131
|
// Engine#announce_switched_context! sent usage null) adds "until the next
|
|
130
132
|
// request", which asks for it again.
|
|
133
|
+
// +window.running+ (the info bar's own running flag) makes the title of a
|
|
134
|
+
// turn in flight: "Context in use (this turn so far)".
|
|
131
135
|
// +side+ (the timing's side block) adds the side requests' line, in the
|
|
132
136
|
// "No request in this session yet" answer too (a /btw before any turn).
|
|
133
137
|
export function ctxBarTip(tokens, memoryIndex, window, segments = null, side = null) {
|
|
@@ -136,11 +140,16 @@ export function ctxBarTip(tokens, memoryIndex, window, segments = null, side = n
|
|
|
136
140
|
const sideText = sideRequestsText(side);
|
|
137
141
|
return `No request in this session yet${sideText ? `\nside requests: ${sideText}` : ""}`;
|
|
138
142
|
}
|
|
139
|
-
const why = window?.afterSwitch
|
|
143
|
+
const why = window?.afterSwitch
|
|
144
|
+
? "Context window unknown until the next request"
|
|
145
|
+
: num(window?.tokens) > 0
|
|
146
|
+
? "No context count yet: the next request counts it"
|
|
147
|
+
: "Context window unknown";
|
|
140
148
|
return tokensTipText(tokens, why, memoryIndex, null, segments, side);
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
return tokensTipText(tokens,
|
|
151
|
+
return tokensTipText(tokens, window?.running ? "Context in use (this turn so far)" : undefined,
|
|
152
|
+
memoryIndex, window, segments, side);
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
// The ctx tooltip's window line ({tokens:, source:, pct:, budget:} the
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
// asks (never a steer or a cut into their turn): Enter queues, no cut is
|
|
30
30
|
// offered, and the line says so.
|
|
31
31
|
//
|
|
32
|
+
// A participant looking (the participant main page: +viewerTurn+) gets no
|
|
33
|
+
// cut and no links: in their own turn the message goes in at its next step
|
|
34
|
+
// (Alt+Enter queues it), on a worker that takes it; in anyone else's it
|
|
35
|
+
// runs as their own turn afterwards (the worker queues it).
|
|
36
|
+
//
|
|
32
37
|
// deliveryLine is pure (node tests); deliveryLineTemplate is its lit view.
|
|
33
38
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
34
39
|
|
|
@@ -50,8 +55,17 @@ export const APPLY_CANT_QUEUE = "your Apply turn can't queue on this worker (res
|
|
|
50
55
|
// mode: review or pair.
|
|
51
56
|
export const PARTICIPANT_TURN_QUEUES = "goes in as your own turn after this participant's turn";
|
|
52
57
|
export const PARTICIPANT_TURN_QUEUES_SHORT = "after their turn";
|
|
58
|
+
// A participant's line: their own turn, the owner's, another participant's.
|
|
59
|
+
export const OWN_TURN_STEERS = "goes in at the next step · Alt+Enter keeps it for after the turn";
|
|
60
|
+
export const OWN_TURN_STEERS_SHORT = "goes in at the next step";
|
|
61
|
+
export const OWN_TURN_QUEUES = "goes in as your own turn after this one";
|
|
62
|
+
export const AFTER_OWNER = "Your message goes in as your own turn after the owner's";
|
|
63
|
+
export const afterTheirs = (name) => `Your message goes in as your own turn after ${name}'s`;
|
|
64
|
+
export const AFTER_SHORT = "after this turn";
|
|
53
65
|
// The idle row: the keys (the placeholder's hint before the row existed).
|
|
54
66
|
export const IDLE_HINT = "Enter to send · Shift+Enter newline · ↑ history";
|
|
67
|
+
// The same for a viewer without the prompt history (a participant).
|
|
68
|
+
export const IDLE_HINT_NO_HISTORY = "Enter to send · Shift+Enter newline";
|
|
55
69
|
|
|
56
70
|
// m:ss (h:mm:ss past an hour) of +seconds+; "" when unknown.
|
|
57
71
|
export function clockText(seconds) {
|
|
@@ -106,6 +120,9 @@ function thinkingOnly(gen, serverGen) {
|
|
|
106
120
|
// cut still wins)
|
|
107
121
|
// participantTurn the running turn is a participant's: Enter queues,
|
|
108
122
|
// no cut (the worker queues the owner's input behind it)
|
|
123
|
+
// viewerTurn a participant looks: {own, steer, by} (their own turn, it
|
|
124
|
+
// takes their message at its next step, its author's name or
|
|
125
|
+
// null for the owner's); null on the owner's page
|
|
109
126
|
// → {text, short, cut: {enabled, shown, reason, label, short}, queue:
|
|
110
127
|
// {enabled, shown, label, short}, armed, enter}: short is the phone's text;
|
|
111
128
|
// armed is what is still armed (a cut no longer possible disarms); enter
|
|
@@ -113,7 +130,8 @@ function thinkingOnly(gen, serverGen) {
|
|
|
113
130
|
// cutting off the cut link goes (the text says why); without the
|
|
114
131
|
// feature the queue link goes.
|
|
115
132
|
export function deliveryLine({ phase = "thinking", gen = null, tool = null, cutAfter = null, serverGen = null, now = Date.now(),
|
|
116
|
-
armed = null, canQueue = true, model = "", held = false, participantTurn = false } = {}) {
|
|
133
|
+
armed = null, canQueue = true, model = "", held = false, participantTurn = false, viewerTurn = null } = {}) {
|
|
134
|
+
if (viewerTurn) return viewerLine(viewerTurn);
|
|
117
135
|
const cutOff = cutAfter === 0 || (cutAfter != null && cutAfter < 0);
|
|
118
136
|
const thinking = phase === "thinking" && thinkingOnly(gen, serverGen);
|
|
119
137
|
const age = genAge(gen, serverGen, now);
|
|
@@ -177,6 +195,16 @@ export function deliveryLine({ phase = "thinking", gen = null, tool = null, cutA
|
|
|
177
195
|
};
|
|
178
196
|
}
|
|
179
197
|
|
|
198
|
+
// A participant's line (deliveryLine's +viewerTurn+): no links; Enter
|
|
199
|
+
// steers only into their own turn on a worker that takes it.
|
|
200
|
+
function viewerLine({ own = false, steer = false, by = null } = {}) {
|
|
201
|
+
const none = { enabled: false, shown: false, reason: "", label: QUEUE_LABEL, short: QUEUE_SHORT };
|
|
202
|
+
const line = (text, short, enter) => ({ text, short, cut: { ...none, label: CUT_LABEL, short: CUT_SHORT }, queue: none, armed: null, enter });
|
|
203
|
+
if (own && steer) return line(`\u21B3 ${OWN_TURN_STEERS}`, `\u21B3 ${OWN_TURN_STEERS_SHORT}`, null);
|
|
204
|
+
if (own) return line(`\u21B3 ${OWN_TURN_QUEUES}`, `\u21B3 ${AFTER_SHORT}`, "queue");
|
|
205
|
+
return line(by ? afterTheirs(by) : AFTER_OWNER, `\u21B3 after ${by ? `${by}'s` : "the owner's"} turn`, "queue");
|
|
206
|
+
}
|
|
207
|
+
|
|
180
208
|
// The line's view: +line+ is deliveryLine's; +onCut+ / +onQueue+ the
|
|
181
209
|
// links' clicks. A disabled cut stays in place, its reason on hover.
|
|
182
210
|
export function deliveryLineTemplate(line, { onCut, onQueue } = {}) {
|
|
@@ -191,8 +219,8 @@ export function deliveryLineTemplate(line, { onCut, onQueue } = {}) {
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
// The idle row: the key hint (none on a phone: no keyboard keys there).
|
|
194
|
-
function idleTemplate() {
|
|
195
|
-
return html`<span class="delivery-where idle"><span class="long">${
|
|
222
|
+
function idleTemplate(hint = IDLE_HINT) {
|
|
223
|
+
return html`<span class="delivery-where idle"><span class="long">${hint}</span><span class="short"></span></span>`;
|
|
196
224
|
}
|
|
197
225
|
|
|
198
226
|
// The row on the page: the key hint while idle, the line while the turn
|
|
@@ -205,7 +233,8 @@ function idleTemplate() {
|
|
|
205
233
|
// session() the open session's id: an Apply hold is that session's
|
|
206
234
|
const TICK_MS = 500;
|
|
207
235
|
|
|
208
|
-
|
|
236
|
+
// +history+: the viewer walks a prompt history with ↑ (the idle hint says so).
|
|
237
|
+
export function createDeliveryControl({ el, read, composerEmpty, send, session = () => null, now = () => Date.now(), history = true,
|
|
209
238
|
setTimer = (fn, ms) => setInterval(fn, ms), clearTimer = (t) => clearInterval(t) }) {
|
|
210
239
|
// "off" (no session: the row is gone), "idle" or "running".
|
|
211
240
|
let mode = "off";
|
|
@@ -239,7 +268,7 @@ export function createDeliveryControl({ el, read, composerEmpty, send, session =
|
|
|
239
268
|
function refresh() {
|
|
240
269
|
if (mode !== "running") {
|
|
241
270
|
mark();
|
|
242
|
-
if (mode === "idle") render(idleTemplate(), el);
|
|
271
|
+
if (mode === "idle") render(idleTemplate(history ? IDLE_HINT : IDLE_HINT_NO_HISTORY), el);
|
|
243
272
|
return;
|
|
244
273
|
}
|
|
245
274
|
line = deliveryLine({ ...read(), now: now(), armed, held: held() });
|
|
@@ -88,7 +88,11 @@ function rowItem(el) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function cardItem(el) {
|
|
91
|
-
|
|
91
|
+
const card = item("card", `${el.dataset.cardId || ""} ${textOf(el.querySelector(".card-title"))}`.trim());
|
|
92
|
+
// Its id on its own (already in the text): the reload check keeps a turn's
|
|
93
|
+
// own cards by it and drops the rest (reload_check.js).
|
|
94
|
+
card.id = el.dataset.cardId || "";
|
|
95
|
+
return card;
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function questionItem(el) {
|
|
@@ -19,6 +19,9 @@ export function imageFiles(dataTransfer) {
|
|
|
19
19
|
// would (spec/web/images_spec.rb pins both): ImageStore::MAX_TURN_REFS
|
|
20
20
|
// images a message, Web::App::MAX_IMAGE_UPLOAD_BYTES a file.
|
|
21
21
|
export const MAX_TURN_IMAGES = 20;
|
|
22
|
+
// Why the composer takes no image (composer_mount.js imagesOff) for a viewer
|
|
23
|
+
// without images (viewer.js): a paste, drop or pick toasts it.
|
|
24
|
+
export const IMAGES_NOT_HERE = "Images can't go in from here yet: describe what the image shows.";
|
|
22
25
|
export const MAX_IMAGE_MB = 20;
|
|
23
26
|
const MAX_IMAGE_BYTES = MAX_IMAGE_MB * 1024 * 1024;
|
|
24
27
|
|
|
@@ -75,8 +78,11 @@ export function imageUrl(sessionId, ref) {
|
|
|
75
78
|
|
|
76
79
|
// Thumbnails for a bubble or a tool row; nothing for none. `src` may be
|
|
77
80
|
// given (a blob: URL while the upload is on its way). A redraw with the
|
|
78
|
-
// same images keeps the <img> elements (no reload).
|
|
81
|
+
// same images keeps the <img> elements (no reload). A number is a count of
|
|
82
|
+
// images the viewer gets no refs to (another participant's: chi web's
|
|
83
|
+
// ParticipantView): a muted "N images" line instead.
|
|
79
84
|
export function thumbsTemplate(sessionId, images) {
|
|
85
|
+
if (typeof images === "number") return images > 0 ? html`<div class="thumbs-hidden">${images === 1 ? "1 image" : `${images} images`}</div>` : nothing;
|
|
80
86
|
const list = (images || []).filter((i) => i && (i.src || i.file));
|
|
81
87
|
if (!list.length) return nothing;
|
|
82
88
|
const thumbs = list.map((img) => {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<link rel="stylesheet" href="/assets/forms.css">
|
|
25
25
|
<link rel="stylesheet" href="/assets/objectives.css">
|
|
26
26
|
<link rel="stylesheet" href="/assets/block_renderers.css">
|
|
27
|
+
<link rel="stylesheet" href="/assets/ws_preview.css">
|
|
27
28
|
<style>
|
|
28
29
|
/* Icon masks shared by the phone info bar and the all view's cards (the stroke only shapes the mask; the icon takes currentColor); the image icon is composer.css's. */
|
|
29
30
|
:root{--icon-archive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M6.5 9h3'/%3E%3C/svg%3E");--icon-unarchive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M8 11.5V8.25M6.5 9.75 8 8.25l1.5 1.5'/%3E%3C/svg%3E")}
|
|
@@ -40,6 +41,8 @@ body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetic
|
|
|
40
41
|
(the gradient covers the body): it is the body's background-color, which is what iOS 26's Home Screen app paints
|
|
41
42
|
the status bar strip in (not theme-color, not html's background; with no colour the strip is black). */
|
|
42
43
|
@media(max-width:600px){body{background:linear-gradient(var(--bg-top),var(--bg) 420px),var(--bg-top)}}
|
|
44
|
+
/* iOS 26 blurs the Home Screen app's top band under the clock; measured 28px total is needed, and a fixed element does not help. */
|
|
45
|
+
@media (display-mode:standalone) and (max-width:600px){body{padding-top:12px}}
|
|
43
46
|
/* Glass: the composer card, the strip cards, the all-sessions cards and the question cards (by selector, so app.js is untouched). */
|
|
44
47
|
.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)}
|
|
45
48
|
@media(hover:hover){#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:var(--card-hover-border)}}
|
|
@@ -184,6 +187,8 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:max(16px,var(--s
|
|
|
184
187
|
/* Plugins' slow setup (chi.init, init_row.js): a quiet line per task over the composer. */
|
|
185
188
|
#initRow{display:flex;flex-direction:column;gap:2px;padding:6px 12px 2px;font-size:12px;line-height:1.4;color:var(--text-3)}
|
|
186
189
|
#initRow[hidden]{display:none}
|
|
190
|
+
/* A control the viewer may not use (viewer.js control): hidden whatever its own display rule says. */
|
|
191
|
+
[data-withheld]{display:none!important}
|
|
187
192
|
.init-task{display:flex;align-items:center;gap:7px;min-width:0;transition:opacity .4s ease}
|
|
188
193
|
.init-task.fading{opacity:0}
|
|
189
194
|
.init-bundle{flex:none;padding:0 5px;border:1px solid var(--border-strong);border-radius:4px;font-size:11px;color:var(--text-muted)}
|
|
@@ -759,6 +764,7 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 16px + var(-
|
|
|
759
764
|
/* A line queued for after the running turn: a dashed bubble, a turn of its own to come. */
|
|
760
765
|
.bubble.user.queued[data-delivery="queue"]{border:1px dashed var(--accent-border);border-left:3px dashed var(--accent);background:transparent}
|
|
761
766
|
.thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
767
|
+
.thumbs-hidden{margin-top:6px;font-size:12px;color:var(--text-muted)}
|
|
762
768
|
.thumb{max-height:120px;max-width:200px;border-radius:6px;border:1px solid var(--border-strong);cursor:zoom-in;object-fit:contain;background:var(--bg)}
|
|
763
769
|
.activity-row .thumbs{flex-basis:100%;margin-left:var(--row-gutter)}
|
|
764
770
|
@media(max-width:600px){.activity-row .thumbs{margin-left:0}}
|
|
@@ -1355,8 +1361,8 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1355
1361
|
.pp-list{list-style:none;display:flex;flex-direction:column;gap:8px;margin:2px 0}
|
|
1356
1362
|
.pp-row{display:grid;grid-template-columns:1fr auto;gap:2px 8px;padding:6px 8px;border-radius:8px;background:var(--bg-inset);border-left:3px solid var(--author-color)}
|
|
1357
1363
|
.pp-row.ended{opacity:.7}
|
|
1358
|
-
.
|
|
1359
|
-
.
|
|
1364
|
+
.pb-who{display:flex;align-items:center;gap:6px;min-width:0;color:var(--text)}
|
|
1365
|
+
.pb-who b{color:var(--author-color);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1360
1366
|
.pp-dot{width:7px;height:7px;border-radius:50%;background:var(--text-faint);flex-shrink:0}
|
|
1361
1367
|
.pp-dot.present{background:var(--ok)}
|
|
1362
1368
|
.pp-tag{font-size:10.5px;padding:0 6px;border-radius:999px;background:var(--bg-raised);color:var(--text-3)}
|
|
@@ -1435,8 +1441,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1435
1441
|
/* On a phone the panel is a sheet: the link goes out by Share or Copy, not by scanning this screen. */
|
|
1436
1442
|
.participants-popover.sheet .pp-qr{display:none}
|
|
1437
1443
|
.pp-foot{display:flex;align-items:center;gap:8px;justify-content:flex-end;padding-top:4px}
|
|
1438
|
-
.pp-view{font-size:12px;color:var(--accent);text-decoration:none}
|
|
1439
|
-
@media(hover:hover){.pp-view:hover{text-decoration:underline}}
|
|
1440
1444
|
/* The Home Screen app's notifications sheet (push_devices.js): the bell opens it; a bottom sheet on a phone. */
|
|
1441
1445
|
.push-devices .ctx-pop-head strong{font-family:inherit}
|
|
1442
1446
|
.push-devices.sheet{border-radius:14px;padding:14px 16px;font-size:14px}
|
|
@@ -2018,6 +2022,38 @@ a{color:var(--accent)}
|
|
|
2018
2022
|
.rs-form textarea,.pp-link input,.llmctx-field select,.llmctx-field input,.ws-add-in,.rename-input,
|
|
2019
2023
|
#modelSearch,.project-create input{font-size:16px}
|
|
2020
2024
|
}
|
|
2025
|
+
/* A participant's page (participant_main.js): a plain logo, their name and the link's end in the top row; the bottom row in
|
|
2026
|
+
the info line's place; the composer given way to one line (their limit, a stopped session, an ended link); the stage's
|
|
2027
|
+
look while their turn waits for the owner (stage_owner_wait.js). */
|
|
2028
|
+
.plain-brand{font-size:18px;font-weight:700;letter-spacing:.02em;color:var(--accent);padding:1px 4px 2px}
|
|
2029
|
+
#participantWho{display:flex;align-items:baseline;gap:12px;flex:1 1 auto;min-width:0;white-space:nowrap}
|
|
2030
|
+
body.participant-view #sessionTitleRow{flex:0 1 auto}
|
|
2031
|
+
#participantWho .pb-ends{margin-left:auto}
|
|
2032
|
+
.pb-who{display:inline-flex;align-items:center;gap:5px;padding:1px 9px;border-radius:999px;border:1px solid var(--border-strong);background:var(--bg-panel);font-size:12px;color:var(--text-2)}
|
|
2033
|
+
.pb-who b{color:var(--who-color,var(--author-5));font-weight:600}
|
|
2034
|
+
.pb-ends{color:var(--text-faint);font-size:12px}
|
|
2035
|
+
#participantBar{flex:1 1 100%;min-width:0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--text-3);padding:2px 0}
|
|
2036
|
+
.pb-pill{display:inline-flex;align-items:center;padding:1px 8px;border-radius:999px;background:var(--bg-raised);color:var(--text-3);white-space:nowrap}
|
|
2037
|
+
.pb-status[data-key]{color:var(--text-2)}
|
|
2038
|
+
.pb-gap{flex:1}
|
|
2039
|
+
.pb-rules{color:var(--text-faint);white-space:nowrap}
|
|
2040
|
+
#participantClosed{padding:12px 14px;color:var(--text-2);font-size:13px;line-height:1.45}
|
|
2041
|
+
#participantClosed b{display:block;color:var(--text);font-weight:600}
|
|
2042
|
+
body.participant-closed :is(#composeGrip,#deliveryLine,#composer,#infoBar){display:none!important}
|
|
2043
|
+
body.participant-view .bubble.user:not(.by-participant)>.origin-label{color:var(--author-owner);text-transform:none;letter-spacing:0;font-weight:600}
|
|
2044
|
+
.ts-owner-wait{margin:4px 0 6px;padding:6px 9px;border-radius:6px;background:var(--warn-bg);border:1px solid var(--warn-border);color:var(--warn-text);font-size:12.5px;line-height:1.4}
|
|
2045
|
+
.ts-owner-wait[hidden]{display:none}
|
|
2046
|
+
.turn-stage.owner-wait{border-left-color:var(--warn)}
|
|
2047
|
+
.turn-stage.owner-wait :is(.ts-now,.ts-trail){display:none}
|
|
2048
|
+
.turn-stage.owner-wait .ts-phase{color:var(--warn-text)}
|
|
2049
|
+
.turn-stage.owner-wait .ts-mark{background:var(--warn);animation:none}
|
|
2050
|
+
@media(max-width:600px){
|
|
2051
|
+
body.participant-view .plain-brand,body.participant-view .pb-ends,body.participant-view .pb-rules{display:none}
|
|
2052
|
+
body.participant-view #sessionTitleRow{flex-basis:auto;order:0}
|
|
2053
|
+
body.participant-view #topBar{flex-wrap:nowrap;align-items:center}
|
|
2054
|
+
/* Their delivery line is its own row over the text field (no session bar's left to lie over). */
|
|
2055
|
+
body.participant-view #deliveryLine.running:not([hidden]){display:flex!important;position:static!important;height:auto!important;padding:6px 2px 0!important}
|
|
2056
|
+
}
|
|
2021
2057
|
</style>
|
|
2022
2058
|
</head>
|
|
2023
2059
|
<body>
|
|
@@ -142,7 +142,8 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
142
142
|
const tokens = state.timing.tokens;
|
|
143
143
|
const ctxTip = ctxBarTip(tokens, state.timing.memoryIndex,
|
|
144
144
|
{ tokens: state.ctxWindow, source: state.ctxWindowSource, pct: state.ctxPct,
|
|
145
|
-
budget: state.llmContext?.budget_tokens, afterSwitch: state.ctxUnknownAfterSwitch
|
|
145
|
+
budget: state.llmContext?.budget_tokens, afterSwitch: state.ctxUnknownAfterSwitch,
|
|
146
|
+
running },
|
|
146
147
|
state.timing.modelSegments, state.timing.side);
|
|
147
148
|
const speedTextNow = lastSpeedText(tokens);
|
|
148
149
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
@@ -18,10 +18,11 @@ export function createMarkdownPlace({
|
|
|
18
18
|
const waiting = new WeakSet();
|
|
19
19
|
|
|
20
20
|
// A text the page already shows as its markdown HTML (POST /api/render,
|
|
21
|
-
// sanitised there), or null: markdown off here
|
|
22
|
-
//
|
|
21
|
+
// sanitised there), or null: markdown off here or no +renderText+ (a
|
|
22
|
+
// viewer without render, viewer.js: no request), or a failed read (the
|
|
23
|
+
// raw text stays).
|
|
23
24
|
function renderMarkdown(text) {
|
|
24
|
-
if (capabilities().display !== true) return Promise.resolve(null);
|
|
25
|
+
if (!renderText || capabilities().display !== true) return Promise.resolve(null);
|
|
25
26
|
return renderText(text)
|
|
26
27
|
.then((d) => (typeof d?.html === "string" && d.html ? d.html : null))
|
|
27
28
|
.catch(() => null);
|
|
@@ -79,7 +80,7 @@ export function createMarkdownPlace({
|
|
|
79
80
|
// on the swap, then the `path:line` ones (linkRefsIn). A null render
|
|
80
81
|
// keeps the text, and the next open asks again.
|
|
81
82
|
function renderStepTexts() {
|
|
82
|
-
if (capabilities().display !== true) return;
|
|
83
|
+
if (!renderText || capabilities().display !== true) return;
|
|
83
84
|
for (const root of roots()) {
|
|
84
85
|
for (const el of root.querySelectorAll(RAW_STEPS)) {
|
|
85
86
|
const step = el.parentElement;
|