ollama_chat 0.0.113 → 0.0.114

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +81 -0
  3. data/README.md +7 -0
  4. data/bin/ollama_chat_log +1 -1
  5. data/bin/ollama_chat_send +3 -1
  6. data/lib/ollama_chat/chat.rb +30 -14
  7. data/lib/ollama_chat/commands.rb +36 -18
  8. data/lib/ollama_chat/compaction.rb +289 -0
  9. data/lib/ollama_chat/config_handling.rb +32 -0
  10. data/lib/ollama_chat/database/migrations/010_add_enabled_to_collections.rb +7 -0
  11. data/lib/ollama_chat/database/models/app_state.rb +48 -16
  12. data/lib/ollama_chat/database/models/session.rb +1 -2
  13. data/lib/ollama_chat/database.rb +4 -2
  14. data/lib/ollama_chat/information.rb +53 -6
  15. data/lib/ollama_chat/logging.rb +3 -2
  16. data/lib/ollama_chat/message.rb +22 -1
  17. data/lib/ollama_chat/message_format.rb +1 -2
  18. data/lib/ollama_chat/message_list.rb +223 -9
  19. data/lib/ollama_chat/model_handling.rb +1 -1
  20. data/lib/ollama_chat/oc.rb +17 -7
  21. data/lib/ollama_chat/ollama_chat_config/default_config.yml +68 -3
  22. data/lib/ollama_chat/parsing.rb +2 -1
  23. data/lib/ollama_chat/prompt_handling.rb +1 -1
  24. data/lib/ollama_chat/prompt_management.rb +1 -0
  25. data/lib/ollama_chat/rag_handling.rb +19 -5
  26. data/lib/ollama_chat/session_management.rb +5 -3
  27. data/lib/ollama_chat/token_estimator/crude.rb +1 -1
  28. data/lib/ollama_chat/token_estimator.rb +1 -1
  29. data/lib/ollama_chat/tools/concern.rb +17 -0
  30. data/lib/ollama_chat/tools/directory_structure.rb +11 -7
  31. data/lib/ollama_chat/tools/eval_ruby.rb +16 -6
  32. data/lib/ollama_chat/tools/get_rfc.rb +2 -0
  33. data/lib/ollama_chat/tools/lookup_group.rb +100 -0
  34. data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
  35. data/lib/ollama_chat/tools/patch_file.rb +23 -5
  36. data/lib/ollama_chat/tools/read_file.rb +1 -1
  37. data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
  38. data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
  39. data/lib/ollama_chat/tools/write_file.rb +25 -6
  40. data/lib/ollama_chat/tools.rb +1 -0
  41. data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
  42. data/lib/ollama_chat/utils/fetcher.rb +1 -1
  43. data/lib/ollama_chat/utils/log_rotation.rb +29 -0
  44. data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
  45. data/lib/ollama_chat/utils.rb +1 -0
  46. data/lib/ollama_chat/version.rb +1 -1
  47. data/lib/ollama_chat.rb +7 -0
  48. data/ollama_chat.gemspec +5 -5
  49. data/spec/ollama_chat/commands_spec.rb +25 -0
  50. data/spec/ollama_chat/compaction_spec.rb +381 -0
  51. data/spec/ollama_chat/config_handling_spec.rb +60 -0
  52. data/spec/ollama_chat/message_list_spec.rb +229 -0
  53. data/spec/ollama_chat/message_spec.rb +143 -0
  54. data/spec/ollama_chat/rag_handling_spec.rb +3 -0
  55. data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
  56. data/spec/ollama_chat/tools/browse_spec.rb +7 -0
  57. data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
  58. data/spec/ollama_chat/tools/concern_spec.rb +77 -0
  59. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
  60. data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
  61. data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
  62. data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
  63. data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
  64. data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
  65. data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
  66. data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
  67. data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
  68. data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
  69. data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
  70. data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
  71. data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
  72. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
  73. data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
  74. data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
  75. data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
  76. data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
  77. data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
  78. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
  79. data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
  80. data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
  81. data/spec/ollama_chat/tools/read_file_spec.rb +7 -0
  82. data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
  83. data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
  84. data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
  85. data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
  86. data/spec/ollama_chat/tools/write_file_spec.rb +96 -0
  87. data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
  88. data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
  89. data/spec/spec_helper.rb +2 -1
  90. metadata +19 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e618b797bf1b5fa84728816e06b83fe3540424b48256ba107deb29c2beb8a219
4
- data.tar.gz: f5abee2701f3fc6d7ba09513f0b0bd5133c7ec10ddb511ee5ea4238c14f9988e
3
+ metadata.gz: e5e93e610536ac396d18e929ef76b3dcecac7d3b9d88a35b8d6ff141e7eba8e4
4
+ data.tar.gz: a7792a3b397ebcc0d88552f0fa1417933f44ff00c6e0fad4eb62d567b5c98bfd
5
5
  SHA512:
6
- metadata.gz: 2f2172cc90da0a8b8c1b7f2306abbc8480db92abeb3629e20b2471e88fdd79ca2d18c209c05690167b564b6911685222dce48496e9825330e43500ff520aab74
7
- data.tar.gz: da6e5e8583b11cb7d766ff0745e97070f2a39ac8f1ca6314a4bcb93d8e9a6c3ec9ef087a055b7c9844317f831fc4a6a590e11f80eaf6b6c80980143ab8385771
6
+ metadata.gz: 95a81969d48e98ff06623f887c33bd0acf94452639fabbf4eca30127ef4d300124f9e12d7bbb97a3d8d86263116d3c772e5d9c403d1e4f4cbdc2df241e1f719f
7
+ data.tar.gz: 92201492f3a4a084c845d11f7957b6af6ea8630e7f242158458cb1bad059bababb8699f0dc2f24204ddfde38bcd47e8491812e03d5f38128f179b443f858a9dc
data/CHANGES.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-09-01 v0.0.114
4
+
5
+ ### Added
6
+
7
+ * Context compaction pipeline:
8
+ * `OllamaChat::Compaction` module with `summarize_for_compaction`,
9
+ `compact_with_retry`, `compact_ratio_tokens`, `tool_summary_line`, and
10
+ `Result` value object.
11
+ * `CompactionError` exception.
12
+ * `MessageList` methods: `compact!`, `find_cut_point`, `find_summary`,
13
+ `compacted_messages`, `compacted_estimate_tokens`, and
14
+ `full_estimate_tokens`.
15
+ * `lookup_group` tool for retrieving pre-summary group content by UUID
16
+ suffix.
17
+ * `summary_template` base method in `Tools::Concern` and `message` field in
18
+ `eval_ruby` and `get_rfc` results.
19
+ * `compaction:` configuration block (`enabled`, `reserve`, `keep_recent`) and
20
+ `prompts.compaction` templates in `default_config.yml`.
21
+ * `conversation_length` in `Information` and dynamic runtime values.
22
+ * Per-message token estimation:
23
+ * `#token_estimate(strip_thinking:)` in `OllamaChat::MessageMixin`.
24
+ * `#total_tokens(strip_thinking:)` in `OllamaChat::MessageList`.
25
+ * Color-coded context gauge:
26
+ * `context_gauge` method in `information.rb` displaying context usage with
27
+ ANSI colors and emoji indicators.
28
+ * Configurable syntax checkers for file tools:
29
+ * `syntax_checkers:` block in `default_config.yml` for `ruby`, `javascript`,
30
+ `python`, and `shell`.
31
+ * `ConfigHandling#syntax_checker_for` and `ConfigHandling#run_syntax_check`
32
+ methods.
33
+ * Integration into `Tools::WriteFile#execute` and `Tools::PatchFile#execute`.
34
+ * Log rotation and restructured log configuration:
35
+ * `LOG` module (`CHAT`, `DATABASE`, `TAIL_LINES`) in `oc.rb`.
36
+ * `OllamaChat::Utils::LogRotation.truncate_tail` for in-place log truncation.
37
+ * `truncate_logs` call in `OllamaChat::Chat#initialize`.
38
+ * Database `Logger` level set to `WARN`.
39
+ * Enabled flag for RAG collections:
40
+ * Migration adding `enabled` boolean to `collections` table.
41
+ * Filtering of disabled collections in `information.rb` and
42
+ `search_knowledge`.
43
+ * Interactive toggle in `edit_collection` and `[DISABLED]` suffix in
44
+ `list_collections`.
45
+ * Monochrome flags:
46
+ * `-m` flag for `bin/ollama_chat_send` to strip ANSI codes from stdin.
47
+ * `-m` flag for `/input` command to strip ANSI codes from file content and
48
+ import results.
49
+ * Session synchronization:
50
+ * `session_sync` method extracted from `session_close` in
51
+ `session_management.rb`.
52
+ * `ensure session.lock` in `set_new_session`.
53
+
54
+ ### Changed
55
+
56
+ * Increased `compaction.reserve.ratio` from **0.30** to **0.60** in
57
+ `default_config.yml`.
58
+ * Switched `Struct.new` to `Data.define` in `ModelMetadata`, `Estimate`, and
59
+ `TagResult`.
60
+ * Standardized user-facing output format to `tokens (bytes)` across
61
+ `read_file`, `write_file`, `session_management`, and `information`.
62
+ * Replaced `.size` with `.bytesize` for multi-byte-safe byte counts in
63
+ `token_estimator/crude.rb`, `fetcher.rb`, `paste_from_clipboard.rb`, and
64
+ `prompt_handling.rb`.
65
+ * Updated `/conversation compact` to delegate to `compact_with_retry`.
66
+ * Threaded `chat` into `TagResolver` for logging, fixing `NoMethodError` in
67
+ rescue blocks.
68
+ * Capped inline directory structure depth:
69
+ * Added `parsing.directory_structure_max_depth` (default **1**) to
70
+ `default_config.yml`.
71
+ * Shifted `max_depth` semantics: **1** = immediate children, **2** = children
72
+ + grandchildren, `nil` = unlimited.
73
+ * Acknowledged prompt state after `/prompt sync` to suppress boot drift nag.
74
+ * Reordered `session_title` prompt for reliability.
75
+ * `/config diff` now calls `reload_config` after `diff_config`.
76
+ * Reordered `/config` subcommand alternatives to `edit diff reload`.
77
+ * Clarified `suffix` description in `directory_structure` as "file extension".
78
+
79
+ ### Fixed
80
+
81
+ * `NoMethodError` in `TagResolver#resolve` rescue block.
82
+ * Unbounded tree expansion in directory structure parsing.
83
+
3
84
  ## 2026-08-25 v0.0.113
4
85
 
5
86
  * Simplified hash syntax by replacing `text: text` with `text:` etc.
data/README.md CHANGED
@@ -37,6 +37,9 @@ The following environment variables can be used to configure behavior:
37
37
  - `OLLAMA_CHAT_HISTORY` - Chat history filename
38
38
  - `OLLAMA_CHAT_USER` - Full name of the chat user
39
39
  - `OLLAMA_CHAT_DEBUG` - Debug mode toggle (1 = enabled)
40
+ - `OLLAMA_CHAT_LOG_CHAT` - Chat log file path (default: `$XDG_STATE_HOME/chat.log`)
41
+ - `OLLAMA_CHAT_LOG_DATABASE` - Database log file path (default: `$XDG_STATE_HOME/database.log`)
42
+ - `OLLAMA_CHAT_LOG_TAIL_LINES` - Max lines to retain in log files (default: `10000`)
40
43
 
41
44
  #### Tool Configuration
42
45
  - `OLLAMA_CHAT_TOOLS_TEST_RUNNER` - Configured test runner for `run_tests` (default: `rspec`)
@@ -45,11 +48,14 @@ The following environment variables can be used to configure behavior:
45
48
  - `OLLAMA_CHAT_TOOLS_JIRA_URL` - Base URL for Jira instance
46
49
  - `OLLAMA_CHAT_TOOLS_JIRA_USER` - Username for Jira authentication
47
50
  - `OLLAMA_CHAT_TOOLS_JIRA_API_TOKEN` - API token for Jira authentication
51
+ - `OLLAMA_CHAT_TOOLS_JIRA_TWG` - Path to the twg CLI binary
48
52
  - `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_URL` - Base URL for ComfyUI server
49
53
  - `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_WORKFLOW` - ComfyUI workflow as JSON string
50
54
  - `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_PROMPT_NODE_ID` - Prompt node ID
51
55
  - `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_FILENAME_PREFIX_NODE_ID` - Filename prefix node ID
52
56
  - `OLLAMA_CHAT_TOOLS_PIRATEWEATHER_API_KEY` - Pirate Weather API key
57
+ - `OLLAMA_CHAT_TOOLS_GHR_URL` - Base URL for GHR API server
58
+ - `OLLAMA_CHAT_TOOLS_RUBY_EVAL_IMAGE_TEMPLATE` - Docker image template for `eval_ruby` (default: `ruby:%{version}-alpine`)
53
59
 
54
60
  #### System & Infrastructure
55
61
  - `XDG_CONFIG_HOME` - XDG Configuration home directory
@@ -59,6 +65,7 @@ The following environment variables can be used to configure behavior:
59
65
  - `EDITOR` - Default text editor
60
66
  - `BROWSER` - Default web browser
61
67
  - `DIFF_TOOL` - Tool for diff operations (default: `vimdiff`)
68
+ - `DIFF_COMMAND` - Command to generate unified diffs (default: `diff -u --color=always`)
62
69
  - `KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES` - Custom ANSI styles for Markdown formatting
63
70
  - `KRAMDOWN_ANSI_STYLES` - Fallback ANSI styles configuration
64
71
  - `OLLAMA_REDIS_URL` - Redis connection URL for documents (documentrix)
data/bin/ollama_chat_log CHANGED
@@ -5,7 +5,7 @@ require 'file/tail'
5
5
  require 'tins/xt'
6
6
 
7
7
  opts = Tins::GO.go 'fm:n:h:', defaults: { ?n => 10 }
8
- log_path = ARGV.shift || OC::OLLAMA::CHAT::LOGFILE
8
+ log_path = ARGV.shift || OC::OLLAMA::CHAT::LOG::CHAT
9
9
 
10
10
  if opts[?h]
11
11
  puts <<~EOT
data/bin/ollama_chat_send CHANGED
@@ -5,7 +5,7 @@ include OllamaChat::FileEditing
5
5
  require 'tins/go'
6
6
  include Tins::GO
7
7
 
8
- opts = go 'f:d:F:rteph', ARGV
8
+ opts = go 'f:d:F:rtemph', ARGV
9
9
 
10
10
  # Displays the usage information for the ollama_chat_send command.
11
11
  #
@@ -25,6 +25,7 @@ def usage(rc = 0)
25
25
  -f CONFIG file to read
26
26
  -d DIR the working directory to derive the socket file from
27
27
  -e Edit content in editor before sending
28
+ -m Strip ANSI color codes from content before sending
28
29
  -F FORMAT file extension for editor (e.g., rb, md)
29
30
  -h Show this help message
30
31
 
@@ -42,6 +43,7 @@ begin
42
43
  opts[?r] ? :socket_input_with_response : :socket_input
43
44
  end
44
45
  content = STDIN.read
46
+ content = Term::ANSIColor.uncolor(content) if opts[?m]
45
47
  if opts[?e]
46
48
  basename = %w[ text ]
47
49
  basename << opts[?F]&.sub(/\A(?=[^.])/, ?.) || '.md'
@@ -69,6 +69,7 @@ class OllamaChat::Chat
69
69
  include OllamaChat::PromptHandling
70
70
  include OllamaChat::SystemPromptManagement
71
71
  include OllamaChat::PromptManagement
72
+ include OllamaChat::Compaction
72
73
  include OllamaChat::Utils::Chooser
73
74
  include OllamaChat::Utils::ValueFormatter
74
75
  include OllamaChat::Utils::UTF8Converter
@@ -106,10 +107,11 @@ class OllamaChat::Chat
106
107
  @opts[?V] and exit version
107
108
  @ollama_chat_config = OllamaChat::OllamaChatConfig.new(@opts[?f])
108
109
  self.config = @ollama_chat_config.config
110
+ truncate_logs
109
111
  @messages = OllamaChat::MessageList.new(self)
110
112
  OllamaChat::Database.setup_models.each { _1.ask_and_send(:seed, self) }
111
- setup_session
112
113
  setup_switches
114
+ setup_session
113
115
  setup_state_selectors(config)
114
116
  connect_ollama
115
117
  @documents = setup_documents
@@ -254,12 +256,14 @@ class OllamaChat::Chat
254
256
  # and a user message, executing it as a one-shot chat interaction.
255
257
  #
256
258
  # @param system [String] the system prompt to guide the model's behavior
257
- # (defaults to current raw_system_prompt)
258
- #
259
+ # (defaults to current raw_system_prompt)
259
260
  # @param prompt [String] the user prompt to send to the model
261
+ # @param stream [Boolean] whether to stream the response (default: false)
262
+ # @param think [Boolean] enable thinking mode for hybrid-thinking models
263
+ # (default: false)
260
264
  #
261
265
  # @return [String] the content of the resulting response message
262
- def generate(system: raw_system_prompt, prompt:)
266
+ def generate(system: raw_system_prompt, prompt:, stream: false, think: false)
263
267
  messages = [
264
268
  OllamaChat::Message.new(
265
269
  role: 'system',
@@ -276,9 +280,8 @@ class OllamaChat::Chat
276
280
  model: @model,
277
281
  messages: ,
278
282
  options: model_options,
279
- stream: false,
280
- think: false,
281
- tools:
283
+ stream: ,
284
+ think:
282
285
  )&.message&.content.to_s
283
286
 
284
287
  if content.empty?
@@ -315,6 +318,18 @@ class OllamaChat::Chat
315
318
  ollama.chat(**opts, &block)
316
319
  end
317
320
 
321
+ # Truncates chat.log and database.log to the configured tail line count
322
+ # before any new logging begins for this session.
323
+ def truncate_logs
324
+ keep = OC::OLLAMA::CHAT::LOG::TAIL_LINES
325
+ OllamaChat::Utils::LogRotation.truncate_tail(
326
+ OC::OLLAMA::CHAT::LOG::CHAT, keep_lines: keep
327
+ )
328
+ OllamaChat::Utils::LogRotation.truncate_tail(
329
+ OC::OLLAMA::CHAT::LOG::DATABASE, keep_lines: keep
330
+ )
331
+ end
332
+
318
333
  # @return [Module] The module containing the database models.
319
334
  def models
320
335
  OllamaChat::Database::Models
@@ -532,14 +547,15 @@ class OllamaChat::Chat
532
547
  )
533
548
  begin
534
549
  retried = false
535
- sent_messages = messages.to_ary
536
- if think_strip.on?
537
- sent_messages = sent_messages.map {
538
- _1.dup.tap { |message|
539
- message.thinking = nil
540
- }
550
+ sent_messages = messages.compacted_messages
551
+ sent_messages = sent_messages.map {
552
+ _1.dup.tap { |message|
553
+ message.sender_name = nil
554
+ message.group_uuid = nil
555
+ message.tool_calls = nil
556
+ message.thinking = nil if think_strip.on?
541
557
  }
542
- end
558
+ }
543
559
  prepare_model(@model)
544
560
  call_ollama_chat(
545
561
  model: @model,
@@ -62,8 +62,8 @@ module OllamaChat::Commands
62
62
 
63
63
  command(
64
64
  name: :config,
65
- regexp: %r(^/config(?:\s+(edit|reload|diff))?$),
66
- complete: [ 'config', %w[ edit reload diff ] ],
65
+ regexp: %r(^/config(?:\s+(edit|diff|reload))?$),
66
+ complete: [ 'config', %w[ edit diff reload ] ],
67
67
  optional: true,
68
68
  help: <<~EOT
69
69
  \u2699\uFE0F View, edit, diff, or reload configuration
@@ -72,11 +72,11 @@ module OllamaChat::Commands
72
72
  case subcommand
73
73
  when 'edit'
74
74
  edit_config
75
- when 'reload'
76
- reload_config
77
75
  when 'diff'
78
76
  diff_config
79
77
  reload_config
78
+ when 'reload'
79
+ reload_config
80
80
  else
81
81
  display_config
82
82
  end
@@ -598,14 +598,15 @@ module OllamaChat::Commands
598
598
 
599
599
  command(
600
600
  name: :conversation,
601
- regexp: %r(^/conversation\s+(clean|save|load)(\s+-c)?(?:\s+([^-].*\.jsonl?))?$),
602
- complete: [ 'conversation', %w[ save load clean ] ],
601
+ regexp: %r(^/conversation\s+(clean|compact|save|load)(\s+-c)?(?:\s+([^-].*\.jsonl?))?$),
602
+ complete: [ 'conversation', %w[ save load clean compact ] ],
603
603
  options: '[-c] [FILENAME]',
604
604
  help: <<~EOT
605
605
  💾 Save/Load conversation state
606
606
  (-c to clean before saving)
607
607
  FILENAME ends with .json or .jsonl
608
608
  Or clean inplace (removes tool content, images, and thinking)
609
+ Or compact (summarize old messages, keep recent)
609
610
  EOT
610
611
  ) do |subcommand,opts,path|
611
612
  if %w[ save load ].include?(subcommand) && path.blank?
@@ -631,6 +632,16 @@ module OllamaChat::Commands
631
632
  else
632
633
  STDOUT.puts 'Cancelled.'
633
634
  end
635
+ when 'compact'
636
+ if confirm?(
637
+ prompt: '🔔 Compact conversation? Old messages will be summarized. (y/n) ',
638
+ yes: /\Ay/i
639
+ )
640
+ then
641
+ compact_with_retry
642
+ else
643
+ STDOUT.puts 'Cancelled.'
644
+ end
634
645
  end
635
646
  :next
636
647
  end
@@ -836,22 +847,23 @@ module OllamaChat::Commands
836
847
 
837
848
  command(
838
849
  name: :input,
839
- regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[apre]|c\s*#{OllamaChat::COLLECTION_NAME_REGEXP.source}|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*))*)(?:\s+([^-].*))?$),
850
+ regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[aprem]|c\s*#{OllamaChat::COLLECTION_NAME_REGEXP.source}|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*))*)(?:\s+([^-].*))?$),
840
851
  optional: true,
841
852
  complete: [ 'input', %w[ path context embedding summary ] ],
842
853
  options: <<~EOT,
843
- [
844
- -w|-a|-p|-e|
845
- -c <collection>|
846
- -t <tags>
847
- ]
854
+ [
855
+ -w|-a|-p|-e|-m|
856
+ -c <collection>|
857
+ -t <tags>
858
+ ]
848
859
  [arg…]
849
860
  EOT
850
861
  help: <<~EOT
851
862
  📥 Import content (read, summarize, embed, context)
852
863
  Subcommands: path, context, embedding, summary
853
- Options: -p (pattern), -w [words], -a (all),
854
- -c [collection], -t [tags], -e (edit)
864
+ Options: -p (pattern), -w [words], -a (all),
865
+ -c [collection], -t [tags], -e (edit),
866
+ -m (monochrome, strip ANSI from file content)
855
867
  EOT
856
868
  ) do |input_mode,opts,arg|
857
869
  disable_content_parsing
@@ -907,19 +919,21 @@ module OllamaChat::Commands
907
919
  end
908
920
  end
909
921
  when 'path'
910
- opts = go_command('pae', opts)
922
+ opts = go_command('paem', opts)
911
923
  if opts[?p]
912
924
  all = opts.fetch(?a, false)
913
925
  patterns = extract_patterns(arg)
914
926
  read = -> pathname {
915
927
  STDOUT.puts "Reading #{pathname.to_s.inspect}."
916
- pathname.read
928
+ content = pathname.read
929
+ opts[?m] ? Term::ANSIColor.uncolor(content) : content
917
930
  }
918
931
  next provide_file_set_content(patterns, all:, &read) || :next
919
932
  elsif arg
920
933
  filename = Pathname.new(arg).expand_path
921
934
  filename.file? or next :next
922
935
  content = filename.read
936
+ content = Term::ANSIColor.uncolor(content) if opts[?m]
923
937
  content = edit_text(content) if opts[?e]
924
938
  content
925
939
  else
@@ -927,14 +941,18 @@ module OllamaChat::Commands
927
941
  next :next
928
942
  end
929
943
  else
930
- opts = go_command('pae', opts)
944
+ opts = go_command('paem', opts)
931
945
  if opts[?p]
932
946
  all = opts.fetch(?a, false)
933
947
  patterns = extract_patterns(arg)
934
- next provide_file_set_content(patterns, all:, skip_blank: true) { import(_1) } || :next
948
+ next provide_file_set_content(patterns, all:, skip_blank: true) do |src|
949
+ content = import(src)
950
+ opts[?m] ? Term::ANSIColor.uncolor(content) : content
951
+ end || :next
935
952
  elsif arg
936
953
  source = arg
937
954
  content = import(source) or next :next
955
+ content = Term::ANSIColor.uncolor(content) if opts[?m]
938
956
  content = edit_text(content) if opts[?e]
939
957
  content
940
958
  else
@@ -0,0 +1,289 @@
1
+ # Provides compaction support for chat sessions. Mixed into `Chat`.
2
+ #
3
+ # Supplies configuration resolvers that derive concrete token budgets
4
+ # from the `compaction:` config block (ratios of `num_ctx` with absolute
5
+ # floors), the LLM-based summarization pipeline
6
+ # (+summarize_for_compaction+), and the +tool_summary_line+ resolver
7
+ # for per-tool summary templates.
8
+ #
9
+ # @see OllamaChat::OllamaChatConfig::DefaultConfig for the `compaction:`
10
+ # block structure (`reserve`, `keep_recent`, `summary`).
11
+ module OllamaChat::Compaction
12
+ class << self
13
+ # Generates a one-line summary for a tool call result.
14
+ #
15
+ # Looks up the registered tool class and delegates to its
16
+ # `summary_template(result:)` class method. Falls back to a generic
17
+ # sentence if the tool is not registered or the template raises.
18
+ #
19
+ # @param tool_name [String] the registered name, e.g. `'read_file'`.
20
+ # @param result [String] the raw JSON result string from `execute`.
21
+ # @return [String] a short natural-language description of the call.
22
+ def tool_summary_line(tool_name, result)
23
+ default_summary = "was called."
24
+ klass = OllamaChat::Tools.registered[tool_name.to_s]&.class
25
+ klass&.summary_template(result:) || default_summary
26
+ rescue StandardError
27
+ default_summary
28
+ end
29
+ end
30
+
31
+ # Value object carrying the outcome of a successful +compact!+ call.
32
+ #
33
+ # @!attribute [r] context_before
34
+ # @return [String] formatted context usage before compaction
35
+ # @!attribute [r] context_after
36
+ # @return [String] formatted context usage after compaction
37
+ # @!attribute [r] candidates
38
+ # @return [Integer] number of messages that were summarized
39
+ # @!attribute [r] candidate_size
40
+ # @return [String] formatted size of the candidate block
41
+ # @!attribute [r] summary_size
42
+ # @return [String] formatted size of the generated summary
43
+ # @!attribute [r] stored_total
44
+ # @return [String] formatted full conversation length
45
+ Result = Data.define(
46
+ :context_before, :context_after,
47
+ :candidates, :candidate_size,
48
+ :summary_size, :stored_total
49
+ )
50
+
51
+ # Generates a context summary for the given messages via LLM.
52
+ #
53
+ # Serializes non-system messages into a structured prompt, calls the
54
+ # model via +generate+, and assembles the final summary with
55
+ # deterministic tool-call entries stored out-of-band.
56
+ #
57
+ # @param messages [Array<OllamaChat::Message>] the messages to summarize.
58
+ # @param previous_summary [OllamaChat::Message, nil] the previous summary
59
+ # message for iterative re-compaction.
60
+ # @return [Array(String, Array<Hash>)] the assembled content and the
61
+ # merged tool-call entries array.
62
+ def summarize_for_compaction(messages:, previous_summary: nil)
63
+ groups = serialize_groups(messages)
64
+ tool_entries = build_tool_entries(messages)
65
+
66
+ es = OllamaChat::TokenEstimator.estimate(groups)
67
+ log(:info, "Compaction: #{messages.size} messages, " \
68
+ "#{es.tokens_formatted} (#{es.bytes_formatted}) groups")
69
+
70
+ narrative_prev = ''
71
+ old_tool_calls = []
72
+ if previous_summary
73
+ narrative_prev = extract_narrative(previous_summary.content)
74
+ old_tool_calls = extract_old_tool_calls(previous_summary)
75
+ end
76
+
77
+ narrative = call_summarizer(groups:, previous_summary: narrative_prev)
78
+ assemble_summary(narrative, tool_entries, old_tool_calls)
79
+ end
80
+
81
+ # Resolves the effective token budget for a concern given a context size.
82
+ #
83
+ # Computes `max(ratio * tokens, min_tokens).floor`, so the floor acts as
84
+ # a guaranteed minimum even for very small context windows.
85
+ #
86
+ # @param name [Symbol] the concern name, e.g. `:reserve`, `:keep_recent`,
87
+ # or `:summary`.
88
+ # @param tokens [Integer] the context window size (`num_ctx`) to derive
89
+ # the budget from.
90
+ # @return [Integer] the resolved token budget.
91
+ def compact_ratio_tokens(name, tokens)
92
+ [
93
+ compact_ratio(name).to_f * tokens,
94
+ compact_min_tokens(name).to_f,
95
+ ].max.floor
96
+ end
97
+
98
+ # Attempts compaction, prompting the user to retry on failure.
99
+ #
100
+ # +compact!+ is idempotent on failure: the LLM call (and thus any
101
+ # +CompactionError+) happens before the message list is mutated, so a
102
+ # retry always starts from a clean state.
103
+ #
104
+ # @return [Boolean] true if compaction succeeded, false if the user
105
+ # declined to retry.
106
+ def compact_with_retry
107
+ result = messages.compact!
108
+ session_sync
109
+ if result
110
+ report_compaction(result)
111
+ else
112
+ STDOUT.puts('Nothing to compact.')
113
+ end
114
+ return true
115
+ rescue OllamaChat::CompactionError => e
116
+ STDERR.puts "⚠️ Compaction failed: #{e.message}"
117
+ log(:error, "Compaction failed", data: {
118
+ model: @model,
119
+ ctx: current_context_length,
120
+ size: messages.size,
121
+ })
122
+ retry if confirm?(
123
+ prompt: '🔔 Retry compaction? (y/n) ',
124
+ yes: /\Ay/i
125
+ )
126
+ false
127
+ end
128
+
129
+ private
130
+
131
+ # Serializes non-system messages into a readable block for the prompt.
132
+ #
133
+ # @param messages [Array<OllamaChat::Message>] the messages to serialize.
134
+ # @return [String] a formatted block of message lines.
135
+ def serialize_groups(messages)
136
+ messages
137
+ .reject { _1.role == 'system' }
138
+ .map { |m| format_summary_line(m) }
139
+ .join("\n")
140
+ end
141
+
142
+ # Formats a single message as a summary line.
143
+ #
144
+ # @param msg [OllamaChat::Message] the message to format.
145
+ # @return [String] a single line describing the message.
146
+ def format_summary_line(msg)
147
+ tool_name = msg.tool_name.full?
148
+ parts = +"[#{msg.role}]"
149
+ parts << " (#{tool_name})" if tool_name
150
+ parts << " (g:#{msg.group_uuid.to_s[-8..]})" if msg.group_uuid
151
+ if tool_name
152
+ line = OllamaChat::Compaction.tool_summary_line(tool_name, msg.content.to_s)
153
+ parts << " #{line}"
154
+ elsif msg.content.present?
155
+ parts << " #{msg.content.to_s.strip}"
156
+ end
157
+ parts
158
+ end
159
+
160
+ # Builds the deterministic tool-call entries from tool result messages.
161
+ #
162
+ # @param messages [Array<OllamaChat::Message>] the messages to scan.
163
+ # @return [Array<Hash>] tool-call entry hashes, empty if no tool results.
164
+ def build_tool_entries(messages)
165
+ messages.filter_map do |msg|
166
+ next unless tool_name = msg.tool_name.full?
167
+ next if tool_name == 'runtime_information'
168
+ uuid = msg.group_uuid.to_s[-8..]
169
+ summary = OllamaChat::Compaction.tool_summary_line(
170
+ tool_name, msg.content.to_s
171
+ )
172
+ time = msg.group_time&.strftime('%Y-%m-%d %H:%M')
173
+ {
174
+ 'type' => 'tool',
175
+ 'name' => tool_name,
176
+ 'summary' => summary,
177
+ 'uuid' => uuid,
178
+ 'time' => time,
179
+ }
180
+ end
181
+ end
182
+
183
+ # Calls the LLM to generate the narrative portion of the summary.
184
+ #
185
+ # @param groups [String] the serialized groups block.
186
+ # @param previous_summary [String, nil] the prior summary for iteration.
187
+ # @return [String] the LLM-generated narrative text.
188
+ def call_summarizer(groups:, previous_summary:)
189
+ previous = previous_summary.to_s
190
+
191
+ prompt = prompt(:summarize, context: 'compaction').to_s % {
192
+ previous:, groups:
193
+ }
194
+ system = prompt(:system, context: 'compaction').to_s
195
+
196
+ es = OllamaChat::TokenEstimator.estimate(prompt)
197
+ log(:info, "Compaction: sending prompt " \
198
+ "#{es.tokens_formatted} (#{es.bytes_formatted}) to #{@model}")
199
+
200
+ response = Infobar.busy(
201
+ label: 'Compacting context…',
202
+ frames: :braille7,
203
+ output: STDOUT,
204
+ ) do
205
+ generate(system:, prompt:, think: true).strip
206
+ end
207
+
208
+ if response.empty?
209
+ log(:error, 'Compaction: LLM returned empty response')
210
+ raise OllamaChat::CompactionError,
211
+ 'Summarization failed: model returned empty response'
212
+ end
213
+
214
+ res_es = OllamaChat::TokenEstimator.estimate(response)
215
+ log(:info, "Compaction: got response " \
216
+ "#{res_es.tokens_formatted} (#{res_es.bytes_formatted})")
217
+
218
+ response
219
+ end
220
+
221
+ # Assembles the final summary content and merged tool entries.
222
+ #
223
+ # @param narrative [String] the LLM-generated narrative.
224
+ # @param tool_entries [Array<Hash>] new tool-call entries from this pass.
225
+ # @param old_tool_entries [Array<Hash>] entries carried over from a
226
+ # prior compaction.
227
+ # @return [Array(String, Array<Hash>)] the content string and the
228
+ # merged entries array.
229
+ def assemble_summary(narrative, tool_entries, old_tool_entries = [])
230
+ all_tools = old_tool_entries + tool_entries
231
+ tool_section = all_tools.map(&:to_json) * ?\n
232
+
233
+ template = prompt(:assemble, context: 'compaction').to_s
234
+ content = template % { narrative:, tool_section: }
235
+
236
+ [content, all_tools]
237
+ end
238
+
239
+ # Extracts the narrative portion from a summary message's content.
240
+ #
241
+ # @param content [String, nil] the summary content.
242
+ # @return [String] the narrative text.
243
+ def extract_narrative(content)
244
+ content.to_s.sub(/\ntool_calls:\n.*\z/m, '').strip
245
+ end
246
+
247
+ # Returns the tool-call entries from a previous summary message.
248
+ #
249
+ # @param summary_msg [OllamaChat::Message] the previous summary message.
250
+ # @return [Array<Hash>] the tool-call entries, empty if none.
251
+ def extract_old_tool_calls(summary_msg)
252
+ summary_msg.tool_calls || []
253
+ end
254
+
255
+ # Returns the absolute minimum token floor for a given concern.
256
+ #
257
+ # @param name [Symbol] the concern name, e.g. `:reserve`, `:keep_recent`,
258
+ # or `:summary`.
259
+ # @return [Integer] the configured `min_tokens` floor.
260
+ def compact_min_tokens(name)
261
+ config.compaction.attribute_get!(name).min_tokens
262
+ end
263
+
264
+ # Returns the fractional ratio (portion of `num_ctx`) for a given concern.
265
+ #
266
+ # @param name [Symbol] the concern name, e.g. `:reserve`, `:keep_recent`,
267
+ # or `:summary`.
268
+ # @return [Float] the configured `ratio` value.
269
+ def compact_ratio(name)
270
+ config.compaction.attribute_get!(name).ratio
271
+ end
272
+
273
+ # Displays a compact compaction report to +STDOUT+ after the
274
+ # +Infobar.busy+ spinner has finished.
275
+ #
276
+ # Replaces the former bare "Conversation compacted." line with
277
+ # before/after context metrics, candidate volume, and summary size.
278
+ #
279
+ # @param result [Result] the compaction result from +compact!+.
280
+ def report_compaction(result)
281
+ STDOUT.puts(<<~EOT)
282
+ ✅ Conversation compacted.
283
+ Summarized: #{result.candidates} messages (#{result.candidate_size})
284
+ Summary: #{result.summary_size}
285
+ Context: #{result.context_before} → #{result.context_after}
286
+ Stored total: #{result.stored_total}
287
+ EOT
288
+ end
289
+ end