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

deepWidgets is an unofficial fork. It is built on DeepSeek Harness (DSH) and is not affiliated with DeepSeek or endorsed by DeepSeek. The import source and license are in UPSTREAM.md. The rest of this README is the upstream DSH 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