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 Harness

English | 中文

DeepSeek Harness (dsh) is an open-source agent harness developed by DeepSeek AI.

It is built on an everything-is-a-plugin architecture and powered by Cordis, whose design is described in _A Programming Paradigm for Spatiotemporal Composability_.

Documentation: https://deepseek-harness.github.io/deepseek-harness/

Developer preview

DeepSeek Harness is in developer preview and iterating rapidly. THERE WILL BE COMPATIBILITY-BREAKING CHANGES.

Review the safety notice before running the project.

Run

Run from npm

Install Node.js, then run:

npx @deepseek-ai/dsh web

The command starts the Web UI at http://127.0.0.1:3080 by default and opens it in the default browser for a local launch. An SSH launch only prints the host URL because the SSH client or editor owns the local forwarded address. Pass --no-open to run the server without opening a browser. See Web UI guide.

Run from source

To run from a repository checkout:

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web

pnpm run build prepares the repository artifacts. pnpm dsh web uses those built artifacts without rebuilding.

Community and support

  • Submit feedback or bug reports through GitHub Discussions.
  • Add the dsh-plugin topic to your plugin repository for discoverability.
  • Join <a href="https://discord.gg/4MrtZUhpxg">DeepSeek Harness Discord community</a>.

Contributing

See CONTRIBUTING.md.

Development

Start with the development guide and architecture documentation.

pnpm run dev:web builds, serves, and rebuilds client bundles on source edits in one terminal, and make help lists the matching Make targets for Web and Desktop; the guide's application commands section owns the full table.

For agents, follow AGENTS.md.

Citation

@misc{deepseek-harness2026,
  title={DeepSeek Harness: Everything is a Plugin},
  author={DeepSeek-AI},
  year={2026},
  publisher={GitHub},
  howpublished={\url{https://github.com/deepseek-ai/deepseek-harness}},
}

License

MIT

Third-party dependencies and their 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