samagotchi 0.96.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 +55 -1
- data/bin/chi +3 -0
- data/docs/architecture.md +12 -2
- data/docs/bundle-authoring.md +7 -4
- data/docs/cli.md +13 -4
- data/docs/configuration.md +13 -0
- data/docs/hooks.md +59 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -2
- data/docs/internals/engine.md +21 -1
- data/docs/memory.md +43 -3
- data/docs/plugins.md +8 -1
- data/docs/sessions.md +2 -2
- data/lib/samagotchi/answer_refs.rb +16 -9
- data/lib/samagotchi/bundle_command/check.rb +5 -3
- data/lib/samagotchi/bundle_command/try/run.rb +19 -4
- 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 +75 -40
- data/lib/samagotchi/hooks/bundle_loader.rb +18 -15
- data/lib/samagotchi/hooks.rb +4 -1
- data/lib/samagotchi/llm/errors.rb +135 -3
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +28 -9
- data/lib/samagotchi/memory_bundle/manifest.rb +17 -0
- data/lib/samagotchi/memory_bundle/profile.rb +14 -4
- data/lib/samagotchi/memory_command.rb +237 -0
- data/lib/samagotchi/output_formatter.rb +8 -2
- data/lib/samagotchi/pending_question.rb +7 -3
- data/lib/samagotchi/plugin/api.rb +12 -3
- data/lib/samagotchi/question_asked.rb +83 -0
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/send_command.rb +11 -11
- data/lib/samagotchi/session_inbox.rb +29 -0
- data/lib/samagotchi/session_manager.rb +11 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +1 -0
- data/lib/samagotchi/testing/bundle.rb +18 -1
- data/lib/samagotchi/tools/memory.rb +1 -1
- data/lib/samagotchi/turn_checkpoint.rb +6 -0
- 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
- data/lib/samagotchi/worker.rb +19 -5
- data/lib/samagotchi/worker_idle_exit.rb +16 -8
- metadata +3 -1
|
@@ -727,6 +727,8 @@ module Samagotchi
|
|
|
727
727
|
@screen.commit("#{paint("model>", 36)} #{output}")
|
|
728
728
|
end
|
|
729
729
|
prefill_confirm(event)
|
|
730
|
+
# /title from any client: the row's title follows.
|
|
731
|
+
@status.update(title: event[:title]) if Array(event[:changed]).include?("title")
|
|
730
732
|
return unless event[:model_name]
|
|
731
733
|
|
|
732
734
|
# After the output, so the status row changes with the line saying why.
|
|
@@ -5,7 +5,7 @@ require_relative "formatting"
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
class TerminalUI
|
|
7
7
|
# The status row under the prompt, the same for both TUIs:
|
|
8
|
-
# `status> model=… | ↳ parent | ⑂ 2 running · 1 waiting | ctx=12.3% (under20) | mem: … | muted: …`.
|
|
8
|
+
# `status> Title | model=… | ↳ parent | ⑂ 2 running · 1 waiting | ctx=12.3% (under20) | mem: … | muted: …`.
|
|
9
9
|
# A UI feeds it what it learns (attached mode from the Bridge's events,
|
|
10
10
|
# the REPL from its Engine's) through #update, and the row is redrawn in
|
|
11
11
|
# the surface's status slot only when its text changes.
|
|
@@ -15,6 +15,8 @@ module Samagotchi
|
|
|
15
15
|
# Memory names shown before ", +N".
|
|
16
16
|
MEMORY_LIMIT = 8
|
|
17
17
|
# What the row shows:
|
|
18
|
+
# title the session's own title (Session#title; not
|
|
19
|
+
# the first message's preview), or nil
|
|
18
20
|
# model, default_model the model in use, and the config's default
|
|
19
21
|
# (named beside another model)
|
|
20
22
|
# served [served, asked, expected]: what the server said it
|
|
@@ -29,12 +31,17 @@ module Samagotchi
|
|
|
29
31
|
# records them as used)
|
|
30
32
|
# muted its --mute list
|
|
31
33
|
# running a turn runs: the row trails STEER_HINT
|
|
32
|
-
FIELDS = %i[model default_model served parent_id children context used_memories preloaded muted running].freeze
|
|
34
|
+
FIELDS = %i[title model default_model served parent_id children context used_memories preloaded muted running].freeze
|
|
33
35
|
|
|
34
36
|
# The tail while a turn runs: where a typed line goes (the next step)
|
|
35
37
|
# and the two other choices. Dropped whole first when the row is cut.
|
|
36
38
|
STEER_HINT = " · typing goes in at the next step (/cut, /queue)"
|
|
37
39
|
|
|
40
|
+
# The longest title shown, and the share of the row it may take on a
|
|
41
|
+
# narrow terminal: the rest of the row still fits.
|
|
42
|
+
TITLE_MAX = 32
|
|
43
|
+
TITLE_SHARE = 3
|
|
44
|
+
|
|
38
45
|
# The turn events that end a turn (the hint goes).
|
|
39
46
|
TURN_END_EVENTS = %i[turn_completed turn_canceled turn_failed].freeze
|
|
40
47
|
|
|
@@ -74,6 +81,7 @@ module Samagotchi
|
|
|
74
81
|
fields.merge!(model: state[:model_name], default_model: default_model) if state[:model_name]
|
|
75
82
|
STATE_LISTS.each { |field, key| fields[field] = Array(state[key]) if state.key?(key) }
|
|
76
83
|
fields[:parent_id] = state[:parent_id] if state.key?(:parent_id)
|
|
84
|
+
fields[:title] = state[:title] if state.key?(:title)
|
|
77
85
|
# The last turn's ctx, so it shows before the next turn's.
|
|
78
86
|
fields[:context] = state[:context_status] if state[:context_status].is_a?(Hash)
|
|
79
87
|
update(**fields)
|
|
@@ -126,7 +134,8 @@ module Samagotchi
|
|
|
126
134
|
model_text = status_model_text(model, @values[:default_model], served: served, served_for: served_for,
|
|
127
135
|
expected: expected.is_a?(String))
|
|
128
136
|
end
|
|
129
|
-
segments = [
|
|
137
|
+
segments = [title_text(@values[:title], width),
|
|
138
|
+
model_text,
|
|
130
139
|
parent ? "↳ #{parent.to_s[0, 8]}" : "",
|
|
131
140
|
children_text(@values[:children]),
|
|
132
141
|
status_context_text(estimate: @values[:context]),
|
|
@@ -147,6 +156,18 @@ module Samagotchi
|
|
|
147
156
|
segments[0..-2] + ["#{segments.last}#{STEER_HINT}"]
|
|
148
157
|
end
|
|
149
158
|
|
|
159
|
+
# The title cut to TITLE_MAX, and to a TITLE_SHARE-th of +width+
|
|
160
|
+
# (the row's other segments come after it); "" without one.
|
|
161
|
+
def title_text(title, width)
|
|
162
|
+
title = title.to_s.strip
|
|
163
|
+
return "" if title.empty?
|
|
164
|
+
|
|
165
|
+
max = [TITLE_MAX, width / TITLE_SHARE].min
|
|
166
|
+
return title if title.length <= max
|
|
167
|
+
|
|
168
|
+
max > 1 ? "#{title[0, max - 1]}…" : ""
|
|
169
|
+
end
|
|
170
|
+
|
|
150
171
|
# `⑂ 2 running · 1 waiting`, the counts there are; "" with neither (no
|
|
151
172
|
# children, or all ended). Not "not reported": a REPL parent is never
|
|
152
173
|
# given its children's replies (no wakes there), and a worker moves
|
|
@@ -889,6 +889,7 @@ module Samagotchi
|
|
|
889
889
|
if command
|
|
890
890
|
show_command_result(command)
|
|
891
891
|
refresh_status_row(clear_unknown_context: true) if command.changed.include?(:model)
|
|
892
|
+
refresh_status_row if command.changed.include?(:title)
|
|
892
893
|
persist_recent_history(input) if command.shell
|
|
893
894
|
return
|
|
894
895
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../plugin/loader"
|
|
4
4
|
require_relative "../plugin/tool_spec"
|
|
5
5
|
require_relative "../hooks"
|
|
6
|
+
require_relative "../question_asked"
|
|
6
7
|
require_relative "../commands/registry"
|
|
7
8
|
require_relative "../session_commands"
|
|
8
9
|
require_relative "../tools/builtins"
|
|
@@ -179,7 +180,7 @@ module Samagotchi
|
|
|
179
180
|
end
|
|
180
181
|
@hooks_namespace = Module.new
|
|
181
182
|
@manifest.hooks.each do |basename, hook|
|
|
182
|
-
next if hook.
|
|
183
|
+
next if hook.events.empty?
|
|
183
184
|
|
|
184
185
|
Hooks::BundleLoader.load_hook_file(@name, basename, File.join(@dir, "hooks", basename), hook, hooks,
|
|
185
186
|
settings: @settings, namespace: @hooks_namespace)
|
|
@@ -203,6 +204,22 @@ module Samagotchi
|
|
|
203
204
|
payload
|
|
204
205
|
end
|
|
205
206
|
|
|
207
|
+
# Fire :question_asked as chi's two seams do (QuestionAsked.fire): a
|
|
208
|
+
# question (+kind+ "question") or an approval ("approval") asked by a
|
|
209
|
+
# step whose text is +text+, with the question's +question+ fields.
|
|
210
|
+
# The hooks' refs are checked as chi checks them (the question caps,
|
|
211
|
+
# file refs under +cwd+, default the session's folder).
|
|
212
|
+
# @return [Array<Hash>] the refs the step and its pending question
|
|
213
|
+
# get, as stored; [] for none
|
|
214
|
+
def question_asked(text, kind: "question", question: {}, cwd: nil)
|
|
215
|
+
step = { role: "model", content: text.to_s }
|
|
216
|
+
fields = failing do
|
|
217
|
+
QuestionAsked.fire(hooks: hooks, conversation: [step], kind: kind.to_s, fields: question.to_h,
|
|
218
|
+
cwd: cwd || @recorder[:cwd])
|
|
219
|
+
end
|
|
220
|
+
Array(fields[:refs])
|
|
221
|
+
end
|
|
222
|
+
|
|
206
223
|
# Build the before_tool_call payload from the real Gate, without firing
|
|
207
224
|
# this bundle's hooks.
|
|
208
225
|
# @param call [Hash] the parsed tool call
|
|
@@ -356,7 +356,7 @@ module Samagotchi
|
|
|
356
356
|
([file] + overlay_files(dir, entry_name, index_text)).each { |path| trash.move(path) }
|
|
357
357
|
Samagotchi::MemoryBundle::IndexUpdater.remove_index(resolved_scope, entry_name)
|
|
358
358
|
"Memory '#{entry_name}' removed from #{resolved_scope} scope: moved to #{trash.dir} " \
|
|
359
|
-
"(`chi bundle trash` lists and empties it); index line dropped."
|
|
359
|
+
"(`chi bundle trash` lists and empties it)#{"; index line dropped" if line}."
|
|
360
360
|
end
|
|
361
361
|
|
|
362
362
|
# The entry's model overlays: <name>.<key>.md with a key ModelOverlay
|
|
@@ -86,6 +86,12 @@ module Samagotchi
|
|
|
86
86
|
settled_record(id)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# The settled outcomes past +max_age+ (SessionInbox.prune_old).
|
|
90
|
+
# @return [Integer] how many went
|
|
91
|
+
def self.prune_settled(session_dir, max_age: SessionInbox::TOMBSTONE_MAX_AGE)
|
|
92
|
+
SessionInbox.prune_old(session_dir, SETTLED_DIR, max_age: max_age)
|
|
93
|
+
end
|
|
94
|
+
|
|
89
95
|
def self.settled_record(id, ended_at: nil, seconds: nil)
|
|
90
96
|
LastTurn.new(outcome: "canceled", cancel_reason: WORKER_STOPPED, ended_at: ended_at, seconds: seconds, enqueued_id: id)
|
|
91
97
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "../client_id"
|
|
|
14
14
|
require_relative "../delivery"
|
|
15
15
|
require_relative "../answer_tail"
|
|
16
16
|
require_relative "../answer_refs"
|
|
17
|
+
require_relative "../question_asked"
|
|
17
18
|
require_relative "../bridge_client"
|
|
18
19
|
require_relative "../bridge/bounded_queue"
|
|
19
20
|
require_relative "../bridge/card_store"
|
|
@@ -839,7 +840,7 @@ module Samagotchi
|
|
|
839
840
|
plugin_warning: turn_snapshot && turn_snapshot["plugin_warning"],
|
|
840
841
|
# Plugins' slow setup still running (chi.init): the page's init row.
|
|
841
842
|
init_tasks: snapshot_list(turn_snapshot, "init_tasks"),
|
|
842
|
-
cards: cards_for_display(live_or_saved(turn_snapshot, "cards") { saved_cards(id) }),
|
|
843
|
+
cards: cards_for_display(live_or_saved(turn_snapshot, "cards") { saved_cards(id) }, cwd: session.working_directory),
|
|
843
844
|
# The composer's / autocomplete; the built-ins until a worker
|
|
844
845
|
# names its plugins' too.
|
|
845
846
|
commands: turn_snapshot&.fetch("commands", nil) || SessionCommands.builtin_registry.listing,
|
|
@@ -871,13 +872,28 @@ module Samagotchi
|
|
|
871
872
|
# dead worker saved can't be answered (its turn is gone), nor a chi
|
|
872
873
|
# REPL's (it shares nothing with the web).
|
|
873
874
|
# A question between turns (the step-limit one) is the snapshot's own.
|
|
875
|
+
# Its refs (the asking reply's, QuestionAsked) are checked again.
|
|
874
876
|
def shown_pending_question(session, turn_snapshot, owner)
|
|
875
|
-
if turn_snapshot
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
877
|
+
pending = if turn_snapshot
|
|
878
|
+
current_turn = turn_snapshot["current_turn"]
|
|
879
|
+
current_turn ? current_turn["pending_question"] : turn_snapshot["pending_question"]
|
|
880
|
+
elsif owner&.worker?
|
|
881
|
+
session.pending_question&.to_h
|
|
882
|
+
end
|
|
883
|
+
with_question_refs(pending, cwd: session.working_directory)
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
# A pending question (a snapshot's, the session file's or a saved
|
|
887
|
+
# card's) with its refs checked again, under a question's caps
|
|
888
|
+
# (QuestionAsked): a stored file is never trusted, and what isn't
|
|
889
|
+
# valid any more is dropped. The question as it was without refs.
|
|
890
|
+
def with_question_refs(pending, cwd:)
|
|
891
|
+
return pending unless pending.is_a?(Hash)
|
|
892
|
+
|
|
893
|
+
key = [:refs, "refs"].find { |name| pending.key?(name) } or return pending
|
|
894
|
+
refs = Samagotchi::AnswerRefs.for_display(pending[key], cwd: cwd, max_refs: Samagotchi::QuestionAsked::MAX_REFS,
|
|
895
|
+
max_bytes: Samagotchi::QuestionAsked::MAX_BYTES)
|
|
896
|
+
refs.empty? ? pending.except(key) : pending.merge(key => refs)
|
|
881
897
|
end
|
|
882
898
|
|
|
883
899
|
# The full show's session part: the file's, with what a live worker's
|
|
@@ -1031,11 +1047,16 @@ module Samagotchi
|
|
|
1031
1047
|
# cards: CardStore#list), else (no live worker) the ones the last
|
|
1032
1048
|
# worker saved in the session's folder (#saved_cards), a card's body as
|
|
1033
1049
|
# body_html: rendered markdown when the renderer is on, else the
|
|
1034
|
-
# escaped text in a <pre
|
|
1050
|
+
# escaped text in a <pre>, and a question card's refs checked again
|
|
1051
|
+
# (#with_question_refs; +cwd+ the session's folder, nil where the
|
|
1052
|
+
# session file isn't read: a file ref then stays only absolute).
|
|
1035
1053
|
# @return [Array<Hash>]
|
|
1036
|
-
def cards_for_display(cards)
|
|
1054
|
+
def cards_for_display(cards, cwd: nil)
|
|
1037
1055
|
Array(cards).filter_map do |card|
|
|
1038
1056
|
next unless card.is_a?(Hash)
|
|
1057
|
+
if card["type"].to_s == "question" && card["pending_question"].is_a?(Hash)
|
|
1058
|
+
next card.merge("pending_question" => with_question_refs(card["pending_question"], cwd: cwd))
|
|
1059
|
+
end
|
|
1039
1060
|
next card unless card["type"].to_s == "card"
|
|
1040
1061
|
|
|
1041
1062
|
card.merge("body_html" => card_body_html(card["body"].to_s))
|
|
@@ -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,
|