Skip to content

feat: image-input variants keep native pasted images for text-only models - #36

Open
Anionex wants to merge 5 commits into
mainfrom
fix/image-input-variants
Open

feat: image-input variants keep native pasted images for text-only models#36
Anionex wants to merge 5 commits into
mainfrom
fix/image-input-variants

Conversation

@Anionex

@Anionex Anionex commented Aug 15, 2026

Copy link
Copy Markdown
Owner

What

Closes the paste-experience gap for text-only models: pasting an image into the DSH Web composer currently turns it into a text reference (capsule in the composer, plain path text in history), while the native flow keeps the image thumbnail and the durable session image.

This PR adds image-input variants:

  • Every model the host positively declares text-only gets a sibling model-selector entry <model> (Vision Toolkit) under a matching provider group. The variant declares image input, so pastes keep the native attachment flow — composer thumbnail, durable session image, history rendering — and the variant's stream() rewrites image blocks into Vision Toolkit descriptions only on the wire before delegating to the upstream route. The session log is untouched; replay and the UI keep the real image.
  • The browser paste interception asks the host before taking a paste over (GET /_dsh/vision-toolkit/paste-policy?sessionId=&model=): pastes stay native for image-capable models (including the variants) and are converted to workspace paths only for models the host confirms text-only. The verdict uses the model-selector label the browser shows (authoritative — the session header only updates on a request), with a documented stale-header fallback when no label is readable.
  • New config: imageInputVariants.enabled (default true) and imageInputVariants.providers (restrict wrapped routes), validated and applied live.

Conversion failures degrade to an explanatory wire block instead of failing the turn; descriptions are cached per attachment (64-entry LRU, concurrent readers join, failed reads are not memoized, cache cleared when the runtime reconfigures).

Files

  • src/image-input-variants.ts (new) — variant adapter, request-time image→description conversion (top-level and tool-result-nested), evidence cache, route sweep (releases on restrict narrowing / lost eligibility / vanished routes, coalesced on topology bursts), label- and header-based paste verdicts with a cached resolver.
  • src/config.tsimageInputVariants schema + validation.
  • src/web.ts, src/paste-images.ts, src/web-request.ts — same-origin paste-policy route forwarding the selector label.
  • src/client/paste-images.tsx — host-verdict-gated paste interception keyed by session+label, focus prefetch, 404 retry.
  • src/index.ts — installer wiring + live settings reconciliation.
  • Tests: tests/image-input-variants.spec.ts (new), updated paste-images-client.spec.ts, web.spec.ts, config.spec.ts.
  • Docs: README (en/zh), CHANGELOG.

Verification

  • pnpm test: 214 passed (baseline 167), 1 pre-existing skip.
  • tsc (host + both client projects) clean; pnpm run build produces clean artifacts; git diff --exit-code -- lib clean.
  • Real-profile smoke (temporary DSH_HOME): bundle installs and mounts; routing the headless session through vision-toolkit-deepseek-official reaches the upstream auth request (AUTH, same as the plain route) while an unregistered provider yields NO_ADAPTER — proving the variant is registered and delegates end-to-end on a real host.
  • Four adversarial review rounds (subagent): rounds 1-2 found H1-H5 + label-verdict and sweep issues, all fixed with pinned regression tests; the final round reports no high-value actionable findings.

Browser path note

The paste/thumbnail flow is a Web profile behavior. This environment's running dsh web (port 3080) serves the released 0.1.7 bundle; swapping in this branch requires restarting that profile, which interrupts the live GUI session, so the browser pass was deferred — the wire-level behavior is covered by the real-profile smoke above plus the client suite (jsdom, real Cordis context, real paste events, real selector-label DOM).

…dels

Register a sibling model-selector entry for every model the host declares
text-only: the variant declares image input, so pasted images keep the
native attachment flow (composer thumbnail, durable session image), and
the variant's stream rewrites image blocks into Vision Toolkit
descriptions only on the wire before delegating to the upstream route.

The browser paste interception now asks the host before taking a paste
over, and only converts to workspace paths when the current model is
confirmed text-only. Add imageInputVariants config (enabled, providers),
a same-origin paste-policy route, and unit coverage for the adapter,
conversion, cache, sweep, and client verdict.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…rough, cache invalidation

- Paste verdict: the model-selector label is now the authoritative route
  fact (the session header only updates on a request), with a label-keyed
  host cache invalidated on topology changes; the client sends the label
  and keys its verdict cache by session+label, so a model switch can never
  serve a stale takeover answer.
- Sweep: release wrappers when restrict narrows or a route loses every
  eligible model; re-check disposal before registering.
- resolveModel: carry context, defaultMaxTokens, and reasoning metadata
  through the variant so selectors and effort configs behave like upstream.
- EvidenceCache: clear on runtime-instance change (reconfigured provider).
- Paste-policy route: forward the model label; contain handler rejections.
- Client: a 404 leaves the verdict unconfirmed and retries on next focus
  instead of standing down for the page lifetime.
- Reuse the host's contentHasImage; tests for all of the above (209 total).
…pinned tests

- labelTakeoverVerdict: an unreadable route catalog now vetoes the
  takeover instead of silently dropping that provider's vote — the
  unreadable route is exactly where an image-capable twin could hide.
- Sweep: coalesce adapters-updated bursts into one extra pass and skip
  probing our own variant routes (they only re-probe their upstream).
- Paste resolver: cache label misses too, so a label matching nothing
  stops paying a full catalog walk per paste.
- Client tests: the 404-recovery test now actually pins the recovery
  (route comes back -> focus re-asks -> next paste is taken over), and
  the variant-label test drives the host veto by label instead of an
  unconditional stub.
- Web spec: duplicate model query is refused with 400.
… sweep coalescing tests, docs

- labelTakeoverVerdict: log the unreadable route so a broken provider is
  diagnosable; the veto stays (no header fallback here, since the header
  may describe a previous model after a switch).
- Client: the variant-veto test now lets the veto response land before
  pasting, so 'native' is the host's verdict and not the unconfirmed
  default.
- Sweep: tests pin burst coalescing and mid-sweep re-arm.
- README (en/zh): document the label-authoritative verdict and the stale
  header fallback window; JSDoc the selector-label heuristic and the
  paste/media-type degradation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant