samagotchi 0.77.0 → 0.79.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 +40 -1
- data/docs/cli.md +2 -1
- data/docs/configuration.md +14 -8
- data/docs/internals/anthropic.md +80 -7
- data/docs/internals/engine.md +3 -2
- data/docs/internals/prompt-caching.md +3 -1
- data/docs/sessions.md +1 -0
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_move.rb +2 -2
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +2 -2
- data/lib/samagotchi/llm/adapter_common.rb +4 -2
- data/lib/samagotchi/llm/anthropic_assembly.rb +14 -0
- data/lib/samagotchi/llm/anthropic_binding_recovery.rb +195 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +74 -17
- data/lib/samagotchi/llm/anthropic_wire.rb +29 -1
- data/lib/samagotchi/llm/chat_loop.rb +61 -5
- data/lib/samagotchi/llm/errors.rb +6 -4
- data/lib/samagotchi/llm/http.rb +2 -1
- data/lib/samagotchi/llm/model_listing.rb +29 -1
- data/lib/samagotchi/llm/openai_chat.rb +12 -2
- data/lib/samagotchi/llm/refusal_memo.rb +3 -2
- data/lib/samagotchi/note_delivery.rb +2 -2
- data/lib/samagotchi/plugin/sessions.rb +2 -2
- data/lib/samagotchi/question_desk.rb +10 -3
- data/lib/samagotchi/reasoning_history.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +2 -3
- data/lib/samagotchi/session_manager.rb +0 -1
- data/lib/samagotchi/session_metrics/totals.rb +4 -3
- data/lib/samagotchi/session_metrics.rb +9 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +2 -2
- data/lib/samagotchi/tools/delegate_result.rb +2 -2
- data/lib/samagotchi/tools/list_sessions.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +61 -53
- data/lib/samagotchi/web/public/composer_fold.js +58 -0
- data/lib/samagotchi/web/public/composer_grip.js +231 -0
- data/lib/samagotchi/web/public/composer_size.js +10 -0
- data/lib/samagotchi/web/public/index.html +27 -3
- data/lib/samagotchi/web/public/model_picker.js +4 -5
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/storage.js +23 -0
- data/lib/samagotchi/web/public/toasts.js +5 -4
- data/lib/samagotchi/worker.rb +1 -0
- metadata +5 -1
|
@@ -144,6 +144,9 @@ module Samagotchi
|
|
|
144
144
|
# (finish_reason length). `retries` counts the network's only.
|
|
145
145
|
:cuts,
|
|
146
146
|
:capped,
|
|
147
|
+
# Replayed thinking blocks a preserved-thinking recovery dropped
|
|
148
|
+
# (:generation_completed's thinking_dropped, ChatLoop).
|
|
149
|
+
:thinking_dropped,
|
|
147
150
|
:model,
|
|
148
151
|
# The model the turn ran on (asked host:model, :generation_started's
|
|
149
152
|
# model_ref; the first generation's), and whether its open generation
|
|
@@ -505,6 +508,7 @@ module Samagotchi
|
|
|
505
508
|
retries: totals.retries,
|
|
506
509
|
cuts: totals.cuts,
|
|
507
510
|
capped: totals.capped,
|
|
511
|
+
thinking_dropped: totals.thinking_dropped,
|
|
508
512
|
started_at: @started_at,
|
|
509
513
|
last_activity_at: @last_activity_at,
|
|
510
514
|
# From the session's first start (an earlier process's too), to now.
|
|
@@ -589,6 +593,7 @@ module Samagotchi
|
|
|
589
593
|
retries: 0,
|
|
590
594
|
cuts: 0,
|
|
591
595
|
capped: 0,
|
|
596
|
+
thinking_dropped: 0,
|
|
592
597
|
model: nil,
|
|
593
598
|
model_ref: nil,
|
|
594
599
|
gen_switch: false,
|
|
@@ -679,12 +684,15 @@ module Samagotchi
|
|
|
679
684
|
end
|
|
680
685
|
end
|
|
681
686
|
|
|
687
|
+
# A generation's cut, output cap and dropped thinking blocks, into the turn.
|
|
682
688
|
def count_cut(event)
|
|
683
689
|
@mutex.synchronize do
|
|
684
690
|
next unless @turn
|
|
685
691
|
|
|
686
692
|
@turn.cuts += 1 if event[:stopped_by] && event[:stopped_by].to_s != "steer"
|
|
687
693
|
@turn.capped += 1 if event[:finish_reason].to_s == "length"
|
|
694
|
+
dropped = event[:thinking_dropped]
|
|
695
|
+
@turn.thinking_dropped += dropped if dropped.is_a?(Integer) && dropped.positive?
|
|
688
696
|
end
|
|
689
697
|
end
|
|
690
698
|
|
|
@@ -872,6 +880,7 @@ module Samagotchi
|
|
|
872
880
|
}
|
|
873
881
|
fields[:cache_write_tokens_sum] = turn.cache_write_sum if turn.cache_write_sum.positive?
|
|
874
882
|
fields[:reprefill_tokens_sum] = turn.reprefill_sum if turn.reprefill_sum.positive?
|
|
883
|
+
fields[:thinking_dropped] = turn.thinking_dropped if turn.thinking_dropped.positive?
|
|
875
884
|
fields.merge!(cost: turn.cost_sum, cost_source: "reported") if turn.cost_sum
|
|
876
885
|
fields[:cost_estimate] = turn.cost_estimate_sum if turn.cost_estimate_sum.positive?
|
|
877
886
|
fields[:switch_prefill_tokens] = turn.switch_prefill_tokens if turn.switch_prefill_tokens
|
|
@@ -600,6 +600,9 @@ module Samagotchi
|
|
|
600
600
|
# A looped session: loop-guard's cuts, or thinking that ran to the cap.
|
|
601
601
|
lines << "thinking cuts: #{snapshot[:cuts].to_i}"
|
|
602
602
|
lines << "output cap hits: #{snapshot[:capped].to_i}"
|
|
603
|
+
# Claude 5.x: replayed thinking a prefix change made the API drop.
|
|
604
|
+
dropped = snapshot[:thinking_dropped].to_i
|
|
605
|
+
lines << "thinking dropped: #{dropped} blocks (preserved-thinking recovery)" if dropped.positive?
|
|
603
606
|
context = snapshot[:context] || {}
|
|
604
607
|
if context[:used_tokens]
|
|
605
608
|
pct = context_pct_text(context, snapshot[:llm_context])
|
|
@@ -42,8 +42,8 @@ require_relative "children_status"
|
|
|
42
42
|
require_relative "log"
|
|
43
43
|
|
|
44
44
|
module Samagotchi
|
|
45
|
-
# Loaded on first use:
|
|
46
|
-
#
|
|
45
|
+
# Loaded on first use: this autoload keeps SessionManager and its ~80
|
|
46
|
+
# files unloaded until the delegate/session path needs them.
|
|
47
47
|
autoload :SessionManager, File.expand_path("session_manager", __dir__)
|
|
48
48
|
|
|
49
49
|
# The in-process terminal front end (`chi --no-shared`, `chi scratch`,
|
|
@@ -10,8 +10,8 @@ require_relative "delegate_wait"
|
|
|
10
10
|
require_relative "../child_reports"
|
|
11
11
|
|
|
12
12
|
module Samagotchi
|
|
13
|
-
# Loaded on first use:
|
|
14
|
-
#
|
|
13
|
+
# Loaded on first use: this autoload keeps SessionManager and its ~80
|
|
14
|
+
# files unloaded until the delegate/session path needs them.
|
|
15
15
|
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
16
16
|
|
|
17
17
|
module Tools
|
|
@@ -6,8 +6,8 @@ require_relative "delegate"
|
|
|
6
6
|
require_relative "delegate_wait"
|
|
7
7
|
|
|
8
8
|
module Samagotchi
|
|
9
|
-
# Loaded on first use:
|
|
10
|
-
#
|
|
9
|
+
# Loaded on first use: this autoload keeps SessionManager and its ~80
|
|
10
|
+
# files unloaded until the delegate/session path needs them.
|
|
11
11
|
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
12
12
|
|
|
13
13
|
module Tools
|
|
@@ -6,8 +6,8 @@ require_relative "../session"
|
|
|
6
6
|
require_relative "peers"
|
|
7
7
|
|
|
8
8
|
module Samagotchi
|
|
9
|
-
# Loaded on first use:
|
|
10
|
-
#
|
|
9
|
+
# Loaded on first use: this autoload keeps SessionManager and its ~80
|
|
10
|
+
# files unloaded until the delegate/session path needs them.
|
|
11
11
|
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
12
12
|
|
|
13
13
|
module Tools
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -56,7 +56,9 @@ import { actionLine, annotationActions, composerWithAction } from "./annotate_ac
|
|
|
56
56
|
import { copyText, installCopy } from "./copy.js";
|
|
57
57
|
import { createQuestionCards } from "./question_cards.js";
|
|
58
58
|
import { splitSnapshotCards } from "./card.js";
|
|
59
|
-
import { PROMPT_LINE_PX, PROMPT_MIN_PX,
|
|
59
|
+
import { PROMPT_LINE_PX, PROMPT_MIN_PX, promptHeight } from "./composer_size.js";
|
|
60
|
+
import { GRIP_KEY, createComposerGrip } from "./composer_grip.js";
|
|
61
|
+
import { readStored, writeStored } from "./storage.js";
|
|
60
62
|
import { createCommandList } from "./command_list.js";
|
|
61
63
|
import { createPromptHistory } from "./prompt_history.js";
|
|
62
64
|
import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
@@ -64,6 +66,7 @@ import { scopeDir, scopeTemplate } from "./scope.js";
|
|
|
64
66
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
65
67
|
import { easeScrollTo, FOLLOW_MS, shouldFollowScroll } from "./scroll.js";
|
|
66
68
|
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
69
|
+
import { waitingOn } from "./notify.js";
|
|
67
70
|
import { applySessionEvent, inTree, sortedByUpdated } from "./sessions_list.js";
|
|
68
71
|
import { createNotifications } from "./notifications.js";
|
|
69
72
|
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
@@ -132,7 +135,7 @@ const llmContextNewControl = createLlmContextControl({
|
|
|
132
135
|
choose: (next) => {
|
|
133
136
|
newChatLlmContext = next;
|
|
134
137
|
renderNewChatLlmContext();
|
|
135
|
-
|
|
138
|
+
focusPrompt();
|
|
136
139
|
},
|
|
137
140
|
id: "llmContextNewPopover",
|
|
138
141
|
anchor: "#llmContextNew",
|
|
@@ -168,7 +171,7 @@ const thinkingNewControl = createThinkingControl({
|
|
|
168
171
|
choose: (next) => {
|
|
169
172
|
newChatThinking = next;
|
|
170
173
|
renderNewChatThinking();
|
|
171
|
-
|
|
174
|
+
focusPrompt();
|
|
172
175
|
},
|
|
173
176
|
id: "thinkingNewPopover",
|
|
174
177
|
anchor: "#thinkingNew",
|
|
@@ -202,6 +205,7 @@ let pageEditorUrl = null;
|
|
|
202
205
|
const promptEl = $("#prompt");
|
|
203
206
|
const actionBtn = $("#actionBtn");
|
|
204
207
|
const cancelBtn = $("#cancelBtn");
|
|
208
|
+
const infoCancelBtn = $("#infoCancelBtn");
|
|
205
209
|
const modelPickEl = $("#modelPick");
|
|
206
210
|
const composeHintEl = $("#composeHint");
|
|
207
211
|
|
|
@@ -739,9 +743,10 @@ function clearSelection() {
|
|
|
739
743
|
closeStream();
|
|
740
744
|
setMarkdownWarning(null);
|
|
741
745
|
setCapabilities(null, null);
|
|
746
|
+
composerGrip.newVisit();
|
|
742
747
|
updateComposerMode();
|
|
743
748
|
updateInfoBar();
|
|
744
|
-
|
|
749
|
+
focusPrompt();
|
|
745
750
|
}
|
|
746
751
|
|
|
747
752
|
// A short line on the start page (the open session went away); gone at the
|
|
@@ -774,11 +779,13 @@ async function select(id) {
|
|
|
774
779
|
closeStream();
|
|
775
780
|
resetSessionState();
|
|
776
781
|
resetTurnView();
|
|
782
|
+
composerGrip.newVisit();
|
|
777
783
|
updateInfoBar();
|
|
778
784
|
contextChips.load(id);
|
|
779
785
|
updateComposerMode();
|
|
780
786
|
await resync();
|
|
781
|
-
|
|
787
|
+
// A folded composer stays folded: typing on the page opens it.
|
|
788
|
+
focusPrompt({ open: false });
|
|
782
789
|
}
|
|
783
790
|
|
|
784
791
|
// Clear everything that belongs to one turn's rendering.
|
|
@@ -815,6 +822,9 @@ async function resync({ endText = null } = {}) {
|
|
|
815
822
|
return;
|
|
816
823
|
}
|
|
817
824
|
if (id !== selected) return;
|
|
825
|
+
// An archived session opens folded: decided once per visit, from its
|
|
826
|
+
// first answer (the list may not hold it yet, or ever under ?dir).
|
|
827
|
+
composerGrip.decideVisit(data.session?.archived);
|
|
818
828
|
const kept = keptCommandBubbles(historyEl, id);
|
|
819
829
|
resetTurnView();
|
|
820
830
|
renderSessionView(data, { kept });
|
|
@@ -1089,6 +1099,7 @@ function updateInfoBar() {
|
|
|
1089
1099
|
archiveBtn.title = buttons.archive.title;
|
|
1090
1100
|
}
|
|
1091
1101
|
if (stopBtn) stopBtn.style.display = buttons.stop.shown ? "" : "none";
|
|
1102
|
+
drawFoldControls();
|
|
1092
1103
|
// No session: the start page hides the info bar (the storage path is on
|
|
1093
1104
|
// #/sessions). The session clock ticks every second while a turn runs:
|
|
1094
1105
|
// lit writes its text alone, the bar's other nodes (a tooltip, a button
|
|
@@ -1260,6 +1271,8 @@ const streamHandlers = createStreamHandlers({
|
|
|
1260
1271
|
composer: {
|
|
1261
1272
|
empty: () => !promptEl.value.trim() && !chips.length,
|
|
1262
1273
|
refill: (text) => {
|
|
1274
|
+
// A failed turn's line back: seen, so the composer opens (this visit).
|
|
1275
|
+
openForVisit();
|
|
1263
1276
|
promptEl.value = text;
|
|
1264
1277
|
fitPrompt();
|
|
1265
1278
|
},
|
|
@@ -1427,6 +1440,8 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1427
1440
|
// (and images) go back into the composer, without replacing text typed
|
|
1428
1441
|
// since (4.13). The composer still holds the text just sent: it goes.
|
|
1429
1442
|
const restored = restoreOnAck(ack?.enqueued_id, earlyRestores, { myId: clientId, sentIds });
|
|
1443
|
+
// A restored line must be seen: the composer opens for this visit.
|
|
1444
|
+
if (restored?.refill && sentTo === selected) openForVisit();
|
|
1430
1445
|
promptEl.value = composerAfterAck({ sent: line, current: promptEl.value, refill: restored?.refill || "" });
|
|
1431
1446
|
fitPrompt();
|
|
1432
1447
|
clearChips();
|
|
@@ -1463,7 +1478,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1463
1478
|
showToast(cantText("send", e), null, { sticky: true });
|
|
1464
1479
|
} finally {
|
|
1465
1480
|
actionBtn.disabled = current.owner === "tui";
|
|
1466
|
-
|
|
1481
|
+
focusPrompt();
|
|
1467
1482
|
}
|
|
1468
1483
|
}
|
|
1469
1484
|
|
|
@@ -1488,7 +1503,8 @@ const composerCommands = createComposerCommands({
|
|
|
1488
1503
|
clear: () => clearComposer(),
|
|
1489
1504
|
done: () => {
|
|
1490
1505
|
actionBtn.disabled = current.owner === "tui";
|
|
1491
|
-
|
|
1506
|
+
// A chip's command (the info bar) leaves a folded composer folded.
|
|
1507
|
+
focusPrompt({ open: false });
|
|
1492
1508
|
},
|
|
1493
1509
|
},
|
|
1494
1510
|
});
|
|
@@ -1505,6 +1521,7 @@ function clearComposer() {
|
|
|
1505
1521
|
async function handleCancel() {
|
|
1506
1522
|
if (!selected) return;
|
|
1507
1523
|
cancelBtn.disabled = true;
|
|
1524
|
+
infoCancelBtn.disabled = true;
|
|
1508
1525
|
try {
|
|
1509
1526
|
await cancelTurn(selected);
|
|
1510
1527
|
} catch (e) {
|
|
@@ -1512,6 +1529,7 @@ async function handleCancel() {
|
|
|
1512
1529
|
if (e.code !== "not_running") alert(e.message);
|
|
1513
1530
|
} finally {
|
|
1514
1531
|
cancelBtn.disabled = false;
|
|
1532
|
+
infoCancelBtn.disabled = false;
|
|
1515
1533
|
}
|
|
1516
1534
|
}
|
|
1517
1535
|
|
|
@@ -1560,7 +1578,7 @@ const modelPicker = createModelPicker({
|
|
|
1560
1578
|
renderNewChatLlmContext();
|
|
1561
1579
|
renderNewChatThinking();
|
|
1562
1580
|
}
|
|
1563
|
-
|
|
1581
|
+
focusPrompt();
|
|
1564
1582
|
},
|
|
1565
1583
|
});
|
|
1566
1584
|
// A pick on a fresh link: /model in its worker (its command bubble and the
|
|
@@ -1611,9 +1629,7 @@ function syncModelPick() {
|
|
|
1611
1629
|
}
|
|
1612
1630
|
// Leaving a session: the start page's remembered model again (no fetch).
|
|
1613
1631
|
function restoreStartPick() {
|
|
1614
|
-
|
|
1615
|
-
try { stored = localStorage.getItem(MODEL_KEY); } catch (_) {}
|
|
1616
|
-
modelPicker.restore(stored);
|
|
1632
|
+
modelPicker.restore(readStored(MODEL_KEY));
|
|
1617
1633
|
renderNewChatLlmContext();
|
|
1618
1634
|
renderNewChatThinking();
|
|
1619
1635
|
}
|
|
@@ -1624,8 +1640,7 @@ async function loadModels() {
|
|
|
1624
1640
|
} catch (_) {
|
|
1625
1641
|
return;
|
|
1626
1642
|
}
|
|
1627
|
-
|
|
1628
|
-
try { stored = localStorage.getItem(MODEL_KEY); } catch (_) {}
|
|
1643
|
+
const stored = readStored(MODEL_KEY);
|
|
1629
1644
|
if (!modelPicker.setModels(payload, stored)) return;
|
|
1630
1645
|
startPickTitle = payload.warning ? `${START_PICK_TITLE} · ${payload.warning}` : START_PICK_TITLE;
|
|
1631
1646
|
modelPickEl.hidden = false;
|
|
@@ -1652,6 +1667,7 @@ let chips = [];
|
|
|
1652
1667
|
function renderChips() {
|
|
1653
1668
|
render(chipsTemplate(chips), chipsEl);
|
|
1654
1669
|
chipsEl.hidden = !chips.length;
|
|
1670
|
+
drawFoldControls();
|
|
1655
1671
|
}
|
|
1656
1672
|
|
|
1657
1673
|
function setChips(list) {
|
|
@@ -1668,7 +1684,7 @@ function addImageFiles(files) {
|
|
|
1668
1684
|
if (!files.length) return false;
|
|
1669
1685
|
for (const file of files) chips.push({ file, name: chipName(file, chips.length), src: URL.createObjectURL(file) });
|
|
1670
1686
|
renderChips();
|
|
1671
|
-
|
|
1687
|
+
focusPrompt();
|
|
1672
1688
|
return true;
|
|
1673
1689
|
}
|
|
1674
1690
|
|
|
@@ -1680,9 +1696,7 @@ chipsEl.addEventListener("click", (e) => {
|
|
|
1680
1696
|
renderChips();
|
|
1681
1697
|
});
|
|
1682
1698
|
// ── Composer height: grows with the text; the top-edge grip sets a floor ───
|
|
1683
|
-
|
|
1684
|
-
let promptFloor = null;
|
|
1685
|
-
try { promptFloor = Number(localStorage.getItem(GRIP_KEY)) || null; } catch (_) {}
|
|
1699
|
+
let promptFloor = Number(readStored(GRIP_KEY)) || null;
|
|
1686
1700
|
|
|
1687
1701
|
// The empty box: one line in a session, three on the start page.
|
|
1688
1702
|
function promptMinPx() {
|
|
@@ -1697,33 +1711,6 @@ function fitPrompt() {
|
|
|
1697
1711
|
promptEl.style.overflowY = scrolls ? "auto" : "hidden";
|
|
1698
1712
|
}
|
|
1699
1713
|
|
|
1700
|
-
const composeGrip = $("#composeGrip");
|
|
1701
|
-
composeGrip.addEventListener("pointerdown", (e) => {
|
|
1702
|
-
e.preventDefault();
|
|
1703
|
-
composeGrip.setPointerCapture(e.pointerId);
|
|
1704
|
-
composeGrip.classList.add("dragging");
|
|
1705
|
-
const startY = e.clientY;
|
|
1706
|
-
const startH = promptEl.offsetHeight;
|
|
1707
|
-
const move = (ev) => {
|
|
1708
|
-
promptFloor = gripFloor(startH + (startY - ev.clientY), window.innerHeight, promptMinPx());
|
|
1709
|
-
fitPrompt();
|
|
1710
|
-
};
|
|
1711
|
-
const up = () => {
|
|
1712
|
-
composeGrip.classList.remove("dragging");
|
|
1713
|
-
composeGrip.removeEventListener("pointermove", move);
|
|
1714
|
-
composeGrip.removeEventListener("pointerup", up);
|
|
1715
|
-
composeGrip.removeEventListener("pointercancel", up);
|
|
1716
|
-
try { localStorage.setItem(GRIP_KEY, String(promptFloor || "")); } catch (_) {}
|
|
1717
|
-
};
|
|
1718
|
-
composeGrip.addEventListener("pointermove", move);
|
|
1719
|
-
composeGrip.addEventListener("pointerup", up);
|
|
1720
|
-
composeGrip.addEventListener("pointercancel", up);
|
|
1721
|
-
});
|
|
1722
|
-
composeGrip.addEventListener("dblclick", () => {
|
|
1723
|
-
promptFloor = null;
|
|
1724
|
-
try { localStorage.removeItem(GRIP_KEY); } catch (_) {}
|
|
1725
|
-
fitPrompt();
|
|
1726
|
-
});
|
|
1727
1714
|
promptEl.addEventListener("input", fitPrompt);
|
|
1728
1715
|
window.addEventListener("resize", fitPrompt);
|
|
1729
1716
|
fitPrompt();
|
|
@@ -1806,6 +1793,29 @@ document.addEventListener("keydown", (e) => {
|
|
|
1806
1793
|
if (e.key === "Escape" && !lightboxEl.hidden) lightboxEl.hidden = true;
|
|
1807
1794
|
});
|
|
1808
1795
|
cancelBtn.addEventListener("click", () => handleCancel());
|
|
1796
|
+
infoCancelBtn.addEventListener("click", () => handleCancel());
|
|
1797
|
+
|
|
1798
|
+
// The composer's grip and fold (composer_grip.js).
|
|
1799
|
+
const composerGrip = createComposerGrip({
|
|
1800
|
+
composeGrip: $("#composeGrip"),
|
|
1801
|
+
promptEl,
|
|
1802
|
+
infoCancelBtn,
|
|
1803
|
+
infoBarEl: $("#infoBar"),
|
|
1804
|
+
session: () => selected,
|
|
1805
|
+
fitPrompt,
|
|
1806
|
+
promptMinPx,
|
|
1807
|
+
floor: () => promptFloor,
|
|
1808
|
+
setFloor: (px) => { promptFloor = px; },
|
|
1809
|
+
draft: () => !!(promptEl.value.trim() || chips.length),
|
|
1810
|
+
waiting: () => !!(waitingOn(allSessions.find((s) => s.id === selected)) || questionCards.pendingQuestion()),
|
|
1811
|
+
turnRunning: () => liveTurn.running,
|
|
1812
|
+
allViewOpen,
|
|
1813
|
+
lightboxOpen: () => !lightboxEl.hidden,
|
|
1814
|
+
imageFiles,
|
|
1815
|
+
addImageFiles,
|
|
1816
|
+
});
|
|
1817
|
+
// Bound here, not hoisted: no top-level code above this line may call them.
|
|
1818
|
+
const { openForVisit, focusPrompt, drawFoldControls } = composerGrip;
|
|
1809
1819
|
// The / autocomplete (command_list.js): the session's commands while the
|
|
1810
1820
|
// composer holds one "/word".
|
|
1811
1821
|
const commandList = createCommandList({
|
|
@@ -1934,9 +1944,10 @@ async function continueSelected() {
|
|
|
1934
1944
|
// continue's 201.
|
|
1935
1945
|
function fillOpener(created) {
|
|
1936
1946
|
if (promptEl.value.trim() || chips.length) return;
|
|
1947
|
+
openForVisit();
|
|
1937
1948
|
promptEl.value = openerText(created);
|
|
1938
1949
|
fitPrompt();
|
|
1939
|
-
|
|
1950
|
+
focusPrompt();
|
|
1940
1951
|
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
1941
1952
|
}
|
|
1942
1953
|
|
|
@@ -2039,9 +2050,10 @@ function showToast(text, action = null, options = {}) { toasts.show(text, action
|
|
|
2039
2050
|
// session's plugins' pills (chi.annotation, annotate_actions.js), which fill
|
|
2040
2051
|
// the composer with their command line and the quote, or send it at once.
|
|
2041
2052
|
function fillComposer(text) {
|
|
2053
|
+
openForVisit();
|
|
2042
2054
|
promptEl.value = text;
|
|
2043
2055
|
fitPrompt();
|
|
2044
|
-
|
|
2056
|
+
focusPrompt();
|
|
2045
2057
|
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
2046
2058
|
promptEl.scrollTop = promptEl.scrollHeight;
|
|
2047
2059
|
}
|
|
@@ -2077,12 +2089,8 @@ const notifications = createNotifications({ button: $("#notifyBtn") });
|
|
|
2077
2089
|
// The latest-sessions strip can be hidden for more chat room; the choice
|
|
2078
2090
|
// stays per browser (it replaced zen mode, so a zen user starts hidden).
|
|
2079
2091
|
function stripHiddenSaved() {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
return saved === null ? localStorage.getItem("chi_zen") === "1" : saved === "1";
|
|
2083
|
-
} catch (_) {
|
|
2084
|
-
return false;
|
|
2085
|
-
}
|
|
2092
|
+
const saved = readStored("chi_strip_hidden");
|
|
2093
|
+
return saved === null ? readStored("chi_zen") === "1" : saved === "1";
|
|
2086
2094
|
}
|
|
2087
2095
|
// A short window hides the strip by itself, so the stage keeps room for a
|
|
2088
2096
|
// card that waits (its question and choices); the sessions pill brings it
|
|
@@ -2098,7 +2106,7 @@ function applyStripHidden() {
|
|
|
2098
2106
|
}
|
|
2099
2107
|
function setStripHidden(hidden) {
|
|
2100
2108
|
if (hidden) lists.familyPop.close();
|
|
2101
|
-
|
|
2109
|
+
writeStored("chi_strip_hidden", hidden ? "1" : "0");
|
|
2102
2110
|
stripShownWhileShort = !hidden && shortWindow.matches;
|
|
2103
2111
|
applyStripHidden();
|
|
2104
2112
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The session's composer can fold away for room to read (the info bar under
|
|
2
|
+
// it stays and becomes the page's bottom edge). The user's own fold or
|
|
3
|
+
// unfold is saved per browser (chi_composer_collapsed); an archived
|
|
4
|
+
// session opens folded for that visit; the page expanding it by itself (a
|
|
5
|
+
// focus, a restored line, a quote) opens it for the visit only. A visit is
|
|
6
|
+
// one select() call.
|
|
7
|
+
|
|
8
|
+
export const COMPOSER_FOLD_KEY = "chi_composer_collapsed";
|
|
9
|
+
|
|
10
|
+
// Whether the composer is folded: the saved choice or an archived visit,
|
|
11
|
+
// unless this visit opened it.
|
|
12
|
+
export function composerFolded({ saved, archivedVisit, visitOpen }) {
|
|
13
|
+
if (visitOpen) return false;
|
|
14
|
+
return !!saved || !!archivedVisit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The composer's grip while it is the fold's handle. Expanded it only says
|
|
18
|
+
// how to resize and hide; folded it is drawn as the handle on the info
|
|
19
|
+
// bar's top edge, pulled up or clicked to open, and marked: `draft` (the
|
|
20
|
+
// box holds text or images: folding never clears them: the accent),
|
|
21
|
+
// `waiting` (the attention colour: the session waits on the user, and its
|
|
22
|
+
// card may not take the focus while a draft is there). The title carries
|
|
23
|
+
// the words.
|
|
24
|
+
// @return {title, folded, draft, waiting}
|
|
25
|
+
export function foldHandle({ folded, draft, waiting }) {
|
|
26
|
+
if (!folded) {
|
|
27
|
+
return { title: "Drag to make the message box taller, down to hide it (double-click resets)", folded: false, draft: false, waiting: false };
|
|
28
|
+
}
|
|
29
|
+
const marked = !!draft;
|
|
30
|
+
let title = "Pull up or click to write";
|
|
31
|
+
if (marked) title += " · a draft is in it";
|
|
32
|
+
if (waiting) title += " · the session waits on you";
|
|
33
|
+
return { title, folded: true, draft: marked, waiting: !!waiting };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Whether a key pressed on the page (the composer folded) opens it and
|
|
37
|
+
// lands in the message box: a printable character, not Space (it scrolls,
|
|
38
|
+
// and the turn marker and the stage headline use it), no Ctrl/Meta/Alt,
|
|
39
|
+
// not taken already, not mid-composition, and not while the focus is in a
|
|
40
|
+
// field of its own (an input, textarea, select or contenteditable), the
|
|
41
|
+
// all-sessions view or the lightbox is open. An IME's first key arrives as
|
|
42
|
+
// "Process": it doesn't open it.
|
|
43
|
+
// +active+: document.activeElement.
|
|
44
|
+
export function opensOnKey(e, { active, allView, lightbox }) {
|
|
45
|
+
if (!e || e.defaultPrevented || e.isComposing) return false;
|
|
46
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
47
|
+
const key = typeof e.key === "string" ? e.key : "";
|
|
48
|
+
if ([...key].length !== 1 || key === " ") return false;
|
|
49
|
+
if (allView || lightbox) return false;
|
|
50
|
+
return !editable(active);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function editable(el) {
|
|
54
|
+
if (!el) return false;
|
|
55
|
+
if (el.isContentEditable) return true;
|
|
56
|
+
const tag = (el.tagName || "").toUpperCase();
|
|
57
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
58
|
+
}
|