samagotchi 0.97.0 → 0.98.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 +30 -1
- data/docs/architecture.md +10 -2
- data/docs/bundle-authoring.md +5 -3
- data/docs/cli.md +8 -3
- data/docs/hooks.md +59 -0
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +18 -0
- data/docs/plugins.md +8 -1
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/answer_refs.rb +16 -9
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +39 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +26 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +18 -15
- data/lib/samagotchi/hooks.rb +4 -1
- data/lib/samagotchi/llm/errors.rb +51 -17
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +28 -9
- data/lib/samagotchi/memory_bundle/manifest.rb +17 -0
- data/lib/samagotchi/output_formatter.rb +8 -2
- data/lib/samagotchi/pending_question.rb +7 -3
- data/lib/samagotchi/plugin/api.rb +5 -0
- data/lib/samagotchi/question_asked.rb +83 -0
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/testing/bundle.rb +18 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +30 -9
- data/lib/samagotchi/web/public/app.js +44 -11
- data/lib/samagotchi/web/public/index.html +36 -13
- data/lib/samagotchi/web/public/info_bar_model.js +0 -2
- data/lib/samagotchi/web/public/lists_view.js +32 -8
- data/lib/samagotchi/web/public/markdown_place.js +25 -8
- data/lib/samagotchi/web/public/question_cards.js +23 -6
- data/lib/samagotchi/web/public/session_rename.js +88 -15
- data/lib/samagotchi/web/public/session_title.js +6 -0
- data/lib/samagotchi/web/public/stage_frame.js +24 -14
- data/lib/samagotchi/web/public/stage_model.js +4 -2
- data/lib/samagotchi/web/public/stage_slots.js +41 -9
- data/lib/samagotchi/web/public/stage_view.js +149 -8
- data/lib/samagotchi/web/public/stream_handlers.js +5 -1
- data/lib/samagotchi/web/public/turn_model.js +20 -0
- data/lib/samagotchi/web/public/turn_view.js +22 -9
- metadata +2 -1
|
@@ -61,13 +61,13 @@ import { createStripGrip } from "./strip_grip.js";
|
|
|
61
61
|
import { waitingOn } from "./notify.js";
|
|
62
62
|
import { applySessionEvent, sortedByUpdated } from "./sessions_list.js";
|
|
63
63
|
import { createNotifications } from "./notifications.js";
|
|
64
|
-
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
64
|
+
import { renameShown, tabTitle, topBarTitle } from "./session_title.js";
|
|
65
65
|
import { restartConfirmText, versionNotice } from "./update_notice.js";
|
|
66
66
|
import {
|
|
67
67
|
appendAboveLiveTiming, mergeTiming, normalizeTiming,
|
|
68
68
|
} from "./timing.js";
|
|
69
69
|
import { createLiveTurn } from "./live_turn.js";
|
|
70
|
-
import { applyFinalAnswer, pairAnswerRefs } from "./answer_refs.js";
|
|
70
|
+
import { applyFinalAnswer, pairAnswerRefs, setAnswerRefs } from "./answer_refs.js";
|
|
71
71
|
import { installRefClicks } from "./refs.js";
|
|
72
72
|
import { linkRefs, touchedFiles } from "./ref_scan.js";
|
|
73
73
|
import { DONE_MS, applyInitEvent, dropDone, fadeDone, initRowTemplate, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
@@ -97,6 +97,8 @@ const composerEl = $("#composer");
|
|
|
97
97
|
// The open session's title in the top bar, when no strip card in sight
|
|
98
98
|
// shows it; the tab's base ("Chi · <project>"), set by renderScope.
|
|
99
99
|
const sessionTitleEl = $("#sessionTitle");
|
|
100
|
+
const sessionTitleRowEl = $("#sessionTitleRow");
|
|
101
|
+
const titleRenameBtn = $("#titleRenameBtn");
|
|
100
102
|
let tabBase = null;
|
|
101
103
|
let tabShown = null;
|
|
102
104
|
const infoTextEl = $("#infoText");
|
|
@@ -210,8 +212,9 @@ const markdownPlace = createMarkdownPlace({
|
|
|
210
212
|
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
211
213
|
normalize,
|
|
212
214
|
pairAnswerRefs,
|
|
215
|
+
setAnswerRefs,
|
|
213
216
|
});
|
|
214
|
-
const { renderMarkdown, linkRefsIn, applyFinalMarkdown, linkTextRefs } = markdownPlace;
|
|
217
|
+
const { renderMarkdown, pairRefsIn, linkRefsIn, applyFinalMarkdown, linkTextRefs } = markdownPlace;
|
|
215
218
|
const turnDom = view === "stage"
|
|
216
219
|
? createStageView({
|
|
217
220
|
...viewArgs, isNearBottom: () => atHistoryEnd(), dockEl: $("#dock"), centerEl: $("#center"), composerEl,
|
|
@@ -291,6 +294,7 @@ const questionCards = createQuestionCards({
|
|
|
291
294
|
askingText: () => turnDom.askingText(),
|
|
292
295
|
renderMarkdown,
|
|
293
296
|
linkRefs: linkRefsIn,
|
|
297
|
+
pairRefs: pairRefsIn,
|
|
294
298
|
});
|
|
295
299
|
// The history's bubbles (bubbles.js): output, end lines, every client's
|
|
296
300
|
// prompts, the recap, warnings and notices, command and note bubbles.
|
|
@@ -473,6 +477,9 @@ const lists = createListsView({
|
|
|
473
477
|
allViewOpen: () => allViewOpen(),
|
|
474
478
|
openAllView: () => openAllView(),
|
|
475
479
|
leaveAllView: (hash) => leaveAllView(hash),
|
|
480
|
+
renameShown,
|
|
481
|
+
onRename: () => rename.open(),
|
|
482
|
+
renaming: () => rename.isOpen(),
|
|
476
483
|
});
|
|
477
484
|
|
|
478
485
|
// Continue, Archive, Delete and Stop, and the start page when the open
|
|
@@ -496,13 +503,31 @@ const sessionActions = createSessionActions({
|
|
|
496
503
|
closeLiveStream: () => streamControl?.close(),
|
|
497
504
|
workerGone,
|
|
498
505
|
});
|
|
499
|
-
// ✎: /title through the chips' path (session_rename.js)
|
|
506
|
+
// The title's ✎: /title through the chips' path (session_rename.js), the
|
|
507
|
+
// input over the title row on screen: the top bar's when it shows the
|
|
508
|
+
// title, else the active strip card's (updateSessionTitle decides which).
|
|
500
509
|
const rename = createRename({
|
|
501
|
-
|
|
510
|
+
doc: document,
|
|
511
|
+
anchor: () => {
|
|
512
|
+
if (!sessionTitleRowEl.hidden) return sessionTitleRowEl;
|
|
513
|
+
const row = topStripEl.querySelector(".card.active .preview-row");
|
|
514
|
+
return row && row.getClientRects().length ? row : null; // not in the All sessions view's hidden strip
|
|
515
|
+
},
|
|
502
516
|
session: () => selected,
|
|
503
517
|
titleOf: (id) => lists.sessionsById().get(id)?.title || null,
|
|
518
|
+
shownTitle: () => shownTitle(),
|
|
519
|
+
closePopovers: () => {
|
|
520
|
+
lists.familyPop.close();
|
|
521
|
+
lists.chainPop.close();
|
|
522
|
+
},
|
|
523
|
+
// Not from inside a strip draw (its paintActive repositions, which may
|
|
524
|
+
// close): the held strip catches up after it.
|
|
525
|
+
onClose: () => queueMicrotask(() => lists.catchUp()),
|
|
504
526
|
run: (line) => runSessionCommand(line),
|
|
505
527
|
});
|
|
528
|
+
titleRenameBtn.addEventListener("click", () => rename.open());
|
|
529
|
+
document.addEventListener("scroll", () => rename.reposition(), true);
|
|
530
|
+
window.addEventListener("resize", () => rename.reposition());
|
|
506
531
|
|
|
507
532
|
const { confirmDelete, leaveGoneSession, continueSelected, archiveOne, setArchived } = sessionActions;
|
|
508
533
|
|
|
@@ -871,6 +896,8 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
871
896
|
snapshotCards.during.forEach((card) => upsertCard(card));
|
|
872
897
|
// A question the worker is blocked on (also after a reload).
|
|
873
898
|
if (data.pending_question && data.pending_question.id && data.pending_question.status === "pending") {
|
|
899
|
+
// Its refs on the asking step, as question_requested puts them.
|
|
900
|
+
if (data.pending_question.refs) turnDom.questionRefs?.(data.pending_question.refs);
|
|
874
901
|
questionCards.renderQuestion(data.pending_question);
|
|
875
902
|
// The turn's replay drew it as asked; the snapshot knows whether it is
|
|
876
903
|
// relayed to the parent now.
|
|
@@ -1055,9 +1082,6 @@ function updateInfoBar() {
|
|
|
1055
1082
|
const deleteBtn = document.getElementById("infoDeleteBtn");
|
|
1056
1083
|
const archiveBtn = document.getElementById("infoArchiveBtn");
|
|
1057
1084
|
const continueBtn = document.getElementById("infoContinueBtn");
|
|
1058
|
-
const renameBtn = document.getElementById("infoRenameBtn");
|
|
1059
|
-
if (renameBtn) renameBtn.style.display = buttons.rename.shown ? "" : "none";
|
|
1060
|
-
rename.follow(buttons.rename.shown ? selected : null);
|
|
1061
1085
|
if (continueBtn) {
|
|
1062
1086
|
continueBtn.style.display = buttons.continue.shown ? "" : "none";
|
|
1063
1087
|
continueBtn.disabled = buttons.continue.disabled;
|
|
@@ -1087,11 +1111,16 @@ function updateInfoBar() {
|
|
|
1087
1111
|
// card says it when the strip is shown and has one; else the top bar. The
|
|
1088
1112
|
// browser tab names it too. It follows the card's (openTitle): a chain link
|
|
1089
1113
|
// reads its card's title (cardTitle), whatever opener it was sent.
|
|
1090
|
-
function
|
|
1114
|
+
function shownTitle() {
|
|
1091
1115
|
const shown = selected ? lists.sessionsById().get(selected) : null;
|
|
1092
1116
|
const first = current.firstPreview || "";
|
|
1093
1117
|
const raw = !selected ? "" : openTitle({ shown, first, cardTitle: (s) => lists.cardTitle(s) });
|
|
1094
|
-
|
|
1118
|
+
return raw === "\u2014" ? "" : raw; // "—": no first message yet
|
|
1119
|
+
}
|
|
1120
|
+
// The top bar's ✎ follows it (not a plain terminal's session), and an open
|
|
1121
|
+
// rename input follows the title row on screen (or closes, none showing).
|
|
1122
|
+
function updateSessionTitle() {
|
|
1123
|
+
const title = shownTitle();
|
|
1095
1124
|
const text = topBarTitle({
|
|
1096
1125
|
title,
|
|
1097
1126
|
cardShown: !!topStripEl.querySelector(".card.active"),
|
|
@@ -1099,7 +1128,9 @@ function updateSessionTitle() {
|
|
|
1099
1128
|
allView: allViewOpen(),
|
|
1100
1129
|
});
|
|
1101
1130
|
if (sessionTitleEl.textContent !== text) sessionTitleEl.textContent = text;
|
|
1102
|
-
if (
|
|
1131
|
+
if (sessionTitleRowEl.hidden !== !text) sessionTitleRowEl.hidden = !text;
|
|
1132
|
+
const renamable = !!text && renameShown(current.owner);
|
|
1133
|
+
if (titleRenameBtn.hidden !== !renamable) titleRenameBtn.hidden = !renamable;
|
|
1103
1134
|
const tip = text ? title : "";
|
|
1104
1135
|
if (sessionTitleEl.title !== tip) sessionTitleEl.title = tip;
|
|
1105
1136
|
const tab = tabBase === null ? null : tabTitle({ title, base: tabBase });
|
|
@@ -1107,6 +1138,8 @@ function updateSessionTitle() {
|
|
|
1107
1138
|
tabShown = tab;
|
|
1108
1139
|
notifications.setBaseTitle(tab);
|
|
1109
1140
|
}
|
|
1141
|
+
rename.follow(selected);
|
|
1142
|
+
rename.reposition();
|
|
1110
1143
|
}
|
|
1111
1144
|
|
|
1112
1145
|
// The stage takes a running turn's rows (its extras); else the history.
|
|
@@ -779,6 +779,10 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
779
779
|
text, then its markdown when web.markdown is on (.markdown, stage_slots.js drawAsking); capped so the card keeps
|
|
780
780
|
its room, read from the top. */
|
|
781
781
|
.ts-asking{max-height:calc(var(--stage-max,60vh) * .35);overflow:auto;scrollbar-width:thin;white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.55;color:var(--narr);border-left:2px solid var(--border-strong);padding-left:10px}
|
|
782
|
+
/* The question room (stage_view.js drawRoom): a model's own question whose asking text overflows that cap. The slot loses its
|
|
783
|
+
cap and its own scroll, and the stage's one scroll covers the whole reply (the stage's cap rises for the turn, stage_frame.js).
|
|
784
|
+
--stage-max also scales the extras, the cloud and the queued slot: a ceiling only, content that fit stays as it is. */
|
|
785
|
+
.turn-stage.question-room .ts-asking{max-height:none;overflow:visible}
|
|
782
786
|
.turn-stage.asking .ts-headline{display:none}
|
|
783
787
|
@keyframes ts-fade{from{opacity:.35}}
|
|
784
788
|
.ts-now{height:var(--ts-now-h);display:flex;align-items:center;gap:8px;min-width:0;padding:0 9px;border-radius:8px;background:var(--accent-tint);font-size:13px}
|
|
@@ -820,7 +824,12 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
820
824
|
.ts-answer>.bubble.output>.copy-btn{top:0;right:0}
|
|
821
825
|
/* The cloud: one chip (steps, calls, a tick per call), the turn view's block behind it. */
|
|
822
826
|
/* The footer: the chip at the stage's bottom edge (the cloud's steps open above it, the scroller's last slot). */
|
|
823
|
-
.ts-foot{flex:none;padding:0 14px 10px}
|
|
827
|
+
.ts-foot{position:relative;flex:none;padding:0 14px 10px}
|
|
828
|
+
/* The question room's jump pill (stage_view.js drawPill): at the scroller's bottom edge, above the chip, while the waiting
|
|
829
|
+
card is out of the stage's view; absolute, so the frame never counts it. */
|
|
830
|
+
.ts-room-pill{position:absolute;bottom:100%;right:14px;margin-bottom:6px;padding:4px 11px;border:1px solid var(--warn-border);border-radius:999px;background:var(--warn-bg);color:var(--warn-text);font-size:12.5px;font-weight:600;line-height:1.4;white-space:nowrap;box-shadow:0 2px 8px var(--shadow-strong);animation:ts-fade .2s ease}
|
|
831
|
+
.ts-room-pill:hover:not(:disabled){filter:none;color:var(--warn-bright);border-color:var(--warn);background:var(--warn-bg)}
|
|
832
|
+
.ts-room-pill[hidden]{display:none}
|
|
824
833
|
/* Below the turn's card: the lines queued for after it (each a turn of its own), apart from the turn by a dashed rule. */
|
|
825
834
|
.ts-queued{flex:none;display:flex;flex-direction:column;gap:6px;padding:8px 14px 10px;border-top:1px dashed var(--border-strong);max-height:calc(var(--stage-max,60vh) * .3);overflow:auto;scrollbar-width:thin}
|
|
826
835
|
.ts-queued:empty{display:none}
|
|
@@ -887,10 +896,6 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
887
896
|
#infoBar #infoStopBtn{background:var(--bg-raised);color:var(--warn-text);border:1px solid var(--border-strong)}
|
|
888
897
|
#infoBar #infoStopBtn:hover{background:var(--warn-bg);border-color:var(--warn-border)}
|
|
889
898
|
#infoBar #infoArchiveBtn{background:transparent;color:var(--text-muted);border:1px solid var(--border-strong)}
|
|
890
|
-
#infoBar #infoRenameBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
891
|
-
#infoBar #infoRenameBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
892
|
-
/* ✎'s input (session_rename.js), in the button's place. */
|
|
893
|
-
#infoBar .rename-input{flex:0 1 18em;min-width:6em;font:inherit;font-size:12px;padding:3px 8px;border-radius:8px;border:1px solid var(--accent-border);background:var(--bg-raised);color:var(--text);outline:none}
|
|
894
899
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
895
900
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
896
901
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
@@ -980,8 +985,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
980
985
|
#infoBar>button{font-size:0;line-height:0;padding:5px 7px}
|
|
981
986
|
#infoBar>button::before{content:"";display:block;width:14px;height:14px;background:currentColor;-webkit-mask:var(--icon) center/contain no-repeat;mask:var(--icon) center/contain no-repeat}
|
|
982
987
|
#infoBar #infoArchiveBtn{--icon:var(--icon-archive)}
|
|
983
|
-
#infoBar #infoRenameBtn{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.5 2.5l3 3L5.5 13.5H2.5v-3z'/%3E%3C/svg%3E")}
|
|
984
|
-
#infoBar .rename-input{flex:1 1 8em;font-size:16px}
|
|
985
988
|
#infoBar #infoArchiveBtn.archived{--icon:var(--icon-unarchive)}
|
|
986
989
|
#infoBar #infoContinueBtn{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8h9M8.5 4.5 12 8l-3.5 3.5'/%3E%3C/svg%3E")}
|
|
987
990
|
#infoBar #infoCancelBtn{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='4' width='8' height='8' rx='1.5'/%3E%3C/svg%3E")}
|
|
@@ -1031,16 +1034,32 @@ body.has-session #scope .scope-where{display:none}
|
|
|
1031
1034
|
#newBtn .brand-new{font-size:16px}
|
|
1032
1035
|
}
|
|
1033
1036
|
/* The bell (notifications): quiet at the top bar's right end; filled in the accent when on, struck through when the browser blocks it. */
|
|
1034
|
-
/* The open session's title, when no strip card in sight shows it (session_title.js): the bar's middle, cut with an ellipsis; the whole first message in its tooltip.
|
|
1035
|
-
|
|
1036
|
-
#
|
|
1037
|
+
/* The open session's title, when no strip card in sight shows it (session_title.js): the bar's middle, cut with an ellipsis; the whole first message in its tooltip.
|
|
1038
|
+
Its row holds it and its ✎ (session_rename.js), the ✎ right after the text (the text doesn't grow, the row does). */
|
|
1039
|
+
#sessionTitleRow{flex:1;min-width:0;display:flex;align-items:center;gap:2px}
|
|
1040
|
+
#sessionTitleRow[hidden]{display:none}
|
|
1041
|
+
#sessionTitle{flex:0 1 auto;min-width:0;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1042
|
+
/* The title's ✎ (the top bar's and the active strip card's): quiet, the text's colour on hover. */
|
|
1043
|
+
.title-rename,.card-rename{flex-shrink:0;min-height:0;padding:0 5px;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--text-muted);font-size:13px;line-height:20px;cursor:pointer}
|
|
1044
|
+
.title-rename:hover,.card-rename:hover{color:var(--text);background:var(--bg-hover)}
|
|
1045
|
+
.title-rename[hidden]{display:none}
|
|
1046
|
+
/* The rename input (session_rename.js): fixed over the title row it renames, at the end of body (the strip draws its cards
|
|
1047
|
+
anew on every session event); above the selection bar (20), under the popovers (40). */
|
|
1048
|
+
.rename-input{position:fixed;z-index:30;box-sizing:border-box;margin:0;font:inherit;font-size:var(--rename-font,14px);padding:0 8px;border-radius:8px;border:1px solid var(--accent-border);background:var(--bg-raised);color:var(--text);outline:none;box-shadow:var(--glass-shadow)}
|
|
1049
|
+
.rename-input::placeholder{color:var(--text-faint)}
|
|
1050
|
+
/* Over the top bar's title row (as wide as the bar's middle): a title's width, not a field to the bell. */
|
|
1051
|
+
.rename-input.over-bar{max-width:32em}
|
|
1037
1052
|
/* A phone: the title is its own row under the bar, so "chi + · pill · bell" stay one line. Nothing on that line wraps to a
|
|
1038
1053
|
row of its own when the pill's text grows mid-turn ("· 1 waiting"): the scope chip and the pill have a base size of 0 (what
|
|
1039
1054
|
a wrapping line counts) and grow to their text, the pill first; the scope chip cuts with an ellipsis before the pill does.
|
|
1040
1055
|
The pill drops its session count while it says live or waiting, so the scope chip keeps its words. */
|
|
1041
1056
|
@media(max-width:600px){
|
|
1042
1057
|
#topBar{flex-wrap:wrap;row-gap:2px}
|
|
1043
|
-
#
|
|
1058
|
+
#sessionTitleRow{flex-basis:100%;order:10}
|
|
1059
|
+
#sessionTitle{font-size:12px}
|
|
1060
|
+
.title-rename{font-size:12px;line-height:18px}
|
|
1061
|
+
/* Under 16px iOS Safari zooms the page on focus. */
|
|
1062
|
+
.rename-input{font-size:16px}
|
|
1044
1063
|
#scope,#stripStatus{flex:1 1 0;min-width:0;max-width:max-content}
|
|
1045
1064
|
#stripStatus{flex-grow:1000;overflow:hidden;text-overflow:ellipsis}
|
|
1046
1065
|
#scope .scope-name{flex-shrink:1;min-width:0}
|
|
@@ -1223,6 +1242,11 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
1223
1242
|
@media (hover:none){.card-tools button{opacity:1}}
|
|
1224
1243
|
.card.active{background:var(--accent-bg);border-color:var(--accent-border)}
|
|
1225
1244
|
.card .preview{font-size:14px;line-height:1.4;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1245
|
+
/* A strip card's title line with its ✎, shown on the active card alone (lists_view.js draws it on each, a select only paints). */
|
|
1246
|
+
.card .preview-row{display:flex;align-items:center;gap:2px;min-width:0}
|
|
1247
|
+
.card .preview-row .preview{flex:1;min-width:0}
|
|
1248
|
+
.card .card-rename{display:none}
|
|
1249
|
+
#topStrip .card.active .card-rename{display:block}
|
|
1226
1250
|
.card .time{font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
1227
1251
|
.card .ctx{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--text-3)}
|
|
1228
1252
|
#topStrip .card .preview{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -1262,7 +1286,7 @@ a{color:var(--accent)}
|
|
|
1262
1286
|
<button type="button" id="newBtn" class="brand" title="New session"><span class="brand-name">chi</span><span class="brand-new">+<span class="brand-new-word"> new session</span></span></button>
|
|
1263
1287
|
<div id="scope" hidden></div>
|
|
1264
1288
|
<button type="button" class="strip-toggle" id="stripStatus" title="All sessions, with search"><span class="pill-count">sessions</span></button>
|
|
1265
|
-
<span id="sessionTitle" hidden></span>
|
|
1289
|
+
<span id="sessionTitleRow" hidden><span id="sessionTitle"></span><button type="button" id="titleRenameBtn" class="title-rename" title="Rename this session" aria-label="Rename this session" hidden>✎</button></span>
|
|
1266
1290
|
<button type="button" id="notifyBtn" aria-label="Notifications" aria-pressed="false" hidden><svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path class="bell" d="M8 1.75a4 4 0 0 0-4 4v2.6L2.75 10.6v.9h10.5v-.9L12 8.35v-2.6a4 4 0 0 0-4-4Z"/><path class="clapper" d="M6.5 13a1.5 1.5 0 0 0 3 0"/><path class="slash" d="M2.5 2.5l11 11"/></svg></button>
|
|
1267
1291
|
</div>
|
|
1268
1292
|
<section id="topStrip" aria-label="Latest sessions" class="loading"><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div></section>
|
|
@@ -1306,7 +1330,6 @@ a{color:var(--accent)}
|
|
|
1306
1330
|
<div class="info" id="infoText"></div>
|
|
1307
1331
|
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
1308
1332
|
<button class="ghost" id="infoCancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
1309
|
-
<button class="ghost" id="infoRenameBtn" title="Rename this session" aria-label="Rename this session" style="display:none">✎</button>
|
|
1310
1333
|
<button class="ghost" id="infoContinueBtn" title="Continue: start the next link of this session's chain from its recap (this one is archived)" style="display:none">continue →</button>
|
|
1311
1334
|
<button class="ghost" id="infoArchiveBtn" title="Archive: hide from the lists, keep for good" style="display:none">archive</button>
|
|
1312
1335
|
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop session</button>
|
|
@@ -42,8 +42,6 @@ export function infoButtons({ id, state, chain, archived, continuing, streaming
|
|
|
42
42
|
const busy = !!id && continuing;
|
|
43
43
|
return {
|
|
44
44
|
continue: { shown: !!id && !chain.next && state.owner !== "tui", disabled: busy, text: busy ? "continuing…" : "continue →" },
|
|
45
|
-
// ✎: /title from the web (a plain terminal's session takes no commands).
|
|
46
|
-
rename: { shown: !!id && state.owner !== "tui" },
|
|
47
45
|
delete: { shown: !!id },
|
|
48
46
|
archive: {
|
|
49
47
|
shown: !!id,
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
// confirmDelete(id), setArchived(id, archive), archiveOne(id, archive)
|
|
22
22
|
// updateInfoBar(), updateSessionTitle(), showToast(text, action)
|
|
23
23
|
// allViewOpen(), openAllView(), leaveAllView(hash)
|
|
24
|
+
// renameShown(owner) whether a card's title has its ✎ (session_title.js)
|
|
25
|
+
// onRename() the active card's ✎ clicked (session_rename.js open)
|
|
26
|
+
// renaming() whether that rename input is open (the strip holds)
|
|
24
27
|
|
|
25
28
|
import { ownerBadge, relativeTime, delegatedBy } from "./format.js";
|
|
26
29
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
@@ -134,7 +137,9 @@ export function cardAnimStyle({ enterMs = null, now = null } = {}, period = DOT_
|
|
|
134
137
|
// +ctx+: title (cardTitle), projectName (the page's scope), activeId (the
|
|
135
138
|
// open session), chainOpen (its chain's popover is open), parent (the
|
|
136
139
|
// delegating session, or null).
|
|
137
|
-
|
|
140
|
+
// renamable: the title line ends in a ✎ (the strip's cards; CSS shows it on
|
|
141
|
+
// the active one alone, since a select only paints the cards again).
|
|
142
|
+
export function cardTemplate(s, { deletable = false, archivable = false, selectable = false, picked = false, skipReason = null, family = null, open = false, rows = false, matchIds = null, entering = false, anim = "", renamable = false } = {}, { title = "", projectName = null, activeId = null, chainOpen = false, parent = null } = {}) {
|
|
138
143
|
const fam = family && family.members.length ? family : null;
|
|
139
144
|
const ts = s.updated_at || s.created_at || "";
|
|
140
145
|
const tip = `${s.id} · updated ${ts} · ${s.status}`;
|
|
@@ -164,8 +169,11 @@ export function cardTemplate(s, { deletable = false, archivable = false, selecta
|
|
|
164
169
|
const memberIds = [...(fam ? fam.members.map((m) => m.session.id) : []),
|
|
165
170
|
...(family?.chain ? family.chain.links.map((x) => x.id).filter((id) => id !== s.id) : [])];
|
|
166
171
|
const cls = `card${s.archived ? " archived" : ""}${waiting || familyWaiting ? " waiting" : ""}${selectable && picked ? " picked" : ""}${fam && rows && open ? " family-open" : ""}${entering ? " entering" : ""}`;
|
|
172
|
+
const preview = renamable
|
|
173
|
+
? html`<div class="preview-row"><div class="preview">${title}</div><button type="button" class="card-rename" data-rename=${s.id} title="Rename this session" aria-label="Rename this session">\u270e</button></div>`
|
|
174
|
+
: html`<div class="preview">${title}</div>`;
|
|
167
175
|
const recap = s.recap ? html`<div class="recap" title=${s.recap}>${s.recap}</div>` : nothing;
|
|
168
|
-
return html`<div class=${cls} data-id=${s.id} data-members=${memberIds.length ? memberIds.join(" ") : nothing} style=${anim || nothing} title=${`${tip}${memTitle ? ` · ${memTitle}` : ""}`}
|
|
176
|
+
return html`<div class=${cls} data-id=${s.id} data-members=${memberIds.length ? memberIds.join(" ") : nothing} style=${anim || nothing} title=${`${tip}${memTitle ? ` · ${memTitle}` : ""}`}>${preview}${recap}<div class="meta"><span class=${`status dot ${s.status}${waiting ? " waiting" : ""}`} title=${dotWord} aria-label=${dotWord}></span>${waitingT}${stoppedT}${time}${ctxT}${folderT}${parentChipTemplate(s, parent)}${chain}${kids}${ownerBadgeTemplate(s)}${archived}</div>${famRows}${skip}${pick}${selectable ? nothing : cardToolsTemplate(s, { deletable, archivable })}</div>`;
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
function cardToolsTemplate(s, { deletable, archivable }) {
|
|
@@ -223,7 +231,7 @@ export function createListsView(deps) {
|
|
|
223
231
|
const {
|
|
224
232
|
doc, win, topStripEl, allListEl, sessions, selected, scope, scopeError, eventsLive, refresh, select,
|
|
225
233
|
confirmDelete, setArchived, archiveOne, updateInfoBar, updateSessionTitle, showToast,
|
|
226
|
-
allViewOpen, openAllView, leaveAllView,
|
|
234
|
+
allViewOpen, openAllView, leaveAllView, renameShown = () => false, onRename = () => {}, renaming = () => false,
|
|
227
235
|
} = deps;
|
|
228
236
|
const $ = (s) => doc.querySelector(s);
|
|
229
237
|
|
|
@@ -261,9 +269,11 @@ export function createListsView(deps) {
|
|
|
261
269
|
return chainTitle(s, sessionsById());
|
|
262
270
|
}
|
|
263
271
|
|
|
272
|
+
// A card with no title yet ("—": no first message) has no ✎ either.
|
|
264
273
|
function cardFor(s, opts) {
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
const title = cardTitle(s);
|
|
275
|
+
return cardTemplate(s, { ...opts, renamable: !!opts.renamable && title !== "\u2014" }, {
|
|
276
|
+
title,
|
|
267
277
|
projectName: scope.projectName,
|
|
268
278
|
activeId: selected(),
|
|
269
279
|
chainOpen: !!opts.family?.chain && chainOpenHead() === s.id,
|
|
@@ -294,9 +304,10 @@ export function createListsView(deps) {
|
|
|
294
304
|
const canHover = win.matchMedia?.("(hover: hover)").matches ?? false;
|
|
295
305
|
function listOrder(el, next, idOf = (s) => s.id) {
|
|
296
306
|
// The strip's popover holds it too (the pointer leaves the strip for it,
|
|
297
|
-
// and a touch screen has no hover at all)
|
|
307
|
+
// and a touch screen has no hover at all), and so does a rename typed
|
|
308
|
+
// over its active card (the card doesn't jump away under the input).
|
|
298
309
|
const holding = el.getClientRects().length > 0
|
|
299
|
-
&& ((canHover && el.matches(":hover")) || (el === topStripEl && familyPop.isOpen()) || chainPopIn(el));
|
|
310
|
+
&& ((canHover && el.matches(":hover")) || (el === topStripEl && (familyPop.isOpen() || renaming())) || chainPopIn(el));
|
|
300
311
|
const held = holding ? drawnOrder.get(el) : null;
|
|
301
312
|
const ordered = held ? heldOrder(held, next, idOf) : next;
|
|
302
313
|
if (held && ordered.some((x, i) => idOf(x) !== idOf(next[i]))) heldLists.add(el);
|
|
@@ -429,6 +440,12 @@ export function createListsView(deps) {
|
|
|
429
440
|
onFamily(el.dataset.id, chip, e);
|
|
430
441
|
return;
|
|
431
442
|
}
|
|
443
|
+
const ren = e.target.closest(".card-rename");
|
|
444
|
+
if (ren) {
|
|
445
|
+
e.stopPropagation();
|
|
446
|
+
onRename();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
432
449
|
const del = e.target.closest(".card-del");
|
|
433
450
|
if (del) {
|
|
434
451
|
e.stopPropagation();
|
|
@@ -497,7 +514,7 @@ export function createListsView(deps) {
|
|
|
497
514
|
}
|
|
498
515
|
const anim = enteringFor(topStripEl, [...cards.map(familyId), STRIP_TILE]);
|
|
499
516
|
const tile = stripTileTemplate(stripCountText(shownSessions().length, all.length), anim(STRIP_TILE));
|
|
500
|
-
draw(topStripEl, html`${cards.map((f) => cardFor(f.head, { family: f, open: familyPop.openHead() === f.head.id, ...anim(familyId(f)) }))}${tile}`);
|
|
517
|
+
draw(topStripEl, html`${cards.map((f) => cardFor(f.head, { family: f, open: familyPop.openHead() === f.head.id, renamable: renameShown(f.head.owner), ...anim(familyId(f)) }))}${tile}`);
|
|
501
518
|
bindCards(topStripEl, select, {
|
|
502
519
|
onFamily: (id, _chip, e) => {
|
|
503
520
|
familyPop.toggle(id);
|
|
@@ -769,6 +786,13 @@ export function createListsView(deps) {
|
|
|
769
786
|
return {
|
|
770
787
|
render,
|
|
771
788
|
renderAll,
|
|
789
|
+
// The rename input closed: a strip held for it catches up (unless the
|
|
790
|
+
// pointer or a popover holds it still).
|
|
791
|
+
catchUp() {
|
|
792
|
+
if (heldLists.has(topStripEl) && !(canHover && topStripEl.matches(":hover")) && !familyPop.isOpen() && !chainPopIn(topStripEl)) {
|
|
793
|
+
renderTop(stripCards());
|
|
794
|
+
}
|
|
795
|
+
},
|
|
772
796
|
paintActive,
|
|
773
797
|
cardTitle,
|
|
774
798
|
sessionsById,
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
// a final answer's rendering and refs, and the refs across the page. The
|
|
4
4
|
// page's state comes in as getters (capabilities, roots, currentDir), read
|
|
5
5
|
// at each call.
|
|
6
|
+
|
|
7
|
+
// The texts whose refs linkTextRefs links: the answers and the closed
|
|
8
|
+
// steps' texts.
|
|
9
|
+
const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
|
|
10
|
+
|
|
6
11
|
export function createMarkdownPlace({
|
|
7
12
|
capabilities, renderText, roots, touchedFiles, currentDir, linkRefs, applyFinalAnswer, findInTurnRoots, normalize,
|
|
8
|
-
pairAnswerRefs,
|
|
13
|
+
pairAnswerRefs, setAnswerRefs = () => {},
|
|
9
14
|
}) {
|
|
10
15
|
// A text the page already shows as its markdown HTML (POST /api/render,
|
|
11
16
|
// sanitised there), or null: markdown off here (no request), or a failed
|
|
@@ -17,9 +22,20 @@ export function createMarkdownPlace({
|
|
|
17
22
|
.catch(() => null);
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
25
|
+
// A plugin's +refs+ (a question's: its pending question's refs) paired
|
|
26
|
+
// with one shown text (answer_refs.js), again after each swap of its
|
|
27
|
+
// nodes: setAnswerRefs starts the pairing over. Not +refs+: nothing.
|
|
28
|
+
function pairRefsIn(el, refs) {
|
|
29
|
+
if (refs === undefined || refs === null) return;
|
|
30
|
+
setAnswerRefs(el, refs);
|
|
31
|
+
pairAnswerRefs(el);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The refs in one rendered text (the stage's asking slot, a kept reply),
|
|
35
|
+
// as linkTextRefs links the answers': a plugin's +refs+ first
|
|
36
|
+
// (pairRefsIn), then the `path:line` ones.
|
|
37
|
+
function linkRefsIn(el, refs) {
|
|
38
|
+
pairRefsIn(el, refs);
|
|
23
39
|
linkRefs(el, touchedFiles(roots()), currentDir());
|
|
24
40
|
}
|
|
25
41
|
|
|
@@ -32,7 +48,8 @@ export function createMarkdownPlace({
|
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
// The refs on the answers' and closed steps' texts, on this page: first
|
|
35
|
-
// each
|
|
51
|
+
// each one's own (a plugin's, answer_refs.js: a step's are a question's,
|
|
52
|
+
// on the reply that asked it), then the `path:line`
|
|
36
53
|
// refs that name a file the session's rows (and cards) touched
|
|
37
54
|
// (ref_scan.js), which leave a plugin's alone. Not mid-stream: at a
|
|
38
55
|
// turn's end and after a reload's render. A ref already made stays as it
|
|
@@ -41,13 +58,13 @@ export function createMarkdownPlace({
|
|
|
41
58
|
const pageRoots = roots();
|
|
42
59
|
const touched = touchedFiles(pageRoots);
|
|
43
60
|
for (const root of pageRoots) {
|
|
44
|
-
for (const el of root.querySelectorAll(
|
|
61
|
+
for (const el of root.querySelectorAll(TEXTS)) pairAnswerRefs(el);
|
|
45
62
|
}
|
|
46
63
|
if (!touched.size) return;
|
|
47
64
|
for (const root of pageRoots) {
|
|
48
|
-
for (const el of root.querySelectorAll(
|
|
65
|
+
for (const el of root.querySelectorAll(TEXTS)) linkRefs(el, touched, currentDir());
|
|
49
66
|
}
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
return { renderMarkdown, linkRefsIn, applyFinalMarkdown, linkTextRefs };
|
|
69
|
+
return { renderMarkdown, pairRefsIn, linkRefsIn, applyFinalMarkdown, linkTextRefs };
|
|
53
70
|
}
|
|
@@ -35,7 +35,10 @@ import { plainText } from "./plain_line.js";
|
|
|
35
35
|
// askingText via the page's view), kept by a question or own approval card
|
|
36
36
|
// @param renderMarkdown (text) => Promise<html|null>: the kept reply's
|
|
37
37
|
// markdown (the page's POST /api/render; null keeps the raw text)
|
|
38
|
-
// @param linkRefs (el) the page's
|
|
38
|
+
// @param linkRefs (el, refs) the page's ref pass on it: a plugin's +refs+
|
|
39
|
+
// (the question's) paired, then the path:line ones
|
|
40
|
+
// @param pairRefs (el, refs) a plugin's +refs+ paired only (the question's
|
|
41
|
+
// own text; markdown_place.js pairRefsIn)
|
|
39
42
|
// @param api {sendAnswer, dismissQuestion, sendCommand} (data.js)
|
|
40
43
|
// @param doc the page's document
|
|
41
44
|
// @return {renderQuestion(pq, {said}), resolveQuestion(id, opts), removeQuestion(),
|
|
@@ -43,7 +46,7 @@ import { plainText } from "./plain_line.js";
|
|
|
43
46
|
// renderContinue(offer), resolveContinue(data), forgetContinue()}
|
|
44
47
|
export function createQuestionCards({
|
|
45
48
|
sessionId, clientId, place, isNearBottom, scrollToEnd, removeHintIfEmpty, askingText = () => "",
|
|
46
|
-
renderMarkdown = () => Promise.resolve(null), linkRefs = () => {},
|
|
49
|
+
renderMarkdown = () => Promise.resolve(null), linkRefs = () => {}, pairRefs = () => {},
|
|
47
50
|
api = { sendAnswer, dismissQuestion, sendCommand }, doc = globalThis.document,
|
|
48
51
|
}) {
|
|
49
52
|
// Live ask_user_question card, keyed by pending question id. Re-rendered from
|
|
@@ -73,6 +76,10 @@ export function createQuestionCards({
|
|
|
73
76
|
const card = doc.createElement("details");
|
|
74
77
|
card.className = approval ? "bubble question approval" : stepLimit ? "bubble question step-limit" : "bubble question";
|
|
75
78
|
card.dataset.qid = pq.id;
|
|
79
|
+
// What asks (the stage's question room is a model's own question's,
|
|
80
|
+
// stage_view.js): the kind, and a delegate's question relayed here.
|
|
81
|
+
card.dataset.kind = pq.kind || "question";
|
|
82
|
+
if (relayView(pq)) card.dataset.relay = "1";
|
|
76
83
|
const asked = keepsAskingText(pq) ? String(said ?? askingText() ?? "").trim() : "";
|
|
77
84
|
if (asked) saidOf.set(card, asked);
|
|
78
85
|
// Open while pending: the question must be actionable.
|
|
@@ -98,6 +105,8 @@ export function createQuestionCards({
|
|
|
98
105
|
const q = doc.createElement("div");
|
|
99
106
|
q.className = "question-text";
|
|
100
107
|
q.textContent = (relay && pq.relay.asked) || pq.question || "";
|
|
108
|
+
// A plugin's refs on the question (its step's: one may name a ticket here too).
|
|
109
|
+
if (!relay) pairRefs(q, questionRefsOf(pq));
|
|
101
110
|
card.appendChild(q);
|
|
102
111
|
}
|
|
103
112
|
|
|
@@ -421,6 +430,7 @@ export function createQuestionCards({
|
|
|
421
430
|
// clamped line), and whole, selectable, at the top of the body
|
|
422
431
|
// when it is open (a drag in the summary would toggle the card).
|
|
423
432
|
const asked = saidOf.get(questionEl);
|
|
433
|
+
const refs = questionRefsOf(pending);
|
|
424
434
|
let full = null;
|
|
425
435
|
if (summary && asked) {
|
|
426
436
|
const line = doc.createElement("div");
|
|
@@ -431,6 +441,7 @@ export function createQuestionCards({
|
|
|
431
441
|
full = doc.createElement("div");
|
|
432
442
|
full.className = "question-said-full";
|
|
433
443
|
full.textContent = asked;
|
|
444
|
+
pairRefs(full, refs);
|
|
434
445
|
questionEl.insertBefore(full, summary.nextSibling);
|
|
435
446
|
}
|
|
436
447
|
// 4.02: a reopened card's summary already carries the result (the
|
|
@@ -441,15 +452,15 @@ export function createQuestionCards({
|
|
|
441
452
|
if (summary && !keepNote) note.classList.add("hidden");
|
|
442
453
|
if (!questionEl.dataset.toggled) questionEl.open = false;
|
|
443
454
|
// After the collapse: a card left open by hand renders at once.
|
|
444
|
-
if (full) renderSaidOnOpen(questionEl, full, asked);
|
|
455
|
+
if (full) renderSaidOnOpen(questionEl, full, asked, refs);
|
|
445
456
|
pending = null;
|
|
446
457
|
}
|
|
447
458
|
|
|
448
459
|
// The body's copy as markdown, from the card's first open on (a reload
|
|
449
460
|
// draws every card in the history: rendering them all would ask once per
|
|
450
461
|
// card). Raw until then, and when the render answers null (then the next
|
|
451
|
-
// open asks again).
|
|
452
|
-
function renderSaidOnOpen(card, full, asked) {
|
|
462
|
+
// open asks again). +refs+: the question's, paired again on the swap.
|
|
463
|
+
function renderSaidOnOpen(card, full, asked, refs) {
|
|
453
464
|
let state = "raw";
|
|
454
465
|
const draw = () => {
|
|
455
466
|
if (!card.open || state !== "raw") return;
|
|
@@ -463,13 +474,19 @@ export function createQuestionCards({
|
|
|
463
474
|
// Sanitised on the server (POST /api/render).
|
|
464
475
|
full.innerHTML = html;
|
|
465
476
|
full.classList.add("markdown", "md-compact");
|
|
466
|
-
linkRefs(full);
|
|
477
|
+
linkRefs(full, refs);
|
|
467
478
|
});
|
|
468
479
|
};
|
|
469
480
|
card.addEventListener("toggle", draw);
|
|
470
481
|
draw();
|
|
471
482
|
}
|
|
472
483
|
|
|
484
|
+
// A pending question's refs (a plugin's, on the reply that asks it), or
|
|
485
|
+
// null: none, or not an array.
|
|
486
|
+
function questionRefsOf(pq) {
|
|
487
|
+
return Array.isArray(pq?.refs) && pq.refs.length ? pq.refs : null;
|
|
488
|
+
}
|
|
489
|
+
|
|
473
490
|
function markQuestionSelection(sel) {
|
|
474
491
|
if (!questionEl || !sel.length) return;
|
|
475
492
|
questionEl.querySelectorAll(".question-choice").forEach((button) => {
|