feat: /image attachment, /browse browser-view, and vision-message support#2
Conversation
…pport - BaseCoder: add pending_images list; run() captures/clears images and passes them to _build_messages; _build_messages builds multi-part vision content when images are present (OpenAI/Anthropic vision API format) - web_scraper: add fetch_page_info() returning structured page data (title, description, status_code, headings, links, content) - terminal: add print_browse() rich browser-view panel and print_image_added() confirmation; update /help table with new commands - commands: add _cmd_browse() and _cmd_image() with full dispatch - repl: add /browse and /image to tab-completion list Agent-Logs-Url: https://github.com/Rahulchaube1/QGo/sessions/50b8c034-e924-4434-912d-f950eba6066b Co-authored-by: Rahulchaube1 <157899057+Rahulchaube1@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds new REPL capabilities for (1) attaching images to the next user turn for vision-capable models and (2) browsing a URL with a richer terminal “browser view”, plus wiring needed to format multi-part (text + image) messages.
Changes:
- Added
/browsecommand +fetch_page_info()for structured page metadata (title/TOC/links) and terminal rendering. - Added
/imagecommand to queue local/remote images and send them with the next message. - Updated
BaseCodermessage building to support multi-part vision message payloads.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
qgo/utils/web_scraper.py |
Adds fetch_page_info() to return structured page data for /browse. |
qgo/ui/terminal.py |
Adds rich rendering for browser view + image-queued confirmation; updates /help. |
qgo/ui/repl.py |
Registers /browse and /image for tab completion. |
qgo/ui/commands.py |
Registers and implements /browse and /image commands. |
qgo/coders/base_coder.py |
Adds pending_images queue and builds multi-part user messages when images are present. |
Comments suppressed due to low confidence (1)
qgo/utils/web_scraper.py:166
- There is an unexpected indent starting at the
"""Minimal fallback using only urllib..."""line; it looks like thedef _fetch_plain(...):wrapper was accidentally removed. As written, this will raiseIndentationErroron import and also breaksfetch_url()'sexcept ImportError: return _fetch_plain(...)fallback path. Reintroducedef _fetch_plain(url: str, timeout: int = 15) -> str:at module scope and indent this block under it.
"""Minimal fallback using only urllib (no requests/bs4)."""
try:
import urllib.request
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| response.raise_for_status() | ||
| result["status_code"] = response.status_code |
There was a problem hiding this comment.
fetch_page_info() calls response.raise_for_status() before recording the HTTP status. For non-2xx responses this will jump to the exception handler and status_code will remain 0, so the browser view can’t display the actual status. Set result["status_code"] = response.status_code before raise_for_status(), or avoid raising and instead handle non-2xx while still parsing title/headings.
| response.raise_for_status() | |
| result["status_code"] = response.status_code | |
| result["status_code"] = response.status_code | |
| response.raise_for_status() |
| self.coder.messages.append({ | ||
| "role": "user", | ||
| "content": f"Web page content from {url}:\n\n{content[:8000]}", | ||
| }) |
There was a problem hiding this comment.
BaseCoder.messages is a list[Message] and _build_messages() calls msg.to_dict() on history items. Here /browse appends a plain dict into self.coder.messages, which will raise AttributeError: 'dict' object has no attribute 'to_dict' on the next run(). Append a qgo.models.Message(...) (or provide a helper on BaseCoder to add context messages) instead of a raw dict.
| elif src.startswith(("http://", "https://")): | ||
| # Remote image — pass URL directly (vision models support this) | ||
| self.coder.pending_images.append(src) | ||
| self.io.print_image_added(src, len(self.coder.pending_images)) |
There was a problem hiding this comment.
PR description says remote HTTPS image URLs are supported, but /image currently accepts both http:// and https://. If HTTPS-only is intended, restrict to https:// (and consider warning on http:// so behavior matches the documented contract).
| # Capture and clear any pending images | ||
| images = self.pending_images[:] | ||
| self.pending_images.clear() |
There was a problem hiding this comment.
pending_images is cleared before _send() is called. If the LLM request fails/raises, the images are lost and won’t be attached on retry. Clear pending_images only after a successful send (or restore them in an exception handler) so queued attachments aren’t dropped on transient errors.
| # Readable content (reuse existing fetch_url) | ||
| result["content"] = fetch_url(url, timeout) |
There was a problem hiding this comment.
fetch_page_info() fetches the page via requests.get(...) and then immediately calls fetch_url(url, ...), which performs a second HTTP request for the same URL. This doubles latency and load for /browse. Consider refactoring fetch_url to accept already-fetched HTML (or extracting the HTML→readable-text logic) so fetch_page_info() can reuse response.text/soup without another network call.
| if meta and isinstance(meta, object) and hasattr(meta, "get"): | ||
| result["description"] = meta.get("content", "") # type: ignore[union-attr] |
There was a problem hiding this comment.
The meta description extraction has redundant checks (isinstance(meta, object) and hasattr(meta, "get")) and a # type: ignore. Since soup.find(...) returns a BeautifulSoup Tag (or None), you can simplify this to if meta: result["description"] = meta.get("content", "") to improve readability and remove the ignore.
| if meta and isinstance(meta, object) and hasattr(meta, "get"): | |
| result["description"] = meta.get("content", "") # type: ignore[union-attr] | |
| if meta: | |
| result["description"] = meta.get("content", "") |
Adds image attachment, a rich terminal browser view, and wires vision-capable multi-part message formatting into the REPL — none of which existed before.
Changes
/image <path|url> …BaseCoder.pending_imagesrun()call, images are bundled into the user turn as OpenAI/Anthropic multi-part content, then cleared/browse <url>fetch_page_info()inweb_scraper.pyreturning{title, description, status_code, headings, links, content}/webVision message building (
base_coder.py)BaseCodergainspending_images: list[str]_build_messagesacceptsimages=and emits the multi-part content array when populated:[{"type": "text", "text": "…"}, {"type": "image_url", "image_url": {"url": "…"}}]Supporting additions
QGoIO.print_browse()— Rich panels for browser viewQGoIO.print_image_added()— per-image queue confirmation/browseand/imageregistered in dispatch table and tab-completion/helptable updated