SLOPSHOPPER

replay-theater

Step through the last turn's edits, one diff at a time.

newpanebandguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · replay-theater
│ ┃ Replay Theater ✕ › fix the failing auth test and add an audit log call │ ┃ Replay Theater 1 2 3 │ ┃ /work/app/src/auth.ts (step 1 of 3) ⏺ Read(src/auth.ts) │ ┃ - if (!claims) throw new Error('invalid ⎿ Read 6 lines │ ┃ token') ⏺ Update(src/auth.ts) │ ┃ + if (!claims || claims.exp < Date.now() / ⎿ Added 2 lines, removed 1 line │ ┃ 1000) throw new Error('invalid token') ⏺ Bash(bun test) │ ┃ + await audit('refresh', claims.sub) ⎿ 3 pass, 1 fail │ ┃ [ Prev ] [ Next ] [ Close ] │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /replay │ ⎿ replay-theater: Replaying │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Replay Theater
Replay Theater 1 2 3 /work/app/src/auth.ts (step 1 of 3) - if (!claims) throw new Error('invalid token') + if (!claims || claims.exp < Date.now() / 1000) throw new Error('invalid token') + await audit('refresh', claims.sub) [ Prev ] [ Next ] [ Close ]
README

DeepSeek Orb

English | 中文

A desktop AI agent assistant that stays on call, for macOS and Windows, built on DeepSeek Harness (dsh 0.2.1-alpha.2).

DeepSeek Orb is an unofficial community project and is not affiliated with DeepSeek AI.

A floating ball rests at the edge of your screen. Tell it what you need: quick, visible actions are executed directly in the current app, finished in front of you through Computer Use; long-running complex tasks are dispatched to a background coding session, and the result comes back to the ball when it finishes. The main window keeps the full dsh Web UI — session management, the plugin market, and model settings — so you can still write code, edit files, and run commands in it.

The app collects no telemetry.

Dual-track agent architecture

The Computer Use Agent inside the floating ball decides how every message is handled; there is no separate task classifier at runtime:

flowchart TD
    user["You speak to the floating ball"] --> orb["Computer Use Agent"]
    orb -->|"Visible GUI actions"| gui["13 GUI tools<br>executed in the current window"]
    orb -->|"Quick lookups: weather, news"| web["web_search / web_fetch<br>answered on the ball"]
    orb -->|"Dig through files, produce documents or websites"| bg["code_agent background session<br>queued, returns immediately"]
    bg -->|"Completion notice"| orb
  • Foreground track · Computer Use: visible operations — opening apps, clicking buttons, filling forms, changing settings — are executed with GUI tools right in front of you, each step based on a live screenshot of the frontmost window. Quick lookups such as weather or news headlines stay on the ball too, answered directly with web_search / web_fetch.
  • Background track · Code Agent: complex tasks — digging through files, producing Word/PPT/Excel documents, building a website — are dispatched through code_agent to a background standard session. Dispatch returns immediately; the ball tells you the background is running and you can keep chatting. When the background session ends and the ball is idle, the completion summary returns to the ball automatically, and the Computer Use Agent decides the next move — keep clicking, dispatch more background work, or wrap up.

A background session is the same kind of session you create manually in the main window; it appears in the dsh_orb folder in the main-window sidebar, where you can open, continue, or stop it. Only the ball conversation that started it can continue or stop it. Follow-up changes to the same deliverable go back to the same background session; unrelated new work opens another one. The Stop button on the floating ball only cancels the ball's Computer Use session and never affects a background session.

The floating ball

On startup the ball rests at the right edge of the primary display, slightly below vertical center, always on top. The default model is DeepSeek-V41-Flash at Max thinking.

  • Hover expands the panel, click pins it, and it collapses when the pointer leaves.
  • Drag moves the ball; release it after dragging about a fifth of it beyond the left or right screen edge and it docks into a thin gray tab — hover again to slide it back.
  • Right-click opens the menu: open the main window, floating-ball agent settings and background agent settings (each track picks its own model and thinking level), the selection-toolbar toggle, the coordinate-encoding toggle (millifraction on Windows, pixels on macOS, switchable either way), and quit DeepSeek Orb.
  • The panel carries the same conversation history as the main window, a composer that grows around the ball (Enter sends, Shift+Enter inserts a newline), the Access chip (view-only / workspace edits / full access, full by default, applying to the ball's commands and the background sessions it dispatches), History (the ball's Computer Use conversations), and New. When the agent asks you a question, the question card is answered right on the ball.
  • Drag-selecting text in any app pops up a toolbar: Search (opens Bing in your default browser), Translate (the result is written into the ball's current conversation), and Send to agent (the text sits by the input; Enter sends it together with your instruction).
  • The ball avatar can be replaced with a custom GIF / PNG / WebP (2 MB cap) in main-window Settings → Floating Ball.

Closing the main window does not quit the app; only "Quit DeepSeek Orb" in the right-click menu ends the process.

Computer Use

Every conversation on the ball runs Computer Use: the first message automatically attaches a screenshot of the frontmost app's visible windows, and another screenshot follows every action, so the model always sees the latest screen state. Screenshots automatically omit the ball, the expanded panel, the selection toolbar, and the observation border, and the overlay never blocks clicks while an operation runs. The observed window gets a glowing observation border around it, marking what the agent is looking at. Windows starts in 0–1000 millifraction encoding and macOS starts in pixels; switch either way in the ball's right-click menu or in main-window Settings → Floating Ball.

Tool list: click (single/double/right click, hold modifiers), input_text, scroll, hotkey, long_press, drag, wait, long_wait, screenshot (saved to the Desktop and copied to the clipboard), open_in_browser, open_in_finder, list_apps, open_app.

  • macOS: the first time you expand the panel you must grant Screen Recording and Accessibility permissions; the onboarding layer opens the matching System Settings pages and disappears once both are granted. Finder automation asks for authorization on first use.
  • Windows: no system-permission onboarding is needed; windows running as administrator refuse to be clicked or typed into.
  • Read the safety notice in SAFETY.md before running.

Platform support

PlatformFloating ball and Computer UseInstallers
macOS (Apple Silicon / Intel)Fully supported; Screen Recording + Accessibility permissions requiredSigned DMG / ZIP; local unsigned preview build
Windows x64Fully supported; administrator windows refuse to be operatedNSIS installer (.exe); local unsigned build
LinuxNo floating ball; the main window works, settings controls are disabledNot a release target

Getting started

Prerequisites

  • Node.js ^22.19.0 || >=24.0.0, pnpm 11.28.5.
  • A DeepSeek API key: save it in the main window under Settings → Models after startup, or export DEEPSEEK_API_KEY=… before starting. Keys live in ~/.dsh/.credentials.yaml, shared with the dsh CLI.
  • The main window's input is available only after a workspace is selected; the floating ball always uses its own dsh_orb workspace, no configuration needed.

<a id="run-from-source"></a>

Run from source

git clone https://github.com/mini-yifan/deepseek-harness-orb.git
cd deepseek-harness-orb
pnpm install
pnpm run dev:desktop

dev:desktop builds the Host, clients, Web frontend, and the Electron shell, then launches the unpackaged app; once everything is built, pnpm run start:desktop skips the build. The main window shows a splash screen first, loads the Web UI when ready, and the floating ball appears at the screen edge.

Local packaging

TargetCommandNotes
macOS (Apple Silicon)pnpm run package:desktop:mac:arm64Signed release flow in MAC-RELEASE-PACK.md; unsigned preview build in UNSIGNED-MAC-PACK.md
Windows x64 (unsigned)pnpm run package:desktop:win:x64:unsignedEnvironment variables and tool cache in UNSIGNED-WIN-PACK.md; SmartScreen will block it — choose "Run anyway"

Packaging details and platform requirements: apps/desktop/README.md.

Relationship to DeepSeek Harness

This repository is an unofficial derivative of deepseek-ai/deepseek-harness, built on dsh 0.2.1-alpha.2 and not affiliated with DeepSeek AI. The main branch carries the DeepSeek Orb product and merges upstream updates continuously. dsh is DeepSeek AI's open-source everything-is-a-plugin agent harness; this product's sessions, plugins, tools, and Web UI are all driven by it. To go deeper:

Known limitations

  • No voice input, no screen-region screenshot selection, and no per-click approval.
  • Only the floating-ball system is excluded from Computer Use screenshots; the main window can always be captured and clicked.
  • Computer Use is an experimental package shipped inside the app as a built-in runtime extra, not an npm dependency of the Desktop Host.
  • No floating ball on Linux; the local unsigned Windows installer is blocked by SmartScreen and must be allowed manually.

License

MIT, inherited from upstream DeepSeek Harness; third-party dependency licenses are disclosed in THIRD_PARTY_NOTICES.md.

Source 1 files
hooks/replay-theater.mjs 134 lines
1// Replay Theater, from "Getting started with Claude Code mods"
2// (https://claude.dev/blog/getting-started-with-claude-code-mods/, Anthropic, 2026-10-01).
3// The post publishes the five hooks that record edits and register /replay
4// unchanged; the rest of the module (stepsFor, the diff, openReplay, the pane
5// and the hint) is completed to the post's description, marked where it starts.
6
7// Replay Theater: step through the last turn's edits, one diff at a time.
8
9// —— completed to the post's description (not in the published excerpt) ——
10
11const EDIT_TOOLS = new Set(["Edit", "Write"]);
12
13// Edits of the running turn, the sealed replay of the last one, and where the pane is.
14const state = { pending: [], replay: [], index: 0, open: false, placed: false };
15
16export function register(on) {
17  on("tool.call", async ($, e, next) => {
18    if (EDIT_TOOLS.has(e.tool)) state.pending.push(...(await stepsFor($, e)));  // old/new text → diff
19    return next(e);                                                              // the edit runs untouched
20  });
21
22  on("turn.start", ($, e, next) => { if (!e.agentId) state.pending = []; return next(e); });
23
24  on("turn.complete", async ($, e, next) => {
25    const r = await next(e);
26    if (!e.agentId && state.pending.length) state.replay = state.pending;       // one replay per turn
27    return r;
28  });
29
30  on("session.start", async ($, e, next) => {
31    const r = await next(e);
32    await $.command.register({ name: "replay", description: "Step through the last turn's file edits" });
33    return r;
34  });
35  on("command.run", { command: "replay" }, async ($, e) => ({ text: (await openReplay($)) ? "Replaying" : "No edits" }));
36
37  // —— completed to the post's description (not in the published excerpt) ——
38
39  on("ui.render", { component: "Pane" }, ($, e, next) => {
40    if (e.requestId !== "replay" || !state.open) return next(e);
41    return theater($, e);
42  });
43
44  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
45    if (state.open && !state.placed) return theater($, e);          // no pane could be placed: the same tree, inline
46    if (state.open || state.replay.length === 0) return next(e);
47    const { Box, Text, Button } = $.ui.resolve(e);
48    const n = state.replay.length;
49    return Box({
50      flexDirection: "row",
51      paddingX: 1,
52      gap: 1,
53      children: [
54        Text({ color: "magenta", bold: true, children: "Replay Theater" }),
55        Text({ dimColor: true, children: `${n} edit${n === 1 ? "" : "s"} this turn — press r or type /replay` }),
56        Button({ label: "Replay", hotkey: "r", onPress: () => openReplay($) }),
57      ],
58    });
59  });
60}
61
62// One replay step per edit: the file, and the text before and after.
63async function stepsFor($, e) {
64  const file = String(e.file_path ?? "");
65  if (e.tool === "Edit") {
66    return [{ file, before: String(e.old_string ?? ""), after: String(e.new_string ?? "") }];
67  }
68  // For a Write, the old contents are read just before the write lands, so the diff is real.
69  const before = await $.fs.read(file).catch(() => "");
70  return [{ file, before, after: String(e.content ?? "") }];
71}
72
73async function openReplay($) {
74  if (state.replay.length === 0) return false;
75  state.index = 0;
76  state.open = true;
77  const opened = await $.ui.open({ id: "replay", title: "Replay Theater", focus: true });
78  state.placed = opened.isPlaced;
79  $.ui.invalidate("ui.render");
80  return true;
81}
82
83function step(delta, $) {
84  state.index = Math.min(Math.max(state.index + delta, 0), state.replay.length - 1);
85  $.ui.invalidate("ui.render");
86}
87
88async function close($) {
89  state.open = false;
90  await $.ui.close({ id: "replay" });
91  $.ui.invalidate("ui.render");
92}
93
94// Lines the edit removed and added, as a unified diff shows them.
95function diffLines(before, after) {
96  const old = before.split("\n");
97  const neu = after.split("\n");
98  const common = new Set(neu);
99  const commonOld = new Set(old);
100  return [
101    ...old.filter((line) => !common.has(line)).map((line) => ({ sign: "-", line })),
102    ...neu.filter((line) => !commonOld.has(line)).map((line) => ({ sign: "+", line })),
103  ];
104}
105
106function theater($, e) {
107  const { Box, Text, Button } = $.ui.resolve(e);
108  const current = state.replay[state.index];
109  const strip = state.replay.map((_, i) => Text({ color: i === state.index ? "magenta" : undefined, bold: i === state.index, children: ` ${i + 1} ` }));
110  const diff = diffLines(current.before, current.after).map(({ sign, line }) =>
111    Text({ color: sign === "+" ? "green" : "red", children: `${sign} ${line}` }),
112  );
113  return Box({
114    flexDirection: "column",
115    border: true,
116    borderColor: "magenta",
117    paddingX: 1,
118    children: [
119      Box({ flexDirection: "row", children: [Text({ bold: true, children: "Replay Theater " }), ...strip] }),
120      Text({ dimColor: true, children: `${current.file}  (step ${state.index + 1} of ${state.replay.length})` }),
121      ...diff,
122      Box({
123        flexDirection: "row",
124        gap: 2,
125        children: [
126          Button({ label: "Prev", hotkey: "p", disabled: state.index === 0, onPress: () => step(-1, $) }),
127          Button({ label: "Next", hotkey: "n", disabled: state.index >= state.replay.length - 1, onPress: () => step(1, $) }),
128          Button({ label: "Close", hotkey: "q", onPress: () => close($) }),
129        ],
130      }),
131    ],
132  });
133}
134