samagotchi 0.92.0 → 0.93.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 +31 -1
- data/bin/chi +4 -4
- data/docs/architecture.md +4 -4
- data/docs/guardrails.md +4 -5
- data/docs/internals/engine.md +6 -0
- data/docs/sessions.md +1 -1
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +12 -14
- data/lib/samagotchi/client_id.rb +18 -0
- data/lib/samagotchi/config/restart_watch.rb +112 -0
- data/lib/samagotchi/config.rb +18 -59
- data/lib/samagotchi/engine.rb +4 -14
- data/lib/samagotchi/guardrails/data_readers.rb +34 -30
- data/lib/samagotchi/guardrails/parent_levels.rb +31 -0
- data/lib/samagotchi/guardrails/parent_policy.rb +9 -20
- data/lib/samagotchi/guardrails/shell_lex.rb +28 -7
- data/lib/samagotchi/hook_question.rb +21 -0
- data/lib/samagotchi/hook_routes.rb +6 -6
- data/lib/samagotchi/hooks/registry.rb +5 -5
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/llm/anthropic_messages.rb +5 -0
- data/lib/samagotchi/parent_answers_command.rb +6 -6
- data/lib/samagotchi/plugin/context.rb +6 -5
- data/lib/samagotchi/plugin/http_transport.rb +14 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -3
- data/lib/samagotchi/question_desk.rb +12 -9
- data/lib/samagotchi/reply_wait/poll.rb +267 -0
- data/lib/samagotchi/reply_wait.rb +47 -190
- data/lib/samagotchi/send_command/parent_gate.rb +83 -0
- data/lib/samagotchi/send_command/wait_target.rb +55 -0
- data/lib/samagotchi/send_command.rb +31 -100
- data/lib/samagotchi/session.rb +10 -9
- data/lib/samagotchi/session_inbox.rb +31 -8
- data/lib/samagotchi/session_manager.rb +48 -15
- data/lib/samagotchi/session_metrics.rb +22 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/testing/bundle.rb +1 -1
- data/lib/samagotchi/testing/host.rb +2 -8
- data/lib/samagotchi/testing/recorder.rb +6 -9
- data/lib/samagotchi/tools/delegate.rb +10 -10
- data/lib/samagotchi/tools/delegate_cursor.rb +32 -8
- data/lib/samagotchi/tools/delegate_relay.rb +31 -16
- data/lib/samagotchi/tools/delegate_wait.rb +4 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +46 -63
- data/lib/samagotchi/web/public/app.js +122 -556
- data/lib/samagotchi/web/public/index.html +1 -1
- data/lib/samagotchi/web/public/send_turn.js +173 -0
- data/lib/samagotchi/web/public/session_actions.js +203 -0
- data/lib/samagotchi/web/public/start_page.js +236 -0
- metadata +10 -1
|
@@ -23,31 +23,23 @@ import {
|
|
|
23
23
|
renderText,
|
|
24
24
|
} from "./data.js";
|
|
25
25
|
import { createContextChips } from "./context_chips.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
withChoice,
|
|
29
|
-
} from "./llm_context_chip.js";
|
|
30
|
-
import {
|
|
31
|
-
chipModel as thinkingChipModel, createThinkingControl,
|
|
32
|
-
withChoice as withThinkingChoice,
|
|
33
|
-
} from "./thinking_chip.js";
|
|
26
|
+
import { createLlmContextControl } from "./llm_context_chip.js";
|
|
27
|
+
import { createThinkingControl } from "./thinking_chip.js";
|
|
34
28
|
import { createReasoningHistoryControl } from "./reasoning_history_chip.js";
|
|
35
29
|
import { renderInfoBar } from "./info_bar.js";
|
|
36
30
|
import { sessionBar, workerBadgeFor } from "./info_bar_model.js";
|
|
37
31
|
import { createShownTexts } from "./shown_texts.js";
|
|
38
|
-
import { MODEL_KEY, modelsFailedText } from "./model_pick.js";
|
|
39
32
|
import { emptySessionState, sessionStateFrom } from "./session_state.js";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import { normalize, firstPreview, failedTurnText, deleteConfirmText, goneSessionNotice, cantText, withLiveStatus, withoutWorker } from "./format.js";
|
|
33
|
+
import { admitImages, chipName, chipsTemplate, imageFiles, thumbsTemplate } from "./images.js";
|
|
34
|
+
import { normalize, failedTurnText, cantText, withLiveStatus, withoutWorker } from "./format.js";
|
|
43
35
|
import { createTurnView, turnHistoryNodes } from "./turn_view.js";
|
|
44
36
|
import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
45
|
-
import { createBubbles, hintEl, outputBubbleTemplate
|
|
37
|
+
import { createBubbles, hintEl, outputBubbleTemplate } from "./bubbles.js";
|
|
46
38
|
import { createCardsPlace } from "./cards_place.js";
|
|
47
39
|
import { createStageView, FOLD_MS } from "./stage_view.js";
|
|
48
40
|
import { createMarkdownPlace } from "./markdown_place.js";
|
|
49
41
|
import { reducedMotion, STAGE_GAP } from "./stage_frame.js";
|
|
50
|
-
import {
|
|
42
|
+
import { openerText, openTitle } from "./chains.js";
|
|
51
43
|
import { createTaskStopper } from "./task_stop.js";
|
|
52
44
|
import { appendQuote } from "./annotations.js";
|
|
53
45
|
import { parsePresets } from "./annotate_presets.js";
|
|
@@ -68,7 +60,7 @@ import { easeScrollTo, FOLLOW_MS, shouldFollowScroll } from "./scroll.js";
|
|
|
68
60
|
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
69
61
|
import { createStripGrip } from "./strip_grip.js";
|
|
70
62
|
import { waitingOn } from "./notify.js";
|
|
71
|
-
import { applySessionEvent,
|
|
63
|
+
import { applySessionEvent, sortedByUpdated } from "./sessions_list.js";
|
|
72
64
|
import { createNotifications } from "./notifications.js";
|
|
73
65
|
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
74
66
|
import { restartConfirmText, versionNotice } from "./update_notice.js";
|
|
@@ -82,8 +74,7 @@ import { linkRefs, touchedFiles } from "./ref_scan.js";
|
|
|
82
74
|
import { DONE_MS, applyInitEvent, dropDone, fadeDone, initRowTemplate, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
83
75
|
import { newClientId, snapshotEvents, workerGoneText } from "./turn_events.js";
|
|
84
76
|
import { applyConfirm, keptCommandBubbles, restoreCommandBubbles } from "./commands.js";
|
|
85
|
-
import { createComposerCommands
|
|
86
|
-
import { composerAfterAck, dropEarlyRestores, restoreOnAck } from "./restore.js";
|
|
77
|
+
import { createComposerCommands } from "./composer.js";
|
|
87
78
|
import { createStreamHandlers } from "./stream_handlers.js";
|
|
88
79
|
import { createListsView } from "./lists_view.js";
|
|
89
80
|
import { createDeliveryControl, QUEUE_NEEDS_WORKER } from "./delivery_line.js";
|
|
@@ -93,6 +84,9 @@ import { toolName } from "./activity.js";
|
|
|
93
84
|
import { toastBottom } from "./toast_place.js";
|
|
94
85
|
import { createToasts } from "./toasts.js";
|
|
95
86
|
import { createUnqueue } from "./unqueue.js";
|
|
87
|
+
import { createStartPage } from "./start_page.js";
|
|
88
|
+
import { createSessionActions } from "./session_actions.js";
|
|
89
|
+
import { createSendTurn } from "./send_turn.js";
|
|
96
90
|
|
|
97
91
|
const $ = (s) => document.querySelector(s);
|
|
98
92
|
const topStripEl = $("#topStrip");
|
|
@@ -122,26 +116,6 @@ const llmContextControl = createLlmContextControl({
|
|
|
122
116
|
running: () => liveTurn.running,
|
|
123
117
|
run: (line) => runSessionCommand(line),
|
|
124
118
|
});
|
|
125
|
-
// The start page's LLM context chip, beside the model picker: the picked
|
|
126
|
-
// model's values, and a choice for the next new session only (sent with its
|
|
127
|
-
// create, then dropped; never remembered: a reload or opening a session
|
|
128
|
-
// drops it too, a failed create keeps it).
|
|
129
|
-
const llmContextNewEl = $("#llmContextNew");
|
|
130
|
-
let newChatLlmContext = null;
|
|
131
|
-
const newChatLlmSummary = () => (selected || modelPickEl.hidden ? null : modelPicker.chosenRow()?.llm_context || null);
|
|
132
|
-
const llmContextNewControl = createLlmContextControl({
|
|
133
|
-
el: llmContextNewEl,
|
|
134
|
-
summary: newChatLlmSummary,
|
|
135
|
-
kind: NEW_SESSION,
|
|
136
|
-
choice: () => newChatLlmContext,
|
|
137
|
-
choose: (next) => {
|
|
138
|
-
newChatLlmContext = next;
|
|
139
|
-
renderNewChatLlmContext();
|
|
140
|
-
focusPrompt();
|
|
141
|
-
},
|
|
142
|
-
id: "llmContextNewPopover",
|
|
143
|
-
anchor: "#llmContextNew",
|
|
144
|
-
});
|
|
145
119
|
// The open session's thinking chip, beside the llm ctx one: /thinking runs
|
|
146
120
|
// in the session's worker.
|
|
147
121
|
const thinkingControl = createThinkingControl({
|
|
@@ -160,41 +134,26 @@ const reasoningHistoryControl = createReasoningHistoryControl({
|
|
|
160
134
|
running: () => liveTurn.running,
|
|
161
135
|
run: (line) => runSessionCommand(line),
|
|
162
136
|
});
|
|
163
|
-
// The start page's
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
137
|
+
// The start page's model picker and its LLM context and thinking chips
|
|
138
|
+
// (start_page.js); a fresh chain link's picker too.
|
|
139
|
+
const startPage = createStartPage({
|
|
140
|
+
pickEl: $("#modelPick"),
|
|
141
|
+
panelEl: $("#modelPanel"),
|
|
142
|
+
searchEl: $("#modelSearch"),
|
|
143
|
+
listEl: $("#modelList"),
|
|
144
|
+
composerEl: $("#composer"),
|
|
145
|
+
centerEl: $("#center"),
|
|
146
|
+
llmContextEl: $("#llmContextNew"),
|
|
147
|
+
thinkingEl: $("#thinkingNew"),
|
|
148
|
+
selected: () => selected,
|
|
149
|
+
state: () => current,
|
|
150
|
+
running: () => liveTurn.running,
|
|
151
|
+
sessions: () => allSessions,
|
|
152
|
+
run: (line) => runSessionCommand(line),
|
|
153
|
+
focusPrompt: () => focusPrompt(),
|
|
154
|
+
toast: (text) => showToast(text),
|
|
155
|
+
listModels,
|
|
180
156
|
});
|
|
181
|
-
function renderNewChatThinking() {
|
|
182
|
-
const chip = thinkingChipModel(withThinkingChoice(newChatThinkingSummary(), newChatThinking), { kind: NEW_SESSION });
|
|
183
|
-
thinkingNewEl.hidden = !chip;
|
|
184
|
-
if (!chip) return thinkingNewControl.close();
|
|
185
|
-
thinkingNewEl.textContent = chip.text;
|
|
186
|
-
thinkingNewEl.title = chip.title;
|
|
187
|
-
thinkingNewEl.classList.toggle("own", chip.own);
|
|
188
|
-
}
|
|
189
|
-
// Hidden without the model's values (the list not in, an older server).
|
|
190
|
-
function renderNewChatLlmContext() {
|
|
191
|
-
const chip = llmContextChipModel(withChoice(newChatLlmSummary(), newChatLlmContext), { kind: NEW_SESSION });
|
|
192
|
-
llmContextNewEl.hidden = !chip;
|
|
193
|
-
if (!chip) return llmContextNewControl.close();
|
|
194
|
-
llmContextNewEl.textContent = chip.text;
|
|
195
|
-
llmContextNewEl.title = chip.title;
|
|
196
|
-
llmContextNewEl.classList.toggle("own", chip.own);
|
|
197
|
-
}
|
|
198
157
|
const markdownWarningEl = $("#markdownWarning");
|
|
199
158
|
// What the page renders that a notice may stand in for (the server's
|
|
200
159
|
// Web::Capabilities, next to markdown_warning); a notice it names isn't
|
|
@@ -210,7 +169,6 @@ const cancelBtn = $("#cancelBtn");
|
|
|
210
169
|
const attachBtn = $("#attachBtn");
|
|
211
170
|
const attachInput = $("#attachInput");
|
|
212
171
|
const infoCancelBtn = $("#infoCancelBtn");
|
|
213
|
-
const modelPickEl = $("#modelPick");
|
|
214
172
|
const composeHintEl = $("#composeHint");
|
|
215
173
|
|
|
216
174
|
let selected = null;
|
|
@@ -506,27 +464,28 @@ const lists = createListsView({
|
|
|
506
464
|
leaveAllView: (hash) => leaveAllView(hash),
|
|
507
465
|
});
|
|
508
466
|
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
467
|
+
// Continue, Archive, Delete and Stop, and the start page when the open
|
|
468
|
+
// session is gone (session_actions.js).
|
|
469
|
+
const sessionActions = createSessionActions({
|
|
470
|
+
selected: () => selected,
|
|
471
|
+
state: () => current,
|
|
472
|
+
sessions: () => allSessions,
|
|
473
|
+
setSessions: (list) => { allSessions = list; },
|
|
474
|
+
api: { continueSession, archiveSession, unarchiveSession, deleteSession, stopSession },
|
|
475
|
+
eventsLive: () => !!eventsControl,
|
|
476
|
+
refresh,
|
|
477
|
+
lists,
|
|
478
|
+
select,
|
|
479
|
+
clearSelection,
|
|
480
|
+
showNotice,
|
|
481
|
+
updateInfoBar,
|
|
482
|
+
toast: showToast,
|
|
483
|
+
addCreatedSession,
|
|
484
|
+
fillOpener,
|
|
485
|
+
closeLiveStream: () => streamControl?.close(),
|
|
486
|
+
workerGone,
|
|
487
|
+
});
|
|
488
|
+
const { confirmDelete, leaveGoneSession, continueSelected, archiveOne, setArchived } = sessionActions;
|
|
530
489
|
|
|
531
490
|
// All sessions: its own view at #/sessions, so the browser's back button
|
|
532
491
|
// leaves it. Opened from the page, it closes with history.back().
|
|
@@ -616,24 +575,13 @@ function updateComposerMode() {
|
|
|
616
575
|
if (document.body.classList.toggle("has-session", !!selected) !== hadSession) {
|
|
617
576
|
hadSession = !hadSession;
|
|
618
577
|
// Back on the start page: its remembered model, not the last link's.
|
|
619
|
-
if (!hadSession) restoreStartPick();
|
|
578
|
+
if (!hadSession) startPage.restoreStartPick();
|
|
620
579
|
fitPrompt();
|
|
621
580
|
placeToast();
|
|
622
581
|
}
|
|
623
|
-
syncModelPick();
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
// The start page's choice was for a new session: opening a session drops it.
|
|
627
|
-
if (newChatLlmContext) {
|
|
628
|
-
newChatLlmContext = null;
|
|
629
|
-
renderNewChatLlmContext();
|
|
630
|
-
}
|
|
631
|
-
thinkingNewControl.close();
|
|
632
|
-
if (newChatThinking) {
|
|
633
|
-
newChatThinking = null;
|
|
634
|
-
renderNewChatThinking();
|
|
635
|
-
}
|
|
636
|
-
}
|
|
582
|
+
startPage.syncModelPick();
|
|
583
|
+
// The start page's choice was for a new session: opening a session drops it.
|
|
584
|
+
if (selected) startPage.dropChoices();
|
|
637
585
|
const running = liveTurn.running && !!selected;
|
|
638
586
|
cancelBtn.style.display = running ? "" : "none";
|
|
639
587
|
// No images for a session a terminal owns (it takes no message from here);
|
|
@@ -768,13 +716,6 @@ function clearNotice() {
|
|
|
768
716
|
}
|
|
769
717
|
promptEl.addEventListener("input", clearNotice);
|
|
770
718
|
|
|
771
|
-
// The open session no longer exists: back to the start page, saying why.
|
|
772
|
-
function leaveGoneSession(id, why) {
|
|
773
|
-
if (selected !== id) return;
|
|
774
|
-
clearSelection();
|
|
775
|
-
showNotice(goneSessionNotice(id, why));
|
|
776
|
-
}
|
|
777
|
-
|
|
778
719
|
async function select(id) {
|
|
779
720
|
clearNotice();
|
|
780
721
|
selected = id;
|
|
@@ -1088,7 +1029,7 @@ async function restartWorker() {
|
|
|
1088
1029
|
function updateInfoBar() {
|
|
1089
1030
|
const { buttons, bar } = sessionBar({
|
|
1090
1031
|
id: selected, state: current, sessions: allSessions, versions, cardTitle: (s) => lists.cardTitle(s),
|
|
1091
|
-
continuing:
|
|
1032
|
+
continuing: sessionActions.continuing, restarting: restartingIds, streaming: !!streamControl, running: liveTurn.running,
|
|
1092
1033
|
now: Date.now(),
|
|
1093
1034
|
});
|
|
1094
1035
|
const stopBtn = document.getElementById("infoStopBtn");
|
|
@@ -1117,7 +1058,7 @@ function updateInfoBar() {
|
|
|
1117
1058
|
updateSessionTitle();
|
|
1118
1059
|
// A fresh link's picker follows what the bar follows: its model, its
|
|
1119
1060
|
// owner, a next link continued from elsewhere.
|
|
1120
|
-
syncModelPick();
|
|
1061
|
+
startPage.syncModelPick();
|
|
1121
1062
|
}
|
|
1122
1063
|
|
|
1123
1064
|
// The open session's title, once on screen (session_title.js): its strip
|
|
@@ -1214,10 +1155,6 @@ function startStream(id, fromSeq) {
|
|
|
1214
1155
|
updateInfoBar();
|
|
1215
1156
|
}
|
|
1216
1157
|
|
|
1217
|
-
// The session this tab is stopping: its stream may close before the stop
|
|
1218
|
-
// request returns.
|
|
1219
|
-
let stoppingId = null;
|
|
1220
|
-
|
|
1221
1158
|
// +opened+ false: the stream never delivered a frame (the proxy answered
|
|
1222
1159
|
// 503, or the bridge refused it), so a retry waits a moment rather than
|
|
1223
1160
|
// spinning through resync → stream → error.
|
|
@@ -1226,7 +1163,7 @@ function workerGone(id, { opened = true } = {}) {
|
|
|
1226
1163
|
streamControl = null;
|
|
1227
1164
|
if (id !== selected) return;
|
|
1228
1165
|
// A turn that was running is over: its worker can't finish it.
|
|
1229
|
-
const stopped = stoppingId === id;
|
|
1166
|
+
const stopped = sessionActions.stoppingId() === id;
|
|
1230
1167
|
const endText = workerGoneText({ turnRunning: liveTurn.running, stopped });
|
|
1231
1168
|
if (endText) {
|
|
1232
1169
|
endTurnView("gone", endText, { stopped });
|
|
@@ -1287,38 +1224,19 @@ const streamHandlers = createStreamHandlers({
|
|
|
1287
1224
|
promptEl.value = text;
|
|
1288
1225
|
fitPrompt();
|
|
1289
1226
|
},
|
|
1290
|
-
setChips
|
|
1227
|
+
setChips,
|
|
1291
1228
|
},
|
|
1292
1229
|
getSession,
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
applyFinalMarkdown: (messages) => applyFinalMarkdown(messages),
|
|
1304
|
-
refreshTimingFromSession: () => refreshTimingFromSession(),
|
|
1305
|
-
makeUserBubble: (prompt, opts) => makeUserBubble(prompt, opts),
|
|
1306
|
-
addEndBubble: (text) => addEndBubble(text),
|
|
1307
|
-
commandBubble: (id) => commandBubble(id),
|
|
1308
|
-
addCommandBubble: (view) => addCommandBubble(view),
|
|
1309
|
-
addNoteBubble: (note) => addNoteBubble(note),
|
|
1310
|
-
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
1311
|
-
setUsedMemories: (names) => setUsedMemories(names),
|
|
1312
|
-
showRecap: (text) => showRecap(text),
|
|
1313
|
-
showGuardrailWarning: (text, label) => showGuardrailWarning(text, label),
|
|
1314
|
-
showHookNotice: (data) => showHookNotice(data),
|
|
1315
|
-
onInitEvent: (type, data) => onInitEvent(type, data),
|
|
1316
|
-
upsertCard: (card) => upsertCard(card),
|
|
1317
|
-
refreshCardBodies: () => refreshCardBodies(),
|
|
1318
|
-
syncModelPick: () => {
|
|
1319
|
-
if (pendingPick) pickRanEarly = true;
|
|
1320
|
-
syncModelPick();
|
|
1321
|
-
},
|
|
1230
|
+
// The page's hooks, passed as they are: function declarations (hoisted)
|
|
1231
|
+
// and the views' methods bound above.
|
|
1232
|
+
resync, updateInfoBar, setTurnRunning, setLiveStatus, endTurnView, readTurnEnd, refreshTimingFromSession,
|
|
1233
|
+
setUsedMemories, onInitEvent, findInTurnRoots, applyFinalMarkdown,
|
|
1234
|
+
// bubbles.js
|
|
1235
|
+
applyPromptOps, showSteers, markRecapStale, appendChunk, makeUserBubble, addEndBubble, commandBubble,
|
|
1236
|
+
addCommandBubble, addNoteBubble, showRecap, showGuardrailWarning, showHookNotice,
|
|
1237
|
+
// cards_place.js
|
|
1238
|
+
upsertCard, refreshCardBodies,
|
|
1239
|
+
syncModelPick: startPage.commandRan,
|
|
1322
1240
|
});
|
|
1323
1241
|
|
|
1324
1242
|
async function refreshTimingFromSession() {
|
|
@@ -1350,150 +1268,6 @@ function addCreatedSession(created) {
|
|
|
1350
1268
|
lists.render();
|
|
1351
1269
|
}
|
|
1352
1270
|
|
|
1353
|
-
// A first message: an idle session, then the turn (or the command) into it
|
|
1354
|
-
// as for any later one (handleSendTurn), so a failed first turn's prompt
|
|
1355
|
-
// comes back into the composer too (sentIds).
|
|
1356
|
-
function handleCreate() {
|
|
1357
|
-
return handleSendTurn();
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// +delivery+: the composer's choice for a running turn (Alt+Enter: queue;
|
|
1361
|
-
// a delivery-line link: cut or queue); else the armed one; a typed /cut or
|
|
1362
|
-
// /queue wins over both.
|
|
1363
|
-
async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
1364
|
-
if (current.owner === "tui") return;
|
|
1365
|
-
// A fresh link's pick first: its /model is queued in the worker once
|
|
1366
|
-
// acked, and the worker runs queued commands before a turn's input.
|
|
1367
|
-
if (pendingPick) await pendingPick;
|
|
1368
|
-
let prompt = turnText(promptEl.value, chips);
|
|
1369
|
-
if (!prompt) return;
|
|
1370
|
-
const armed = deliveryControl.take();
|
|
1371
|
-
// An image-only send's text is its placeholders: not for ↑.
|
|
1372
|
-
const typed = promptEl.value.trim() !== "";
|
|
1373
|
-
// The composer holds the line as typed until the ack (composerAfterAck).
|
|
1374
|
-
const line = prompt;
|
|
1375
|
-
// Where the line goes (composer.js): the page's own reply, a session to
|
|
1376
|
-
// make first (the start page), a command, or a turn.
|
|
1377
|
-
let route = routeComposerLine(prompt, {
|
|
1378
|
-
session: !!selected, running: liveTurn.running, commands: current.commands, images: chips.length,
|
|
1379
|
-
});
|
|
1380
|
-
if (route.create) {
|
|
1381
|
-
actionBtn.disabled = true;
|
|
1382
|
-
try {
|
|
1383
|
-
const s = await createIdleSession({ dir: scope.dir, model: chosenModel(), preview: route.send,
|
|
1384
|
-
llm_context: choiceWords(newChatLlmContext),
|
|
1385
|
-
thinking: newChatThinking || undefined });
|
|
1386
|
-
// The choice was for this chat only.
|
|
1387
|
-
newChatLlmContext = null;
|
|
1388
|
-
renderNewChatLlmContext();
|
|
1389
|
-
newChatThinking = null;
|
|
1390
|
-
renderNewChatThinking();
|
|
1391
|
-
addCreatedSession(s);
|
|
1392
|
-
await select(s.id);
|
|
1393
|
-
} catch (e) {
|
|
1394
|
-
alert(e.message);
|
|
1395
|
-
return;
|
|
1396
|
-
} finally {
|
|
1397
|
-
actionBtn.disabled = false;
|
|
1398
|
-
}
|
|
1399
|
-
// In the new session, by its command list (select read it).
|
|
1400
|
-
route = routeSessionLine(route.send, { commands: current.commands, images: chips.length, delivery: route.delivery });
|
|
1401
|
-
}
|
|
1402
|
-
if (route.reply) {
|
|
1403
|
-
// The start page's reply is a notice: no session is made for it (2.29).
|
|
1404
|
-
if (selected) addCommandBubble({ label: null, line: route.line, text: route.reply });
|
|
1405
|
-
else showNotice(route.reply);
|
|
1406
|
-
clearComposer();
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (route.command) return composerCommands.send(route.command);
|
|
1410
|
-
prompt = route.send;
|
|
1411
|
-
const running = liveTurn.running && !!selected;
|
|
1412
|
-
const delivery = route.delivery || (running ? chosen || armed : null);
|
|
1413
|
-
const hint = historyEl.querySelector(".hint");
|
|
1414
|
-
if (hint) hint.remove();
|
|
1415
|
-
// Sending moves a finished turn out of the stage at once (else the echo
|
|
1416
|
-
// would land among its rows); into a running one it is a queued row there.
|
|
1417
|
-
turnDom.handOff?.({ immediate: true });
|
|
1418
|
-
const sending = chips.slice();
|
|
1419
|
-
// Into a running turn the echo says what it waits for (its delivery); a
|
|
1420
|
-
// line with images always waits for the turn's end (the worker queues it).
|
|
1421
|
-
const shownDelivery = running ? (sending.length ? "queue" : delivery || "next_step") : null;
|
|
1422
|
-
const echo = makeUserBubble(prompt, { state: "queued", own: true, images: sending, delivery: shownDelivery });
|
|
1423
|
-
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1424
|
-
if (!current.firstPreview || current.firstPreview==="—") {
|
|
1425
|
-
current.firstPreview = firstPreview(prompt);
|
|
1426
|
-
updateInfoBar();
|
|
1427
|
-
}
|
|
1428
|
-
actionBtn.disabled = true;
|
|
1429
|
-
// The session the line goes to: the page may switch to another while an
|
|
1430
|
-
// image uploads or the turn is sent, and what follows stays with this one.
|
|
1431
|
-
const sentTo = selected;
|
|
1432
|
-
try {
|
|
1433
|
-
await uploadChips(sending, sentTo, { upload: uploadImage });
|
|
1434
|
-
const ack = await sendTurn(sentTo, prompt, { clientId, images: turnRefs(sending), history: typed, delivery });
|
|
1435
|
-
if (ack?.command_id) {
|
|
1436
|
-
// The worker knew it as a command (its list was newer than ours):
|
|
1437
|
-
// its command_ran shows it, not a prompt bubble.
|
|
1438
|
-
echo.remove();
|
|
1439
|
-
clearComposer();
|
|
1440
|
-
if (sentTo === selected) followWokenWorker();
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
if (ack?.enqueued_id) sentIds.add(ack.enqueued_id);
|
|
1444
|
-
// A cut the worker didn't take (no thinking-only step: cut off) goes in
|
|
1445
|
-
// at the next step; a queue whose turn ended meanwhile is the next turn.
|
|
1446
|
-
if (ack?.cut === "off" || (ack?.delivery === "next_step" && echo.dataset.delivery === "queue")) {
|
|
1447
|
-
setBubbleDelivery(echo, ack.cut === "off" ? "next_step" : null);
|
|
1448
|
-
}
|
|
1449
|
-
refreshHistory();
|
|
1450
|
-
// A turn that failed fast was restored before this ack: its prompt
|
|
1451
|
-
// (and images) go back into the composer, without replacing text typed
|
|
1452
|
-
// since (4.13). The composer still holds the text just sent: it goes.
|
|
1453
|
-
const restored = restoreOnAck(ack?.enqueued_id, earlyRestores, { myId: clientId, sentIds });
|
|
1454
|
-
// A restored line must be seen: the composer opens for this visit.
|
|
1455
|
-
if (restored?.refill && sentTo === selected) openForVisit();
|
|
1456
|
-
promptEl.value = composerAfterAck({ sent: line, current: promptEl.value, refill: restored?.refill || "" });
|
|
1457
|
-
fitPrompt();
|
|
1458
|
-
// Only the sent chips go: images added since stay (they go up at their own send).
|
|
1459
|
-
const kept = chipsAfterSend(chips, sending);
|
|
1460
|
-
setChips(restored?.images?.length ? kept.concat(restoredChips(sentTo, restored.images)) : kept);
|
|
1461
|
-
// Its turn_enqueued usually tagged the echo already; the ack covers a
|
|
1462
|
-
// worker that wasn't live (no turn_enqueued then).
|
|
1463
|
-
if (ack?.enqueued_id && !echo.dataset.enqueuedId && !bubbleById(ack.enqueued_id)) {
|
|
1464
|
-
echo.dataset.enqueuedId = ack.enqueued_id;
|
|
1465
|
-
}
|
|
1466
|
-
// A live stream carries this turn already; restarting it would drop the
|
|
1467
|
-
// events in between. Without one, POST /turn just woke a worker: its
|
|
1468
|
-
// events start after the seq the session was rendered at (0 for a new
|
|
1469
|
-
// worker process). A switch since then opened the other session's own.
|
|
1470
|
-
if (sentTo === selected) followWokenWorker();
|
|
1471
|
-
} catch (e) {
|
|
1472
|
-
echo.remove();
|
|
1473
|
-
// The turn never reached the worker's queue: an early restore waiting
|
|
1474
|
-
// for its ack is never taken back (4.13), so drop it here. The prompt
|
|
1475
|
-
// is still in the composer, as the echo was removed.
|
|
1476
|
-
dropEarlyRestores(earlyRestores);
|
|
1477
|
-
// The chips stay (uploaded ones keep their ref, to sentTo) for another try: an
|
|
1478
|
-
// image refused (bad_image, too_large, bad_images) says why below.
|
|
1479
|
-
if (e.code === "owned_by_tui") {
|
|
1480
|
-
if (sentTo !== selected) return showToast(cantText(`send to ${sentTo.slice(0, 8)}`, e), null, { sticky: true });
|
|
1481
|
-
current.owner = "tui";
|
|
1482
|
-
updateComposerMode();
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
// A stale tab sent into a session deleted meanwhile: the start page
|
|
1486
|
-
// says so, and the line stays in the composer.
|
|
1487
|
-
if (e.code === "not_found") return leaveGoneSession(sentTo, "not_found");
|
|
1488
|
-
// A /queue to a worker that can't queue (it would steer): its words, the text kept.
|
|
1489
|
-
if (e.code === "delivery_unavailable" && sentTo === selected) return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1490
|
-
showToast(cantText("send", e), null, { sticky: true });
|
|
1491
|
-
} finally {
|
|
1492
|
-
actionBtn.disabled = current.owner === "tui";
|
|
1493
|
-
focusPrompt();
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
1271
|
// /model, !ls … from the composer, and a control's command (the llm ctx
|
|
1498
1272
|
// and thinking chips): composer.js.
|
|
1499
1273
|
const composerCommands = createComposerCommands({
|
|
@@ -1503,7 +1277,7 @@ const composerCommands = createComposerCommands({
|
|
|
1503
1277
|
followWokenWorker: () => followWokenWorker(),
|
|
1504
1278
|
refreshHistory: () => refreshHistory(),
|
|
1505
1279
|
failed: (line, text) => addCommandBubble({ label: null, line, text, failed: true }),
|
|
1506
|
-
// As
|
|
1280
|
+
// As send_turn.js says a turn's failure after a switch.
|
|
1507
1281
|
failedElsewhere: (line, text, id) => showToast(cantText(`run ${line} in ${id.slice(0, 8)}`, text), null, { sticky: true }),
|
|
1508
1282
|
ownedByTui: () => {
|
|
1509
1283
|
current.owner = "tui";
|
|
@@ -1521,6 +1295,51 @@ const composerCommands = createComposerCommands({
|
|
|
1521
1295
|
},
|
|
1522
1296
|
});
|
|
1523
1297
|
|
|
1298
|
+
// The composer's send (send_turn.js): a line into the open session, or a
|
|
1299
|
+
// new session first on the start page.
|
|
1300
|
+
const sendTurnControl = createSendTurn({
|
|
1301
|
+
session: () => selected,
|
|
1302
|
+
state: () => current,
|
|
1303
|
+
running: () => liveTurn.running,
|
|
1304
|
+
clientId,
|
|
1305
|
+
sentIds,
|
|
1306
|
+
earlyRestores,
|
|
1307
|
+
api: { createIdleSession, uploadImage, sendTurn },
|
|
1308
|
+
dir: scope.dir,
|
|
1309
|
+
startPage,
|
|
1310
|
+
takeDelivery: () => deliveryControl.take(),
|
|
1311
|
+
composer: {
|
|
1312
|
+
el: promptEl,
|
|
1313
|
+
button: actionBtn,
|
|
1314
|
+
chips: () => chips,
|
|
1315
|
+
setChips,
|
|
1316
|
+
fit: fitPrompt,
|
|
1317
|
+
clear: clearComposer,
|
|
1318
|
+
openForVisit: () => openForVisit(),
|
|
1319
|
+
focus: () => focusPrompt(),
|
|
1320
|
+
},
|
|
1321
|
+
historyEl,
|
|
1322
|
+
turnDom,
|
|
1323
|
+
makeUserBubble,
|
|
1324
|
+
bubbleById,
|
|
1325
|
+
addCommandBubble,
|
|
1326
|
+
addCreatedSession,
|
|
1327
|
+
select,
|
|
1328
|
+
showNotice,
|
|
1329
|
+
updateInfoBar,
|
|
1330
|
+
updateComposerMode,
|
|
1331
|
+
followWokenWorker,
|
|
1332
|
+
refreshHistory,
|
|
1333
|
+
leaveGoneSession,
|
|
1334
|
+
runCommand: (command) => composerCommands.send(command),
|
|
1335
|
+
toast: showToast,
|
|
1336
|
+
});
|
|
1337
|
+
// +delivery+: the composer's choice for a running turn (Alt+Enter: queue;
|
|
1338
|
+
// a delivery-line link: cut or queue); else the armed one.
|
|
1339
|
+
function handleSendTurn({ delivery = null } = {}) {
|
|
1340
|
+
return sendTurnControl.send({ delivery });
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1524
1343
|
function runSessionCommand(line) {
|
|
1525
1344
|
return composerCommands.run(line);
|
|
1526
1345
|
}
|
|
@@ -1552,132 +1371,10 @@ async function handleCancel() {
|
|
|
1552
1371
|
$("#newBtn").addEventListener("click", () => {
|
|
1553
1372
|
if (allViewOpen()) leaveAllView("");
|
|
1554
1373
|
clearSelection();
|
|
1555
|
-
loadModels({ asked: true });
|
|
1374
|
+
startPage.loadModels({ asked: true });
|
|
1556
1375
|
});
|
|
1557
1376
|
|
|
1558
|
-
|
|
1559
|
-
// The hosts' models from GET /api/models, the choice this browser made last
|
|
1560
|
-
// preselected while it is still offered (else the server's default). Hidden
|
|
1561
|
-
// until the list is in, and in a session (its model is in the info bar),
|
|
1562
|
-
// but a fresh chain link's (chains.js freshLink): there it shows the link's
|
|
1563
|
-
// model and a pick runs /model in the link, remembered neither as this
|
|
1564
|
-
// browser's choice nor in Recent. A host that failed is the picker's
|
|
1565
|
-
// tooltip; a route that fails leaves the picker hidden and the server's
|
|
1566
|
-
// default in charge. The search list itself is model_picker.js'; a pick
|
|
1567
|
-
// sends the focus to the message box.
|
|
1568
|
-
const START_PICK_TITLE = "The model the new session starts on";
|
|
1569
|
-
let startPickTitle = START_PICK_TITLE;
|
|
1570
|
-
// The fresh link the picker shows (null: the start page, or a session it
|
|
1571
|
-
// is hidden in), and a link pick's /model still in flight (a send waits).
|
|
1572
|
-
let pickLinkId = null;
|
|
1573
|
-
let pickSelected = null;
|
|
1574
|
-
let pendingPick = null;
|
|
1575
|
-
let pendingPickId = null;
|
|
1576
|
-
// A command_ran came while the pick was in flight (its sync was skipped):
|
|
1577
|
-
// the ack syncs then.
|
|
1578
|
-
let pickRanEarly = false;
|
|
1579
|
-
const modelPicker = createModelPicker({
|
|
1580
|
-
button: modelPickEl,
|
|
1581
|
-
panel: $("#modelPanel"),
|
|
1582
|
-
input: $("#modelSearch"),
|
|
1583
|
-
list: $("#modelList"),
|
|
1584
|
-
composer: $("#composer"),
|
|
1585
|
-
center: $("#center"),
|
|
1586
|
-
remember: () => !selected,
|
|
1587
|
-
onPick: (name) => {
|
|
1588
|
-
if (selected) pickForLink(name);
|
|
1589
|
-
else {
|
|
1590
|
-
renderNewChatLlmContext();
|
|
1591
|
-
renderNewChatThinking();
|
|
1592
|
-
}
|
|
1593
|
-
focusPrompt();
|
|
1594
|
-
},
|
|
1595
|
-
});
|
|
1596
|
-
// A pick on a fresh link: /model in its worker (its command bubble and the
|
|
1597
|
-
// info bar follow from command_ran); refused, the button goes back to the
|
|
1598
|
-
// link's model, if the page is still on that link.
|
|
1599
|
-
function pickForLink(name) {
|
|
1600
|
-
const id = selected;
|
|
1601
|
-
const run = runSessionCommand(`/model ${name}`).then((ok) => {
|
|
1602
|
-
const ranEarly = pendingPick === run && pickRanEarly;
|
|
1603
|
-
if (pendingPick === run) {
|
|
1604
|
-
pendingPick = null;
|
|
1605
|
-
pendingPickId = null;
|
|
1606
|
-
pickRanEarly = false;
|
|
1607
|
-
}
|
|
1608
|
-
if ((!ok || ranEarly) && selected === id) syncModelPick();
|
|
1609
|
-
return ok;
|
|
1610
|
-
});
|
|
1611
|
-
pendingPick = run;
|
|
1612
|
-
pendingPickId = id;
|
|
1613
|
-
pickRanEarly = false;
|
|
1614
|
-
}
|
|
1615
|
-
// The button's name, tooltip and label for what the page shows: a fresh
|
|
1616
|
-
// link's model, else the start page's (whose choice stays as it is).
|
|
1617
|
-
// Toggles the fresh-link class (the CSS shows the button in a session
|
|
1618
|
-
// only with it); the panel closes when the link changes or stops being fresh.
|
|
1619
|
-
function syncModelPick() {
|
|
1620
|
-
// The list walk only for a link that is fresh otherwise (the info bar
|
|
1621
|
-
// calls this every second while a turn runs).
|
|
1622
|
-
const fresh = !!selected && freshLink(current, { running: liveTurn.running })
|
|
1623
|
-
&& !chainNeighbours(allSessions, selected, { continues: current.continues }).next;
|
|
1624
|
-
document.body.classList.toggle("fresh-link", fresh);
|
|
1625
|
-
const linkId = fresh ? selected : null;
|
|
1626
|
-
// Another session (or the start page), or the link stopped being fresh:
|
|
1627
|
-
// a panel left open would come back open where it no longer belongs.
|
|
1628
|
-
if (selected !== pickSelected || linkId !== pickLinkId) modelPicker.close();
|
|
1629
|
-
pickSelected = selected;
|
|
1630
|
-
pickLinkId = linkId;
|
|
1631
|
-
if (fresh) {
|
|
1632
|
-
// A pick in flight keeps its name until its /model is taken (its
|
|
1633
|
-
// command_ran syncs again then).
|
|
1634
|
-
if (!(pendingPick && pendingPickId === selected)) modelPicker.show(current.model);
|
|
1635
|
-
modelPickEl.title = "The model this link starts on";
|
|
1636
|
-
modelPickEl.setAttribute("aria-label", "Model this link starts on");
|
|
1637
|
-
} else {
|
|
1638
|
-
modelPickEl.title = startPickTitle;
|
|
1639
|
-
modelPickEl.setAttribute("aria-label", "Model for the new session");
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
// Leaving a session: the start page's remembered model again (no fetch).
|
|
1643
|
-
function restoreStartPick() {
|
|
1644
|
-
modelPicker.restore(readStored(MODEL_KEY));
|
|
1645
|
-
renderNewChatLlmContext();
|
|
1646
|
-
renderNewChatThinking();
|
|
1647
|
-
}
|
|
1648
|
-
// A reload cuts the fetch: its error is no news (modelsFailedText).
|
|
1649
|
-
let pageLeaving = false;
|
|
1650
|
-
addEventListener("beforeunload", () => { pageLeaving = true; });
|
|
1651
|
-
addEventListener("pagehide", () => { pageLeaving = true; });
|
|
1652
|
-
addEventListener("pageshow", () => { pageLeaving = false; }); // back from the bfcache
|
|
1653
|
-
// +asked+: + new session's load, for the start page it opens.
|
|
1654
|
-
async function loadModels({ asked = false } = {}) {
|
|
1655
|
-
let payload;
|
|
1656
|
-
try {
|
|
1657
|
-
payload = await listModels();
|
|
1658
|
-
} catch (e) {
|
|
1659
|
-
const text = modelsFailedText(e, { startPage: !selected, asked, leaving: pageLeaving });
|
|
1660
|
-
if (text) showToast(text);
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
const stored = readStored(MODEL_KEY);
|
|
1664
|
-
if (!modelPicker.setModels(payload, stored)) return;
|
|
1665
|
-
startPickTitle = payload.warning ? `${START_PICK_TITLE} · ${payload.warning}` : START_PICK_TITLE;
|
|
1666
|
-
modelPickEl.hidden = false;
|
|
1667
|
-
// A fresh link open meanwhile shows its own model again, not the stored one.
|
|
1668
|
-
syncModelPick();
|
|
1669
|
-
renderNewChatLlmContext();
|
|
1670
|
-
renderNewChatThinking();
|
|
1671
|
-
}
|
|
1672
|
-
// The picker's choice for a create request; nothing (the server's default)
|
|
1673
|
-
// while the list is not in.
|
|
1674
|
-
function chosenModel() {
|
|
1675
|
-
return modelPickEl.hidden ? undefined : modelPicker.chosen() || undefined;
|
|
1676
|
-
}
|
|
1677
|
-
actionBtn.addEventListener("click", () => {
|
|
1678
|
-
if (!selected) handleCreate();
|
|
1679
|
-
else handleSendTurn();
|
|
1680
|
-
});
|
|
1377
|
+
actionBtn.addEventListener("click", () => handleSendTurn());
|
|
1681
1378
|
|
|
1682
1379
|
// ── Images: paste or drop into the composer, chips until sent ─────────────
|
|
1683
1380
|
const chipsEl = $("#chips");
|
|
@@ -1901,7 +1598,7 @@ promptEl.addEventListener("keydown", (e) => {
|
|
|
1901
1598
|
// An IME's Enter picks the composition, it doesn't send.
|
|
1902
1599
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
1903
1600
|
e.preventDefault();
|
|
1904
|
-
if (!selected)
|
|
1601
|
+
if (!selected) handleSendTurn();
|
|
1905
1602
|
// Alt/Option+Enter: queued for after the running turn (D6), only to a
|
|
1906
1603
|
// worker that queues (else it would steer): the text stays, a note says why.
|
|
1907
1604
|
else if (e.altKey && liveTurn.running && !current.workerFeatures.includes("delivery")) {
|
|
@@ -1909,81 +1606,12 @@ promptEl.addEventListener("keydown", (e) => {
|
|
|
1909
1606
|
} else handleSendTurn(e.altKey ? { delivery: "queue" } : {});
|
|
1910
1607
|
}
|
|
1911
1608
|
});
|
|
1912
|
-
$("#infoStopBtn").addEventListener("click",
|
|
1913
|
-
if (!selected) return;
|
|
1914
|
-
if (!confirm(`Stop session ${selected}?`)) return;
|
|
1915
|
-
const id = selected;
|
|
1916
|
-
stoppingId = id;
|
|
1917
|
-
try {
|
|
1918
|
-
await stopSession(id);
|
|
1919
|
-
if (id === selected) stoppedWorker(id);
|
|
1920
|
-
} catch (e) {
|
|
1921
|
-
// A refusal (a terminal holds it) or a worker that won't stop: said, as
|
|
1922
|
-
// a failed restart is.
|
|
1923
|
-
showToast(cantText(`stop ${id.slice(0, 8)}`, e), null, { sticky: true });
|
|
1924
|
-
} finally {
|
|
1925
|
-
stoppingId = null;
|
|
1926
|
-
}
|
|
1927
|
-
// The list hears of the stop from the hub (the stop route rescans it).
|
|
1928
|
-
if (!eventsControl) refresh();
|
|
1929
|
-
});
|
|
1609
|
+
$("#infoStopBtn").addEventListener("click", () => sessionActions.stopSelected());
|
|
1930
1610
|
$("#infoDeleteBtn").addEventListener("click", () => {
|
|
1931
1611
|
if (selected) confirmDelete(selected);
|
|
1932
1612
|
});
|
|
1933
1613
|
$("#infoContinueBtn").addEventListener("click", () => continueSelected());
|
|
1934
|
-
$("#infoArchiveBtn").addEventListener("click", () =>
|
|
1935
|
-
if (!selected) return;
|
|
1936
|
-
if (selectedArchived()) setArchived(selected, false);
|
|
1937
|
-
else setArchived(selected, true);
|
|
1938
|
-
});
|
|
1939
|
-
|
|
1940
|
-
// ── Continue (session chains) ───────────────────────────────────────────────
|
|
1941
|
-
// Continue → starts the next link of the open session's chain (the server
|
|
1942
|
-
// waits up to 30 s for its recap, archives it, starts the new one with the
|
|
1943
|
-
// recap as a note) and opens it with the opener in the composer, not sent
|
|
1944
|
-
// (fillOpener: the previous link's own first message when it continues one).
|
|
1945
|
-
// A link continued already opens that next link; its open delegates move
|
|
1946
|
-
// to the new link (the toast counts them); a refusal for open delegates
|
|
1947
|
-
// that can't move names them, each a link.
|
|
1948
|
-
// One continue per session at a time (the server locks it too); another
|
|
1949
|
-
// session's continue may run beside it.
|
|
1950
|
-
const continuingIds = new Set();
|
|
1951
|
-
|
|
1952
|
-
async function continueSelected() {
|
|
1953
|
-
if (!selected || continuingIds.has(selected)) return;
|
|
1954
|
-
const id = selected;
|
|
1955
|
-
const short = id.slice(0, 8);
|
|
1956
|
-
continuingIds.add(id);
|
|
1957
|
-
updateInfoBar();
|
|
1958
|
-
try {
|
|
1959
|
-
const created = await continueSession(id);
|
|
1960
|
-
markArchived([id], true);
|
|
1961
|
-
addCreatedSession(created);
|
|
1962
|
-
const next = created.id;
|
|
1963
|
-
// A model fallback's warning stays until closed.
|
|
1964
|
-
const sticky = !!created.model_warning;
|
|
1965
|
-
if (selected !== id) {
|
|
1966
|
-
showToast(continuedText(id, created, { opened: false }), { label: "Open", run: () => select(next) }, { sticky });
|
|
1967
|
-
return;
|
|
1968
|
-
}
|
|
1969
|
-
await select(next);
|
|
1970
|
-
if (selected === next) fillOpener(created);
|
|
1971
|
-
showToast(continuedText(id, created), null, { sticky });
|
|
1972
|
-
} catch (e) {
|
|
1973
|
-
const refusal = continueRefusal(e, allSessions);
|
|
1974
|
-
if (refusal.kind === "open") {
|
|
1975
|
-
if (selected === id) await select(refusal.id);
|
|
1976
|
-
showToast(`${short} is continued already: ${selected === refusal.id ? "opened" : "see"} ${refusal.id.slice(0, 8)}`);
|
|
1977
|
-
} else if (refusal.kind === "children") {
|
|
1978
|
-
showToast(openChildrenTemplate(id, refusal.children, sessionHash), null, { sticky: true });
|
|
1979
|
-
} else {
|
|
1980
|
-
showToast(`Can't continue ${short}: ${refusal.text}`, null, { sticky: true });
|
|
1981
|
-
}
|
|
1982
|
-
} finally {
|
|
1983
|
-
continuingIds.delete(id);
|
|
1984
|
-
updateInfoBar();
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1614
|
+
$("#infoArchiveBtn").addEventListener("click", () => sessionActions.toggleArchiveSelected());
|
|
1987
1615
|
|
|
1988
1616
|
// The new link's composer: the opener (openerText: the previous link's own,
|
|
1989
1617
|
// else the constant), unless a draft is there (it stays). +created+: the
|
|
@@ -1997,68 +1625,6 @@ function fillOpener(created) {
|
|
|
1997
1625
|
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
1998
1626
|
}
|
|
1999
1627
|
|
|
2000
|
-
// ── Archive ────────────────────────────────────────────────────────────────
|
|
2001
|
-
// Archive hides a session (and its delegates) from the strip and the list,
|
|
2002
|
-
// kept for good; "include archived" in all sessions finds it. No confirm:
|
|
2003
|
-
// the toast's Undo takes it back. Archiving the open session stays on it
|
|
2004
|
-
// (the button flips to unarchive); its worker was stopped by the server.
|
|
2005
|
-
|
|
2006
|
-
function selectedArchived() {
|
|
2007
|
-
return !!allSessions.find((s) => s.id === selected)?.archived;
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
// The ids in the server's answer flip here at once; the hub's events
|
|
2011
|
-
// follow with the same.
|
|
2012
|
-
function markArchived(ids, archived) {
|
|
2013
|
-
const set = new Set(ids);
|
|
2014
|
-
allSessions = allSessions.map((s) => (set.has(s.id) ? { ...s, archived } : s));
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// One archive / unarchive request, the core the info bar, a card's button
|
|
2018
|
-
// and a batch share. The ids in the answer flip in the list at once. When
|
|
2019
|
-
// the open session's worker was stopped (it, or a delegate of the archived
|
|
2020
|
-
// one), its stream ending is a stop, not a lost worker.
|
|
2021
|
-
// The open session's worker was stopped from here (the stop button, an
|
|
2022
|
-
// archive): don't wait for its stream to notice.
|
|
2023
|
-
function stoppedWorker(id) {
|
|
2024
|
-
streamControl?.close();
|
|
2025
|
-
current.status = "stopped";
|
|
2026
|
-
workerGone(id);
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
// @return {ok: true, result} | {ok: false, error}
|
|
2030
|
-
async function archiveOne(id, archive) {
|
|
2031
|
-
// A live worker in the tree is stopped by the archive: the open
|
|
2032
|
-
// session's stream ending then is a stop.
|
|
2033
|
-
const mine = archive && selected && inTree(allSessions, id, selected) ? selected : null;
|
|
2034
|
-
if (mine) stoppingId = mine;
|
|
2035
|
-
try {
|
|
2036
|
-
const result = archive ? await archiveSession(id) : await unarchiveSession(id);
|
|
2037
|
-
markArchived(archive ? result.archived : result.unarchived, archive);
|
|
2038
|
-
if (mine && mine === selected && (result.stopped || []).includes(mine)) stoppedWorker(mine);
|
|
2039
|
-
return { ok: true, result };
|
|
2040
|
-
} catch (error) {
|
|
2041
|
-
return { ok: false, error };
|
|
2042
|
-
} finally {
|
|
2043
|
-
// Another call may have taken it meanwhile: clear only our own.
|
|
2044
|
-
if (mine && stoppingId === mine) stoppingId = null;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
async function setArchived(id, archive, { undo = true } = {}) {
|
|
2049
|
-
const short = id.slice(0, 8);
|
|
2050
|
-
const answer = await archiveOne(id, archive);
|
|
2051
|
-
if (!answer.ok) {
|
|
2052
|
-
alert(`Could not ${archive ? "archive" : "unarchive"} session ${short}: ${answer.error.message}`);
|
|
2053
|
-
return;
|
|
2054
|
-
}
|
|
2055
|
-
lists.render();
|
|
2056
|
-
updateInfoBar();
|
|
2057
|
-
if (!eventsControl) refresh();
|
|
2058
|
-
const what = archive ? "Archived" : "Unarchived";
|
|
2059
|
-
showToast(`${what} ${short}`, undo ? { label: "Undo", run: () => setArchived(id, !archive, { undo: false }) } : null);
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
1628
|
// The toast sits above the session's composer card (measured: it grows with
|
|
2063
1629
|
// a wrapping footer or a taller box), so it never covers the card's
|
|
2064
1630
|
// controls, and above the stage while it shows and a fresh link's open
|
|
@@ -2191,7 +1757,7 @@ if (document.body.dataset.sessionsDir) {
|
|
|
2191
1757
|
renderScope();
|
|
2192
1758
|
updateInfoBar();
|
|
2193
1759
|
updateComposerMode();
|
|
2194
|
-
loadModels();
|
|
1760
|
+
startPage.loadModels();
|
|
2195
1761
|
// The list: the hub's stream when the browser has EventSource, else fetched.
|
|
2196
1762
|
if (typeof globalThis.EventSource === "function") openSessionEvents();
|
|
2197
1763
|
else refresh();
|