samagotchi 0.66.0 → 0.68.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 +80 -1
- data/docs/architecture.md +55 -30
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +129 -38
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -1
- data/docs/testing.md +18 -4
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +307 -549
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +21 -11
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +71 -93
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +1 -1
- data/lib/samagotchi/web/public/app.js +103 -539
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/commands.js +2 -2
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +15 -7
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +2 -8
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/session_state.js +14 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +35 -2
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +22 -24
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +27 -1
|
@@ -32,47 +32,49 @@ import {
|
|
|
32
32
|
} from "./thinking_chip.js";
|
|
33
33
|
import { createReasoningHistoryControl } from "./reasoning_history_chip.js";
|
|
34
34
|
import { renderInfoBar } from "./info_bar.js";
|
|
35
|
+
import { sessionBar, workerBadgeFor } from "./info_bar_model.js";
|
|
35
36
|
import { createShownTexts } from "./shown_texts.js";
|
|
36
37
|
import { MODEL_KEY } from "./model_pick.js";
|
|
37
38
|
import { emptySessionState, sessionStateFrom } from "./session_state.js";
|
|
38
39
|
import { createModelPicker } from "./model_picker.js";
|
|
39
|
-
import { chipName, chipsTemplate, imageFiles, restoredChips, thumbsTemplate, turnRefs, turnText } from "./images.js";
|
|
40
|
-
import { normalize, firstPreview, failedTurnText,
|
|
41
|
-
import { createTurnView,
|
|
42
|
-
import {
|
|
40
|
+
import { chipName, chipsTemplate, imageFiles, restoredChips, thumbsTemplate, turnRefs, turnText, uploadChips } from "./images.js";
|
|
41
|
+
import { normalize, firstPreview, failedTurnText, deleteConfirmText, goneSessionNotice, withLiveStatus, withoutWorker } from "./format.js";
|
|
42
|
+
import { createTurnView, turnHistoryNodes } from "./turn_view.js";
|
|
43
|
+
import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
44
|
+
import { createBubbles, hintEl, outputBubbleTemplate, setBubbleDelivery } from "./bubbles.js";
|
|
45
|
+
import { createCardsPlace } from "./cards_place.js";
|
|
43
46
|
import { createStageView, FOLD_MS } from "./stage_view.js";
|
|
44
|
-
import { CONTINUE_OPENER, isOpener,
|
|
47
|
+
import { CONTINUE_OPENER, isOpener, continueRefusal, continuedText, openChildrenTemplate } from "./chains.js";
|
|
45
48
|
import { createTaskStopper } from "./task_stop.js";
|
|
46
49
|
import { appendQuote } from "./annotations.js";
|
|
47
50
|
import { parsePresets } from "./annotate_presets.js";
|
|
48
51
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
49
52
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
50
53
|
import { copyText, installCopy } from "./copy.js";
|
|
51
|
-
import { ctxBarText, ctxBarTip, lastSpeedText, speedText } from "./ctx.js";
|
|
52
54
|
import { createQuestionCards } from "./question_cards.js";
|
|
53
|
-
import {
|
|
55
|
+
import { splitSnapshotCards } from "./card.js";
|
|
54
56
|
import { PROMPT_LINE_PX, PROMPT_MIN_PX, gripFloor, promptHeight } from "./composer_size.js";
|
|
55
57
|
import { createCommandList } from "./command_list.js";
|
|
56
58
|
import { createPromptHistory } from "./prompt_history.js";
|
|
57
59
|
import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
58
60
|
import { scopeDir, scopeTemplate } from "./scope.js";
|
|
59
61
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
60
|
-
import { easeScrollTo, FOLLOW_MS, shouldFollowScroll
|
|
62
|
+
import { easeScrollTo, FOLLOW_MS, shouldFollowScroll } from "./scroll.js";
|
|
61
63
|
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
62
|
-
import { applySessionEvent,
|
|
64
|
+
import { applySessionEvent, inTree, sortedByUpdated } from "./sessions_list.js";
|
|
63
65
|
import { createNotifications } from "./notifications.js";
|
|
64
66
|
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
65
|
-
import { restartConfirmText, versionNotice
|
|
67
|
+
import { restartConfirmText, versionNotice } from "./update_notice.js";
|
|
66
68
|
import {
|
|
67
|
-
appendAboveLiveTiming,
|
|
69
|
+
appendAboveLiveTiming, mergeTiming, normalizeTiming,
|
|
68
70
|
} from "./timing.js";
|
|
69
71
|
import { createLiveTurn } from "./live_turn.js";
|
|
70
72
|
import { applyFinalAnswer, pairAnswerRefs } from "./answer_refs.js";
|
|
71
73
|
import { installRefClicks } from "./refs.js";
|
|
72
74
|
import { linkRefs, touchedFiles } from "./ref_scan.js";
|
|
73
|
-
import { DONE_MS, applyInitEvent, dropDone,
|
|
74
|
-
import {
|
|
75
|
-
import { applyConfirm,
|
|
75
|
+
import { DONE_MS, applyInitEvent, dropDone, fadeDone, initRowTemplate, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
76
|
+
import { newClientId, snapshotEvents, workerGoneText } from "./turn_events.js";
|
|
77
|
+
import { applyConfirm, keptCommandBubbles, restoreCommandBubbles } from "./commands.js";
|
|
76
78
|
import { createComposerCommands, routeComposerLine, routeSessionLine } from "./composer.js";
|
|
77
79
|
import { composerAfterAck, dropEarlyRestores, restoreOnAck } from "./restore.js";
|
|
78
80
|
import { createStreamHandlers } from "./stream_handlers.js";
|
|
@@ -88,7 +90,6 @@ const allListEl = $("#allList");
|
|
|
88
90
|
const historyEl = $("#history");
|
|
89
91
|
const emptyEl = $("#empty");
|
|
90
92
|
const composerEl = $("#composer");
|
|
91
|
-
const infoBarEl = $("#infoBar");
|
|
92
93
|
// The open session's title in the top bar, when no strip card in sight
|
|
93
94
|
// shows it; the tab's base ("Chi · <project>"), set by renderScope.
|
|
94
95
|
const sessionTitleEl = $("#sessionTitle");
|
|
@@ -291,13 +292,38 @@ const questionCards = createQuestionCards({
|
|
|
291
292
|
removeHintIfEmpty: () => removeHintIfEmpty(),
|
|
292
293
|
askingText: () => turnDom.askingText(),
|
|
293
294
|
});
|
|
294
|
-
// The
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
295
|
+
// The history's bubbles (bubbles.js): output, end lines, every client's
|
|
296
|
+
// prompts, the recap, warnings and notices, command and note bubbles.
|
|
297
|
+
const bubbles = createBubbles({
|
|
298
|
+
historyEl,
|
|
299
|
+
appendToHistory: (el) => appendToHistory(el),
|
|
300
|
+
isNearBottom: () => isNearBottom(),
|
|
301
|
+
turnDom,
|
|
302
|
+
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
303
|
+
shownTexts,
|
|
304
|
+
liveTurn,
|
|
305
|
+
selected: () => selected,
|
|
306
|
+
clientId,
|
|
307
|
+
});
|
|
308
|
+
const {
|
|
309
|
+
removeHintIfEmpty, appendChunk, addEndBubble, makeUserBubble, bubbleById, applyPromptOps, showRecap, markRecapStale,
|
|
310
|
+
showGuardrailWarning, showSteers, showHookNotice, addCommandBubble, commandBubble, addNoteBubble,
|
|
311
|
+
} = bubbles;
|
|
312
|
+
// The plugin cards and a snapshot's notices in the history (cards_place.js).
|
|
313
|
+
const cards = createCardsPlace({
|
|
314
|
+
historyEl,
|
|
315
|
+
appendToHistory: (el) => appendToHistory(el),
|
|
316
|
+
isNearBottom: () => isNearBottom(),
|
|
317
|
+
turnDom,
|
|
318
|
+
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
319
|
+
selected: () => selected,
|
|
320
|
+
clientId,
|
|
321
|
+
questionCards,
|
|
322
|
+
bubbles,
|
|
323
|
+
followWokenWorker: () => followWokenWorker(),
|
|
324
|
+
});
|
|
325
|
+
const { upsertCard, placeSnapshotCards, refreshCardBodies } = cards;
|
|
326
|
+
turnRoots().forEach((root) => root.addEventListener("click", cards.onCardAction));
|
|
301
327
|
// The enqueued_ids this page sent (from the /turn acks): only these come back
|
|
302
328
|
// into the composer when their turn fails.
|
|
303
329
|
const sentIds = new Set();
|
|
@@ -383,8 +409,9 @@ function openSessionEvents() {
|
|
|
383
409
|
// never forcing it) or when a newer chi is installed than chi web runs.
|
|
384
410
|
// Once per notice per tab: every reconnect's snapshot says it again.
|
|
385
411
|
const versions = { served: null, installed: null };
|
|
386
|
-
// The
|
|
387
|
-
|
|
412
|
+
// The sessions whose Restart is under way (their button says so): one
|
|
413
|
+
// session's restart never blocks another's.
|
|
414
|
+
const restartingIds = new Set();
|
|
388
415
|
const versionNoticesShown = new Set();
|
|
389
416
|
function noticeVersions(data) {
|
|
390
417
|
if (typeof data.version === "string") versions.served = data.version;
|
|
@@ -645,7 +672,7 @@ function setLiveStatus(status) {
|
|
|
645
672
|
const initRowEl = $("#initRow");
|
|
646
673
|
const initTasks = newInitTasks();
|
|
647
674
|
function renderInitRow() {
|
|
648
|
-
|
|
675
|
+
render(initRowTemplate(initTasks), initRowEl);
|
|
649
676
|
initRowEl.hidden = initTasks.size === 0;
|
|
650
677
|
}
|
|
651
678
|
function onInitEvent(type, data) {
|
|
@@ -655,7 +682,8 @@ function onInitEvent(type, data) {
|
|
|
655
682
|
const session = selected;
|
|
656
683
|
setTimeout(() => {
|
|
657
684
|
if (session !== selected) return;
|
|
658
|
-
|
|
685
|
+
fadeDone(initTasks, done);
|
|
686
|
+
renderInitRow();
|
|
659
687
|
setTimeout(() => {
|
|
660
688
|
if (session !== selected) return;
|
|
661
689
|
dropDone(initTasks, done);
|
|
@@ -767,8 +795,8 @@ async function resync({ endText = null } = {}) {
|
|
|
767
795
|
renderSessionView(data, { kept });
|
|
768
796
|
// Preview is read-only: a worker is woken only by POST /turn, so attach
|
|
769
797
|
// only when a live bridge exists.
|
|
770
|
-
|
|
771
|
-
if (
|
|
798
|
+
current.streamSeq = typeof data.last_event_seq === "number" ? (data.last_event_id || data.last_event_seq) : null;
|
|
799
|
+
if (current.streamSeq !== null) startStream(id, current.streamSeq);
|
|
772
800
|
else if (endText) addEndBubble(endText);
|
|
773
801
|
} finally {
|
|
774
802
|
resyncsInFlight -= 1;
|
|
@@ -853,15 +881,6 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
853
881
|
updateComposerMode();
|
|
854
882
|
}
|
|
855
883
|
|
|
856
|
-
// The history's one line while it has nothing else (loading, an error,
|
|
857
|
-
// no output yet).
|
|
858
|
-
function hintEl(text) {
|
|
859
|
-
const el = document.createElement("div");
|
|
860
|
-
el.className = "hint";
|
|
861
|
-
el.textContent = text;
|
|
862
|
-
return el;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
884
|
function renderHistory(items) {
|
|
866
885
|
if (!items.length) {
|
|
867
886
|
historyEl.replaceChildren(hintEl("No output yet. Send a message."));
|
|
@@ -871,12 +890,7 @@ function renderHistory(items) {
|
|
|
871
890
|
historyEl.replaceChildren(turnHistoryNodes(items, current.timing, { thumbs: (images) => thumbsTemplate(selected, images) }));
|
|
872
891
|
linkTextRefs();
|
|
873
892
|
} else {
|
|
874
|
-
historyEl.replaceChildren(...items.map((c) =>
|
|
875
|
-
const el = document.createElement("div");
|
|
876
|
-
el.className = "bubble output";
|
|
877
|
-
el.textContent = String(c);
|
|
878
|
-
return el;
|
|
879
|
-
}));
|
|
893
|
+
historyEl.replaceChildren(...items.map((c) => elementOf(outputBubbleTemplate(c))));
|
|
880
894
|
}
|
|
881
895
|
historyEl.scrollTop = historyEl.scrollHeight;
|
|
882
896
|
}
|
|
@@ -984,30 +998,6 @@ function atHistoryEnd() {
|
|
|
984
998
|
return shouldFollowScroll({ scrollHeight, scrollTop, clientHeight });
|
|
985
999
|
}
|
|
986
1000
|
|
|
987
|
-
function appendChunk(text) {
|
|
988
|
-
if (!text) return;
|
|
989
|
-
const k = normalize(text);
|
|
990
|
-
if (!k || shownTexts.shows(k)) return;
|
|
991
|
-
const follow = isNearBottom();
|
|
992
|
-
const hint = historyEl.querySelector(".hint");
|
|
993
|
-
if (hint) hint.remove();
|
|
994
|
-
const div = document.createElement("div");
|
|
995
|
-
div.className = "bubble output";
|
|
996
|
-
div.textContent = text;
|
|
997
|
-
appendToHistory(div);
|
|
998
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// +stopped+: a stop someone chose (a cancel, the session's stop), drawn in
|
|
1002
|
-
// the stopped style (timing.js STOPPED_LINE_CLASS); else a failure's line.
|
|
1003
|
-
function addEndBubble(text, { stopped = false } = {}) {
|
|
1004
|
-
const div = document.createElement("div");
|
|
1005
|
-
div.className = stopped ? STOPPED_LINE_CLASS : "bubble cancel";
|
|
1006
|
-
div.textContent = text;
|
|
1007
|
-
appendToHistory(div);
|
|
1008
|
-
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
1001
|
// Everything a turn's end (completed, canceled, failed) closes: then its
|
|
1012
1002
|
// end line (+endText+, the cancel or failed line), then the step cards that
|
|
1013
1003
|
// left the collapsed block (card.js leavesBlock), where a reload puts them.
|
|
@@ -1031,352 +1021,8 @@ function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false
|
|
|
1031
1021
|
if (questionCards.pendingQuestion()) questionCards.resolveQuestion(questionCards.pendingQuestion().id, { cancelled: true, reason: "turn ended" });
|
|
1032
1022
|
}
|
|
1033
1023
|
|
|
1034
|
-
// ── Prompt bubbles ─────────────────────────────────────────────────────────
|
|
1035
|
-
// Every client's prompts, keyed by enqueued_id: queued (badge) until their
|
|
1036
|
-
// turn starts, or steered when merged into a running turn. This tab's own
|
|
1037
|
-
// prompts are echoed at send (data-own) and tagged by their turn_enqueued.
|
|
1038
|
-
|
|
1039
|
-
// The bubble a reload draws (turn_html.js), with this page's own fields.
|
|
1040
|
-
// +delivery+: how it went into a running turn (next_step, cut, queue): its
|
|
1041
|
-
// queued badge says what it waits for.
|
|
1042
|
-
function makeUserBubble(prompt, { state = null, label = null, enqueuedId = null, own = false, images = null, delivery = null } = {}) {
|
|
1043
|
-
const hint = historyEl.querySelector(".hint");
|
|
1044
|
-
if (hint) hint.remove();
|
|
1045
|
-
const div = elementOf(userBubbleTemplate({ content: prompt }, {
|
|
1046
|
-
label, state, own, enqueuedId, delivery, key: normalize(prompt), thumbs: thumbsTemplate(selected, images),
|
|
1047
|
-
}));
|
|
1048
|
-
const follow = isNearBottom();
|
|
1049
|
-
appendToHistory(div);
|
|
1050
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1051
|
-
return div;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function setBubbleState(bubble, state) {
|
|
1055
|
-
bubble.classList.remove("queued", "steered", "failed");
|
|
1056
|
-
bubble.querySelector(".state-badge")?.remove();
|
|
1057
|
-
if (!BADGES[state]) {
|
|
1058
|
-
delete bubble.dataset.userState;
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
bubble.classList.add(state);
|
|
1062
|
-
bubble.dataset.userState = state;
|
|
1063
|
-
bubble.append(elementOf(stateBadgeTemplate(state, bubble.dataset.delivery || null)));
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// A bubble's delivery changed (the ack's cut: off goes in at the next
|
|
1067
|
-
// step; a queue to a turn that ended meanwhile is just the next turn): its
|
|
1068
|
-
// badge follows.
|
|
1069
|
-
function setBubbleDelivery(bubble, delivery) {
|
|
1070
|
-
if ((bubble.dataset.delivery || null) === (delivery || null)) return;
|
|
1071
|
-
if (delivery) bubble.dataset.delivery = delivery;
|
|
1072
|
-
else delete bubble.dataset.delivery;
|
|
1073
|
-
if (bubble.dataset.userState) setBubbleState(bubble, bubble.dataset.userState);
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function bubbleById(enqueuedId) {
|
|
1077
|
-
if (!enqueuedId) return null;
|
|
1078
|
-
return findInTurnRoots(".bubble.user[data-enqueued-id]").find(
|
|
1079
|
-
(el) => el.dataset.enqueuedId === enqueuedId,
|
|
1080
|
-
) || null;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// This tab's oldest untagged echo, preferring one with the same text.
|
|
1084
|
-
function untaggedOwnBubble(prompt) {
|
|
1085
|
-
const echoes = findInTurnRoots(".bubble.user[data-own]:not([data-enqueued-id])");
|
|
1086
|
-
const text = normalize(prompt || "");
|
|
1087
|
-
return echoes.find((el) => el.dataset.userContent === text) || echoes[0] || null;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Returns the bubble of the turn a turn_started starts (the stage takes it
|
|
1091
|
-
// into its prompt line), else null.
|
|
1092
|
-
function applyPromptOps(event) {
|
|
1093
|
-
if (event.type === "turn_started") liveTurn.wakeStart = !!event.continue && isReportClient(event.origin?.client_id);
|
|
1094
|
-
const ops = promptOps(event, { myId: clientId, known: (id) => !!bubbleById(id), unmatchedMerge: liveTurn.unmatchedMerge, wakeStart: liveTurn.wakeStart });
|
|
1095
|
-
if (event.type === "input_merged") liveTurn.unmatchedMerge = false;
|
|
1096
|
-
if (event.type === "pending_input_merged") liveTurn.unmatchedMerge = false;
|
|
1097
|
-
let started = null;
|
|
1098
|
-
for (const op of ops) {
|
|
1099
|
-
if (op.op === "report") {
|
|
1100
|
-
liveTurn.unmatchedMerge = "report";
|
|
1101
|
-
} else if (op.op === "add") {
|
|
1102
|
-
const bubble = makeUserBubble(op.prompt, { state: op.state, label: op.label, enqueuedId: op.enqueuedId, images: op.images, delivery: op.delivery });
|
|
1103
|
-
if (op.state === "started") started = bubble;
|
|
1104
|
-
if (event.type === "pending_input_merged") liveTurn.wakeStart = false;
|
|
1105
|
-
} else if (op.op === "tag") {
|
|
1106
|
-
if (bubbleById(op.enqueuedId)) continue;
|
|
1107
|
-
const echo = untaggedOwnBubble(op.prompt);
|
|
1108
|
-
if (echo) {
|
|
1109
|
-
echo.dataset.enqueuedId = op.enqueuedId;
|
|
1110
|
-
if (op.delivery) setBubbleDelivery(echo, op.delivery);
|
|
1111
|
-
}
|
|
1112
|
-
// Re-rendered from a snapshot: the echo is gone, the prompt still queued.
|
|
1113
|
-
else makeUserBubble(op.prompt, { state: "queued", own: true, enqueuedId: op.enqueuedId, images: op.images, delivery: op.delivery });
|
|
1114
|
-
} else if (op.op === "start") {
|
|
1115
|
-
const bubble = bubbleById(op.enqueuedId) || untaggedOwnBubble(op.prompt);
|
|
1116
|
-
// A resync (a new worker woken by this page's send) re-rendered the
|
|
1117
|
-
// history and took the echo with it: draw the prompt again.
|
|
1118
|
-
if (!bubble) started = makeUserBubble(op.prompt, { own: true, enqueuedId: op.enqueuedId, images: op.images });
|
|
1119
|
-
else {
|
|
1120
|
-
if (op.enqueuedId) bubble.dataset.enqueuedId = op.enqueuedId;
|
|
1121
|
-
setBubbleState(bubble, null);
|
|
1122
|
-
started = bubble;
|
|
1123
|
-
}
|
|
1124
|
-
} else if (op.op === "steer") {
|
|
1125
|
-
const bubble = bubbleById(op.enqueuedId);
|
|
1126
|
-
if (bubble) setBubbleState(bubble, "steered");
|
|
1127
|
-
else liveTurn.unmatchedMerge = true;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
return started;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// ── Idle recap ─────────────────────────────────────────────────────────────
|
|
1134
|
-
// A summary the worker writes after a quiet stretch or as it leaves: shown at
|
|
1135
|
-
// the end of the history. A turn starting leaves it in place, marked
|
|
1136
|
-
// earlier; only a new recap replaces it.
|
|
1137
|
-
|
|
1138
|
-
function showRecap(text, turnsSince = 0) {
|
|
1139
|
-
if (!text) return;
|
|
1140
|
-
removeRecap();
|
|
1141
|
-
const details = document.createElement("details");
|
|
1142
|
-
details.className = "bubble recap";
|
|
1143
|
-
if (turnsSince > 0) details.classList.add("stale");
|
|
1144
|
-
details.open = true;
|
|
1145
|
-
const summary = document.createElement("summary");
|
|
1146
|
-
summary.textContent = recapLabel(turnsSince);
|
|
1147
|
-
const body = document.createElement("div");
|
|
1148
|
-
body.className = "recap-body";
|
|
1149
|
-
body.textContent = text;
|
|
1150
|
-
details.appendChild(summary);
|
|
1151
|
-
details.appendChild(body);
|
|
1152
|
-
// A stale one sits where it was written, above the turns it doesn't cover.
|
|
1153
|
-
// Never in the stage: a recap is between turns.
|
|
1154
|
-
turnDom.settleFold?.();
|
|
1155
|
-
const before = recapPlace(turnPromptBubbles(), turnsSince);
|
|
1156
|
-
keepFollowing(historyEl, () => (before ? historyEl.insertBefore(details, before) : appendAboveLiveTiming(historyEl, details, liveTurn.timing.el)));
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// A turn started: the recap describes the chat before it.
|
|
1160
|
-
function markRecapStale() {
|
|
1161
|
-
historyEl.querySelectorAll(".bubble.recap:not(.stale)").forEach((el) => {
|
|
1162
|
-
el.classList.add("stale");
|
|
1163
|
-
const summary = el.querySelector("summary");
|
|
1164
|
-
if (summary) summary.textContent = recapLabel(0, true);
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// A guardrail hook or rule file that failed to load (once per worker), or
|
|
1169
|
-
// a bundle's plugin (+label+ "plugins").
|
|
1170
|
-
// +before+: the element it goes above (a snapshot's), else the end.
|
|
1171
|
-
function showGuardrailWarning(text, label = "guardrails", { before = null } = {}) {
|
|
1172
|
-
if (!text) return;
|
|
1173
|
-
const el = document.createElement("div");
|
|
1174
|
-
el.className = "bubble guardrail-warning";
|
|
1175
|
-
el.textContent = `${label}: ${text}`;
|
|
1176
|
-
const follow = isNearBottom();
|
|
1177
|
-
if (before) historyEl.insertBefore(el, before);
|
|
1178
|
-
else appendToHistory(el);
|
|
1179
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// A hook's line to the user (event[:notify]): "<bundle>: text", in the
|
|
1183
|
-
// warning colour for level warn.
|
|
1184
|
-
// +before+: the element it goes above (a snapshot's notice), else the end.
|
|
1185
|
-
// A plugin's steers (pending_input_merged's steers): a row of the running
|
|
1186
|
-
// turn's step, else (no turn runs) a bubble.
|
|
1187
|
-
function showSteers(steers) {
|
|
1188
|
-
for (const steer of steers || []) {
|
|
1189
|
-
if (turnDom.steer(steer)) continue;
|
|
1190
|
-
removeHintIfEmpty();
|
|
1191
|
-
const row = elementOf(steerRowTemplate(steer, { bubble: true }));
|
|
1192
|
-
const follow = isNearBottom();
|
|
1193
|
-
appendToHistory(row);
|
|
1194
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function showHookNotice(data, { before = null } = {}) {
|
|
1199
|
-
removeHintIfEmpty();
|
|
1200
|
-
const el = document.createElement("div");
|
|
1201
|
-
el.className = "bubble hook-notice" + (data.level === "warn" ? " warn" : "") + (data.cls ? ` ${data.cls}` : "");
|
|
1202
|
-
el.textContent = data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
|
|
1203
|
-
if (data.title) el.title = data.title;
|
|
1204
|
-
const follow = isNearBottom();
|
|
1205
|
-
if (before) historyEl.insertBefore(el, before);
|
|
1206
|
-
else appendToHistory(el);
|
|
1207
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// ── plugin cards ───────────────────────────────────────────────────────────
|
|
1211
|
-
// A plugin's card (docs/plugins.md, Cards): a glass card between turns, or
|
|
1212
|
-
// a row of the running turn's step. The same id updates the card in place.
|
|
1213
|
-
// An action button runs its command line in the session (sendCommand), as
|
|
1214
|
-
// if typed.
|
|
1215
|
-
|
|
1216
|
-
function cardElement(id) {
|
|
1217
|
-
if (!id) return null;
|
|
1218
|
-
return findInTurnRoots(".plugin-card").find((el) => el.dataset.cardId === String(id)) || null;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// Show +card+: update the one with its id where it is, else add it (a
|
|
1222
|
-
// running turn's card as a row of its step; else before +before+ or at the
|
|
1223
|
-
// end of the history). A running turn's card with actions asks the user
|
|
1224
|
-
// something (check-in's Nudge / Keep going / Stop): it goes under the turn's
|
|
1225
|
-
// block, not into a step that collapses when the next one starts.
|
|
1226
|
-
function upsertCard(card, { before = null } = {}) {
|
|
1227
|
-
if (!card || !card.id) return;
|
|
1228
|
-
const existing = cardElement(card.id);
|
|
1229
|
-
if (existing) {
|
|
1230
|
-
const drawn = drawCard(existing, card);
|
|
1231
|
-
drawn.classList.add("card-updated");
|
|
1232
|
-
setTimeout(() => drawn.classList.remove("card-updated"), 900);
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
removeHintIfEmpty();
|
|
1236
|
-
const el = drawCard(document.createElement(isNoticeCard(card) ? "details" : "div"), card);
|
|
1237
|
-
const asks = (card.actions || []).some((a) => a && a.command);
|
|
1238
|
-
if (card.in_turn && !before && !asks && turnDom.card(el)) return;
|
|
1239
|
-
const follow = isNearBottom();
|
|
1240
|
-
if (before) historyEl.insertBefore(el, before);
|
|
1241
|
-
else appendToHistory(el);
|
|
1242
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// A snapshot's cards and notices, each above the first turn after it; a
|
|
1246
|
-
// turn's own notice where it showed live (placeTurnNotice).
|
|
1247
|
-
function placeSnapshotCards(entries) {
|
|
1248
|
-
const users = turnPromptBubbles();
|
|
1249
|
-
for (const entry of entries) {
|
|
1250
|
-
const before = cardPlace(users, entry);
|
|
1251
|
-
if (isCard(entry)) upsertCard(entry, { before });
|
|
1252
|
-
else if (entry.type === "question") placeResolvedQuestion(entry, before);
|
|
1253
|
-
else if (entry.type === "guardrail_warning") showGuardrailWarning(entry.message, entry.label || "guardrails", { before });
|
|
1254
|
-
else if (entry.in_turn) placeTurnNotice(entry, before);
|
|
1255
|
-
else if (entry.type === "hook_notice") showHookNotice(entry, { before });
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// A finished turn's row, a hook's notice or the loop's asking again
|
|
1260
|
-
// (+before+: the next turn's prompt, or null for the last turn): a row of
|
|
1261
|
-
// its step, above the row of the call it came before, as live; else (a
|
|
1262
|
-
// plain answer, a step the history doesn't have) a bubble before the
|
|
1263
|
-
// turn's answer, or before its timing line when it has none.
|
|
1264
|
-
function placeTurnNotice(entry, before) {
|
|
1265
|
-
const extra = entry.type === "llm_context_edited" ? llmContextNotice(entry) : {};
|
|
1266
|
-
const line = noticeLine(entry);
|
|
1267
|
-
const turnEls = reloadedTurnEls(before);
|
|
1268
|
-
const place = turnNoticePlace(entry);
|
|
1269
|
-
const block = turnEls.find((el) => el.matches(".turn-work"));
|
|
1270
|
-
const step = place && block ? block.querySelectorAll(":scope > .gen")[place.step] : null;
|
|
1271
|
-
if (!step) {
|
|
1272
|
-
showHookNotice({ ...entry, ...extra, line }, { before: turnAnswerEl(turnEls) || before });
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
const el = document.createElement("div");
|
|
1276
|
-
el.className = "hook-notice" + (entry.level === "warn" ? " warn" : "") + (extra.cls ? ` ${extra.cls}` : "");
|
|
1277
|
-
el.textContent = line;
|
|
1278
|
-
if (extra.title) el.title = extra.title;
|
|
1279
|
-
let body = step.querySelector(":scope > .activity-body");
|
|
1280
|
-
if (!body) {
|
|
1281
|
-
body = document.createElement("div");
|
|
1282
|
-
body.className = "activity-body";
|
|
1283
|
-
step.appendChild(body);
|
|
1284
|
-
}
|
|
1285
|
-
const row = [...body.children].find((r) => r.dataset.key === place.rowKey);
|
|
1286
|
-
body.insertBefore(el, row || null);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// A prompt that started a turn: not a line steered into a running one.
|
|
1290
|
-
const TURN_PROMPT = ".bubble.user:not(.steered)";
|
|
1291
|
-
|
|
1292
|
-
// The history's turn prompts, oldest first (what turns_since counts).
|
|
1293
|
-
function turnPromptBubbles() {
|
|
1294
|
-
return [...historyEl.querySelectorAll(`:scope > ${TURN_PROMPT}`)];
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// The elements of the reloaded turn that ends before +before+ (the next
|
|
1298
|
-
// turn's prompt, or null for the last turn), its prompt first.
|
|
1299
|
-
function reloadedTurnEls(before) {
|
|
1300
|
-
const turnEls = [];
|
|
1301
|
-
for (let el = before ? before.previousElementSibling : historyEl.lastElementChild; el; el = el.previousElementSibling) {
|
|
1302
|
-
turnEls.unshift(el);
|
|
1303
|
-
if (el.matches(TURN_PROMPT)) break;
|
|
1304
|
-
}
|
|
1305
|
-
return turnEls;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// What goes after a reloaded turn's own rows and cards: its answer, else
|
|
1309
|
-
// its timing line; null when it has neither.
|
|
1310
|
-
function turnAnswerEl(turnEls) {
|
|
1311
|
-
return turnEls.filter((el) => el.matches(".bubble.output, .empty-answer")).pop() || turnEls.find((el) => el.matches(".turn-timing")) || null;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// A finished turn's question (the snapshot's cards keep it with its
|
|
1315
|
-
// answer): the card as it was left live, resolved, before the lines the
|
|
1316
|
-
// user sent into the turn after it (a steered bubble's data-step: the step
|
|
1317
|
-
// that read it) and the turn's answer. A pending one is the running turn's
|
|
1318
|
-
// (pending_question draws it).
|
|
1319
|
-
function placeResolvedQuestion(entry, before) {
|
|
1320
|
-
const pq = entry.pending_question;
|
|
1321
|
-
if (!pq?.id || !(entry.answer || entry.cancelled)) return;
|
|
1322
|
-
const turnEls = reloadedTurnEls(before);
|
|
1323
|
-
const asked = Number(entry.iteration) || 1;
|
|
1324
|
-
// The reply that asked: its step's text in the reloaded block (the card
|
|
1325
|
-
// keeps it, as live), never the running turn's.
|
|
1326
|
-
const said = stepText(turnEls.find((el) => el.matches(".turn-work")), asked);
|
|
1327
|
-
questionCards.renderQuestion(pq, { said });
|
|
1328
|
-
const card = questionCards.questionCard();
|
|
1329
|
-
if (!card || card.dataset.qid !== String(pq.id)) return;
|
|
1330
|
-
questionCards.resolveQuestion(pq.id, { answer: entry.answer || null, cancelled: !!entry.cancelled, reason: entry.reason || "" });
|
|
1331
|
-
const later = turnEls.find((el) => el.matches(".bubble.user.steered") && Number(el.dataset.step) > asked);
|
|
1332
|
-
historyEl.insertBefore(card, later || turnAnswerEl(turnEls) || before);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// A live card's body comes as text; the worker's snapshot (through the
|
|
1336
|
-
// server) has it rendered. One re-read for a burst of cards.
|
|
1337
|
-
let cardBodiesTimer = null;
|
|
1338
|
-
function refreshCardBodies() {
|
|
1339
|
-
clearTimeout(cardBodiesTimer);
|
|
1340
|
-
const id = selected;
|
|
1341
|
-
cardBodiesTimer = setTimeout(async () => {
|
|
1342
|
-
try {
|
|
1343
|
-
const data = await getSession(id, { cards: true });
|
|
1344
|
-
if (id !== selected) return;
|
|
1345
|
-
for (const card of data.cards || []) {
|
|
1346
|
-
const el = card.type === "card" && cardElement(card.id);
|
|
1347
|
-
if (el) refreshCardBody(el, card);
|
|
1348
|
-
}
|
|
1349
|
-
} catch (_) {}
|
|
1350
|
-
}, 150);
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
function onCardAction(e) {
|
|
1354
|
-
const button = e.target.closest(".plugin-card .card-action");
|
|
1355
|
-
if (!button || !selected) return;
|
|
1356
|
-
const card = button.closest(".plugin-card");
|
|
1357
|
-
const err = card.querySelector(".card-error");
|
|
1358
|
-
err.classList.add("hidden");
|
|
1359
|
-
button.disabled = true;
|
|
1360
|
-
sendCommand(selected, button.dataset.command, { clientId, card: true })
|
|
1361
|
-
.then(() => {
|
|
1362
|
-
// The command woke the worker: follow its stream if none is open.
|
|
1363
|
-
followWokenWorker();
|
|
1364
|
-
})
|
|
1365
|
-
.catch((error) => {
|
|
1366
|
-
err.textContent = error.message;
|
|
1367
|
-
err.classList.remove("hidden");
|
|
1368
|
-
})
|
|
1369
|
-
.finally(() => { button.disabled = false; });
|
|
1370
|
-
}
|
|
1371
|
-
turnRoots().forEach((root) => root.addEventListener("click", onCardAction));
|
|
1372
|
-
|
|
1373
|
-
function removeRecap() {
|
|
1374
|
-
historyEl.querySelectorAll(".bubble.recap").forEach((el) => el.remove());
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
1024
|
// The info bar's "copy chi --attach": the full id, which the page shows
|
|
1378
|
-
// only short; "copied ✓" for 2 s.
|
|
1379
|
-
let attachCopiedAt = 0;
|
|
1025
|
+
// only short; "copied ✓" for 2 s (current.attachCopiedAt: that session's).
|
|
1380
1026
|
infoTextEl.addEventListener("click", (e) => {
|
|
1381
1027
|
if (e.target.closest(".worker-restart")) restartWorker();
|
|
1382
1028
|
// Pushed as openAllView does, so the view's back returns here.
|
|
@@ -1387,28 +1033,26 @@ infoTextEl.addEventListener("click", (e) => {
|
|
|
1387
1033
|
});
|
|
1388
1034
|
infoTextEl.addEventListener("click", async (e) => {
|
|
1389
1035
|
if (!e.target.closest(".copy-attach") || !selected) return;
|
|
1390
|
-
|
|
1391
|
-
|
|
1036
|
+
const id = selected;
|
|
1037
|
+
if (!(await copyText(`chi --attach ${id}`)) || id !== selected) return;
|
|
1038
|
+
current.attachCopiedAt = Date.now();
|
|
1392
1039
|
updateInfoBar();
|
|
1393
1040
|
setTimeout(updateInfoBar, 2000);
|
|
1394
1041
|
});
|
|
1395
1042
|
|
|
1396
|
-
// The open session's worker
|
|
1397
|
-
// a badge, with Restart when the worker can (else the stop command).
|
|
1043
|
+
// The open session's worker badge (info_bar_model.js workerBadgeFor).
|
|
1398
1044
|
function currentWorkerBadge() {
|
|
1399
|
-
|
|
1400
|
-
return workerBadge({ workerVersion: current.workerVersion, features: current.workerFeatures,
|
|
1401
|
-
installed: versions.installed, served: versions.served, sessionId: selected });
|
|
1045
|
+
return workerBadgeFor(selected, current, versions);
|
|
1402
1046
|
}
|
|
1403
1047
|
|
|
1404
1048
|
// Restart: the session moves to a new worker on the newest chi; the page
|
|
1405
1049
|
// follows it (its bridge_up), the composer's draft stays as it is.
|
|
1406
1050
|
async function restartWorker() {
|
|
1407
1051
|
const badge = currentWorkerBadge();
|
|
1408
|
-
if (!badge || !badge.restart ||
|
|
1052
|
+
if (!badge || !badge.restart || restartingIds.has(selected)) return;
|
|
1409
1053
|
if (!confirm(restartConfirmText(current.workerVersion, badge.newest))) return;
|
|
1410
1054
|
const id = selected;
|
|
1411
|
-
|
|
1055
|
+
restartingIds.add(id);
|
|
1412
1056
|
updateInfoBar();
|
|
1413
1057
|
try {
|
|
1414
1058
|
const result = await restartSession(id);
|
|
@@ -1416,80 +1060,41 @@ async function restartWorker() {
|
|
|
1416
1060
|
} catch (e) {
|
|
1417
1061
|
showToast(`Can't restart: ${String(e.message).replace(/ \(\d+\)$/, "")}`, null, { sticky: true });
|
|
1418
1062
|
} finally {
|
|
1419
|
-
|
|
1063
|
+
restartingIds.delete(id);
|
|
1420
1064
|
updateInfoBar();
|
|
1421
1065
|
}
|
|
1422
1066
|
}
|
|
1423
1067
|
|
|
1068
|
+
// The session bar (info_bar_model.js sessionBar): its buttons, and the
|
|
1069
|
+
// info line rendered by info_bar.js.
|
|
1424
1070
|
function updateInfoBar() {
|
|
1071
|
+
const { buttons, bar } = sessionBar({
|
|
1072
|
+
id: selected, state: current, sessions: allSessions, versions, cardTitle: (s) => lists.cardTitle(s),
|
|
1073
|
+
continuing: continuingIds, restarting: restartingIds, streaming: !!streamControl, running: liveTurn.running,
|
|
1074
|
+
now: Date.now(),
|
|
1075
|
+
});
|
|
1425
1076
|
const stopBtn = document.getElementById("infoStopBtn");
|
|
1426
1077
|
const deleteBtn = document.getElementById("infoDeleteBtn");
|
|
1427
1078
|
const archiveBtn = document.getElementById("infoArchiveBtn");
|
|
1428
1079
|
const continueBtn = document.getElementById("infoContinueBtn");
|
|
1429
|
-
const chain = selected ? chainNeighbours(allSessions, selected, { continues: current.continues }) : { prev: null, next: null };
|
|
1430
1080
|
if (continueBtn) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
continueBtn.
|
|
1434
|
-
const busy = !!selected && continuingIds.has(selected);
|
|
1435
|
-
continueBtn.disabled = busy;
|
|
1436
|
-
continueBtn.textContent = busy ? "continuing…" : "continue →";
|
|
1081
|
+
continueBtn.style.display = buttons.continue.shown ? "" : "none";
|
|
1082
|
+
continueBtn.disabled = buttons.continue.disabled;
|
|
1083
|
+
continueBtn.textContent = buttons.continue.text;
|
|
1437
1084
|
}
|
|
1438
|
-
if (deleteBtn) deleteBtn.style.display =
|
|
1085
|
+
if (deleteBtn) deleteBtn.style.display = buttons.delete.shown ? "" : "none";
|
|
1439
1086
|
if (archiveBtn) {
|
|
1440
|
-
archiveBtn.style.display =
|
|
1441
|
-
|
|
1442
|
-
archiveBtn.
|
|
1443
|
-
archiveBtn.
|
|
1444
|
-
archiveBtn.title = archived ? "Unarchive: back to the lists" : "Archive: hide from the lists, keep for good";
|
|
1445
|
-
}
|
|
1446
|
-
if (!selected) {
|
|
1447
|
-
// The start page hides the info bar (the storage path is on #/sessions).
|
|
1448
|
-
renderInfoBar(infoTextEl, null);
|
|
1449
|
-
if (stopBtn) stopBtn.style.display = "none";
|
|
1450
|
-
updateSessionTitle();
|
|
1451
|
-
return;
|
|
1087
|
+
archiveBtn.style.display = buttons.archive.shown ? "" : "none";
|
|
1088
|
+
archiveBtn.textContent = buttons.archive.text;
|
|
1089
|
+
archiveBtn.classList.toggle("archived", buttons.archive.archived);
|
|
1090
|
+
archiveBtn.title = buttons.archive.title;
|
|
1452
1091
|
}
|
|
1453
|
-
if (stopBtn) stopBtn.style.display =
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
1460
|
-
const sessionDuration = currentSessionDuration();
|
|
1461
|
-
// A delegated session names its parent, as a link to it.
|
|
1462
|
-
const parentChip = delegatedBy(current.parentId, allSessions.find((x) => x.id === current.parentId) || null);
|
|
1463
|
-
const badge = currentWorkerBadge();
|
|
1464
|
-
// The session clock ticks every second while a turn runs: lit writes its
|
|
1465
|
-
// text alone, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
1466
|
-
renderInfoBar(infoTextEl, {
|
|
1467
|
-
id: selected,
|
|
1468
|
-
copied: Date.now() - attachCopiedAt < 2000,
|
|
1469
|
-
status: current.status,
|
|
1470
|
-
model: modelLabel(current.model, ...(current.served || [])),
|
|
1471
|
-
// A link of a session chain: ← the previous link, → the next one (each
|
|
1472
|
-
// only while the list holds it).
|
|
1473
|
-
chain: { prev: chainLink(chain.prev), next: chainLink(chain.next) },
|
|
1474
|
-
parent: parentChip ? { id: current.parentId, title: parentChip.title } : null,
|
|
1475
|
-
// A parent names its delegates; the link opens all sessions.
|
|
1476
|
-
kids: childrenSummary({ id: selected }, allSessions),
|
|
1477
|
-
llmContext: current.llmContext,
|
|
1478
|
-
thinking: current.thinking,
|
|
1479
|
-
reasoningHistory: current.reasoningHistory,
|
|
1480
|
-
// The tools chip (toolkit_chip.js): null on full.
|
|
1481
|
-
toolkit: current.toolkit,
|
|
1482
|
-
// The model notes the session's prompt carried.
|
|
1483
|
-
notes: promptNotesChip(current.promptNotes),
|
|
1484
|
-
memories: current.usedMemories,
|
|
1485
|
-
preloaded: current.preloadedMemories,
|
|
1486
|
-
muted: current.mutedMemories,
|
|
1487
|
-
dir: current.dir,
|
|
1488
|
-
ctx: { text: ctxBarText(current.ctxPct, current.ctxWindowSource), tip: ctxTip },
|
|
1489
|
-
speed: speedTextNow ? { text: speedTextNow, tip: `Decode speed of the last generation${avgSpeed ? `; session average ${avgSpeed}` : ""}` } : null,
|
|
1490
|
-
clock: sessionDuration === null ? "" : `session ${formatDuration(sessionDuration)}`,
|
|
1491
|
-
worker: badge ? { badge, busy: restartingId === selected } : null,
|
|
1492
|
-
});
|
|
1092
|
+
if (stopBtn) stopBtn.style.display = buttons.stop.shown ? "" : "none";
|
|
1093
|
+
// No session: the start page hides the info bar (the storage path is on
|
|
1094
|
+
// #/sessions). The session clock ticks every second while a turn runs:
|
|
1095
|
+
// lit writes its text alone, the bar's other nodes (a tooltip, a button
|
|
1096
|
+
// mid-press) stay.
|
|
1097
|
+
renderInfoBar(infoTextEl, bar);
|
|
1493
1098
|
updateSessionTitle();
|
|
1494
1099
|
}
|
|
1495
1100
|
|
|
@@ -1519,18 +1124,6 @@ function updateSessionTitle() {
|
|
|
1519
1124
|
}
|
|
1520
1125
|
}
|
|
1521
1126
|
|
|
1522
|
-
// A chain's link (chains.js chainNeighbours) as the info bar's ← / →
|
|
1523
|
-
// shows it (info_bar.js chainLinkTemplate), or null.
|
|
1524
|
-
function chainLink(link) {
|
|
1525
|
-
if (!link) return null;
|
|
1526
|
-
return { id: link.id, date: chainDate(link.session?.created_at), preview: link.session ? lists.cardTitle(link.session) : "",
|
|
1527
|
-
archived: !!link.session?.archived };
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function currentSessionDuration() {
|
|
1531
|
-
return sessionDurationNow(current.timing, liveTurn.running);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
1127
|
// The stage takes a running turn's rows (its extras); else the history.
|
|
1535
1128
|
function appendToHistory(el) {
|
|
1536
1129
|
// (The stage's place first moves a turn still sliding away into the history.)
|
|
@@ -1577,12 +1170,12 @@ function setUsedMemories(names) {
|
|
|
1577
1170
|
// worker have been up already, turn_started says so (replaysDrawnTurn).
|
|
1578
1171
|
function followWokenWorker() {
|
|
1579
1172
|
if (streamControl?.live) return;
|
|
1580
|
-
startStream(selected,
|
|
1581
|
-
streamFromGuess =
|
|
1173
|
+
startStream(selected, current.streamSeq ?? 0);
|
|
1174
|
+
current.streamFromGuess = current.streamSeq === null;
|
|
1582
1175
|
}
|
|
1583
1176
|
|
|
1584
1177
|
function startStream(id, fromSeq) {
|
|
1585
|
-
streamFromGuess = false;
|
|
1178
|
+
current.streamFromGuess = false;
|
|
1586
1179
|
if (streamControl) {
|
|
1587
1180
|
streamControl.close();
|
|
1588
1181
|
streamControl = null;
|
|
@@ -1652,8 +1245,8 @@ const streamHandlers = createStreamHandlers({
|
|
|
1652
1245
|
earlyRestores,
|
|
1653
1246
|
stream: {
|
|
1654
1247
|
takeGuess: () => {
|
|
1655
|
-
const guess = streamFromGuess;
|
|
1656
|
-
streamFromGuess = false;
|
|
1248
|
+
const guess = current.streamFromGuess;
|
|
1249
|
+
current.streamFromGuess = false;
|
|
1657
1250
|
return guess;
|
|
1658
1251
|
},
|
|
1659
1252
|
restart: () => {
|
|
@@ -1711,12 +1304,6 @@ async function refreshTimingFromSession() {
|
|
|
1711
1304
|
}
|
|
1712
1305
|
|
|
1713
1306
|
|
|
1714
|
-
function removeHintIfEmpty() {
|
|
1715
|
-
const hint = historyEl.querySelector(".hint");
|
|
1716
|
-
if (!hint) return;
|
|
1717
|
-
// Remove the placeholder when the card will be the first content in history.
|
|
1718
|
-
if (!historyEl.querySelector(".bubble")) hint.remove();
|
|
1719
|
-
}
|
|
1720
1307
|
function closeStream() {
|
|
1721
1308
|
turnDom.reset();
|
|
1722
1309
|
if (streamControl) {
|
|
@@ -1806,18 +1393,18 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1806
1393
|
updateInfoBar();
|
|
1807
1394
|
}
|
|
1808
1395
|
actionBtn.disabled = true;
|
|
1396
|
+
// The session the line goes to: the page may switch to another while an
|
|
1397
|
+
// image uploads or the turn is sent, and what follows stays with this one.
|
|
1809
1398
|
const sentTo = selected;
|
|
1810
1399
|
try {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
}
|
|
1814
|
-
const ack = await sendTurn(selected, prompt, { clientId, images: turnRefs(sending), history: typed, delivery });
|
|
1400
|
+
await uploadChips(sending, sentTo, { upload: uploadImage });
|
|
1401
|
+
const ack = await sendTurn(sentTo, prompt, { clientId, images: turnRefs(sending), history: typed, delivery });
|
|
1815
1402
|
if (ack?.command_id) {
|
|
1816
1403
|
// The worker knew it as a command (its list was newer than ours):
|
|
1817
1404
|
// its command_ran shows it, not a prompt bubble.
|
|
1818
1405
|
echo.remove();
|
|
1819
1406
|
clearComposer();
|
|
1820
|
-
followWokenWorker();
|
|
1407
|
+
if (sentTo === selected) followWokenWorker();
|
|
1821
1408
|
return;
|
|
1822
1409
|
}
|
|
1823
1410
|
if (ack?.enqueued_id) sentIds.add(ack.enqueued_id);
|
|
@@ -1834,7 +1421,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1834
1421
|
promptEl.value = composerAfterAck({ sent: line, current: promptEl.value, refill: restored?.refill || "" });
|
|
1835
1422
|
fitPrompt();
|
|
1836
1423
|
clearChips();
|
|
1837
|
-
if (restored?.images?.length) setChips(restoredChips(
|
|
1424
|
+
if (restored?.images?.length) setChips(restoredChips(sentTo, restored.images));
|
|
1838
1425
|
// Its turn_enqueued usually tagged the echo already; the ack covers a
|
|
1839
1426
|
// worker that wasn't live (no turn_enqueued then).
|
|
1840
1427
|
if (ack?.enqueued_id && !echo.dataset.enqueuedId && !bubbleById(ack.enqueued_id)) {
|
|
@@ -1843,17 +1430,18 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1843
1430
|
// A live stream carries this turn already; restarting it would drop the
|
|
1844
1431
|
// events in between. Without one, POST /turn just woke a worker: its
|
|
1845
1432
|
// events start after the seq the session was rendered at (0 for a new
|
|
1846
|
-
// worker process).
|
|
1847
|
-
followWokenWorker();
|
|
1433
|
+
// worker process). A switch since then opened the other session's own.
|
|
1434
|
+
if (sentTo === selected) followWokenWorker();
|
|
1848
1435
|
} catch (e) {
|
|
1849
1436
|
echo.remove();
|
|
1850
1437
|
// The turn never reached the worker's queue: an early restore waiting
|
|
1851
1438
|
// for its ack is never taken back (4.13), so drop it here. The prompt
|
|
1852
1439
|
// is still in the composer, as the echo was removed.
|
|
1853
1440
|
dropEarlyRestores(earlyRestores);
|
|
1854
|
-
// The chips stay (uploaded ones keep their ref) for another try: an
|
|
1441
|
+
// The chips stay (uploaded ones keep their ref, to sentTo) for another try: an
|
|
1855
1442
|
// image refused (bad_image, too_large, bad_images) says why below.
|
|
1856
1443
|
if (e.code === "owned_by_tui") {
|
|
1444
|
+
if (sentTo !== selected) return alert(e.message);
|
|
1857
1445
|
current.owner = "tui";
|
|
1858
1446
|
updateComposerMode();
|
|
1859
1447
|
return;
|
|
@@ -1862,7 +1450,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1862
1450
|
// says so, and the line stays in the composer.
|
|
1863
1451
|
if (e.code === "not_found") return leaveGoneSession(sentTo, "not_found");
|
|
1864
1452
|
// A /queue to a worker that can't queue (it would steer): its words, the text kept.
|
|
1865
|
-
if (e.code === "delivery_unavailable") return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1453
|
+
if (e.code === "delivery_unavailable" && sentTo === selected) return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1866
1454
|
alert(e.message);
|
|
1867
1455
|
} finally {
|
|
1868
1456
|
actionBtn.disabled = current.owner === "tui";
|
|
@@ -1902,30 +1490,6 @@ function clearComposer() {
|
|
|
1902
1490
|
fitPrompt();
|
|
1903
1491
|
}
|
|
1904
1492
|
|
|
1905
|
-
function addCommandBubble(view) {
|
|
1906
|
-
removeHintIfEmpty();
|
|
1907
|
-
const div = document.createElement("div");
|
|
1908
|
-
// Page-only: kept across a resync of this session (keptCommandBubbles).
|
|
1909
|
-
if (selected) div.dataset.keptFor = selected;
|
|
1910
|
-
drawCommandBubble(div, view);
|
|
1911
|
-
const follow = isNearBottom();
|
|
1912
|
-
appendToHistory(div);
|
|
1913
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
function commandBubble(commandId) {
|
|
1917
|
-
if (!commandId) return null;
|
|
1918
|
-
return findInTurnRoots(".bubble.command").find((el) => el.dataset.commandId === String(commandId)) || null;
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
function addNoteBubble(note) {
|
|
1922
|
-
removeHintIfEmpty();
|
|
1923
|
-
const div = elementOf(noteBubbleTemplate(note));
|
|
1924
|
-
const follow = isNearBottom();
|
|
1925
|
-
appendToHistory(div);
|
|
1926
|
-
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
1493
|
async function handleCancel() {
|
|
1930
1494
|
if (!selected) return;
|
|
1931
1495
|
cancelBtn.disabled = true;
|