samagotchi 0.4.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -1
  3. data/README.md +13 -2
  4. data/bin/chi +29 -39
  5. data/docs/cli.md +135 -73
  6. data/docs/configuration.md +15 -20
  7. data/docs/hooks.md +1 -1
  8. data/docs/memory.md +40 -0
  9. data/docs/plugins.md +50 -0
  10. data/docs/releasing.md +9 -6
  11. data/docs/sessions.md +3 -3
  12. data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
  13. data/lib/samagotchi/bridge/sse_writer.rb +0 -3
  14. data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
  15. data/lib/samagotchi/bridge.rb +16 -11
  16. data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
  17. data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
  18. data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -8
  19. data/lib/samagotchi/bundles/system/identity.md +5 -0
  20. data/lib/samagotchi/bundles/system/manifest.yml +5 -5
  21. data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
  22. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  23. data/lib/samagotchi/client.rb +16 -20
  24. data/lib/samagotchi/config.rb +40 -100
  25. data/lib/samagotchi/engine.rb +50 -354
  26. data/lib/samagotchi/kernel_loop.rb +33 -44
  27. data/lib/samagotchi/live_versions.rb +7 -1
  28. data/lib/samagotchi/llm/errors.rb +17 -0
  29. data/lib/samagotchi/llm/http.rb +4 -18
  30. data/lib/samagotchi/llm/openai_chat.rb +17 -0
  31. data/lib/samagotchi/model_profile.rb +4 -10
  32. data/lib/samagotchi/note_command.rb +2 -1
  33. data/lib/samagotchi/reply_wait.rb +48 -4
  34. data/lib/samagotchi/self_report.rb +20 -2
  35. data/lib/samagotchi/send_command.rb +84 -6
  36. data/lib/samagotchi/session.rb +4 -2
  37. data/lib/samagotchi/session_manager.rb +18 -37
  38. data/lib/samagotchi/system_prompt.rb +403 -0
  39. data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
  40. data/lib/samagotchi/terminal_ui/attached_loop.rb +120 -83
  41. data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
  42. data/lib/samagotchi/terminal_ui/event_renderer.rb +23 -7
  43. data/lib/samagotchi/terminal_ui/formatting.rb +32 -22
  44. data/lib/samagotchi/terminal_ui/input_support.rb +3 -4
  45. data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
  46. data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
  47. data/lib/samagotchi/terminal_ui/surface.rb +1 -1
  48. data/lib/samagotchi/terminal_ui.rb +95 -690
  49. data/lib/samagotchi/thinking.rb +11 -0
  50. data/lib/samagotchi/tool_activity.rb +52 -2
  51. data/lib/samagotchi/tool_runner.rb +3 -0
  52. data/lib/samagotchi/tools/execute.rb +3 -3
  53. data/lib/samagotchi/tools/output_guardrails.rb +8 -7
  54. data/lib/samagotchi/tools/read.rb +4 -4
  55. data/lib/samagotchi/update_command.rb +2 -1
  56. data/lib/samagotchi/version.rb +1 -1
  57. data/lib/samagotchi/web/app.rb +170 -35
  58. data/lib/samagotchi/web/lan.rb +99 -0
  59. data/lib/samagotchi/web/message_parts.rb +15 -11
  60. data/lib/samagotchi/web/public/activity.js +7 -0
  61. data/lib/samagotchi/web/public/app.js +99 -54
  62. data/lib/samagotchi/web/public/chat_view.js +5 -1
  63. data/lib/samagotchi/web/public/index.html +163 -17
  64. data/lib/samagotchi/web/public/model_pick.js +136 -0
  65. data/lib/samagotchi/web/public/model_picker.js +224 -0
  66. data/lib/samagotchi/web/public/notify.js +10 -0
  67. data/lib/samagotchi/web/public/stage_model.js +110 -0
  68. data/lib/samagotchi/web/public/stage_view.js +580 -0
  69. data/lib/samagotchi/web/public/timing.js +6 -2
  70. data/lib/samagotchi/web/public/turn_events.js +9 -5
  71. data/lib/samagotchi/web/public/turn_model.js +11 -3
  72. data/lib/samagotchi/web/public/turn_view.js +74 -19
  73. data/lib/samagotchi/web/qr.rb +40 -0
  74. data/lib/samagotchi/web/server.rb +101 -11
  75. data/lib/samagotchi/web/token.rb +97 -0
  76. metadata +27 -3
  77. data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63f5d3415cd7f3cfe7f2811ab19bcabca4eafe62e349fd9d0d28fcd157c38340
4
- data.tar.gz: a2fea7402c8a1cba19e795320c134e12a5cf58b026934cc954c4ecd80e6d765c
3
+ metadata.gz: 94cd11abcfad07ad2843b4e0fc2eea368d77ab6080403d308db906270a1832c6
4
+ data.tar.gz: 21ac6e7093ee196d98e2ba50979cdbb28f1a3a10ab0e9cac9bad01c88a078daf
5
5
  SHA512:
6
- metadata.gz: b8c4eb982683f47f10b9f77af3bc0b32518b8707860fcb796377870bb838ba13d326ece7b2e28f675097507478ec3723206ecd654fe8720e8653bf9a234f6930
7
- data.tar.gz: fd059521495cca9d033924c818c8b35da945e6354b7806acdf65af7f275e51f9b20f5b228ff00dd81c2f16d73abd8de881d505b7eb3620e0a70792e28f4d7965
6
+ metadata.gz: fa39259864928d70d8cf830d0c7cc15c6865cea75d383484979897800a9f79d6f9fa4d79df3700285d9871e8d0cf5420e9af7c0143ab08e71182befa287b52af
7
+ data.tar.gz: 24f0fe6863767b628c1eec6957bf6cf33d8966b45f8b2cbdfd7ab268d6c323835dba8ff09fbdd9359775263693228a7a11f0d125cf0a38d7f044d6932ca7c4b9
data/CHANGELOG.md CHANGED
@@ -8,6 +8,84 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.5.0] - 2026-09-30
12
+
13
+ ### Security
14
+
15
+ - `chi web` and the worker Bridge refuse requests from other websites. Before,
16
+ a page open in your browser could start a session that runs commands, or
17
+ read your sessions and their output. Please update if you run `chi web`.
18
+
19
+ ### Added
20
+
21
+ - Skills: a memory named `skill_<name>` holds the steps of a task you did with
22
+ chi. Say "let's memorize this" and chi saves it; next time it reads and
23
+ follows it, and fixes a step that turned out different in the same turn
24
+ (docs/memory.md, Skills). The system bundle's identity and memory guide
25
+ teach it.
26
+ - The `skills` bundle (`chi bundle install skills`): `/skill save [name]
27
+ [--system]`, `/skill list`, `/skill show`, `/skill diff`; older versions of
28
+ each skill kept (`history_keep`) with a one-line diff after every update;
29
+ a nudge when a followed skill's step fails and the model goes on without
30
+ fixing it (docs/plugins.md, The skills bundle).
31
+ - `chi web --web-host lan` (`web.host: lan`, or one of this machine's IPv4
32
+ addresses): the web UI on your phone. chi web also listens on the LAN
33
+ address and prints a link with an access token and its QR code; every
34
+ request from another machine needs the token, which the page keeps in a
35
+ cookie. `chi web --new-token` replaces it; `chi self` says when chi web runs
36
+ on the LAN. Plain http: for a home network only. New runtime dependency:
37
+ `rqrcode_core`.
38
+ - A third web view, `web.view: stage` (`--web-view stage`): the running turn
39
+ sits above the composer, its tool calls in an expandable cloud, and hands
40
+ off smoothly into the history when it ends. `web.view: turn | stage | chat`
41
+ replaces `web.turn_view` (turn stays the default).
42
+ - The start page's model picker is searchable: models grouped by host, your
43
+ last 5 picks first, every typed word matched ("deepseek4.1 fla" finds
44
+ `openrouter · deepseek/deepseek-v4.1-flash`), arrow keys, Enter and Esc.
45
+
46
+ ### Changed
47
+
48
+ - config.yml edits apply without restarting `chi web`: new sessions read the
49
+ file, and running ones pick up settings they read each time (retries,
50
+ limits, log level). Old flat `UPPER_CASE` keys and `backend:` are no longer
51
+ read; they warn as unknown keys.
52
+ - The terminal REPL (`chi --no-shared`, `chi scratch`) shows turns the way
53
+ the attached terminal does, and all three UIs end a turn with the same
54
+ words ("✕ turn canceled (Ctrl-C) · 3.1s", "✕ turn failed: …"); a provider
55
+ retry shows as a dim line. The REPL-only settings `thinking.ui`,
56
+ `thinking.render_interval` and `status.width_mode` / `max_width` /
57
+ `fixed_width` are gone.
58
+ - `session.keep_status` defaults to none: a session left "running" by a
59
+ crashed worker is cleaned up like any other; a session with a live worker
60
+ is never removed.
61
+ - `chi send --image` to a model that can't take images is refused before
62
+ anything is sent, with the reason, instead of failing in the session.
63
+ - A low/medium/high thinking level on a llama.cpp chat host whose template
64
+ ignores it says so once.
65
+ - The web answers `/stats`, `/recap` and `/detach` itself (they're terminal
66
+ commands) instead of showing a worker error.
67
+ - Piped input to an attached chi (`echo "do X" | chi`, `chi -p X` with no
68
+ terminal) waits for its turns to end and exits 1 if one failed, instead of
69
+ detaching at once.
70
+
71
+ ### Fixed
72
+
73
+ - OpenRouter's "overloaded" error sent inside an HTTP 200 is retried like a
74
+ 503 instead of failing the turn.
75
+ - The context line the model sees during a long tool loop counts the tool
76
+ output added since the last request, so the model knows when to wrap up.
77
+ - `chi send --wait` no longer hangs when the turn fails very fast (for
78
+ example right after an earlier failure); it says the turn failed and why.
79
+ - On Linux the system prompt now gets the "prefer rg" hint when rg is
80
+ installed (the check only worked on macOS).
81
+ - `chi sessions list --live` shows the test sessions of a test run.
82
+ - `chi -p` with no terminal (stdin from a pipe or /dev/null) sends its prompt
83
+ once: a failed turn re-sent it in a loop (hundreds of requests a second),
84
+ and an attached `chi -p` didn't send it at all.
85
+
86
+ Update with `chi update`. Bundle added: skills 0.1.0 (`chi bundle install
87
+ skills`).
88
+
11
89
  ## [0.4.0] - 2026-09-30
12
90
 
13
91
  ### Added
@@ -234,7 +312,8 @@ and long-lived sessions.
234
312
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
235
313
  hotkey panel that send selected text or the clipboard to your sessions.
236
314
 
237
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.4.0...HEAD
315
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.5.0...HEAD
316
+ [0.5.0]: https://github.com/dm1try/samagotchi/compare/v0.4.0...v0.5.0
238
317
  [0.4.0]: https://github.com/dm1try/samagotchi/compare/v0.3.0...v0.4.0
239
318
  [0.3.0]: https://github.com/dm1try/samagotchi/compare/v0.2.0...v0.3.0
240
319
  [0.2.0]: https://github.com/dm1try/samagotchi/releases/tag/v0.2.0
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # samagotchi
2
2
 
3
- An agent harness that relies heavily on memory. Samagotchi is the engine; chi
4
- (pronounced "chee") is its short name and CLI command.
3
+ chi is a local-first, human-in-the-loop agent harness. It remembers what you
4
+ teach it, as memories and skills, and corrects them when a step turns out
5
+ different. You stay in the loop from the terminal or the browser.
6
+
7
+ Samagotchi is the engine; chi (pronounced "chee") is its short name and CLI
8
+ command.
5
9
 
6
10
  > **Pre-1.0:** config and commands may change between minor versions (0.2 →
7
11
  > 0.3); the [CHANGELOG](CHANGELOG.md) says what changed. chi is used daily and
@@ -103,6 +107,7 @@ chi --no-shared # the plain in-process REPL
103
107
  chi -p "explain lib/" --non-interactive # one turn, print the answer, exit
104
108
  chi --resume <session-id> # continue a saved session
105
109
  chi web --open # web UI: this project's sessions (--scope=all: every one)
110
+ chi web --web-host lan # the web UI on your phone too: scan the QR code it prints
106
111
  chi sessions list # this project's saved sessions (--scope=all: every one)
107
112
  pbpaste | chi note --source slack <id> # background context for a session (no turn)
108
113
  pbpaste | chi send -m "same bug?" <id> # a message to a session, the clipboard quoted above it
@@ -113,6 +118,12 @@ chi send --new --wait -m "review feat/x" # a new session you can watch in the w
113
118
 
114
119
  `/model` switches models, Ctrl-C cancels a turn, and Ctrl-D or `/detach` detaches (the session keeps running; `chi --attach ID` comes back). `/exit` detaches and stops the session's worker too, unless something still needs it (a running turn, another UI); `chi --resume ID` picks the conversation up again. `/exit --delete` also deletes the session once the worker has gone; `chi sessions delete ID` deletes one from the shell. `/archive` (or `chi sessions archive ID`) hides a session from every list and keeps it for good; `chi sessions list --archived` finds it again.
115
120
 
121
+ `chi web --web-host lan` opens the web UI to your home network with an
122
+ access token: anyone with the printed link (or its QR code) can run commands
123
+ as you, and it is plain http, readable by anyone on the same Wi-Fi. Use it at
124
+ home, never on a shared network, and `chi web --new-token` if a link leaks.
125
+ See [chi web on your phone](docs/cli.md#chi-web-on-your-phone).
126
+
116
127
  ### Context notes
117
128
 
118
129
  `chi note` pushes text into one or more sessions as background, not as a
data/bin/chi CHANGED
@@ -14,7 +14,7 @@ end
14
14
  require "optparse"
15
15
  require "samagotchi/config"
16
16
 
17
- Samagotchi::ConfigFile.load_global_env!
17
+ Samagotchi::ConfigFile.load!
18
18
  require "samagotchi"
19
19
 
20
20
  # `chi self`: print version, source dir, config/memory/session paths, model and
@@ -50,7 +50,7 @@ if ARGV[0] == "sessions"
50
50
  puts " delete [--force] ID... # delete sessions for good (IDs or unique prefixes); --force stops a live worker first"
51
51
  puts " prune [--dry-run] [--days N] [--keep N] [--keep-status running,...] [--test-only]"
52
52
  puts " clean [--dry-run] [--days N] # test sessions (SAMAGOTCHI_ENV=test, CI) and leftover chi scratch ones: all of them, or those older than N days"
53
- puts "Defaults: days=14 keep=500 keep_status=running (env overrides: SAMAGOTCHI_SESSION_RETENTION_DAYS etc.)"
53
+ puts "Defaults: days=14 keep=500 keep_status=none (config: session.retention_days, session.max_count, session.keep_status)"
54
54
  exit 0
55
55
  end
56
56
  dry_run = sessions_args.include?("--dry-run")
@@ -110,10 +110,12 @@ if ARGV[0] == "sessions"
110
110
  project = scope == "all" || cwd ? nil : Samagotchi::ProjectScope.root_for(Dir.pwd)
111
111
  scope_note = ->(count) { "#{count} session(s) in #{File.basename(project)} (--scope=all: every project)" }
112
112
  # The picker (chi note from a script): filters apply before the limit,
113
- # and test runs stay out.
113
+ # and test runs stay out, unless this is one (SAMAGOTCHI_ENV=test, CI):
114
+ # then its own sessions are what it looks for.
114
115
  if live || cwd || (format && format != "text")
115
116
  summaries = Samagotchi::SessionManager.session_summaries(
116
- live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: false, project_root: project,
117
+ live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: Samagotchi::Session.test_session_env?,
118
+ project_root: project,
117
119
  include_archived: include_archived
118
120
  )
119
121
  case format
@@ -142,7 +144,7 @@ if ARGV[0] == "sessions"
142
144
  end
143
145
  # A delegated session points at its parent.
144
146
  child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
145
- flag = summary[:scratch] ? " [scratch]" : ""
147
+ flag = summary[:scratch] ? " [scratch]" : (summary[:test_run] ? " [test]" : "")
146
148
  flag += " [archived]" if summary[:archived]
147
149
  ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
148
150
  puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
@@ -899,15 +901,9 @@ scratch = ARGV[0] == "scratch"
899
901
  ARGV.shift if scratch
900
902
 
901
903
  # no_interrupt / no_default_input start from config.yml or the env; the flags below turn them on.
902
- options = { verbose: false, no_interrupt: Samagotchi::Config.get("no_interrupt"), non_interactive: false, no_default_input: Samagotchi::Config.get("no_default_input"), web: false, web_port: nil, web_open: false, web_markdown: nil, web_turn_view: nil }
904
+ options = { verbose: false, no_interrupt: Samagotchi::Config.get("no_interrupt"), non_interactive: false, no_default_input: Samagotchi::Config.get("no_default_input"), web: false, web_port: nil, web_open: false, web_markdown: nil, web_view: nil }
903
905
  cli_overrides = {}
904
906
 
905
- if ARGV.any? { |arg| arg == "--backend" || arg.start_with?("--backend=") }
906
- warn "Error: --backend was removed. A host's api: in config.yml picks the loop " \
907
- "(api: openai for the chat API; see docs/configuration.md)."
908
- exit 1
909
- end
910
-
911
907
  # Strict kebab enforcement: --recap_base_url is unknown (OptionParser would otherwise accept _ as -)
912
908
  ARGV.each do |arg|
913
909
  next unless arg.start_with?("--") && arg.include?("_")
@@ -916,7 +912,7 @@ ARGV.each do |arg|
916
912
  base = arg.split("=", 2).first
917
913
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
918
914
  dashed = base.tr("_", "-")
919
- known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open] + Samagotchi::Config.cli_entries.map(&:cli_flag)
915
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
920
916
  if known.include?(dashed) || known.include?(base)
921
917
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
922
918
  exit 1
@@ -956,12 +952,12 @@ parser = OptionParser.new do |opts|
956
952
  opts.on("--no-default-input", "Skip default REPL input prefill") { options[:no_default_input] = true; cli_overrides["no_default_input"] = true }
957
953
  opts.on("--port PORT", "Port for `chi web` (default: 4567, env SAMAGOTCHI_WEB_PORT)") { |p| options[:web_port] = p.to_i; cli_overrides["web.port"] = p.to_s }
958
954
  opts.on("--open", "Open browser for `chi web`") { options[:web_open] = true }
955
+ opts.on("--new-token", "`chi web`: replace the LAN access token (web.host: lan); links and QR codes made with the old one stop working") { options[:new_token] = true }
959
956
  opts.on("--scope VALUE", %w[project all],
960
957
  "Which sessions `chi web` and `chi sessions list` show: project (the default: this folder's git project; " \
961
958
  "every session outside a repo) or all") { |v| options[:scope] = v }
962
959
  # Accepted values of string entries that the registry doesn't type as enums.
963
- value_hints = { "thinking.ui" => "spinner|off", "status.line" => "on|off", "status.width_mode" => "terminal_cap|fixed",
964
- "web.host" => "127.0.0.1|::1|localhost", "recap.sentences" => "N|N-M",
960
+ value_hints = { "status.line" => "on|off", "web.host" => "127.0.0.1|::1|localhost|lan|IP", "recap.sentences" => "N|N-M",
965
961
  "web.annotate_presets" => "text|text" }
966
962
  # Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
967
963
  # Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
@@ -969,7 +965,7 @@ parser = OptionParser.new do |opts|
969
965
  next if %w[no_interrupt no_default_input].include?(entry.key)
970
966
  next if entry.key == "web.port" # already handled as --port
971
967
  next if entry.key == "default.model" # alias --model above
972
- next if %w[web.markdown web.turn_view].include?(entry.key) # handled below with explicit inverse flags
968
+ next if entry.key == "web.markdown" # handled below with explicit inverse flags
973
969
  case entry.type
974
970
  when :bool
975
971
  # --[no-]: a bool that defaults to on is only useful switched off.
@@ -989,8 +985,6 @@ parser = OptionParser.new do |opts|
989
985
  end
990
986
  opts.on("--web-markdown", "Render finalized LLM Markdown in `chi web` (requires optional commonmarker gem)") { options[:web_markdown] = true; cli_overrides["web.markdown"] = true }
991
987
  opts.on("--no-web-markdown", "Disable Markdown rendering in `chi web`") { options[:web_markdown] = false; cli_overrides["web.markdown"] = false }
992
- opts.on("--web-turn-view", "Show each turn in `chi web` as one block of steps (the live one at the bottom; the default); ?view=turn|chat overrides per page") { options[:web_turn_view] = true; cli_overrides["web.turn_view"] = true }
993
- opts.on("--no-web-turn-view", "Show turns in `chi web` as the classic row of bubbles instead of the turn view") { options[:web_turn_view] = false; cli_overrides["web.turn_view"] = false }
994
988
  # Strict underscore rejection: --recap_base_url etc. are not registered and will raise OptionParser::InvalidOption
995
989
  opts.on("--version", "Print chi's version") { puts "chi #{Samagotchi::VERSION}"; exit }
996
990
  opts.on("-h", "--help", "Show help") { puts opts; exit }
@@ -1011,6 +1005,10 @@ if stray
1011
1005
  warn "Error: #{web || scratch ? "unexpected argument" : "unknown command"} #{stray} (see chi --help)"
1012
1006
  exit 1
1013
1007
  end
1008
+ if options[:new_token] && !web
1009
+ warn "Error: --new-token goes with chi web (chi web --new-token)"
1010
+ exit 1
1011
+ end
1014
1012
 
1015
1013
  # A scratch session is new, runs here and goes when it ends.
1016
1014
  if scratch
@@ -1021,16 +1019,9 @@ if scratch
1021
1019
  end
1022
1020
  end
1023
1021
 
1024
- # Apply CLI precedence: CLI > ENV > file
1025
- unless cli_overrides.empty?
1026
- Samagotchi::Config.reload!(cli_overrides: cli_overrides)
1027
- # Keep ENV in sync for legacy readers still using ENV directly
1028
- cli_overrides.each do |k, v|
1029
- entry = Samagotchi::Config.find_by_key(k)
1030
- next unless entry&.env_exposed?
1031
- ENV[entry.env_key] = v.to_s
1032
- end
1033
- end
1022
+ # Apply CLI precedence: CLI > ENV > file. Workers this chi spawns get
1023
+ # these through their env (Config.cli_env).
1024
+ Samagotchi::Config.reload!(cli_overrides: cli_overrides) unless cli_overrides.empty?
1034
1025
 
1035
1026
  # The debug log (Samagotchi::LogPath, level log.level) for every command.
1036
1027
  # -v (the plain REPL only): debug records, each mirrored to stderr.
@@ -1053,10 +1044,10 @@ when "web"
1053
1044
  options[:web_markdown] = true
1054
1045
  elsif arg == "--no-web-markdown"
1055
1046
  options[:web_markdown] = false
1056
- elsif arg == "--web-turn-view"
1057
- options[:web_turn_view] = true
1058
- elsif arg == "--no-web-turn-view"
1059
- options[:web_turn_view] = false
1047
+ elsif arg == "--web-view" && Samagotchi::Config::BY_KEY["web.view"].enum_values.include?(web_args[i + 1])
1048
+ options[:web_view] = web_args[i + 1]
1049
+ elsif arg.start_with?("--web-view=") && Samagotchi::Config::BY_KEY["web.view"].enum_values.include?(arg.split("=", 2).last)
1050
+ options[:web_view] = arg.split("=", 2).last
1060
1051
  elsif arg.start_with?("--port=")
1061
1052
  options[:web_port] = arg.split("=", 2).last.to_i
1062
1053
  end
@@ -1076,15 +1067,12 @@ end
1076
1067
  if options[:web]
1077
1068
  chi_update_hint.call
1078
1069
  require "samagotchi/web/server"
1079
- port = options[:web_port] || Samagotchi::Config.get("web.port") || 4567
1080
- # legacy env fallback
1081
- port = ENV.fetch("SAMAGOTCHI_WEB_PORT", port.to_s).to_i rescue port.to_i
1070
+ port = options[:web_port] || Samagotchi::Config.get("web.port")
1082
1071
  markdown = options[:web_markdown]
1083
1072
  markdown = Samagotchi::Config.get("web.markdown") if markdown.nil?
1084
- turn_view = options[:web_turn_view]
1085
- turn_view = Samagotchi::Config.get("web.turn_view") if turn_view.nil?
1073
+ view = options[:web_view] || Samagotchi::Config.get("web.view")
1086
1074
  exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
1087
- markdown: markdown, turn_view: turn_view,
1075
+ markdown: markdown, view: view, new_token: options[:new_token],
1088
1076
  annotate_presets: Samagotchi::Config.get("web.annotate_presets"))
1089
1077
  end
1090
1078
 
@@ -1183,7 +1171,7 @@ if launch == :attached
1183
1171
  end
1184
1172
 
1185
1173
  begin
1186
- Samagotchi::TerminalUI.new(
1174
+ ended = Samagotchi::TerminalUI.new(
1187
1175
  prompt: options[:prompt],
1188
1176
  session_id: options[:resume],
1189
1177
  no_interrupt: options[:no_interrupt] || options[:non_interactive],
@@ -1194,6 +1182,8 @@ begin
1194
1182
  model_name: options[:model],
1195
1183
  scratch: scratch
1196
1184
  ).run
1185
+ # `chi -p X </dev/null`: the turn failed (nothing was sent again).
1186
+ exit 1 if ended == :turn_failed
1197
1187
  rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
1198
1188
  warn "Error: #{e.message}"
1199
1189
  exit 1
data/docs/cli.md CHANGED
@@ -12,15 +12,16 @@
12
12
  - `chi --attach <session-id>` — attach the terminal to a session's worker (e.g. one started from the Web UI), waking one if it has exited
13
13
  - A session id can be shortened to any unique prefix (like git): `chi --attach 2ea8`. `--resume`, `--attach`, `sessions stop`, `sessions archive` and `sessions delete` take one; an ambiguous prefix lists the sessions it matches.
14
14
  - `chi web [--port 4567] [--open] [--scope=all]` — start the Web UI (single localhost port session control plane) on this git project's sessions (`--scope=all`, or a folder in no repo: every session); if a chi web already runs on the port, print (with `--open`, open) its page for this folder and exit. Something else on the port (an older chi web too) exits 1 with "port N is in use"
15
+ - `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
15
16
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages
16
- - `chi web --no-web-turn-view` — show turns as the classic row of bubbles instead of the default turn view (each turn as one block of steps, the running one at the bottom); `?view=turn|chat` on the page URL overrides it (see [Web turn view](#web-turn-view))
17
+ - `chi web --web-view stage|chat` — draw turns with the stage view (the running turn pinned above the composer) or as the classic row of bubbles instead of the default turn view (each turn as one block of steps, the running one at the bottom); `?view=turn|stage|chat` on the page URL overrides it (see [Web views](#web-views))
17
18
  - `chi sessions list|stop|archive|unarchive|delete|prune|clean` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md))
18
19
  - `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
19
20
  - `chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one picks it up); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead, and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; see [Starting a session](sessions.md#starting-a-session))
20
21
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text to live sessions as context notes (see [Desktop helper](desktop.md))
21
22
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles
22
23
  - `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
23
- - `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs (see [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle) and [the check-in bundle](plugins.md#the-check-in-bundle))
24
+ - `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs (see [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle))
24
25
 
25
26
  ### First setup
26
27
 
@@ -71,7 +72,7 @@ known-names 0.1.0 0.1.1 updated
71
72
  infra_tools 1.0.0 skipped (not from chi)
72
73
  Chi Helper 0.2.0 up to date (launch file refreshed)
73
74
  workers 2 live on 0.2.0: they move to 0.3.0 at idle exit (30 min) or chi sessions stop 2ea8c1f0 91b0d2aa
74
- Also shipped, not installed: check-in, source-links (chi bundle install NAME)
75
+ Also shipped, not installed: check-in, skills, source-links (chi bundle install NAME)
75
76
  done
76
77
  ```
77
78
 
@@ -148,8 +149,8 @@ flag also works as `--kebab-case VALUE`, e.g. `--server-host`, `--server-port`,
148
149
  `api: openai` in config.yml is driven through the OpenAI chat API (streamed; a remote
149
150
  provider via `url:` and `api_key_env:`); every other host gets chi's own raw-prompt loop. `/model` and `--model host:model` switch hosts, and
150
151
  the loop with them. See [Configuration](configuration.md) (`hosts:` and `api:`).
151
- `--backend`, `SAMAGOTCHI_BACKEND` and a `backend:` key were removed; chi says so if
152
- it sees one.
152
+ `--backend`, `SAMAGOTCHI_BACKEND` and a `backend:` key were removed (`backend:` warns
153
+ as an unknown key).
153
154
 
154
155
  ### Entrypoint scenarios
155
156
 
@@ -417,16 +418,54 @@ touch screen), and so does each code block of a rendered answer. An answer
417
418
  copies its Markdown source, not the rendered text; a code block copies just
418
419
  its code; a prompt copies the text as you typed it.
419
420
 
420
- ### Web turn view
421
+ ### Web views
421
422
 
422
- The turn view, the default, shows a turn as *one block* where the work
423
- happens (the classic chat view renders a turn with tool calls as a row of
424
- bubbles: one thinking block, one activity panel and one answer bubble per
425
- generation; `web.turn_view: false` brings it back). The running
423
+ `web.view` picks how the page draws a turn: `turn` (the default, below),
424
+ `stage` (the running turn pinned above the composer, below) or `chat` (the
425
+ classic row of bubbles: one thinking block, one activity panel and one
426
+ answer bubble per generation).
427
+
428
+ ```sh
429
+ chi web --web-view stage # or chat; turn is the default
430
+ ```
431
+
432
+ The setting also supports `SAMAGOTCHI_WEB_VIEW=stage` or the global config:
433
+
434
+ ```yaml
435
+ web:
436
+ view: stage
437
+ ```
438
+
439
+ `?view=turn`, `?view=stage` or `?view=chat` on the page URL picks the view
440
+ for that page load, whatever the config says; the parameter is dropped
441
+ when you switch between the project and all-sessions views. The terminal
442
+ UIs are not affected.
443
+
444
+ **The stage view** pins the running turn above the composer, in its own
445
+ card, so it stays on screen without scrolling: a status row (what it is
446
+ doing, the step, the elapsed time), your prompt on one line, the newest
447
+ narration sentence (else the newest thinking one, in italics; click it for
448
+ the step's reasoning), the running tool with what it does, and the last
449
+ three calls. A card that needs you (a question, an approval, check-in)
450
+ sits in the stage too. The chip under it, `N steps · M tool calls` with one
451
+ tick per call, opens the turn view's block in place (newest step first
452
+ while it runs; a tick opens its step). `▾` folds the stage to its status
453
+ row, remembered in this browser. When the turn ends the answer shows in
454
+ the stage, and the whole turn moves up into the history once you are not
455
+ using the stage (the pointer over it, a touch or scroll in the last 4 s,
456
+ keyboard focus or a selection keep it) for 1.5 s; sending the next message
457
+ moves it at once. The history is never scrolled while a turn runs, and only
458
+ follows the hand-off if you were at its end. The `/` command list opens
459
+ over the stage's lower edge.
460
+
461
+ **The turn view** shows a turn as *one block* where the work
462
+ happens. The running
426
463
  generation is the live part at the bottom (its thinking, its narration, its
427
464
  tool rows), the earlier ones stack above it collapsed to one line each
428
- (their narration's first line, else `working with <tools>`, and a call
429
- count), expandable for inspection. The live thinking is one line: the
465
+ (their narration's first line, else their first call's title such as
466
+ `edit lib/a.rb`, and a call count), expandable for inspection. A tool row
467
+ says what the call did: a file's path relative to the session's folder, a
468
+ command without its leading `cd … &&` (the full parameters on hover). The live thinking is one line: the
430
469
  newest complete sentence, changing at most once per 1.5 s. Click it for the
431
470
  full text; a peek is per step (the next step's thinking starts closed
432
471
  again). When a step ends its thinking closes to a plain `thinking` line
@@ -447,22 +486,6 @@ steps saved before that have none, so they show no thinking. An `edit` or
447
486
  made (up to 120 lines or 8 KB), live and after a reload (see
448
487
  [Guardrails](guardrails.md#ask) for the diff an approval shows first).
449
488
 
450
- ```sh
451
- chi web --no-web-turn-view # the classic chat view; --web-turn-view is the default
452
- ```
453
-
454
- The setting also supports `SAMAGOTCHI_WEB_TURN_VIEW=false` or the global config:
455
-
456
- ```yaml
457
- web:
458
- turn_view: false
459
- ```
460
-
461
- `?view=chat` on the page URL forces the classic chat view for that page load
462
- and `?view=turn` the turn view, whatever the config says; the parameter is dropped
463
- when you switch between the project and all-sessions views. The terminal
464
- UIs are not affected.
465
-
466
489
  ### Web annotate presets
467
490
 
468
491
  Selecting text in an answer, a thinking block, a tool row or one of your
@@ -489,6 +512,51 @@ web:
489
512
  there means the default, not "none": use `""` in the file or on the command
490
513
  line. A `chi web` that already runs keeps its list; restart it.
491
514
 
515
+ ### chi web on your phone
516
+
517
+ `chi web` listens on 127.0.0.1 only. `--web-host lan` (or `web.host: lan`
518
+ in config.yml) also opens it on this machine's private IPv4 address, for a
519
+ phone on the same Wi-Fi:
520
+
521
+ ```sh
522
+ chi web --web-host lan
523
+ ```
524
+
525
+ ```
526
+ Chi Web on http://127.0.0.1:4567/?dir=/Users/me/projects/app (public: …)
527
+ LAN: http://192.168.1.55:4567/?token=… ← anyone with this link can run commands as you
528
+ Plain http: the link and your traffic can be read by anyone on this Wi-Fi.
529
+ <the link's QR code>
530
+ ```
531
+
532
+ Scan the QR code with the phone's camera. The page trades the token in the
533
+ link for a cookie (kept 400 days) and drops it from the address, so a
534
+ bookmark or a home-screen icon keeps working across restarts. A home-screen
535
+ web app on iOS has cookies of its own: if it opens on "needs chi web's
536
+ access token", paste the token there (the part of the link after
537
+ `token=`), or open the link once in it.
538
+
539
+ - Every request from another machine needs the token (the cookie, or
540
+ `Authorization: Bearer <token>` for curl); this Mac (127.0.0.1) needs none.
541
+ Without it the page says how to get in and the API answers 401.
542
+ - The token lives in `$XDG_STATE_HOME/samagotchi/web-token` (0600).
543
+ `chi web --new-token` replaces it: a running chi web takes the new one at
544
+ once, and every phone has to scan the new QR code.
545
+ - A second `chi web` prints the LAN link and QR code again. A plain
546
+ `chi web --web-host lan` while a chi web without LAN access runs asks you
547
+ to stop that one first. `chi self` says whether chi web runs on the LAN.
548
+ - `lan` picks the first private address (10.x, 172.16–31.x, 192.168.x) on an
549
+ interface that is up, not a VPN tunnel, bridge, VM or container, and names
550
+ the others; `web.host: 10.0.0.3` picks one yourself. An address outside
551
+ those ranges (a Tailscale 100.x one, a public one) works, with a warning.
552
+ After the address changes (a new Wi-Fi), restart chi web. IPv6 isn't
553
+ offered.
554
+ - It is plain http: the token and everything you do travel unencrypted on
555
+ the Wi-Fi. Use it on your home network, never on a shared one (a café, an
556
+ office guest network), and run `chi web --new-token` if a link leaks.
557
+ - On http the browser has no notifications: the bell is hidden on the phone,
558
+ and the tab title still counts what needs you.
559
+
492
560
  ## Runtime Model Switch (Assist Mode)
493
561
 
494
562
  In interactive assist mode, you can switch the request model without restarting:
@@ -576,38 +644,23 @@ Behavior details:
576
644
 
577
645
  ## Status Line
578
646
 
579
- Assist mode can render a compact generalized status line that can include mode,
580
- context estimate, and active memory hints.
647
+ The REPL and attached mode show one status row under the prompt, drawn when what it says
648
+ changes:
581
649
 
582
- Behavior:
583
-
584
- - A static status line is printed before the next `>` prompt in assist mode.
585
- - During spinner rendering, status details are rendered in the spinner block.
586
- - When llama.cpp streaming payload includes usage fields, status prefers server-derived token telemetry (`p`, `c`, `t`) and context percent.
587
- - If server usage fields are absent, status falls back to the `:context_status` estimate telemetry.
588
- - When a memory is loaded between tool rounds, the spinner line includes a `loaded: <memory>` notification immediately after the spinner frame.
589
- - After responses, memory details are shown via the same unified `status>` line.
590
- - The legacy standalone `memories>` summary line is no longer emitted.
591
- - With `--mute`, the sticky and idle rows add `muted: <names>` after `mem:` (the
592
- spinner row doesn't). Attached, `mem:` shows the used memories and the
593
- session's `--memory` list before the first turn records them.
594
-
595
- Configuration:
596
-
597
- - `SAMAGOTCHI_STATUS_LINE` (default `on`): set to `off`, `false`, or `0` to disable status-line rendering.
598
- - `SAMAGOTCHI_STATUS_WIDTH_MODE` (default `terminal_cap`): one of `terminal_cap`, `fixed`.
599
- - `SAMAGOTCHI_STATUS_MAX_WIDTH` (default `160`): maximum width used by `terminal_cap`.
600
- - `SAMAGOTCHI_STATUS_FIXED_WIDTH` (default `120`): fixed width used by `fixed` mode.
601
-
602
- Width mode behavior:
603
-
604
- - `terminal_cap`: use `min(terminal_columns, SAMAGOTCHI_STATUS_MAX_WIDTH)`, single-line with `+N` overflow indicator.
605
- - `fixed`: use `SAMAGOTCHI_STATUS_FIXED_WIDTH`, single-line with `+N` overflow indicator.
606
-
607
- Notes:
650
+ ```
651
+ status> model=Qwen3.6-35B | ↳ 3f2a1c9e | ctx=12.3% (under20) | mem: notes, cli_usage | muted: gh-helper
652
+ ```
608
653
 
609
- - Spinner rendering remains app-managed to keep cursor cleanup deterministic.
610
- - Raw terminal auto-wrap is intentionally avoided in the spinner region.
654
+ - `model=`: the model in use, `(default: …)` beside it when it isn't the config's default, and
655
+ `model=<served> (served; asked <name>)` when the server said it served another model.
656
+ - `↳ <id>`: the session that delegated this one.
657
+ - `ctx=`: the kernel's context estimate and its bucket, updated during a turn (on hosts that
658
+ report none, `api: openai`, at the turn's end).
659
+ - `mem:`: the memories the session read, with its `--memory` list; `muted:` its `--mute` list.
660
+ Up to 8 names each, then `+N`.
661
+ - The row is cut to the terminal's width. Without a live region (output or input not a terminal,
662
+ `TERM=dumb`) it prints as a line when it changes.
663
+ - `SAMAGOTCHI_STATUS_LINE` / `status.line` (default `on`): `off`, `false` or `0` hides it.
611
664
 
612
665
  ## Tool Tally
613
666
 
@@ -622,36 +675,45 @@ It lists the top 3 tools by count (ties go to the tool used first), the failed c
622
675
  (a call a guardrail or an approval blocked counts as a call, not as failed) and the last
623
676
  call with its parameters, cut to the terminal width. It starts over with each turn.
624
677
 
625
- - Attached mode: the second row of the activity slot, shown while the slot is (the
678
+ - The REPL and attached mode: the second row of the activity slot, shown while the slot is (the
626
679
  model generating or a tool running). Joining a turn mid-way seeds it from the turn so far.
627
- - The REPL (`--no-shared`): a row under the spinner row. The spinner stops while tools
628
- run (the `tool>` lines show them), so the tally shows while the model generates
629
- between tool rounds; the spinner block is one row taller from then on.
630
680
  - The web: the activity panel's summary reads `activity · 12 tool calls (2 failed) · execute ×7 · …`
631
681
  (without `last:`: the rows show it).
632
682
 
633
- ## Thinking Spinner Sentence
683
+ ## Activity Row
684
+
685
+ While a turn runs, one row above the prompt says what it is doing, the spinner frame first
686
+ (the REPL and attached mode alike):
634
687
 
635
- While the model generates, the spinner row shows the newest complete sentence of its thinking
636
- (`model> thinking · <sentence> |` in the REPL, `| thinking · <sentence>` in attached mode), or of its
637
- answer (`writing ·`), like the web's thinking ticker: the same sentence rules (a list number such as
638
- `118.` is no sentence end; a newline is one), and the row changes at most once every 1.5 s so it
639
- doesn't flicker. A long sentence is cut with `…`; a Qwen `TURN:` prefix and inline markdown are left
640
- out. Before the first sentence the row reads `thinking...`. With `TERM=dumb` there is no spinner row.
688
+ - `| thinking…` while the model starts, then `| thinking · <sentence>`: the newest complete
689
+ sentence of its thinking, or of its answer (`writing ·`), like the web's thinking ticker: the
690
+ same sentence rules (a list number such as `118.` is no sentence end; a newline is one), and the
691
+ row changes at most once every 1.5 s so it doesn't flicker. A long sentence is cut with `…`; a
692
+ Qwen `TURN:` prefix and inline markdown are left out.
693
+ - `| waiting for the first token… 5s` after 2 s with nothing streamed.
694
+ - `| running execute…` while a tool runs (its `tool>` line prints when it ends).
695
+ - `| retrying (1/4 in 0.5s): Errno::ECONNREFUSED` while a network error is retried.
696
+ - `| mcp: starting servers…` while a plugin's slow setup (an init task) runs, between turns too;
697
+ `mcp> ✓ …` prints when it is done.
641
698
 
642
- - When a memory entry is loaded during thinking, the spinner line also shows a compact inline preview
643
- of that tool call (for example `tool: memory_read(name=...)`) for live visibility before end-of-turn
644
- tool logs; the sentence gets the room left.
699
+ The spinner turns with time, so a turn that gets no chunks still looks alive. Without a live
700
+ region (output or input not a terminal, `TERM=dumb`) there is no activity row; the turn's lines
701
+ still print as they end.
645
702
 
646
703
  ## Thinking-Phase Cancellation
647
704
 
648
- During assist-mode thinking (while the spinner is active), you can cancel an in-flight model request without exiting the process:
705
+ While a turn runs, you can cancel it without exiting the process:
649
706
 
650
707
  - Press `Ctrl-C` to cancel the active request.
651
708
 
652
709
  Behavior notes:
653
710
 
654
711
  - Cancellation returns control to the prompt immediately; what you typed there stays.
712
+ - The turn ends with one line, `✕ turn canceled (Ctrl-C) · 3.1s`, and for a prompt turn a dim
713
+ `partial progress kept; !rollback restores the pre-turn state` under it (a canceled continue is back where it
714
+ started). A failed turn ends with `✕ turn failed: <summary> · 2.0s` and a dim `prompt restored for retry`. The
715
+ REPL and attached mode say the same; the web says `✕ canceled (Ctrl-C)` (`stopped` for its Stop button,
716
+ `by a hook` for a hook's).
655
717
  - Visible text the canceled request had streamed stays in the conversation, marked `[interrupted]`, so the next
656
718
  message (or a continue) picks up from the half-finished reply; the canceled request's thinking and any unfinished
657
719
  tool call are dropped.