samagotchi 0.3.0 → 0.5.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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +162 -1
  3. data/README.md +29 -2
  4. data/bin/chi +60 -69
  5. data/docs/cli.md +211 -77
  6. data/docs/configuration.md +118 -21
  7. data/docs/desktop.md +39 -4
  8. data/docs/guardrails.md +11 -0
  9. data/docs/hooks.md +89 -7
  10. data/docs/memory.md +40 -0
  11. data/docs/plugins.md +50 -0
  12. data/docs/releasing.md +15 -12
  13. data/docs/sessions.md +20 -18
  14. data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
  15. data/lib/samagotchi/bridge/sse_writer.rb +0 -3
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
  17. data/lib/samagotchi/bridge.rb +20 -12
  18. data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
  19. data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
  20. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +178 -5
  21. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  22. data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
  23. data/lib/samagotchi/bundles/system/config_modification_protocol.md +9 -10
  24. data/lib/samagotchi/bundles/system/delegated.md +6 -7
  25. data/lib/samagotchi/bundles/system/identity.md +5 -0
  26. data/lib/samagotchi/bundles/system/manifest.yml +6 -6
  27. data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
  28. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  29. data/lib/samagotchi/client.rb +25 -26
  30. data/lib/samagotchi/commands/registry.rb +8 -0
  31. data/lib/samagotchi/config.rb +97 -113
  32. data/lib/samagotchi/desktop/macos/App.swift +12 -8
  33. data/lib/samagotchi/desktop/macos/ChiRunner.swift +4 -2
  34. data/lib/samagotchi/desktop/macos/Images.swift +113 -0
  35. data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
  36. data/lib/samagotchi/desktop/macos/Panel.swift +112 -9
  37. data/lib/samagotchi/desktop/macos.rb +59 -8
  38. data/lib/samagotchi/desktop_command.rb +6 -3
  39. data/lib/samagotchi/edit_preview.rb +82 -0
  40. data/lib/samagotchi/engine.rb +236 -443
  41. data/lib/samagotchi/gem_update.rb +89 -0
  42. data/lib/samagotchi/guardrails/approval.rb +26 -4
  43. data/lib/samagotchi/guardrails/load_failures.rb +9 -3
  44. data/lib/samagotchi/host_registry.rb +8 -12
  45. data/lib/samagotchi/idle_client.rb +24 -15
  46. data/lib/samagotchi/idle_reminders.rb +2 -2
  47. data/lib/samagotchi/image_store.rb +10 -6
  48. data/lib/samagotchi/kernel_loop.rb +59 -123
  49. data/lib/samagotchi/live_versions.rb +65 -0
  50. data/lib/samagotchi/llm/api_key.rb +41 -0
  51. data/lib/samagotchi/llm/chat_loop.rb +77 -13
  52. data/lib/samagotchi/llm/errors.rb +38 -7
  53. data/lib/samagotchi/llm/http.rb +19 -22
  54. data/lib/samagotchi/llm/openai_chat.rb +22 -26
  55. data/lib/samagotchi/memory_bundle/installer.rb +65 -63
  56. data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
  57. data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
  58. data/lib/samagotchi/memory_bundle/status.rb +4 -1
  59. data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
  60. data/lib/samagotchi/model_profile.rb +27 -10
  61. data/lib/samagotchi/note_command.rb +2 -1
  62. data/lib/samagotchi/prompt.rb +4 -2
  63. data/lib/samagotchi/reminder_store.rb +1 -9
  64. data/lib/samagotchi/reply_wait.rb +48 -4
  65. data/lib/samagotchi/self_report.rb +37 -5
  66. data/lib/samagotchi/send_command.rb +190 -17
  67. data/lib/samagotchi/session.rb +4 -2
  68. data/lib/samagotchi/session_commands.rb +38 -8
  69. data/lib/samagotchi/session_manager.rb +19 -53
  70. data/lib/samagotchi/system_prompt.rb +403 -0
  71. data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
  72. data/lib/samagotchi/terminal_ui/attached_loop.rb +141 -108
  73. data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
  74. data/lib/samagotchi/terminal_ui/event_renderer.rb +29 -8
  75. data/lib/samagotchi/terminal_ui/formatting.rb +41 -22
  76. data/lib/samagotchi/terminal_ui/input_support.rb +7 -23
  77. data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
  78. data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
  79. data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
  80. data/lib/samagotchi/terminal_ui/surface.rb +1 -1
  81. data/lib/samagotchi/terminal_ui.rb +142 -923
  82. data/lib/samagotchi/text_diff.rb +181 -0
  83. data/lib/samagotchi/thinking.rb +126 -0
  84. data/lib/samagotchi/tool_activity.rb +52 -2
  85. data/lib/samagotchi/tool_runner.rb +37 -1
  86. data/lib/samagotchi/tools/ask_user_question.rb +41 -33
  87. data/lib/samagotchi/tools/edit.rb +23 -9
  88. data/lib/samagotchi/tools/execute.rb +3 -3
  89. data/lib/samagotchi/tools/output_guardrails.rb +8 -7
  90. data/lib/samagotchi/tools/read.rb +4 -4
  91. data/lib/samagotchi/tools/write.rb +4 -0
  92. data/lib/samagotchi/turn_flow.rb +12 -2
  93. data/lib/samagotchi/update_command.rb +309 -0
  94. data/lib/samagotchi/update_hint.rb +59 -0
  95. data/lib/samagotchi/version.rb +1 -1
  96. data/lib/samagotchi/vision_support.rb +6 -4
  97. data/lib/samagotchi/web/app.rb +173 -38
  98. data/lib/samagotchi/web/lan.rb +99 -0
  99. data/lib/samagotchi/web/message_parts.rb +19 -10
  100. data/lib/samagotchi/web/public/activity.js +10 -0
  101. data/lib/samagotchi/web/public/app.js +135 -78
  102. data/lib/samagotchi/web/public/chat_view.js +8 -1
  103. data/lib/samagotchi/web/public/data.js +2 -0
  104. data/lib/samagotchi/web/public/diff_view.js +58 -0
  105. data/lib/samagotchi/web/public/index.html +185 -18
  106. data/lib/samagotchi/web/public/model_pick.js +136 -0
  107. data/lib/samagotchi/web/public/model_picker.js +224 -0
  108. data/lib/samagotchi/web/public/notify.js +10 -0
  109. data/lib/samagotchi/web/public/question_card.js +3 -1
  110. data/lib/samagotchi/web/public/stage_model.js +110 -0
  111. data/lib/samagotchi/web/public/stage_view.js +580 -0
  112. data/lib/samagotchi/web/public/timing.js +6 -2
  113. data/lib/samagotchi/web/public/turn_events.js +38 -10
  114. data/lib/samagotchi/web/public/turn_model.js +11 -3
  115. data/lib/samagotchi/web/public/turn_view.js +76 -20
  116. data/lib/samagotchi/web/qr.rb +40 -0
  117. data/lib/samagotchi/web/server.rb +101 -11
  118. data/lib/samagotchi/web/token.rb +97 -0
  119. data/lib/samagotchi/worker.rb +5 -4
  120. metadata +38 -3
  121. data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
@@ -50,6 +50,15 @@ module Samagotchi
50
50
  " #{paint(refs.map { |ref| "→ image #{ref[:width]}×#{ref[:height]}" }.join(", "), 90)}"
51
51
  end
52
52
 
53
+ # An edit/write row's change, after the row: " +3 −1" (green, red).
54
+ # +diff+ is EditPreview's hash, symbol or string keys (SSE, snapshot).
55
+ def format_tool_diff_suffix(diff)
56
+ return "" unless diff.is_a?(Hash)
57
+
58
+ get = ->(key) { diff[key] || diff[key.to_s] }
59
+ " #{paint("+#{get.(:added).to_i}", 32)} #{paint("\u2212#{get.(:removed).to_i}", 31)}"
60
+ end
61
+
53
62
  # A card (Engine#show_card) as a framed block: the title and its
54
63
  # source, the body as wrapped plain text (no terminal markdown), and
55
64
  # one `→ <command>` line per action, the label after it when it says
@@ -126,6 +135,33 @@ module Samagotchi
126
135
  text.strip.empty? ? [] : lines
127
136
  end
128
137
 
138
+ # ── Turn ends: one set of words for the REPL, attached mode and the
139
+ # web (timing.js cancelLineText, format.js failedTurnText) ──────────
140
+
141
+ # A cancel's reason as the UIs name it (anything else as it is).
142
+ CANCEL_REASONS = { "ctrl_c" => "Ctrl-C", "user" => "stopped", "hook" => "by a hook" }.freeze
143
+ # Under a canceled prompt turn (a canceled continue is back where it
144
+ # started): its partial progress stays in the conversation.
145
+ ROLLBACK_HINT = "partial progress kept; !rollback restores the pre-turn state"
146
+
147
+ # "✕ turn canceled (Ctrl-C) · 3.1s"
148
+ def turn_canceled_line(reason, duration_ms)
149
+ label = reason.to_s.empty? ? nil : CANCEL_REASONS.fetch(reason.to_s, reason.to_s)
150
+ "#{paint("✕", 33)} turn canceled#{" (#{label})" if label}#{turn_duration_suffix(duration_ms)}"
151
+ end
152
+
153
+ # "✕ turn failed: <summary> · 2.0s"
154
+ def turn_failed_line(detail, duration_ms)
155
+ "#{paint("✕", 31)} turn failed: #{detail}#{turn_duration_suffix(duration_ms)}"
156
+ end
157
+
158
+ # A dim, indented line under a turn's end: what became of its prompt.
159
+ def turn_end_hint(text) = " #{paint(text, 90)}"
160
+
161
+ def turn_duration_suffix(duration_ms)
162
+ duration_ms.nil? ? "" : " · #{format_elapsed_duration(duration_ms)}"
163
+ end
164
+
129
165
  private
130
166
 
131
167
  # The recap shown on return: dim, one "recap>" block, noting how many
@@ -165,9 +201,11 @@ module Samagotchi
165
201
 
166
202
  # ── The status line (the REPL's, and attached mode's idle one) ────────
167
203
 
168
- # SAMAGOTCHI_STATUS_LINE=off hides it.
204
+ # status.line: off hides it.
205
+ STATUS_LINE_OFF = "off"
206
+
169
207
  def status_line_enabled?
170
- value = ENV.fetch(STATUS_LINE_ENV, STATUS_LINE_ON).to_s.strip.downcase
208
+ value = Samagotchi::Config.get("status.line").to_s.strip.downcase
171
209
  !(value.empty? || value == STATUS_LINE_OFF || value == "0" || value == "false")
172
210
  end
173
211
 
@@ -195,10 +233,8 @@ module Samagotchi
195
233
  "model=#{model} (default: #{default_model})"
196
234
  end
197
235
 
198
- # @param server [Hash, nil] the server's own numbers ({ctx_pct:, prompt_tokens:, ...})
199
236
  # @param estimate [Hash, nil] the kernel's estimate ({est_pct:, bucket:})
200
- def status_context_text(server: nil, estimate: nil)
201
- return format_server_context_segment(server) if server.is_a?(Hash)
237
+ def status_context_text(estimate: nil)
202
238
  return "" unless estimate.is_a?(Hash)
203
239
 
204
240
  pct = format("%.1f", estimate[:est_pct].to_f)
@@ -208,23 +244,6 @@ module Samagotchi
208
244
  "ctx=#{pct}% (#{bucket})"
209
245
  end
210
246
 
211
- def format_server_context_segment(status)
212
- pct = status[:ctx_pct]
213
- base = pct ? "ctx=#{format('%.1f', pct)}%" : "ctx=srv"
214
-
215
- tokens = []
216
- prompt_tokens = status[:prompt_tokens]
217
- completion_tokens = status[:completion_tokens]
218
- total_tokens = status[:total_tokens]
219
- tokens << "p=#{prompt_tokens}" if prompt_tokens
220
- tokens << "c=#{completion_tokens}" if completion_tokens
221
- tokens << "t=#{total_tokens}" if total_tokens
222
-
223
- return base if tokens.empty?
224
-
225
- "#{base} (#{tokens.join(' ')})"
226
- end
227
-
228
247
  # @param label [String] "mem" for the used memories, "muted" for the
229
248
  # session's --mute list
230
249
  def status_memory_text(names, limit, label: "mem")
@@ -4,6 +4,7 @@ require "json"
4
4
  require "fileutils"
5
5
  require "reline"
6
6
 
7
+ require_relative "../config"
7
8
  require_relative "../memory_paths"
8
9
  require_relative "../tools/memory"
9
10
  require_relative "../session_commands"
@@ -11,20 +12,18 @@ require_relative "../session_commands"
11
12
  module Samagotchi
12
13
  class TerminalUI
13
14
  # Input the REPL and the attached TUI share: the persistent prompt
14
- # history, Tab completion (/commands, @path in the cwd, #memory), the
15
- # #memory shorthand rewrite, multiline reads at the main prompt, and a
16
- # one-shot prefill of the next read (the default input, a prompt given
17
- # back after a failed turn).
15
+ # history, Tab completion (/commands, @path in the cwd, #memory, which
16
+ # inserts #name and the model reads it as typed), multiline reads at the
17
+ # main prompt, and a one-shot prefill of the next read (the default
18
+ # input, a prompt given back after a failed turn).
18
19
  #
19
20
  # Included for private use; it keeps state in @next_input_prefill and
20
21
  # reads @no_default_input.
21
22
  module InputSupport
22
- PROMPT_HISTORY_ENV = "SAMAGOTCHI_HISTORY_FILE"
23
23
  XDG_STATE_HOME_ENV = "XDG_STATE_HOME"
24
24
  PROMPT_HISTORY_FILE = "history.json"
25
25
  PROMPT_HISTORY_STATE_DIR = "samagotchi"
26
26
  PROMPT_HISTORY_LIMIT = 20
27
- DEFAULT_INPUT_ENV = "SAMAGOTCHI_DEFAULT_INPUT"
28
27
  AT_PATH_COMPLETION_PREFIX = "@"
29
28
  MEMORY_COMPLETION_PREFIX = "#"
30
29
  AT_PATH_COMPLETION_MAX_CANDIDATES = 200
@@ -204,23 +203,8 @@ module Samagotchi
204
203
  }
205
204
  end
206
205
 
207
- def normalize_model_input(input)
208
- input.to_s.gsub(/(^|[^\w\/])#((?:project|system)\/)?([a-zA-Z0-9][a-zA-Z0-9_-]*)/) do
209
- prefix = Regexp.last_match(1)
210
- scoped = Regexp.last_match(2).to_s
211
- name = Regexp.last_match(3)
212
- scope = scoped.delete_suffix("/")
213
- normalized = if scope.empty?
214
- "memory \"#{name}\""
215
- else
216
- "memory \"#{name}\" in #{scope} scope"
217
- end
218
- "#{prefix}#{normalized}"
219
- end
220
- end
221
-
222
206
  def history_file_path
223
- explicit = ENV[PROMPT_HISTORY_ENV].to_s.strip
207
+ explicit = Samagotchi::Config.get("history.file").to_s.strip
224
208
  return explicit unless explicit.empty?
225
209
 
226
210
  File.join(xdg_state_home, PROMPT_HISTORY_STATE_DIR, PROMPT_HISTORY_FILE)
@@ -288,7 +272,7 @@ module Samagotchi
288
272
  def default_input_text
289
273
  return nil unless default_input_wanted?
290
274
 
291
- default = ENV.fetch(DEFAULT_INPUT_ENV, nil)
275
+ default = Samagotchi::Config.get("default.input")
292
276
  return nil if default.nil? || default.strip.empty?
293
277
 
294
278
  default
@@ -6,17 +6,19 @@ require_relative "surface"
6
6
 
7
7
  module Samagotchi
8
8
  class TerminalUI
9
- # Attached mode's Surface when a live region can't be drawn (output or
9
+ # Both TUIs' Surface when a live region can't be drawn (output or
10
10
  # input not a terminal, TERM=dumb, or a Reline the seam doesn't support):
11
11
  # append-only output. Committed text and the notes, status and hints rows
12
12
  # print as lines. The activity slot is dropped, because redrawing it in
13
13
  # place needs cursor movement (the turn's lines still print as they end).
14
- # The prompt is Reline's (or a plain read's) own, so output may cross it.
14
+ # The prompt is Reline's own, so output may cross it; the editor slot
15
+ # prints the prompt of a plain read (the REPL's ? off a terminal).
15
16
  class PlainSurface
16
17
  include Surface
17
18
 
18
- # @param out [IO]
19
- def initialize(out:)
19
+ # @param out [IO, nil] nil writes to $stdout as it is at each write
20
+ # (specs swap it after the UI is built)
21
+ def initialize(out: nil)
20
22
  @out = out
21
23
  @lock = Monitor.new
22
24
  end
@@ -27,15 +29,15 @@ module Samagotchi
27
29
  def columns = IO.console&.winsize&.last || 80
28
30
 
29
31
  def commit(text)
30
- synchronize { @out.puts(text) }
32
+ synchronize { out.puts(text) }
31
33
  end
32
34
 
33
35
  def set_slot(name, rows)
34
36
  check_slot!(name)
35
- raise ArgumentError, "Reline draws the editor slot" if name == :editor
36
37
  return if name == :activity
38
+ return synchronize { out.print(Array(rows).join("\n")) && out.flush } if name == :editor
37
39
 
38
- synchronize { lay_out(rows, width: columns).each { |row| @out.puts(row) } }
40
+ synchronize { lay_out(rows, width: columns).each { |row| out.puts(row) } }
39
41
  end
40
42
 
41
43
  # Nothing printed can be taken back.
@@ -46,6 +48,10 @@ module Samagotchi
46
48
  end
47
49
 
48
50
  def close; end
51
+
52
+ private
53
+
54
+ def out = @out || $stdout
49
55
  end
50
56
  end
51
57
  end
@@ -14,6 +14,8 @@ module Samagotchi
14
14
  # an optional "; reason"; a substring would pick the wrong one ("y" is
15
15
  # in "Deny").
16
16
  class QuestionPrompt
17
+ DIFF_CODES = { "+" => 32, "-" => 31, "@" => 2, "\\" => 2 }.freeze
18
+
17
19
  # A parsed answer. +error+ set: re-ask. +note+: print, then accept.
18
20
  Answer = Struct.new(:selected, :freeform, :error, :note, keyword_init: true) do
19
21
  def ok? = error.nil?
@@ -24,6 +26,8 @@ module Samagotchi
24
26
  # @param pending [Hash] Engine#pending_question (symbol or string keys)
25
27
  def initialize(pending)
26
28
  field = ->(key) { pending[key] || pending[key.to_s] }
29
+ approval = field.(:approval)
30
+ @preview = approval.is_a?(Hash) ? (approval[:preview] || approval["preview"]) : nil
27
31
  @id = field.(:id).to_s
28
32
  @question = field.(:question).to_s
29
33
  @options = Array(field.(:options)).map { |v| v.to_s.strip }.reject(&:empty?)
@@ -49,6 +53,34 @@ module Samagotchi
49
53
  hint: slot_hint, question_code: approval? ? 33 : 94, paint: paint)
50
54
  end
51
55
 
56
+ # The most diff lines #preview_lines prints; the web shows them all.
57
+ PREVIEW_LINES = 40
58
+
59
+ # An edit/write approval's dry-run diff (approval.preview, symbol or
60
+ # string keys: nested keys are strings after a reload or SSE), as
61
+ # lines to print above the slot: coloured +/-, dim @@, cut at
62
+ # PREVIEW_LINES. [] for none.
63
+ def preview_lines(paint: ->(text, _code) { text })
64
+ preview = @preview
65
+ return [] unless preview.is_a?(Hash)
66
+
67
+ get = ->(key) { preview[key] || preview[key.to_s] }
68
+ return [paint.("this edit would fail: #{get.(:error)}", 33)] if get.(:error)
69
+ return [paint.("diff not shown: #{get.(:skipped)}", 2)] if get.(:skipped)
70
+
71
+ lines = get.(:text).to_s.split("\n")
72
+ return [] if lines.empty?
73
+
74
+ # TextDiff's own cut note ("… N more lines") counts toward the rest.
75
+ cut = lines.last.to_s[/\A… (\d+) more lines\z/, 1]
76
+ lines.pop if cut
77
+ hidden = [lines.size - PREVIEW_LINES, 0].max + cut.to_i
78
+ shown = lines.first(PREVIEW_LINES).map { |line| paint.(line, diff_code(line)) }
79
+ shown.unshift(paint.("new file", 2)) if get.(:new_file)
80
+ shown << paint.("… #{hidden} more lines (full diff on the web)", 2) if hidden.positive?
81
+ shown
82
+ end
83
+
52
84
  # The one line that stays in the scrollback once the question closes:
53
85
  # the question and what became of it.
54
86
  # @param outcome [String] the answer (#answer_text) or what closed it
@@ -148,6 +180,9 @@ module Samagotchi
148
180
  found = options.find { |o| o.downcase == tok.downcase || o.downcase.include?(tok.downcase) }
149
181
  found ? [found, nil] : [nil, "Unknown option '#{tok}'. Use numbers 1-#{options.size} or exact labels."]
150
182
  end
183
+
184
+ # "+" green, "-" red, "@@" and "\ No newline" dim, context plain.
185
+ def diff_code(line) = DIFF_CODES.fetch(line[0], 0)
151
186
  end
152
187
  end
153
188
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formatting"
4
+
5
+ module Samagotchi
6
+ class TerminalUI
7
+ # The status row under the prompt, the same for both TUIs:
8
+ # `status> model=… | ↳ parent | ctx=12.3% (under20) | mem: … | muted: …`.
9
+ # A UI feeds it what it learns (attached mode from the Bridge's events,
10
+ # the REPL from its Engine's) through #update, and the row is redrawn in
11
+ # the surface's status slot only when its text changes.
12
+ class StatusRow
13
+ include Formatting
14
+
15
+ # Memory names shown before ", +N".
16
+ MEMORY_LIMIT = 8
17
+ # What the row shows:
18
+ # model, default_model the model in use, and the config's default
19
+ # (named beside another model)
20
+ # served [served, asked]: what the server said it
21
+ # served for the name asked (ServedModel)
22
+ # parent_id the session that delegated this one
23
+ # context the kernel's estimate {est_pct:, bucket:}
24
+ # used_memories the memories the session read
25
+ # preloaded its --memory list (shown until a turn
26
+ # records them as used)
27
+ # muted its --mute list
28
+ FIELDS = %i[model default_model served parent_id context used_memories preloaded muted].freeze
29
+
30
+ # @param surface [Surface] with #columns
31
+ def initialize(surface)
32
+ @surface = surface
33
+ @values = { used_memories: [], preloaded: [], muted: [] }
34
+ @shown = nil
35
+ end
36
+
37
+ # The surface it draws on (the REPL swaps in a live region): the row
38
+ # is drawn there at the next change.
39
+ def surface=(surface)
40
+ @surface = surface
41
+ @shown = nil
42
+ end
43
+
44
+ def [](field) = @values[field]
45
+
46
+ # Change some fields; the row is redrawn when its text changed.
47
+ def update(**fields)
48
+ unknown = fields.keys - FIELDS
49
+ raise ArgumentError, "unknown status field(s): #{unknown.join(", ")}" if unknown.any?
50
+
51
+ @values.merge!(fields)
52
+ refresh
53
+ end
54
+
55
+ # Draw the row unless it shows that already (status.line: off draws
56
+ # nothing).
57
+ def refresh
58
+ return unless status_line_enabled?
59
+
60
+ rows = rows(@surface.columns - 1)
61
+ return if rows == @shown
62
+
63
+ @shown = rows
64
+ rows.empty? ? @surface.clear_slot(:status) : @surface.set_slot(:status, rows)
65
+ end
66
+
67
+ # @return [Array<String>] the row cut to +width+ (none with nothing to say)
68
+ def rows(width)
69
+ served, served_for = @values[:served]
70
+ model = @values[:model]
71
+ parent = @values[:parent_id]
72
+ segments = [model ? status_model_text(model, @values[:default_model], served: served, served_for: served_for) : "",
73
+ parent ? "↳ #{parent.to_s[0, 8]}" : "",
74
+ status_context_text(estimate: @values[:context]),
75
+ status_memory_text(Array(@values[:used_memories]) | Array(@values[:preloaded]), MEMORY_LIMIT),
76
+ status_memory_text(@values[:muted], MEMORY_LIMIT, label: "muted")].reject(&:empty?)
77
+ status_rows(segments, width)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  #
16
16
  # The slots, top down around the prompt: +activity+ (spinner, thinking
17
17
  # preview), +editor+ (the prompt), +status+, +notes+ (question choices),
18
- # +hints+. An implementation without a live region (LegacySurface) may
18
+ # +hints+. An implementation without a live region (PlainSurface) may
19
19
  # print a slot's rows as plain output instead, so the rows stay on screen.
20
20
  #
21
21
  # Instead of rows, a slot may hold content that lays itself out: anything