Class: Rubino::CLI::Chat::ImageInbox
- Inherits:
-
Object
- Object
- Rubino::CLI::Chat::ImageInbox
- Defined in:
- lib/rubino/cli/chat/image_inbox.rb
Overview
The REPL's image-attachment inbox (attach an image from the terminal), extracted from ChatCommand (#17).
Attachments live in #pending_image_paths between the prompt read and
the turn; run_turn consumes + clears them via #take! so each image is
sent once into the native vision slot (image_paths →
Lifecycle#execute → adapter with:).
Class Method Summary collapse
-
.resolve_oneshot(query, flag_values) ⇒ Object
Builds the [text, image_paths] pair for a one-shot turn.
Instance Method Summary collapse
-
#extract_images!(input, ui) ⇒ Object
Parses the line for image references (@image, dropped/quoted/escaped path), moves any into @pending_image_paths and returns the cleaned text.
-
#handle_image_command(input, ui) ⇒ Object
Handles the REPL-local image commands.
- #pending_image_paths ⇒ Object
-
#stage_flag_images(flag_values, ui) ⇒ Object
Seeds the interactive pending-images inbox from --image/-i flag paths (#160), through the SAME attachment gate every other staging surface uses (Attachments::Classify + Policy via ImageInput#attachment_error).
-
#take! ⇒ Object
Consumes the turn's queued image attachments (the native vision slot) and resets so they're attached exactly once, not re-sent next turn.
Class Method Details
.resolve_oneshot(query, flag_values) ⇒ Object
Builds the [text, image_paths] pair for a one-shot turn. Pulls @image / dropped-path tokens out of the prompt (so they hit the vision slot, not the literal text) and prepends any paths given via --image. Flag paths are expanded the same way as in-line tokens; a flag path that isn't a readable image is reported and skipped rather than silently dropped.
Every candidate then passes the SAME secure-by-default attachment gate as the server/run path (Attachments::Classify + Policy, via ImageInput#attachment_error) — a policy rejection is a clean one-line error BEFORE any network call, not five provider retries (#98).
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/rubino/cli/chat/image_inbox.rb', line 24 def self.resolve_oneshot(query, flag_values) flag_paths = Array(flag_values).map { |p| Interaction::ImageInput.(p) } flag_paths.each do |p| next if LLM::ContentBuilder.image_file?(p) && File.file?(p) warn "rubino: ignoring --image #{p} (not a readable image file)" end valid_flags = flag_paths.select { |p| LLM::ContentBuilder.image_file?(p) && File.file?(p) } valid_flags.each do |p| reason = Interaction::ImageInput.(p) raise Rubino::Error, "--image #{p}: #{reason}" if reason end result = Interaction::ImageInput.parse(query, existing: valid_flags) if (rejection = result.rejected.first) raise Rubino::Error, "#{rejection[:path]}: #{rejection[:reason]}" end [result.text, result.image_paths] end |
Instance Method Details
#extract_images!(input, ui) ⇒ Object
Parses the line for image references (@image, dropped/quoted/escaped path), moves any into @pending_image_paths and returns the cleaned text. Non-image references are left in the text (current behaviour). Shows an in-prompt indicator for whatever is now attached. A candidate the attachment policy rejects (oversize / spoofed extension / unsafe) is dropped with a one-line warning instead of being shipped (#98).
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/rubino/cli/chat/image_inbox.rb', line 85 def extract_images!(input, ui) result = Interaction::ImageInput.parse(input, existing: pending_image_paths) result.rejected.each do |rejection| ui.warning("not attached — #{File.basename(rejection[:path])}: #{rejection[:reason]}") end newly = result.image_paths - pending_image_paths @pending_image_paths = result.image_paths # A line with text AND an @image sends BOTH on THIS turn (the cleaned # text is non-empty, so the main loop submits now); an image-only line # stages for the next message. The indicator must match that # disposition — saying "sent with your next message" on a text+image # line is wrong (#225). unless newly.empty? attached_now = !result.text.strip.empty? show_image_indicator(ui, newly, attached_now: attached_now) end result.text end |
#handle_image_command(input, ui) ⇒ Object
Handles the REPL-local image commands. Returns true when it consumed the
input (so the main loop should next), false otherwise.
/paste — grab an image from the clipboard into image_paths
/clear-images — drop all pending attachments
rubocop:disable Naming/PredicateMethod -- "did I consume the line", not a pure predicate
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/rubino/cli/chat/image_inbox.rb', line 110 def handle_image_command(input, ui) case input.strip.downcase when "/clear-images", "/clear-image" if pending_image_paths.empty? ui.info("No attached images to clear.") else ui.info("Cleared #{pending_image_paths.size} attached image(s).") @pending_image_paths = [] end true when "/paste" paste_clipboard_image(ui) true else false end end |
#pending_image_paths ⇒ Object
45 46 47 |
# File 'lib/rubino/cli/chat/image_inbox.rb', line 45 def pending_image_paths @pending_image_paths ||= [] end |
#stage_flag_images(flag_values, ui) ⇒ Object
Seeds the interactive pending-images inbox from --image/-i flag paths (#160), through the SAME attachment gate every other staging surface uses (Attachments::Classify + Policy via ImageInput#attachment_error). A bad flag path warns and is skipped — interactive startup must not die on it the way one-shot raises. Staged images show the usual indicator and are covered by /clear-images, as documented.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/rubino/cli/chat/image_inbox.rb', line 63 def stage_flag_images(flag_values, ui) Array(flag_values).each do |raw| path = Interaction::ImageInput.(raw) unless LLM::ContentBuilder.image_file?(path) && File.file?(path) ui.warning("not attached — #{raw}: not a readable image file") next end if (reason = Interaction::ImageInput.(path)) ui.warning("not attached — #{File.basename(path)}: #{reason}") next end pending_image_paths << path unless pending_image_paths.include?(path) end show_image_indicator(ui, pending_image_paths) unless pending_image_paths.empty? end |
#take! ⇒ Object
Consumes the turn's queued image attachments (the native vision slot) and resets so they're attached exactly once, not re-sent next turn.
51 52 53 54 55 |
# File 'lib/rubino/cli/chat/image_inbox.rb', line 51 def take! paths = pending_image_paths @pending_image_paths = [] paths end |