samagotchi 0.152.0 → 0.154.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 +45 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundle-authoring.md +1 -1
- data/docs/bundles.md +93 -12
- data/docs/cli.md +83 -99
- data/docs/configuration.md +46 -40
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +15 -15
- data/docs/hooks.md +13 -12
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +5 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +42 -12
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +8 -8
- data/docs/internals/prompt-caching.md +8 -4
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +20 -17
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +52 -43
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +25 -25
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +18 -8
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +63 -2642
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +106 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +36 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -42,6 +42,11 @@ const NOT_AN_IMAGE = "the renderer's SVG can't be shown as an image";
|
|
|
42
42
|
// this tall and scrolls sideways instead.
|
|
43
43
|
const MIN_SHOWN_HEIGHT = 120;
|
|
44
44
|
|
|
45
|
+
// The zoom button's words: the picture fitted to the column, or at its natural size.
|
|
46
|
+
const ZOOM_IN = "Show at full size";
|
|
47
|
+
const ZOOM_OUT = "Fit to the column";
|
|
48
|
+
|
|
49
|
+
const ZOOM_ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><circle cx="7" cy="7" r="4.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="m10.2 10.2 3.3 3.3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
|
|
45
50
|
const ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M5.5 4 2 8l3.5 4M10.5 4 14 8l-3.5 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
46
51
|
|
|
47
52
|
// The picture's URL and size. The XML parser reads the root, as the image will: a
|
|
@@ -286,6 +291,9 @@ export function createBlockHost({
|
|
|
286
291
|
return Promise.race([image, bound]).finally(() => clearTimeout(timer));
|
|
287
292
|
}
|
|
288
293
|
|
|
294
|
+
// The picture's name says where its words are: the source <pre> is hidden, so a
|
|
295
|
+
// screen reader reaches it through the toggle. The zoom is a button too, for the
|
|
296
|
+
// keyboard (a click on the picture does the same); it shows on keyboard focus only.
|
|
289
297
|
function makeFigure(picture, themeName, lang) {
|
|
290
298
|
const figure = doc.createElement("figure");
|
|
291
299
|
figure.className = DRAWN;
|
|
@@ -294,17 +302,33 @@ export function createBlockHost({
|
|
|
294
302
|
scroll.className = "block-scroll";
|
|
295
303
|
const img = doc.createElement("img");
|
|
296
304
|
img.className = "block-img";
|
|
297
|
-
img.alt = `${lang} diagram`;
|
|
305
|
+
img.alt = `${lang} diagram, its source under the Show the source button`;
|
|
298
306
|
showPicture(img, picture);
|
|
299
307
|
scroll.append(img);
|
|
300
|
-
const
|
|
301
|
-
toggle
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
figure.append(scroll, toggle);
|
|
308
|
+
const zoom = button("block-zoom", ZOOM_ICON);
|
|
309
|
+
const toggle = button("block-toggle", ICON);
|
|
310
|
+
figure.append(scroll, zoom, toggle);
|
|
311
|
+
setZoomed(figure, false);
|
|
305
312
|
return figure;
|
|
306
313
|
}
|
|
307
314
|
|
|
315
|
+
function button(className, icon) {
|
|
316
|
+
const el = doc.createElement("button");
|
|
317
|
+
el.type = "button";
|
|
318
|
+
el.className = className;
|
|
319
|
+
litRender(icon, el);
|
|
320
|
+
return el;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function setZoomed(figure, on) {
|
|
324
|
+
figure.classList.toggle("natural", on);
|
|
325
|
+
const zoom = figure.querySelector(":scope > .block-zoom");
|
|
326
|
+
if (!zoom) return;
|
|
327
|
+
zoom.setAttribute("aria-pressed", on ? "true" : "false");
|
|
328
|
+
zoom.setAttribute("aria-label", on ? ZOOM_OUT : ZOOM_IN);
|
|
329
|
+
zoom.title = on ? ZOOM_OUT : ZOOM_IN;
|
|
330
|
+
}
|
|
331
|
+
|
|
308
332
|
function showPicture(img, { url, width, height }) {
|
|
309
333
|
if (width && height) {
|
|
310
334
|
img.setAttribute("width", width);
|
|
@@ -476,7 +500,7 @@ export function createBlockHost({
|
|
|
476
500
|
scan(root);
|
|
477
501
|
|
|
478
502
|
root.addEventListener("mousedown", (e) => {
|
|
479
|
-
if (e.target.closest?.(".block-toggle")) e.preventDefault();
|
|
503
|
+
if (e.target.closest?.(".block-toggle, .block-zoom")) e.preventDefault();
|
|
480
504
|
});
|
|
481
505
|
// Delegated, so a Revise clone's work too.
|
|
482
506
|
root.addEventListener("click", (e) => {
|
|
@@ -490,7 +514,10 @@ export function createBlockHost({
|
|
|
490
514
|
}
|
|
491
515
|
const note = e.target.closest?.(".block-note");
|
|
492
516
|
if (note) { copy(note.dataset.text); return; }
|
|
493
|
-
if (e.target.closest?.(".block-img"))
|
|
517
|
+
if (e.target.closest?.(".block-zoom, .block-img")) {
|
|
518
|
+
const figure = e.target.closest(`.${DRAWN}`);
|
|
519
|
+
setZoomed(figure, !figure.classList.contains("natural"));
|
|
520
|
+
}
|
|
494
521
|
});
|
|
495
522
|
}
|
|
496
523
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. */
|
|
1
|
+
/* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. The zoom button, the keyboard's way to the natural size, shows on keyboard focus only, left of the toggle. */
|
|
2
2
|
.block-drawn{position:relative;margin:0 0 12px;white-space:normal}
|
|
3
3
|
.code-wrap>.block-drawn{margin:0}
|
|
4
4
|
.code-wrap>.block-drawn.source{height:0}
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
.block-img{display:block;max-width:100%;max-height:50000px;object-fit:contain;height:auto;margin:0 auto;cursor:zoom-in}
|
|
9
9
|
.block-drawn.natural .block-img{max-width:50000px;margin:0;cursor:zoom-out}
|
|
10
10
|
.block-drawn.source .block-scroll{display:none}
|
|
11
|
-
.block-toggle{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
|
|
11
|
+
.block-toggle,.block-zoom{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
|
|
12
12
|
.code-wrap>.block-drawn>.block-toggle{right:34px}
|
|
13
|
-
.block-
|
|
14
|
-
.
|
|
15
|
-
.block-
|
|
13
|
+
.block-zoom{right:34px}
|
|
14
|
+
.code-wrap>.block-drawn>.block-zoom{right:63px}
|
|
15
|
+
.block-drawn.source>.block-zoom{display:none}
|
|
16
|
+
.block-zoom:not(:focus-visible){pointer-events:none}
|
|
17
|
+
.block-toggle svg,.block-zoom svg{display:block}
|
|
18
|
+
.block-drawn:hover>.block-toggle,.code-wrap:hover>.block-drawn>.block-toggle,.block-toggle:focus-visible,.block-zoom:focus-visible,.block-drawn.source>.block-toggle{opacity:1}
|
|
19
|
+
.block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){filter:none}@media(hover:hover){.block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){color:var(--text-bright)}}
|
|
16
20
|
.block-drawn.source:not(:last-child){margin:0}
|
|
17
21
|
/* Its words are the attribute's, so a bubble's text stays the answer's (block_host.js fail). */
|
|
18
22
|
.block-note{margin:4px 0 12px;font-size:12px;color:var(--text-3);white-space:normal}
|
|
@@ -305,8 +305,8 @@ export function continueRefusal(error, sessions = []) {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
// The open-children refusal as the toast shows it: the open delegates the
|
|
308
|
-
// continue can't move to the next link (an older chi's worker, a
|
|
309
|
-
//
|
|
308
|
+
// continue can't move to the next link (an older chi's worker, a
|
|
309
|
+
// one-shot run), each a link to its session with what to do about it.
|
|
310
310
|
// @param hrefFor (id) → its page's href
|
|
311
311
|
export function openChildrenTemplate(prevId, children, hrefFor) {
|
|
312
312
|
const n = children.length;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// URL-safe and a space is one "+".
|
|
13
13
|
//
|
|
14
14
|
// Normalized: whitespace collapsed, and what the participant page shows
|
|
15
|
-
// instead (
|
|
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",
|
|
18
18
|
// "<cwd>" → "."), the home folder "~". Not the owner's token (Redact's
|
|
@@ -41,10 +41,10 @@ export const HIT_CLASS = "claim-hit";
|
|
|
41
41
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
42
42
|
|
|
43
43
|
export const MARK = "[redacted]";
|
|
44
|
-
//
|
|
45
|
-
// (lib/samagotchi/
|
|
44
|
+
// Samagotchi::Redact's PATTERNS and VALUES
|
|
45
|
+
// (lib/samagotchi/redact.rb), as JS: keep them in step.
|
|
46
46
|
// spec/fixtures/redact_cases.json holds cases both sides read the same
|
|
47
|
-
// (
|
|
47
|
+
// (redact_spec.rb, claim_link.test.js).
|
|
48
48
|
const SECRET_PATTERNS = [
|
|
49
49
|
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g,
|
|
50
50
|
/\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The composer's mode on the owner page (used by app.js, the store's
|
|
2
|
+
// `composer` redraw): its placeholder, key hint, action button, the attach
|
|
3
|
+
// button, the start page's empty state and the delivery line's row, from the
|
|
4
|
+
// open session, its owner and whether its turn runs.
|
|
5
|
+
|
|
6
|
+
// A phone's composer fits a few words and has no Shift+Enter: the short
|
|
7
|
+
// placeholder there.
|
|
8
|
+
export const NARROW_QUERY = "(max-width: 600px)";
|
|
9
|
+
|
|
10
|
+
// The key hint under the composer (shown while it has focus; on the start
|
|
11
|
+
// page only: in a session the placeholder carries it, the hint row is gone).
|
|
12
|
+
export const SEND_HINT = "Enter to send · Shift+Enter for a newline · ↑ history";
|
|
13
|
+
|
|
14
|
+
// What the composer shows, pure: +selected+ the open session's id (null: the
|
|
15
|
+
// start page), +owner+ its owner, +running+ whether its turn runs, +narrow+ a
|
|
16
|
+
// phone's width. While a turn runs, Send still sends: the prompt merges into
|
|
17
|
+
// the turn at its next step (steering); Cancel is its own button then.
|
|
18
|
+
// @return {placeholder, hint, button, disabled, empty, clearHistory,
|
|
19
|
+
// attachHidden, delivery}: +disabled+ undefined while a turn runs (the
|
|
20
|
+
// button keeps whatever a send in flight set); +empty+ the start page's
|
|
21
|
+
// empty state shown, +clearHistory+ #history emptied (the start page only);
|
|
22
|
+
// +delivery+ the delivery line's mode (delivery_line.js show).
|
|
23
|
+
export function composerMode({ selected, owner, running, narrow }) {
|
|
24
|
+
const session = !!selected;
|
|
25
|
+
const tui = session && owner === "tui";
|
|
26
|
+
const live = !!running && session;
|
|
27
|
+
const pick = (long, short) => (narrow ? short : long);
|
|
28
|
+
// No images for a session a terminal owns (it takes no message from here);
|
|
29
|
+
// on a phone none while a turn runs either: the row keeps its room for the
|
|
30
|
+
// message box (steering).
|
|
31
|
+
const attachHidden = tui || (live && !!narrow);
|
|
32
|
+
// The delivery line's row in a session: the key hint while idle, where a
|
|
33
|
+
// message goes in while the turn runs. A terminal owns the session: the
|
|
34
|
+
// composer is off, so is its row.
|
|
35
|
+
const delivery = !session || tui ? "off" : running ? "running" : "idle";
|
|
36
|
+
const base = { attachHidden, delivery, empty: false, clearHistory: false };
|
|
37
|
+
if (live) {
|
|
38
|
+
return {
|
|
39
|
+
...base,
|
|
40
|
+
placeholder: pick("Running… a message goes in at the next step · Enter to send · Alt+Enter queues", "Steer the turn…"),
|
|
41
|
+
hint: SEND_HINT, button: "Send", disabled: undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!session) {
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
placeholder: "Ask chi anything…", hint: "Enter to start · Shift+Enter for a newline", button: "Start", disabled: false,
|
|
48
|
+
empty: true, clearHistory: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (tui) {
|
|
52
|
+
return {
|
|
53
|
+
...base,
|
|
54
|
+
placeholder: pick("A terminal chi owns this session · its one-turn run (chi --non-interactive) lets go as it ends", "A terminal chi owns this session"),
|
|
55
|
+
hint: "", button: "Send", disabled: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// The keys are in the delivery line's row above.
|
|
59
|
+
return { ...base, placeholder: "Message chi…", hint: SEND_HINT, button: "Send", disabled: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The composer's mode drawn: update() is the store's `composer` redraw, and a
|
|
63
|
+
// window crossing the phone's width redraws it too (an input the store
|
|
64
|
+
// doesn't hold, with the composer as its only reader: the bar doesn't read it).
|
|
65
|
+
//
|
|
66
|
+
// +els+: {body, prompt, hint, button, attach, empty, history}. +store+: the
|
|
67
|
+
// page's session store (selected, current.owner; read only). +liveTurn+:
|
|
68
|
+
// live_turn.js's (running). +startPage+: start_page.js's (restoreStartPick,
|
|
69
|
+
// syncModelPick, dropChoices). +deliveryControl+: delivery_line.js's.
|
|
70
|
+
// +moreSheet+: more_sheet.js's (⋯ is a phone's alone). +fitPrompt()+,
|
|
71
|
+
// +placeToast()+: the page's. +win+: the window (matchMedia).
|
|
72
|
+
export function createComposerMode({
|
|
73
|
+
els, store, liveTurn, startPage, deliveryControl, moreSheet, fitPrompt, placeToast, win = globalThis,
|
|
74
|
+
}) {
|
|
75
|
+
const narrowScreen = win.matchMedia(NARROW_QUERY);
|
|
76
|
+
let hadSession = false;
|
|
77
|
+
|
|
78
|
+
function update() {
|
|
79
|
+
// In a session the box is one line: its minimum follows the class.
|
|
80
|
+
if (els.body.classList.toggle("has-session", !!store.selected) !== hadSession) {
|
|
81
|
+
hadSession = !hadSession;
|
|
82
|
+
// Back on the start page: its remembered model, not the last link's.
|
|
83
|
+
if (!hadSession) startPage.restoreStartPick();
|
|
84
|
+
fitPrompt();
|
|
85
|
+
placeToast();
|
|
86
|
+
}
|
|
87
|
+
startPage.syncModelPick();
|
|
88
|
+
// The start page's choice was for a new session: opening a session drops it.
|
|
89
|
+
if (store.selected) startPage.dropChoices();
|
|
90
|
+
const mode = composerMode({
|
|
91
|
+
selected: store.selected, owner: store.current.owner, running: liveTurn.running, narrow: narrowScreen.matches,
|
|
92
|
+
});
|
|
93
|
+
els.attach.hidden = mode.attachHidden;
|
|
94
|
+
deliveryControl.show(mode.delivery);
|
|
95
|
+
els.prompt.placeholder = mode.placeholder;
|
|
96
|
+
els.hint.textContent = mode.hint;
|
|
97
|
+
els.button.textContent = mode.button;
|
|
98
|
+
if (mode.disabled !== undefined) els.button.disabled = mode.disabled;
|
|
99
|
+
els.empty.classList.toggle("hidden", !mode.empty);
|
|
100
|
+
if (mode.clearHistory) els.history.replaceChildren();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
narrowScreen.addEventListener("change", () => {
|
|
104
|
+
update();
|
|
105
|
+
// ⋯ is a phone's alone: a window grown past it takes its sheet along.
|
|
106
|
+
if (!narrowScreen.matches && moreSheet.isOpen()) moreSheet.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return { update };
|
|
110
|
+
}
|
|
@@ -97,13 +97,13 @@ export function deleteConfirmText(session) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// The badge on a session a process holds right now (the list's `owner`):
|
|
100
|
-
// a worker (
|
|
100
|
+
// a worker (every UI can send), or a terminal chi's one-turn run (--non-interactive).
|
|
101
101
|
export function ownerBadge(owner, id) {
|
|
102
102
|
if (owner === "worker") {
|
|
103
103
|
return { kind: "worker", text: "live", title: `A worker runs this session; a terminal joins with: chi --attach ${id}` };
|
|
104
104
|
}
|
|
105
105
|
if (owner === "tui") {
|
|
106
|
-
return { kind: "tui", text: "terminal", title: "A terminal chi
|
|
106
|
+
return { kind: "tui", text: "terminal", title: "A terminal chi runs one turn in this session (chi --non-interactive) and lets go as it ends" };
|
|
107
107
|
}
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
@@ -171,7 +171,7 @@ export function childrenSummary(parent, sessions) {
|
|
|
171
171
|
|
|
172
172
|
// Whether the info bar offers "stop": only while a worker runs the session
|
|
173
173
|
// (the list says so, or this page streams from one a send just woke). A
|
|
174
|
-
// stopped or idle-exited session has nothing to stop, and a
|
|
174
|
+
// stopped or idle-exited session has nothing to stop, and a one-turn run's is
|
|
175
175
|
// refused by the server.
|
|
176
176
|
export function canStopSession({ owner, streaming }) {
|
|
177
177
|
if (owner === "tui") return false;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ownerClaimHash, participantClaimLink, selectionClaim, showClaim, turnIdOf } from "./claim_link.js";
|
|
2
|
+
import { sessionHash } from "./route.js";
|
|
3
|
+
|
|
4
|
+
// Where a claim's text can be on the owner's page: answers, own messages, steps.
|
|
5
|
+
export const CLAIM_ROOTS = ".bubble.output, .bubble.user .user-message, .gen-text";
|
|
6
|
+
|
|
7
|
+
// The owner page's claim links (used by app.js; claim_link.js does the text
|
|
8
|
+
// and DOM work): a claim link opened on this page (#/s/<id>?t=…&q=…, or the
|
|
9
|
+
// participant's #s=<id>&t=… opened in this browser) shown once its session is
|
|
10
|
+
// drawn, and annotate's "Copy link" for a selection. The participant page has
|
|
11
|
+
// its own copies on purpose (docs/internals/web-frontend.md, Two pages).
|
|
12
|
+
//
|
|
13
|
+
// +historyEl+: #history. +store+: the page's session store (selected,
|
|
14
|
+
// current.dir, sessions; read only). +claimTarget()+: participants_panel.js's
|
|
15
|
+
// (the people a link would open for, or null). +toast(text, action,
|
|
16
|
+
// options)+, +copyText(text)+ (copy.js), +allViewOpen()+: the page's.
|
|
17
|
+
// +home+: the home folder the page shows as ~ (body data-home). +win+: the
|
|
18
|
+
// window (location, history).
|
|
19
|
+
export function createOwnerClaim({
|
|
20
|
+
historyEl, store, claimTarget, toast, copyText, allViewOpen, home = "", win = globalThis,
|
|
21
|
+
}) {
|
|
22
|
+
// A claim link waiting for its session to be drawn (selectFromHash sets it).
|
|
23
|
+
let pendingClaim = null;
|
|
24
|
+
|
|
25
|
+
// How the participant page shows this page's text (claim_link.js).
|
|
26
|
+
function opts() {
|
|
27
|
+
return { home, cwd: store.current.dir || "" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pending(claim) {
|
|
31
|
+
pendingClaim = claim;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The claim link's quote scrolled to and marked, its text read as the
|
|
35
|
+
// participant page shows it (the session's folder relative, home ~); a
|
|
36
|
+
// quote not found marks its turn; neither found: the top, and a toast.
|
|
37
|
+
// The hash is the session's again (select() already set it when it
|
|
38
|
+
// switched), so the same link opened again is a hashchange.
|
|
39
|
+
function showPending() {
|
|
40
|
+
const claim = pendingClaim;
|
|
41
|
+
if (!claim || claim.sessionId !== store.selected) return;
|
|
42
|
+
pendingClaim = null;
|
|
43
|
+
const { location, history } = win;
|
|
44
|
+
if (!allViewOpen() && location.hash !== sessionHash(store.selected)) {
|
|
45
|
+
history.replaceState(null, "", location.pathname + location.search + sessionHash(store.selected));
|
|
46
|
+
}
|
|
47
|
+
if (showClaim(historyEl, claim, { roots: CLAIM_ROOTS, opts: opts() })) return;
|
|
48
|
+
historyEl.scrollTop = 0;
|
|
49
|
+
toast("The linked turn isn't in this session.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Annotate's "Copy link" for a selection in a turn's answer, step or own
|
|
53
|
+
// message: the participant page's link when the session has a person
|
|
54
|
+
// active (it opens in the browser their review link was opened in: one
|
|
55
|
+
// link per browser, the cookie), else this page's own.
|
|
56
|
+
function linkFor(source, range) {
|
|
57
|
+
// Not in a Revise stack's switched copy (revise_switch.js: view only, no claim key; Verify isn't offered there either).
|
|
58
|
+
if (!store.selected || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)
|
|
59
|
+
|| source.root.closest("[data-stack-view]")) return null;
|
|
60
|
+
const turnId = turnIdOf(historyEl, source.root);
|
|
61
|
+
if (!turnId) return null;
|
|
62
|
+
const claim = { turnId, ...selectionClaim(source.root, range, opts()) };
|
|
63
|
+
if (!claim.q) return null;
|
|
64
|
+
const target = claimTarget();
|
|
65
|
+
if (!target) {
|
|
66
|
+
const { location } = win;
|
|
67
|
+
return { url: `${location.origin}${location.pathname}${location.search}${ownerClaimHash(store.selected, claim)}`,
|
|
68
|
+
title: "A link to this on your page (no person is invited to this session)" };
|
|
69
|
+
}
|
|
70
|
+
const whose = target.names.map((name) => `${name}'s`).join(" or ");
|
|
71
|
+
return { url: participantClaimLink(target.base, store.selected, claim),
|
|
72
|
+
title: `A link to this for ${whose} browser: it opens where their review link was opened (one link per browser). Opened here, it shows your page.` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function copyLink({ url }) {
|
|
76
|
+
if (await copyText(url)) toast("Link copied");
|
|
77
|
+
else toast(`Couldn't copy: ${url}`, null, { sticky: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { opts, pending, showPending, linkFor, copyLink };
|
|
81
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { selectionClaim } from "./claim_link.js";
|
|
2
|
+
import { CLAIM_ROOTS } from "./owner_claim.js";
|
|
3
|
+
import { authorBadge } from "./participant_names.js";
|
|
4
|
+
import { sessionHash } from "./route.js";
|
|
5
|
+
import { CHECK_NOT_ON_PAGE, ownerCheckHash } from "./verdict_card.js";
|
|
6
|
+
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
7
|
+
import { VERIFY_UNAVAILABLE, verifyWorkerUnavailable } from "./verify_preset.js";
|
|
8
|
+
import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
9
|
+
|
|
10
|
+
// The owner page's Verify (used by app.js): its verdict cards in the history
|
|
11
|
+
// (a live verdict, a session's saved ones), Verify fresh on an unclear
|
|
12
|
+
// verdict's card (verdict_fresh.js), Verify's claim from a selection, and
|
|
13
|
+
// the composer's armed claim (anchor/set/clear: the one holder; the next
|
|
14
|
+
// send makes it a Verify turn).
|
|
15
|
+
// The participant page has its own copies on purpose
|
|
16
|
+
// (docs/internals/web-frontend.md, Two pages).
|
|
17
|
+
//
|
|
18
|
+
// +historyEl+: #history. +store+: the page's session store (it writes
|
|
19
|
+
// current.verdicts and current.verifyFresh through store.update; reads
|
|
20
|
+
// selected, sessions, workerFeatures, bridgeUp). +api(path, opts)+: data.js's.
|
|
21
|
+
// +claim+: owner_claim.js's (its opts()). +liveTurn+: live_turn.js's (whether
|
|
22
|
+
// a turn runs, its id). +toast(text)+: the page's. +win+: the window (a
|
|
23
|
+
// verdict's check in another session opens by its hash).
|
|
24
|
+
export function createOwnerVerify({ historyEl, store, api, claim, liveTurn, toast, win = globalThis }) {
|
|
25
|
+
// Verify fresh's failed starts, by check (verdict_fresh.js keeps them here).
|
|
26
|
+
const freshErrors = {};
|
|
27
|
+
// The composer's armed Verify claim ({turn_id, quote}) or null: set by the
|
|
28
|
+
// Verify preset, a restored Verify turn's draft; let go by an emptied
|
|
29
|
+
// composer, another session, the send's ack.
|
|
30
|
+
let armed = null;
|
|
31
|
+
|
|
32
|
+
function anchor() {
|
|
33
|
+
return armed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function set(next) {
|
|
37
|
+
armed = next;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// clear() lets go of whatever is armed; clear(a) only of +a+ (a send's ack
|
|
41
|
+
// for the claim it took, not one armed since).
|
|
42
|
+
function clear(...only) {
|
|
43
|
+
if (only.length && only[0] !== armed) return;
|
|
44
|
+
armed = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A verdict ({check_turn_id, verdict, fresh?, by?…}) onto its check's
|
|
48
|
+
// card: into the session's list once, and a fresh one ends its check's
|
|
49
|
+
// "verifying fresh".
|
|
50
|
+
function placeVerdict(record) {
|
|
51
|
+
if (!record) return;
|
|
52
|
+
const known = (store.current.verdicts || []).some((item) => item.check_turn_id === record.check_turn_id);
|
|
53
|
+
const patch = {};
|
|
54
|
+
if (!known) patch.verdicts = [...(store.current.verdicts || []), record];
|
|
55
|
+
// A fresh verdict ends its check's "verifying fresh" (the card shows it).
|
|
56
|
+
if (record.fresh) patch.verifyFresh = settleRunning(store.current.verifyFresh, record);
|
|
57
|
+
store.update({ current: patch });
|
|
58
|
+
const badge = record.by && authorBadge(record.by);
|
|
59
|
+
placeVerdictWithFresh(historyEl, record, {
|
|
60
|
+
who: badge ? badge.name : "You",
|
|
61
|
+
author: record.by || null,
|
|
62
|
+
roots: CLAIM_ROOTS,
|
|
63
|
+
opts: claim.opts(),
|
|
64
|
+
onMissingCheck: (missing) => {
|
|
65
|
+
const hash = ownerCheckHash(store.sessions, store.selected, missing.check_turn_id);
|
|
66
|
+
if (hash) win.location.hash = hash;
|
|
67
|
+
else toast(CHECK_NOT_ON_PAGE);
|
|
68
|
+
},
|
|
69
|
+
}, freshState());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The owner's Verify fresh on an unclear verdict's card (verdict_fresh.js):
|
|
73
|
+
// always theirs to start; its running check links to the child's session.
|
|
74
|
+
function freshState() {
|
|
75
|
+
const id = store.selected;
|
|
76
|
+
const unavailable = verifyWorkerUnavailable(store.current.workerFeatures, store.current.bridgeUp);
|
|
77
|
+
return {
|
|
78
|
+
verdicts: store.current.verdicts || [],
|
|
79
|
+
running: store.current.verifyFresh || [],
|
|
80
|
+
errors: freshErrors,
|
|
81
|
+
canStart: !!id && !unavailable,
|
|
82
|
+
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
83
|
+
start: async (of) => {
|
|
84
|
+
try {
|
|
85
|
+
const body = await api(freshURL(id, of), { method: "POST", body: "{}" });
|
|
86
|
+
return { ok: true, child_id: body.child_id || null };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return { ok: false, error: error.message };
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
childHref: (childId) => sessionHash(childId),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The session's saved verdicts, onto the history just drawn.
|
|
96
|
+
function renderSaved() {
|
|
97
|
+
for (const record of store.current.verdicts || []) placeVerdict(record);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Verify's claim: a selection in an ended turn's final answer (the only
|
|
101
|
+
// text Engine#verified_anchor takes; verify_target.js), else null and no
|
|
102
|
+
// Verify on the bar.
|
|
103
|
+
function anchorFor({ source, range }) {
|
|
104
|
+
if (source?.kind !== "answer" || !range) return null;
|
|
105
|
+
const running = liveTurn.running ? { turnId: liveTurn.timing.turnId || null } : null;
|
|
106
|
+
const turnId = verifyTurnOf(historyEl, source.root, OWNER_FINAL, { running });
|
|
107
|
+
const quote = turnId ? selectionClaim(source.root, range, claim.opts()).q : "";
|
|
108
|
+
return turnId && quote ? { turn_id: turnId, quote } : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { placeVerdict, freshState, renderSaved, anchorFor, anchor, set, clear };
|
|
112
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - live_timing.js sets `timing().activeTurn` in place (start, finish,
|
|
11
11
|
// finishLine): the running turn's own clock, which mergeTurnTiming keeps.
|
|
12
12
|
// - verdict_fresh.js `startFresh` pushes onto `fresh.running`, which is
|
|
13
|
-
// `verifyFresh` (
|
|
13
|
+
// `verifyFresh` (owner_verify.js freshState hands the array out): returning the
|
|
14
14
|
// entry for an update would leave the card holding the old array.
|
|
15
15
|
//
|
|
16
16
|
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
@@ -46,12 +46,12 @@ export const FIELDS = {
|
|
|
46
46
|
verdicts: {
|
|
47
47
|
source: (d) => list(d.session?.verdicts),
|
|
48
48
|
redraws: [],
|
|
49
|
-
writers: "placeVerdict (its card draws itself: placeVerdictWithFresh)",
|
|
49
|
+
writers: "owner_verify.js placeVerdict (its card draws itself: placeVerdictWithFresh)",
|
|
50
50
|
},
|
|
51
51
|
verifyFresh: {
|
|
52
52
|
source: (d) => list(d.session?.verify_fresh),
|
|
53
53
|
redraws: [],
|
|
54
|
-
writers: "placeVerdict (settleRunning); verdict_fresh.js startFresh pushes in place (D8)",
|
|
54
|
+
writers: "owner_verify.js placeVerdict (settleRunning); verdict_fresh.js startFresh pushes in place (D8)",
|
|
55
55
|
},
|
|
56
56
|
createdAt: { redraws: ["infoBar"], writers: "the payload" },
|
|
57
57
|
ctxPct: { redraws: ["infoBar"], writers: "stream_handlers.js (generation_chunk, context_status)" },
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { GRIP_OPEN_PX } from "./composer_size.js";
|
|
2
2
|
import { stripGripAction, stripGripTitle } from "./strip.js";
|
|
3
3
|
|
|
4
|
-
// The latest-sessions strip's grip (used by
|
|
4
|
+
// The latest-sessions strip's grip (used by strip_hidden.js): the handle on the edge
|
|
5
5
|
// between the strip and the chat, the composer's grip's twin, mirrored. A
|
|
6
6
|
// drag up (or a tap) hides the strip, a drag down (or a tap) shows it, and
|
|
7
7
|
// Enter or Space toggles too. The strip snaps: unlike the composer's box it
|
|
8
8
|
// does not follow the pointer, so nothing is measured while dragging — only
|
|
9
9
|
// whether the release would act (.will-toggle, the pill in the accent).
|
|
10
10
|
// The grip owns its title and aria-expanded (composer_grip.js's
|
|
11
|
-
// drawFoldControls does the same for the composer's):
|
|
12
|
-
// returned refresh() from
|
|
11
|
+
// drawFoldControls does the same for the composer's): strip_hidden.js calls
|
|
12
|
+
// the returned refresh() from its apply, and nothing else writes them.
|
|
13
13
|
// No click listener: a click still fires after pointerdown's preventDefault
|
|
14
14
|
// and would toggle twice. No document listener either: the composer's grip
|
|
15
15
|
// needs one for typing on the page, this one acts only on itself.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readStored, writeStored } from "./storage.js";
|
|
2
|
+
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
3
|
+
import { createStripGrip } from "./strip_grip.js";
|
|
4
|
+
|
|
5
|
+
// The saved choice (storage.js): the strip hidden; chi_zen is zen mode's old
|
|
6
|
+
// name, read when the new key was never written.
|
|
7
|
+
export const STRIP_HIDDEN_KEY = "chi_strip_hidden";
|
|
8
|
+
const ZEN_KEY = "chi_zen";
|
|
9
|
+
|
|
10
|
+
// The latest-sessions strip hidden or shown (used by app.js): the body's
|
|
11
|
+
// strip-hidden class, the grip (strip_grip.js) that toggles it, and the short
|
|
12
|
+
// window's rule. The strip can be hidden for more chat room; the choice stays
|
|
13
|
+
// per browser (it replaced zen mode, so a zen user starts hidden). A short
|
|
14
|
+
// window hides the strip by itself, so the stage keeps room for a card that
|
|
15
|
+
// waits (its question and choices); the grip brings it back until the window
|
|
16
|
+
// is tall again. Not saved: the saved choice is the user's own hide or show.
|
|
17
|
+
//
|
|
18
|
+
// +gripEl+: #stripGrip. +body+: the page's body (the class). +onApply()+:
|
|
19
|
+
// after each change (the top bar's title follows: app.js updateSessionTitle).
|
|
20
|
+
// +closeFamilyPop()+: a hide takes the strip's family popover along. +win+:
|
|
21
|
+
// the window (matchMedia). +storage+: storage.js's guarded read and write.
|
|
22
|
+
// Applied once at creation.
|
|
23
|
+
export function createStripHidden({
|
|
24
|
+
gripEl, body = globalThis.document.body, onApply, closeFamilyPop, win = globalThis,
|
|
25
|
+
storage = { read: readStored, write: writeStored },
|
|
26
|
+
}) {
|
|
27
|
+
const shortWindow = win.matchMedia(SHORT_WINDOW_QUERY);
|
|
28
|
+
let shownWhileShort = false;
|
|
29
|
+
|
|
30
|
+
function saved() {
|
|
31
|
+
const value = storage.read(STRIP_HIDDEN_KEY);
|
|
32
|
+
return value === null ? storage.read(ZEN_KEY) === "1" : value === "1";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The grip is given the hidden state and the toggle; it speaks its own
|
|
36
|
+
// title and aria-expanded, refreshed after every change.
|
|
37
|
+
const gripRefresh = createStripGrip({
|
|
38
|
+
gripEl,
|
|
39
|
+
isHidden: () => body.classList.contains("strip-hidden"),
|
|
40
|
+
setHidden: (hidden) => setHidden(hidden),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function apply() {
|
|
44
|
+
body.classList.toggle("strip-hidden", stripAutoHidden({
|
|
45
|
+
saved: saved(), short: shortWindow.matches, shownWhileShort,
|
|
46
|
+
}));
|
|
47
|
+
gripRefresh();
|
|
48
|
+
onApply();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setHidden(hidden) {
|
|
52
|
+
if (hidden) closeFamilyPop();
|
|
53
|
+
storage.write(STRIP_HIDDEN_KEY, hidden ? "1" : "0");
|
|
54
|
+
shownWhileShort = !hidden && shortWindow.matches;
|
|
55
|
+
apply();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
apply();
|
|
59
|
+
shortWindow.addEventListener("change", () => {
|
|
60
|
+
if (!shortWindow.matches) shownWhileShort = false;
|
|
61
|
+
apply();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { setHidden };
|
|
65
|
+
}
|