draftwatch 0.2.1__tar.gz → 0.2.3__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 (23) hide show
  1. {draftwatch-0.2.1 → draftwatch-0.2.3}/PKG-INFO +1 -1
  2. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/app.py +216 -34
  3. draftwatch-0.2.3/draftwatch/assets/favicon.png +0 -0
  4. draftwatch-0.2.3/draftwatch/assets/icon.png +0 -0
  5. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch.egg-info/PKG-INFO +1 -1
  6. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch.egg-info/SOURCES.txt +2 -0
  7. {draftwatch-0.2.1 → draftwatch-0.2.3}/pyproject.toml +1 -1
  8. {draftwatch-0.2.1 → draftwatch-0.2.3}/LICENSE +0 -0
  9. {draftwatch-0.2.1 → draftwatch-0.2.3}/README.md +0 -0
  10. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/__init__.py +0 -0
  11. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/__main__.py +0 -0
  12. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/assets/codemirror.js +0 -0
  13. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/assets/marked.js +0 -0
  14. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/assets/purify.js +0 -0
  15. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/assets/turndown.js +0 -0
  16. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/assets/xterm.css +0 -0
  17. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/assets/xterm.js +0 -0
  18. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch/term.py +0 -0
  19. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch.egg-info/dependency_links.txt +0 -0
  20. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch.egg-info/entry_points.txt +0 -0
  21. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch.egg-info/requires.txt +0 -0
  22. {draftwatch-0.2.1 → draftwatch-0.2.3}/draftwatch.egg-info/top_level.txt +0 -0
  23. {draftwatch-0.2.1 → draftwatch-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwatch
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -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.1"
44
- RELEASE_DATE = "2026-07-07"
43
+ __version__ = "0.2.3"
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
@@ -244,6 +244,53 @@ def resolve_new_file(root, path):
244
244
  return cand
245
245
 
246
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
+
247
294
  def list_repo_files(root, has_repo=True, limit=5000):
248
295
  """Files for the picker. In a git repo: tracked + non-ignored untracked,
249
296
  sorted, deduped. In write-only mode (no repo), fall back to a filesystem
@@ -793,6 +840,7 @@ STATIC_ASSETS = {
793
840
  "marked.js": "application/javascript; charset=utf-8",
794
841
  "purify.js": "application/javascript; charset=utf-8",
795
842
  "turndown.js": "application/javascript; charset=utf-8",
843
+ "favicon.png": "image/png",
796
844
  }
797
845
 
798
846
 
@@ -887,6 +935,8 @@ class Handler(BaseHTTPRequestHandler):
887
935
  # writer's legitimate external images still render;
888
936
  # with connect-src+Referrer-Policy above, a stray
889
937
  # image beacon carries no token and no document data.
938
+ # blob: is for local images, fetched with the token
939
+ # from /api/image and shown via object URLs.
890
940
  # nosniff — don't let a response be reinterpreted as another type.
891
941
  self.send_header("Referrer-Policy", "no-referrer")
892
942
  self.send_header("X-Content-Type-Options", "nosniff")
@@ -895,7 +945,7 @@ class Handler(BaseHTTPRequestHandler):
895
945
  "default-src 'self'; "
896
946
  "script-src 'self' 'unsafe-inline'; "
897
947
  "style-src 'self' 'unsafe-inline'; "
898
- "img-src 'self' data: https:; "
948
+ "img-src 'self' data: blob: https:; "
899
949
  "connect-src 'self'; "
900
950
  "base-uri 'none'; form-action 'none'; frame-ancestors 'none'")
901
951
  self.end_headers()
@@ -939,6 +989,8 @@ class Handler(BaseHTTPRequestHandler):
939
989
  self._api_baselines()
940
990
  elif path == "/api/files":
941
991
  self._api_files()
992
+ elif path == "/api/image":
993
+ self._api_image()
942
994
  else:
943
995
  self._send(404, "not found", "text/plain")
944
996
 
@@ -1031,6 +1083,22 @@ class Handler(BaseHTTPRequestHandler):
1031
1083
  self._json({"files": list_repo_files(st.root, has_repo),
1032
1084
  "current": current, "repo": has_repo})
1033
1085
 
1086
+ def _api_image(self):
1087
+ """A local image referenced by the open file, for the preview. Token-gated
1088
+ like the rest of /api/* (the client fetches it with the header and shows
1089
+ it as a blob: URL), so file contents never load without the session."""
1090
+ st = self.state
1091
+ with st.lock:
1092
+ relpath = st.relpath
1093
+ vals = self._query().get("src")
1094
+ try:
1095
+ abspath, mime = resolve_image(st.root, relpath, vals[0] if vals else "")
1096
+ with open(abspath, "rb") as fh:
1097
+ data = fh.read()
1098
+ except (ValueError, OSError) as e:
1099
+ return self._send(404, str(e), "text/plain")
1100
+ self._send(200, data, mime)
1101
+
1034
1102
  # ---- POST ----
1035
1103
  def do_POST(self):
1036
1104
  path = self.path.split("?", 1)[0]
@@ -1395,6 +1463,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
1395
1463
  <meta name="referrer" content="no-referrer">
1396
1464
  <meta name="viewport" content="width=device-width, initial-scale=1">
1397
1465
  <title>Draftwatch</title>
1466
+ <link rel="icon" type="image/png" href="/static/favicon.png">
1398
1467
  <script src="/static/codemirror.js"></script>
1399
1468
  <script src="/static/marked.js"></script>
1400
1469
  <script src="/static/purify.js"></script>
@@ -1550,7 +1619,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
1550
1619
 
1551
1620
  /* app chrome (native window via pywebview, ?app=1): the OS title bar
1552
1621
  already says "draftwatch · file", so the in-page wordmark is redundant —
1553
- drop it, tighten the toolbar, and make the chrome feel native (no text
1622
+ drop the word (the logo, outside .title, stays as the brand), tighten the toolbar, and make the chrome feel native (no text
1554
1623
  selection on controls, no web-page tells). */
1555
1624
  body.app-mode header .title { display: none; }
1556
1625
  body.app-mode header { padding: 5px 10px; }
@@ -1571,14 +1640,22 @@ INDEX_HTML = r"""<!DOCTYPE html>
1571
1640
  display: flex;
1572
1641
  flex-direction: column;
1573
1642
  }
1643
+ /* logo on the left, spanning both toolbar rows; the rows stack beside it */
1574
1644
  header {
1575
1645
  display: flex;
1576
- flex-direction: column;
1577
- gap: 6px;
1646
+ align-items: center;
1647
+ gap: 10px;
1578
1648
  padding: 8px 12px;
1579
1649
  border-bottom: 1px solid var(--border);
1580
1650
  background: var(--panel);
1581
1651
  }
1652
+ header .rows {
1653
+ flex: 1;
1654
+ min-width: 0;
1655
+ display: flex;
1656
+ flex-direction: column;
1657
+ gap: 6px;
1658
+ }
1582
1659
  /* Two fixed rows that never wrap into each other, so the layout stays put
1583
1660
  regardless of title length or window width:
1584
1661
  row 1 = what you're looking at (title, file picker, baseline selector —
@@ -1601,19 +1678,11 @@ INDEX_HTML = r"""<!DOCTYPE html>
1601
1678
  gap: 7px;
1602
1679
  letter-spacing: .02em;
1603
1680
  }
1604
- /* the +/− mark: the product in two characters (keep the add, strike the del) */
1605
- .mark {
1606
- display: inline-flex;
1607
- align-items: center;
1608
- background: var(--bg);
1609
- border: 1px solid var(--border);
1610
- border-radius: 5px;
1611
- padding: 0 5px;
1612
- font-weight: 700;
1613
- line-height: 1.45;
1614
- }
1615
- .mark .ma { color: var(--add-fg); }
1616
- .mark .md { color: var(--del-fg); text-decoration: line-through; }
1681
+ /* the logo: same drawing as the Dock icon and favicon (scripts/make_icon.py),
1682
+ inline so it stays crisp at any size. Fixed brand colors, not the accent. */
1683
+ .logo { width: 28px; height: 28px; flex: none; display: block; }
1684
+ header > .logo { width: 54px; height: 54px; }
1685
+ #about h2 .logo { width: 48px; height: 48px; }
1617
1686
  header .status { color: var(--muted); }
1618
1687
  header #status {
1619
1688
  flex: 1 1 auto;
@@ -1727,30 +1796,42 @@ INDEX_HTML = r"""<!DOCTYPE html>
1727
1796
  background: rgba(127, 127, 127, .28);
1728
1797
  }
1729
1798
  #editor-host .cm-placeholder { color: var(--muted); font-style: italic; }
1730
- /* CM search panel, themed with the app variables (light/dark for free) */
1731
- .cm-panels {
1732
- background: var(--panel) !important;
1733
- color: var(--text) !important;
1734
- border-color: var(--border) !important;
1799
+ /* CM search panel, themed with the app variables (light/dark for free).
1800
+ Scoped under #editor-host on purpose: CodeMirror's base theme injects
1801
+ two-class rules (e.g. `.ͼ2 .cm-textfield`, `.ͼ2 .cm-button`) that outrank a
1802
+ bare `.cm-panels input`, and since no dark theme is registered with CM it
1803
+ always applies its LIGHT palette (white input, pale gradient buttons, 70%
1804
+ font). Unscoped, that left near-white dark-mode text on a white field. */
1805
+ #editor-host .cm-panels {
1806
+ background: var(--panel);
1807
+ color: var(--text);
1808
+ border-color: var(--border);
1735
1809
  }
1736
- .cm-panels input, .cm-panels button, .cm-panels label {
1810
+ #editor-host .cm-panels input, #editor-host .cm-panels button, #editor-host .cm-panels label {
1737
1811
  font-family: var(--mono);
1738
1812
  font-size: 12px;
1739
1813
  color: var(--text);
1740
1814
  }
1741
- .cm-panels input {
1815
+ #editor-host .cm-panels input[type="checkbox"] { accent-color: var(--accent); }
1816
+ #editor-host .cm-panels .cm-textfield {
1742
1817
  background: var(--bg);
1743
1818
  border: 1px solid var(--border);
1744
1819
  border-radius: 4px;
1745
1820
  }
1746
- .cm-panels button {
1821
+ #editor-host .cm-panels .cm-textfield:focus { outline: none; border-color: var(--accent); }
1822
+ #editor-host .cm-panels .cm-button {
1747
1823
  background: var(--bg);
1748
- border: 1px solid var(--border) !important;
1824
+ background-image: none;
1825
+ border: 1px solid var(--border);
1749
1826
  border-radius: 4px;
1750
1827
  cursor: pointer;
1751
1828
  }
1752
- .cm-searchMatch { background: var(--warn-bg); outline: 1px solid var(--warn-border); }
1753
- .cm-searchMatch-selected { background: var(--warn-border); }
1829
+ #editor-host .cm-panels .cm-button:hover { border-color: var(--accent); }
1830
+ #editor-host .cm-panels .cm-button:active { background-image: none; }
1831
+ #editor-host .cm-searchMatch { background: var(--warn-bg); outline: 1px solid var(--warn-border); }
1832
+ #editor-host .cm-searchMatch-selected { background: var(--warn-border); }
1833
+ /* highlightSelectionMatches: CM's default is a bright translucent green */
1834
+ #editor-host .cm-selectionMatch { background: rgba(127, 127, 127, .22); }
1754
1835
 
1755
1836
  /* rendered markdown preview: reading typography, sanitized content only */
1756
1837
  #preview {
@@ -1790,6 +1871,8 @@ INDEX_HTML = r"""<!DOCTYPE html>
1790
1871
  color: var(--muted);
1791
1872
  }
1792
1873
  #preview img { max-width: 100%; }
1874
+ /* a local image the server could not serve: the alt text shows in its place */
1875
+ #preview img.img-missing { outline: 1px dashed var(--del-fg); outline-offset: 2px; color: var(--muted); }
1793
1876
  #preview hr { border: none; border-top: 1px solid var(--border); }
1794
1877
  #preview table { border-collapse: collapse; }
1795
1878
  #preview th, #preview td { border: 1px solid var(--border); padding: 4px 9px; }
@@ -1986,9 +2069,26 @@ INDEX_HTML = r"""<!DOCTYPE html>
1986
2069
  </style>
1987
2070
  </head>
1988
2071
  <body>
2072
+ <!-- logo geometry mirrors scripts/make_icon.py (1024 grid, tile at 100..924) -->
2073
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
2074
+ <defs>
2075
+ <linearGradient id="dw-logo-bg" x1="0" y1="0" x2="0" y2="1">
2076
+ <stop offset="0" stop-color="#3d82c7"/><stop offset="1" stop-color="#215892"/>
2077
+ </linearGradient>
2078
+ <symbol id="dw-logo" viewBox="100 100 824 824">
2079
+ <rect x="100" y="100" width="824" height="824" rx="185" fill="url(#dw-logo-bg)"/>
2080
+ <path fill="#f5f9fe" fill-rule="evenodd"
2081
+ d="M322 262H488A250 250 0 0 1 488 762H322Z M428 368H488A144 144 0 0 1 488 656H428Z"/>
2082
+ <rect x="470" y="452" width="120" height="42" rx="21" fill="#7ed08a"/>
2083
+ <rect x="470" y="530" width="92" height="42" rx="21" fill="#f08e7f"/>
2084
+ </symbol>
2085
+ </defs>
2086
+ </svg>
1989
2087
  <header>
2088
+ <svg class="logo" aria-hidden="true"><use href="#dw-logo"/></svg>
2089
+ <div class="rows">
1990
2090
  <div class="bar">
1991
- <span class="title"><span class="mark"><span class="ma">+</span><span class="md">−</span></span>Draftwatch</span>
2091
+ <span class="title"><span class="word">Draftwatch</span></span>
1992
2092
  <span class="filepick">
1993
2093
  <label class="status" for="file-select">file</label>
1994
2094
  <select id="file-select" title="pick a writing file in this repo"></select>
@@ -2013,6 +2113,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
2013
2113
  <button id="accent-toggle" title="accent color — click to cycle"><span class="swatch"></span><span id="accent-name">blue</span></button>
2014
2114
  <button id="about-btn" title="what is Draftwatch?">about</button>
2015
2115
  </div>
2116
+ </div>
2016
2117
  </header>
2017
2118
 
2018
2119
  <div id="banner">
@@ -2025,7 +2126,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
2025
2126
  <div id="about" role="dialog" aria-modal="true" aria-label="About Draftwatch">
2026
2127
  <div class="card">
2027
2128
  <button class="about-close" id="about-close" title="close (Esc)">close ✕</button>
2028
- <h2><span class="mark"><span class="ma">+</span><span class="md">−</span></span>Draftwatch</h2>
2129
+ <h2><svg class="logo" aria-hidden="true"><use href="#dw-logo"/></svg>Draftwatch</h2>
2029
2130
  <p class="tag">Review an AI agent's edits as a real git diff — locally.</p>
2030
2131
  <p>An autonomous agent (Claude Code and the like) edits your files on disk.
2031
2132
  Draftwatch shows you exactly what changed, as the word-level diff
@@ -2933,6 +3034,61 @@ INDEX_HTML = r"""<!DOCTYPE html>
2933
3034
  strongDelimiter: "**",
2934
3035
  linkStyle: "inlined"
2935
3036
  });
3037
+
3038
+ // Local images (e.g. ![](figs/chart.png)). A relative src would resolve to a
3039
+ // bare path on this server and 404, so the sanitizer moves it to data-md-src
3040
+ // (no request fires from the markup), and loadLocalImages fetches it through
3041
+ // the token-gated /api/image route and shows it as a blob: URL. data-md-src
3042
+ // keeps the markdown's own path, which Turndown writes back on preview edits
3043
+ // (never the blob: URL; its default rule would also drop a src-less image).
3044
+ function isLocalSrc(s) {
3045
+ return !!s && !/^([a-z][a-z0-9+.-]*:|\/|#)/i.test(s);
3046
+ }
3047
+ DOMPurify.addHook("afterSanitizeAttributes", function (node) {
3048
+ if (node.nodeName !== "IMG") return;
3049
+ var src = node.getAttribute("src");
3050
+ if (!isLocalSrc(src)) return;
3051
+ node.setAttribute("data-md-src", src);
3052
+ node.removeAttribute("src");
3053
+ });
3054
+ TURN.addRule("localImage", {
3055
+ filter: function (node) {
3056
+ return node.nodeName === "IMG" && node.hasAttribute("data-md-src");
3057
+ },
3058
+ replacement: function (content, node) {
3059
+ var clean = function (a) { return (a || "").replace(/(\n+\s*)+/g, "\n"); };
3060
+ var title = clean(node.getAttribute("title"));
3061
+ return "![" + clean(node.getAttribute("alt")) + "](" +
3062
+ node.getAttribute("data-md-src") + (title ? ' "' + title + '"' : "") + ")";
3063
+ }
3064
+ });
3065
+ var imageCache = {}; // "<file>|<src>" -> Promise of a blob: URL, or null
3066
+ function clearImageCache() {
3067
+ Object.keys(imageCache).forEach(function (k) {
3068
+ imageCache[k].then(function (u) { if (u) URL.revokeObjectURL(u); });
3069
+ });
3070
+ imageCache = {};
3071
+ }
3072
+ function loadLocalImages(root) {
3073
+ root.querySelectorAll("img[data-md-src]").forEach(function (img) {
3074
+ var src = img.getAttribute("data-md-src");
3075
+ var key = (currentFile || "") + "|" + src;
3076
+ if (!imageCache[key]) {
3077
+ imageCache[key] = fetch("/api/image?src=" + encodeURIComponent(src),
3078
+ { headers: { "X-Draftwatch-Token": TOKEN } })
3079
+ .then(function (r) { return r.ok ? r.blob() : null; })
3080
+ .then(function (b) { return b ? URL.createObjectURL(b) : null; })
3081
+ .catch(function () { return null; });
3082
+ // don't cache a miss: the image may be added to disk later
3083
+ imageCache[key].then(function (u) { if (!u) delete imageCache[key]; });
3084
+ }
3085
+ imageCache[key].then(function (u) {
3086
+ if (u) { img.src = u; img.classList.remove("img-missing"); }
3087
+ else img.classList.add("img-missing");
3088
+ });
3089
+ });
3090
+ }
3091
+
2936
3092
  function renderPreview() {
2937
3093
  var out = "";
2938
3094
  try {
@@ -2945,6 +3101,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
2945
3101
  // (innerHTML assignment does not fire 'input', but keep the intent explicit)
2946
3102
  $("preview").innerHTML = out ||
2947
3103
  '<p class="empty">nothing to preview</p>';
3104
+ loadLocalImages($("preview"));
2948
3105
  }
2949
3106
 
2950
3107
  // Convert the edited rendered HTML back to markdown and push it into the
@@ -2975,7 +3132,8 @@ INDEX_HTML = r"""<!DOCTYPE html>
2975
3132
  $("preview").setAttribute("contenteditable", on ? "true" : "false");
2976
3133
  // the format buttons + save stay available in preview — you can write here too
2977
3134
  $("view-toggle").textContent = on ? "source" : "preview";
2978
- if (on) renderPreview();
3135
+ // re-read local images each time preview opens, so a regenerated chart shows
3136
+ if (on) { clearImageCache(); renderPreview(); }
2979
3137
  }
2980
3138
  $("view-toggle").addEventListener("click", function () { setPreviewMode(!previewMode); });
2981
3139
 
@@ -3661,6 +3819,30 @@ def _webview_available():
3661
3819
  return False
3662
3820
 
3663
3821
 
3822
+ def _app_identity_kwargs(webview):
3823
+ """Brand the native window as Draftwatch rather than the Python interpreter
3824
+ running it: the Dock icon (draftwatch/assets/icon.png, drawn by
3825
+ scripts/make_icon.py) and, on macOS, the app name in the menu bar and its
3826
+ About/Hide/Quit items. Best effort: any failure leaves pywebview's defaults.
3827
+ Returns the kwargs for webview.start()."""
3828
+ if sys.platform == "darwin":
3829
+ try:
3830
+ from Foundation import NSBundle # PyObjC, a pywebview dependency on macOS
3831
+ info = NSBundle.mainBundle().infoDictionary()
3832
+ if info is not None:
3833
+ info["CFBundleName"] = "Draftwatch"
3834
+ except Exception:
3835
+ pass
3836
+ icon = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "icon.png")
3837
+ try:
3838
+ import inspect
3839
+ takes_icon = "icon" in inspect.signature(webview.start).parameters
3840
+ except (TypeError, ValueError):
3841
+ takes_icon = False
3842
+ # start(icon=) arrived in pywebview 5; on 4.x the window just keeps its default
3843
+ return {"icon": icon} if takes_icon and os.path.isfile(icon) else {}
3844
+
3845
+
3664
3846
  def _run_app_window(url, title):
3665
3847
  """Open the UI in a native window (PyWebView). Returns True when the window
3666
3848
  ran and was closed by the user; False when unavailable, in which case the
@@ -3678,7 +3860,7 @@ def _run_app_window(url, title):
3678
3860
  try:
3679
3861
  webview.create_window(title, url, width=1280, height=860,
3680
3862
  min_size=(880, 560))
3681
- webview.start()
3863
+ webview.start(**_app_identity_kwargs(webview))
3682
3864
  return True
3683
3865
  except Exception as e:
3684
3866
  sys.stderr.write(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwatch
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -12,6 +12,8 @@ draftwatch.egg-info/entry_points.txt
12
12
  draftwatch.egg-info/requires.txt
13
13
  draftwatch.egg-info/top_level.txt
14
14
  draftwatch/assets/codemirror.js
15
+ draftwatch/assets/favicon.png
16
+ draftwatch/assets/icon.png
15
17
  draftwatch/assets/marked.js
16
18
  draftwatch/assets/purify.js
17
19
  draftwatch/assets/turndown.js
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "draftwatch"
7
- version = "0.2.1"
7
+ version = "0.2.3"
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"
File without changes
File without changes
File without changes