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

Source snapshot of the DeepSeek Harness and eight Strique plugins, with standalone browser-interface installers.
The current snapshot uses Harness 0.2.1-rc.2 and updates Branding, Codex CLI, MCP Manager, Memory, Notify, and Skills Manager for that runtime. Univer 0.3.7 and BetterSidebar 0.28.0 use the pulled upstream source snapshots from the local development workspace. Source revisions record the upstream commits included in this refresh.
Installer owner-state updates use atomic replacement so concurrent launch/status readers cannot observe partially written JSON.
This refresh passed the isolated source setup, all 14 installer lifecycle tests, and a macOS arm64 packaged browser smoke that rendered Strique branding and BetterSidebar without page errors. Platform-specific installer builds run in GitHub Actions.
0.1.0. Leave the update URL empty until hosting is configured.strique-win32-x64 artifact from the successful run and extract its .exe.The installer bundles Node, the harness, and the configured plugins. End users do not need GitHub accounts or a separate Node installation. These are unsigned development installers.
The Build Strique installers workflow builds all five supported OS/architecture targets. See standalone documentation for installation, signed updates, and limitations.
This repository contains a source snapshot rather than submodules. On the development machine, run node setup/export-source.mjs NEW_OUTPUT_DIRECTORY and review the resulting snapshot before updating this repository. Changes on the development machine reach users only after a new snapshot is committed, a release is built, and its installer or signed update is distributed.
Credentials, sessions, dependency directories, and original Git histories are excluded. Upstream license files remain in their project folders.
hooks/replay-theater.mjs 134 lines1// 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