Incident Studio · dsh-incident-studio
English | 中文
A right-sidebar plugin for DeepSeek Harness that puts screen frames and log lines on one millisecond timeline. Record, mark the seconds that went wrong, drag boxes on the frame, type one sentence — then hand the agent a keyframe strip, an animated GIF, the aligned log slice and your description, straight into the composer.
Nothing here is game- or project-specific: log sources are configured, any program that writes a file works, and programs can push events directly.
The problem it solves
A hands-on bug report ("I picked up the chest and the character floats") is obvious on screen and obscure in the log. You write a sentence, the agent has to guess which of the last 900 log lines was "that moment".
This plugin ties three things to one clock:
| Evidence | Where it comes from | Precision |
|---|---|---|
| Picture | Host-side frame capture (three paths, below) | 6 fps by default, every frame carries wall-clock time |
| Events | Log file tailing, markers, HTTP ingest | line-write time, tens of ms; ingest can be exact |
| Your words | boxes drawn on the frame + one sentence | attached to the selected range |
Every delivered log line is prefixed with [00:12.340] relative to the recording start — the same origin as the frames — so the agent never has to guess.
Using it
Record ──> frames + log lines, one clock
│
Mark (⚑) when it breaks ──> Stop
│
Review: drag the range, box the problem, one sentence
│
Build artifacts ──> frames.png (6-frame strip) + clip.gif, saved in the workspace
│
Fill composer ──> text + attachments in the input box (or Send now)
- Open the panel: the
+in the right sidebar (guide card "Incident Studio"), or the🎥 Capturebutton under the composer. On first mount it opens itself once and remembers that inlocalStorage. - Configure log sources (once): panel → Settings → absolute path (or workspace-relative). The file may appear later — start recording first, launch the game afterwards.
- Record: pick a capture source →
● Start recording. Hit⚑ Markwhen it breaks (as often as you like). - Review: the default range is the last mark ±3 s; drag the ends yourself or use
All/Last 8s/±3s around mark. Moving the playhead moves the log list; clicking a line seeks the picture. - Annotate: drag a box on the frame; each box takes a note.
- Hand off:
Build artifacts→Fill composer. The strip is attached by default, the GIF is optional; press Enter yourself or useSend now.
The ingest endpoint (for programs)
Any local process (a mod, a test script, a dev server) can push events instead of writing a file:
curl -X POST http://127.0.0.1:<dsh-port>/dsh-incident-studio/api/ingest \
-H "content-type: application/json" \
-H "x-incident-token: <token shown in the panel's Settings>" \
-d '{"text":"entered the raiders room","src":"RConnect","level":"info"}'
The token is per host process and visible in the panel; the same-origin panel needs no token. Pass wall (epoch ms) to place an event at an exact time; otherwise arrival time is used.
What you get on disk
Into the current session workspace, .dsh-incidents/<recording-id>/:
.dsh-incidents/20261104-193045-a1b2c3/
frames-20261104193045.png six-frame strip with timestamps and red boxes
clip-20261104193045.gif animated GIF of the range (460 px wide)
bundle.md full note: description, box coordinates, every event line
bundle.json the same, machine-readable
An excerpt of bundle.md:
# Character floats after picking up a chest
- **Captured at**: 2026-11-04 19:30:45 (+08:00)
- **Range**: `00:12.340` → `00:16.780` (00:04.440)
- **Capture path**: GDI screen grab 6fps
## What went wrong
Picked up the chest, the character levitates and never returns to the walk animation
## Frames
**Keyframe strip** — 6 of 26 frames, with the annotation boxes burned in

## Annotations
- Region #1: normalized x=0.412, y=0.308, w=0.096, h=0.104; note: floating, no shadow; at 00:14.000
## Event stream (in range)
```log
[00:12.210] (host) RConnectGame: moveTest -> 966,900
[00:14.250] (host) ReferenceError: chest state mismatch (sost:LEVITATING)
[00:15.000] ⚑ problem
What lands in the composer is the **compact** version: phenomenon, range, artifact paths, box notes, and at most 80 log lines (errors and markers kept first); the full record is one path away.
## Three capture paths
| What the host process can reach | Path | Notes |
|---|---|---|
| `electron.desktopCapturer` (Electron main process) | timed JPEG grabs | 6 fps / 960 px / q62 by default; can pick a **window** |
| Node only (Electron utility process, plain `dsh web` host) | **Windows GDI grabber** (`tools/grab-frames.ps1`) | whole virtual desktop (all monitors), one long-lived PowerShell writing JPEGs; the host deletes each temp file as it ingests it |
| A real browser on `dsh web` | `getDisplayMedia` + `MediaRecorder` | needs screen-sharing permission; frames are extracted locally afterwards |
All three produce "frames with wall-clock time", so annotation, export and hand-off are identical.
> **Measured, not assumed**: the DSH desktop host cannot reach `desktopCapturer` (`/state` reports `nativeReason: "desktopCapturer 不可用"`), so the desktop app actually takes the GDI path. GDI uses `System.Drawing` + `Graphics.CopyFromScreen`: at 320 px / 4 fps a frame is ~5 KB, 1.4 s yields 5 frames with strictly increasing timestamps and the temp folder is gone after stop (`node test/live-capture.mjs`). The cost is that you cannot pick a single window.
Frame buffering is capped (900 frames / 96 MB / 5 minutes by default); the oldest frames are dropped, so a long recording cannot bloat the host.
## Configuration
The panel's Settings edits `<DSH_HOME>/storages/dsh-incident-studio/profiles.json` (`DSH_HOME` defaults to `~/.dsh`; on Windows `%USERPROFILE%\.dsh`). You can edit it directly:
```json
{
"version": 1,
"activeId": "my-project",
"profiles": [
{
"id": "my-project",
"name": "My project",
"note": "main instance and test instance",
"sources": [
{ "id": "main", "label": "main", "color": "#6aa9ff", "kind": "file",
"path": "C:/Users/you/AppData/Roaming/MyApp/logs/main.log", "enabled": true, "preRoll": true },
{ "id": "test", "label": "test", "color": "#7bd88f", "kind": "file",
"path": "C:/Users/you/AppData/Roaming/MyApp-test/logs/main.log", "enabled": true, "preRoll": true }
]
}
]
}
pathdoes not expand%VAR%or$HOME; use an absolute path or one relative to the workspace.preRoll(on by default) takes the last 96 KB / 800 lines when recording starts and marks them "before the recording"; those lines have no per-line timestamp, so they get ordinal placeholders (-Nms) to keep their order.kindisfiletoday;ingestandmarkerevents are generated by the plugin itself.
Optional host config, on the profile's patch row:
- id: incident-studio
config:
artifactsDirName: .dsh-incidents # artifact folder, relative to the session workspace
captureFps: 6 # frame rate
captureWidth: 960 # frame width
captureQuality: 62 # JPEG quality
A copy-paste example: Remains (Adobe AIR) mod logs
AIR games write mod logs under File.applicationStorageDirectory, i.e. %APPDATA%\<app-id>\Local Store\:
C:/Users/<you>/AppData/Roaming/pfe/Local Store/<ModName>.log ← main instance
C:/Users/<you>/AppData/Roaming/pfe2/Local Store/<ModName>.log ← second test instance
Put those two absolute paths in two log sources. These logs are append-only: only new lines are captured, preRoll brings the tail in as "before the recording", and since the lines carry no timestamps the alignment is "the moment this line hit the file".
Install
Requires DeepSeek Harness 0.2.1+ (it uses the 0.2.1 Connection service and sidebar tab API).
# straight from this repo — client.js is committed, there is no build step
dsh plugin --profile <profile> add https://github.com/Eclipse-NotFound/dsh-incident-studio
# once it is on npm
dsh plugin --profile <profile> add dsh-incident-studio
Desktop profiles are managed by the app: install from Settings → Plugins with the package spec, not by pointing the CLI at profiles/web (that will not show up in the desktop app). A new host half needs one app restart; the browser half is republished and re-applied on the page.
Disable it by removing dsh-incident-studio from dsh.profile.bundles, or with a profile patch:
- id: incident-studio
disabled: true
Privacy and boundaries
- Frames are captured only after you press Start recording, and stop when you stop. The GDI path grabs the whole virtual desktop — move sensitive windows away first.
- Frames, GIFs and notes are written only into the session workspace's
.dsh-incidents/;profiles.jsonand the two receipts live in$DSH_HOME/storages/dsh-incident-studio/. - No network calls, no telemetry. Data reaches a model only after you press Fill composer / Send — through DSH's normal message path, exactly like typing it yourself.
- Every
/dsh-incident-studio/api/*route goes through the host's same-origin check; other local processes can only push events with the per-processx-incident-token./openmay only open paths inside this session's.dsh-incidents/. - The self-check receipts record counts and stages only (
client-boot.json: how far the page got;capture-capability.json: how many screens/windows exist). No window titles, no pixels.
Develop
npm ci # dev deps: type references plus jsdom / react-dom / gifenc for tests
node tools/build-client.mjs # concatenate src/client/*.js into client.js, with a syntax check
node test/run.mjs # 250 assertions: host routes, client pure functions, seat registration, jsdom click-through, GIF/strip, capture polling
node test/live-capture.mjs # real GDI capture (grabs ~1.4 s of screen at 320px, deletes it immediately; keep it out of CI)
node tools/check-live.mjs --fetch # check a running DSH: routes, module graph, served bundle, capture capability, page receipts
client.js is a generated artifact — do not edit it by hand. src/vendor/gifenc.js is generated from node_modules/gifenc by tools/vendor-gifenc.mjs (MIT).
index.js host half: log tailing, frame capture, 17 routes, artifacts
src/client/00-head.js … 90-tail.js browser half, concatenated into client.js in order
tools/grab-frames.ps1 Windows GDI screen grabber
tools/check-live.mjs live self-check (routes / module graph / served bundle / receipts)
test/host.test.mjs real HTTP server exercising every route
test/pure.test.mjs loads the shipped client.js and tests the pure helpers
test/client-apply.test.mjs runs apply() against a fake client context, then the hand-off chain
test/capture.test.mjs capture polling, stop, failure reporting, API error mapping
test/dom.test.mjs clicks through the whole panel inside jsdom
test/live-capture.mjs real-machine GDI capture
check-live.mjs answers "is it actually loaded": 403 means a route is registered and rejecting external callers by design (404 means it is not registered); the module graph must contain this plugin and the served client.js must be byte-identical to your build (plus one source-map trailer); client-boot.json must show apply → seat → panel.
Known limitations
- Publishing timing: the host half is assembled at boot (editing
index.jsneeds a DSH restart); the browser bundle follows live updates and re-runsapplywhen the host republishes. Measured: a real content change to the profile'scordis.patch.ymltriggers a host recompose, editing files inside the plugin folder does not. - Alignment precision: tailing sees a line once it is written, so expect tens of milliseconds; use
/ingestwithwallwhen it must be exact. - The desktop path is a frame sequence, not a video: 6 fps reads gradient/interpolation glitches fine, single-frame animation curves less so. Raising
captureFpsto 10–15 costs memory and JPEG work. - GDI grabs the whole desktop: no per-window capture, all monitors merged.
- GIF uses one 128-colour palette (fixed from the first frame): small files and a clean picture for screen content; large gradients may band slightly.
- Browser webm extraction relies on seeking:
MediaRecorderoutput has no index, so a trailing frame can be lost in extreme cases. - No voice: descriptions are typed and boxed.
- Artifacts live in the workspace: add
.dsh-incidents/to your.gitignore.
License
MIT. The product instinct is inspired by AGIHunt/blurt (a macOS menu-bar "talk to your agent" tool); this is a general-purpose incident panel inside DSH, with a different implementation and setting.
No comments yet. Be the first to write one.