draftwatch 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {draftwatch-0.2.0 → draftwatch-0.2.2}/PKG-INFO +24 -40
  2. draftwatch-0.2.2/README.md +89 -0
  3. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/app.py +224 -17
  4. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch.egg-info/PKG-INFO +24 -40
  5. {draftwatch-0.2.0 → draftwatch-0.2.2}/pyproject.toml +1 -1
  6. draftwatch-0.2.0/README.md +0 -105
  7. {draftwatch-0.2.0 → draftwatch-0.2.2}/LICENSE +0 -0
  8. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/__init__.py +0 -0
  9. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/__main__.py +0 -0
  10. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/assets/codemirror.js +0 -0
  11. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/assets/marked.js +0 -0
  12. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/assets/purify.js +0 -0
  13. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/assets/turndown.js +0 -0
  14. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/assets/xterm.css +0 -0
  15. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/assets/xterm.js +0 -0
  16. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch/term.py +0 -0
  17. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch.egg-info/SOURCES.txt +0 -0
  18. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch.egg-info/dependency_links.txt +0 -0
  19. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch.egg-info/entry_points.txt +0 -0
  20. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch.egg-info/requires.txt +0 -0
  21. {draftwatch-0.2.0 → draftwatch-0.2.2}/draftwatch.egg-info/top_level.txt +0 -0
  22. {draftwatch-0.2.0 → draftwatch-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwatch
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Review an AI agent's edits to your writing files as a real git word-diff — keep or revert changes hunk by hunk, then commit.
5
5
  Author: Mike Konczal
6
6
  License: MIT
@@ -22,13 +22,19 @@ Dynamic: license-file
22
22
 
23
23
  # Draftwatch
24
24
 
25
- Draftwatch is a lightweight IDE for writers. You can use it to create and format new Markdown documents from scratch, or use it to review an AI agent's edits to your writing exactly the way a developer reviews a pull request.
25
+ ![Draftwatch reviewing an agent's edits: your working text on the left in a real editor, the word diff against your baseline on the right. Click any change to revert it.](https://raw.githubusercontent.com/mtkonczal/Draftwatch/main/assets/draftwatch_screenshot.png)
26
+
27
+ It’s difficult for writers to have AI act as an editor, because it’s never clear exactly what it changed. You can’t trust eyeballing it, but making the changes manually yourself means you don’t save much time. So you either ignore this potentially powerful writing helper, or you trust it at the risk that it does far more than you asked.
28
+
29
+ Enter Draftwatch. Draftwatch is a lightweight editing tool for writers. You can use it to create and format new Markdown documents from scratch, but its more powerful use is reviewing an AI agent's edits to your writing, down to the exact word. Changes are managed by git, the way a developer reviews changes to their code, but formatted for writers who need to see each exact word change to feel comfortable.
26
30
 
27
31
  When reviewing AI edits, instead of guessing what an LLM changed in your document, Draftwatch shows you the exact, git-backed word-diff. You can step through, keep or revert each change individually, and commit when you're done.
28
32
 
29
- Crucially, the diff comes from your local git—not from the AI vendor and not from a JavaScript approximation. You get absolute, independent verification of what the agent or script actually did.
33
+ The diff comes from your local git, not from the AI vendor and not from a JavaScript approximation. You get independent verification of what the agent or script actually did.
30
34
 
31
- Python 3.9+ and git are the only requirements. The front-end libraries (CodeMirror 6, marked, DOMPurify, Turndown, xterm.js) are vendored and served locally, so Draftwatch works completely offline and binds to localhost only.
35
+ Why this vibe-coded app? I designed it to be an Integrated Development Environment (IDE) for writers. Most IDEs show `git diff` at the line level, which is appropriate for coders (where each statement is generally its own line) but terrible for writers, who work at the paragraph level. This IDE is built around displaying `git diff --word-diff=porcelain`, which lets writers see the specific words being edited. Even the IDEs that do show this make it harder for writers to track edits, and they carry coding features and visual baggage that writers won’t need.
36
+
37
+ Python 3.9+ and git are the only requirements. If you are a writer, AI itself can help you install these widely used tools. The front-end libraries (CodeMirror 6, marked, DOMPurify, Turndown, xterm.js) are vendored and served locally, so Draftwatch works completely offline and binds to your local computer.
32
38
 
33
39
  ## Install
34
40
 
@@ -59,36 +65,17 @@ Or to start with a specific file:
59
65
  draftwatch draft.md
60
66
  ```
61
67
 
62
- You can also run it outside any git repository. Draftwatch starts in
63
- **write-only mode** — the editor, preview, and saving all work, but the review
64
- loop (diffs, revert, commit) is off because there's no git to compare against.
65
- The right panel offers a one-click **initialize git here** to turn the folder
66
- into a repo and switch on the full review loop.
68
+ You can also run it outside any git repository. Draftwatch starts in **write-only mode**: the editor, preview, and saving all work, but the review loop (diffs, revert, commit) is off because there's no git to compare against. The right panel offers a one-click **initialize git here** to turn the folder into a repo and switch on the full review loop.
67
69
 
68
- Starting a second instance while one is already running just works: if the
69
- default port is busy, Draftwatch picks a free one and prints the URL.
70
+ Starting a second instance while one is already running just works: if the default port is busy, Draftwatch picks a free one and prints the URL.
70
71
 
71
72
  Draftwatch opens a two-panel review window: your source on the left (a real editor with markdown highlighting, search, and a live preview), the diff against your baseline on the right. Review the changes, revert the ones you don't want, apply, then commit. Committing advances the baseline, so the next agent pass starts clean.
72
73
 
73
74
  Start without a file (`draftwatch`) to pick one in the window.
74
75
 
75
- ### Options
76
-
77
- ```
78
- draftwatch [target] [--port 8787] [--no-open] [--no-terminal] [--app | --no-app]
79
- ```
80
-
81
- - `target`: file to watch. Optional; omit it to pick one in the UI.
82
- - `--port`: default `8787`. If omitted and the default is busy, Draftwatch
83
- picks a free port automatically; pass `--port` to pin an exact one (it then
84
- fails loudly if that port is taken).
85
- - `--no-open`: don't auto-open a window (useful headless or over SSH).
86
- - `--no-terminal`: disable the embedded terminal panel entirely (its routes are removed from the server, not just hidden in the UI).
87
- - `--app` / `--no-app`: force or disable the native window. It is on by default when pywebview is installed and falls back to the browser otherwise.
88
-
89
76
  ## The terminal panel
90
77
 
91
- Click **terminal** in the toolbar to open a third panel running a real shell in your repo (macOS/Linux). Launch `claude`, `codex`, or any command there: when the agent edits the file you're watching, the diff panel lights up live — prompt on the right, review in the middle, write on the left. **hide** collapses the panel and leaves the shell running (an agent mid-task keeps working); **end session** kills the shell and everything it started. No snapshots are taken when you run commands — edits accumulate against whatever baseline you've selected, and you review them on your schedule. On Windows the panel is unavailable and Draftwatch simply runs without it.
78
+ Click **terminal** in the toolbar to open a third panel running a real shell in your repo (macOS/Linux). Launch `claude`, `codex`, or any command there: when the agent edits the file you're watching, the diff panel lights up live: prompt on the right, review in the middle, write on the left. **hide** collapses the panel and leaves the shell running (an agent mid-task keeps working); **end session** kills the shell and everything it started. No snapshots are taken when you run commands; edits accumulate against whatever baseline you've selected, and you review them on your schedule. On Windows the panel is unavailable and Draftwatch runs without it.
92
79
 
93
80
  ## Features
94
81
 
@@ -101,26 +88,23 @@ Click **terminal** in the toolbar to open a third panel running a real shell in
101
88
  - Embedded terminal panel (macOS/Linux): run your agent next to the diff without leaving the window.
102
89
  - Resizable panels: drag the dividers to change the split (double-click to reset).
103
90
 
104
- ## Security
105
-
106
- Draftwatch binds `127.0.0.1` only — there is deliberately no option to bind another interface, so the tool is never exposed on your network. Every request carries a per-session token, the Host and Origin headers are validated to defeat DNS-rebinding, and the markdown preview is sanitized with DOMPurify before rendering. The tool never talks to any LLM.
107
-
108
- The terminal panel is a real shell, so it gets extra care: its input routes accept the session token **only** as a request header (keystrokes never appear in URLs, browser history, or logs), the server pipes bytes to the PTY without ever parsing them, ending a session kills the shell's whole process group, and no shell outlives Draftwatch. `--no-terminal` removes the feature from the server entirely.
109
-
110
- ## Tests
91
+ ### Options
111
92
 
112
- ```bash
113
- python3 testing/test_reconstruct.py # reconstruction invariants
114
- python3 testing/test_acceptance.py # end-to-end server tests
93
+ ```
94
+ draftwatch [target] [--port 8787] [--no-open] [--no-terminal] [--app | --no-app]
115
95
  ```
116
96
 
117
- ## Development
118
-
119
- The front-end libraries are vendored into `draftwatch/assets/` and committed, so end users never need Node. To rebuild them: `npm install && npm run build:vendor`.
97
+ - `target`: file to watch. Optional; omit it to pick one in the UI.
98
+ - `--port`: default `8787`. If omitted and the default is busy, Draftwatch
99
+ picks a free port automatically; pass `--port` to pin an exact one (it then
100
+ fails loudly if that port is taken).
101
+ - `--no-open`: don't auto-open a window (useful headless or over SSH).
102
+ - `--no-terminal`: disable the embedded terminal panel entirely (its routes are removed from the server, not just hidden in the UI).
103
+ - `--app` / `--no-app`: force or disable the native window. It is on by default when pywebview is installed and falls back to the browser otherwise.
120
104
 
121
105
  ## Author
122
106
 
123
- Built by Mike Konczal. Vibe-coded with Fable 5.
107
+ Built by Mike Konczal. You can find out more about me at my webpage [here](https://www.mikekonczal.com/). Vibe-coded with Fable 5.
124
108
 
125
109
  ## License
126
110
 
@@ -0,0 +1,89 @@
1
+ # Draftwatch
2
+
3
+ ![Draftwatch reviewing an agent's edits: your working text on the left in a real editor, the word diff against your baseline on the right. Click any change to revert it.](https://raw.githubusercontent.com/mtkonczal/Draftwatch/main/assets/draftwatch_screenshot.png)
4
+
5
+ It’s difficult for writers to have AI act as an editor, because it’s never clear exactly what it changed. You can’t trust eyeballing it, but making the changes manually yourself means you don’t save much time. So you either ignore this potentially powerful writing helper, or you trust it at the risk that it does far more than you asked.
6
+
7
+ Enter Draftwatch. Draftwatch is a lightweight editing tool for writers. You can use it to create and format new Markdown documents from scratch, but its more powerful use is reviewing an AI agent's edits to your writing, down to the exact word. Changes are managed by git, the way a developer reviews changes to their code, but formatted for writers who need to see each exact word change to feel comfortable.
8
+
9
+ When reviewing AI edits, instead of guessing what an LLM changed in your document, Draftwatch shows you the exact, git-backed word-diff. You can step through, keep or revert each change individually, and commit when you're done.
10
+
11
+ The diff comes from your local git, not from the AI vendor and not from a JavaScript approximation. You get independent verification of what the agent or script actually did.
12
+
13
+ Why this vibe-coded app? I designed it to be an Integrated Development Environment (IDE) for writers. Most IDEs show `git diff` at the line level, which is appropriate for coders (where each statement is generally its own line) but terrible for writers, who work at the paragraph level. This IDE is built around displaying `git diff --word-diff=porcelain`, which lets writers see the specific words being edited. Even the IDEs that do show this make it harder for writers to track edits, and they carry coding features and visual baggage that writers won’t need.
14
+
15
+ Python 3.9+ and git are the only requirements. If you are a writer, AI itself can help you install these widely used tools. The front-end libraries (CodeMirror 6, marked, DOMPurify, Turndown, xterm.js) are vendored and served locally, so Draftwatch works completely offline and binds to your local computer.
16
+
17
+ ## Install
18
+
19
+ Draftwatch is available on PyPI. You can install or run it using your preferred Python package manager:
20
+
21
+ ```bash
22
+ # pipx
23
+ pipx install draftwatch
24
+
25
+ # uv
26
+ uv tool install draftwatch
27
+
28
+ # standard pip
29
+ pip install draftwatch
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Run it from inside a git repository you write in:
35
+
36
+ ```bash
37
+ draftwatch
38
+ ```
39
+
40
+ Or to start with a specific file:
41
+
42
+ ```bash
43
+ draftwatch draft.md
44
+ ```
45
+
46
+ You can also run it outside any git repository. Draftwatch starts in **write-only mode**: the editor, preview, and saving all work, but the review loop (diffs, revert, commit) is off because there's no git to compare against. The right panel offers a one-click **initialize git here** to turn the folder into a repo and switch on the full review loop.
47
+
48
+ Starting a second instance while one is already running just works: if the default port is busy, Draftwatch picks a free one and prints the URL.
49
+
50
+ Draftwatch opens a two-panel review window: your source on the left (a real editor with markdown highlighting, search, and a live preview), the diff against your baseline on the right. Review the changes, revert the ones you don't want, apply, then commit. Committing advances the baseline, so the next agent pass starts clean.
51
+
52
+ Start without a file (`draftwatch`) to pick one in the window.
53
+
54
+ ## The terminal panel
55
+
56
+ Click **terminal** in the toolbar to open a third panel running a real shell in your repo (macOS/Linux). Launch `claude`, `codex`, or any command there: when the agent edits the file you're watching, the diff panel lights up live: prompt on the right, review in the middle, write on the left. **hide** collapses the panel and leaves the shell running (an agent mid-task keeps working); **end session** kills the shell and everything it started. No snapshots are taken when you run commands; edits accumulate against whatever baseline you've selected, and you review them on your schedule. On Windows the panel is unavailable and Draftwatch runs without it.
57
+
58
+ ## Features
59
+
60
+ - Real git word-diffs, so what you see is exactly what git sees.
61
+ - Keep or revert changes one hunk at a time, or all at once, then commit from the UI.
62
+ - Switchable baseline: last push, HEAD, or an earlier commit.
63
+ - Jump between changes or collapse to a changes-only view for long documents.
64
+ - Editable markdown preview alongside the raw source.
65
+ - You and the agent can both edit; your unsaved work is never clobbered when the file changes on disk.
66
+ - Embedded terminal panel (macOS/Linux): run your agent next to the diff without leaving the window.
67
+ - Resizable panels: drag the dividers to change the split (double-click to reset).
68
+
69
+ ### Options
70
+
71
+ ```
72
+ draftwatch [target] [--port 8787] [--no-open] [--no-terminal] [--app | --no-app]
73
+ ```
74
+
75
+ - `target`: file to watch. Optional; omit it to pick one in the UI.
76
+ - `--port`: default `8787`. If omitted and the default is busy, Draftwatch
77
+ picks a free port automatically; pass `--port` to pin an exact one (it then
78
+ fails loudly if that port is taken).
79
+ - `--no-open`: don't auto-open a window (useful headless or over SSH).
80
+ - `--no-terminal`: disable the embedded terminal panel entirely (its routes are removed from the server, not just hidden in the UI).
81
+ - `--app` / `--no-app`: force or disable the native window. It is on by default when pywebview is installed and falls back to the browser otherwise.
82
+
83
+ ## Author
84
+
85
+ Built by Mike Konczal. You can find out more about me at my webpage [here](https://www.mikekonczal.com/). Vibe-coded with Fable 5.
86
+
87
+ ## License
88
+
89
+ MIT. See `LICENSE`.
@@ -40,8 +40,8 @@ TERM_SUPPORTED = _term is not None
40
40
  # Single source of truth for the version and the date of the latest release.
41
41
  # __init__.py re-exports __version__; the About panel shows both (injected into
42
42
  # the served page from these constants).
43
- __version__ = "0.2.0"
44
- RELEASE_DATE = "2026-07-06"
43
+ __version__ = "0.2.2"
44
+ RELEASE_DATE = "2026-09-23"
45
45
 
46
46
  # Preferred port. When --port is not given, Draftwatch tries this first and
47
47
  # falls back to a free port if it is busy (so a second instance can start while
@@ -145,6 +145,21 @@ def push_ref(root):
145
145
  return None, None, None
146
146
 
147
147
 
148
+ def verify_commit_ref(root, ref):
149
+ """True only if `ref` names an existing commit object in `root`.
150
+
151
+ Two guards: reject anything not a plain string or beginning with '-' (so a
152
+ ref can never be parsed by a later `git diff <ref>` as an option — e.g.
153
+ `--output=FILE` would make git write to an arbitrary path), then confirm the
154
+ ref actually resolves to a commit with `rev-parse --verify`. The client only
155
+ ever sends full SHAs from the commit dropdown; this hardens the API against a
156
+ hand-crafted request (which still needs the session token to arrive)."""
157
+ if not isinstance(ref, str) or not ref or ref.startswith("-"):
158
+ return False
159
+ rc, _, _ = run_git(["rev-parse", "--verify", "--quiet", ref + "^{commit}"], root)
160
+ return rc == 0
161
+
162
+
148
163
  def git_diff_text(root, baseline, relpath, abspath):
149
164
  """Run the appropriate word-diff command. Returns diff text ('' if no diff).
150
165
 
@@ -229,6 +244,53 @@ def resolve_new_file(root, path):
229
244
  return cand
230
245
 
231
246
 
247
+ # Local images the preview may load: extension -> MIME type. The image route
248
+ # serves only these, only from inside the repo root, and only up to the size
249
+ # cap, so it can't be used to read arbitrary files (source, .env, keys).
250
+ IMAGE_TYPES = {
251
+ ".png": "image/png",
252
+ ".jpg": "image/jpeg",
253
+ ".jpeg": "image/jpeg",
254
+ ".gif": "image/gif",
255
+ ".webp": "image/webp",
256
+ ".avif": "image/avif",
257
+ ".svg": "image/svg+xml",
258
+ ".bmp": "image/bmp",
259
+ ".ico": "image/x-icon",
260
+ }
261
+ MAX_IMAGE_BYTES = 25 * 1024 * 1024
262
+
263
+
264
+ def resolve_image(root, relpath, src):
265
+ """Resolve an <img src> from the markdown to (abspath, mime), or raise ValueError.
266
+
267
+ `src` is the raw attribute value, resolved relative to the open file's
268
+ directory (the repo root when no file is open), the way a markdown renderer
269
+ on disk would. URLs, absolute paths, and anything resolving outside the repo
270
+ or to a non-image type are refused.
271
+ """
272
+ from urllib.parse import unquote
273
+ s = (src or "").split("#", 1)[0].split("?", 1)[0].strip()
274
+ s = unquote(s)
275
+ if not s:
276
+ raise ValueError("no image path given")
277
+ if ":" in s.split("/", 1)[0] or s.startswith("/") or os.path.isabs(s):
278
+ raise ValueError("only relative image paths are served")
279
+ base = os.path.join(root, os.path.dirname(relpath)) if relpath else root
280
+ cand = os.path.realpath(os.path.join(base, s))
281
+ root_prefix = root.rstrip(os.sep) + os.sep
282
+ if not cand.startswith(root_prefix):
283
+ raise ValueError("refusing an image outside the repository")
284
+ mime = IMAGE_TYPES.get(os.path.splitext(cand)[1].lower())
285
+ if mime is None:
286
+ raise ValueError("not an image type the preview serves")
287
+ if not os.path.isfile(cand):
288
+ raise ValueError("image not found")
289
+ if os.path.getsize(cand) > MAX_IMAGE_BYTES:
290
+ raise ValueError("image too large")
291
+ return cand, mime
292
+
293
+
232
294
  def list_repo_files(root, has_repo=True, limit=5000):
233
295
  """Files for the picker. In a git repo: tracked + non-ignored untracked,
234
296
  sorted, deduped. In write-only mode (no repo), fall back to a filesystem
@@ -860,6 +922,31 @@ class Handler(BaseHTTPRequestHandler):
860
922
  self.send_header("Content-Type", ctype)
861
923
  self.send_header("Content-Length", str(len(body)))
862
924
  self.send_header("Cache-Control", "no-store")
925
+ # Defense-in-depth headers. The preview already runs agent-authored
926
+ # markdown through DOMPurify; these add a second layer and, crucially,
927
+ # keep the session token from leaking off-origin:
928
+ # Referrer-Policy — the token rides in the launch URL; never send a
929
+ # Referer carrying it to any resource the page loads.
930
+ # CSP — scripts/styles/fetch are same-origin only, so a
931
+ # sanitizer bypass still can't call out or exfiltrate
932
+ # (connect-src 'self'). base/form/frame are locked
933
+ # down. img-src stays permissive (data:/https:) so a
934
+ # writer's legitimate external images still render;
935
+ # with connect-src+Referrer-Policy above, a stray
936
+ # image beacon carries no token and no document data.
937
+ # blob: is for local images, fetched with the token
938
+ # from /api/image and shown via object URLs.
939
+ # nosniff — don't let a response be reinterpreted as another type.
940
+ self.send_header("Referrer-Policy", "no-referrer")
941
+ self.send_header("X-Content-Type-Options", "nosniff")
942
+ self.send_header(
943
+ "Content-Security-Policy",
944
+ "default-src 'self'; "
945
+ "script-src 'self' 'unsafe-inline'; "
946
+ "style-src 'self' 'unsafe-inline'; "
947
+ "img-src 'self' data: blob: https:; "
948
+ "connect-src 'self'; "
949
+ "base-uri 'none'; form-action 'none'; frame-ancestors 'none'")
863
950
  self.end_headers()
864
951
  try:
865
952
  self.wfile.write(body)
@@ -901,6 +988,8 @@ class Handler(BaseHTTPRequestHandler):
901
988
  self._api_baselines()
902
989
  elif path == "/api/files":
903
990
  self._api_files()
991
+ elif path == "/api/image":
992
+ self._api_image()
904
993
  else:
905
994
  self._send(404, "not found", "text/plain")
906
995
 
@@ -993,6 +1082,22 @@ class Handler(BaseHTTPRequestHandler):
993
1082
  self._json({"files": list_repo_files(st.root, has_repo),
994
1083
  "current": current, "repo": has_repo})
995
1084
 
1085
+ def _api_image(self):
1086
+ """A local image referenced by the open file, for the preview. Token-gated
1087
+ like the rest of /api/* (the client fetches it with the header and shows
1088
+ it as a blob: URL), so file contents never load without the session."""
1089
+ st = self.state
1090
+ with st.lock:
1091
+ relpath = st.relpath
1092
+ vals = self._query().get("src")
1093
+ try:
1094
+ abspath, mime = resolve_image(st.root, relpath, vals[0] if vals else "")
1095
+ with open(abspath, "rb") as fh:
1096
+ data = fh.read()
1097
+ except (ValueError, OSError) as e:
1098
+ return self._send(404, str(e), "text/plain")
1099
+ self._send(200, data, mime)
1100
+
996
1101
  # ---- POST ----
997
1102
  def do_POST(self):
998
1103
  path = self.path.split("?", 1)[0]
@@ -1170,6 +1275,8 @@ class Handler(BaseHTTPRequestHandler):
1170
1275
  ref = body.get("ref")
1171
1276
  if not ref:
1172
1277
  return self._json({"error": "missing ref"}, 400)
1278
+ if not verify_commit_ref(st.root, ref):
1279
+ return self._json({"error": "invalid or unknown commit ref"}, 400)
1173
1280
  label = body.get("label", ref)
1174
1281
  with st.lock:
1175
1282
  st.baseline = {"kind": "commit", "ref": ref, "label": label}
@@ -1352,6 +1459,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
1352
1459
  <html lang="en">
1353
1460
  <head>
1354
1461
  <meta charset="utf-8">
1462
+ <meta name="referrer" content="no-referrer">
1355
1463
  <meta name="viewport" content="width=device-width, initial-scale=1">
1356
1464
  <title>Draftwatch</title>
1357
1465
  <script src="/static/codemirror.js"></script>
@@ -1639,8 +1747,13 @@ INDEX_HTML = r"""<!DOCTYPE html>
1639
1747
  .panel.term h2 button {
1640
1748
  text-transform: none; letter-spacing: normal; font-weight: 400;
1641
1749
  }
1642
- #term-host { flex: 1; min-height: 0; padding: 6px 2px 2px 8px; }
1643
- #term-host .terminal { height: 100%; }
1750
+ /* Padding lives on the xterm element, not #term-host: the fit addon sizes
1751
+ the terminal from the parent's computed width/height (which, with the
1752
+ global border-box rule, includes the parent's padding) minus the
1753
+ terminal element's own padding. Padding the host makes fit() run one
1754
+ column wide, pushing text under the viewport scrollbar. */
1755
+ #term-host { flex: 1; min-height: 0; }
1756
+ #term-host .terminal { height: 100%; padding: 6px 2px 2px 8px; }
1644
1757
  .panel h2 {
1645
1758
  margin: 0;
1646
1759
  padding: 6px 12px;
@@ -1681,30 +1794,42 @@ INDEX_HTML = r"""<!DOCTYPE html>
1681
1794
  background: rgba(127, 127, 127, .28);
1682
1795
  }
1683
1796
  #editor-host .cm-placeholder { color: var(--muted); font-style: italic; }
1684
- /* CM search panel, themed with the app variables (light/dark for free) */
1685
- .cm-panels {
1686
- background: var(--panel) !important;
1687
- color: var(--text) !important;
1688
- border-color: var(--border) !important;
1797
+ /* CM search panel, themed with the app variables (light/dark for free).
1798
+ Scoped under #editor-host on purpose: CodeMirror's base theme injects
1799
+ two-class rules (e.g. `.ͼ2 .cm-textfield`, `.ͼ2 .cm-button`) that outrank a
1800
+ bare `.cm-panels input`, and since no dark theme is registered with CM it
1801
+ always applies its LIGHT palette (white input, pale gradient buttons, 70%
1802
+ font). Unscoped, that left near-white dark-mode text on a white field. */
1803
+ #editor-host .cm-panels {
1804
+ background: var(--panel);
1805
+ color: var(--text);
1806
+ border-color: var(--border);
1689
1807
  }
1690
- .cm-panels input, .cm-panels button, .cm-panels label {
1808
+ #editor-host .cm-panels input, #editor-host .cm-panels button, #editor-host .cm-panels label {
1691
1809
  font-family: var(--mono);
1692
1810
  font-size: 12px;
1693
1811
  color: var(--text);
1694
1812
  }
1695
- .cm-panels input {
1813
+ #editor-host .cm-panels input[type="checkbox"] { accent-color: var(--accent); }
1814
+ #editor-host .cm-panels .cm-textfield {
1696
1815
  background: var(--bg);
1697
1816
  border: 1px solid var(--border);
1698
1817
  border-radius: 4px;
1699
1818
  }
1700
- .cm-panels button {
1819
+ #editor-host .cm-panels .cm-textfield:focus { outline: none; border-color: var(--accent); }
1820
+ #editor-host .cm-panels .cm-button {
1701
1821
  background: var(--bg);
1702
- border: 1px solid var(--border) !important;
1822
+ background-image: none;
1823
+ border: 1px solid var(--border);
1703
1824
  border-radius: 4px;
1704
1825
  cursor: pointer;
1705
1826
  }
1706
- .cm-searchMatch { background: var(--warn-bg); outline: 1px solid var(--warn-border); }
1707
- .cm-searchMatch-selected { background: var(--warn-border); }
1827
+ #editor-host .cm-panels .cm-button:hover { border-color: var(--accent); }
1828
+ #editor-host .cm-panels .cm-button:active { background-image: none; }
1829
+ #editor-host .cm-searchMatch { background: var(--warn-bg); outline: 1px solid var(--warn-border); }
1830
+ #editor-host .cm-searchMatch-selected { background: var(--warn-border); }
1831
+ /* highlightSelectionMatches: CM's default is a bright translucent green */
1832
+ #editor-host .cm-selectionMatch { background: rgba(127, 127, 127, .22); }
1708
1833
 
1709
1834
  /* rendered markdown preview: reading typography, sanitized content only */
1710
1835
  #preview {
@@ -1744,6 +1869,8 @@ INDEX_HTML = r"""<!DOCTYPE html>
1744
1869
  color: var(--muted);
1745
1870
  }
1746
1871
  #preview img { max-width: 100%; }
1872
+ /* a local image the server could not serve: the alt text shows in its place */
1873
+ #preview img.img-missing { outline: 1px dashed var(--del-fg); outline-offset: 2px; color: var(--muted); }
1747
1874
  #preview hr { border: none; border-top: 1px solid var(--border); }
1748
1875
  #preview table { border-collapse: collapse; }
1749
1876
  #preview th, #preview td { border: 1px solid var(--border); padding: 4px 9px; }
@@ -2083,9 +2210,21 @@ INDEX_HTML = r"""<!DOCTYPE html>
2083
2210
  }
2084
2211
 
2085
2212
  // ---- session token (from the URL draftwatch printed/opened) ----
2213
+ // Read it once, then keep it in sessionStorage and strip it out of the
2214
+ // visible URL. This keeps the token out of the address bar, browser history,
2215
+ // and any screen-share, while a reload still authenticates (sessionStorage
2216
+ // survives reloads within the tab). Note: this does NOT remove the token from
2217
+ // the browser process's command line on a shared host — see the README
2218
+ // security notes; prefer the native window (--app) there.
2086
2219
  var TOKEN = (function () {
2220
+ var t = "";
2087
2221
  var m = /(?:\?|&)t=([^&]+)/.exec(window.location.search);
2088
- return m ? decodeURIComponent(m[1]) : "";
2222
+ if (m) t = decodeURIComponent(m[1]);
2223
+ try {
2224
+ if (t) sessionStorage.setItem("draftwatch-token", t);
2225
+ else t = sessionStorage.getItem("draftwatch-token") || "";
2226
+ } catch (e) {}
2227
+ return t;
2089
2228
  })();
2090
2229
 
2091
2230
  // ---- launch surface (native window vs browser) ----
@@ -2096,6 +2235,17 @@ INDEX_HTML = r"""<!DOCTYPE html>
2096
2235
  var APP_FALLBACK = /(?:\?|&)appfallback=1(?:&|$)/.test(window.location.search);
2097
2236
  if (APP_MODE) document.body.classList.add("app-mode");
2098
2237
 
2238
+ // Now that the token and launch flags have been read, drop `t` from the
2239
+ // address bar (other params are preserved). The token still lives in the JS
2240
+ // var and sessionStorage, so requests and reloads keep working.
2241
+ try {
2242
+ var _u = new URL(window.location.href);
2243
+ if (_u.searchParams.has("t")) {
2244
+ _u.searchParams.delete("t");
2245
+ window.history.replaceState({}, "", _u.pathname + _u.search + _u.hash);
2246
+ }
2247
+ } catch (e) {}
2248
+
2099
2249
  // ---- server notification of client state (turn-based contract) ----
2100
2250
  function postJSON(url, obj, cb) {
2101
2251
  fetch(url, {
@@ -2864,6 +3014,61 @@ INDEX_HTML = r"""<!DOCTYPE html>
2864
3014
  strongDelimiter: "**",
2865
3015
  linkStyle: "inlined"
2866
3016
  });
3017
+
3018
+ // Local images (e.g. ![](figs/chart.png)). A relative src would resolve to a
3019
+ // bare path on this server and 404, so the sanitizer moves it to data-md-src
3020
+ // (no request fires from the markup), and loadLocalImages fetches it through
3021
+ // the token-gated /api/image route and shows it as a blob: URL. data-md-src
3022
+ // keeps the markdown's own path, which Turndown writes back on preview edits
3023
+ // (never the blob: URL; its default rule would also drop a src-less image).
3024
+ function isLocalSrc(s) {
3025
+ return !!s && !/^([a-z][a-z0-9+.-]*:|\/|#)/i.test(s);
3026
+ }
3027
+ DOMPurify.addHook("afterSanitizeAttributes", function (node) {
3028
+ if (node.nodeName !== "IMG") return;
3029
+ var src = node.getAttribute("src");
3030
+ if (!isLocalSrc(src)) return;
3031
+ node.setAttribute("data-md-src", src);
3032
+ node.removeAttribute("src");
3033
+ });
3034
+ TURN.addRule("localImage", {
3035
+ filter: function (node) {
3036
+ return node.nodeName === "IMG" && node.hasAttribute("data-md-src");
3037
+ },
3038
+ replacement: function (content, node) {
3039
+ var clean = function (a) { return (a || "").replace(/(\n+\s*)+/g, "\n"); };
3040
+ var title = clean(node.getAttribute("title"));
3041
+ return "![" + clean(node.getAttribute("alt")) + "](" +
3042
+ node.getAttribute("data-md-src") + (title ? ' "' + title + '"' : "") + ")";
3043
+ }
3044
+ });
3045
+ var imageCache = {}; // "<file>|<src>" -> Promise of a blob: URL, or null
3046
+ function clearImageCache() {
3047
+ Object.keys(imageCache).forEach(function (k) {
3048
+ imageCache[k].then(function (u) { if (u) URL.revokeObjectURL(u); });
3049
+ });
3050
+ imageCache = {};
3051
+ }
3052
+ function loadLocalImages(root) {
3053
+ root.querySelectorAll("img[data-md-src]").forEach(function (img) {
3054
+ var src = img.getAttribute("data-md-src");
3055
+ var key = (currentFile || "") + "|" + src;
3056
+ if (!imageCache[key]) {
3057
+ imageCache[key] = fetch("/api/image?src=" + encodeURIComponent(src),
3058
+ { headers: { "X-Draftwatch-Token": TOKEN } })
3059
+ .then(function (r) { return r.ok ? r.blob() : null; })
3060
+ .then(function (b) { return b ? URL.createObjectURL(b) : null; })
3061
+ .catch(function () { return null; });
3062
+ // don't cache a miss: the image may be added to disk later
3063
+ imageCache[key].then(function (u) { if (!u) delete imageCache[key]; });
3064
+ }
3065
+ imageCache[key].then(function (u) {
3066
+ if (u) { img.src = u; img.classList.remove("img-missing"); }
3067
+ else img.classList.add("img-missing");
3068
+ });
3069
+ });
3070
+ }
3071
+
2867
3072
  function renderPreview() {
2868
3073
  var out = "";
2869
3074
  try {
@@ -2876,6 +3081,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
2876
3081
  // (innerHTML assignment does not fire 'input', but keep the intent explicit)
2877
3082
  $("preview").innerHTML = out ||
2878
3083
  '<p class="empty">nothing to preview</p>';
3084
+ loadLocalImages($("preview"));
2879
3085
  }
2880
3086
 
2881
3087
  // Convert the edited rendered HTML back to markdown and push it into the
@@ -2906,7 +3112,8 @@ INDEX_HTML = r"""<!DOCTYPE html>
2906
3112
  $("preview").setAttribute("contenteditable", on ? "true" : "false");
2907
3113
  // the format buttons + save stay available in preview — you can write here too
2908
3114
  $("view-toggle").textContent = on ? "source" : "preview";
2909
- if (on) renderPreview();
3115
+ // re-read local images each time preview opens, so a regenerated chart shows
3116
+ if (on) { clearImageCache(); renderPreview(); }
2910
3117
  }
2911
3118
  $("view-toggle").addEventListener("click", function () { setPreviewMode(!previewMode); });
2912
3119
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwatch
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Review an AI agent's edits to your writing files as a real git word-diff — keep or revert changes hunk by hunk, then commit.
5
5
  Author: Mike Konczal
6
6
  License: MIT
@@ -22,13 +22,19 @@ Dynamic: license-file
22
22
 
23
23
  # Draftwatch
24
24
 
25
- Draftwatch is a lightweight IDE for writers. You can use it to create and format new Markdown documents from scratch, or use it to review an AI agent's edits to your writing exactly the way a developer reviews a pull request.
25
+ ![Draftwatch reviewing an agent's edits: your working text on the left in a real editor, the word diff against your baseline on the right. Click any change to revert it.](https://raw.githubusercontent.com/mtkonczal/Draftwatch/main/assets/draftwatch_screenshot.png)
26
+
27
+ It’s difficult for writers to have AI act as an editor, because it’s never clear exactly what it changed. You can’t trust eyeballing it, but making the changes manually yourself means you don’t save much time. So you either ignore this potentially powerful writing helper, or you trust it at the risk that it does far more than you asked.
28
+
29
+ Enter Draftwatch. Draftwatch is a lightweight editing tool for writers. You can use it to create and format new Markdown documents from scratch, but its more powerful use is reviewing an AI agent's edits to your writing, down to the exact word. Changes are managed by git, the way a developer reviews changes to their code, but formatted for writers who need to see each exact word change to feel comfortable.
26
30
 
27
31
  When reviewing AI edits, instead of guessing what an LLM changed in your document, Draftwatch shows you the exact, git-backed word-diff. You can step through, keep or revert each change individually, and commit when you're done.
28
32
 
29
- Crucially, the diff comes from your local git—not from the AI vendor and not from a JavaScript approximation. You get absolute, independent verification of what the agent or script actually did.
33
+ The diff comes from your local git, not from the AI vendor and not from a JavaScript approximation. You get independent verification of what the agent or script actually did.
30
34
 
31
- Python 3.9+ and git are the only requirements. The front-end libraries (CodeMirror 6, marked, DOMPurify, Turndown, xterm.js) are vendored and served locally, so Draftwatch works completely offline and binds to localhost only.
35
+ Why this vibe-coded app? I designed it to be an Integrated Development Environment (IDE) for writers. Most IDEs show `git diff` at the line level, which is appropriate for coders (where each statement is generally its own line) but terrible for writers, who work at the paragraph level. This IDE is built around displaying `git diff --word-diff=porcelain`, which lets writers see the specific words being edited. Even the IDEs that do show this make it harder for writers to track edits, and they carry coding features and visual baggage that writers won’t need.
36
+
37
+ Python 3.9+ and git are the only requirements. If you are a writer, AI itself can help you install these widely used tools. The front-end libraries (CodeMirror 6, marked, DOMPurify, Turndown, xterm.js) are vendored and served locally, so Draftwatch works completely offline and binds to your local computer.
32
38
 
33
39
  ## Install
34
40
 
@@ -59,36 +65,17 @@ Or to start with a specific file:
59
65
  draftwatch draft.md
60
66
  ```
61
67
 
62
- You can also run it outside any git repository. Draftwatch starts in
63
- **write-only mode** — the editor, preview, and saving all work, but the review
64
- loop (diffs, revert, commit) is off because there's no git to compare against.
65
- The right panel offers a one-click **initialize git here** to turn the folder
66
- into a repo and switch on the full review loop.
68
+ You can also run it outside any git repository. Draftwatch starts in **write-only mode**: the editor, preview, and saving all work, but the review loop (diffs, revert, commit) is off because there's no git to compare against. The right panel offers a one-click **initialize git here** to turn the folder into a repo and switch on the full review loop.
67
69
 
68
- Starting a second instance while one is already running just works: if the
69
- default port is busy, Draftwatch picks a free one and prints the URL.
70
+ Starting a second instance while one is already running just works: if the default port is busy, Draftwatch picks a free one and prints the URL.
70
71
 
71
72
  Draftwatch opens a two-panel review window: your source on the left (a real editor with markdown highlighting, search, and a live preview), the diff against your baseline on the right. Review the changes, revert the ones you don't want, apply, then commit. Committing advances the baseline, so the next agent pass starts clean.
72
73
 
73
74
  Start without a file (`draftwatch`) to pick one in the window.
74
75
 
75
- ### Options
76
-
77
- ```
78
- draftwatch [target] [--port 8787] [--no-open] [--no-terminal] [--app | --no-app]
79
- ```
80
-
81
- - `target`: file to watch. Optional; omit it to pick one in the UI.
82
- - `--port`: default `8787`. If omitted and the default is busy, Draftwatch
83
- picks a free port automatically; pass `--port` to pin an exact one (it then
84
- fails loudly if that port is taken).
85
- - `--no-open`: don't auto-open a window (useful headless or over SSH).
86
- - `--no-terminal`: disable the embedded terminal panel entirely (its routes are removed from the server, not just hidden in the UI).
87
- - `--app` / `--no-app`: force or disable the native window. It is on by default when pywebview is installed and falls back to the browser otherwise.
88
-
89
76
  ## The terminal panel
90
77
 
91
- Click **terminal** in the toolbar to open a third panel running a real shell in your repo (macOS/Linux). Launch `claude`, `codex`, or any command there: when the agent edits the file you're watching, the diff panel lights up live — prompt on the right, review in the middle, write on the left. **hide** collapses the panel and leaves the shell running (an agent mid-task keeps working); **end session** kills the shell and everything it started. No snapshots are taken when you run commands — edits accumulate against whatever baseline you've selected, and you review them on your schedule. On Windows the panel is unavailable and Draftwatch simply runs without it.
78
+ Click **terminal** in the toolbar to open a third panel running a real shell in your repo (macOS/Linux). Launch `claude`, `codex`, or any command there: when the agent edits the file you're watching, the diff panel lights up live: prompt on the right, review in the middle, write on the left. **hide** collapses the panel and leaves the shell running (an agent mid-task keeps working); **end session** kills the shell and everything it started. No snapshots are taken when you run commands; edits accumulate against whatever baseline you've selected, and you review them on your schedule. On Windows the panel is unavailable and Draftwatch runs without it.
92
79
 
93
80
  ## Features
94
81
 
@@ -101,26 +88,23 @@ Click **terminal** in the toolbar to open a third panel running a real shell in
101
88
  - Embedded terminal panel (macOS/Linux): run your agent next to the diff without leaving the window.
102
89
  - Resizable panels: drag the dividers to change the split (double-click to reset).
103
90
 
104
- ## Security
105
-
106
- Draftwatch binds `127.0.0.1` only — there is deliberately no option to bind another interface, so the tool is never exposed on your network. Every request carries a per-session token, the Host and Origin headers are validated to defeat DNS-rebinding, and the markdown preview is sanitized with DOMPurify before rendering. The tool never talks to any LLM.
107
-
108
- The terminal panel is a real shell, so it gets extra care: its input routes accept the session token **only** as a request header (keystrokes never appear in URLs, browser history, or logs), the server pipes bytes to the PTY without ever parsing them, ending a session kills the shell's whole process group, and no shell outlives Draftwatch. `--no-terminal` removes the feature from the server entirely.
109
-
110
- ## Tests
91
+ ### Options
111
92
 
112
- ```bash
113
- python3 testing/test_reconstruct.py # reconstruction invariants
114
- python3 testing/test_acceptance.py # end-to-end server tests
93
+ ```
94
+ draftwatch [target] [--port 8787] [--no-open] [--no-terminal] [--app | --no-app]
115
95
  ```
116
96
 
117
- ## Development
118
-
119
- The front-end libraries are vendored into `draftwatch/assets/` and committed, so end users never need Node. To rebuild them: `npm install && npm run build:vendor`.
97
+ - `target`: file to watch. Optional; omit it to pick one in the UI.
98
+ - `--port`: default `8787`. If omitted and the default is busy, Draftwatch
99
+ picks a free port automatically; pass `--port` to pin an exact one (it then
100
+ fails loudly if that port is taken).
101
+ - `--no-open`: don't auto-open a window (useful headless or over SSH).
102
+ - `--no-terminal`: disable the embedded terminal panel entirely (its routes are removed from the server, not just hidden in the UI).
103
+ - `--app` / `--no-app`: force or disable the native window. It is on by default when pywebview is installed and falls back to the browser otherwise.
120
104
 
121
105
  ## Author
122
106
 
123
- Built by Mike Konczal. Vibe-coded with Fable 5.
107
+ Built by Mike Konczal. You can find out more about me at my webpage [here](https://www.mikekonczal.com/). Vibe-coded with Fable 5.
124
108
 
125
109
  ## License
126
110
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "draftwatch"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Review an AI agent's edits to your writing files as a real git word-diff — keep or revert changes hunk by hunk, then commit."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,105 +0,0 @@
1
- # Draftwatch
2
-
3
- Draftwatch is a lightweight IDE for writers. You can use it to create and format new Markdown documents from scratch, or use it to review an AI agent's edits to your writing exactly the way a developer reviews a pull request.
4
-
5
- When reviewing AI edits, instead of guessing what an LLM changed in your document, Draftwatch shows you the exact, git-backed word-diff. You can step through, keep or revert each change individually, and commit when you're done.
6
-
7
- Crucially, the diff comes from your local git—not from the AI vendor and not from a JavaScript approximation. You get absolute, independent verification of what the agent or script actually did.
8
-
9
- Python 3.9+ and git are the only requirements. The front-end libraries (CodeMirror 6, marked, DOMPurify, Turndown, xterm.js) are vendored and served locally, so Draftwatch works completely offline and binds to localhost only.
10
-
11
- ## Install
12
-
13
- Draftwatch is available on PyPI. You can install or run it using your preferred Python package manager:
14
-
15
- ```bash
16
- # pipx
17
- pipx install draftwatch
18
-
19
- # uv
20
- uv tool install draftwatch
21
-
22
- # standard pip
23
- pip install draftwatch
24
- ```
25
-
26
- ## Usage
27
-
28
- Run it from inside a git repository you write in:
29
-
30
- ```bash
31
- draftwatch
32
- ```
33
-
34
- Or to start with a specific file:
35
-
36
- ```bash
37
- draftwatch draft.md
38
- ```
39
-
40
- You can also run it outside any git repository. Draftwatch starts in
41
- **write-only mode** — the editor, preview, and saving all work, but the review
42
- loop (diffs, revert, commit) is off because there's no git to compare against.
43
- The right panel offers a one-click **initialize git here** to turn the folder
44
- into a repo and switch on the full review loop.
45
-
46
- Starting a second instance while one is already running just works: if the
47
- default port is busy, Draftwatch picks a free one and prints the URL.
48
-
49
- Draftwatch opens a two-panel review window: your source on the left (a real editor with markdown highlighting, search, and a live preview), the diff against your baseline on the right. Review the changes, revert the ones you don't want, apply, then commit. Committing advances the baseline, so the next agent pass starts clean.
50
-
51
- Start without a file (`draftwatch`) to pick one in the window.
52
-
53
- ### Options
54
-
55
- ```
56
- draftwatch [target] [--port 8787] [--no-open] [--no-terminal] [--app | --no-app]
57
- ```
58
-
59
- - `target`: file to watch. Optional; omit it to pick one in the UI.
60
- - `--port`: default `8787`. If omitted and the default is busy, Draftwatch
61
- picks a free port automatically; pass `--port` to pin an exact one (it then
62
- fails loudly if that port is taken).
63
- - `--no-open`: don't auto-open a window (useful headless or over SSH).
64
- - `--no-terminal`: disable the embedded terminal panel entirely (its routes are removed from the server, not just hidden in the UI).
65
- - `--app` / `--no-app`: force or disable the native window. It is on by default when pywebview is installed and falls back to the browser otherwise.
66
-
67
- ## The terminal panel
68
-
69
- Click **terminal** in the toolbar to open a third panel running a real shell in your repo (macOS/Linux). Launch `claude`, `codex`, or any command there: when the agent edits the file you're watching, the diff panel lights up live — prompt on the right, review in the middle, write on the left. **hide** collapses the panel and leaves the shell running (an agent mid-task keeps working); **end session** kills the shell and everything it started. No snapshots are taken when you run commands — edits accumulate against whatever baseline you've selected, and you review them on your schedule. On Windows the panel is unavailable and Draftwatch simply runs without it.
70
-
71
- ## Features
72
-
73
- - Real git word-diffs, so what you see is exactly what git sees.
74
- - Keep or revert changes one hunk at a time, or all at once, then commit from the UI.
75
- - Switchable baseline: last push, HEAD, or an earlier commit.
76
- - Jump between changes or collapse to a changes-only view for long documents.
77
- - Editable markdown preview alongside the raw source.
78
- - You and the agent can both edit; your unsaved work is never clobbered when the file changes on disk.
79
- - Embedded terminal panel (macOS/Linux): run your agent next to the diff without leaving the window.
80
- - Resizable panels: drag the dividers to change the split (double-click to reset).
81
-
82
- ## Security
83
-
84
- Draftwatch binds `127.0.0.1` only — there is deliberately no option to bind another interface, so the tool is never exposed on your network. Every request carries a per-session token, the Host and Origin headers are validated to defeat DNS-rebinding, and the markdown preview is sanitized with DOMPurify before rendering. The tool never talks to any LLM.
85
-
86
- The terminal panel is a real shell, so it gets extra care: its input routes accept the session token **only** as a request header (keystrokes never appear in URLs, browser history, or logs), the server pipes bytes to the PTY without ever parsing them, ending a session kills the shell's whole process group, and no shell outlives Draftwatch. `--no-terminal` removes the feature from the server entirely.
87
-
88
- ## Tests
89
-
90
- ```bash
91
- python3 testing/test_reconstruct.py # reconstruction invariants
92
- python3 testing/test_acceptance.py # end-to-end server tests
93
- ```
94
-
95
- ## Development
96
-
97
- The front-end libraries are vendored into `draftwatch/assets/` and committed, so end users never need Node. To rebuild them: `npm install && npm run build:vendor`.
98
-
99
- ## Author
100
-
101
- Built by Mike Konczal. Vibe-coded with Fable 5.
102
-
103
- ## License
104
-
105
- MIT. See `LICENSE`.
File without changes
File without changes