samagotchi 0.50.0 → 0.51.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 +44 -1
- data/docs/cli.md +21 -7
- data/docs/configuration.md +11 -9
- data/docs/hooks.md +3 -2
- data/docs/internals/engine.md +4 -2
- data/docs/sessions.md +5 -4
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +24 -9
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +33 -0
- data/lib/samagotchi/host_registry.rb +16 -3
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -2
- data/lib/samagotchi/send_command.rb +63 -10
- data/lib/samagotchi/session_commands.rb +9 -0
- data/lib/samagotchi/session_inbox.rb +27 -7
- data/lib/samagotchi/session_manager.rb +13 -3
- data/lib/samagotchi/session_metrics.rb +25 -6
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +51 -4
- data/lib/samagotchi/terminal_ui.rb +48 -5
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -2
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +66 -28
- data/lib/samagotchi/web/public/index.html +17 -15
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/turn_events.js +3 -2
- data/lib/samagotchi/web/public/turn_html.js +3 -1
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_inbound.rb +8 -5
- data/lib/samagotchi.rb +1 -0
- metadata +3 -1
|
@@ -752,6 +752,14 @@ module Samagotchi
|
|
|
752
752
|
def run_input_line(session, input)
|
|
753
753
|
return if input.empty?
|
|
754
754
|
|
|
755
|
+
# /cut TEXT and /queue TEXT are deliveries only while a turn runs; at
|
|
756
|
+
# the prompt both run the text as a normal turn, as their usage says.
|
|
757
|
+
if (choice = steer_choice(input))
|
|
758
|
+
return @surface.commit(STEER_CHOICE_USAGE) if choice.last.empty?
|
|
759
|
+
|
|
760
|
+
input = choice.last
|
|
761
|
+
end
|
|
762
|
+
|
|
755
763
|
# /model, /models, !rollback, !cmd, /continue (shared with workers);
|
|
756
764
|
# an anytime command's cards print as it shows them (btw's
|
|
757
765
|
# "thinking…" before the answer).
|
|
@@ -1083,6 +1091,10 @@ module Samagotchi
|
|
|
1083
1091
|
# plugin's /btw, /model alone) starts now on its own thread; one that
|
|
1084
1092
|
# runs after the turn (/model X, !cmd) waits in the queue, in arrival
|
|
1085
1093
|
# order with the other lines that wait; others go back in the prompt.
|
|
1094
|
+
#
|
|
1095
|
+
# A plain line goes in at the running turn's next step boundary and
|
|
1096
|
+
# never cuts; /cut TEXT asks for a cut, /queue TEXT waits for the turn
|
|
1097
|
+
# to end and runs as a turn of its own.
|
|
1086
1098
|
# @return [Boolean, :back] whether the turn took it, :back to put it back
|
|
1087
1099
|
def steer_line(line)
|
|
1088
1100
|
local = local_command(line) unless line.nil?
|
|
@@ -1091,6 +1103,12 @@ module Samagotchi
|
|
|
1091
1103
|
return detach_note if local == :detach
|
|
1092
1104
|
return false if @active_cancel_controller&.cancelled?
|
|
1093
1105
|
|
|
1106
|
+
# Before every command check: /cut and /queue are the UIs' own words
|
|
1107
|
+
# for a delivery (they are listed for Tab and /help, never run).
|
|
1108
|
+
if (choice = steer_choice(line))
|
|
1109
|
+
return submit_steer_choice(*choice)
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1094
1112
|
policy = command_registry.mid_turn(line)
|
|
1095
1113
|
return start_anytime_command(line) if policy == :anytime
|
|
1096
1114
|
return command_queued(line) if policy == :queue
|
|
@@ -1108,12 +1126,37 @@ module Samagotchi
|
|
|
1108
1126
|
# Behind a command that waits for the turn's end, a line waits too.
|
|
1109
1127
|
return wait_for_turn_end(line) if @command_waits
|
|
1110
1128
|
|
|
1111
|
-
epoch = @engine.input_epoch
|
|
1112
1129
|
@pending_input_queue.push(line.strip)
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1130
|
+
@surface.commit(NEXT_STEP_NOTE)
|
|
1131
|
+
true
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
# /cut TEXT and /queue TEXT during a turn, which the REPL would
|
|
1135
|
+
# otherwise read as prompts (SessionCommands registers them for Tab and
|
|
1136
|
+
# help only). A cut asks the engine to cut the generation only thinking
|
|
1137
|
+
# too long; a queue waits for the turn's end (the REPL's own after-turn
|
|
1138
|
+
# list). Empty text gets the usage line. A line with images never steers
|
|
1139
|
+
# (as a plain one doesn't).
|
|
1140
|
+
# @return [Boolean] whether the turn took it
|
|
1141
|
+
def submit_steer_choice(choice, text)
|
|
1142
|
+
if text.empty?
|
|
1143
|
+
@surface.commit(STEER_CHOICE_USAGE)
|
|
1144
|
+
return true
|
|
1145
|
+
end
|
|
1146
|
+
return image_line_waits(text) if ImageInput.extract(text).any?
|
|
1147
|
+
|
|
1148
|
+
# A /queue waits for the turn's end; a /cut behind a command that
|
|
1149
|
+
# already waits there goes after it too.
|
|
1150
|
+
if choice == :queue || @command_waits
|
|
1151
|
+
@surface.commit(COMMAND_QUEUED)
|
|
1152
|
+
return wait_for_turn_end(text)
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
epoch = @engine.input_epoch
|
|
1156
|
+
@pending_input_queue.push(text)
|
|
1157
|
+
# With no session running the cut is :off; the text still waits for
|
|
1158
|
+
# the running turn's next step.
|
|
1159
|
+
@surface.commit(steer_cut_note(@engine.cut_for_steer(nil, epoch: epoch)))
|
|
1117
1160
|
true
|
|
1118
1161
|
end
|
|
1119
1162
|
|
|
@@ -47,6 +47,9 @@ module Samagotchi
|
|
|
47
47
|
}
|
|
48
48
|
title = tool_title(tool_name, call, cwd: cwd)
|
|
49
49
|
event[:title] = title if title
|
|
50
|
+
# The name the model used when an alias ran a built-in (called_as: "bash"
|
|
51
|
+
# for an execute), for a UI that shows it.
|
|
52
|
+
event[:called_as] = call[:called_as] if call.is_a?(Hash) && call[:called_as]
|
|
50
53
|
# The model's own words for a command, which the TUI's tool line
|
|
51
54
|
# shows in place of the cut params.
|
|
52
55
|
description = command_description(tool_name, call)
|
|
@@ -57,6 +57,8 @@ module Samagotchi
|
|
|
57
57
|
# The worker runs in its session's working directory.
|
|
58
58
|
title = ToolActivity.tool_title(call[:name], call, cwd: Dir.pwd)
|
|
59
59
|
started[:title] = title if title
|
|
60
|
+
# The name the model called it by when that was an alias's (bash).
|
|
61
|
+
started[:called_as] = call[:called_as] if call[:called_as]
|
|
60
62
|
# The full command for a richer UI (params stays cut at 80).
|
|
61
63
|
view = ToolView.for(call[:name], call)&.to_h
|
|
62
64
|
started[:view] = view if view
|
|
@@ -56,6 +56,13 @@ module Samagotchi
|
|
|
56
56
|
"forget_outputs" => { content: "note", also: %w[note], verbatim: %w[ids keep restore] }
|
|
57
57
|
}.freeze
|
|
58
58
|
|
|
59
|
+
# A name the model uses for a built-in, mapped to that built-in (a model
|
|
60
|
+
# that keeps calling `bash` with execute's arguments; before this such a
|
|
61
|
+
# call died as an unknown tool). It only takes effect when the
|
|
62
|
+
# arguments really are the target's shape; the call then carries
|
|
63
|
+
# called_as: with the model's own spelling.
|
|
64
|
+
ALIASES = { "bash" => "execute" }.freeze
|
|
65
|
+
|
|
59
66
|
# One built-in's mapping, resolved from its schema and OVERRIDES.
|
|
60
67
|
Row = Data.define(:name, :content_keys, :path_key, :fields, :aliases, :verbatim, :fallback, :fallback_key,
|
|
61
68
|
:options) do
|
|
@@ -109,6 +116,14 @@ module Samagotchi
|
|
|
109
116
|
def build(name, args, raw: nil)
|
|
110
117
|
name = name.to_s
|
|
111
118
|
args = args.is_a?(Hash) ? args.transform_keys(&:to_s) : {}
|
|
119
|
+
# An alias runs its target (bash → execute); the row is the target's,
|
|
120
|
+
# and the call keeps the model's own spelling on called_as:.
|
|
121
|
+
# Not the target's shape (a String main argument, only its keys) and
|
|
122
|
+
# the alias doesn't take: the call passes through as an unknown tool.
|
|
123
|
+
if (target = ALIASES[name.downcase])
|
|
124
|
+
return aliasable?(row(target), args) ? aliased(name, target, args, raw) : passthrough(name, args, raw)
|
|
125
|
+
end
|
|
126
|
+
|
|
112
127
|
row = row(name)
|
|
113
128
|
return passthrough(name, args, raw) unless row
|
|
114
129
|
|
|
@@ -120,6 +135,24 @@ module Samagotchi
|
|
|
120
135
|
call
|
|
121
136
|
end
|
|
122
137
|
|
|
138
|
+
# An execute-shaped call the model spelled +name+: built as its +target+,
|
|
139
|
+
# tagged with the model's spelling for the activity event and the log.
|
|
140
|
+
def aliased(name, target, args, raw)
|
|
141
|
+
call = build(target, args, raw: raw)
|
|
142
|
+
call[:called_as] = name
|
|
143
|
+
call
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The alias gate: a String main argument, and every key one the target's
|
|
147
|
+
# schema knows (its aliases included). More, or differently shaped, the
|
|
148
|
+
# call is the model's own and passes through as an unknown tool.
|
|
149
|
+
def aliasable?(row, args)
|
|
150
|
+
return false unless row
|
|
151
|
+
|
|
152
|
+
row.content_keys.map { |key| value(row, args, key) }.any?(String) &&
|
|
153
|
+
(args.keys - (row.content_keys + [row.path_key].compact + row.fields + row.aliases.keys)).empty?
|
|
154
|
+
end
|
|
155
|
+
|
|
123
156
|
def content_value(row, args)
|
|
124
157
|
found = row.content_keys.lazy.map { |key| value(row, args, key) }.find { |v| !v.nil? }
|
|
125
158
|
found.nil? ? "" : found.to_s
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -1065,8 +1065,12 @@ module Samagotchi
|
|
|
1065
1065
|
images = turn_images(id, body["images"])
|
|
1066
1066
|
return error_response(400, "bad_images", images) if images.is_a?(String)
|
|
1067
1067
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1068
|
+
delivery = body["delivery"] || body[:delivery]
|
|
1069
|
+
return error_response(400, "bad_delivery", "delivery must be a string") unless delivery.nil? || delivery.is_a?(String)
|
|
1070
|
+
|
|
1071
|
+
result = SessionManager.deliver_turn(id, prompt: prompt.to_s, client_id: client_id, images: images,
|
|
1072
|
+
delivery: delivery, state_dir: @state_dir, manager: @manager,
|
|
1073
|
+
bridge: -> { live_bridge_client(id) })
|
|
1070
1074
|
case result[:status]
|
|
1071
1075
|
when :accepted
|
|
1072
1076
|
record_history(body, prompt)
|
|
@@ -233,6 +233,9 @@ module Samagotchi
|
|
|
233
233
|
part = { tool: name, params: params.to_s }
|
|
234
234
|
title = ToolActivity.tool_title(name, call, cwd: cwd)
|
|
235
235
|
part[:title] = title if title
|
|
236
|
+
# The name the model called it by when that was an alias's (the
|
|
237
|
+
# saved call is rebuilt by BuiltinCalls, which sets it again).
|
|
238
|
+
part[:called_as] = call[:called_as] if call[:called_as]
|
|
236
239
|
# Built from the saved call (never saved itself), so older sessions
|
|
237
240
|
# get it too.
|
|
238
241
|
view = ToolView.for(name, call)&.to_h
|
|
@@ -53,6 +53,7 @@ export function addStarted(model, event = {}, now = null) {
|
|
|
53
53
|
row.label = event.label || row.label;
|
|
54
54
|
row.params = event.params || row.params;
|
|
55
55
|
row.title = event.title || row.title;
|
|
56
|
+
row.called_as = event.called_as || row.called_as;
|
|
56
57
|
row.view = event.view || row.view;
|
|
57
58
|
markStart(row, event, now);
|
|
58
59
|
return { row, created: false };
|
|
@@ -68,6 +69,9 @@ export function addStarted(model, event = {}, now = null) {
|
|
|
68
69
|
// project-relative path, a command without its "cd … &&"); rows show it
|
|
69
70
|
// in place of the params when there is one.
|
|
70
71
|
title: event.title || null,
|
|
72
|
+
// The name the model called the tool by when it was an alias's ("bash"
|
|
73
|
+
// run as execute); the row says so.
|
|
74
|
+
called_as: event.called_as || null,
|
|
71
75
|
// The call's full arguments for the expanded row and the hovers (the
|
|
72
76
|
// server's ToolView: an execute/task_create's command, its cwd).
|
|
73
77
|
view: event.view || null,
|
|
@@ -104,6 +108,7 @@ export function addCompleted(model, event = {}, now = null) {
|
|
|
104
108
|
label: event.label,
|
|
105
109
|
params: event.activity?.params || "",
|
|
106
110
|
title: event.activity?.title,
|
|
111
|
+
called_as: event.activity?.called_as,
|
|
107
112
|
view: event.view,
|
|
108
113
|
});
|
|
109
114
|
row = synthesized.row;
|
|
@@ -113,6 +118,7 @@ export function addCompleted(model, event = {}, now = null) {
|
|
|
113
118
|
row.output_truncated = !!event.output_truncated;
|
|
114
119
|
if (event.activity?.params) row.params = event.activity.params;
|
|
115
120
|
if (event.activity?.title) row.title = event.activity.title;
|
|
121
|
+
if (event.activity?.called_as) row.called_as = event.activity.called_as;
|
|
116
122
|
// The images the tool returned (a reload in the turn view draws them from
|
|
117
123
|
// the saved parts, turn_html.js toolRowHtml).
|
|
118
124
|
if (event.images?.length) row.images = event.images;
|
|
@@ -56,6 +56,7 @@ import { shouldFollowScroll, keepFollowing } from "./scroll.js";
|
|
|
56
56
|
import { SHORT_WINDOW_QUERY, stripAutoHidden, stripColumns, stripShowParts } from "./strip.js";
|
|
57
57
|
import { applySessionEvent, batchCounts, batchTargets, batchToastText, families, familyId, familyOpen, familyWaits, filterFamilies, flattenFamilies, heldOrder, inTree, rangeIds, runBatch, listedSessions, sortedByUpdated, stoppedBadge, waitingBadge, waitingFirst, waitingSearchText } from "./sessions_list.js";
|
|
58
58
|
import { createNotifications } from "./notifications.js";
|
|
59
|
+
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
59
60
|
import { restartConfirmText, versionNotice, workerBadge } from "./update_notice.js";
|
|
60
61
|
import {
|
|
61
62
|
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, mergeTiming, normalizeTiming, STOPPED_LINE_CLASS,
|
|
@@ -71,7 +72,11 @@ const historyEl = $("#history");
|
|
|
71
72
|
const emptyEl = $("#empty");
|
|
72
73
|
const composerEl = $("#composer");
|
|
73
74
|
const infoBarEl = $("#infoBar");
|
|
74
|
-
|
|
75
|
+
// The open session's title in the top bar, when no strip card in sight
|
|
76
|
+
// shows it; the tab's base ("Chi · <project>"), set by renderScope.
|
|
77
|
+
const sessionTitleEl = $("#sessionTitle");
|
|
78
|
+
let tabBase = null;
|
|
79
|
+
let tabShown = null;
|
|
75
80
|
const infoTextEl = $("#infoText");
|
|
76
81
|
// The open session's attached context, in its own row of the session bar
|
|
77
82
|
// (updateInfoBar redraws #infoText every tick; the chips keep their popover).
|
|
@@ -342,7 +347,9 @@ function renderScope() {
|
|
|
342
347
|
el.innerHTML = `<span class="scope-name" title="Sessions of every project">all projects</span>${backLink}${where ? `<span class="scope-where" title="New chats start in ${escapeHtml(where)}">new chat in<span class="path"><bdi>${escapeHtml(where)}</bdi></span></span>` : ""}`;
|
|
343
348
|
}
|
|
344
349
|
el.hidden = false;
|
|
345
|
-
|
|
350
|
+
// The tab: "<session title> · Chi · <project>" (updateSessionTitle).
|
|
351
|
+
tabBase = scope.projectName ? `Chi · ${scope.projectName}` : "Chi";
|
|
352
|
+
updateSessionTitle();
|
|
346
353
|
// The links follow the hash (an open session, the all-sessions view).
|
|
347
354
|
el.querySelector(".scope-all:not(.scope-back)")?.addEventListener("click", (e) => {
|
|
348
355
|
e.currentTarget.href = allScopeHref(location.hash, scope.dir, location.search);
|
|
@@ -642,6 +649,8 @@ function paintActive() {
|
|
|
642
649
|
document.querySelectorAll(".family-row[data-id]").forEach((el) => {
|
|
643
650
|
el.classList.toggle("active", !!selected && el.dataset.id === selected);
|
|
644
651
|
});
|
|
652
|
+
// The open session's card may have come into the strip or left it.
|
|
653
|
+
updateSessionTitle();
|
|
645
654
|
}
|
|
646
655
|
|
|
647
656
|
// The latest sessions (as many as fit, a family one card), then the way
|
|
@@ -693,11 +702,11 @@ function noteChainFamilies(fams) {
|
|
|
693
702
|
}
|
|
694
703
|
|
|
695
704
|
// The hidden strip's pill: "▾ 5 sessions · ● 1 live · ● 1 waiting",
|
|
696
|
-
// following the list: its
|
|
697
|
-
//
|
|
705
|
+
// following the list: its sessions, the number the All sessions tile
|
|
706
|
+
// shows, and the live and waiting ones (a waiting delegate too).
|
|
698
707
|
function renderStripShow() {
|
|
699
708
|
const shown = shownSessions();
|
|
700
|
-
const { count, live, waiting } = stripShowParts(shown
|
|
709
|
+
const { count, live, waiting } = stripShowParts(shown);
|
|
701
710
|
const liveHtml = live ? ` · <span class="live"><span class="live-dot" aria-hidden="true"></span>${escapeHtml(live)}</span>` : "";
|
|
702
711
|
const waitingHtml = waiting ? ` · <span class="waiting"><span class="waiting-dot" aria-hidden="true"></span>${escapeHtml(waiting)}</span>` : "";
|
|
703
712
|
$("#stripShow").innerHTML = `<span class="pill-arrow" aria-hidden="true">\u25BE</span> ${escapeHtml(count)}${liveHtml}${waitingHtml}`;
|
|
@@ -999,6 +1008,7 @@ function applyView() {
|
|
|
999
1008
|
topStripEl.scrollLeft = 0; // a phone's strip scrolls sideways to the tile
|
|
1000
1009
|
}
|
|
1001
1010
|
document.body.classList.toggle("all-view", open);
|
|
1011
|
+
updateSessionTitle();
|
|
1002
1012
|
// Its chip was in the list just hidden (the strip, or all sessions).
|
|
1003
1013
|
chainPop.close();
|
|
1004
1014
|
if (open) {
|
|
@@ -1037,6 +1047,7 @@ function updateComposerMode() {
|
|
|
1037
1047
|
if (document.body.classList.toggle("has-session", !!selected) !== hadSession) {
|
|
1038
1048
|
hadSession = !hadSession;
|
|
1039
1049
|
fitPrompt();
|
|
1050
|
+
placeToast();
|
|
1040
1051
|
}
|
|
1041
1052
|
if (selected) {
|
|
1042
1053
|
modelPicker.close();
|
|
@@ -1148,7 +1159,6 @@ function clearSelection() {
|
|
|
1148
1159
|
setCapabilities(null);
|
|
1149
1160
|
updateComposerMode();
|
|
1150
1161
|
updateInfoBar();
|
|
1151
|
-
updateFirstPeek();
|
|
1152
1162
|
promptEl.focus();
|
|
1153
1163
|
}
|
|
1154
1164
|
|
|
@@ -1182,7 +1192,6 @@ async function select(id) {
|
|
|
1182
1192
|
closeStream();
|
|
1183
1193
|
resetSessionState();
|
|
1184
1194
|
resetTurnView();
|
|
1185
|
-
updateFirstPeek();
|
|
1186
1195
|
updateInfoBar();
|
|
1187
1196
|
contextChips.load(id);
|
|
1188
1197
|
updateComposerMode();
|
|
@@ -1334,7 +1343,6 @@ function renderSessionView(data) {
|
|
|
1334
1343
|
// A turn ran out of iterations and waits for a yes or no.
|
|
1335
1344
|
if (data.continue_offer) questionCards.renderContinue(data.continue_offer);
|
|
1336
1345
|
updateInfoBar();
|
|
1337
|
-
updateFirstPeek();
|
|
1338
1346
|
updateComposerMode();
|
|
1339
1347
|
}
|
|
1340
1348
|
|
|
@@ -1922,11 +1930,11 @@ function updateInfoBar() {
|
|
|
1922
1930
|
renderStable(infoTextEl, "");
|
|
1923
1931
|
infoTextEl.title = "";
|
|
1924
1932
|
if (stopBtn) stopBtn.style.display = "none";
|
|
1933
|
+
updateSessionTitle();
|
|
1925
1934
|
return;
|
|
1926
1935
|
}
|
|
1927
1936
|
if (stopBtn) stopBtn.style.display = canStopSession({ owner: current.owner, streaming: !!streamControl }) ? "" : "none";
|
|
1928
1937
|
const idShort = selected.slice(0,8);
|
|
1929
|
-
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1930
1938
|
const ctxText = ctxBarText(current.ctxPct, current.ctxWindowSource);
|
|
1931
1939
|
const tokens = current.timing.tokens;
|
|
1932
1940
|
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex,
|
|
@@ -1958,15 +1966,37 @@ function updateInfoBar() {
|
|
|
1958
1966
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1959
1967
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1960
1968
|
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${chainHtml}${parentHtml}${kidsHtml}${llmContextChipHtml(current.llmContext, escapeHtml)}${notesHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1961
|
-
// The title line follows the card's: a chain link opened with the opener
|
|
1962
|
-
// reads its chain's first link's title (cardTitle).
|
|
1963
|
-
const shown = sessionsById().get(selected);
|
|
1964
|
-
const titleText = shown && isOpener(current.firstPreview) ? cardTitle(shown) : current.firstPreview;
|
|
1965
|
-
const previewHtml = `<div class="preview-line"><span class="preview">${escapeHtml(titleText || "—")}</span> · <span class="mem" title="${escapeHtml(current.usedMemories.join(", "))}">mem: ${escapeHtml(memText)}</span></div>`;
|
|
1966
1969
|
// The session clock ticks every second while a turn runs: only its text
|
|
1967
1970
|
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
1968
|
-
renderStable(infoTextEl, metaHtml
|
|
1971
|
+
renderStable(infoTextEl, metaHtml, { ".session-time": timingText });
|
|
1969
1972
|
infoTextEl.title = memoriesTooltip(current.usedMemories, current.preloadedMemories, current.mutedMemories);
|
|
1973
|
+
updateSessionTitle();
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// The open session's title, once on screen (session_title.js): its strip
|
|
1977
|
+
// card says it when the strip is shown and has one; else the top bar. The
|
|
1978
|
+
// browser tab names it too. It follows the card's: a chain link opened with
|
|
1979
|
+
// the opener reads its chain's first link's title (cardTitle).
|
|
1980
|
+
function updateSessionTitle() {
|
|
1981
|
+
const shown = selected ? sessionsById().get(selected) : null;
|
|
1982
|
+
const first = current.firstPreview || "";
|
|
1983
|
+
const raw = !selected ? "" : (shown && isOpener(first) ? cardTitle(shown) : first);
|
|
1984
|
+
const title = raw === "\u2014" ? "" : raw; // "—": no first message yet
|
|
1985
|
+
const text = topBarTitle({
|
|
1986
|
+
title,
|
|
1987
|
+
cardShown: !!topStripEl.querySelector(".card.active"),
|
|
1988
|
+
stripHidden: document.body.classList.contains("strip-hidden"),
|
|
1989
|
+
allView: allViewOpen(),
|
|
1990
|
+
});
|
|
1991
|
+
if (sessionTitleEl.textContent !== text) sessionTitleEl.textContent = text;
|
|
1992
|
+
if (sessionTitleEl.hidden !== !text) sessionTitleEl.hidden = !text;
|
|
1993
|
+
const tip = text ? title : "";
|
|
1994
|
+
if (sessionTitleEl.title !== tip) sessionTitleEl.title = tip;
|
|
1995
|
+
const tab = tabBase === null ? null : tabTitle({ title, base: tabBase });
|
|
1996
|
+
if (tab !== null && tab !== tabShown) {
|
|
1997
|
+
tabShown = tab;
|
|
1998
|
+
notifications.setBaseTitle(tab);
|
|
1999
|
+
}
|
|
1970
2000
|
}
|
|
1971
2001
|
|
|
1972
2002
|
// The info bar's ← / → link to a chain's link (chains.js chainNeighbours):
|
|
@@ -2024,16 +2054,6 @@ async function mergeTurnTiming(id, data) {
|
|
|
2024
2054
|
return true;
|
|
2025
2055
|
}
|
|
2026
2056
|
|
|
2027
|
-
function updateFirstPeek() {
|
|
2028
|
-
if (current.ctxPct !== null && current.ctxPct > 20 && current.firstPreview) {
|
|
2029
|
-
firstPeekEl.textContent = current.firstPreview;
|
|
2030
|
-
firstPeekEl.classList.add("visible");
|
|
2031
|
-
} else {
|
|
2032
|
-
firstPeekEl.textContent = "";
|
|
2033
|
-
firstPeekEl.classList.remove("visible");
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
2057
|
function setUsedMemories(names) {
|
|
2038
2058
|
current.usedMemories = names.slice();
|
|
2039
2059
|
updateInfoBar();
|
|
@@ -2119,7 +2139,6 @@ const streamHandlers = {
|
|
|
2119
2139
|
if (pct !== null && !Number.isNaN(pct)) {
|
|
2120
2140
|
current.ctxPct = pct;
|
|
2121
2141
|
updateInfoBar();
|
|
2122
|
-
updateFirstPeek();
|
|
2123
2142
|
}
|
|
2124
2143
|
},
|
|
2125
2144
|
generation_started: (data) => {
|
|
@@ -2308,7 +2327,6 @@ const streamHandlers = {
|
|
|
2308
2327
|
if (pct !== null) {
|
|
2309
2328
|
current.ctxPct = pct;
|
|
2310
2329
|
updateInfoBar();
|
|
2311
|
-
updateFirstPeek();
|
|
2312
2330
|
}
|
|
2313
2331
|
},
|
|
2314
2332
|
question_requested: (data) => {
|
|
@@ -2469,7 +2487,6 @@ async function handleSendTurn() {
|
|
|
2469
2487
|
if (!current.firstPreview || current.firstPreview==="—") {
|
|
2470
2488
|
current.firstPreview = normalize(prompt).slice(0,80) + (normalize(prompt).length>80?"…":"");
|
|
2471
2489
|
updateInfoBar();
|
|
2472
|
-
updateFirstPeek();
|
|
2473
2490
|
}
|
|
2474
2491
|
actionBtn.disabled = true;
|
|
2475
2492
|
try {
|
|
@@ -2771,7 +2788,9 @@ new ResizeObserver(() => {
|
|
|
2771
2788
|
const follow = atHistoryEnd();
|
|
2772
2789
|
centerEl.style.setProperty("--dock-h", `${dockEl.offsetHeight}px`);
|
|
2773
2790
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
2791
|
+
placeToast();
|
|
2774
2792
|
}).observe(dockEl);
|
|
2793
|
+
window.addEventListener("resize", () => placeToast());
|
|
2775
2794
|
|
|
2776
2795
|
promptEl.addEventListener("paste", (e) => {
|
|
2777
2796
|
const files = imageFiles(e.clipboardData);
|
|
@@ -2981,6 +3000,23 @@ async function setArchived(id, archive, { undo = true } = {}) {
|
|
|
2981
3000
|
showToast(`${what} ${short}`, undo ? { label: "Undo", run: () => setArchived(id, !archive, { undo: false }) } : null);
|
|
2982
3001
|
}
|
|
2983
3002
|
|
|
3003
|
+
// The toast sits above the session's composer card (measured: it grows with
|
|
3004
|
+
// a wrapping footer or a taller box), so it never covers the card's
|
|
3005
|
+
// controls; with no card in sight (the start page, all sessions) the CSS's
|
|
3006
|
+
// 24 px from the bottom.
|
|
3007
|
+
function placeToast() {
|
|
3008
|
+
const gap = 12;
|
|
3009
|
+
const dock = document.getElementById("dock");
|
|
3010
|
+
const r = dock.getBoundingClientRect();
|
|
3011
|
+
const over = document.body.classList.contains("has-session") && r.height > 0;
|
|
3012
|
+
const bottom = over ? `${Math.round(window.innerHeight - r.top + gap)}px` : "";
|
|
3013
|
+
const root = document.documentElement.style;
|
|
3014
|
+
if (root.getPropertyValue("--toast-bottom") !== bottom) {
|
|
3015
|
+
if (bottom) root.setProperty("--toast-bottom", bottom);
|
|
3016
|
+
else root.removeProperty("--toast-bottom");
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
2984
3020
|
// sticky: stays until its action or its ✕ (else gone after 6 s).
|
|
2985
3021
|
let toastTimer = null;
|
|
2986
3022
|
// html: +text+ is markup this page built (its parts escaped), not words.
|
|
@@ -2988,6 +3024,7 @@ function showToast(text, action = null, { sticky = false, html = false } = {}) {
|
|
|
2988
3024
|
const el = $("#toast");
|
|
2989
3025
|
el.innerHTML = `<span>${html ? text : escapeHtml(text)}</span>${action ? `<button type="button" class="toast-action">${escapeHtml(action.label)}</button>` : ""}` +
|
|
2990
3026
|
(sticky ? `<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss">✕</button>` : "");
|
|
3027
|
+
placeToast();
|
|
2991
3028
|
el.hidden = false;
|
|
2992
3029
|
el.querySelector(".toast-action")?.addEventListener("click", () => {
|
|
2993
3030
|
el.hidden = true;
|
|
@@ -3047,6 +3084,7 @@ function applyStripHidden() {
|
|
|
3047
3084
|
document.body.classList.toggle("strip-hidden", stripAutoHidden({
|
|
3048
3085
|
saved: stripHiddenSaved(), short: shortWindow.matches, shownWhileShort: stripShownWhileShort,
|
|
3049
3086
|
}));
|
|
3087
|
+
updateSessionTitle();
|
|
3050
3088
|
}
|
|
3051
3089
|
function setStripHidden(hidden) {
|
|
3052
3090
|
if (hidden) familyPop.close();
|
|
@@ -45,8 +45,6 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
45
45
|
#topStrip:empty{display:grid}
|
|
46
46
|
#center{flex:1;display:flex;flex-direction:column;min-height:45vh;background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
|
47
47
|
#stage{flex:1;display:flex;flex-direction:column;position:relative;min-height:160px;background:var(--bg);overflow:hidden}
|
|
48
|
-
#firstPeek{display:none;padding:8px 12px;background:var(--accent-bg);color:var(--text);border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
49
|
-
#firstPeek.visible{display:block}
|
|
50
48
|
#composer #cancelBtn{order:2;background:var(--danger-bg)}
|
|
51
49
|
#history{flex:1;overflow:auto;padding:16px max(16px,calc((100% - 960px)/2));display:flex;flex-direction:column;gap:12px;background:var(--bg);min-height:160px}
|
|
52
50
|
/* No child shrinks to fit: a held (overflow:hidden) answer bubble would, clipped under the dock with nothing to scroll. */
|
|
@@ -158,6 +156,7 @@ button.task-stop{flex:none;margin-left:auto;padding:1px 8px;border:1px solid col
|
|
|
158
156
|
button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:none}
|
|
159
157
|
.ts-now button.task-stop{align-self:center}
|
|
160
158
|
.activity-params{color:var(--text-muted);overflow-wrap:anywhere}
|
|
159
|
+
.activity-alias{color:var(--text-faint);white-space:nowrap}
|
|
161
160
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
162
161
|
.activity-output{margin:4px 0 2px var(--row-gutter);font-size:12px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;background:var(--bg);border-radius:6px;padding:6px 8px;max-height:160px;overflow:auto}
|
|
163
162
|
/* A row's status and tool name never wrap (a phone broke them letter by
|
|
@@ -681,8 +680,6 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
681
680
|
#infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);max-width:48ch;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left}
|
|
682
681
|
/* The footer sits on the card's glass: its chips are tinted, not solid, so the glass reads through them. */
|
|
683
682
|
#infoBar .meta .model{font-size:11px;padding:2px 6px;border-radius:6px;background:color-mix(in srgb,var(--bg-hover) 55%,transparent);color:var(--text-3)}
|
|
684
|
-
/* The memory chip ("mem 2", when the session used some): phones only, where the "mem: …" line is hidden. */
|
|
685
|
-
#infoBar .meta .mem-chip{display:none}
|
|
686
683
|
#infoBar .meta .model.served-mismatch{background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
687
684
|
/* The open session's worker runs an older chi: its version, and Restart when it can. */
|
|
688
685
|
#infoBar .meta .worker-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:2px 2px 2px 6px;border-radius:6px;background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border);white-space:nowrap}
|
|
@@ -690,9 +687,6 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
690
687
|
#infoBar .meta .worker-restart{padding:1px 8px;border-radius:5px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
691
688
|
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}
|
|
692
689
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
693
|
-
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;line-clamp:1;overflow:hidden;line-height:1.4;max-height:1.4em}
|
|
694
|
-
#infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
|
|
695
|
-
#infoBar .mem{font-size:12px;color:var(--text-muted)}
|
|
696
690
|
#infoBar button{padding:4px 12px;border-radius:8px;font-size:12px;font-weight:600}
|
|
697
691
|
#infoBar #infoStopBtn{background:var(--bg-raised);color:var(--warn-text);border:1px solid var(--border-strong)}
|
|
698
692
|
#infoBar #infoStopBtn:hover{background:var(--warn-bg);border-color:var(--warn-border)}
|
|
@@ -744,12 +738,11 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
744
738
|
.ctx-pop-actions button:hover{color:var(--text-hover)}
|
|
745
739
|
.ctx-pop-actions .ctx-pop-detach{color:var(--text-muted);background:transparent}
|
|
746
740
|
.ctx-pop-actions .ctx-pop-detach:hover{color:var(--danger-text);border-color:color-mix(in srgb,var(--danger) 55%,transparent)}
|
|
747
|
-
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable)
|
|
741
|
+
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable). */
|
|
748
742
|
@media(max-width:600px){
|
|
749
743
|
/* The key hint: cut off at this width, and phones have no Shift+Enter. */
|
|
750
744
|
#composeHint{display:none}
|
|
751
745
|
#infoBar{flex-wrap:nowrap;align-items:center;gap:6px;padding:6px 10px 8px}
|
|
752
|
-
#infoBar .preview-line{display:none}
|
|
753
746
|
/* No terminal on a phone to attach from. */
|
|
754
747
|
#infoBar .meta .copy-attach{display:none}
|
|
755
748
|
#infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
|
|
@@ -757,8 +750,8 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
757
750
|
#infoBar .meta>*{flex-shrink:0}
|
|
758
751
|
#infoBar .meta .id{order:-2}
|
|
759
752
|
#infoBar .meta .status{order:-1}
|
|
760
|
-
/*
|
|
761
|
-
#infoBar .meta .mem-chip{
|
|
753
|
+
/* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
754
|
+
#infoBar .meta .mem-chip{order:-1}
|
|
762
755
|
/* A parent's children chip too: its waiting part is what to see. */
|
|
763
756
|
#infoBar .meta .children-chip{order:-1}
|
|
764
757
|
/* A chain's ← → links too: the way to the other days. */
|
|
@@ -825,6 +818,14 @@ body.has-session #scope .scope-where{display:none}
|
|
|
825
818
|
/* The ▾ glyph draws tiny at the pill's 11 px: at 20 px it is about as tall
|
|
826
819
|
as the pill text's capitals (phone and desktop alike). */
|
|
827
820
|
/* The bell (notifications): quiet at the top bar's right end; filled in the accent when on, struck through when the browser blocks it. */
|
|
821
|
+
/* 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. */
|
|
822
|
+
#sessionTitle{flex:1;min-width:0;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
823
|
+
#sessionTitle[hidden]{display:none}
|
|
824
|
+
/* A phone: the title is its own row under the bar, so "chi + · pill · bell" stay one line. */
|
|
825
|
+
@media(max-width:600px){
|
|
826
|
+
#topBar{flex-wrap:wrap;row-gap:2px}
|
|
827
|
+
#sessionTitle{flex-basis:100%;order:10;font-size:12px}
|
|
828
|
+
}
|
|
828
829
|
#notifyBtn{margin-left:auto;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;min-height:0;background:transparent;border:1px solid transparent;border-radius:999px;color:var(--text-muted)}
|
|
829
830
|
#notifyBtn[hidden]{display:none}
|
|
830
831
|
#notifyBtn:hover{color:var(--text-2);border-color:var(--border)}
|
|
@@ -947,7 +948,8 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
947
948
|
.card.archived{opacity:.6}
|
|
948
949
|
.card.archived:hover{opacity:.85}
|
|
949
950
|
.card .archived-badge,.family-row .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
950
|
-
|
|
951
|
+
/* bottom: above the session's composer card when one shows (--toast-bottom, app.js placeToast). */
|
|
952
|
+
#toast{position:fixed;left:50%;bottom:var(--toast-bottom,24px);transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
951
953
|
#toast[hidden]{display:none}
|
|
952
954
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
953
955
|
#toast .toast-close{background:transparent;border:0;color:var(--text-muted);cursor:pointer;padding:2px 4px;margin-left:-6px}
|
|
@@ -1016,11 +1018,11 @@ a{color:var(--accent)}
|
|
|
1016
1018
|
<div id="scope" hidden></div>
|
|
1017
1019
|
<button type="button" class="strip-toggle" id="stripShow" title="Show the latest sessions"><span class="pill-arrow" aria-hidden="true">▾</span> sessions</button>
|
|
1018
1020
|
<button type="button" class="strip-toggle" id="stripHideTop" title="Hide the latest sessions"><span class="pill-arrow" aria-hidden="true">▴</span> hide</button>
|
|
1021
|
+
<span id="sessionTitle" hidden></span>
|
|
1019
1022
|
<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>
|
|
1020
1023
|
</div>
|
|
1021
1024
|
<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>
|
|
1022
1025
|
<section id="center">
|
|
1023
|
-
<div id="firstPeek"></div>
|
|
1024
1026
|
<div id="stage">
|
|
1025
1027
|
<div id="markdownWarning" class="markdown-warning" hidden></div>
|
|
1026
1028
|
<div id="history"></div>
|
|
@@ -1045,7 +1047,7 @@ a{color:var(--accent)}
|
|
|
1045
1047
|
<button type="button" id="modelPick" class="model-pick" title="The model the new chat starts on" aria-label="Model for the new chat" aria-haspopup="listbox" aria-expanded="false" hidden><span class="model-pick-name"></span></button>
|
|
1046
1048
|
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1047
1049
|
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1048
|
-
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">
|
|
1050
|
+
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
1049
1051
|
<div class="split">
|
|
1050
1052
|
<button id="actionBtn">Start</button>
|
|
1051
1053
|
</div>
|
|
@@ -1056,7 +1058,7 @@ a{color:var(--accent)}
|
|
|
1056
1058
|
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
1057
1059
|
<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>
|
|
1058
1060
|
<button class="ghost" id="infoArchiveBtn" title="Archive: hide from the lists, keep for good" style="display:none">archive</button>
|
|
1059
|
-
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop</button>
|
|
1061
|
+
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop session</button>
|
|
1060
1062
|
<button class="ghost" id="infoDeleteBtn" title="Delete session (for good)" style="display:none">delete</button>
|
|
1061
1063
|
</div>
|
|
1062
1064
|
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalize } from "./format.js";
|
|
2
|
+
|
|
3
|
+
// Where the open session's title shows: once on screen. Its strip card says
|
|
4
|
+
// it when the strip is shown and has an `.active` card for it; otherwise
|
|
5
|
+
// (strip hidden, an older session without a card, a delegate folded into its
|
|
6
|
+
// parent's card) the top bar does. The All sessions view shows none.
|
|
7
|
+
// Plain rules, no DOM; app.js wires them (updateSessionTitle).
|
|
8
|
+
|
|
9
|
+
const TAB_TITLE_CHARS = 40;
|
|
10
|
+
|
|
11
|
+
// The top bar's text, or "" when it shows no title.
|
|
12
|
+
export function topBarTitle({ title, cardShown, stripHidden, allView }) {
|
|
13
|
+
const text = normalize(title);
|
|
14
|
+
if (!text || allView) return "";
|
|
15
|
+
if (cardShown && !stripHidden) return "";
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The browser tab: "<title, cut at 40> · <base>", the base alone without a
|
|
20
|
+
// session title (base = "Chi · <project>" or "Chi").
|
|
21
|
+
export function tabTitle({ title, base }) {
|
|
22
|
+
const text = normalize(title);
|
|
23
|
+
if (!text) return base;
|
|
24
|
+
const cut = text.length > TAB_TITLE_CHARS ? `${text.slice(0, TAB_TITLE_CHARS)}…` : text;
|
|
25
|
+
return `${cut} · ${base}`;
|
|
26
|
+
}
|
|
@@ -18,13 +18,11 @@ export function stripColumns(width) {
|
|
|
18
18
|
// And how many wait on the user (notify.js waitingOn: a question, an
|
|
19
19
|
// approval, a card), so a hidden strip still says one needs an answer.
|
|
20
20
|
// Plain text parts; live and waiting are "" when none. Zero sessions keep
|
|
21
|
-
// the bare label.
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// session, a waiting delegate is still one thing waiting on the user
|
|
25
|
-
export function stripShowParts(sessions, { cards = null } = {}) {
|
|
21
|
+
// the bare label. The count is sessions (a folded delegate counts), the
|
|
22
|
+
// number the "All sessions" tile shows.
|
|
23
|
+
export function stripShowParts(sessions) {
|
|
26
24
|
const list = sessions || [];
|
|
27
|
-
const n =
|
|
25
|
+
const n = list.length;
|
|
28
26
|
const liveN = list.filter((s) => s && s.owner === "worker").length;
|
|
29
27
|
const waitingN = list.filter((s) => waitingOn(s)).length;
|
|
30
28
|
return {
|