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
|
@@ -47,13 +47,14 @@ import { createSessionStore } from "./session_store.js";
|
|
|
47
47
|
import { createFeatureGates } from "./feature_gates.js";
|
|
48
48
|
import { stillOn } from "./latest.js";
|
|
49
49
|
import { normalize, cantText } from "./format.js";
|
|
50
|
-
import { turnHistoryNodes } from "./turn_block.js";
|
|
51
50
|
import { elementOf } from "./turn_html.js";
|
|
52
51
|
import { createBubbles } from "./bubbles.js";
|
|
53
52
|
import { createCardsPlace } from "./cards_place.js";
|
|
54
53
|
import { createStageView } from "./stage_view.js";
|
|
55
54
|
import { createReloadCheck } from "./reload_check.js";
|
|
55
|
+
import { placeCardsOffscreen } from "./cards_offscreen.js";
|
|
56
56
|
import { createWorkingSet } from "./working_set_view.js";
|
|
57
|
+
import { createWorkingSetPreview } from "./ws_reader.js";
|
|
57
58
|
import { createObjectives } from "./objectives_wiring.js";
|
|
58
59
|
import { trackWorkerUnavailable } from "./objectives_track.js";
|
|
59
60
|
import { createGapClock } from "./time_gaps.js";
|
|
@@ -124,7 +125,11 @@ import { createSendTurn } from "./send_turn.js";
|
|
|
124
125
|
import { createSessionSync } from "./session_sync.js";
|
|
125
126
|
import { createSessionView } from "./session_view.js";
|
|
126
127
|
import { createMarkdownTip } from "./markdown_tip.js";
|
|
128
|
+
import { IMAGES_NOT_HERE } from "./images.js";
|
|
127
129
|
import { withoutStaleParams } from "./stale_params.js";
|
|
130
|
+
import { createViewer } from "./viewer.js";
|
|
131
|
+
import { createParticipantMain } from "./participant_main.js";
|
|
132
|
+
import { PARTICIPANT_REVISE_UNAVAILABLE, participantMayRevise, participantReviseUnavailable } from "./participant_revise.js";
|
|
128
133
|
|
|
129
134
|
// A link's #code= (behind an auth proxy): traded for the cookie before
|
|
130
135
|
// anything else runs; the page reloads without it.
|
|
@@ -141,6 +146,8 @@ const authLost = createAuthLost();
|
|
|
141
146
|
onAuthLost(authLost);
|
|
142
147
|
|
|
143
148
|
const $ = (s) => document.querySelector(s);
|
|
149
|
+
// Who looks and what they may do (viewer.js): every owner-only control and call asks it.
|
|
150
|
+
const viewer = createViewer(document.body);
|
|
144
151
|
const topStripEl = $("#topStrip");
|
|
145
152
|
const allListEl = $("#allList");
|
|
146
153
|
const historyEl = $("#history");
|
|
@@ -150,36 +157,38 @@ const composerEl = $("#composer");
|
|
|
150
157
|
// shows it; the tab's base ("Chi · <project>"), set by renderScope.
|
|
151
158
|
const sessionTitleEl = $("#sessionTitle");
|
|
152
159
|
const sessionTitleRowEl = $("#sessionTitleRow");
|
|
153
|
-
const titleRenameBtn = $("#titleRenameBtn");
|
|
160
|
+
const titleRenameBtn = viewer.control($("#titleRenameBtn"), "rename");
|
|
154
161
|
let tabBase = null;
|
|
155
162
|
let tabShown = null;
|
|
156
163
|
const infoTextEl = $("#infoText");
|
|
157
164
|
// The open session's attached context, in its own row of the session bar
|
|
158
165
|
// (updateInfoBar redraws #infoText every tick; the chips keep their popover).
|
|
159
|
-
const contextChips = createContextChips({
|
|
166
|
+
const contextChips = viewer.can("context") ? createContextChips({
|
|
160
167
|
el: $("#contextChips"),
|
|
161
168
|
api: { list: listContext, show: getContext, detach: detachContext },
|
|
162
169
|
toast: (text) => showToast(text),
|
|
163
|
-
});
|
|
170
|
+
}) : null;
|
|
164
171
|
// The open session's ⚙ chip in the info bar, and its panel (thinking,
|
|
165
172
|
// reasoning history, LLM context): a pick runs its command in the session's
|
|
166
173
|
// worker.
|
|
167
|
-
const settingsControl = createSettingsControl({
|
|
174
|
+
const settingsControl = viewer.can("session_controls") ? createSettingsControl({
|
|
168
175
|
el: infoTextEl,
|
|
169
176
|
summaries: () => (store.selected ? { thinking: store.current.thinking, reasoningHistory: store.current.reasoningHistory,
|
|
170
177
|
llmContext: store.current.llmContext } : null),
|
|
171
178
|
session: () => store.selected,
|
|
172
179
|
running: () => liveTurn.running,
|
|
173
180
|
run: (line) => runSessionCommand(line),
|
|
174
|
-
});
|
|
175
|
-
// ⋯ on the info bar, and its menu: the chips and buttons the row has no room for.
|
|
176
|
-
const
|
|
181
|
+
}) : null;
|
|
182
|
+
// ⋯ on the info bar, and its menu: the chips and buttons the row has no room for; and the bar's other buttons.
|
|
183
|
+
const [continueBtn, archiveBtn, stopBtn, deleteBtn, moreBtn] = viewer.controls(
|
|
184
|
+
["#infoContinueBtn", "#infoArchiveBtn", "#infoStopBtn", "#infoDeleteBtn", "#infoMoreBtn"].map($), "session_controls");
|
|
185
|
+
const moreSheet = moreBtn ? createMoreSheet({ el: $("#infoBar"), button: moreBtn, session: () => store.selected, hash: sessionHash }) : null;
|
|
177
186
|
// The bar's fold (bar_fold.js): one row, what doesn't fit folded into ⋯. A
|
|
178
187
|
// resize, the fonts or a zoom refolds it by itself: ⋯ follows on the redraw.
|
|
179
188
|
const barFold = createBarFold({ barEl: $("#infoBar"), keysFor: barItemKeys, onChange: () => store.touch("infoBar") });
|
|
180
189
|
// The open session's participants chip in the info bar, and its panel
|
|
181
190
|
// (Invite, revoke, caps, the participant view).
|
|
182
|
-
const participantsControl = createParticipantsControl({
|
|
191
|
+
const participantsControl = viewer.can("participants_admin") ? createParticipantsControl({
|
|
183
192
|
el: infoTextEl,
|
|
184
193
|
session: () => store.selected,
|
|
185
194
|
api: { list: listParticipants, invite: inviteParticipant, revoke: revokeParticipant, cap: setParticipantCap },
|
|
@@ -195,7 +204,7 @@ const participantsControl = createParticipantsControl({
|
|
|
195
204
|
},
|
|
196
205
|
onChange: () => store.touch("infoBar"),
|
|
197
206
|
toast: (text, action, options) => showToast(text, action, options),
|
|
198
|
-
});
|
|
207
|
+
}) : null;
|
|
199
208
|
// The start page's model picker, its ⚙ panel and 🧰 list with the override chips
|
|
200
209
|
// (start_page.js); a fresh chain link's picker too.
|
|
201
210
|
const startPage = createStartPage({
|
|
@@ -226,15 +235,14 @@ const markdownTip = createMarkdownTip($("#markdownTip"));
|
|
|
226
235
|
// What the page renders that a notice may stand in for (the server's
|
|
227
236
|
// Web::Capabilities, next to markdown_warning); a notice it names isn't
|
|
228
237
|
// drawn (shownElsewhere). An older server sends none: every notice shows.
|
|
229
|
-
// And
|
|
230
|
-
// editor
|
|
231
|
-
// to it: refs.js's actions read both (
|
|
238
|
+
// And whether this request's viewer can open a path in this machine's
|
|
239
|
+
// editor (per request), with the editor's URL template (editor_url) next
|
|
240
|
+
// to it: refs.js's actions read both (refEditor).
|
|
232
241
|
let pageCapabilities = {};
|
|
233
242
|
let pageEditorUrl = null;
|
|
234
243
|
const promptEl = $("#prompt");
|
|
235
244
|
const actionBtn = $("#actionBtn");
|
|
236
|
-
const attachBtn = $("#attachBtn");
|
|
237
|
-
const attachInput = $("#attachInput");
|
|
245
|
+
const [attachBtn, attachInput] = viewer.controls([$("#attachBtn"), $("#attachInput")], "images");
|
|
238
246
|
const composeHintEl = $("#composeHint");
|
|
239
247
|
|
|
240
248
|
// The open session (store.selected), its fields (store.current: session_state.js;
|
|
@@ -244,6 +252,12 @@ const composeHintEl = $("#composeHint");
|
|
|
244
252
|
// the page's first draw); an input the store doesn't hold calls
|
|
245
253
|
// store.touch(name).
|
|
246
254
|
const store = createSessionStore({ initial: emptySessionState() });
|
|
255
|
+
// An invited person's link: their session, name, bottom row and limits (participant_main.js).
|
|
256
|
+
const participant = viewer.kind() === "participant" ? createParticipantMain({ body: document.body, topBar: $("#topBar"), infoBar: $("#infoBar"),
|
|
257
|
+
composer: $("#composer"), store, running: () => liveTurn.running, stage: { redraw: () => turnDom.redraw?.() }, read: (id) => getSession(id),
|
|
258
|
+
onClosed: () => reviseStacks?.decorate() }) : null;
|
|
259
|
+
// Revise's worker gate and its words: a participant's own (participant_revise.js).
|
|
260
|
+
const [reviseGate, reviseOffText] = participant ? [participantReviseUnavailable, PARTICIPANT_REVISE_UNAVAILABLE] : [reviseWorkerUnavailable, undefined];
|
|
247
261
|
// The page actions the open session's worker must support (feature_gates.js),
|
|
248
262
|
// the store's featureGates redraw: a gate's redraw runs when its answer
|
|
249
263
|
// flips (a restart brings the feature, the worker goes). forgetSession and
|
|
@@ -258,7 +272,7 @@ const featureGates = createFeatureGates([
|
|
|
258
272
|
// Revise on an answer and the composer's Revising chip (revise_stack.js).
|
|
259
273
|
{
|
|
260
274
|
name: "revise",
|
|
261
|
-
unavailable: (cur) =>
|
|
275
|
+
unavailable: (cur) => reviseGate(cur.workerFeatures, cur.bridgeUp),
|
|
262
276
|
redraw: () => redrawRevise(),
|
|
263
277
|
},
|
|
264
278
|
// The objectives' Where are we? (objectives_track.js).
|
|
@@ -274,11 +288,11 @@ const openId = () => store.selected;
|
|
|
274
288
|
let pendingReviseAnchor = null;
|
|
275
289
|
let reviseStacks = null;
|
|
276
290
|
const reviseChipEl = $("#reviseChip");
|
|
277
|
-
const reviseUnavailable = () =>
|
|
291
|
+
const reviseUnavailable = () => reviseGate(store.current.workerFeatures, store.current.bridgeUp);
|
|
278
292
|
function redrawRevise() {
|
|
279
293
|
if (reviseChipEl) {
|
|
280
294
|
render(reviseChipTemplate(pendingReviseAnchor, {
|
|
281
|
-
unavailable: reviseUnavailable(),
|
|
295
|
+
unavailable: reviseUnavailable(), text: reviseOffText,
|
|
282
296
|
onClear: () => { setReviseAnchor(null); focusPrompt(); },
|
|
283
297
|
}), reviseChipEl);
|
|
284
298
|
reviseChipEl.hidden = !pendingReviseAnchor;
|
|
@@ -314,7 +328,7 @@ let eventsControl = null;
|
|
|
314
328
|
const shownTexts = createShownTexts();
|
|
315
329
|
const markdownPlace = createMarkdownPlace({
|
|
316
330
|
capabilities: () => pageCapabilities,
|
|
317
|
-
renderText,
|
|
331
|
+
renderText: viewer.can("render") ? renderText : null,
|
|
318
332
|
roots: () => turnRoots(),
|
|
319
333
|
touchedFiles,
|
|
320
334
|
currentDir: () => store.current.dir,
|
|
@@ -331,14 +345,17 @@ const { renderMarkdown, pairRefsIn, linkRefsIn, applyFinalMarkdown } = markdownP
|
|
|
331
345
|
// what each turn brought into context, in a rail in the stage, a footprint
|
|
332
346
|
// under each finished turn and a "set N" chip in the mem chip's place
|
|
333
347
|
// (working_set_view.js).
|
|
334
|
-
const
|
|
348
|
+
const wsLine = viewer.can("session_controls") ? (line) => (store.selected ? sendCommand(store.selected, line, { clientId }) : null) : null;
|
|
349
|
+
// What chi has of an item (the owner's): the rows' peek and the reader over the stage (ws_reader.js).
|
|
350
|
+
const wsPreview = document.body.dataset.workingSet === "1" && viewer.can("working_set_items")
|
|
351
|
+
? createWorkingSetPreview({ centerEl: $("#center"), session: () => store.selected, state: () => store.current, sendLine: wsLine }) : null;
|
|
352
|
+
const workingSet = document.body.dataset.workingSet === "1" && viewer.can("working_set")
|
|
335
353
|
? createWorkingSet({
|
|
336
354
|
historyEl, stageRoot: () => turnDom.root, state: () => store.current, session: () => store.selected, infoEl: infoTextEl,
|
|
337
|
-
onChange: () => store.touch("infoBar"), getWorkingSet,
|
|
338
|
-
// A drop or undrop from the list: the session's command, as typed.
|
|
339
|
-
sendLine: (line) => (store.selected ? sendCommand(store.selected, line, { clientId }) : null),
|
|
355
|
+
onChange: () => store.touch("infoBar"), getWorkingSet, sendLine: wsLine, peek: wsPreview?.peek ?? null, onTimeline: wsPreview?.open ?? null,
|
|
340
356
|
})
|
|
341
357
|
: null;
|
|
358
|
+
wsPreview?.attach(workingSet);
|
|
342
359
|
document.body.classList.toggle("working-set", !!workingSet);
|
|
343
360
|
// The running turn's DOM: the stage pinned above the composer
|
|
344
361
|
// (stage_view.js), the turn block (turn_block.js) in its cloud.
|
|
@@ -349,14 +366,15 @@ const turnDom = createStageView({
|
|
|
349
366
|
sessionId: () => store.selected,
|
|
350
367
|
// The stop-task button on a running task_wait, for a worker that has
|
|
351
368
|
// the route (its sidecar features).
|
|
352
|
-
taskStop: createTaskStopper({ sessionId: () => store.selected, features: () => store.current.workerFeatures }),
|
|
369
|
+
taskStop: viewer.can("session_controls") ? createTaskStopper({ sessionId: () => store.selected, features: () => store.current.workerFeatures }) : null,
|
|
353
370
|
dockEl: $("#dock"),
|
|
354
371
|
centerEl: $("#center"),
|
|
355
372
|
renderMarkdown,
|
|
356
373
|
linkRefs: linkRefsIn,
|
|
357
374
|
workingSet,
|
|
358
375
|
// Stop turn in its status row (disabled until the request settles).
|
|
359
|
-
onCancel: () => handleCancel(),
|
|
376
|
+
onCancel: viewer.can("stop_any_turn") || viewer.can("stop_own_turn") ? () => handleCancel() : null,
|
|
377
|
+
canCancel: () => !participant || participant.canCancel(), ownerWait: () => participant?.ownerWait() ?? null,
|
|
360
378
|
onHandedOff: (handed) => reloadCheck.handedOff(handed),
|
|
361
379
|
// The quiet signal (stage_stall.js): the stream, what the turn waits on, a joiner's step clock.
|
|
362
380
|
streamLive: () => sessionSync.streamLive(), timingNote: () => liveTurn.timing.note, serverGen: () => liveTurn.serverGen,
|
|
@@ -364,11 +382,13 @@ const turnDom = createStageView({
|
|
|
364
382
|
// The opt-in reload check (web.reload_check): a handed-off turn against a
|
|
365
383
|
// reload of it, the counts to chi web (reload_check.js).
|
|
366
384
|
const reloadCheck = createReloadCheck({
|
|
367
|
-
enabled: () => document.body.dataset.reloadCheck === "1",
|
|
385
|
+
enabled: () => document.body.dataset.reloadCheck === "1" && viewer.can("session_controls"),
|
|
368
386
|
sessionId: () => store.selected,
|
|
369
387
|
running: () => liveTurn.running,
|
|
370
388
|
read: (id) => getSession(id),
|
|
371
|
-
render: (data) =>
|
|
389
|
+
render: (data) => sessionView.reloadRender(data),
|
|
390
|
+
place: (holder, entries) => placeCardsOffscreen(holder, entries, { capabilities: pageCapabilities, answers: viewer.can("answer_cards") }),
|
|
391
|
+
capabilities: () => pageCapabilities,
|
|
372
392
|
post: (id, body) => api(`/api/sessions/${id}/reload_check`, { method: "POST", body: JSON.stringify(body) }),
|
|
373
393
|
});
|
|
374
394
|
// The history's bottom reserve and the toast's place follow the composer
|
|
@@ -388,7 +408,7 @@ function turnRoots() {
|
|
|
388
408
|
return [historyEl, turnDom.root];
|
|
389
409
|
}
|
|
390
410
|
const systemOneWrongAction = createSystemOneWrongAction({ sessionId: () => store.selected });
|
|
391
|
-
turnRoots().forEach((root) => root.addEventListener("click", systemOneWrongAction));
|
|
411
|
+
if (viewer.can("answer_cards")) turnRoots().forEach((root) => root.addEventListener("click", systemOneWrongAction));
|
|
392
412
|
function findInTurnRoots(selector) {
|
|
393
413
|
return turnRoots().flatMap((root) => [...root.querySelectorAll(selector)]);
|
|
394
414
|
}
|
|
@@ -425,10 +445,11 @@ const deliveryControl = createDeliveryControl({
|
|
|
425
445
|
serverGen: liveTurn.serverGen,
|
|
426
446
|
canQueue: store.current.workerFeatures.includes("delivery"),
|
|
427
447
|
model: store.current.model,
|
|
428
|
-
participantTurn: !!liveTurn.participantTurn,
|
|
448
|
+
participantTurn: !!liveTurn.participantTurn, viewerTurn: participant?.deliveryTurn() ?? null,
|
|
429
449
|
};
|
|
430
450
|
},
|
|
431
451
|
composerEmpty: () => !composer.hasDraft(),
|
|
452
|
+
history: viewer.can("prompt_history"),
|
|
432
453
|
send: (delivery) => handleSendTurn({ delivery }),
|
|
433
454
|
session: () => store.selected,
|
|
434
455
|
});
|
|
@@ -445,6 +466,7 @@ const objectives = createObjectives({
|
|
|
445
466
|
const questionCards = createQuestionCards({
|
|
446
467
|
sessionId: () => store.selected,
|
|
447
468
|
clientId,
|
|
469
|
+
answers: viewer.can("answer_cards"),
|
|
448
470
|
place: (card) => appendToHistory(card),
|
|
449
471
|
isNearBottom: () => isNearBottom(),
|
|
450
472
|
scrollToEnd: () => { historyEl.scrollTop = historyEl.scrollHeight; },
|
|
@@ -498,9 +520,10 @@ turnRoots().forEach((root) => root.addEventListener("click", cards.onCardAction)
|
|
|
498
520
|
// the ones made later come in as getters, and each reaches the other lazily.
|
|
499
521
|
const sessionView = createSessionView({
|
|
500
522
|
store, historyEl, getSession,
|
|
501
|
-
liveTurn, turnDom, questionCards, shownTexts, objectives, featureGates, markdownTip, workingSet, gapClock,
|
|
523
|
+
liveTurn, turnDom, questionCards, shownTexts, objectives, featureGates, markdownTip, workingSet, wsPreview, gapClock,
|
|
502
524
|
bubbles, cards, markdown: markdownPlace,
|
|
503
525
|
capabilities: () => pageCapabilities,
|
|
526
|
+
can: viewer.can,
|
|
504
527
|
page: {
|
|
505
528
|
setReviseAnchor, renderInitRow, setMarkdownWarning, setCapabilities, findInTurnRoots, appendToHistory,
|
|
506
529
|
setTurnRunning, mergeTurnTiming, applySessionPayload, turnEndStatus, replaceSelectedCard,
|
|
@@ -510,7 +533,7 @@ const sessionView = createSessionView({
|
|
|
510
533
|
commandList: () => commandList, initTasks: () => initTasks, resetTurnView: () => sessionSync.resetTurnView(),
|
|
511
534
|
},
|
|
512
535
|
});
|
|
513
|
-
const { forgetSession,
|
|
536
|
+
const { forgetSession, readTurnEnd, endTurnView } = sessionView;
|
|
514
537
|
const sessionSync = createSessionSync({
|
|
515
538
|
store, historyEl, emptyEl, getSession, openStream,
|
|
516
539
|
liveTurn, turnDom, questionCards, contextChips, participantsControl,
|
|
@@ -555,9 +578,10 @@ function sweepFoldsOnce() {
|
|
|
555
578
|
// The list by fetching: the fallback when there is no events stream (a
|
|
556
579
|
// chi web without a hub, or ?dir the server refused: the 400 is read here).
|
|
557
580
|
async function refresh() {
|
|
581
|
+
if (!viewer.can("sessions")) return;
|
|
558
582
|
try {
|
|
559
583
|
store.update({ sessions: await listSessions("updated_at", "desc", { dir: scope.dir }) });
|
|
560
|
-
notifications
|
|
584
|
+
notifications?.noticeList(store.sessions);
|
|
561
585
|
sweepFoldsOnce();
|
|
562
586
|
} catch (e) {
|
|
563
587
|
if (e.status !== 400 || !scope.dir) throw e;
|
|
@@ -571,6 +595,7 @@ async function refresh() {
|
|
|
571
595
|
// (re)connect, then upserts and removals. The selected session's owner and
|
|
572
596
|
// worker follow its events (a new worker's bridge_up reconnects the stream).
|
|
573
597
|
function openSessionEvents() {
|
|
598
|
+
if (!viewer.can("sessions")) return;
|
|
574
599
|
eventsControl = openEvents({
|
|
575
600
|
// The list redraws the lists and the bar (its children chip, its chain's
|
|
576
601
|
// ← → links): store.update({sessions}).
|
|
@@ -578,19 +603,19 @@ function openSessionEvents() {
|
|
|
578
603
|
versionNotices.notice(data);
|
|
579
604
|
versionNotices.reconnected(data.version);
|
|
580
605
|
if (data.chi_update) versionNotices.frame(data.chi_update, { snapshot: true });
|
|
581
|
-
notifications
|
|
606
|
+
notifications?.noticeSessions("snapshot", data);
|
|
582
607
|
store.update({ sessions: applySessionEvent(store.sessions, "snapshot", data) });
|
|
583
608
|
sweepFoldsOnce();
|
|
584
609
|
const sess = store.sessions.find((s) => s.id === store.selected);
|
|
585
610
|
if (sess) followSelectedSession(sess);
|
|
586
611
|
}),
|
|
587
612
|
session: (data) => store.batch(() => {
|
|
588
|
-
notifications
|
|
613
|
+
notifications?.noticeSessions("session", data);
|
|
589
614
|
store.update({ sessions: applySessionEvent(store.sessions, "session", data) });
|
|
590
615
|
if (data.session && data.session.id === store.selected) followSelectedSession(data.session);
|
|
591
616
|
}),
|
|
592
617
|
session_gone: (data) => {
|
|
593
|
-
notifications
|
|
618
|
+
notifications?.noticeSessions("session_gone", data);
|
|
594
619
|
lists.forget(data.id);
|
|
595
620
|
store.update({ sessions: applySessionEvent(store.sessions, "session_gone", data) });
|
|
596
621
|
leaveGoneSession(data.id, "deleted");
|
|
@@ -621,17 +646,19 @@ const EVENTS_REOPEN_MS = 3000;
|
|
|
621
646
|
// to the same place without ?dir); "all projects" otherwise, with where a
|
|
622
647
|
// new session starts on the start page.
|
|
623
648
|
function renderScope() {
|
|
624
|
-
const el = $("#scope");
|
|
625
|
-
|
|
626
|
-
|
|
649
|
+
const el = viewer.control($("#scope"), "sessions");
|
|
650
|
+
if (el) {
|
|
651
|
+
render(scopeTemplate(scope, { error: scopeError, where: () => ({ hash: location.hash }) }), el);
|
|
652
|
+
el.hidden = false;
|
|
653
|
+
}
|
|
627
654
|
// The tab: "<session title> · Chi · <project>" (updateSessionTitle).
|
|
628
655
|
tabBase = scope.projectName ? `Chi · ${scope.projectName}` : "Chi";
|
|
629
656
|
updateSessionTitle();
|
|
630
657
|
}
|
|
631
658
|
|
|
632
|
-
createProjectPicker({
|
|
633
|
-
|
|
634
|
-
|
|
659
|
+
if (viewer.can("sessions")) createProjectPicker({ el: $("#scope"), scope, scopeError: () => scopeError, api });
|
|
660
|
+
// The strip, its pill and grip: the session list's (lists_view.js draws into the hidden strip all the same).
|
|
661
|
+
viewer.controls([topStripEl, $("#stripStatus"), $("#stripGrip")], "sessions");
|
|
635
662
|
|
|
636
663
|
// The session lists (lists_view.js): the strip, all sessions, their cards,
|
|
637
664
|
// select mode, the family and chain popovers.
|
|
@@ -713,7 +740,7 @@ const rename = createRename({
|
|
|
713
740
|
onClose: () => queueMicrotask(() => lists.catchUp()),
|
|
714
741
|
run: (line) => runSessionCommand(line),
|
|
715
742
|
});
|
|
716
|
-
titleRenameBtn
|
|
743
|
+
titleRenameBtn?.addEventListener("click", () => rename.open());
|
|
717
744
|
document.addEventListener("scroll", () => rename.reposition(), true);
|
|
718
745
|
window.addEventListener("resize", () => rename.reposition());
|
|
719
746
|
|
|
@@ -723,7 +750,7 @@ const { confirmDelete, leaveGoneSession, continueSelected, archiveOne, setArchiv
|
|
|
723
750
|
// leaves it. Opened from the page, it closes with history.back().
|
|
724
751
|
let allViewPushed = false;
|
|
725
752
|
function allViewOpen() {
|
|
726
|
-
return location.hash === ALL_SESSIONS_HASH;
|
|
753
|
+
return !participant && location.hash === ALL_SESSIONS_HASH;
|
|
727
754
|
}
|
|
728
755
|
function openAllView() {
|
|
729
756
|
if (allViewOpen()) return;
|
|
@@ -763,7 +790,8 @@ function setSessionHash(id) {
|
|
|
763
790
|
function selectFromHash() {
|
|
764
791
|
const claim = claimFromHash(location.hash);
|
|
765
792
|
if (claim) ownerClaim.pending(claim);
|
|
766
|
-
|
|
793
|
+
// A participant's page has one session, whatever the hash names.
|
|
794
|
+
const intent = participant ? { kind: "session", id: participant.sessionId } : hashIntent(location.hash, { selected: store.selected, claim });
|
|
767
795
|
if (intent.kind === "session" && intent.id !== store.selected) select(intent.id);
|
|
768
796
|
else if (claim) ownerClaim.showPending();
|
|
769
797
|
else if (intent.kind === "start") clearSelection();
|
|
@@ -771,7 +799,7 @@ function selectFromHash() {
|
|
|
771
799
|
// The page's claim links (owner_claim.js): one opened here, once drawn, and
|
|
772
800
|
// annotate's Copy link.
|
|
773
801
|
const ownerClaim = createOwnerClaim({
|
|
774
|
-
historyEl, store, claimTarget: () => participantsControl
|
|
802
|
+
historyEl, store, claimTarget: () => participantsControl?.claimTarget() ?? null,
|
|
775
803
|
toast: (...args) => showToast(...args), copyText, allViewOpen, home: document.body.dataset.home || "",
|
|
776
804
|
});
|
|
777
805
|
function applyView() {
|
|
@@ -807,7 +835,7 @@ window.addEventListener("hashchange", () => {
|
|
|
807
835
|
const composerMode = createComposerMode({
|
|
808
836
|
els: { body: document.body, prompt: promptEl, hint: composeHintEl, button: actionBtn, attach: attachBtn, empty: emptyEl,
|
|
809
837
|
history: historyEl },
|
|
810
|
-
store, liveTurn, startPage, deliveryControl, fitPrompt, placeToast,
|
|
838
|
+
store, liveTurn, startPage, deliveryControl, fitPrompt, placeToast, steers: () => !participant || participant.deliveryTurn().steer,
|
|
811
839
|
});
|
|
812
840
|
|
|
813
841
|
function setTurnRunning(running) {
|
|
@@ -863,8 +891,8 @@ function clearSelection() {
|
|
|
863
891
|
store.update({ selected: null });
|
|
864
892
|
setSessionHash(null);
|
|
865
893
|
forgetSession();
|
|
866
|
-
contextChips
|
|
867
|
-
participantsControl
|
|
894
|
+
contextChips?.clear();
|
|
895
|
+
participantsControl?.load(null);
|
|
868
896
|
});
|
|
869
897
|
focusPrompt();
|
|
870
898
|
}
|
|
@@ -901,22 +929,26 @@ function setMarkdownWarning(warning) {
|
|
|
901
929
|
markdownWarningEl.hidden = false;
|
|
902
930
|
}
|
|
903
931
|
|
|
904
|
-
// Both from one payload, together; the body's has-editor class follows.
|
|
905
|
-
|
|
932
|
+
// Both from one payload, together; the body's has-editor class follows. A
|
|
933
|
+
// show's viewer narrows the page's (viewer.js); none read (forgetSession) is the stamp's again.
|
|
934
|
+
function setCapabilities(capabilities, editorUrl, shownViewer = null) {
|
|
906
935
|
pageCapabilities = capabilities && typeof capabilities === "object" ? capabilities : {};
|
|
907
936
|
pageEditorUrl = pageCapabilities.editor === true && typeof editorUrl === "string" && editorUrl ? editorUrl : null;
|
|
908
937
|
document.body.classList.toggle("has-editor", pageEditorUrl !== null);
|
|
938
|
+
if (shownViewer) viewer.update(shownViewer);
|
|
939
|
+
else if (!capabilities) viewer.reset();
|
|
940
|
+
participant?.shown(shownViewer);
|
|
909
941
|
}
|
|
910
942
|
|
|
911
|
-
//
|
|
912
|
-
function
|
|
943
|
+
// Where this page can open a ref (refs.js REF_ACTIONS): not the viewer's capabilities (viewer.js).
|
|
944
|
+
function refEditor() {
|
|
913
945
|
return { editor: pageCapabilities.editor === true, editorUrl: pageEditorUrl };
|
|
914
946
|
}
|
|
915
947
|
|
|
916
948
|
// Refs anywhere on the page (a tool row's file, its diff lines, an
|
|
917
949
|
// answer's path:line): one listener set; "Copy path" says so in a toast.
|
|
918
|
-
installRefClicks(document.body,
|
|
919
|
-
copy: async (text) => showToast((await copyText(text)) ? "Path copied" : "Couldn't copy the path"),
|
|
950
|
+
installRefClicks(document.body, refEditor, {
|
|
951
|
+
copy: async (text) => showToast((await copyText(text)) ? "Path copied" : "Couldn't copy the path"), whatChiHas: wsPreview?.whatChiHas ?? null,
|
|
920
952
|
});
|
|
921
953
|
|
|
922
954
|
// Sticky-follow scroll: only auto-scroll to the bottom while the user is
|
|
@@ -963,7 +995,7 @@ const ownerVerify = createOwnerVerify({
|
|
|
963
995
|
|
|
964
996
|
// Apply: the owner's Apply turn into the composer, after what is typed
|
|
965
997
|
// there; never sent from here.
|
|
966
|
-
historyEl.addEventListener("click", async (e) => {
|
|
998
|
+
if (viewer.can("apply")) historyEl.addEventListener("click", async (e) => {
|
|
967
999
|
const button = e.target.closest(".review-apply-btn");
|
|
968
1000
|
if (!button || !store.selected) return;
|
|
969
1001
|
const id = store.selected;
|
|
@@ -992,15 +1024,12 @@ function sessionWaits() {
|
|
|
992
1024
|
}
|
|
993
1025
|
function updateInfoBar() {
|
|
994
1026
|
annotateBar?.refresh();
|
|
1027
|
+
if (participant) return [participant.drawBar(), updateSessionTitle()];
|
|
995
1028
|
const { buttons, bar } = sessionBar({
|
|
996
1029
|
id: store.selected, state: store.current, sessions: store.sessions, versions: versionNotices.versions(), cardTitle: (s) => lists.cardTitle(s),
|
|
997
1030
|
continuing: sessionActions.continuing, restarting: versionNotices.restarting, streaming: sessionSync.hasStream(), running: liveTurn.running, waiting: sessionWaits(),
|
|
998
|
-
workingSet: workingSet ? workingSet.chip() : undefined, participants: participantsControl
|
|
1031
|
+
workingSet: workingSet ? workingSet.chip() : undefined, participants: participantsControl?.chip() ?? null,
|
|
999
1032
|
});
|
|
1000
|
-
const stopBtn = document.getElementById("infoStopBtn");
|
|
1001
|
-
const deleteBtn = document.getElementById("infoDeleteBtn");
|
|
1002
|
-
const archiveBtn = document.getElementById("infoArchiveBtn");
|
|
1003
|
-
const continueBtn = document.getElementById("infoContinueBtn");
|
|
1004
1033
|
if (continueBtn) {
|
|
1005
1034
|
continueBtn.style.display = buttons.continue.shown ? "" : "none";
|
|
1006
1035
|
continueBtn.disabled = buttons.continue.disabled;
|
|
@@ -1016,6 +1045,8 @@ function updateInfoBar() {
|
|
|
1016
1045
|
archiveBtn.title = buttons.archive.title;
|
|
1017
1046
|
}
|
|
1018
1047
|
if (stopBtn) stopBtn.style.display = buttons.stop.shown ? "" : "none";
|
|
1048
|
+
// The ⚙ chip only with its panel (session_controls, viewer.js).
|
|
1049
|
+
if (bar && !settingsControl) bar.settings = null;
|
|
1019
1050
|
drawFoldControls();
|
|
1020
1051
|
// No session: the start page hides the info bar (the storage path is on
|
|
1021
1052
|
// #/sessions). The session clock ticks every second while a turn runs:
|
|
@@ -1023,7 +1054,7 @@ function updateInfoBar() {
|
|
|
1023
1054
|
// mid-press) stay.
|
|
1024
1055
|
renderInfoBar(infoTextEl, bar);
|
|
1025
1056
|
const { folded } = barFold.update({ bar, buttons });
|
|
1026
|
-
moreSheet
|
|
1057
|
+
moreSheet?.update({ bar, buttons, folded });
|
|
1027
1058
|
updateSessionTitle();
|
|
1028
1059
|
// A fresh link's picker follows what the bar follows: its model, its
|
|
1029
1060
|
// owner, a next link continued from elsewhere.
|
|
@@ -1035,6 +1066,7 @@ function updateInfoBar() {
|
|
|
1035
1066
|
// browser tab names it too. It follows the card's (openTitle): a chain link
|
|
1036
1067
|
// reads its card's title (cardTitle), whatever opener it was sent.
|
|
1037
1068
|
function shownTitle() {
|
|
1069
|
+
if (participant) return participant.title();
|
|
1038
1070
|
const shown = store.selected ? lists.sessionsById().get(store.selected) : null;
|
|
1039
1071
|
const first = store.current.firstPreview || "";
|
|
1040
1072
|
const raw = !store.selected ? "" : openTitle({ shown, first, cardTitle: (s) => lists.cardTitle(s) });
|
|
@@ -1053,13 +1085,14 @@ function updateSessionTitle() {
|
|
|
1053
1085
|
if (sessionTitleEl.textContent !== text) sessionTitleEl.textContent = text;
|
|
1054
1086
|
if (sessionTitleRowEl.hidden !== !text) sessionTitleRowEl.hidden = !text;
|
|
1055
1087
|
const renamable = !!text && renameShown(store.current.owner);
|
|
1056
|
-
if (titleRenameBtn.hidden !== !renamable) titleRenameBtn.hidden = !renamable;
|
|
1088
|
+
if (titleRenameBtn && titleRenameBtn.hidden !== !renamable) titleRenameBtn.hidden = !renamable;
|
|
1057
1089
|
const tip = text ? title : "";
|
|
1058
1090
|
if (sessionTitleEl.title !== tip) sessionTitleEl.title = tip;
|
|
1059
1091
|
const tab = tabBase === null ? null : tabTitle({ title, base: tabBase });
|
|
1060
1092
|
if (tab !== null && tab !== tabShown) {
|
|
1061
1093
|
tabShown = tab;
|
|
1062
|
-
notifications.setBaseTitle(tab);
|
|
1094
|
+
if (notifications) notifications.setBaseTitle(tab);
|
|
1095
|
+
else document.title = tab;
|
|
1063
1096
|
}
|
|
1064
1097
|
rename.follow(store.selected);
|
|
1065
1098
|
rename.reposition();
|
|
@@ -1139,7 +1172,7 @@ function turnEndStatus(data) {
|
|
|
1139
1172
|
|
|
1140
1173
|
// The stream's event handlers (stream_handlers.js); renderSessionView
|
|
1141
1174
|
// replays a snapshot through them too.
|
|
1142
|
-
const streamHandlers = createStreamHandlers({
|
|
1175
|
+
const streamHandlers = (participant ? participant.wrapHandlers : (handlers) => handlers)(createStreamHandlers({
|
|
1143
1176
|
selected: () => store.selected,
|
|
1144
1177
|
store,
|
|
1145
1178
|
clientId,
|
|
@@ -1152,7 +1185,7 @@ const streamHandlers = createStreamHandlers({
|
|
|
1152
1185
|
contextChips,
|
|
1153
1186
|
workingSet,
|
|
1154
1187
|
participantsControl,
|
|
1155
|
-
placeApply,
|
|
1188
|
+
placeApply: viewer.can("apply") ? placeApply : null,
|
|
1156
1189
|
placeVerdict: ownerVerify.placeVerdict,
|
|
1157
1190
|
restoreVerify: (anchor) => ownerVerify.set(anchor),
|
|
1158
1191
|
restoreRevise,
|
|
@@ -1191,7 +1224,7 @@ const streamHandlers = createStreamHandlers({
|
|
|
1191
1224
|
syncModelPick: startPage.commandRan,
|
|
1192
1225
|
objectivesUpdated: (data) => objectives.update(data),
|
|
1193
1226
|
cardActionFailed: cards.actionFailed,
|
|
1194
|
-
});
|
|
1227
|
+
}));
|
|
1195
1228
|
|
|
1196
1229
|
async function refreshTimingFromSession() {
|
|
1197
1230
|
if (!store.selected) return;
|
|
@@ -1216,7 +1249,7 @@ function addCreatedSession(created) {
|
|
|
1216
1249
|
|
|
1217
1250
|
// /model, !ls … from the composer, and a control's command (the llm ctx
|
|
1218
1251
|
// and thinking chips): composer.js.
|
|
1219
|
-
const composerCommands = createComposerCommands({
|
|
1252
|
+
const composerCommands = viewer.can("session_controls") ? createComposerCommands({
|
|
1220
1253
|
session: () => store.selected,
|
|
1221
1254
|
clientId,
|
|
1222
1255
|
sendCommand,
|
|
@@ -1239,7 +1272,7 @@ const composerCommands = createComposerCommands({
|
|
|
1239
1272
|
focusPrompt({ open: false });
|
|
1240
1273
|
},
|
|
1241
1274
|
},
|
|
1242
|
-
});
|
|
1275
|
+
}) : null;
|
|
1243
1276
|
|
|
1244
1277
|
// The composer's send (send_turn.js): a line into the open session, or a
|
|
1245
1278
|
// new session first on the start page.
|
|
@@ -1283,7 +1316,8 @@ const sendTurnControl = createSendTurn({
|
|
|
1283
1316
|
followWokenWorker,
|
|
1284
1317
|
refreshHistory,
|
|
1285
1318
|
leaveGoneSession,
|
|
1286
|
-
|
|
1319
|
+
closesOn: participant?.refused ?? null,
|
|
1320
|
+
runCommand: composerCommands ? (command) => composerCommands.send(command) : null,
|
|
1287
1321
|
toast: showToast,
|
|
1288
1322
|
});
|
|
1289
1323
|
// +delivery+: the composer's choice for a running turn (Alt+Enter: queue;
|
|
@@ -1293,7 +1327,7 @@ function handleSendTurn({ delivery = null } = {}) {
|
|
|
1293
1327
|
}
|
|
1294
1328
|
|
|
1295
1329
|
function runSessionCommand(line) {
|
|
1296
|
-
return composerCommands
|
|
1330
|
+
return composerCommands?.run(line);
|
|
1297
1331
|
}
|
|
1298
1332
|
|
|
1299
1333
|
function clearComposer() {
|
|
@@ -1321,7 +1355,7 @@ async function handleCancel() {
|
|
|
1321
1355
|
// session (from all sessions too).
|
|
1322
1356
|
// The typed draft stays in the composer: paste in a session, then + new session
|
|
1323
1357
|
// starts from it (with the model picker). clearSelection leaves #prompt alone.
|
|
1324
|
-
$("#newBtn")
|
|
1358
|
+
viewer.control($("#newBtn"), "sessions")?.addEventListener("click", () => {
|
|
1325
1359
|
if (allViewOpen()) leaveAllView("");
|
|
1326
1360
|
clearSelection();
|
|
1327
1361
|
startPage.loadModels({ asked: true });
|
|
@@ -1347,7 +1381,8 @@ const composer = mountComposer({
|
|
|
1347
1381
|
toast: (text) => showToast(text),
|
|
1348
1382
|
onWrite: () => noteDraft(),
|
|
1349
1383
|
// ↑/↓: the prompt history shared with the TUI.
|
|
1350
|
-
loadHistory: fetchHistory,
|
|
1384
|
+
loadHistory: viewer.can("prompt_history") ? fetchHistory : null,
|
|
1385
|
+
imagesOff: viewer.can("images") ? null : IMAGES_NOT_HERE,
|
|
1351
1386
|
// Alt/Option+Enter: queued for after the running turn (D6), only to a
|
|
1352
1387
|
// worker that queues (else it would steer): the text stays, a note says why.
|
|
1353
1388
|
send: ({ queue }) => {
|
|
@@ -1412,19 +1447,19 @@ document.addEventListener("keydown", (e) => {
|
|
|
1412
1447
|
const commandList = createCommandList({
|
|
1413
1448
|
list: $("#commandList"),
|
|
1414
1449
|
prompt: promptEl,
|
|
1415
|
-
commands: () => (store.selected && store.current.owner !== "tui" ? store.current.commands : []),
|
|
1450
|
+
commands: () => (viewer.can("session_controls") && store.selected && store.current.owner !== "tui" ? store.current.commands : []),
|
|
1416
1451
|
afterPick: () => {
|
|
1417
1452
|
noteDraft();
|
|
1418
1453
|
fitPrompt();
|
|
1419
1454
|
},
|
|
1420
1455
|
});
|
|
1421
1456
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1457
|
+
stopBtn?.addEventListener("click", () => sessionActions.stopSelected());
|
|
1458
|
+
deleteBtn?.addEventListener("click", () => {
|
|
1424
1459
|
if (store.selected) confirmDelete(store.selected);
|
|
1425
1460
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1461
|
+
continueBtn?.addEventListener("click", () => continueSelected());
|
|
1462
|
+
archiveBtn?.addEventListener("click", () => sessionActions.toggleArchiveSelected());
|
|
1428
1463
|
|
|
1429
1464
|
// The new link's composer: the opener (openerText: the previous link's own,
|
|
1430
1465
|
// else the constant). A draft written in this session (or the new link, or
|
|
@@ -1457,6 +1492,7 @@ function restoreDraft(draft, opener) {
|
|
|
1457
1492
|
const toasts = createToasts({ el: $("#toast"), onChange: placeToast });
|
|
1458
1493
|
// chi's versions, the update sheet and the worker Restart (version_notices.js).
|
|
1459
1494
|
// After toasts: openSessionEvents, its first caller, runs at boot.
|
|
1495
|
+
if (participant) onAuthLost(participant.linkEnded);
|
|
1460
1496
|
const versionNotices = createVersionNotices({
|
|
1461
1497
|
store, toasts, dialog: $("#updateSheet"), loadedVersion: document.body.dataset.version,
|
|
1462
1498
|
api: { updateChi, restartWorkers, restartWeb, restartSession },
|
|
@@ -1515,7 +1551,7 @@ turnRoots().forEach((root) => installCopy(root));
|
|
|
1515
1551
|
// chi-ui blocks (inline_widgets.js) are a core drawer, ahead of the bundles'
|
|
1516
1552
|
// renderers; a picks' Fill reply only fills the composer.
|
|
1517
1553
|
const widgets = createWidgetDrawer({ roots: turnRoots(), composerText: () => promptEl.value, fillComposer });
|
|
1518
|
-
createBlockHost({ roots: turnRoots(), sessionId: () => store.selected, drawers: [widgets] });
|
|
1554
|
+
createBlockHost({ roots: turnRoots(), sessionId: () => store.selected, drawers: [widgets], renderers: viewer.can("block_renderers") });
|
|
1519
1555
|
// A steps block, a thinking block and the recap open and close by hand
|
|
1520
1556
|
// with their height animated (details_motion.js), in the history and on
|
|
1521
1557
|
// the stage.
|
|
@@ -1528,34 +1564,34 @@ installAnswerFold({
|
|
|
1528
1564
|
});
|
|
1529
1565
|
// Revise stacks (revise_stack.js): the superseded versions fold to stubs,
|
|
1530
1566
|
// the top answer gets its version chip, a final answer its Revise button.
|
|
1531
|
-
reviseStacks = installReviseStacks({
|
|
1567
|
+
reviseStacks = viewer.can("revise") || viewer.can("revise_own_turn") ? installReviseStacks({
|
|
1532
1568
|
historyEl,
|
|
1533
1569
|
sessionId: () => store.selected,
|
|
1534
1570
|
projectRoot: () => store.sessions.find((s) => s.id === store.selected)?.project_root ?? null,
|
|
1535
1571
|
running: () => (liveTurn.running ? { turnId: liveTurn.timing.turnId || null } : null),
|
|
1536
1572
|
verifyTurn: (turnId) => (store.current.verdicts || []).some((v) => v.check_turn_id === turnId),
|
|
1537
|
-
unavailable: reviseUnavailable,
|
|
1538
|
-
armed: () => pendingReviseAnchor?.turn_id ?? null,
|
|
1573
|
+
unavailable: reviseUnavailable, unavailableText: reviseOffText,
|
|
1574
|
+
armed: () => pendingReviseAnchor?.turn_id ?? null, mayRevise: participant ? participantMayRevise(participant.closed) : undefined,
|
|
1539
1575
|
onRevise: (anchor) => {
|
|
1540
1576
|
setReviseAnchor(anchor);
|
|
1541
1577
|
openForVisit();
|
|
1542
1578
|
focusPrompt();
|
|
1543
1579
|
},
|
|
1544
|
-
});
|
|
1580
|
+
}) : null;
|
|
1545
1581
|
|
|
1546
1582
|
// Notifications (notifications.js): an OS notification and a title count
|
|
1547
1583
|
// when a session needs the user while this tab is not in front. On a
|
|
1548
1584
|
// project's page the Home Screen app's device tidy-up still covers every
|
|
1549
1585
|
// project: it gets a stream of every session of its own (allEvents).
|
|
1550
|
-
const notifications = createNotifications({
|
|
1586
|
+
const notifications = viewer.can("push") ? createNotifications({
|
|
1551
1587
|
button: $("#notifyBtn"),
|
|
1552
1588
|
scoped: !!scope.dir,
|
|
1553
1589
|
allEvents: (handlers, opts) => openEvents(handlers, opts),
|
|
1554
|
-
});
|
|
1590
|
+
}) : null;
|
|
1555
1591
|
|
|
1556
1592
|
// The strip hidden or shown (strip_hidden.js): its grip, the saved choice and
|
|
1557
1593
|
// the short window's rule; applied now.
|
|
1558
|
-
createStripHidden({
|
|
1594
|
+
if (viewer.can("sessions")) createStripHidden({
|
|
1559
1595
|
gripEl: $("#stripGrip"), body: document.body, onApply: () => updateSessionTitle(),
|
|
1560
1596
|
closeFamilyPop: () => lists.familyPop.close(),
|
|
1561
1597
|
});
|
|
@@ -1575,7 +1611,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
1575
1611
|
// before it (none at load) would redraw nothing.
|
|
1576
1612
|
store.on("lists", () => lists.render());
|
|
1577
1613
|
store.on("paintActive", () => lists.paintActive());
|
|
1578
|
-
store.on("settingsPanel", () => settingsControl
|
|
1614
|
+
store.on("settingsPanel", () => settingsControl?.refresh());
|
|
1579
1615
|
// The working set's list offers pin only under forget.
|
|
1580
1616
|
store.on("workingSetStrategy", () => workingSet?.strategyChanged());
|
|
1581
1617
|
store.on("featureGates", () => featureGates.check(store.current));
|
|
@@ -1583,7 +1619,7 @@ store.on("composer", composerMode.update);
|
|
|
1583
1619
|
store.on("composer", () => objectives.redraw());
|
|
1584
1620
|
// The answers' Revise buttons follow liveTurn.running too (none on the
|
|
1585
1621
|
// running turn's answer), which touches the composer.
|
|
1586
|
-
store.on("composer", () => reviseStacks
|
|
1622
|
+
store.on("composer", () => reviseStacks?.decorate());
|
|
1587
1623
|
store.on("infoBar", updateInfoBar);
|
|
1588
1624
|
|
|
1589
1625
|
applyView();
|
|
@@ -1599,7 +1635,7 @@ store.batch(() => {
|
|
|
1599
1635
|
store.touch("composer");
|
|
1600
1636
|
store.touch("infoBar");
|
|
1601
1637
|
});
|
|
1602
|
-
startPage.loadModels();
|
|
1638
|
+
if (viewer.can("models")) startPage.loadModels();
|
|
1603
1639
|
// The list: the hub's stream when the browser has EventSource, else fetched.
|
|
1604
1640
|
if (typeof globalThis.EventSource === "function") openSessionEvents();
|
|
1605
1641
|
else refresh();
|