dsh-ui-screenshot
DeepSeek Harness 的 Web 插件:在输入框添加截图按钮,截屏并作为草稿图片插入,仅当模型支持视觉时显示。
- Stars
- 0
- Language
- TypeScript
- Created
- Sep 2, 2026
- Updated
- Sep 2, 2026
Introduction
description: "Screenshot button for the DSH Web composer: capture the screen and insert it as a draft image, shown only when the active model supports image input; for users and maintainers of the screenshot experience." kind: "package-reference"
@deepseek-ai/dsh-client-ui-screenshot
English | 中文
Summary
This package adds a screenshot button to the DSH Web composer tool row. Clicking it asks the browser to share a surface (whole screen, a window, or a tab), captures one decoded frame, and inserts it as a draft image in the composer attachment rail — the same image path used by paste and drag-and-drop. The button is rendered only when the active session's model supports image input, so a text-only model never sees the control. It makes no model-visible prose change and owns no server state.
Table of Contents
- Use this package
- Understand the implementation
- Further Exploration
- Why this package collaborates with ui-conversation and ui-model-selection
- Model Experience
- Known Limitations and Deferred Work
- Dev Note
Use this package
Mount this plugin alongside ui-conversation and ui-model-selection. A camera button then appears in the conversation.input.right tool row, immediately before the model selector, for any session whose effective model advertises image input. A session with a text-only model, no resolved model, or an unlisted model renders nothing.
Capturing
Clicking the button calls getDisplayMedia; the user picks a surface in the browser's share dialog. The first decoded frame is drawn to a canvas and exported as a PNG, registered on the conversation service as a browser-owned draft image, and appended through the public inputActions.addImages path so it shows in the composer attachment rail exactly like a pasted or dropped image. The capture stream is stopped after one frame.
Failure handling
If the user cancels or the runtime lacks display capture, the button does nothing. A captured file the admission path refuses is released so no orphan draft remains.
Understand the implementation
Implementation internals — click to expand
The button is a conversation.input.right entry registered with ctx.slots.register. The inject face closes over the conversation service for draft-image registration (createDraftImages / releaseDraftImage) and over the modelDirectories service for the session's shared model directory. The component reads the directory store through useSyncExternalStore, and a pure predicate (modelSupportsImage) decides visibility from the effective current model's inputModalities. It collaborates with other plugins only through cordis services — no cross-plugin value imports. The screenshot is captured in capture.ts, which waits for play() plus one presented frame (requestVideoFrameCallback, with a poll-and-timeout fallback) so the sample is not a black frame.
Further Exploration
Read these pages when the screenshot surface is not enough.
- ui-conversation — declares the
conversation.input.rightslot, the draft-image controller, and the composer. - ui-model-selection — owns the per-session model directory the button gates on.
- Client package map — adjacent browser UI packages.
Why this package collaborates with ui-conversation and ui-model-selection
This plugin is a thin UI add-on, so it deliberately delegates its two real jobs — inserting the image, and knowing whether the model can see it — to the packages that already own them. It reaches them through cordis services, never by importing another plugin's code (client bundles forbid cross-plugin value imports, so collaboration is by contract).
Inserting the image → ui-conversation (the conversation service)
The composer and its attachment rail are owned by ui-conversation. Its ConversationController (ctx.conversation) is the single owner of the browser draft-image registry: createDraftImages allocates the ComposerAttachment (id, preview URL, dimensions) that the rail renders and that submission serializes, and releaseDraftImage cleans it up. Re-implementing a second image-intake path here would fork the validation, preview-lifecycle, and submit-serialization logic, and the two paths could disagree about which objects are live. The button therefore routes the captured PNG through that one registry plus the session's inputActions.addImages, exactly as paste and drag-and-drop do.
Knowing whether the model can see images → ui-model-selection (the modelDirectories service)
ui-model-selection's ModelDirectoryResolver (ctx.modelDirectories) is the only place that knows the effective current model and its inputModalities: it merges the host-generation catalog (session.modelCatalog) with the session's durable modelSelection projection into one reactive directory that both the /model popup and the composer model seat share. A composer tool-row slot receives no model information at all, and the composer must not import ui-model-selection (the dependency is one-way — ui-model-selection → ui-conversation, never back). So this plugin reads that same directory service; loading its own catalog would double the fetch and the selection projection and could disagree with the model seat about which model is active.
The result is one source of truth for both concerns across the composer, the model seat, and this button.
Model Experience
What the model sees
Nothing is added to the model context by this plugin itself. The screenshot the user takes becomes an image part of the user's next message, delivered through the existing attachment-admission path; the model sees it as ordinary user image content.
Token effect
Image tokens are billed per the provider's image accounting only when a message carrying the screenshot is submitted. The button and its own copy add no tokens.
KV Cache effect
None. The screenshot rides a new user message; it does not alter the stable prompt prefix, so it does not by itself invalidate the KV cache. A message that admits the image extends the history tail like any other message.
Known Limitations and Deferred Work
These are the current package constraints.
- Browser capture only — the button relies on
getDisplayMedia; it is unavailable in non-browser runtimes and requires a secure context (localhost is secure). - No server-side gating — the button is a proactive UX affordance only. A server that rejects image prompts for a text-only model still rejects them; the button is hidden for a catalog-known text model but not re-validated on the server.
- Catalog-membership dependent — a model the catalog no longer advertises (or a hand-entered unlisted id) is treated as non-vision, hiding the button.
Dev Note
Working context for maintainers — click to expand
None.
DSH STORE Catalog Contract
Dependencies
- Runtime: none.
- Peer (provided by the DSH host):
@deepseek-ai/cordis, plus the host-injected client services@deepseek-ai/dsh-api-session-controller,@deepseek-ai/dsh-client-locale,@deepseek-ai/dsh-client-ui-conversation,@deepseek-ai/dsh-client-ui-model-selection,@deepseek-ai/dsh-client-ui-renderer,@deepseek-ai/dsh-client-ui-session. These are resolved from the host's client module table at runtime, not installed by this package. - No optional dependencies.
Permissions
- The button asks the browser to share a surface (whole screen / a window / a tab) via the user-granted media-capture prompt, decodes one frame, and inserts it as a draft image through the composer attachment rail (the same path used by paste / drag-and-drop).
- It owns no server state, makes no filesystem writes, and makes no outbound network calls.
- The captured frame is transient in the browser and is only written to the current draft.
External services
- None. Capture is browser-local after the user grants the share; everything else rides the host's existing session/composer services.
Failure bounds
- The button only renders for a session whose effective model advertises image input
(
model.inputModalities/ vision-capable). A text-only model, an unresolved model, or an unlisted model renders no button. - If the user denies the surface-share prompt, nothing is inserted and the action is a no-op.
- Capture/insert failures are contained to the composer action; they do not affect the host session.
Disposable-profile install / start / uninstall evidence
dsh plugin --profile tmp add github:cczzyy-cn/dsh-ui-screenshot#v0.1.1
# Profile cordis.patch.yml: - insert: { id: client-ui-screenshot, name: '@deepseek-ai/dsh-client-ui-screenshot' }
dsh profile start tmp & # camera icon appears only for a vision-capable model
dsh plugin --profile tmp rm @deepseek-ai/dsh-client-ui-screenshot
dsh profile stop tmp # clean profile exit, no residue