samagotchi 0.2.0 → 0.3.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -1
  3. data/README.md +40 -4
  4. data/bin/chi +86 -19
  5. data/docs/cli.md +108 -5
  6. data/docs/configuration.md +229 -45
  7. data/docs/desktop.md +6 -0
  8. data/docs/hooks.md +126 -5
  9. data/docs/plugins.md +68 -2
  10. data/docs/releasing.md +18 -8
  11. data/docs/sessions.md +30 -4
  12. data/lib/samagotchi/answer_display.rb +95 -0
  13. data/lib/samagotchi/archive_store.rb +90 -0
  14. data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
  15. data/lib/samagotchi/bootstrap/probe.rb +262 -0
  16. data/lib/samagotchi/bootstrap_command.rb +347 -0
  17. data/lib/samagotchi/bridge/pending_card.rb +89 -0
  18. data/lib/samagotchi/bridge/turn_accumulator.rb +14 -3
  19. data/lib/samagotchi/bridge.rb +9 -0
  20. data/lib/samagotchi/bridge_client.rb +6 -2
  21. data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
  22. data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
  23. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +358 -0
  24. data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
  25. data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
  26. data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
  27. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  28. data/lib/samagotchi/bundles/system/self_map.md +8 -2
  29. data/lib/samagotchi/client.rb +72 -13
  30. data/lib/samagotchi/config.rb +196 -36
  31. data/lib/samagotchi/desktop/macos/ChiRunner.swift +13 -7
  32. data/lib/samagotchi/desktop/macos/Panel.swift +71 -19
  33. data/lib/samagotchi/empty_answer_retry.rb +43 -0
  34. data/lib/samagotchi/engine.rb +233 -36
  35. data/lib/samagotchi/guardrails/approval.rb +9 -0
  36. data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
  37. data/lib/samagotchi/guardrails.rb +1 -0
  38. data/lib/samagotchi/hooks/registry.rb +24 -5
  39. data/lib/samagotchi/host_registry.rb +4 -3
  40. data/lib/samagotchi/idle_recap.rb +5 -1
  41. data/lib/samagotchi/kernel_loop.rb +47 -15
  42. data/lib/samagotchi/llm/chat_loop.rb +59 -20
  43. data/lib/samagotchi/llm/errors.rb +21 -3
  44. data/lib/samagotchi/llm/http.rb +42 -13
  45. data/lib/samagotchi/llm/openai_chat.rb +12 -4
  46. data/lib/samagotchi/log_subscriber.rb +18 -3
  47. data/lib/samagotchi/model_profile.rb +1 -1
  48. data/lib/samagotchi/plugin/context.rb +22 -1
  49. data/lib/samagotchi/plugin/sessions.rb +3 -1
  50. data/lib/samagotchi/reply_wait.rb +126 -0
  51. data/lib/samagotchi/sampling_settings.rb +58 -0
  52. data/lib/samagotchi/self_report.rb +1 -0
  53. data/lib/samagotchi/send_command.rb +153 -7
  54. data/lib/samagotchi/session.rb +52 -11
  55. data/lib/samagotchi/session_archive_command.rb +107 -0
  56. data/lib/samagotchi/session_commands.rb +11 -2
  57. data/lib/samagotchi/session_manager.rb +114 -9
  58. data/lib/samagotchi/session_metrics.rb +222 -106
  59. data/lib/samagotchi/steer.rb +72 -0
  60. data/lib/samagotchi/terminal_ui/attached_loop.rb +39 -6
  61. data/lib/samagotchi/terminal_ui/event_renderer.rb +13 -8
  62. data/lib/samagotchi/terminal_ui/formatting.rb +31 -8
  63. data/lib/samagotchi/terminal_ui/input_support.rb +3 -0
  64. data/lib/samagotchi/terminal_ui.rb +77 -4
  65. data/lib/samagotchi/tool_activity.rb +3 -1
  66. data/lib/samagotchi/tools/builtins.rb +15 -4
  67. data/lib/samagotchi/tools/delegate_wait.rb +26 -69
  68. data/lib/samagotchi/tools/execute.rb +52 -14
  69. data/lib/samagotchi/tools/task_runtime.rb +19 -0
  70. data/lib/samagotchi/tools/task_wait.rb +27 -3
  71. data/lib/samagotchi/turn_note.rb +60 -6
  72. data/lib/samagotchi/version.rb +1 -1
  73. data/lib/samagotchi/vision_support.rb +2 -6
  74. data/lib/samagotchi/web/app.rb +88 -4
  75. data/lib/samagotchi/web/public/activity.js +10 -1
  76. data/lib/samagotchi/web/public/annotate_presets.js +26 -0
  77. data/lib/samagotchi/web/public/annotations.js +13 -0
  78. data/lib/samagotchi/web/public/app.js +437 -88
  79. data/lib/samagotchi/web/public/card.js +5 -3
  80. data/lib/samagotchi/web/public/chat_view.js +10 -1
  81. data/lib/samagotchi/web/public/copy.js +20 -4
  82. data/lib/samagotchi/web/public/ctx.js +15 -0
  83. data/lib/samagotchi/web/public/data.js +21 -6
  84. data/lib/samagotchi/web/public/format.js +9 -0
  85. data/lib/samagotchi/web/public/index.html +38 -2
  86. data/lib/samagotchi/web/public/notify.js +175 -0
  87. data/lib/samagotchi/web/public/question_card.js +2 -1
  88. data/lib/samagotchi/web/public/sessions_list.js +7 -0
  89. data/lib/samagotchi/web/public/timing.js +39 -14
  90. data/lib/samagotchi/web/public/turn_events.js +46 -0
  91. data/lib/samagotchi/web/public/turn_view.js +47 -7
  92. data/lib/samagotchi/web/server.rb +8 -4
  93. data/lib/samagotchi/web/session_hub.rb +2 -1
  94. data/lib/samagotchi/web/session_summary.rb +24 -1
  95. data/lib/samagotchi/worker.rb +11 -0
  96. metadata +20 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da62382ff837576724b356e7ac03ccb484c382628b4a3a7fcfaacf799de4cc92
4
- data.tar.gz: 2822682cf66025598dbb6effc4598f97b3658fec0153cfcfaf5088a7385764dd
3
+ metadata.gz: 950fbee37d2110efad37f8d3f8981144f49af9a2ccf75649d5953b1fe7773260
4
+ data.tar.gz: aaad7adb0e9aa3c81b601df48fde4ebab965862ed954454c8b79e2de841f0789
5
5
  SHA512:
6
- metadata.gz: 860855c62b763f58c44d06e137f8f45abceb81e4e8169c5c46182b69ae00442cc44c285c056e79120ce2b5a6ef2516c3e7fb75739188b650fc6c918644f25796
7
- data.tar.gz: 429c0f7b9fc76def59cd87a9493bddaca7813d38f73f8ce974616f9f55af89767aca8b7f9b2cd629b8e0a7fe606e98eee05ee52199a74ec6d3d13c00f23c2e82
6
+ metadata.gz: 1b4d092f263192c242dead3ae0c556758a38af2f01285dc590362d698696a31d38801c70e64ff85daa33067ceda92be7687915024d1e81802bd0a2c365cc292d
7
+ data.tar.gz: 7a06a4d47dea9168e40b0ae0ba7bc6cbfe1b377ba9ac29a998d19f0ee17a766de3186786894deea8b6765a78e21b14e3e7ece8238ab401354c43a5eba4d1815b
data/CHANGELOG.md CHANGED
@@ -8,6 +8,120 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.3.0] - 2026-09-29
12
+
13
+ ### Added
14
+
15
+ - `chi bootstrap [HOST[:PORT]|URL]`: the first setup in one command. It finds
16
+ out whether the server is llama.cpp or OpenAI-compatible (with no target, it
17
+ looks on localhost's usual ports), picks the model, sends a test request and
18
+ writes config.yml; an existing config gets a `hosts:` entry added after a
19
+ backup, its other lines untouched. `--key-env VAR` for a server that wants a
20
+ key, `--model`, `--name`, `--no-test`, `--dry-run`. The first-run "no model
21
+ configured" line points at it.
22
+ - Web notifications: a session that needs you (a question or approval, a
23
+ failed turn, a turn done after 10 s or more) while the tab is in the
24
+ background counts in the page title, `(N) Chi`, and with the new bell in the
25
+ top bar on, shows an OS notification that opens the session.
26
+ - `chi scratch`: a one-time session in the terminal that leaves nothing behind.
27
+ It is deleted however it ends, saves no memories and starts no other
28
+ sessions; one left by a killed process goes at the next sweep or
29
+ `chi sessions clean`.
30
+ - Archiving a session: `chi sessions archive ID`, `/archive` in a terminal or
31
+ `archive` in the web's info bar hides it (and its delegates) from every list
32
+ and keeps it for good; `chi sessions list --archived` and "include archived"
33
+ in the web's all sessions find it, and a message you send to it brings it
34
+ back.
35
+ - `chi send --new` starts a session with the message, the way the web does, so
36
+ it shows in the web at once (`--dir`, `--model`); `chi send --wait` (new or
37
+ one existing session) blocks and prints the answer, exit 3 when the turn
38
+ waits for your answer.
39
+ - The `check-in` bundle (`chi bundle install check-in`): after 50 tool calls
40
+ in one turn with no answer (then every 50 more) a card asks you to nudge
41
+ the model, let it keep going or stop the turn; `mode: nudge` nudges it by
42
+ itself. `/checkin` shows and changes it for the session.
43
+ - Plugins can steer the running turn: `ctx.steer(text)` and a hook's
44
+ `event[:steer]` put text into the turn as its own message at the loop's
45
+ next step, shown as `<bundle>> nudged: …`; `ctx.stop_turn(reason)` stops it.
46
+ - The `source-links` bundle (`chi bundle install source-links`): refs like
47
+ `JIRA-123` or `GH-45` in an answer become links in the web, and a one-line
48
+ `sources:` note follows the turn in the terminal. Sources are patterns in
49
+ `bundles: source-links:`.
50
+ - Plugins can change how an answer is shown without changing what the model
51
+ sees: an after_turn hook's `event[:present]` sets a display version of the
52
+ answer, which the web renders (and keeps after a reload).
53
+ - Annotate presets: selecting text in an answer offers quick replies next to
54
+ Annotate (default "Agreed" and "Could you please elaborate?"); a pill puts
55
+ the quote and the text in the composer without sending.
56
+ `web.annotate_presets` (`|`-separated or a YAML list; `""` turns them off).
57
+ - `sampling:` on a `hosts:` or `models:` entry (temperature, top_p, top_k,
58
+ min_p, penalties, …) is sent with every request to that host or model;
59
+ `temperature: null` sends none, so the provider's default applies.
60
+ - `retry.empty_answer` (default 1): when the model returns an empty answer
61
+ (or runs out of tokens while thinking), chi asks again once in the same
62
+ turn; the terminal and the web show "↻ empty answer, asking again".
63
+ - The context fill is saved with the session: `chi sessions list` and the web
64
+ list show it (`ctx 12%`), and `/stats`, the status line and the web meter
65
+ show it right after a restart or a reload. Token totals now cover the whole
66
+ session across worker restarts.
67
+ - `chi send --wait ID` with no message waits for the session's next reply
68
+ without sending anything (also on a running session), e.g. after `--wait`
69
+ exited 3 for a question.
70
+ - The desktop panel has a "New session in <folder>" row (⏎ starts a session
71
+ with the selection there).
72
+ - Web notifications also cover a check-in card waiting for you, and a chi tab
73
+ in front keeps the tabs behind it from notifying twice.
74
+
75
+ ### Changed
76
+
77
+ - Stop ends a turn that is waiting in `task_wait` or running `execute` within
78
+ about a second, and a Stop right after a turn starts ends it at once. A
79
+ background task keeps running: the model is told it's still running and how
80
+ to continue or stop it, and the cancel note lists the session's tasks still
81
+ running. A stopped wait shows as "stopped".
82
+ - config.yml: a nested key now wins over its legacy flat `SAMAGOTCHI_*` key
83
+ (one warning names both); an unknown key warns with a did-you-mean;
84
+ top-level keys like `max_tool_output_chars` no longer warn; `no_interrupt`
85
+ and `no_default_input` work from config and env.
86
+ - A refused connection to a host fails at once with "can't reach host … — is
87
+ the server running?" instead of retrying for half a minute.
88
+ - A host whose `/props` doesn't answer costs one probe per 30 s, not a wait
89
+ at every turn.
90
+ - `chi -p … --non-interactive` with an empty answer says so on stderr and
91
+ exits 1.
92
+ - docs/configuration.md uses nested keys throughout and lists every setting;
93
+ the README starts with `chi bootstrap`.
94
+
95
+ ### Removed
96
+
97
+ - The unused config keys `bridge.enable` and `thinking.preview_lines` (they
98
+ now warn as unknown).
99
+
100
+ ### Fixed
101
+
102
+ - The web keeps a failed turn's prompt and reason after a reload, and a failed
103
+ turn's prompt comes back to the composer, also for a first message from the
104
+ start page.
105
+ - Web answer links appear with the answer instead of being swapped in after it.
106
+ - The title badge clears when you switch to the tab in Safari, and drops a
107
+ question answered from another tab or client.
108
+ - `/archive` and `/exit` typed in the web composer get a short reply instead
109
+ of an error.
110
+ - An approval card for a tool without a command or path shows its arguments.
111
+ - A hook notice from before the turn stays in its step after a reload.
112
+ - Card actions (check-in's Nudge, …) no longer leave a command bubble or line.
113
+ - A 404 from a native (llama.cpp) host hints at `api: openai`.
114
+ - A config `memories:` entry that can't be loaded is no longer called `--memory`.
115
+ - `chi --resume`/`--attach` refuse a leftover scratch session.
116
+ - `chi sessions list --live/--cwd/--format` show `[scratch]` too.
117
+ - The legacy-key warning names the right nested key.
118
+
119
+ ### Bundles
120
+
121
+ - New: `check-in` 0.1.1 (`chi bundle install check-in`, needs chi 0.3.0) and
122
+ `source-links` 0.2.0 (`chi bundle install source-links`). No other bundle
123
+ changed since 0.2.0.
124
+
11
125
  ## [0.2.0] - 2026-09-27
12
126
 
13
127
  The first public release. chi is an agent harness for local models
@@ -39,5 +153,6 @@ and long-lived sessions.
39
153
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
40
154
  hotkey panel that send selected text or the clipboard to your sessions.
41
155
 
42
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.2.0...HEAD
156
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.3.0...HEAD
157
+ [0.3.0]: https://github.com/dm1try/samagotchi/compare/v0.2.0...v0.3.0
43
158
  [0.2.0]: https://github.com/dm1try/samagotchi/releases/tag/v0.2.0
data/README.md CHANGED
@@ -32,9 +32,28 @@ bin/chi self
32
32
  `bin/chi` runs the checkout; `bundle exec rake gem:install` installs it as a
33
33
  local gem, which puts `chi` on your PATH.
34
34
 
35
+ ## Set up
36
+
37
+ Point chi at your model server; it works out the rest and writes
38
+ `~/.config/samagotchi/config.yml`:
39
+
40
+ ```sh
41
+ chi bootstrap 192.168.1.29:8081 # llama.cpp on another machine
42
+ chi bootstrap localhost:11434 # Ollama (any OpenAI-compatible server)
43
+ chi bootstrap https://openrouter.ai/api/v1 --key-env OPENROUTER_API_KEY
44
+ chi bootstrap # look on this machine's usual ports
45
+ ```
46
+
47
+ It finds out whether the server is llama.cpp or OpenAI-compatible, picks the
48
+ model (or asks, when there are several), sends one test request and writes
49
+ the config. With a config already there, it adds a `hosts:` entry and keeps
50
+ the rest of the file. Edit the file later as in [Configure](#configure);
51
+ `chi bootstrap --help` has the options.
52
+
35
53
  ## Configure
36
54
 
37
- Create `~/.config/samagotchi/config.yml` (or `$XDG_CONFIG_HOME/samagotchi/config.yml`):
55
+ To write the config by hand instead, or to change it later: create
56
+ `~/.config/samagotchi/config.yml` (or `$XDG_CONFIG_HOME/samagotchi/config.yml`):
38
57
 
39
58
  ```yaml
40
59
  default:
@@ -44,7 +63,20 @@ server:
44
63
  port: 8080
45
64
  ```
46
65
 
47
- Other settings (multiple hosts, transports, timeouts, the idle recap) are in
66
+ `server:` is a llama.cpp `llama-server`. For any other OpenAI-compatible
67
+ server (vLLM, LM Studio, Ollama, a gateway), name it under `hosts:` with
68
+ `api: openai` instead:
69
+
70
+ ```yaml
71
+ default:
72
+ model: local:qwen3:8b # host:model; the ids are in GET <url>/models
73
+ hosts:
74
+ local:
75
+ url: http://localhost:11434/v1 # the API base, /v1 included
76
+ api: openai
77
+ ```
78
+
79
+ `chi self` shows the model and host chi will use. Other settings (multiple hosts, transports, timeouts, the idle recap) are in
48
80
  [Configuration](docs/configuration.md).
49
81
 
50
82
  ## Use
@@ -58,11 +90,12 @@ chi web --open # web UI: this project's sessions (--s
58
90
  chi sessions list # this project's saved sessions (--scope=all: every one)
59
91
  pbpaste | chi note --source slack <id> # background context for a session (no turn)
60
92
  pbpaste | chi send -m "same bug?" <id> # a message to a session, the clipboard quoted above it
93
+ chi send --new --wait -m "review feat/x" # a new session you can watch in the web; prints the answer
61
94
  ```
62
95
 
63
96
  `@shot.png` in a prompt (or a pasted/dropped image in the web UI) shows the model an image, when it can see them; see [Images](docs/cli.md#images).
64
97
 
65
- `/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.
98
+ `/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.
66
99
 
67
100
  ### Context notes
68
101
 
@@ -78,7 +111,9 @@ reply (a normal session: `chi --attach <id>` steers it).
78
111
 
79
112
  `chi send` is the other half: the text goes in as your message, the same as
80
113
  typing it in the attached terminal or the web composer, and a turn runs. Piped
81
- stdin plus `-m` puts the stdin above the message as a `>` quote.
114
+ stdin plus `-m` puts the stdin above the message as a `>` quote. `chi send --new`
115
+ starts a session with the message instead (it shows in the web at once), and
116
+ `--wait` blocks and prints the answer.
82
117
 
83
118
  ### Send to chi (macOS)
84
119
 
@@ -119,6 +154,7 @@ once, for the session, for the repo, or for the whole rule in the repo.
119
154
  ```sh
120
155
  bundle exec rspec # Ruby specs
121
156
  npm test # web frontend specs
157
+ npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
122
158
  ```
123
159
 
124
160
  ## License
data/bin/chi CHANGED
@@ -38,16 +38,18 @@ if ARGV[0] == "sessions"
38
38
  sessions_args = ARGV[1..] || []
39
39
  sub = sessions_args[0]
40
40
  if sub.nil? || %w[-h --help help].include?(sub)
41
- puts "Usage: chi sessions <list|stop|delete|prune|clean> [options]"
41
+ puts "Usage: chi sessions <list|stop|archive|unarchive|delete|prune|clean> [options]"
42
42
  puts " list [--sort updated_at|created_at] [--order desc|asc] [--limit N]"
43
- puts " [--live] [--cwd PATH] [--format text|json|tsv]"
43
+ puts " [--live] [--cwd PATH] [--format text|json|tsv] [--archived]"
44
44
  puts " --live: sessions a worker runs now (the ones chi note reaches), 10 unless --limit"
45
45
  puts " --cwd PATH: sessions in PATH or below; json/tsv (id<TAB>description) are for scripts"
46
46
  puts " [--scope=all]: every project's sessions; by default only this git project's (all outside a repo)"
47
+ puts " --archived: archived sessions too (marked [archived]; json: archived: true)"
47
48
  puts " stop ID... # stop each session's worker (IDs or unique prefixes); chi --resume ID then starts a fresh one"
49
+ puts " archive ID... # hide sessions (and their delegates) from every list and keep them for good; unarchive ID... brings them back"
48
50
  puts " delete [--force] ID... # delete sessions for good (IDs or unique prefixes); --force stops a live worker first"
49
51
  puts " prune [--dry-run] [--days N] [--keep N] [--keep-status running,...] [--test-only]"
50
- puts " clean [--dry-run] [--days N] # test sessions (SAMAGOTCHI_ENV=test, CI): all of them, or those older than N days"
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"
51
53
  puts "Defaults: days=14 keep=500 keep_status=running (env overrides: SAMAGOTCHI_SESSION_RETENTION_DAYS etc.)"
52
54
  exit 0
53
55
  end
@@ -60,6 +62,7 @@ if ARGV[0] == "sessions"
60
62
  order = nil
61
63
  limit = nil
62
64
  live = sessions_args.include?("--live")
65
+ include_archived = sessions_args.include?("--archived")
63
66
  cwd = nil
64
67
  format = nil
65
68
  scope = nil
@@ -89,6 +92,9 @@ if ARGV[0] == "sessions"
89
92
  when "delete"
90
93
  require "samagotchi/session_delete_command"
91
94
  exit Samagotchi::SessionDeleteCommand.new(sessions_args[1..]).run
95
+ when "archive", "unarchive"
96
+ require "samagotchi/session_archive_command"
97
+ exit Samagotchi::SessionArchiveCommand.new(sub, sessions_args[1..]).run
92
98
  when "list"
93
99
  unless format.nil? || %w[text json tsv].include?(format)
94
100
  warn "Unknown format #{format.inspect}: use --format text|json|tsv"
@@ -107,7 +113,8 @@ if ARGV[0] == "sessions"
107
113
  # and test runs stay out.
108
114
  if live || cwd || (format && format != "text")
109
115
  summaries = Samagotchi::SessionManager.session_summaries(
110
- live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: false, project_root: project
116
+ live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: false, project_root: project,
117
+ include_archived: include_archived
111
118
  )
112
119
  case format
113
120
  when "json"
@@ -115,7 +122,11 @@ if ARGV[0] == "sessions"
115
122
  # or nil; recap: the saved recap's first sentence, or nil; project:
116
123
  # its git project's root, or nil
117
124
  # parent_id: the session that delegated it (the delegate tool), or nil
118
- keys = %i[id short_id desc cwd project updated_at live busy owner recap parent_id]
125
+ # archived: hidden from the lists (only with --archived can it be true)
126
+ # scratch: a `chi scratch` session (deleted when its REPL ends, a
127
+ # leftover one at the next sweep)
128
+ # ctx_pct: how full the context was after the last turn, or nil
129
+ keys = %i[id short_id desc cwd project updated_at live busy owner recap parent_id archived scratch ctx_pct]
119
130
  puts JSON.generate(summaries.map { |summary| summary.slice(*keys) })
120
131
  when "tsv"
121
132
  summaries.each { |summary| puts "#{summary[:id]}\t#{summary[:desc]}" }
@@ -131,7 +142,10 @@ if ARGV[0] == "sessions"
131
142
  end
132
143
  # A delegated session points at its parent.
133
144
  child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
134
- puts "#{summary[:id]} #{state.ljust(8)} #{summary[:updated_at]} #{desc}#{child}"
145
+ flag = summary[:scratch] ? " [scratch]" : ""
146
+ flag += " [archived]" if summary[:archived]
147
+ ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
148
+ puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
135
149
  end
136
150
  if project
137
151
  puts "#{summaries.empty? ? "" : "\n"}#{scope_note.call(summaries.size)}"
@@ -143,7 +157,8 @@ if ARGV[0] == "sessions"
143
157
  end
144
158
  sort ||= "updated_at"
145
159
  order ||= "desc"
146
- sessions = Samagotchi::SessionManager.list_sessions(sort: sort, order: order, limit: limit, project_root: project)
160
+ sessions = Samagotchi::SessionManager.list_sessions(sort: sort, order: order, limit: limit, project_root: project,
161
+ include_archived: include_archived)
147
162
  # The saved recap's first sentence, else the last prompt, cut as before.
148
163
  state_dir = Samagotchi::Session.default_state_dir
149
164
  list_text = lambda do |s|
@@ -152,9 +167,14 @@ if ARGV[0] == "sessions"
152
167
  end
153
168
  # A delegated session points at its parent: ↳ <parent's short id>.
154
169
  row = lambda do |s|
155
- flag = s.test_run ? " [test]" : ""
170
+ flag = s.scratch ? " [scratch]" : (s.test_run ? " [test]" : "")
171
+ flag += " [archived]" if s.archived
156
172
  child = s.parent_id ? " ↳ #{s.parent_id[0, 8]}" : ""
157
- "#{s.id} #{s.status.ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
173
+ # How full the context was after the last turn: "ctx 12%", blank when unknown.
174
+ ctx = Samagotchi::SessionMetrics.context_label(
175
+ Samagotchi::SessionMetrics.saved_context_pct(Samagotchi::Session.session_dir(s.id, state_dir: state_dir))
176
+ )
177
+ "#{s.id} #{s.status.ljust(8)} #{ctx.ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
158
178
  end
159
179
  if project
160
180
  sessions.each { |s| puts row.call(s) }
@@ -214,7 +234,7 @@ if ARGV[0] == "sessions"
214
234
  end
215
235
  exit 0
216
236
  else
217
- warn "Unknown sessions subcommand: #{sub}. Use: list, stop, delete, prune, clean"
237
+ warn "Unknown sessions subcommand: #{sub}. Use: list, stop, archive, unarchive, delete, prune, clean"
218
238
  exit 1
219
239
  end
220
240
  end
@@ -231,6 +251,12 @@ if ARGV[0] == "send"
231
251
  exit Samagotchi::SendCommand.new(ARGV[1..] || []).run
232
252
  end
233
253
 
254
+ # `chi bootstrap`: find a model server and write config.yml, before OptionParser (--model, --name are its own)
255
+ if ARGV[0] == "bootstrap"
256
+ require "samagotchi/bootstrap_command"
257
+ exit Samagotchi::BootstrapCommand.new(ARGV[1..] || []).run
258
+ end
259
+
234
260
  # `chi desktop`: the native "Send to chi" helper, before OptionParser (its own flags)
235
261
  if ARGV[0] == "desktop"
236
262
  require "samagotchi/desktop_command"
@@ -881,7 +907,13 @@ if ARGV[0] == "bundle"
881
907
  exit 0
882
908
  end
883
909
 
884
- options = { verbose: false, no_interrupt: false, non_interactive: false, no_default_input: false, web: false, web_port: nil, web_open: false, web_markdown: nil, web_turn_view: nil }
910
+ # `chi scratch`: the run options below, for a session that leaves nothing
911
+ # behind (TerminalUI scratch:). The word goes before OptionParser sees it.
912
+ scratch = ARGV[0] == "scratch"
913
+ ARGV.shift if scratch
914
+
915
+ # no_interrupt / no_default_input start from config.yml or the env; the flags below turn them on.
916
+ 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 }
885
917
  cli_overrides = {}
886
918
 
887
919
  if ARGV.any? { |arg| arg == "--backend" || arg.start_with?("--backend=") }
@@ -908,10 +940,12 @@ end
908
940
  parser = OptionParser.new do |opts|
909
941
  opts.banner = <<~BANNER
910
942
  Usage: chi [options]
943
+ chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
944
+ chi scratch [options] a one-time session in this terminal: nothing is kept
911
945
  chi web [--port PORT] [--open] [--scope=all] the web UI
912
- chi sessions <list|stop|delete|prune|clean>
946
+ chi sessions <list|stop|archive|unarchive|delete|prune|clean>
913
947
  chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
914
- chi send [-m TEXT] ID... a message into sessions, as if typed there
948
+ chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
915
949
  chi bundle <install|upgrade|uninstall|status|diff|list|build>
916
950
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
917
951
  chi self version, paths, model and bundles
@@ -938,7 +972,8 @@ parser = OptionParser.new do |opts|
938
972
  "every session outside a repo) or all") { |v| options[:scope] = v }
939
973
  # Accepted values of string entries that the registry doesn't type as enums.
940
974
  value_hints = { "thinking.ui" => "spinner|off", "status.line" => "on|off", "status.width_mode" => "terminal_cap|fixed",
941
- "web.host" => "127.0.0.1|::1|localhost", "recap.sentences" => "N|N-M" }
975
+ "web.host" => "127.0.0.1|::1|localhost", "recap.sentences" => "N|N-M",
976
+ "web.annotate_presets" => "text|text" }
942
977
  # Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
943
978
  # Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
944
979
  Samagotchi::Config.cli_entries.each do |entry|
@@ -981,12 +1016,22 @@ end
981
1016
  # The subcommands with their own parsers ran above; what parse! leaves is
982
1017
  # `web` or nothing. Anything else is a typo, not a prompt: refuse it rather
983
1018
  # than start a session.
984
- stray = ARGV.first == "web" ? ARGV[1] : ARGV.first
1019
+ web = ARGV.first == "web" && !scratch
1020
+ stray = web ? ARGV[1] : ARGV.first
985
1021
  if stray
986
- warn "Error: #{ARGV.first == "web" ? "unexpected argument" : "unknown command"} #{stray} (see chi --help)"
1022
+ warn "Error: #{web || scratch ? "unexpected argument" : "unknown command"} #{stray} (see chi --help)"
987
1023
  exit 1
988
1024
  end
989
1025
 
1026
+ # A scratch session is new, runs here and goes when it ends.
1027
+ if scratch
1028
+ flag = { resume: "--resume", attach: "--attach", shared: "--shared" }.find { |key, _flag| options[key] }&.last
1029
+ if flag
1030
+ warn "Error: chi scratch starts a new session in this terminal and keeps nothing; it can't take #{flag}"
1031
+ exit 1
1032
+ end
1033
+ end
1034
+
990
1035
  # Apply CLI precedence: CLI > ENV > file
991
1036
  unless cli_overrides.empty?
992
1037
  Samagotchi::Config.reload!(cli_overrides: cli_overrides)
@@ -1039,7 +1084,8 @@ if options[:web]
1039
1084
  turn_view = options[:web_turn_view]
1040
1085
  turn_view = Samagotchi::Config.get("web.turn_view") if turn_view.nil?
1041
1086
  exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
1042
- markdown: markdown, turn_view: turn_view)
1087
+ markdown: markdown, turn_view: turn_view,
1088
+ annotate_presets: Samagotchi::Config.get("web.annotate_presets"))
1043
1089
  end
1044
1090
 
1045
1091
  # --resume and --attach take a unique id prefix too, like git.
@@ -1050,6 +1096,22 @@ rescue Samagotchi::Session::AmbiguousId => e
1050
1096
  exit 1
1051
1097
  end
1052
1098
 
1099
+ # A `chi scratch` session a killed REPL left behind would turn into a kept
1100
+ # one if opened again; the next sweep deletes it.
1101
+ %i[resume attach].each do |key|
1102
+ next unless options[key]
1103
+
1104
+ leftover = begin
1105
+ Samagotchi::Session.load(options[key]).scratch
1106
+ rescue ArgumentError
1107
+ false # no such session: the launch says so
1108
+ end
1109
+ next unless leftover
1110
+
1111
+ warn "Error: that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
1112
+ exit 1
1113
+ end
1114
+
1053
1115
  # --memory and --mute names are checked here, before the launch: a worker's
1054
1116
  # stderr goes nowhere, so its engine's warnings would not be seen. Warnings
1055
1117
  # only; the session starts either way.
@@ -1082,7 +1144,11 @@ if options[:memories] || options[:muted]
1082
1144
  end
1083
1145
 
1084
1146
  require "samagotchi/launch_mode"
1085
- launch, launch_note = Samagotchi::LaunchMode.resolve(options, shared_config: Samagotchi::Config.get("session.shared"))
1147
+ launch, launch_note = if scratch
1148
+ [:repl, nil]
1149
+ else
1150
+ Samagotchi::LaunchMode.resolve(options, shared_config: Samagotchi::Config.get("session.shared"))
1151
+ end
1086
1152
  warn launch_note if launch_note
1087
1153
 
1088
1154
  if launch == :attached
@@ -1124,7 +1190,8 @@ begin
1124
1190
  memories: options[:memories] || [],
1125
1191
  muted_memories: options[:muted] || [],
1126
1192
  non_interactive: options[:non_interactive],
1127
- model_name: options[:model]
1193
+ model_name: options[:model],
1194
+ scratch: scratch
1128
1195
  ).run
1129
1196
  rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
1130
1197
  warn "Error: #{e.message}"
data/docs/cli.md CHANGED
@@ -2,22 +2,60 @@
2
2
 
3
3
  ## Commands
4
4
 
5
+ - `chi bootstrap [HOST[:PORT]|URL]` — first setup: find the model server (llama.cpp or OpenAI-compatible), pick the model, send a test request and write config.yml, or add a `hosts:` entry to an existing one (see [First setup](#first-setup))
5
6
  - `chi` — start a session in a background worker and attach the terminal to it, so the Web UI (or another terminal) can share it (see [Sharing a session](#sharing-a-session))
6
7
  - `chi -p "your prompt"` — run a prompt, then stay attached
7
8
  - `chi -p "your prompt" --non-interactive` — run a prompt, print the answer, exit
8
9
  - `chi --resume <session-id>` — resume a prior session (in its worker)
9
10
  - `chi --no-shared [--resume <session-id>]` — the plain in-process REPL instead, for this run
11
+ - `chi scratch [options]` — a one-time session in the plain in-process REPL, in this folder, that leaves nothing behind (see [Scratch sessions](#scratch-sessions))
10
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
11
- - A session id can be shortened to any unique prefix (like git): `chi --attach 2ea8`. `--resume`, `--attach`, `sessions stop` and `sessions delete` take one; an ambiguous prefix lists the sessions it matches.
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.
12
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"
13
15
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages
14
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))
15
- - `chi sessions list|stop|delete|prune|clean` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>` (see [Sessions](sessions.md))
17
+ - `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))
16
18
  - `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))
17
- - `chi send [-m TEXT] (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 (see [Sessions: Sending a message](sessions.md#sending-a-message))
19
+ - `chi send [-m TEXT] (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 (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))
18
20
  - `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))
19
21
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles
20
- - `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) and [the loop-guard bundle](plugins.md#the-loop-guard-bundle))
22
+ - `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))
23
+
24
+ ### First setup
25
+
26
+ `chi bootstrap TARGET` names the model server and writes the config for it:
27
+
28
+ ```sh
29
+ chi bootstrap 192.168.1.29:8081 # host:port (port 8080 when none)
30
+ chi bootstrap https://openrouter.ai/api/v1 --key-env OPENROUTER_API_KEY
31
+ chi bootstrap # try localhost 8080, 11434, 1234, 8000
32
+ ```
33
+
34
+ - **What it is.** llama.cpp's `/props` answering means the native API (no
35
+ `api:`); otherwise `GET /v1/models` answering means an OpenAI-compatible
36
+ server (`api: openai`). A URL with a path is the API base as given
37
+ (`…/api/v1`); a domain without a scheme is tried over https, then http.
38
+ Each request is tried once, with 5 s timeouts, so a refused port answers
39
+ at once.
40
+ - **The key.** A server that answers 401/403 wants an API key: `--key-env VAR`
41
+ names the environment variable holding it (on a terminal chi asks for the
42
+ name). Only the variable's name is written, never the key.
43
+ - **The model.** One model is taken; with several, `--model ID` picks one
44
+ (a terminal gets a numbered list, a script the ids and exit 2). A llama.cpp
45
+ server also shows its context size and the prompt profile its chat template
46
+ matches.
47
+ - **The test.** One short chat request ("test: answered in 1.1 s"); `--no-test`
48
+ skips it. A failed test still writes the config, says so and exits 1.
49
+ - **The file.** With no config.yml it writes a small commented one:
50
+ `default.model` as `<host>:<model>` and one `hosts:` entry named `local`
51
+ (localhost), `lan` (an IP) or after the domain (`openrouter`); `--name`
52
+ sets it. An existing file is left as it is apart from the new entry, added
53
+ at the end of its `hosts:` block (it gets a `hosts:` block, with a
54
+ `default` entry for its `server:` first, when it has none), after a backup
55
+ to `config.yml.bak-<time>`. `default.model` is set only when the file has
56
+ none. A server already in the file writes nothing; a file in YAML flow style
57
+ or with anchors gets the lines printed to paste instead. `--dry-run` shows
58
+ what it would write.
21
59
 
22
60
  ## Flags
23
61
 
@@ -64,6 +102,7 @@ it sees one.
64
102
  | `chi --resume ID -p "next step" --non-interactive` | Resume `ID`, run the prompt, save, exit. |
65
103
  | `chi --resume ID -p "next step"` | Resume `ID`, send the prompt, **stay attached** to that session. |
66
104
  | `chi --no-shared [...]` | The same, in the plain in-process REPL. |
105
+ | `chi scratch [-p ...] [--non-interactive]` | A new session in the plain REPL, deleted when it ends. |
67
106
 
68
107
  Notes:
69
108
 
@@ -75,6 +114,28 @@ Notes:
75
114
  - Non-interactive runs (`-p` with `--non-interactive`, or bare `--non-interactive`)
76
115
  print only the final result output — no spinner, status line, or REPL.
77
116
 
117
+ ### Scratch sessions
118
+
119
+ `chi scratch` is `chi --no-shared` for a session you won't keep: a quick
120
+ question, a try-out. It takes the run options (`-p`, `--non-interactive`,
121
+ `--model`, `--profile`, `--memory`, `--mute`, `-v`, …); `--resume`, `--attach`
122
+ and `--shared` are refused. Its first line says it is a scratch session.
123
+
124
+ - The session is deleted however it ends: `/exit`, Ctrl-D, Ctrl-C at the
125
+ prompt, an error, SIGTERM or SIGHUP. There is no recap, and the lines typed
126
+ are not added to the prompt history.
127
+ - It never shows in `chi web`. A process killed with `kill -9` leaves its
128
+ session behind, marked `"scratch": true` in its session.json: `chi sessions
129
+ list` shows it as `[scratch]`, and the next sweep or `chi sessions clean`
130
+ deletes it. `chi --resume` and `--attach` refuse it (exit 1), so it never
131
+ turns into a kept session.
132
+ - Memories are read and preloaded as usual, but nothing is saved: `memory_write`
133
+ answers "scratch session: nothing is saved", and `write`/`edit` into the
134
+ memories folder are denied (a guardrail, rule `scratch-session`). `execute`
135
+ can still write files anywhere, memories included.
136
+ - No child sessions: the `delegate` tools are not offered, and a plugin's
137
+ `ctx.sessions.fork` (btw's side session) refuses, since they would outlive it.
138
+
78
139
  ### Sharing a session
79
140
 
80
141
  Plain `chi` runs the session in a background worker and attaches the terminal
@@ -122,7 +183,8 @@ A worker nobody uses exits after `session.idle_exit_minutes` (30 by default, `0`
122
183
  for never): no turn running or queued, no UI attached (an open web tab or an
123
184
  attached terminal counts, even an idle one) and no reminder registered. The next
124
185
  prompt or `--attach` wakes a new worker with the conversation intact; `/stats`
125
- counters start over (the recap is saved with the session).
186
+ keeps counting from the turns before (they are saved in the session's
187
+ `analytics.json`, one record per turn), and the recap is saved with the session.
126
188
 
127
189
  A session you leave with nothing in it (no prompt sent, no `/model` switch, no
128
190
  note or image) is deleted as its worker exits, and `/exit` says so; set
@@ -134,6 +196,21 @@ a `chi --resume ID` after it starts a fresh one. A worker still running an
134
196
  older chi (from before an upgrade) takes turns but not commands; the attached
135
197
  terminal and the Web UI say so, with that restart line.
136
198
 
199
+ `chi sessions archive ID...` hides sessions from every list (the terminal's,
200
+ the web's, `list_sessions`) and keeps them for good: the retention sweep never
201
+ deletes an archived session, nor counts it. Its delegates go with it. A live
202
+ worker is stopped first; a session running a turn (or with a delegate running
203
+ one), open in a plain REPL, or a `chi scratch` one is refused. `chi sessions
204
+ list --archived` shows them too, marked `[archived]` (`archived: true` in
205
+ `--format json`); `chi sessions unarchive ID...` brings them back, and so does
206
+ a message you send to one (the web, an attached terminal, `chi send`), but not
207
+ a delegate's follow-up or a reminder. The web archives from the info bar
208
+ (`archive`, before `stop`); "include archived" by the all-sessions search
209
+ finds archived sessions. `/archive` in a terminal leaves the session and
210
+ archives it (an empty session is discarded instead; `chi scratch` refuses
211
+ it). See
212
+ [Sessions](sessions.md#archiving-a-session).
213
+
137
214
  `chi sessions delete [--force] ID...` deletes sessions for good: the
138
215
  session file and its whole directory (notes, images, queued input). Each id
139
216
  (or unique prefix) gets one line: `deleted`, or `refused` with the reason. A
@@ -316,6 +393,32 @@ and `?view=turn` the turn view, whatever the config says; the parameter is dropp
316
393
  when you switch between the project and all-sessions views. The terminal
317
394
  UIs are not affected.
318
395
 
396
+ ### Web annotate presets
397
+
398
+ Selecting text in an answer, a thinking block, a tool row or one of your
399
+ messages shows **Annotate**, which quotes the selection into the composer
400
+ for a note under it. Next to it sit quick replies, by default `Agreed` and
401
+ `Could you please elaborate?`: a click quotes the selection the same way
402
+ with that text already written as the note. It only fills the composer,
403
+ never sends, so you can collect several quotes and edit before sending.
404
+
405
+ The list is `web.annotate_presets`, `|`-separated (at most five; a preset
406
+ can't contain `|`; a YAML list works too):
407
+
408
+ ```sh
409
+ chi web --web-annotate-presets "Yes|No|Why this way?"
410
+ chi web --web-annotate-presets "" # only Annotate
411
+ ```
412
+
413
+ ```yaml
414
+ web:
415
+ annotate_presets: "Agreed|Could you please elaborate?"
416
+ ```
417
+
418
+ `SAMAGOTCHI_WEB_ANNOTATE_PRESETS` overrides the file, but an empty value
419
+ there means the default, not "none": use `""` in the file or on the command
420
+ line. A `chi web` that already runs keeps its list; restart it.
421
+
319
422
  ## Runtime Model Switch (Assist Mode)
320
423
 
321
424
  In interactive assist mode, you can switch the request model without restarting: