samagotchi 0.131.0 → 0.133.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 +88 -1
- data/bin/chi +19 -1
- data/docs/architecture.md +59 -1
- data/docs/cli.md +29 -8
- data/docs/configuration.md +115 -16
- data/docs/desktop.md +16 -6
- data/docs/guardrails.md +10 -4
- data/docs/internals/anthropic.md +3 -2
- data/docs/internals/engine.md +4 -4
- data/docs/internals/prompt-caching.md +26 -6
- data/docs/memory.md +16 -2
- data/docs/plugins.md +68 -1
- data/docs/sessions.md +60 -6
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +35 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
- data/lib/samagotchi/bundles/model-notes/manifest.yml +4 -1
- data/lib/samagotchi/bundles/model-notes/model_notes_tone.md +3 -0
- data/lib/samagotchi/bundles/model-notes/notes/README.md +14 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +18 -0
- data/lib/samagotchi/chi_home.rb +311 -0
- data/lib/samagotchi/chi_shim.rb +77 -0
- data/lib/samagotchi/child_env.rb +5 -3
- data/lib/samagotchi/config.rb +2 -0
- data/lib/samagotchi/context_fetch.rb +4 -2
- data/lib/samagotchi/desktop/macos.rb +100 -18
- data/lib/samagotchi/engine.rb +35 -4
- data/lib/samagotchi/engine_snapshots.rb +5 -0
- data/lib/samagotchi/gem_update.rb +19 -10
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +21 -2
- data/lib/samagotchi/guardrails/pair_command.rb +57 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +6 -3
- data/lib/samagotchi/guardrails/review_paths.rb +7 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/home_command.rb +145 -0
- data/lib/samagotchi/home_doctor.rb +177 -0
- data/lib/samagotchi/home_notice.rb +80 -0
- data/lib/samagotchi/installed_versions.rb +5 -3
- data/lib/samagotchi/llm_context_command.rb +53 -7
- data/lib/samagotchi/llm_context_override.rb +3 -2
- data/lib/samagotchi/plugin/api.rb +45 -1
- data/lib/samagotchi/plugin/context.rb +34 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/prompt_sections.rb +124 -0
- data/lib/samagotchi/project_env.rb +360 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +33 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +68 -19
- data/lib/samagotchi/session_manager.rb +54 -1
- data/lib/samagotchi/session_model_settings.rb +5 -2
- data/lib/samagotchi/system_prompt.rb +34 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +22 -3
- data/lib/samagotchi/testing/host.rb +22 -1
- data/lib/samagotchi/testing/recorder.rb +6 -1
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tools/builtins.rb +13 -2
- data/lib/samagotchi/update_command.rb +65 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +61 -1
- data/lib/samagotchi/web/public/app.js +4 -1
- data/lib/samagotchi/web/public/chip_popover.js +5 -2
- data/lib/samagotchi/web/public/composer.css +11 -1
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/index.html +52 -10
- data/lib/samagotchi/web/public/level_chip.js +22 -38
- data/lib/samagotchi/web/public/llm_context_chip.js +30 -73
- data/lib/samagotchi/web/public/model_pick.js +26 -0
- data/lib/samagotchi/web/public/model_picker.js +3 -1
- data/lib/samagotchi/web/public/review_start.js +13 -57
- data/lib/samagotchi/web/public/send_turn.js +3 -2
- data/lib/samagotchi/web/public/start_page.js +131 -99
- data/lib/samagotchi/web/public/start_settings.js +298 -0
- data/lib/samagotchi/web/public/start_toolkit.js +157 -0
- data/lib/samagotchi/web/public/thinking_chip.js +9 -22
- data/lib/samagotchi/web/server.rb +27 -8
- data/lib/samagotchi/web/update_check.rb +3 -2
- metadata +11 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "cli/command"
|
|
4
4
|
require_relative "cli/flags"
|
|
5
|
+
require_relative "chi_home"
|
|
5
6
|
require_relative "config"
|
|
6
7
|
require_relative "desktop"
|
|
7
8
|
require_relative "gem_update"
|
|
@@ -57,6 +58,10 @@ module Samagotchi
|
|
|
57
58
|
# Hidden: the old version, passed by the chi that installed this one.
|
|
58
59
|
f.value "--gem-from"
|
|
59
60
|
end
|
|
61
|
+
# "1" in a chi update another chi exec'd into the chi home (#to_home):
|
|
62
|
+
# it doesn't exec again. An env marker, not a flag: the home's chi may
|
|
63
|
+
# be older than the one exec'ing it, and refuse a flag it doesn't know.
|
|
64
|
+
IN_HOME_ENV = "SAMAGOTCHI_UPDATE_IN_HOME"
|
|
60
65
|
# The switches the newer gem's update gets again (hand_over).
|
|
61
66
|
HANDED_OVER = { "--no-gem" => :no_gem, "--no-bundles" => :no_bundles, "--no-desktop" => :no_desktop,
|
|
62
67
|
"--no-register" => :no_register, "--to" => :to, "--json" => :json }.freeze
|
|
@@ -73,11 +78,16 @@ module Samagotchi
|
|
|
73
78
|
# @param platform [#call] register: → a Desktop::MacOS-like object
|
|
74
79
|
# @param exec [#call] argv → replaces this process (the new chi's update)
|
|
75
80
|
# @param bundler [Boolean] running under Bundler (bundle exec)
|
|
81
|
+
# @param home [#current, #record_first!] the chi home (ChiHome)
|
|
82
|
+
# @param env [Hash] IN_HOME_ENV is taken out of it
|
|
76
83
|
def initialize(argv, stdout: $stdout, stderr: $stderr, gem_spec: InstalledGem.spec,
|
|
77
84
|
shipped_dir: MemoryBundle::SourceNormalizer::SHIPPED_DIR, platform: nil,
|
|
78
85
|
supported: Desktop.supported?, state_dir: Session.default_state_dir, web: nil,
|
|
79
|
-
gem_update:
|
|
80
|
-
@
|
|
86
|
+
gem_update: nil, exec: nil, bundler: nil, home: ChiHome, env: ENV)
|
|
87
|
+
@home = home
|
|
88
|
+
@in_home = env.delete(IN_HOME_ENV) == "1"
|
|
89
|
+
# Built after the home is known: its install env goes into the home.
|
|
90
|
+
@gem_update_given = gem_update
|
|
81
91
|
@exec = exec || ->(argv) { Kernel.exec(*argv) }
|
|
82
92
|
@bundler = bundler.nil? ? (defined?(::Bundler) || !ENV["BUNDLE_GEMFILE"].to_s.empty?) : bundler
|
|
83
93
|
@argv = argv.dup
|
|
@@ -102,6 +112,11 @@ module Samagotchi
|
|
|
102
112
|
end
|
|
103
113
|
return usage_error("invalid version #{options[:to]}") if options[:to] && !Gem::Version.correct?(options[:to])
|
|
104
114
|
|
|
115
|
+
# The home's chi decides the rest (--to's floor included); never from
|
|
116
|
+
# a checkout (ChiHome.current is nil there).
|
|
117
|
+
@run_home = @gem_spec && @home.current(gem_spec: @gem_spec)
|
|
118
|
+
return to_home(options) if away_from_home?(options)
|
|
119
|
+
|
|
105
120
|
if options[:to] && !options[:gem_from] && Gem::Version.new(options[:to]) <= Gem::Version.new(VERSION)
|
|
106
121
|
@stderr.puts("chi update: #{options[:to]} is not newer than the running chi (#{VERSION})")
|
|
107
122
|
return 1
|
|
@@ -113,6 +128,12 @@ module Samagotchi
|
|
|
113
128
|
return 1
|
|
114
129
|
end
|
|
115
130
|
|
|
131
|
+
# No home yet: the first record, once this chi's own checks passed.
|
|
132
|
+
@run_home ||= record_home(options)
|
|
133
|
+
return to_home(options) if away_from_home?(options)
|
|
134
|
+
|
|
135
|
+
@gem_update = @gem_update_given ||
|
|
136
|
+
GemUpdate.new(gem_bin: ChiHome.gem_bin(@run_home), install_env: ChiHome.install_env(@run_home))
|
|
116
137
|
@dry_run = options[:dry_run]
|
|
117
138
|
gem = gem_row(options)
|
|
118
139
|
rows = [gem, system_bundle_row, *bundle_rows(options), desktop_row(options), *live_rows].compact
|
|
@@ -130,6 +151,42 @@ module Samagotchi
|
|
|
130
151
|
|
|
131
152
|
def command_name = "chi update"
|
|
132
153
|
|
|
154
|
+
# Whether this run is on another Ruby than the chi home's, and not one
|
|
155
|
+
# another chi exec'd or handed over to.
|
|
156
|
+
def away_from_home?(options) = @run_home && !@in_home && !options[:gem_from] && !@run_home.running_here?
|
|
157
|
+
|
|
158
|
+
# The first chi home (ChiHome.record_first!: the helper's Ruby or this
|
|
159
|
+
# one, whichever has the newer chi), said once. Not in a dry run, a run
|
|
160
|
+
# another chi exec'd or handed over to, nor under Bundler.
|
|
161
|
+
def record_home(options)
|
|
162
|
+
return nil if options[:dry_run] || @in_home || options[:gem_from] || @bundler
|
|
163
|
+
|
|
164
|
+
recorded = @home.record_first!(gem_spec: @gem_spec, launch_path: helper_launch_path, bundler: false)
|
|
165
|
+
notice(options, "chi home set to #{recorded.ruby} (chi home set to change)") if recorded
|
|
166
|
+
recorded
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A chi on another Ruby than the home's updates the home: exec its chi
|
|
170
|
+
# (newer?, --to and the bundle rows are then the home's), once
|
|
171
|
+
# (IN_HOME_ENV).
|
|
172
|
+
def to_home(options)
|
|
173
|
+
notice(options, "updating the chi home (#{@run_home.ruby})")
|
|
174
|
+
@stdout.flush
|
|
175
|
+
@exec.call([{ IN_HOME_ENV => "1" }, *@run_home.boot_argv(run: ChiHome::CHI_BOOT), "--", "update", *@argv])
|
|
176
|
+
0 # only when exec is a spec's stand-in
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def notice(options, text) = (options[:json] ? @stderr : @stdout).puts(text)
|
|
180
|
+
|
|
181
|
+
def helper_launch_path
|
|
182
|
+
return nil unless @supported
|
|
183
|
+
|
|
184
|
+
helper = @platform.call(register: false)
|
|
185
|
+
helper.respond_to?(:launch_path) ? helper.launch_path : nil
|
|
186
|
+
rescue StandardError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
133
190
|
# @return [Hash, Integer] the options, or the usage error's exit status
|
|
134
191
|
# (every error reads "unknown option", a --gem-from without its value too)
|
|
135
192
|
def parse
|
|
@@ -181,16 +238,17 @@ module Samagotchi
|
|
|
181
238
|
hand_over(options, row)
|
|
182
239
|
end
|
|
183
240
|
|
|
184
|
-
# exec the
|
|
185
|
-
# Without
|
|
241
|
+
# exec the newest installed samagotchi: the chi home's boot, else the
|
|
242
|
+
# wrapper. Without either (installed with --bindir), this version syncs.
|
|
186
243
|
def hand_over(options, row)
|
|
187
|
-
wrapper = InstalledGem.wrapper(@gem_spec)
|
|
188
|
-
return done(row, :update, "run chi update again to sync with it") unless wrapper
|
|
244
|
+
wrapper = InstalledGem.wrapper(@gem_spec) unless @run_home
|
|
245
|
+
return done(row, :update, "run chi update again to sync with it") unless @run_home || wrapper
|
|
189
246
|
|
|
190
247
|
passed = options[:to] ? ["--to", options[:to]] : []
|
|
191
248
|
passed += HANDED_OVER.except("--to").select { |_, key| options[key] }.keys
|
|
249
|
+
args = ["update", "--no-gem", "--gem-from", VERSION, *passed]
|
|
192
250
|
@stdout.flush
|
|
193
|
-
@exec.call(
|
|
251
|
+
@exec.call(@run_home ? @run_home.boot_argv(run: ChiHome::CHI_BOOT) + ["--", *args] : [RbConfig.ruby, wrapper, *args])
|
|
194
252
|
done(row, :update) # only when exec is a spec's stand-in
|
|
195
253
|
end
|
|
196
254
|
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -48,6 +48,8 @@ require_relative "../context_sources"
|
|
|
48
48
|
require_relative "../context_providers"
|
|
49
49
|
require_relative "../recap_store"
|
|
50
50
|
require_relative "../toolkit"
|
|
51
|
+
require_relative "../toolkit_suggest"
|
|
52
|
+
require_relative "../system_one/client"
|
|
51
53
|
require_relative "../turn_flow"
|
|
52
54
|
require_relative "../session_chain"
|
|
53
55
|
require_relative "../session_access"
|
|
@@ -78,6 +80,7 @@ require_relative "proxy_gate"
|
|
|
78
80
|
require_relative "../log"
|
|
79
81
|
require_relative "../paths"
|
|
80
82
|
require_relative "../redacted_inspect"
|
|
83
|
+
require_relative "../chi_home"
|
|
81
84
|
require_relative "../child_env"
|
|
82
85
|
require_relative "../installed_gem"
|
|
83
86
|
require_relative "../live_versions"
|
|
@@ -759,8 +762,17 @@ module Samagotchi
|
|
|
759
762
|
error_response(503, "update_start_failed", "couldn't start chi update")
|
|
760
763
|
end
|
|
761
764
|
|
|
765
|
+
# The chi update child: with a chi home, the home's chi (its gem dirs
|
|
766
|
+
# are what new workers activate); else this Ruby and the wrapper.
|
|
767
|
+
def update_argv(version, home: ChiHome.current)
|
|
768
|
+
args = ["update", "--to", version, "--no-desktop", "--json"]
|
|
769
|
+
return home.boot_argv(run: ChiHome::CHI_BOOT) + ["--", *args] if home
|
|
770
|
+
|
|
771
|
+
[RbConfig.ruby, InstalledGem.wrapper, *args]
|
|
772
|
+
end
|
|
773
|
+
|
|
762
774
|
def perform_update(version)
|
|
763
|
-
argv =
|
|
775
|
+
argv = update_argv(version)
|
|
764
776
|
said = nil
|
|
765
777
|
output, success = @update_runner.call(argv, env: ChildEnv.unbundled,
|
|
766
778
|
progress: lambda { |line|
|
|
@@ -1209,9 +1221,57 @@ module Samagotchi
|
|
|
1209
1221
|
add_sampling(models, registry)
|
|
1210
1222
|
payload = { default: default, models: models }
|
|
1211
1223
|
payload[:warning] = warnings.join("; ") unless warnings.empty?
|
|
1224
|
+
toolkits = toolkits_listing(registry)
|
|
1225
|
+
payload[:toolkits] = toolkits if toolkits
|
|
1212
1226
|
json_response(200, payload)
|
|
1213
1227
|
end
|
|
1214
1228
|
|
|
1229
|
+
# Kits the web never offers: the participants' boundary, a delegate
|
|
1230
|
+
# child's, and ToolkitSuggest's defer sentinel.
|
|
1231
|
+
PICKER_HIDDEN_KITS = [Toolkit::REVIEW, Toolkit::CHILD, "undecided"].freeze
|
|
1232
|
+
|
|
1233
|
+
# The start page's toolkit picker: {default:, suggest:, kits: [{name,
|
|
1234
|
+
# description?}]}. Built-in and config kits only (a bundle's load in
|
|
1235
|
+
# the worker), less PICKER_HIDDEN_KITS and any kit a new session would
|
|
1236
|
+
# refuse (Toolkit.check_new!) or that doesn't resolve (a base that is a
|
|
1237
|
+
# bundle kit); the configured default always listed, first when it
|
|
1238
|
+
# isn't one of them. +suggest+: "on" only when the worker would ask
|
|
1239
|
+
# (toolkit.suggest.mode on, a System 1 host, at least 2 described
|
|
1240
|
+
# candidates), else the mode ("audit") or "off". nil when it can't be
|
|
1241
|
+
# read: the picker stays hidden.
|
|
1242
|
+
def toolkits_listing(registry)
|
|
1243
|
+
default = Toolkit.configured.name
|
|
1244
|
+
config = Toolkit.config_kits
|
|
1245
|
+
kits = Toolkit.kits(config: config)
|
|
1246
|
+
listed = kits.values.filter_map do |kit|
|
|
1247
|
+
next if PICKER_HIDDEN_KITS.include?(kit.name) || !ToolkitSuggest.resolves?(kit.name, kits)
|
|
1248
|
+
next unless new_session_kit?(kit.name, config)
|
|
1249
|
+
|
|
1250
|
+
kit.description.to_s.empty? ? { name: kit.name } : { name: kit.name, description: kit.description }
|
|
1251
|
+
end
|
|
1252
|
+
listed.unshift({ name: default }) if listed.none? { |kit| kit[:name] == default }
|
|
1253
|
+
{ default: default, suggest: toolkit_suggest_mode(registry, kits, default), kits: listed }
|
|
1254
|
+
rescue StandardError => e
|
|
1255
|
+
Log.warn(:web, "toolkits_listing_failed", error: e.class.name, msg: e.message)
|
|
1256
|
+
nil
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
def new_session_kit?(name, config)
|
|
1260
|
+
Toolkit.check_new!(Toolkit.new(name: name), config: config)
|
|
1261
|
+
true
|
|
1262
|
+
rescue Toolkit::Refused
|
|
1263
|
+
false
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
def toolkit_suggest_mode(registry, kits, default)
|
|
1267
|
+
mode = Config.get("toolkit.suggest.mode").to_s.strip.downcase
|
|
1268
|
+
return mode == "audit" ? "audit" : "off" unless mode == "on"
|
|
1269
|
+
return "off" if ToolkitSuggest.candidates(kits, default_name: default).size < 2
|
|
1270
|
+
return "off" unless SystemOne::Client.from_config(host_registry: registry, mode: "on", tag: :toolkit)
|
|
1271
|
+
|
|
1272
|
+
"on"
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1215
1275
|
# Each model's configured sampling as /model words it ("temperature=0.6
|
|
1216
1276
|
# (hosts.work)"), on the models that have some: the picker's tooltip;
|
|
1217
1277
|
# and each model's LLM context (LLMContextStrategy::Explained#summary).
|
|
@@ -194,7 +194,7 @@ const participantsControl = createParticipantsControl({
|
|
|
194
194
|
onChange: () => updateInfoBar(),
|
|
195
195
|
toast: (text, action, options) => showToast(text, action, options),
|
|
196
196
|
});
|
|
197
|
-
// The start page's model picker
|
|
197
|
+
// The start page's model picker, its ⚙ panel and 🧰 list with the override chips
|
|
198
198
|
// (start_page.js); a fresh chain link's picker too.
|
|
199
199
|
const startPage = createStartPage({
|
|
200
200
|
pickEl: $("#modelPick"),
|
|
@@ -203,9 +203,12 @@ const startPage = createStartPage({
|
|
|
203
203
|
listEl: $("#modelList"),
|
|
204
204
|
composerEl: $("#composer"),
|
|
205
205
|
centerEl: $("#center"),
|
|
206
|
+
settingsEl: $("#settingsNew"),
|
|
206
207
|
llmContextEl: $("#llmContextNew"),
|
|
207
208
|
thinkingEl: $("#thinkingNew"),
|
|
208
209
|
reviewEl: $("#reviewNew"),
|
|
210
|
+
toolkitBtnEl: $("#toolkitBtn"),
|
|
211
|
+
toolkitEl: $("#toolkitNew"),
|
|
209
212
|
selected: () => selected,
|
|
210
213
|
state: () => current,
|
|
211
214
|
running: () => liveTurn.running,
|
|
@@ -45,10 +45,12 @@ export function sheetPlace({ innerWidth, innerHeight }) {
|
|
|
45
45
|
// open, else a refresh with a newer summary); onClose(): the caller forgets
|
|
46
46
|
// its open state; onSubmit(form); onAct(name): a data-act other than close.
|
|
47
47
|
// sheet: a bottom sheet in a narrow window (sheetPlace). label: the
|
|
48
|
-
// dialog's accessible name (aria-label), none by default.
|
|
48
|
+
// dialog's accessible name (aria-label), none by default. onOpen(): after
|
|
49
|
+
// an open has shown and placed it (where focus can go into it: show runs
|
|
50
|
+
// while it is still hidden).
|
|
49
51
|
// @return {pop, open, close, refresh, isOpen, opener}
|
|
50
52
|
export function createChipPopover({ id, className, width: widest, el, anchor: anchorSel, openers = anchorSel, summary,
|
|
51
|
-
session = () => null, show, onClose = () => {}, onSubmit, onAct = () => {},
|
|
53
|
+
session = () => null, show, onClose = () => {}, onSubmit, onAct = () => {}, onOpen = () => {},
|
|
52
54
|
sheet = false, label = null, doc = document, win = window }) {
|
|
53
55
|
const pop = doc.createElement("div");
|
|
54
56
|
pop.id = id;
|
|
@@ -93,6 +95,7 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
|
|
|
93
95
|
show(data, true);
|
|
94
96
|
pop.hidden = false;
|
|
95
97
|
place(anchor);
|
|
98
|
+
onOpen();
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
// A closed popover is empty, so each open draws a fresh form (unsent
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
the / list, and the 16px text fields for iOS. */
|
|
6
6
|
/* The image button's icon mask (the stroke only shapes the mask; the icon takes currentColor); index.html's phone info
|
|
7
7
|
bar and all view's cards use it too. */
|
|
8
|
-
:root{--icon-image: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='2' y='2.75' width='12' height='10.5' rx='1.5'/%3E%3Ccircle cx='5.75' cy='6.25' r='1.1'/%3E%3Cpath d='m2.5 12.5 3.75-3.75 2.5 2.5 1.75-1.75 3 3'/%3E%3C/svg%3E")}
|
|
8
|
+
:root{--icon-toolkit: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.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1.75' y='5.25' width='12.5' height='8' rx='1.5'/%3E%3Cpath d='M5.75 5.25v-1.5a1 1 0 0 1 1-1h2.5a1 1 0 0 1 1 1v1.5M1.75 8.75h12.5M7 8.75v1.5h2v-1.5'/%3E%3C/svg%3E");--icon-settings: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.4' stroke-linejoin='round'%3E%3Cpath d='M12.75 6.14L14.45 6.59L14.45 9.41L12.75 9.86L12.67 10.04L13.56 11.56L11.56 13.56L10.04 12.67L9.86 12.75L9.41 14.45L6.59 14.45L6.14 12.75L5.96 12.67L4.44 13.56L2.44 11.56L3.33 10.04L3.25 9.86L1.55 9.41L1.55 6.59L3.25 6.14L3.33 5.96L2.44 4.44L4.44 2.44L5.96 3.33L6.14 3.25L6.59 1.55L9.41 1.55L9.86 3.25L10.04 3.33L11.56 2.44L13.56 4.44L12.67 5.96Z'/%3E%3Ccircle cx='8' cy='8' r='2.1'/%3E%3C/svg%3E");--icon-image: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='2' y='2.75' width='12' height='10.5' rx='1.5'/%3E%3Ccircle cx='5.75' cy='6.25' r='1.1'/%3E%3Cpath d='m2.5 12.5 3.75-3.75 2.5 2.5 1.75-1.75 3 3'/%3E%3C/svg%3E")}
|
|
9
9
|
/* The composer card: the glass card both pages' composers sit in (the owner's #dock, the stage view's .dock-card, the
|
|
10
10
|
participant page's .p-card), with its focus ring; its footer (the owner's info bar, the participant's usage) under
|
|
11
11
|
a hairline; and the composer's line, the quiet row above the text field (the owner's delivery line, the
|
|
@@ -73,6 +73,16 @@ body.has-session.composer-folded #composeGrip::after{opacity:1;width:48px;margin
|
|
|
73
73
|
#composer .actions-col .attach-btn::before{content:"";display:block;width:16px;height:16px;margin:auto;background:currentColor;-webkit-mask:var(--icon-image) center/contain no-repeat;mask:var(--icon-image) center/contain no-repeat}
|
|
74
74
|
#composer .actions-col .attach-btn:hover{filter:none;color:var(--text);background:color-mix(in srgb,var(--bg-hover) 75%,transparent)}
|
|
75
75
|
#composer .actions-col .attach-btn[hidden]{display:none}
|
|
76
|
+
/* An icon button on the row (the start page's ⚙ and 🧰, start_settings.js and start_toolkit.js): quiet like the image button, its icon a mask in --icon; before it (order 2, earlier in the markup).
|
|
77
|
+
.set: something under it is set, an accent dot on a phone (the override chips say it on a wider row). */
|
|
78
|
+
#composer .actions-col .icon-btn{order:2;position:relative;flex:none;width:28px;padding:0;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--text-muted)}
|
|
79
|
+
#composer .actions-col .icon-btn::before{content:"";display:block;width:16px;height:16px;margin:auto;background:currentColor;-webkit-mask:var(--icon) center/contain no-repeat;mask:var(--icon) center/contain no-repeat}
|
|
80
|
+
#composer .actions-col .icon-btn:hover,#composer .actions-col .icon-btn[aria-expanded="true"]{filter:none;color:var(--text);background:color-mix(in srgb,var(--bg-hover) 75%,transparent)}
|
|
81
|
+
#composer .actions-col .icon-btn[hidden],body.has-session #composer .actions-col .icon-btn{display:none}
|
|
82
|
+
#composer .actions-col .settings-btn{--icon:var(--icon-settings)}
|
|
83
|
+
#composer .actions-col .toolkit-btn{--icon:var(--icon-toolkit)}
|
|
84
|
+
@media(max-width:600px){#composer .actions-col .icon-btn.set{color:var(--accent)}#composer .actions-col .icon-btn.set::after{content:"";position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--accent)}}
|
|
85
|
+
@media(pointer:coarse){#composer .actions-col .icon-btn{width:36px;height:36px;min-height:36px}}
|
|
76
86
|
/* In a session the composer is one row: the text field (one line, growing with its text; fitPrompt sets its height)
|
|
77
87
|
and Cancel · Send at its bottom right. The key hint is in the placeholder there, so its row goes. */
|
|
78
88
|
body.has-session #composer{flex-direction:row;align-items:flex-end;padding:10px}
|
|
@@ -57,13 +57,17 @@ export function listSessions(sort, order, { dir, ...opts } = {}) {
|
|
|
57
57
|
// it); either left out means the server's default. preview: that message,
|
|
58
58
|
// which names the session until its turn is saved. llm_context: the
|
|
59
59
|
// session's own LLM context words ({strategy, apply, budget}), when the
|
|
60
|
-
// start page's
|
|
61
|
-
// start page's
|
|
60
|
+
// start page's ⚙ panel set some; thinking: its own level ("low"), when the
|
|
61
|
+
// start page's ⚙ panel set one; toolkit: the kit it starts on, when the
|
|
62
|
+
// start page's 🧰 list picked one (none: config's default, maybe suggested).
|
|
62
63
|
// review: {subjects} makes it a review session (it starts idle; no preview).
|
|
63
|
-
|
|
64
|
+
// The other fields are api()'s fetch options.
|
|
65
|
+
export function createIdleSession({ dir, model, preview, llm_context: llmContext, thinking, toolkit, review,
|
|
66
|
+
...opts } = {}) {
|
|
64
67
|
const start = review ? { review } : { idle: true, ...(preview ? { preview } : {}) };
|
|
65
68
|
const body = { ...start, ...(dir ? { dir } : {}), ...(model ? { model } : {}),
|
|
66
|
-
...(llmContext ? { llm_context: llmContext } : {}), ...(thinking ? { thinking } : {})
|
|
69
|
+
...(llmContext ? { llm_context: llmContext } : {}), ...(thinking ? { thinking } : {}),
|
|
70
|
+
...(toolkit ? { toolkit } : {}) };
|
|
67
71
|
return api("/api/sessions", { method: "POST", body: JSON.stringify(body), ...opts });
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -607,6 +607,10 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
607
607
|
#lightbox img{max-width:96vw;max-height:94vh;border-radius:6px}
|
|
608
608
|
/* The model picker (the start page, and a fresh chain link: start_page.js syncModelPick): reads as a label, "host:model ▾", left of Start; a button under a drawn caret that opens a searchable list (model_picker.js). Its name ellipsizes within max-width. */
|
|
609
609
|
#composer .actions-col .model-pick{order:0;position:relative;display:inline-block;max-width:min(50%,380px);height:28px;min-height:28px;padding:0 22px 0 8px;border-radius:8px;overflow:hidden;background:transparent;color:var(--text-muted);font-size:12px;line-height:28px;text-align:left;text-overflow:ellipsis;transition:background .12s ease,color .12s ease}
|
|
610
|
+
/* The start page's row stays one row (Start never wraps): the model name gives way first, down to its min-width, then the override chips ellipsize (100000x their shrink weight: at 1000x their share of the shrink was still a fraction of a pixel, enough for an ellipsis). Scoped off the session composer and a fresh link's button. */
|
|
611
|
+
body:not(.has-session) #composer .actions-col{flex-wrap:nowrap}
|
|
612
|
+
body:not(.has-session) #composer .actions-col .model-pick{flex:0 100000 auto;min-width:min(84px,20vw)}
|
|
613
|
+
body:not(.has-session) #composer .actions-col .llmctx-new{flex:0 1 auto;min-width:40px}
|
|
610
614
|
#composer .actions-col .model-pick:hover,#composer .actions-col .model-pick[aria-expanded="true"]{filter:none;background:color-mix(in srgb,var(--bg-hover) 75%,transparent);color:var(--text-2)}
|
|
611
615
|
#composer .actions-col .model-pick[hidden],body.has-session #composer .actions-col .model-pick{display:none}
|
|
612
616
|
.model-pick-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -616,15 +620,12 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
616
620
|
/* A fresh link's (body.fresh-link): the session composer is one row and .actions-col is content-sized there, so a px cap keeps the text field's room and Send in view. :not([hidden]) keeps a failed /api/models' hide (this selector outranks the [hidden] one). */
|
|
617
621
|
body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){display:inline-block;flex:0 1 auto;min-width:0;max-width:200px}
|
|
618
622
|
@media(max-width:600px){body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){max-width:100px}}
|
|
619
|
-
/* The new session's
|
|
620
|
-
#composer .actions-col .llmctx-new{order:
|
|
621
|
-
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--
|
|
622
|
-
#composer .actions-col .llmctx-new.own{color:var(--accent);border-color:var(--accent-border)}
|
|
623
|
+
/* The new session's override chips (start_page.js: what the ⚙ panel has set, only then), right of the key hint next to ⚙: accent; a click opens the panel. A phone has ⚙'s dot instead (below). */
|
|
624
|
+
#composer .actions-col .llmctx-new{order:1;flex:0 1 auto;min-width:0;max-width:min(30%,220px);height:24px;min-height:24px;line-height:22px;padding:0 6px;border-radius:6px;border:1px solid var(--accent-border);background:transparent;color:var(--accent);font-size:11px;font-weight:400;overflow:hidden;text-overflow:ellipsis}
|
|
625
|
+
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--accent);background:var(--accent-bg)}
|
|
623
626
|
#composer .actions-col .llmctx-new[hidden],body.has-session #composer .actions-col .llmctx-new{display:none}
|
|
624
|
-
/*
|
|
625
|
-
#composer .actions-col .
|
|
626
|
-
/* Phones (the key hint is gone there): the chips keep their whole text and the row wraps, Start staying right (its margin-left:auto) on the last line; only a model name wider than the row ellipsizes. */
|
|
627
|
-
@media(max-width:600px){#composer .actions-col .model-pick{flex:0 1 auto;min-width:0;max-width:100%}#composer .actions-col .llmctx-new,#composer .actions-col .think-new{flex:0 0 auto;max-width:100%}}
|
|
627
|
+
/* Phones (the key hint is gone there): still one row, with tighter gaps; the override chips go (⚙'s dot and label say what is set). */
|
|
628
|
+
@media(max-width:600px){#composer .actions-col .model-pick{flex:0 1 auto;min-width:0;max-width:100%}body:not(.has-session) #composer .actions-col{gap:4px}body:not(.has-session) #composer .actions-col .model-pick{flex-shrink:20;min-width:min(84px,20vw)}#composer .actions-col .llmctx-new{display:none}}
|
|
628
629
|
/* The model search: opaque like #commandList (a list over the conversation
|
|
629
630
|
must stay readable), above the composer, or below it (.down) when the
|
|
630
631
|
start page leaves no room above, or over the composer (.over) when
|
|
@@ -1120,6 +1121,44 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1120
1121
|
.pp-working button{font:inherit;font-size:12px;padding:3px 10px;border-radius:6px;cursor:pointer;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
1121
1122
|
.pp-working button:disabled{opacity:.7;cursor:progress}
|
|
1122
1123
|
.pp-link{display:flex;gap:4px}
|
|
1124
|
+
/* The start page's ⚙ panel (start_settings.js): rows of segments (a label, then pill buttons with aria-pressed), the context's Apply and budget disclosure, the review row; a bottom sheet on a phone. */
|
|
1125
|
+
.start-settings{gap:10px}
|
|
1126
|
+
.start-settings .ctx-pop-head strong{font-family:inherit}
|
|
1127
|
+
.ss-row{display:flex;align-items:flex-start;gap:10px}
|
|
1128
|
+
.ss-label{flex:0 0 64px;font-size:12px;line-height:26px;color:var(--text-muted)}
|
|
1129
|
+
.ss-seg{flex:1;min-width:0;display:flex;flex-wrap:wrap;gap:4px}
|
|
1130
|
+
.ss-seg button{height:26px;min-height:26px;padding:0 9px;border:1px solid var(--border-strong);border-radius:7px;background:transparent;color:var(--text-2);font-size:12px;line-height:24px;white-space:nowrap}
|
|
1131
|
+
.ss-seg button:hover{filter:none;color:var(--text);border-color:var(--accent-border)}
|
|
1132
|
+
.ss-seg button[aria-pressed="true"]{background:var(--accent-bg);color:var(--accent);border-color:var(--accent-border)}
|
|
1133
|
+
.ss-seg button:focus-visible,.ss-link:focus-visible,.ss-back:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
1134
|
+
.ss-more{margin:-4px 0 0 74px}
|
|
1135
|
+
.ss-link{padding:2px 4px;border:0;border-radius:5px;background:transparent;color:var(--accent);font-size:12px}
|
|
1136
|
+
.ss-link:hover{filter:none;background:var(--accent-bg)}
|
|
1137
|
+
.ss-ctx-form{display:flex;flex-direction:column;gap:8px;margin:0 0 0 74px}
|
|
1138
|
+
.ss-anyway{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px}
|
|
1139
|
+
.ss-rule{width:100%;margin:0;border:0;border-top:1px solid var(--border)}
|
|
1140
|
+
.ss-review{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px}
|
|
1141
|
+
.ss-review>.ss-link:first-child{margin-left:-4px}
|
|
1142
|
+
.ss-review-set{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-size:12.5px}
|
|
1143
|
+
.ss-back{margin:0 2px 0 -6px;padding:0 6px;border:0;border-radius:5px;background:transparent;color:var(--text-muted);font-size:18px;line-height:1}
|
|
1144
|
+
.ss-back:hover{filter:none;color:var(--text);background:var(--bg-hover)}
|
|
1145
|
+
.start-settings.sheet{border-radius:14px;padding:14px 16px}
|
|
1146
|
+
@media(max-width:400px){.ss-row{flex-direction:column;gap:4px}.ss-label{flex:none;line-height:1.4}.ss-more,.ss-ctx-form{margin-left:0}}
|
|
1147
|
+
@media(pointer:coarse){.ss-seg button{height:36px;min-height:36px;line-height:34px;padding:0 12px;font-size:13px}.ss-link,.ss-back{min-height:36px}.ss-label{line-height:36px}}
|
|
1148
|
+
/* The start page's 🧰 list (start_toolkit.js): a row per kit (a check, its name, a muted detail), the checked one accent; a bottom sheet on a phone. */
|
|
1149
|
+
.start-toolkit{gap:8px}
|
|
1150
|
+
.start-toolkit .ctx-pop-head strong{font-family:inherit}
|
|
1151
|
+
.st-rows{display:flex;flex-direction:column;gap:2px;margin:0 -6px}
|
|
1152
|
+
.st-row{display:flex;align-items:baseline;gap:8px;width:100%;min-height:30px;padding:5px 8px 5px 6px;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--text-2);font-size:13px;text-align:left}
|
|
1153
|
+
.st-row:hover{filter:none;color:var(--text);background:var(--bg-hover)}
|
|
1154
|
+
.st-row[aria-pressed="true"]{color:var(--accent);background:var(--accent-bg);border-color:var(--accent-border)}
|
|
1155
|
+
.st-row:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}
|
|
1156
|
+
.st-check{flex:0 0 14px;text-align:center}
|
|
1157
|
+
.st-name{flex:none;font-weight:600}
|
|
1158
|
+
.st-detail{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:12px}
|
|
1159
|
+
.start-toolkit.sheet{border-radius:14px;padding:14px 16px}
|
|
1160
|
+
.start-toolkit.sheet .st-detail{white-space:normal}
|
|
1161
|
+
@media(pointer:coarse){.st-row{min-height:40px;align-items:center}}
|
|
1123
1162
|
.rs-form{display:flex;flex-direction:column;gap:6px;margin-top:6px}
|
|
1124
1163
|
.rs-form textarea{width:100%;resize:vertical;min-height:52px;font-family:ui-monospace,monospace;font-size:12px;color:var(--text);background:var(--bg-inset);border:1px solid var(--border-strong);border-radius:6px;padding:6px 8px}
|
|
1125
1164
|
.rs-form textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
|
|
@@ -1604,11 +1643,14 @@ a{color:var(--accent)}
|
|
|
1604
1643
|
</div>
|
|
1605
1644
|
<div class="actions-col">
|
|
1606
1645
|
<button type="button" id="modelPick" class="model-pick" title="The model the new session starts on" aria-label="Model for the new session" aria-haspopup="listbox" aria-expanded="false" hidden><span class="model-pick-name"></span></button>
|
|
1646
|
+
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1607
1647
|
<button type="button" id="thinkingNew" class="llmctx-new think-new" aria-haspopup="dialog" hidden></button>
|
|
1608
1648
|
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1609
|
-
<button type="button" id="reviewNew" class="llmctx-new review-new" aria-haspopup="dialog"
|
|
1610
|
-
<
|
|
1649
|
+
<button type="button" id="reviewNew" class="llmctx-new review-new" aria-haspopup="dialog" hidden></button>
|
|
1650
|
+
<button type="button" id="toolkitNew" class="llmctx-new toolkit-new" aria-haspopup="dialog" hidden></button>
|
|
1611
1651
|
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
1652
|
+
<button type="button" id="settingsNew" class="icon-btn settings-btn" title="Session settings" aria-label="Session settings" aria-haspopup="dialog" hidden></button>
|
|
1653
|
+
<button type="button" id="toolkitBtn" class="icon-btn toolkit-btn" title="Toolkit" aria-label="Toolkit" aria-haspopup="dialog" hidden></button>
|
|
1612
1654
|
<button type="button" id="attachBtn" class="attach-btn" title="Add images (or paste or drop them here)" aria-label="Add images" hidden></button>
|
|
1613
1655
|
<input type="file" id="attachInput" accept="image/*" multiple hidden>
|
|
1614
1656
|
<div class="split">
|
|
@@ -16,29 +16,22 @@
|
|
|
16
16
|
// element as the other chips')
|
|
17
17
|
// formClass, popoverClass, nowClass the form's, popover's and "Now:"
|
|
18
18
|
// line's own classes
|
|
19
|
-
// when(
|
|
20
|
-
// page's new session, thinking only)
|
|
21
|
-
//
|
|
22
|
-
// The start page's new session (kind NEW_SESSION) has a thinking chip only
|
|
23
|
-
// (thinking_chip.js keeps its choice): the form shows the model's level and
|
|
24
|
-
// takes a choice for the next create.
|
|
19
|
+
// when() the form's note on when it applies
|
|
25
20
|
|
|
26
21
|
import { createChipPopover } from "./chip_popover.js";
|
|
27
|
-
import { NEW_SESSION, SESSION } from "./llm_context_chip.js";
|
|
28
22
|
import { html, render } from "./vendor/lit-html.js";
|
|
29
23
|
|
|
30
24
|
export const DEFAULT = "default";
|
|
31
25
|
|
|
32
|
-
// The chip: its text, tooltip and whether the session
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
export function chipModel(cfg, summary
|
|
26
|
+
// The chip: its text, tooltip and whether the session set its own; null
|
|
27
|
+
// without a summary (an older worker, or a host the setting means nothing
|
|
28
|
+
// on).
|
|
29
|
+
export function chipModel(cfg, summary) {
|
|
36
30
|
if (!summary || !summary.level) return null;
|
|
37
31
|
const own = !!summary.own;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
return { text: `${cfg.prefix} ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
32
|
+
const from = own ? "this session" : summary.source || "the model's own";
|
|
33
|
+
const title = `${cfg.name}: ${summary.level} (${from}). Click to change it for this session.`;
|
|
34
|
+
return { text: `${cfg.prefix} ${summary.level}${own ? " · session" : ""}`, title, own };
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
// The command line for a select value ("default" unsets the session's own),
|
|
@@ -57,41 +50,33 @@ export function nowText(summary) {
|
|
|
57
50
|
|
|
58
51
|
const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
|
|
59
52
|
|
|
60
|
-
// The popover's form (a lit-html template)
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const fresh = kind === NEW_SESSION;
|
|
66
|
-
const chosen = (fresh ? choice : summary.own) || DEFAULT;
|
|
67
|
-
const follow = summary.own && !fresh ? "follow the model" : `follow the model (${summary.level})`;
|
|
53
|
+
// The popover's form (a lit-html template); +now+ is the summary its "Now:"
|
|
54
|
+
// line reads (refresh passes a newer one, the rest stays as the form opened).
|
|
55
|
+
export function formTemplate(cfg, summary, { running = false, now = summary } = {}) {
|
|
56
|
+
const chosen = summary.own || DEFAULT;
|
|
57
|
+
const follow = summary.own ? "follow the model" : `follow the model (${summary.level})`;
|
|
68
58
|
return html`<form class=${`${cfg.formClass} llmctx-form`}><div class="ctx-pop-head"><strong>${cfg.name}</strong
|
|
69
|
-
><span class="ctx-pop-kind"
|
|
59
|
+
><span class="ctx-pop-kind">this session</span
|
|
70
60
|
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
71
61
|
><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
|
|
72
62
|
><label class="llmctx-field"><span>Level</span><span class="select-wrap"><select name="level">${option(DEFAULT, follow, chosen)}${
|
|
73
63
|
cfg.levels.map((level) => option(level, level, chosen))}</select></span></label
|
|
74
|
-
><div class="ctx-pop-meta">${cfg.when(
|
|
64
|
+
><div class="ctx-pop-meta">${cfg.when()}${running ? " A turn is running: it applies after this turn." : ""}</div
|
|
75
65
|
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
76
66
|
}
|
|
77
67
|
|
|
78
68
|
// The chip's popover (chip_popover.js' shell), as createLlmContextControl's:
|
|
79
69
|
// el is where the chip is drawn (clicks are delegated: #infoText,
|
|
80
70
|
// info_bar.js); summary(): the open session's summary; session(): its id;
|
|
81
|
-
// running(): a turn runs; run(line): sends the command.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// and anchor (the chip's selector, by default its data attribute) keep it
|
|
85
|
-
// apart from the other controls on the same element (the anchor defaults
|
|
86
|
-
// to the config's).
|
|
71
|
+
// running(): a turn runs; run(line): sends the command. id and anchor (the
|
|
72
|
+
// chip's selector, by default the config's data attribute) keep it apart
|
|
73
|
+
// from the other controls on the same element.
|
|
87
74
|
export function createLevelControl({ cfg, el, summary, session = () => null, running = () => false, run,
|
|
88
|
-
kind = SESSION, choice = () => null, choose = null,
|
|
89
75
|
id, anchor = cfg.anchor,
|
|
90
76
|
doc = document, win = window }) {
|
|
91
|
-
const fresh = kind === NEW_SESSION;
|
|
92
77
|
let initial = null;
|
|
93
|
-
// What the open form was drawn from: the
|
|
94
|
-
//
|
|
78
|
+
// What the open form was drawn from: the session's summary and the form's
|
|
79
|
+
// options.
|
|
95
80
|
let shown = null;
|
|
96
81
|
const draw = (now) => render(formTemplate(cfg, shown.data, { ...shown.opts, now }), shell.pop);
|
|
97
82
|
|
|
@@ -99,8 +84,8 @@ export function createLevelControl({ cfg, el, summary, session = () => null, run
|
|
|
99
84
|
// "Now:" follows (the same template again: lit writes that line alone).
|
|
100
85
|
function show(data, opening) {
|
|
101
86
|
if (opening) {
|
|
102
|
-
initial =
|
|
103
|
-
shown = { data, opts: { running: running()
|
|
87
|
+
initial = data.own || DEFAULT;
|
|
88
|
+
shown = { data, opts: { running: running() } };
|
|
104
89
|
}
|
|
105
90
|
draw(data);
|
|
106
91
|
}
|
|
@@ -109,7 +94,6 @@ export function createLevelControl({ cfg, el, summary, session = () => null, run
|
|
|
109
94
|
const level = form.elements.level.value;
|
|
110
95
|
const was = initial;
|
|
111
96
|
shell.close();
|
|
112
|
-
if (fresh) return choose(level === DEFAULT ? null : level);
|
|
113
97
|
const line = commandLine(cfg, level, was);
|
|
114
98
|
if (line) run(line);
|
|
115
99
|
}
|