samagotchi 0.168.0 → 0.169.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 +38 -1
- data/docs/architecture.md +1 -1
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +2 -2
- data/docs/internals/block-renderers.md +1 -1
- data/docs/internals/bridge.md +16 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +2 -1
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/web-frontend.md +62 -22
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +10 -6
- data/docs/testing.md +12 -0
- data/lib/samagotchi/bridge.rb +12 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/client_id.rb +3 -0
- data/lib/samagotchi/command_intake.rb +7 -7
- data/lib/samagotchi/compat.rb +14 -5
- data/lib/samagotchi/engine/objectives.rb +10 -1
- data/lib/samagotchi/guardrails/approvals.rb +8 -3
- data/lib/samagotchi/session_continue.rb +8 -11
- data/lib/samagotchi/session_manager/removal.rb +3 -3
- data/lib/samagotchi/session_stores.rb +79 -0
- data/lib/samagotchi/turn_stages/objectives.rb +6 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +3 -1
- data/lib/samagotchi/web/public/block_watch.js +1 -1
- data/lib/samagotchi/web/public/cards_place.js +5 -1
- data/lib/samagotchi/web/public/index.html +58 -39
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +20 -6
- data/lib/samagotchi/web/public/stage_asking.js +283 -0
- data/lib/samagotchi/web/public/stage_frame.js +7 -11
- data/lib/samagotchi/web/public/stage_model.js +37 -11
- data/lib/samagotchi/web/public/stage_opened.js +28 -18
- data/lib/samagotchi/web/public/stage_slots.js +13 -89
- data/lib/samagotchi/web/public/stage_view.js +202 -160
- data/lib/samagotchi/web/public/stage_waiting.js +180 -0
- data/lib/samagotchi/web/public/stream_handlers.js +1 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/session_show.rb +14 -4
- data/lib/samagotchi/worker.rb +3 -4
- metadata +4 -1
data/lib/samagotchi/compat.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rubygems/version"
|
|
4
|
+
require "date"
|
|
4
5
|
require_relative "version"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
@@ -11,23 +12,31 @@ module Samagotchi
|
|
|
11
12
|
# decided (kept until then). spec/compat_spec.rb fails once VERSION
|
|
12
13
|
# passes a shim's +remove_after+, naming it. Each Ruby file names its
|
|
13
14
|
# shim's id in a comment ("compat shim <id>").
|
|
15
|
+
# A shim stays at least 14 days after the release that added it.
|
|
14
16
|
# @!attribute files [Array<String>] where the shim lives (from the repository root)
|
|
15
|
-
Shim = Data.define(:id, :added_in, :remove_after, :files, :what)
|
|
17
|
+
Shim = Data.define(:id, :added_in, :added_on, :remove_after, :files, :what)
|
|
18
|
+
MIN_AGE_DAYS = 14
|
|
16
19
|
|
|
17
20
|
SHIMS = [
|
|
18
|
-
Shim.new(id: :cards_v1_seed, added_in: "0.151.0", remove_after: "0.170.0",
|
|
21
|
+
Shim.new(id: :cards_v1_seed, added_in: "0.151.0", added_on: "2026-11-05", remove_after: "0.170.0",
|
|
19
22
|
files: ["lib/samagotchi/bridge/card_store.rb"],
|
|
20
23
|
what: "CardStore#seed_v1 reads a cards.json from before the turn log; its comment says a retention " \
|
|
21
|
-
"period after the release that wrote version 2 (0.151.0, 2026-11-05)")
|
|
24
|
+
"period after the release that wrote version 2 (0.151.0, 2026-11-05)"),
|
|
25
|
+
Shim.new(id: :approvals_bare_list, added_in: "0.169.0", added_on: "2026-11-12", remove_after: "0.172.0",
|
|
26
|
+
files: ["lib/samagotchi/guardrails/approvals.rb"],
|
|
27
|
+
what: "Approvals reads the bare list written by chi 0.166 and before, converting it on write")
|
|
22
28
|
].freeze
|
|
23
29
|
|
|
24
30
|
module_function
|
|
25
31
|
|
|
26
32
|
# The shims +version+ has passed the remove_after of.
|
|
27
33
|
# @return [Array<Shim>]
|
|
28
|
-
def overdue_shims(version: VERSION, shims: SHIMS)
|
|
34
|
+
def overdue_shims(version: VERSION, shims: SHIMS, today: Date.today)
|
|
29
35
|
current = Gem::Version.new(version)
|
|
30
|
-
shims.select
|
|
36
|
+
shims.select do |shim|
|
|
37
|
+
shim.remove_after && current > Gem::Version.new(shim.remove_after) &&
|
|
38
|
+
Date.iso8601(shim.added_on) + MIN_AGE_DAYS <= today
|
|
39
|
+
end
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
def verdict(worker_version:, version: VERSION, worker_protocol: nil, protocol: nil)
|
|
@@ -19,13 +19,22 @@ module Samagotchi
|
|
|
19
19
|
return unless session
|
|
20
20
|
|
|
21
21
|
synchronize_events do
|
|
22
|
-
event = @objectives_stage.announce(session,
|
|
22
|
+
event = @objectives_stage.announce(session, view: objectives_view(session.id, session.messages))
|
|
23
23
|
announce(event) if event
|
|
24
24
|
end
|
|
25
25
|
rescue StandardError => e
|
|
26
26
|
Log.warn(:turn, "objectives_announce_failed", error: e.class.name, msg: e.message)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# The view the worker announces and includes in its snapshots.
|
|
30
|
+
# @param session_id [String] the session whose objectives to read
|
|
31
|
+
# @param messages [Array<Hash>] the messages held by the caller's snapshot
|
|
32
|
+
# @param state_dir [String] the sessions dir
|
|
33
|
+
# @return [Hash, nil]
|
|
34
|
+
def objectives_view(session_id, messages, state_dir: session_state_dir)
|
|
35
|
+
Objectives.view(session_id, messages, state_dir: state_dir, running: live_conversation&.dup)
|
|
36
|
+
end
|
|
37
|
+
|
|
29
38
|
# Check this (ObjectivesCheck)'s one side request. Its usage is in the
|
|
30
39
|
# session's side line. Raises as ctx.ask_model's request does.
|
|
31
40
|
# @param request [Array<Hash>] chat messages
|
|
@@ -30,8 +30,8 @@ module Samagotchi
|
|
|
30
30
|
#
|
|
31
31
|
# The file is a versioned store, {"version", "approvals": [...]}
|
|
32
32
|
# (FORMAT); a write keeps its version and the keys this chi doesn't
|
|
33
|
-
# know. A bare list of entries (what 0.166 and before wrote) is
|
|
34
|
-
#
|
|
33
|
+
# know. A bare list of entries (what 0.166 and before wrote) is read and
|
|
34
|
+
# becomes versioned on the next write.
|
|
35
35
|
class Approvals
|
|
36
36
|
FILE = "approvals.json"
|
|
37
37
|
COLLECTION = "approvals"
|
|
@@ -192,6 +192,10 @@ module Samagotchi
|
|
|
192
192
|
def parse(text)
|
|
193
193
|
data = JSON.parse(text)
|
|
194
194
|
case data
|
|
195
|
+
when Array
|
|
196
|
+
# compat shim approvals_bare_list
|
|
197
|
+
document = FORMAT.read({ COLLECTION => data }, collection: COLLECTION)
|
|
198
|
+
[document.entries.map(&:raw).select { |e| e.is_a?(Hash) }, document]
|
|
195
199
|
when Hash
|
|
196
200
|
raise JSON::ParserError, "#{COLLECTION} is not a list" unless data.fetch(COLLECTION, []).is_a?(Array)
|
|
197
201
|
|
|
@@ -199,7 +203,8 @@ module Samagotchi
|
|
|
199
203
|
# An entry it can't read (no scope) is kept as it is: it matches
|
|
200
204
|
# nothing, and a write keeps it for the chi that wrote it.
|
|
201
205
|
[document.entries.map(&:raw).select { |e| e.is_a?(Hash) }, document]
|
|
202
|
-
else
|
|
206
|
+
else
|
|
207
|
+
raise JSON::ParserError, "not a versioned store"
|
|
203
208
|
end
|
|
204
209
|
end
|
|
205
210
|
|
|
@@ -21,6 +21,7 @@ require_relative "worker_link"
|
|
|
21
21
|
require_relative "model_profile"
|
|
22
22
|
require_relative "log"
|
|
23
23
|
require_relative "session_manager"
|
|
24
|
+
require_relative "session_stores"
|
|
24
25
|
|
|
25
26
|
module Samagotchi
|
|
26
27
|
# The next link of a session chain: chi send --new --continues and web
|
|
@@ -76,7 +77,7 @@ module Samagotchi
|
|
|
76
77
|
# (taking an UndoContext) that gives it back when the continue fails;
|
|
77
78
|
# empty when there is nothing to undo (CARRY's comment says why).
|
|
78
79
|
# +store+ is the SessionManager::SESSION_STATE_STORES store it carries.
|
|
79
|
-
CarryStep = Data.define(:name, :store, :carry, :undo)
|
|
80
|
+
CarryStep = Data.define(:name, :store, :carry, :undo, :files)
|
|
80
81
|
# What a carry step works on: the previous link (a Session), the new
|
|
81
82
|
# link's id, the delegates moving (ChildMove::Plan#move).
|
|
82
83
|
CarryContext = Data.define(:previous, :to, :state_dir, :move)
|
|
@@ -100,15 +101,16 @@ module Samagotchi
|
|
|
100
101
|
# Its open delegates (ChildMove): given back to the previous link
|
|
101
102
|
# first, then the rings one rang the link with just before the undo.
|
|
102
103
|
CarryStep.new(name: :children, store: :session_files, carry: :carry_children,
|
|
103
|
-
undo: { moved_back: :undo_move, link_stopped: :return_rings }
|
|
104
|
+
undo: { moved_back: :undo_move, link_stopped: :return_rings },
|
|
105
|
+
files: %w[delegates.json parent.json]),
|
|
104
106
|
# Its movable context too, with a note saying what moved and stayed.
|
|
105
107
|
# No undo: ContextSources.move copies (the previous link keeps its
|
|
106
108
|
# sources), and the link's copies go with its delete (remove_files).
|
|
107
|
-
CarryStep.new(name: :context, store: :context_sources, carry: :carry_context, undo: {}),
|
|
109
|
+
CarryStep.new(name: :context, store: :context_sources, carry: :carry_context, undo: {}, files: ["left.json"]),
|
|
108
110
|
# Its objectives: the list reads as the latest word. No undo: a note
|
|
109
111
|
# written into the link only (NoteMove::CONTINUE_SOURCES: never given
|
|
110
112
|
# back), which goes with its delete.
|
|
111
|
-
CarryStep.new(name: :objectives, store: :session_files, carry: :carry_objectives, undo: {}),
|
|
113
|
+
CarryStep.new(name: :objectives, store: :session_files, carry: :carry_objectives, undo: {}, files: []),
|
|
112
114
|
# Its waiting notes last: the carry above read a pending /objectives
|
|
113
115
|
# edit from them first. They keep their names, so they still sort
|
|
114
116
|
# ahead of the link's own notes. A note sent to the previous link
|
|
@@ -117,17 +119,12 @@ module Samagotchi
|
|
|
117
119
|
# once the link is out of the chain, and again for a note an older chi
|
|
118
120
|
# wrote after its folder went.
|
|
119
121
|
CarryStep.new(name: :notes, store: :session_files, carry: :carry_notes,
|
|
120
|
-
undo: { link_unlinked: :give_back_notes, link_deleted: :give_back_strays })
|
|
122
|
+
undo: { link_unlinked: :give_back_notes, link_deleted: :give_back_strays }, files: [])
|
|
121
123
|
].freeze
|
|
122
124
|
|
|
123
125
|
# The SessionManager::SESSION_STATE_STORES a continue deliberately
|
|
124
126
|
# leaves with the previous link, and why: every store is in CARRY or here.
|
|
125
|
-
NOT_CARRIED = {
|
|
126
|
-
plugin_session_state: "a bundle's per-session state (ctx.session_dir) stays with the archived link",
|
|
127
|
-
participants: "the archive ends the previous link's review (D8); a continue copies no grants",
|
|
128
|
-
system_one_runs: "System 1's saved runs record the previous link's own turns",
|
|
129
|
-
context_candidates_log: "the candidates log records the previous link's own turns"
|
|
130
|
-
}.freeze
|
|
127
|
+
NOT_CARRIED = SessionStores::STORES.reject(&:carried?).to_h { |store| [store.file, store.continue] }.freeze
|
|
131
128
|
|
|
132
129
|
# The model a new link runs on (#model_for): the name to spawn it with,
|
|
133
130
|
# and why it isn't the previous link's (nil when it is, or was given).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../session_stores"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
class SessionManager
|
|
5
7
|
# A delete that would pull the session from under its live worker.
|
|
@@ -61,9 +63,7 @@ module Samagotchi
|
|
|
61
63
|
|
|
62
64
|
# Every store with session-scoped state. Deletion, retention and failed
|
|
63
65
|
# continuation cleanup all pass through this registry.
|
|
64
|
-
SESSION_STATE_STORES =
|
|
65
|
-
session_files plugin_session_state context_sources participants system_one_runs context_candidates_log
|
|
66
|
-
].freeze
|
|
66
|
+
SESSION_STATE_STORES = SessionStores.removal_stores
|
|
67
67
|
|
|
68
68
|
# Removal: archive, unarchive and delete a session (an archive takes its
|
|
69
69
|
# delegates too), and the retention sweep's entry. Calls LifecyclePart's
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# The files that hold state for a session, including stores outside its
|
|
5
|
+
# sidecar directory. `corpus_dir` is relative to the compatibility corpus.
|
|
6
|
+
# A Symbol in `continue` names the carry step; a String explains why it stays.
|
|
7
|
+
module SessionStores
|
|
8
|
+
Store = Data.define(:file, :owner, :removal, :cross_version, :continue, :corpus_dir, :corpus_exception) do
|
|
9
|
+
def initialize(file:, owner:, removal:, cross_version:, continue:, corpus_dir:, corpus_exception: nil)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def carried? = continue.is_a?(Symbol)
|
|
14
|
+
|
|
15
|
+
def corpus_exception? = !corpus_exception.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
SIDE_CAR = "sessions/compat-session"
|
|
19
|
+
CONTEXT = "context/sessions/compat-session"
|
|
20
|
+
|
|
21
|
+
STORES = [
|
|
22
|
+
Store.new(file: "archived", owner: "ArchiveStore", removal: :session_files, cross_version: true,
|
|
23
|
+
continue: "the archived link remains its own session", corpus_dir: SIDE_CAR),
|
|
24
|
+
Store.new(file: "state.json", owner: "PluginSessionState", removal: :plugin_session_state, cross_version: false,
|
|
25
|
+
continue: "bundle state stays with the archived link", corpus_dir: nil),
|
|
26
|
+
Store.new(file: "left.json", owner: "ContextSources", removal: :context_sources, cross_version: true,
|
|
27
|
+
continue: :context, corpus_dir: CONTEXT),
|
|
28
|
+
Store.new(file: "participants.json", owner: "Participants", removal: :participants, cross_version: true,
|
|
29
|
+
continue: "a continue ends the previous link's review", corpus_dir: SIDE_CAR),
|
|
30
|
+
Store.new(file: "judgments.jsonl", owner: "SystemOne::RunsStore", removal: :system_one_runs, cross_version: false,
|
|
31
|
+
continue: "saved judgments belong to the previous link", corpus_dir: nil),
|
|
32
|
+
Store.new(file: "context_candidates.jsonl", owner: "ContextCandidatesLog", removal: :context_candidates_log,
|
|
33
|
+
cross_version: false, continue: "candidate records belong to the previous link", corpus_dir: nil),
|
|
34
|
+
Store.new(file: "bridge.json", owner: "WorkerSidecar", removal: :session_files, cross_version: true,
|
|
35
|
+
continue: "the bridge endpoint belongs to the old worker", corpus_dir: SIDE_CAR),
|
|
36
|
+
Store.new(file: "cards.json", owner: "Bridge::CardStore", removal: :session_files, cross_version: true,
|
|
37
|
+
continue: "cards describe turns in the previous link", corpus_dir: SIDE_CAR),
|
|
38
|
+
Store.new(file: "pending_card.json", owner: "Bridge::PendingCard", removal: :session_files, cross_version: true,
|
|
39
|
+
continue: "an open card belongs to its running turn", corpus_dir: SIDE_CAR),
|
|
40
|
+
Store.new(file: "turn_drawings.jsonl", owner: "Bridge::TurnDrawings", removal: :session_files, cross_version: true,
|
|
41
|
+
continue: "drawings describe turns in the previous link", corpus_dir: "sessions/compat-drawn"),
|
|
42
|
+
Store.new(file: "move.json", owner: "ChildMove", removal: :session_files, cross_version: true,
|
|
43
|
+
continue: "an interrupted move belongs to the previous continue", corpus_dir: SIDE_CAR),
|
|
44
|
+
Store.new(file: "continue.json", owner: "ContinueIntent", removal: :session_files, cross_version: true,
|
|
45
|
+
continue: "the intent records the previous link's continue", corpus_dir: SIDE_CAR),
|
|
46
|
+
Store.new(file: "objectives.lock", owner: "Objectives", removal: :session_files, cross_version: false,
|
|
47
|
+
continue: "a lock is not session data", corpus_dir: SIDE_CAR),
|
|
48
|
+
Store.new(file: "objectives_cards.json", owner: "Objectives", removal: :session_files, cross_version: true,
|
|
49
|
+
continue: "proposal answers belong to the previous link", corpus_dir: SIDE_CAR),
|
|
50
|
+
Store.new(file: "objective_checks.jsonl", owner: "ObjectivesCheck", removal: :session_files, cross_version: true,
|
|
51
|
+
continue: "checks belong to the previous link", corpus_dir: SIDE_CAR),
|
|
52
|
+
Store.new(file: "objective_tracks.jsonl", owner: "ObjectivesTrack", removal: :session_files, cross_version: true,
|
|
53
|
+
continue: "tracks belong to the previous link", corpus_dir: SIDE_CAR),
|
|
54
|
+
Store.new(file: "owner.lock", owner: "OwnerLock", removal: :session_files, cross_version: false,
|
|
55
|
+
continue: "the owner lock is not session data", corpus_dir: SIDE_CAR),
|
|
56
|
+
Store.new(file: "reminders.json", owner: "ReminderCarry", removal: :session_files, cross_version: true,
|
|
57
|
+
continue: "a restart hand-off is not a continue carry", corpus_dir: SIDE_CAR),
|
|
58
|
+
Store.new(file: "recap.json", owner: "RecapStore", removal: :session_files, cross_version: true,
|
|
59
|
+
continue: "the recap remains with the previous link", corpus_dir: SIDE_CAR),
|
|
60
|
+
Store.new(file: "stopped", owner: "Session", removal: :session_files, cross_version: true,
|
|
61
|
+
continue: "the stop marker belongs to the previous link", corpus_dir: SIDE_CAR),
|
|
62
|
+
Store.new(file: "parent.json", owner: "Session", removal: :session_files, cross_version: true,
|
|
63
|
+
continue: :children, corpus_dir: SIDE_CAR),
|
|
64
|
+
Store.new(file: "starting", owner: "StartingMarker", removal: :session_files, cross_version: true,
|
|
65
|
+
continue: "a start marker belongs to its spawn", corpus_dir: nil,
|
|
66
|
+
corpus_exception: "the corpus marker would always be stale, so it cannot pin the live-start handshake"),
|
|
67
|
+
Store.new(file: "thinking_tails.jsonl", owner: "ThinkingTails", removal: :session_files, cross_version: true,
|
|
68
|
+
continue: "generation tails remain with the previous link", corpus_dir: SIDE_CAR),
|
|
69
|
+
Store.new(file: "delegates.json", owner: "Tools::DelegateCursors", removal: :session_files, cross_version: true,
|
|
70
|
+
continue: :children, corpus_dir: SIDE_CAR),
|
|
71
|
+
Store.new(file: "delegates.lock", owner: "Tools::DelegateCursors", removal: :session_files, cross_version: false,
|
|
72
|
+
continue: "a lock is not session data", corpus_dir: SIDE_CAR)
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
75
|
+
def self.removal_stores = STORES.map(&:removal).uniq.freeze
|
|
76
|
+
|
|
77
|
+
def self.corpus_stores = STORES.select(&:cross_version).freeze
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -97,18 +97,16 @@ module Samagotchi
|
|
|
97
97
|
# @param state_dir [String] the sessions dir
|
|
98
98
|
def note(session, note, recorder:, state_dir:) = audit(state_dir, recorder).note(session, note)
|
|
99
99
|
|
|
100
|
-
# The session's objectives as the page draws them (
|
|
101
|
-
# that differs from the last announced: after an edit (its file
|
|
102
|
-
# pending), once a list note is in, at a turn's end (its moves)
|
|
103
|
-
# a rollback. A session with no list announces nothing. The caller
|
|
100
|
+
# The session's objectives as the page draws them (Engine#objectives_view),
|
|
101
|
+
# when that differs from the last announced: after an edit (its file
|
|
102
|
+
# written, pending), once a list note is in, at a turn's end (its moves)
|
|
103
|
+
# and after a rollback. A session with no list announces nothing. The caller
|
|
104
104
|
# (Engine#announce_objectives) holds the event lock, the one guard of
|
|
105
105
|
# the last view, announces the event and logs a failure: it only
|
|
106
106
|
# informs, never fails a turn.
|
|
107
|
-
# @param
|
|
108
|
-
# @param running [Array<Hash>, nil] the running turn's conversation (Engine#live_conversation), nil between turns
|
|
107
|
+
# @param view [Hash, nil] the view from Engine#objectives_view
|
|
109
108
|
# @return [Hash, nil] the :objectives_updated event to announce, nil when the view is the same
|
|
110
|
-
def announce(session,
|
|
111
|
-
view = Samagotchi::Objectives.view(session.id, session.messages, state_dir: state_dir, running: running&.dup)
|
|
109
|
+
def announce(session, view:)
|
|
112
110
|
return if view.nil? || view == @announced
|
|
113
111
|
|
|
114
112
|
@announced = view
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -448,12 +448,14 @@ const questionCards = createQuestionCards({
|
|
|
448
448
|
isNearBottom: () => isNearBottom(),
|
|
449
449
|
scrollToEnd: () => { historyEl.scrollTop = historyEl.scrollHeight; },
|
|
450
450
|
removeHintIfEmpty: () => removeHintIfEmpty(),
|
|
451
|
-
askingText: () => turnDom.askingText(),
|
|
451
|
+
askingText: (pq) => turnDom.askingText(pq),
|
|
452
452
|
renderMarkdown,
|
|
453
453
|
linkRefs: linkRefsIn,
|
|
454
454
|
pairRefs: pairRefsIn,
|
|
455
455
|
// The bar's chip reads questionCards.pendingQuestion() (sessionWaits).
|
|
456
456
|
onPending: () => store.touch("infoBar"),
|
|
457
|
+
// The stage's waiting cards as data (stage_waiting.js).
|
|
458
|
+
onSettled: (key) => turnDom.settleWaiting?.(key),
|
|
457
459
|
});
|
|
458
460
|
// The history's bubbles (bubbles.js): output, end lines, every client's
|
|
459
461
|
// prompts, the recap, warnings and notices, command and note bubbles.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// One observer per turn root, filtered to TEXTS, as copy.js and answer_fold.js:
|
|
3
3
|
// every path that fills a root (a reload, a session switch, a turn's end, an
|
|
4
4
|
// answer_display, Revise's clones, a step's swap) arrives the same way, and
|
|
5
|
-
// cards,
|
|
5
|
+
// cards, a lead-in bubble and question replies are never touched. A block is
|
|
6
6
|
// drawn when it comes near its scroller's view (a closed <details> opening is
|
|
7
7
|
// such a coming near), and one that moves to another scroller while it waits
|
|
8
8
|
// (the stage's hand-off to #history) is watched there.
|
|
@@ -15,6 +15,7 @@ import { stepText } from "./turn_block.js";
|
|
|
15
15
|
import { llmContextNotice, noticeLine } from "./turn_events.js";
|
|
16
16
|
import { hookNoticeTemplate, TURN_PROMPT, turnPromptBubbles } from "./bubbles.js";
|
|
17
17
|
import { stillOn } from "./latest.js";
|
|
18
|
+
import { pluginCardKey } from "./stage_waiting.js";
|
|
18
19
|
|
|
19
20
|
// The elements of the reloaded turn that ends before +before+ (the next
|
|
20
21
|
// turn's prompt, or null for the last turn), its prompt first.
|
|
@@ -71,15 +72,18 @@ export function createCardsPlace({
|
|
|
71
72
|
if (!card || !card.id) return;
|
|
72
73
|
turnDom.settleFold?.();
|
|
73
74
|
const existing = cardElement(card.id);
|
|
75
|
+
const asks = (card.actions || []).some((a) => a && a.command);
|
|
74
76
|
if (existing) {
|
|
75
77
|
const drawn = drawCard(existing, card);
|
|
76
78
|
drawn.classList.add("card-updated");
|
|
77
79
|
setTimeout(() => drawn.classList.remove("card-updated"), 900);
|
|
80
|
+
// Redrawn without its actions (check-in's card turned into its notice
|
|
81
|
+
// row): it no longer asks (stage_waiting.js).
|
|
82
|
+
if (!asks) turnDom.settleWaiting?.(pluginCardKey(card.id), { only: "asks" });
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
bubbles.removeHintIfEmpty();
|
|
81
86
|
const el = drawCard(document.createElement(isNoticeCard(card) ? "details" : "div"), card);
|
|
82
|
-
const asks = (card.actions || []).some((a) => a && a.command);
|
|
83
87
|
if (card.in_turn && !before && !asks && turnDom.card(el)) return;
|
|
84
88
|
bubbles.placeFollowing(el, before);
|
|
85
89
|
}
|
|
@@ -49,10 +49,10 @@ body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetic
|
|
|
49
49
|
/* A session that waits on the user (a question, an approval, a card with actions): the warning colour of its question card, also when active or hovered. */
|
|
50
50
|
#topStrip .card.waiting,#allList .card.waiting{border-color:color-mix(in srgb,var(--warn) 75%,transparent)}
|
|
51
51
|
@media(hover:hover){#topStrip .card.waiting:hover,#allList .card.waiting:hover{border-color:var(--warn)}}
|
|
52
|
-
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.turn-stage,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel)}.bubble.question{background:
|
|
53
|
-
@media (prefers-reduced-transparency:reduce){.turn-stage,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.question{background:
|
|
52
|
+
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.turn-stage,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel)}.bubble.question{background:var(--bg-panel)}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel))}}
|
|
53
|
+
@media (prefers-reduced-transparency:reduce){.turn-stage,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.question{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}}
|
|
54
54
|
body.no-glass .turn-stage,body.no-glass #topStrip .card,body.no-glass #allList .card,body.no-glass #allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
55
|
-
body.no-glass .bubble.question{background:
|
|
55
|
+
body.no-glass .bubble.question{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
56
56
|
body.no-glass .bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
57
57
|
button,input,textarea,select{font:inherit}
|
|
58
58
|
button{background:var(--primary);border:0;color:var(--on-solid);padding:8px 12px;border-radius:8px;cursor:pointer;transition:filter .12s ease,background .12s ease}
|
|
@@ -426,24 +426,26 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
426
426
|
.gen:not(.live) .gen-text{font-size:14px;color:var(--text-2)}
|
|
427
427
|
.gen-text:empty{display:none}
|
|
428
428
|
.gen .activity-body{margin-top:6px}
|
|
429
|
-
/* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. A step that closed with text (a line steered in after it, its answer gone from the block) is a row: its summary is the way to its text.
|
|
430
|
-
|
|
431
|
-
.turn-work:not(
|
|
432
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen{
|
|
433
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen
|
|
429
|
+
/* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. A step that closed with text (a line steered in after it, its answer gone from the block) is a row: its summary is the way to its text. So is a running turn's block opened in the
|
|
430
|
+
history (data-opened-turn, stage_asking.js openTurn): the step's text is in it while a card without a lead-in waits. */
|
|
431
|
+
.turn-work:not([data-opened-turn]):not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)){border:0;padding:0;background:transparent}
|
|
432
|
+
.turn-work:not([data-opened-turn]):not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text))>summary,.turn-work:not([data-opened-turn]):not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen>summary{display:none}
|
|
433
|
+
.turn-work:not([data-opened-turn]):not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen{margin:0;padding:0;border:0;background:transparent}
|
|
434
|
+
.turn-work:not([data-opened-turn]):not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
|
|
434
435
|
@media(max-width:600px){.gen{--step-indent:8px}}
|
|
435
|
-
/* A question card asks for input like the composer card, so it wears the same glass (the recipe above)
|
|
436
|
-
|
|
436
|
+
/* A question card asks for input like the composer card, so it wears the same glass (the recipe above), no amber: what
|
|
437
|
+
waits says so in the stage's status row ("waiting for you", its dot) and on a waiting lead-in bubble. */
|
|
438
|
+
.bubble.question{border-left:3px solid var(--border-strong);padding:10px 12px}
|
|
437
439
|
/* Collapsed (resolved) question: one line, like the thinking and activity blocks.
|
|
438
|
-
The left bar
|
|
440
|
+
The left bar is neutral here; the existing answered/cancelled/denied classes
|
|
439
441
|
(which apply whether open or not) recolour it once resolved. */
|
|
440
442
|
.bubble.question:not([open]){padding:3px 10px;background:transparent;border-color:transparent;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;align-self:flex-start;border-radius:6px;transition:background .12s ease}
|
|
441
|
-
.bubble.question:not([open]){border-left-color:var(--
|
|
443
|
+
.bubble.question:not([open]){border-left-color:var(--border-strong)}
|
|
442
444
|
@media(hover:hover){.bubble.question:not([open]):hover{background:var(--bg-panel)}}
|
|
443
445
|
.bubble.question:not([open])>summary{font-size:12px;font-weight:500}
|
|
444
446
|
.bubble.question>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
|
|
445
447
|
.bubble.question>summary::-webkit-details-marker{display:none}
|
|
446
|
-
.bubble.question>summary::before{content:"▸ ";color:var(--
|
|
448
|
+
.bubble.question>summary::before{content:"▸ ";color:var(--text-faint)}
|
|
447
449
|
.bubble.question[open]>summary::before{content:"▾ "}
|
|
448
450
|
/* An answered card keeps the reply that asked it (question_cards.js): muted under its "Q → A" line, two lines while
|
|
449
451
|
closed (newlines as spaces); open, the summary's copy goes and the body's starts the card, whole, as written and
|
|
@@ -456,6 +458,9 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
456
458
|
.bubble.said+.bubble.question :is(.question-said,.question-said-full){display:none}
|
|
457
459
|
/* Its asking step in the steps block doesn't repeat the reason either (stage_opened.js data-said, until the hand-off). */
|
|
458
460
|
.turn-work>details.gen[data-said]>.gen-text{display:none}
|
|
461
|
+
/* A card that waits in the stage has no toggle of its own (stage_view.js): the stage's fold is the one. */
|
|
462
|
+
.turn-stage .bubble.question:not(.answered):not(.cancelled)>summary{cursor:default}
|
|
463
|
+
.turn-stage .bubble.question:not(.answered):not(.cancelled)>summary::before{content:none}
|
|
459
464
|
.question-reason-link{float:right;margin-left:12px;padding:0;border:0;background:none;color:var(--accent);font-size:12.5px;font-weight:500;line-height:inherit}
|
|
460
465
|
@media(hover:hover){.question-reason-link:hover:not(:disabled){filter:none;text-decoration:underline}}
|
|
461
466
|
/* The reply that asked the model's own question, a conversation bubble while its turn runs (live only): a label line ("chi ·
|
|
@@ -479,19 +484,13 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
479
484
|
--md-strong-color:currentColor;--md-blockquote-color:currentColor;--md-code-color:currentColor;
|
|
480
485
|
--md-pre-size:12px;--md-cell-size:12.5px
|
|
481
486
|
}
|
|
482
|
-
.ts-asking.md-compact{
|
|
483
|
-
--md-heading-size:14.5px;--md-heading-color:var(--text-bright);
|
|
484
|
-
--md-subheading-size:14px;--md-subheading-color:var(--text-2);
|
|
485
|
-
--md-strong-color:var(--text-bright);--md-blockquote-color:var(--text-muted);--md-code-color:var(--text);
|
|
486
|
-
--md-pre-size:12.5px;--md-cell-size:13px
|
|
487
|
-
}
|
|
488
487
|
.gen-text.md-compact{
|
|
489
488
|
--md-heading-size:14.5px;--md-heading-color:var(--text);
|
|
490
489
|
--md-subheading-size:14px;--md-subheading-color:var(--text-2);
|
|
491
490
|
--md-strong-color:var(--text);--md-blockquote-color:var(--text-muted);--md-code-color:var(--text);
|
|
492
491
|
--md-pre-size:12.5px;--md-cell-size:13px
|
|
493
492
|
}
|
|
494
|
-
.md-compact:is(.
|
|
493
|
+
.md-compact:is(.question-said-full,.gen-text,.said-text){white-space:normal}
|
|
495
494
|
.md-compact :is(h1,h2,h3,h4,h5,h6){font-size:var(--md-heading-size);font-weight:600;line-height:1.4;margin:8px 0 2px;color:var(--md-heading-color)}
|
|
496
495
|
.md-compact :is(h4,h5,h6){font-size:var(--md-subheading-size);color:var(--md-subheading-color)}
|
|
497
496
|
.md-compact :is(p,ul,ol,blockquote,table,pre){margin:0 0 6px}
|
|
@@ -541,7 +540,7 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
541
540
|
.question-option{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--text-2);padding:6px 8px;border:1px solid var(--border);border-radius:8px;cursor:pointer}
|
|
542
541
|
@media(hover:hover){.question-option:hover{background:var(--bg-hover)}}
|
|
543
542
|
.question-option.selected{border-color:var(--ok);color:var(--text)}
|
|
544
|
-
.question-option input{margin:0;accent-color:var(--
|
|
543
|
+
.question-option input{margin:0;accent-color:var(--accent)}
|
|
545
544
|
/* The model's recommended option (a label ending in "(Recommended)"): an accent row and a small tag. */
|
|
546
545
|
.question-option.recommended{border-color:var(--accent-border);background:var(--accent-bg);color:var(--text)}
|
|
547
546
|
.question-option.recommended.selected{border-color:var(--ok)}
|
|
@@ -558,7 +557,7 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
558
557
|
.bubble.question.cancelled .question-result{color:var(--text-muted)}
|
|
559
558
|
.question-submit{align-self:flex-end;font-size:13px}
|
|
560
559
|
.question-dismiss{font-size:13px;margin-left:8px}
|
|
561
|
-
.bubble.question.approval{border-left-color:var(--
|
|
560
|
+
.bubble.question.approval{border-left-color:var(--border-strong)}
|
|
562
561
|
.bubble.question.approval.answered:not(.denied){border-left-color:var(--ok)}
|
|
563
562
|
.bubble.question.approval:not([open]){background:transparent}
|
|
564
563
|
@media(hover:hover){.bubble.question.approval:not([open]):hover{background:var(--bg-panel)}}
|
|
@@ -575,9 +574,9 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
575
574
|
.question-relayed.hidden{display:none}
|
|
576
575
|
.question-relayed a{color:var(--text);font-weight:600}
|
|
577
576
|
.approval-tool{font-weight:700;color:var(--text);margin-bottom:4px}
|
|
578
|
-
|
|
577
|
+
/* What it approves, neutral like the card (no amber on question cards): the command in the code's own box. */
|
|
578
|
+
.approval-what{margin:0 0 6px;padding:8px 10px;background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;white-space:pre-wrap;word-break:break-all;color:var(--text-2);font-size:12.5px}
|
|
579
579
|
.approval-where,.approval-why{font-size:12px;color:var(--text-muted);overflow-wrap:anywhere}
|
|
580
|
-
.approval-why{color:var(--warn-text)}
|
|
581
580
|
.approval-label{color:var(--text-faint)}
|
|
582
581
|
/* A diff (diff_view.js): the approval card's dry run and a tool row's change. */
|
|
583
582
|
.approval-diff{margin:0 0 6px}
|
|
@@ -841,7 +840,9 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
841
840
|
.turn-stage[hidden]{display:none}
|
|
842
841
|
.turn-stage.ended{border-left-color:var(--border-strong)}
|
|
843
842
|
.turn-stage[data-phase="answered"]{border-left-color:var(--ok)}
|
|
844
|
-
.turn-stage[data-phase="canceled"]
|
|
843
|
+
.turn-stage[data-phase="canceled"]{border-left-color:var(--warn)}
|
|
844
|
+
/* A card waits: the box has no amber edge (the status row's word and dot say it). */
|
|
845
|
+
.turn-stage[data-phase="waiting for you"]{border-left-color:var(--border-strong)}
|
|
845
846
|
.turn-stage[data-phase="failed"]{border-left-color:var(--danger)}
|
|
846
847
|
.turn-stage[data-phase="no answer"]{border-left-color:var(--warn)}
|
|
847
848
|
.turn-stage.enter{animation:ts-rise .35s cubic-bezier(.3,.7,.2,1)}
|
|
@@ -900,21 +901,36 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
900
901
|
.ts-headline.thinking{font-size:13.5px;font-style:italic;color:var(--text-muted)}
|
|
901
902
|
.ts-think-tag{font-style:normal;font-size:12.5px;color:var(--think-text);margin-right:6px}
|
|
902
903
|
.ts-headline.fresh{animation:ts-fade .3s ease}
|
|
903
|
-
/*
|
|
904
|
-
|
|
905
|
-
its room, read from the top. */
|
|
906
|
-
.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}
|
|
907
|
-
/* The asking form (stage_view.js, a turn opened in the history while the model's own question waits): its reply is a
|
|
908
|
-
bubble in the conversation (.bubble.said), the stage shows the status row and the card alone; the prompt, the live
|
|
904
|
+
/* The asking form (stage_view.js, a card docked: its turn opened in the history, a reply before chi's own question or
|
|
905
|
+
approval a bubble there, .bubble.said): the stage shows the status row and the card alone; the prompt, the live
|
|
909
906
|
slots (the working set's rail with them) and the steps chip come back with the working form after the answer. */
|
|
910
907
|
.turn-stage.asking-form :is(.ts-prompt,.ts-live,.ts-foot){display:none}
|
|
911
|
-
/* It is one box (the mock-up's option A): the stage's frame takes the card's look (
|
|
912
|
-
|
|
913
|
-
box
|
|
914
|
-
.turn-stage.asking-form{background:
|
|
908
|
+
/* It is one box (the mock-up's option A): the stage's frame takes the card's look (opaque, no glass, no amber: the status
|
|
909
|
+
row's "waiting for you" and its dot say it waits), the status row is the box's first line, and the card inside drops
|
|
910
|
+
its own box, so there is no box in a box. */
|
|
911
|
+
.turn-stage.asking-form{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none;border-radius:12px}
|
|
915
912
|
.turn-stage.asking-form .ts-scroll{padding-top:6px}
|
|
916
|
-
.turn-stage.asking-form .ts-extras
|
|
917
|
-
|
|
913
|
+
.turn-stage.asking-form .ts-extras>:is(.bubble.question,.bubble.plugin-card:not(.warn)){border:0;border-radius:0;background:transparent;padding:0}
|
|
914
|
+
/* The asking form folded (stage_view.js, its fold is that question's only): the status row says "waiting for you" and the
|
|
915
|
+
question (the mini tool's slot, in the UI font, right after the phase), then the step and the timing. On a phone those
|
|
916
|
+
two leave the row and the question takes a second line under it. */
|
|
917
|
+
.ts-mini.ts-mini-question{order:1;font-family:inherit;font-size:12.5px;color:var(--text)}
|
|
918
|
+
/* Two cards docked (stage_view.js drawDocked): one at a time, the one that stops chi in front; the pager after the phase
|
|
919
|
+
says where it is ("‹ 1 of 2 ›"), the other waits behind it. */
|
|
920
|
+
.turn-stage .ts-extras>.ts-behind{display:none}
|
|
921
|
+
.ts-pager{flex:none;display:inline-flex;align-items:center;gap:2px;color:var(--text-2)}
|
|
922
|
+
.ts-pager[hidden]{display:none}
|
|
923
|
+
.ts-page{flex:none;min-width:22px;height:22px;padding:0 4px;border-radius:6px;background:transparent;color:var(--text-muted);font-size:14px;line-height:1}
|
|
924
|
+
.ts-page:disabled{opacity:.35}
|
|
925
|
+
@media(hover:hover){.ts-page:hover:not(:disabled){filter:none;background:var(--bg-hover);color:var(--text)}}
|
|
926
|
+
.ts-page-label{font-variant-numeric:tabular-nums}
|
|
927
|
+
/* check-in docked while nothing blocks (stage_view.js .asks-live): the row says the running call right after the phase,
|
|
928
|
+
then the turn's time; the step count is the history's steps line. The live step's text streams under the card. */
|
|
929
|
+
.turn-stage.asks-live .ts-status>:not(.ts-mark,.ts-phase,.ts-mini){order:2}
|
|
930
|
+
.turn-stage.asks-live .ts-mini{order:1}
|
|
931
|
+
.turn-stage.asks-live .ts-step{display:none}
|
|
932
|
+
.ts-asks-text{padding-top:8px;border-top:1px dashed var(--border-strong);white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.55;color:var(--text)}
|
|
933
|
+
.turn-stage.asking-form.collapsed .ts-status>:not(.ts-mark,.ts-phase,.ts-mini){order:2}
|
|
918
934
|
@keyframes ts-fade{from{opacity:.35}}
|
|
919
935
|
.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}
|
|
920
936
|
.ts-spin{flex:none;width:11px;height:11px;border:2px solid var(--warn);border-right-color:transparent;border-radius:50%;animation:init-spin .8s steps(16) infinite}
|
|
@@ -974,7 +990,7 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
974
990
|
:is(.ts-extras>.bubble.user,.ts-prompt:not(.full)>.bubble.user)>.user-message.markdown :is(hr,img,.copy-btn){display:none}
|
|
975
991
|
/* Inside the dock's own blur a second backdrop-filter sees nothing: the cards are opaque here (as #commandList is). */
|
|
976
992
|
.turn-stage .bubble.question,.turn-stage .bubble.plugin-card{-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
|
|
977
|
-
.turn-stage .bubble.question{background:
|
|
993
|
+
.turn-stage .bubble.question{background:var(--bg-panel)}
|
|
978
994
|
.turn-stage .bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel))}
|
|
979
995
|
.turn-stage .bubble.plugin-card.warn{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel))}
|
|
980
996
|
.ts-answer{display:flex;flex-direction:column;gap:8px}
|
|
@@ -1194,7 +1210,10 @@ button.ws-row:hover:not(:disabled){filter:none}@media(hover:hover){button.ws-row
|
|
|
1194
1210
|
/* The quiet signal's short text on a phone; in the status row it takes the step's and the mini tool's room. */
|
|
1195
1211
|
.ts-stall .long{display:none}
|
|
1196
1212
|
.ts-stall .short{display:inline}
|
|
1197
|
-
.turn-stage.stalled:is(.collapsed,.plain) :is(.ts-step,.ts-mini){display:none}
|
|
1213
|
+
.turn-stage.stalled:is(.collapsed,.plain) :is(.ts-step,.ts-mini:not(.ts-mini-question)){display:none}
|
|
1214
|
+
.turn-stage.asking-form.collapsed .ts-status{flex-wrap:wrap;row-gap:4px}
|
|
1215
|
+
.turn-stage.asking-form.collapsed :is(.ts-step,.ts-timing){display:none}
|
|
1216
|
+
.ts-mini.ts-mini-question{order:3;flex-basis:100%}
|
|
1198
1217
|
/* An approval's Submit in sight on a phone: its folder, reason and rows a little tighter. */
|
|
1199
1218
|
.approval-where,.approval-why{line-height:1.35}
|
|
1200
1219
|
.bubble.question.approval .question-option{padding:5px 8px}
|
|
@@ -36,7 +36,7 @@ export function createMarkdownPlace({
|
|
|
36
36
|
pairAnswerRefs(el);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// The refs in one rendered text (
|
|
39
|
+
// The refs in one rendered text (a lead-in bubble, a kept reply),
|
|
40
40
|
// as linkTextRefs links the answers': a plugin's +refs+ first
|
|
41
41
|
// (pairRefsIn), then the `path:line` ones.
|
|
42
42
|
function linkRefsIn(el, refs) {
|