interlatch 0.23.1__tar.gz → 0.23.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 (328) hide show
  1. {interlatch-0.23.1 → interlatch-0.23.2}/CHANGELOG.md +41 -0
  2. {interlatch-0.23.1 → interlatch-0.23.2}/PKG-INFO +2 -2
  3. {interlatch-0.23.1 → interlatch-0.23.2}/README.ja.md +1 -1
  4. {interlatch-0.23.1 → interlatch-0.23.2}/README.md +1 -1
  5. interlatch-0.23.2/changelog.d/agent-icons.md +3 -0
  6. interlatch-0.23.2/changelog.d/devices-admin-only.md +3 -0
  7. interlatch-0.23.2/changelog.d/windows-polish.md +5 -0
  8. {interlatch-0.23.1 → interlatch-0.23.2}/docs/devices.md +3 -2
  9. {interlatch-0.23.1 → interlatch-0.23.2}/docs/install.md +12 -5
  10. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/devices.md +2 -2
  11. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/install.md +11 -4
  12. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/cli.py +4 -2
  13. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/config.py +11 -3
  14. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/copilot_parser.py +8 -3
  15. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/hub.py +37 -20
  16. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/install.py +2 -0
  17. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/menubar.py +18 -3
  18. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/migrate.py +2 -1
  19. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/notify.py +57 -1
  20. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/people.py +3 -3
  21. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/server.py +55 -28
  22. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/util.py +73 -0
  23. interlatch-0.23.2/src/chronicle/web/AGENT-ICONS.txt +29 -0
  24. interlatch-0.23.2/src/chronicle/web/agent-antigravity.svg +1 -0
  25. interlatch-0.23.2/src/chronicle/web/agent-bob.svg +1 -0
  26. interlatch-0.23.2/src/chronicle/web/agent-chatgpt.svg +1 -0
  27. interlatch-0.23.2/src/chronicle/web/agent-claude-ai.svg +1 -0
  28. interlatch-0.23.2/src/chronicle/web/agent-claude.svg +1 -0
  29. interlatch-0.23.2/src/chronicle/web/agent-codex.svg +1 -0
  30. interlatch-0.23.2/src/chronicle/web/agent-copilot.svg +1 -0
  31. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/app.css +22 -2
  32. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/app.js +48 -6
  33. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/panel.css +5 -0
  34. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/panel.js +7 -2
  35. interlatch-0.23.2/src/chronicle/windows.py +569 -0
  36. interlatch-0.23.2/tests/golden.py +33 -0
  37. interlatch-0.23.2/tests/goldens/cli.json +2786 -0
  38. interlatch-0.23.2/tests/goldens/diagram.json +3077 -0
  39. interlatch-0.23.2/tests/goldens/parsers/antigravity.json +564 -0
  40. interlatch-0.23.2/tests/goldens/parsers/bob.json +604 -0
  41. interlatch-0.23.2/tests/goldens/parsers/claude_code.json +557 -0
  42. interlatch-0.23.2/tests/goldens/parsers/codex.json +1400 -0
  43. interlatch-0.23.2/tests/goldens/parsers/copilot.json +445 -0
  44. interlatch-0.23.2/tests/goldens/systems.json +1019 -0
  45. interlatch-0.23.2/tests/test_cli_pins.py +191 -0
  46. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_projects.py +7 -9
  47. interlatch-0.23.2/tests/test_map_goldens.py +66 -0
  48. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_notify.py +29 -0
  49. interlatch-0.23.2/tests/test_parser_goldens.py +151 -0
  50. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_people_web.py +5 -0
  51. interlatch-0.23.2/tests/test_route_matrix.py +508 -0
  52. interlatch-0.23.2/tests/test_windows.py +465 -0
  53. interlatch-0.23.1/changelog.d/agents-chronicle-retired.md +0 -3
  54. interlatch-0.23.1/changelog.d/docs-review.md +0 -4
  55. interlatch-0.23.1/changelog.d/interlatch-vscode.md +0 -4
  56. interlatch-0.23.1/changelog.d/leaderboard.md +0 -4
  57. interlatch-0.23.1/changelog.d/move-keeps-commands.md +0 -4
  58. interlatch-0.23.1/changelog.d/restart-animation.md +0 -5
  59. interlatch-0.23.1/changelog.d/vscode-marketplace.md +0 -2
  60. interlatch-0.23.1/changelog.d/windows.md +0 -3
  61. interlatch-0.23.1/src/chronicle/windows.py +0 -266
  62. interlatch-0.23.1/tests/test_windows.py +0 -203
  63. {interlatch-0.23.1 → interlatch-0.23.2}/.github/dependabot.yml +0 -0
  64. {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/changelog.yml +0 -0
  65. {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/ci.yml +0 -0
  66. {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/docs.yml +0 -0
  67. {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/release.yml +0 -0
  68. {interlatch-0.23.1 → interlatch-0.23.2}/.gitignore +0 -0
  69. {interlatch-0.23.1 → interlatch-0.23.2}/.pre-commit-config.yaml +0 -0
  70. {interlatch-0.23.1 → interlatch-0.23.2}/.python-version +0 -0
  71. {interlatch-0.23.1 → interlatch-0.23.2}/CONTRIBUTING.md +0 -0
  72. {interlatch-0.23.1 → interlatch-0.23.2}/LICENSE +0 -0
  73. {interlatch-0.23.1 → interlatch-0.23.2}/ROADMAP.ja.md +0 -0
  74. {interlatch-0.23.1 → interlatch-0.23.2}/ROADMAP.md +0 -0
  75. {interlatch-0.23.1 → interlatch-0.23.2}/SECURITY.md +0 -0
  76. {interlatch-0.23.1 → interlatch-0.23.2}/changelog.d/README.md +0 -0
  77. {interlatch-0.23.1 → interlatch-0.23.2}/dev.sh +0 -0
  78. {interlatch-0.23.1 → interlatch-0.23.2}/docker/.dockerignore +0 -0
  79. {interlatch-0.23.1 → interlatch-0.23.2}/docker/.env.example +0 -0
  80. {interlatch-0.23.1 → interlatch-0.23.2}/docker/Caddyfile +0 -0
  81. {interlatch-0.23.1 → interlatch-0.23.2}/docker/Dockerfile +0 -0
  82. {interlatch-0.23.1 → interlatch-0.23.2}/docker/compose.yaml +0 -0
  83. {interlatch-0.23.1 → interlatch-0.23.2}/docker/wheels/.gitignore +0 -0
  84. {interlatch-0.23.1 → interlatch-0.23.2}/docs/README.md +0 -0
  85. {interlatch-0.23.1 → interlatch-0.23.2}/docs/_site/hooks.py +0 -0
  86. {interlatch-0.23.1 → interlatch-0.23.2}/docs/analysis.md +0 -0
  87. {interlatch-0.23.1 → interlatch-0.23.2}/docs/cli.md +0 -0
  88. {interlatch-0.23.1 → interlatch-0.23.2}/docs/configuration.md +0 -0
  89. {interlatch-0.23.1 → interlatch-0.23.2}/docs/dashboard.md +0 -0
  90. {interlatch-0.23.1 → interlatch-0.23.2}/docs/demo/make_demo.py +0 -0
  91. {interlatch-0.23.1 → interlatch-0.23.2}/docs/demo/record_demo.py +0 -0
  92. {interlatch-0.23.1 → interlatch-0.23.2}/docs/design/ja/learning.md +0 -0
  93. {interlatch-0.23.1 → interlatch-0.23.2}/docs/design/learning.md +0 -0
  94. {interlatch-0.23.1 → interlatch-0.23.2}/docs/development.md +0 -0
  95. {interlatch-0.23.1 → interlatch-0.23.2}/docs/diagrams/analysis.excalidraw.svg +0 -0
  96. {interlatch-0.23.1 → interlatch-0.23.2}/docs/diagrams/architecture.excalidraw.svg +0 -0
  97. {interlatch-0.23.1 → interlatch-0.23.2}/docs/docker.md +0 -0
  98. {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/demo.gif +0 -0
  99. {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/home.png +0 -0
  100. {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/map.png +0 -0
  101. {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/palette.png +0 -0
  102. {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/session.png +0 -0
  103. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/README.md +0 -0
  104. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/analysis.md +0 -0
  105. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/cli.md +0 -0
  106. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/configuration.md +0 -0
  107. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/dashboard.md +0 -0
  108. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/development.md +0 -0
  109. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/docker.md +0 -0
  110. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/join-a-hub.md +0 -0
  111. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/mcp.md +0 -0
  112. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/moving-from-chronicle.md +0 -0
  113. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/postgres.md +0 -0
  114. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/privacy.md +0 -0
  115. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/sources.md +0 -0
  116. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/suggestions.md +0 -0
  117. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/troubleshooting.md +0 -0
  118. {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/vscode.md +0 -0
  119. {interlatch-0.23.1 → interlatch-0.23.2}/docs/join-a-hub.md +0 -0
  120. {interlatch-0.23.1 → interlatch-0.23.2}/docs/mcp.md +0 -0
  121. {interlatch-0.23.1 → interlatch-0.23.2}/docs/moving-from-chronicle.md +0 -0
  122. {interlatch-0.23.1 → interlatch-0.23.2}/docs/postgres.md +0 -0
  123. {interlatch-0.23.1 → interlatch-0.23.2}/docs/privacy.md +0 -0
  124. {interlatch-0.23.1 → interlatch-0.23.2}/docs/sources.md +0 -0
  125. {interlatch-0.23.1 → interlatch-0.23.2}/docs/suggestions.md +0 -0
  126. {interlatch-0.23.1 → interlatch-0.23.2}/docs/troubleshooting.md +0 -0
  127. {interlatch-0.23.1 → interlatch-0.23.2}/docs/vscode.md +0 -0
  128. {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/.firebaserc +0 -0
  129. {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/.gitignore +0 -0
  130. {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/README.md +0 -0
  131. {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/firebase.json +0 -0
  132. {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/firestore.indexes.json +0 -0
  133. {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/firestore.rules +0 -0
  134. {interlatch-0.23.1 → interlatch-0.23.2}/mkdocs.yml +0 -0
  135. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/README.md +0 -0
  136. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/art.json +0 -0
  137. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/filing.html +0 -0
  138. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/loops.json +0 -0
  139. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/loops.py +0 -0
  140. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/members.js +0 -0
  141. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/render.html +0 -0
  142. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/render.py +0 -0
  143. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/scenes-more.js +0 -0
  144. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/scenes.html +0 -0
  145. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/Chronicle.icns +0 -0
  146. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/Interlatch.spec +0 -0
  147. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/build.sh +0 -0
  148. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/chronicle_app.py +0 -0
  149. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/entitlements.plist +0 -0
  150. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon-3d.webp +0 -0
  151. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon-blueprint-prompt.md +0 -0
  152. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon-blueprint.webp +0 -0
  153. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon.png +0 -0
  154. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/make_icon.py +0 -0
  155. {interlatch-0.23.1 → interlatch-0.23.2}/packaging/release.py +0 -0
  156. {interlatch-0.23.1 → interlatch-0.23.2}/pyproject.toml +0 -0
  157. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/__init__.py +0 -0
  158. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/__main__.py +0 -0
  159. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/access.py +0 -0
  160. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/agents.py +0 -0
  161. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/analyze.py +0 -0
  162. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/antigravity_parser.py +0 -0
  163. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/artifacts.py +0 -0
  164. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/bob_parser.py +0 -0
  165. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/cases.py +0 -0
  166. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/chat_import.py +0 -0
  167. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/chatgpt_export.py +0 -0
  168. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/claude_export.py +0 -0
  169. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/codex_cloud.py +0 -0
  170. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/codex_parser.py +0 -0
  171. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/connectors.py +0 -0
  172. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/container.py +0 -0
  173. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/db.py +0 -0
  174. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/desktop.py +0 -0
  175. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/diagram.py +0 -0
  176. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/digest.py +0 -0
  177. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/export_md.py +0 -0
  178. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/friction.py +0 -0
  179. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/glossary.py +0 -0
  180. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/groups.py +0 -0
  181. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/hooks.py +0 -0
  182. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/i18n.py +0 -0
  183. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/i18n_ja.py +0 -0
  184. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/ingest.py +0 -0
  185. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/instructions.py +0 -0
  186. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/ladder.py +0 -0
  187. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/leaderboard.py +0 -0
  188. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/learning.py +0 -0
  189. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/llm.py +0 -0
  190. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/mcp_server.py +0 -0
  191. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/mirror.py +0 -0
  192. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/news.py +0 -0
  193. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/parser.py +0 -0
  194. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/pg.py +0 -0
  195. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/pricing.py +0 -0
  196. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/providers.py +0 -0
  197. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/redact.py +0 -0
  198. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/reviews.py +0 -0
  199. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/screen.py +0 -0
  200. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/search.py +0 -0
  201. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/session_export.py +0 -0
  202. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/statusline.py +0 -0
  203. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/suggest.py +0 -0
  204. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/synthesize.py +0 -0
  205. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/systems.py +0 -0
  206. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/tailnet.py +0 -0
  207. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/team_store.py +0 -0
  208. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/update.py +0 -0
  209. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/views.py +0 -0
  210. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/PLEX-OFL.txt +0 -0
  211. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-book.webp +0 -0
  212. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-camera.webp +0 -0
  213. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-chip.webp +0 -0
  214. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-clock.webp +0 -0
  215. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-cloud.webp +0 -0
  216. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-coins.webp +0 -0
  217. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-container.webp +0 -0
  218. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-dashboard.webp +0 -0
  219. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-dex.webp +0 -0
  220. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-document.webp +0 -0
  221. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-flag.webp +0 -0
  222. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-folder.webp +0 -0
  223. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-gateway.webp +0 -0
  224. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-gears.webp +0 -0
  225. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-glitch.webp +0 -0
  226. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-globe.webp +0 -0
  227. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-laptop.webp +0 -0
  228. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-log.webp +0 -0
  229. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-lumi.webp +0 -0
  230. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-magnifier.webp +0 -0
  231. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-pip.webp +0 -0
  232. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-plug.webp +0 -0
  233. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-rack.webp +0 -0
  234. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-tink.webp +0 -0
  235. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/boot.js +0 -0
  236. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/favicon.png +0 -0
  237. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/i18n.js +0 -0
  238. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon-180.png +0 -0
  239. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon-192.png +0 -0
  240. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon-512.png +0 -0
  241. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon.png +0 -0
  242. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/index.html +0 -0
  243. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/ja.js +0 -0
  244. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/learning.js +0 -0
  245. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/manifest.webmanifest +0 -0
  246. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/panel.html +0 -0
  247. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-mono-400.woff2 +0 -0
  248. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-mono-500.woff2 +0 -0
  249. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-mono-600.woff2 +0 -0
  250. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-400.woff2 +0 -0
  251. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-500.woff2 +0 -0
  252. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-600.woff2 +0 -0
  253. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-700.woff2 +0 -0
  254. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/rough.js +0 -0
  255. {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/worker.py +0 -0
  256. {interlatch-0.23.1 → interlatch-0.23.2}/tests/antigravity_fixture.py +0 -0
  257. {interlatch-0.23.1 → interlatch-0.23.2}/tests/codex_fixture.py +0 -0
  258. {interlatch-0.23.1 → interlatch-0.23.2}/tests/conftest.py +0 -0
  259. {interlatch-0.23.1 → interlatch-0.23.2}/tests/copilot_fixture.py +0 -0
  260. {interlatch-0.23.1 → interlatch-0.23.2}/tests/friction_fixture.py +0 -0
  261. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_analysis.py +0 -0
  262. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_antigravity.py +0 -0
  263. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_artifacts.py +0 -0
  264. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_bob_analysis.py +0 -0
  265. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_boot_web.py +0 -0
  266. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_case_files.py +0 -0
  267. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_cases.py +0 -0
  268. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_chat_import.py +0 -0
  269. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_codex.py +0 -0
  270. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_codex_analysis.py +0 -0
  271. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_codex_cloud.py +0 -0
  272. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_container.py +0 -0
  273. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_copilot_bob.py +0 -0
  274. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_dedicated_hub.py +0 -0
  275. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_dedupe.py +0 -0
  276. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_desktop.py +0 -0
  277. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_diagram.py +0 -0
  278. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_ee_boundary.py +0 -0
  279. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_friction.py +0 -0
  280. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_glossary.py +0 -0
  281. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_group_sharing.py +0 -0
  282. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub.py +0 -0
  283. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_accept.py +0 -0
  284. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_news.py +0 -0
  285. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_projects_listed.py +0 -0
  286. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_purge.py +0 -0
  287. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_sharing_race.py +0 -0
  288. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_i18n_web.py +0 -0
  289. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_ingest.py +0 -0
  290. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_instructions.py +0 -0
  291. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_integration.py +0 -0
  292. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_ladder.py +0 -0
  293. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_language.py +0 -0
  294. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_leaderboard.py +0 -0
  295. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_learning_glance.py +0 -0
  296. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_learning_interests.py +0 -0
  297. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_learning_web.py +0 -0
  298. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_leave_project.py +0 -0
  299. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_menubar.py +0 -0
  300. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_migrate.py +0 -0
  301. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_mirror.py +0 -0
  302. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_parser.py +0 -0
  303. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_people.py +0 -0
  304. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_people_hub.py +0 -0
  305. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_project_groups.py +0 -0
  306. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_providers.py +0 -0
  307. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_queue.py +0 -0
  308. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_regressions.py +0 -0
  309. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_release.py +0 -0
  310. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_screen.py +0 -0
  311. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_security.py +0 -0
  312. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_setup.py +0 -0
  313. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_statusline.py +0 -0
  314. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_suggest.py +0 -0
  315. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_suggest_cli.py +0 -0
  316. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_systems.py +0 -0
  317. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_team.py +0 -0
  318. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_team_home.py +0 -0
  319. {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_update.py +0 -0
  320. {interlatch-0.23.1 → interlatch-0.23.2}/tests/windows_smoke.py +0 -0
  321. {interlatch-0.23.1 → interlatch-0.23.2}/uv.lock +0 -0
  322. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/.vscodeignore +0 -0
  323. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/LICENSE +0 -0
  324. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/README.md +0 -0
  325. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/extension.js +0 -0
  326. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/media/icon.png +0 -0
  327. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/media/interlatch.svg +0 -0
  328. {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/package.json +0 -0
@@ -3,6 +3,47 @@
3
3
  <!-- Don't add lines here: a pull request puts its line in a new file in changelog.d/ (see changelog.d/README.md),
4
4
  and each release moves the files it took into a dated section below. -->
5
5
 
6
+ ## 0.23.1 (2026-10-13)
7
+
8
+ - **`agents-chronicle` is retired:** Interlatch is published only as `interlatch` now, and `agents-chronicle` is no
9
+ longer on PyPI. An install under the old name gets no more updates: **Update** in the dashboard still moves it to
10
+ `interlatch`, or see [Moving from Chronicle](docs/moving-from-chronicle.md) for the commands.
11
+
12
+ ## 0.23.0 (2026-10-13)
13
+
14
+ - **Leaderboard:** sign in with Google and compare your hours of agent time, sessions and tokens over 7, 30 or 90
15
+ days with everyone else who joined, under **Leaderboard** in the dashboard (`interlatch leaderboard` shows where
16
+ you stand). Only the totals from this computer's coding-agent sessions are sent, with your first name (rename it
17
+ any time) and your Google picture. No email, projects, prompts or titles. **Leave** deletes your row.
18
+
19
+ ## 0.22.1 (2026-10-11)
20
+
21
+ This release never reached PyPI (its upload failed after the repository's rename); these changes first shipped
22
+ there in 0.23.0.
23
+
24
+ - **Docs:** the quick links atop the docs home (Docs, 日本語, English) open their pages instead of a 404, Install
25
+ lists every way to analyze sessions (Claude Code, Codex, IBM Bob with a Bob API key, or a model provider's API with
26
+ your own key), and links, **What's new** and **Download** under **Status › Updates** among them, point at the
27
+ repository's new name, [Chatixia-AI/interlatch](https://github.com/Chatixia-AI/interlatch).
28
+ - **Moving from Chronicle keeps the commands:** **Update** on an `agents-chronicle` install no longer leaves it
29
+ without the `interlatch` and `chronicle` commands, which kept the dashboard from coming back and stopped the
30
+ background sync, the session hooks and the MCP server. Already hit by it? [Troubleshooting](docs/troubleshooting.md#moving-from-chronicle)
31
+ has the two commands that put it right.
32
+ - **Restarting, animated:** while the dashboard restarts after an update, its dialog turns the arrows inside a ring
33
+ that swells and fades, with a sweep along its foot (with animations off they turn slowly and the rest holds still).
34
+ The last places that still said Chronicle say Interlatch: the diagrams in the docs, the log file's lines, the
35
+ Postgres quick start, and the names the dashboard and menu-bar panel use for their requests and what the browser
36
+ remembers. Settings a browser kept under the old names carry over, and a sign-in to a hub from before stays valid.
37
+ - **Windows:** `uv tool install interlatch` and `interlatch install` now work on Windows 10 and 11. Task Scheduler runs
38
+ the 15-minute sync and keeps the dashboard up, without a window, and the hooks run in Git Bash and in PowerShell
39
+ ([Windows](docs/install.md#windows)). A Windows computer can't join a team hub yet.
40
+ - **The VS Code extension is now Interlatch:** a new extension, `Chatixia-AI.interlatch` 0.3.0, with the sections
41
+ **Interlatch: This File** and **Interlatch: Files in Workspace**. Your `chronicle.*` settings still apply until you
42
+ set their `interlatch.*` counterparts. Uninstall the Chronicle extension after installing it; Interlatch offers to,
43
+ once ([VS Code extension](docs/vscode.md)).
44
+ - **The VS Code extension on the Marketplace:** install **Interlatch** from VS Code's Extensions view
45
+ ([VS Code extension](docs/vscode.md#install)). Releases publish it whenever its own version goes up.
46
+
6
47
  ## 0.22.0 (2026-10-10)
7
48
 
8
49
  - **Your Chronicle setup moves over by itself:** the first time the dashboard or the background sync starts,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: interlatch
3
- Version: 0.23.1
3
+ Version: 0.23.2
4
4
  Summary: Shared memory for your coding agents: records every session (Claude Code, Codex, Copilot, Bob, Antigravity) locally and keeps what they learned, each lesson linked to the session it came from.
5
5
  Project-URL: Homepage, https://interlatch.com
6
6
  Project-URL: Documentation, https://interlatch.com/docs/
@@ -191,7 +191,7 @@ sessions are archived and wait in the analysis queue.
191
191
 
192
192
  **Windows or Linux?** The desktop app is macOS only. On Windows, `uv tool install interlatch` and `interlatch install`
193
193
  work as on a Mac, with Task Scheduler running the sync and the dashboard ([Windows](https://github.com/Chatixia-AI/interlatch/blob/main/docs/install.md#windows)); a
194
- Windows computer can't join a team hub yet. On Linux, `interlatch install` runs the sync and the dashboard
194
+ Windows computer joining a team hub is untested yet. On Linux, `interlatch install` runs the sync and the dashboard
195
195
  as systemd user units, so a Linux box can be the [hub](https://github.com/Chatixia-AI/interlatch/blob/main/docs/devices.md#a-linux-hub) for your other computers. A
196
196
  team's hub also runs [in Docker](https://github.com/Chatixia-AI/interlatch/blob/main/docs/docker.md).
197
197
 
@@ -154,7 +154,7 @@ Ollama。**Status › Analysis** または `interlatch config set analysis.backe
154
154
 
155
155
  **Windows や Linux は?** デスクトップアプリは macOS 専用です。Windows では `uv tool install interlatch` と `interlatch install` が
156
156
  Mac と同じように使え、同期とダッシュボードはタスク スケジューラが動かします([Windows](docs/ja/install.md#windows))。
157
- Windows のコンピューターはまだチームのハブに参加できません。Linux では `interlatch install` が同期とダッシュボードを systemd の
157
+ Windows のコンピューターからのチームのハブへの参加はまだ未検証です。Linux では `interlatch install` が同期とダッシュボードを systemd の
158
158
  ユーザーユニットとして動かすので、Linux マシンをほかのコンピューターの[ハブ](docs/ja/devices.md#linux-のハブ)にできます。
159
159
  チームのハブは [Docker](docs/ja/docker.md) でも動かせます。
160
160
 
@@ -156,7 +156,7 @@ sessions are archived and wait in the analysis queue.
156
156
 
157
157
  **Windows or Linux?** The desktop app is macOS only. On Windows, `uv tool install interlatch` and `interlatch install`
158
158
  work as on a Mac, with Task Scheduler running the sync and the dashboard ([Windows](docs/install.md#windows)); a
159
- Windows computer can't join a team hub yet. On Linux, `interlatch install` runs the sync and the dashboard
159
+ Windows computer joining a team hub is untested yet. On Linux, `interlatch install` runs the sync and the dashboard
160
160
  as systemd user units, so a Linux box can be the [hub](docs/devices.md#a-linux-hub) for your other computers. A
161
161
  team's hub also runs [in Docker](docs/docker.md).
162
162
 
@@ -0,0 +1,3 @@
1
+ - **Agent logos:** the Sessions list and its agent filter, Sources, MCP and the menu-bar panel mark each session and
2
+ source with its agent's own logo (Claude Code, Codex, GitHub Copilot, IBM Bob, Google Antigravity, claude.ai,
3
+ ChatGPT) instead of a colour dot.
@@ -0,0 +1,3 @@
1
+ - **Hub computers stay with admins:** on a hub with people, a member or read-only person no longer receives the hub's
2
+ computers, its team store's database, host and status, or its allowed sign-ins; only admins (and the hub computer
3
+ itself) do.
@@ -0,0 +1,5 @@
1
+ - **Windows polish:** The dashboard can show an Interlatch icon in the notification area (turn on the menu-bar icon
2
+ under **Status › Recording**, or run `interlatch ui --menu-bar`): click it to open the dashboard, right-click for Sync
3
+ Now and Quit, which stops the dashboard until you next sign in. Release notifications show as Windows toasts. The VS
4
+ Code extension's file history and folder list, hub folders and projects, and a person's projects now match Windows
5
+ paths however they're spelled (`C:\work\app`, `c:/work/app`).
@@ -602,5 +602,6 @@ corrupted.
602
602
  your phone, so the hub's tailnet address is out of their reach. Claude Code's Remote Control, from the phone into
603
603
  a session on the hub, does have Interlatch's tools.
604
604
  - **A copy to read offline** on the other computers.
605
- - **A Windows computer in a hub.** Interlatch runs on Windows on its own ([Windows](install.md#windows)), but parts of
606
- the hub expect project folders as `/…` paths.
605
+ - **A Windows computer in a hub.** Interlatch runs on Windows on its own ([Windows](install.md#windows)), and the hub
606
+ matches Windows folders however they're spelled (`C:\work\app`, `c:/work/app`), but joining one from Windows is
607
+ untested.
@@ -117,7 +117,7 @@ launchd agents do not run alongside it (harmless, but redundant).
117
117
 
118
118
  ## Windows
119
119
 
120
- On Windows 10 and 11 Interlatch is the command-line install; there is no desktop app or menu-bar icon. In PowerShell:
120
+ On Windows 10 and 11 Interlatch is the command-line install; there is no desktop app. In PowerShell:
121
121
 
122
122
  ```powershell
123
123
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # uv, if you don't have it yet
@@ -141,13 +141,20 @@ The hooks it adds to Claude Code run `C:/Users/<you>/.local/bin/interlatch.exe h
141
141
  hook in Git Bash, or in PowerShell without Git for Windows, and forward slashes read the same in both. A folder with
142
142
  a space in its name is written in its short form (`C:/Users/ADALOV~1/…`).
143
143
 
144
- Interlatch reads and writes text in UTF-8, as the coding agents write it, whatever language Windows is set to.
144
+ Interlatch reads and writes text in UTF-8, as the coding agents write it, whatever language Windows is set to. It
145
+ matches a Windows path however an agent or VS Code spells it (`C:\work\app`, `c:/work/app`), so the
146
+ [VS Code extension](vscode.md)'s file history and folder list work there too.
147
+
148
+ The **Menu-bar icon** switch under **Settings › Status › Recording** puts Interlatch's icon in the notification area
149
+ (by the clock) while the Dashboard task runs the dashboard; `interlatch ui --menu-bar` shows it from a terminal. Click
150
+ it to open the dashboard, or right-click for **Sync Now** and **Quit Interlatch**. Quit stops the dashboard and the icon
151
+ until you next sign in, run `interlatch install`, or run `interlatch ui`; the Sync task keeps recording. A new
152
+ version's [notification](#updating) (`[updates] notify`) shows as a Windows notification.
145
153
 
146
154
  Not on Windows yet:
147
155
 
148
- - **Joining a team hub.** Parts of the hub expect project folders as `/…` paths, so it is untested from Windows.
149
- - **The VS Code extension's file history,** which looks files up by `/…` paths.
150
- - **Desktop notifications** of a new version, and the menu-bar icon.
156
+ - **Joining a team hub** is untested from Windows: the hub matches Windows folders however they're spelled, but no
157
+ team has run one with a Windows computer yet.
151
158
 
152
159
  ## The menu-bar icon
153
160
 
@@ -598,5 +598,5 @@ sudo tailscale set --operator=$USER # `interlatch tailnet on` が Tailscale
598
598
  接続するので、ハブの tailnet 内のアドレスには届きません。スマートフォンから Claude Code の Remote Control でハブのセッションを
599
599
  操作すれば、Interlatch のツールを使えます。
600
600
  - **ほかのコンピューターでオフラインで読めるコピー。**
601
- - **ハブに参加する Windows のコンピューター。** Interlatch は Windows 単体では動きます([Windows](install.md#windows))が、
602
- ハブの一部はプロジェクトのフォルダーを `/…` 形式のパスとして扱います。
601
+ - **ハブに参加する Windows のコンピューター。** Interlatch は Windows 単体では動き([Windows](install.md#windows))、
602
+ ハブは Windows のフォルダーを書き方(`C:\work\app`、`c:/work/app`)にかかわらず照合しますが、Windows からの参加は未検証です。
@@ -113,7 +113,7 @@ Claude Code と Mac 本体には、次の 4 つを設定します(それぞれ
113
113
 
114
114
  ## Windows
115
115
 
116
- Windows 10 と 11 では、Interlatch はコマンドライン版を使います。デスクトップアプリとメニューバーアイコンはありません。PowerShell で:
116
+ Windows 10 と 11 では、Interlatch はコマンドライン版を使います。デスクトップアプリはありません。PowerShell で:
117
117
 
118
118
  ```powershell
119
119
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # uv, if you don't have it yet
@@ -138,12 +138,19 @@ Claude Code に追加するフックは `C:/Users/<you>/.local/bin/interlatch.ex
138
138
  名前に空白を含むフォルダーは短い形式(`C:/Users/ADALOV~1/…`)で書きます。
139
139
 
140
140
  Interlatch は、Windows の表示言語にかかわらず、コーディングエージェントが書くのと同じ UTF-8 でテキストを読み書きします。
141
+ Windows のパスは、エージェントや VS Code の書き方(`C:\work\app`、`c:/work/app`)にかかわらず同じものとして扱うので、
142
+ [VS Code 拡張](vscode.md)のファイル履歴とフォルダーの一覧も使えます。
143
+
144
+ **Settings › Status › Recording** の **Menu-bar icon** スイッチをオンにすると、Dashboard タスクがダッシュボードを動かしている間、
145
+ 通知領域(時計の横)に Interlatch のアイコンが出ます。ターミナルからは `interlatch ui --menu-bar` で表示できます。クリックで
146
+ ダッシュボードを開き、右クリックで **Sync Now** と **Quit Interlatch** を選べます。Quit すると、次にサインインするか、
147
+ `interlatch install` か `interlatch ui` を実行するまで、ダッシュボードとアイコンは止まります(Sync タスクは記録を続けます)。
148
+ 新しいバージョンの[通知](#アップデート)(`[updates] notify`)は Windows の通知として表示されます。
141
149
 
142
150
  Windows ではまだできないこと:
143
151
 
144
- - **チームのハブへの参加。** ハブの一部はプロジェクトのフォルダーを `/…` 形式のパスとして扱うため、Windows からは未検証です。
145
- - **VS Code 拡張のファイル履歴。** ファイルを `/…` 形式のパスで探します。
146
- - 新しいバージョンの**デスクトップ通知**と、メニューバーアイコン。
152
+ - **チームのハブへの参加**は Windows から未検証です。ハブは Windows のフォルダーを書き方にかかわらず照合しますが、
153
+ Windows のコンピューターを含むチームではまだ運用されていません。
147
154
 
148
155
  ## メニューバーアイコン
149
156
 
@@ -2364,6 +2364,7 @@ def _hub_leave_project(cfg, console, args) -> int:
2364
2364
  from pathlib import Path
2365
2365
 
2366
2366
  from . import hub
2367
+ from .util import normalize_project_path
2367
2368
 
2368
2369
  if not cfg.is_spoke:
2369
2370
  console.print("This computer has not joined a hub.")
@@ -2376,14 +2377,15 @@ def _hub_leave_project(cfg, console, args) -> int:
2376
2377
  known = hub.handshake(cfg).get("projects") or []
2377
2378
  except hub.HubError:
2378
2379
  known = (hub.last_folders(cfg) or {}).get("projects") or []
2379
- known += [{"path": x, "name": Path(x).name} for x in cfg.hub_left if x not in {k["path"] for k in known}]
2380
+ known += [{"path": x, "name": Path(x).name} for x in cfg.hub_left
2381
+ if x not in {normalize_project_path(k.get("path")) for k in known}]
2380
2382
  project, problem = _pick_project(known, wanted)
2381
2383
  if problem:
2382
2384
  console.print(problem, highlight=False)
2383
2385
  return 1
2384
2386
  name = project.get("name") or Path(project["path"]).name
2385
2387
  if args.action == "rejoin":
2386
- if project["path"] not in cfg.hub_left:
2388
+ if normalize_project_path(project["path"]) not in cfg.hub_left:
2387
2389
  console.print(f"This computer hasn't left {name}.", highlight=False)
2388
2390
  return 0
2389
2391
  cfg = hub.rejoin_project(cfg, project["path"])
@@ -11,6 +11,8 @@ from collections.abc import Mapping
11
11
  from dataclasses import dataclass, field
12
12
  from pathlib import Path
13
13
 
14
+ from .util import is_abs_path, normalize_project_path, path_kind
15
+
14
16
  DEFAULT_CONFIG_TOML = """\
15
17
  # Interlatch configuration.
16
18
  # Edit freely; changes apply on the next sync / worker run.
@@ -441,6 +443,12 @@ def _pick(cls, values: dict):
441
443
  return cls(**{k: v for k, v in values.items() if k in allowed})
442
444
 
443
445
 
446
+ def _map_path(path: str) -> str:
447
+ """A [hub] path_map side: a Windows folder as normalize_project_path spells it; a POSIX one without a trailing /
448
+ ("/" becomes "", which maps every path)."""
449
+ return normalize_project_path(path) if path_kind(path) in ("drive", "unc") else path.rstrip("/")
450
+
451
+
444
452
  def load_config(home: Path | None = None, *, create: bool = True) -> Config:
445
453
  home = (home or chronicle_home()).expanduser()
446
454
  path = home / "config.toml"
@@ -498,12 +506,12 @@ def load_config(home: Path | None = None, *, create: bool = True) -> Config:
498
506
  server_menu_bar=bool(server.get("menu_bar", False)),
499
507
  server_trusted_proxies=[str(x).strip() for x in server.get("trusted_proxies", ["127.0.0.1", "::1"]) or [] if str(x).strip()],
500
508
  hub_url=str(hub.get("url") or "").strip().rstrip("/"),
501
- hub_path_map={str(k).rstrip("/"): str(v).rstrip("/") for k, v in path_map.items()} if isinstance(path_map, dict) else {},
502
- hub_folders={str(Path(str(k)).expanduser()).rstrip("/") or "/": str(v).rstrip("/") for k, v in folders.items()
509
+ hub_path_map={_map_path(str(k)): _map_path(str(v)) for k, v in path_map.items()} if isinstance(path_map, dict) else {},
510
+ hub_folders={normalize_project_path(str(Path(str(k)).expanduser())): normalize_project_path(str(v)) for k, v in folders.items()
503
511
  if str(k).strip() and str(v).strip()} if isinstance(folders, dict) else {},
504
512
  hub_share=share if share in SHARE_MODES else "everything",
505
513
  hub_all_folders=hub.get("all_folders") is True, # only an explicit true shares every folder
506
- hub_left=sorted({str(x).rstrip("/") for x in hub.get("left") or [] if str(x).startswith("/")})
514
+ hub_left=sorted({normalize_project_path(str(x)) for x in hub.get("left") or [] if is_abs_path(str(x))})
507
515
  if isinstance(hub.get("left"), list) else [],
508
516
  hub_accept="everything" if accept == "everything" else "knowledge", # a typo never lets transcripts in
509
517
  hub_store=store if store in STORES else "",
@@ -47,12 +47,17 @@ def shift_iso(ts: str | None, ms: float) -> str | None:
47
47
  return (dt + timedelta(milliseconds=ms)).isoformat(timespec="milliseconds").replace("+00:00", "Z") if dt else ts
48
48
 
49
49
 
50
+ _URI_DRIVE = re.compile(r"/[A-Za-z]:(?:/|$)") # a Windows drive as a URI's path spells it
51
+
52
+
50
53
  def uri_path(uri) -> str | None:
51
- """file:///Users/x or a VS Code URI object -> /Users/x."""
54
+ """file:///Users/x or a VS Code URI object -> /Users/x. On Windows file:///c%3A/x -> c:/x, not /c:/x."""
52
55
  if isinstance(uri, dict):
53
- return uri.get("fsPath") or uri.get("path")
56
+ p = uri.get("fsPath") or uri.get("path")
57
+ return p[1:] if isinstance(p, str) and _URI_DRIVE.match(p) else p
54
58
  if isinstance(uri, str) and uri.startswith("file:"):
55
- return unquote(urlparse(uri).path) or None
59
+ p = unquote(urlparse(uri).path)
60
+ return (p[1:] if _URI_DRIVE.match(p) else p) or None
56
61
  if isinstance(uri, str) and uri.startswith("vscode-remote:"): # vscode-remote://ssh-remote+host/path -> host:/path
57
62
  u = urlparse(uri)
58
63
  host = unquote(u.netloc).split("+", 1)[-1]
@@ -49,7 +49,7 @@ from urllib.parse import quote, urlencode, urlparse
49
49
 
50
50
  from . import __version__, news
51
51
  from .config import Config
52
- from .util import utcnow_iso
52
+ from .util import is_abs_path, normalize_project_path, path_kind, path_variants, utcnow_iso
53
53
 
54
54
  log = logging.getLogger("interlatch.hub")
55
55
 
@@ -388,11 +388,14 @@ def spoke_repos(cfg: Config, roots: list[Root]) -> dict[str, list[str]]:
388
388
 
389
389
  # ------------------------------------------------------------------ folders added by hand
390
390
  def under(path: str | None, folder: str) -> bool:
391
- """`path` is `folder` or somewhere below it."""
392
- if not path:
391
+ """`path` is `folder` or somewhere below it, compared as normalize_project_path spells them (C:\\x is under c:/).
392
+ A POSIX folder never holds a Windows path (a drive or a UNC share), nor the other way round; no folder (""), nothing."""
393
+ if not path or not folder:
393
394
  return False
394
- folder = folder.rstrip("/") or "/"
395
- return path == folder or path.startswith(folder if folder == "/" else folder + "/")
395
+ path, folder = normalize_project_path(path), normalize_project_path(folder)
396
+ if path_kind(path) != path_kind(folder):
397
+ return False
398
+ return path == folder or path.startswith(folder if folder.endswith("/") else folder + "/")
396
399
 
397
400
 
398
401
  def clean_folders(raw) -> dict[str, str]:
@@ -402,8 +405,8 @@ def clean_folders(raw) -> dict[str, str]:
402
405
  out: dict[str, str] = {}
403
406
  for k, v in list(raw.items())[:MAX_FOLDERS]:
404
407
  k, v = str(k).strip(), str(v).strip()
405
- if k.startswith("/") and v.startswith("/") and len(k) <= 1024 and len(v) <= 1024 and "\0" not in k + v:
406
- out[k.rstrip("/") or "/"] = v.rstrip("/") or "/"
408
+ if is_abs_path(k) and is_abs_path(v) and len(k) <= 1024 and len(v) <= 1024 and "\0" not in k + v:
409
+ out[normalize_project_path(k)] = normalize_project_path(v)
407
410
  return out
408
411
 
409
412
 
@@ -435,8 +438,8 @@ def clean_left(raw) -> list[str]:
435
438
  """A spoke's [hub] left as the hub accepts it: absolute project paths, a bounded number of them."""
436
439
  if not isinstance(raw, list):
437
440
  return []
438
- return sorted({x.rstrip("/") or "/" for x in raw[:MAX_FOLDERS]
439
- if isinstance(x, str) and x.startswith("/") and len(x) <= 1024 and "\0" not in x})
441
+ return sorted({normalize_project_path(x) for x in raw[:MAX_FOLDERS]
442
+ if isinstance(x, str) and is_abs_path(x) and len(x) <= 1024 and "\0" not in x})
440
443
 
441
444
 
442
445
  def hub_projects(conn, limit: int = 1000, only: list[str] | None = None) -> list[dict]:
@@ -874,7 +877,7 @@ def group_routes(cfg: Config, conn=None) -> GroupRoutes:
874
877
  linked = {g["id"]: g["hub_project"] for g in groups(conn) if g.get("hub_project")}
875
878
  paths = [r[0] for r in conn.execute("SELECT DISTINCT project_path FROM sessions WHERE project_path IS NOT NULL")]
876
879
  group_of = {p: gid for p, (gid, _) in assign(conn, paths).items()}
877
- active = {p: (linked[gid], gid) for p, gid in group_of.items() if gid in linked and p.startswith("/")}
880
+ active = {p: (linked[gid], gid) for p, gid in group_of.items() if gid in linked and is_abs_path(p)}
878
881
  kept = {r[0]: r[1] for r in conn.execute("SELECT path, hub_project FROM group_shares") if r[0] not in active}
879
882
  finally:
880
883
  if own:
@@ -1149,11 +1152,12 @@ def _shared_records(cfg: Config, have: dict, *, scope: list[str] | None = None,
1149
1152
  else routes.decide(rec["project_path"], cfg.hub_folders)
1150
1153
  if via is not None:
1151
1154
  goes, ok = via
1155
+ goes = normalize_project_path(goes) # [hub] left and a person's projects are spelled so
1152
1156
  if not ok or goes in left or (scope is not None and goes not in scope):
1153
1157
  excluded += 1 # kept out of the group, out of it now, left, or a project this person doesn't share
1154
1158
  continue
1155
1159
  elif limited or left:
1156
- goes = destination(cfg, rec["project_path"], rec["remote"], remotes or {})
1160
+ goes = normalize_project_path(destination(cfg, rec["project_path"], rec["remote"], remotes or {}))
1157
1161
  if goes in left or (limited and (goes is None or (scope is not None and goes not in scope))):
1158
1162
  excluded += 1 # not one of the hub's projects, one this person doesn't share, or one it left
1159
1163
  continue
@@ -1199,7 +1203,7 @@ def hub_project_of(cfg: Config, paths, conn=None) -> dict[str, dict]:
1199
1203
  top = max((f for f in repos if under(path, f)), key=len, default=None)
1200
1204
  via = None if top else routes.decide(path, cfg.hub_folders)
1201
1205
  goes = repos[top] if top else (via[0] if via[1] else None) if via else destination(cfg, path, None, {})
1202
- if goes and goes not in cfg.hub_left:
1206
+ if goes and normalize_project_path(goes) not in cfg.hub_left:
1203
1207
  out[path] = {"path": goes, "name": names.get(goes) or Path(goes).name or goes}
1204
1208
  return out
1205
1209
 
@@ -1483,19 +1487,21 @@ def add_folder(cfg: Config, folder: str, project: dict, hello: dict) -> tuple[Co
1483
1487
  `project`; one this computer left is joined again. `hello`: the hub's answer to handshake(), for the git remotes
1484
1488
  it knows. Returns the reloaded config and, when nothing changed because the folder's repository already goes
1485
1489
  there by its git remote, that remote. Raises FolderError when the folder can't go there."""
1486
- name = project.get("name") or Path(project["path"]).name
1490
+ folder = normalize_project_path(folder)
1491
+ project_path = normalize_project_path(project["path"])
1492
+ name = project.get("name") or Path(project_path).name
1487
1493
  if not Path(folder).is_dir():
1488
1494
  raise FolderError("{folder} is not a folder.", folder=folder)
1489
1495
  info = git_info(folder)
1490
1496
  owner = (hello.get("remotes") or {}).get(info[1]) if info else None
1491
- if owner and owner != project["path"]:
1497
+ if owner and normalize_project_path(owner) != project_path:
1492
1498
  raise FolderError("{folder} is in the git repository {remote}, which the hub files under {owner}. A repository "
1493
1499
  "belongs to one project, so its sessions can't go to {name}.",
1494
1500
  folder=folder, remote=info[1], owner=Path(owner).name, name=name)
1495
- left = [x for x in cfg.hub_left if x != project["path"]]
1501
+ left = [x for x in cfg.hub_left if x != project_path]
1496
1502
  if owner:
1497
1503
  return _save_hub(cfg, left=left) if left != cfg.hub_left else cfg, info[1]
1498
- return _save_hub(cfg, folders={**cfg.hub_folders, folder: project["path"]},
1504
+ return _save_hub(cfg, folders={**cfg.hub_folders, folder: project_path},
1499
1505
  left=left if left != cfg.hub_left else None), None
1500
1506
 
1501
1507
 
@@ -1505,6 +1511,7 @@ def remove_folder(cfg: Config, folder: str, client: HubClient | None = None) ->
1505
1511
  shares knowledge first has the hub take back what it shared from there (withdraw); one that sends transcripts
1506
1512
  leaves them to the hub, which files them by their git remote or their own folder again. Returns the reloaded
1507
1513
  config and how many sessions the hub took back (None: it wasn't asked)."""
1514
+ folder = normalize_project_path(folder)
1508
1515
  if folder not in cfg.hub_folders:
1509
1516
  raise FolderError("{folder} was not added.", folder=folder)
1510
1517
  taken = None
@@ -1539,12 +1546,14 @@ def leave_project(cfg: Config, project: str) -> Config:
1539
1546
  if not cfg.shares_knowledge:
1540
1547
  raise FolderError("Only a computer that shares knowledge can leave a project: one that sends its transcripts "
1541
1548
  "sends them all.")
1549
+ project = normalize_project_path(project)
1542
1550
  return cfg if project in cfg.hub_left else _save_hub(cfg, left=[*cfg.hub_left, project])
1543
1551
 
1544
1552
 
1545
1553
  @_one_change
1546
1554
  def rejoin_project(cfg: Config, project: str) -> Config:
1547
1555
  """Undo leave_project: sessions filed under `project` are shared again, and its teammates' lessons come back."""
1556
+ project = normalize_project_path(project)
1548
1557
  return _save_hub(cfg, left=[x for x in cfg.hub_left if x != project]) if project in cfg.hub_left else cfg
1549
1558
 
1550
1559
 
@@ -1963,10 +1972,16 @@ def team_lessons(cfg: Config, body: dict, person: dict | None = None, conn=None)
1963
1972
  except HubError as exc:
1964
1973
  log.warning("team store: this hub's own sessions not shared: %s", exc)
1965
1974
  remotes = [normalize_remote(x) or x for x in body.get("remotes") or [] if isinstance(x, str) and len(x) <= 300][:500]
1966
- projects = [x for x in body.get("projects") or [] if isinstance(x, str) and x.startswith("/") and len(x) <= 1024]
1975
+ projects = [normalize_project_path(x) for x in body.get("projects") or []
1976
+ if isinstance(x, str) and is_abs_path(x) and len(x) <= 1024]
1967
1977
  left = clean_left(body.get("left")) # projects the computer left: none of their lessons
1968
1978
  within = projects_of(person)
1969
- data = _in_store(lambda: store.lessons_for(machine_id, remotes, projects[:MAX_FOLDERS], within=within, left=left))
1979
+
1980
+ def spelled(paths): # a Windows project the store may hold as C:\x or c:/x (filed before it was normalized)
1981
+ return None if paths is None else list(dict.fromkeys(v for p in paths for v in path_variants(p)))
1982
+
1983
+ data = _in_store(lambda: store.lessons_for(machine_id, remotes, spelled(projects[:MAX_FOLDERS]), within=spelled(within),
1984
+ left=spelled(left)))
1970
1985
  version = hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()[:32]
1971
1986
  if body.get("version") == version:
1972
1987
  return {"team": True, "version": version, "unchanged": True}
@@ -2243,13 +2258,15 @@ class ProjectResolver:
2243
2258
  def resolve(self, machine_id: str | None, path: str | None, remote: str | None = None) -> str | None:
2244
2259
  if not path or not machine_id or machine_id == self.me:
2245
2260
  return self.project_of(path)
2261
+ if path_kind(path) in ("drive", "unc"): # another computer's Windows folder: spelled one way, as its [hub]
2262
+ path = normalize_project_path(path) # folders and left are, so C:\app and c:/app are one project here
2246
2263
  return self.project_of(self._resolve(machine_id, path, remote))
2247
2264
 
2248
2265
  def _resolve(self, machine_id: str, path: str, remote: str | None) -> str:
2249
2266
  norm, top = normalize_remote(remote), None
2250
2267
  for repo_top, repo_remote in sorted(self.repos(machine_id).values(), key=lambda v: -len(v[0])):
2251
- if path == repo_top or path.startswith(repo_top.rstrip("/") + "/"):
2252
- top, norm = repo_top, norm or repo_remote
2268
+ if under(path, repo_top):
2269
+ top, norm = normalize_project_path(repo_top), norm or repo_remote
2253
2270
  break
2254
2271
  local_top = self.local_remotes().get(norm) if norm else None
2255
2272
  added = self.folders(machine_id)
@@ -377,6 +377,8 @@ def _windows_install(cfg: Config, exe: str, label: str, args: list[str], *, ever
377
377
  arguments = windows.runner_arguments(args, log=log, home=cfg.home, path_dirs=[str(Path(claude).parent)] if claude else [])
378
378
  if dry_run:
379
379
  return None
380
+ if label == UI_LABEL:
381
+ windows.clear_quit(cfg.home) # a Quit from the icon lasts until this: the dashboard runs again now
380
382
  xml = windows.task_xml(description=description, command=str(pythonw), arguments=arguments,
381
383
  every_minutes=every_minutes, keep_alive=keep_alive)
382
384
  return windows.install_task(WINDOWS_TASKS[label], xml)
@@ -256,6 +256,13 @@ def setting_info(cfg: Config) -> dict:
256
256
  from .install import UI_LABEL, launchd_status
257
257
  from .update import install_method
258
258
 
259
+ if sys.platform == "win32":
260
+ from .windows import task_status
261
+ return {"supported": True, "on": cfg.server_menu_bar, "shown": SHOWN, "app_extra": True,
262
+ "login_item": bool(os.environ.get("INTERLATCH_WINDOWS_TASK")),
263
+ "agent": bool(task_status("Dashboard").get("installed")), "can_install": False,
264
+ "command": "", "listed_as": "Interlatch"}
265
+
259
266
  if sys.platform != "darwin" or install_method()["kind"] in ("app", "container"): # the app always shows it
260
267
  return {"supported": False}
261
268
  cmd = extra_command()
@@ -307,6 +314,8 @@ def wants_menu_bar(cfg: Config) -> bool:
307
314
  (or a checkout's dev server) adds no second icon."""
308
315
  from .install import UI_LABEL
309
316
 
317
+ if sys.platform == "win32":
318
+ return cfg.server_menu_bar and bool(os.environ.get("INTERLATCH_WINDOWS_TASK"))
310
319
  return (sys.platform == "darwin" and cfg.server_menu_bar
311
320
  and os.environ.get("XPC_SERVICE_NAME") == UI_LABEL)
312
321
 
@@ -737,8 +746,15 @@ class StatusMenu:
737
746
 
738
747
  # ---------------------------------------------------------------------------------------------- `interlatch ui`
739
748
  def run_with_server(cfg: Config, httpd, url: str) -> bool:
740
- """`interlatch ui` with the menu-bar item: the dashboard serves on a thread, AppKit runs on this (main) thread
741
- until Quit or Ctrl+C. False, with nothing started, when the `app` extra (PyObjC) is not installed."""
749
+ """`interlatch ui` with the menu-bar item: the dashboard serves on a thread, AppKit (or Windows tray) runs on this
750
+ (main) thread until Quit or Ctrl+C. False, with nothing started, when not supported."""
751
+ global SHOWN
752
+ if sys.platform == "win32":
753
+ from .windows import run_tray_with_server
754
+
755
+ SHOWN = True # while the icon shows: run_tray_with_server returns when it is gone
756
+ SHOWN = run_tray_with_server(cfg, httpd, url)
757
+ return SHOWN
742
758
  try:
743
759
  import AppKit
744
760
  from PyObjCTools import AppHelper
@@ -747,7 +763,6 @@ def run_with_server(cfg: Config, httpd, url: str) -> bool:
747
763
  return False
748
764
  from .install import UI_LABEL
749
765
 
750
- global SHOWN
751
766
  SHOWN = True
752
767
  app = httpd.app
753
768
  AppKit.NSApplication.sharedApplication().setActivationPolicy_(AppKit.NSApplicationActivationPolicyAccessory)
@@ -43,6 +43,7 @@ try:
43
43
  except ImportError: # Windows, where Chronicle never ran: there is no folder of its to move
44
44
  fcntl = None
45
45
  from .install import LEGACY_BUNDLE_ID, LEGACY_MCP_NAME, MCP_NAME
46
+ from .util import abs_path_sql
46
47
 
47
48
  log = logging.getLogger("interlatch.migrate")
48
49
 
@@ -314,7 +315,7 @@ def _projects(cfg: Config) -> list[Path]:
314
315
  conn = sqlite3.connect(f"file:{cfg.db_path}?mode=ro", uri=True, timeout=30)
315
316
  try:
316
317
  rows = conn.execute("SELECT DISTINCT project_path FROM sessions WHERE agent = 'claude' "
317
- "AND project_path LIKE '/%'").fetchall()
318
+ f"AND {abs_path_sql('project_path')}").fetchall() # /x, and on Windows C:\x
318
319
  except sqlite3.Error:
319
320
  return []
320
321
  finally:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The background sync calls release_check() on every run. It asks pypi.org at most once a day (update.check_due) and
4
4
  notifies once per release, so a notification that was ignored or dismissed does not come back. macOS posts through
5
- osascript, Linux through notify-send; neither needs anything installed.
5
+ osascript, Linux through notify-send, Windows through PowerShell (a toast); none needs anything installed.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -11,6 +11,7 @@ import logging
11
11
  import platform
12
12
  import shutil
13
13
  import subprocess
14
+ import sys
14
15
 
15
16
  from .config import Config
16
17
 
@@ -25,10 +26,65 @@ def _applescript(text: str) -> str:
25
26
  return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
26
27
 
27
28
 
29
+ # Windows shows a toast only for an app it knows by its AppUserModelID: an unknown one ("Interlatch") shows nothing and
30
+ # raises nothing. This is the ID of the Windows PowerShell shortcut that every Windows has (BurntToast uses it too).
31
+ WINDOWS_AUMID = r"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe"
32
+ # The title and message come on stdin as JSON (ASCII: json.dumps escapes the rest), never inside the script itself.
33
+ # A toast, else (no WinRT) a notification-area balloon; exit 1 when neither could be shown.
34
+ WINDOWS_SCRIPT = r"""$ErrorActionPreference = 'Stop'
35
+ try {
36
+ $data = [Console]::In.ReadToEnd() | ConvertFrom-Json
37
+ try {
38
+ $null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
39
+ $xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
40
+ $text = $xml.GetElementsByTagName('text')
41
+ $null = $text.Item(0).AppendChild($xml.CreateTextNode([string]$data.title))
42
+ $null = $text.Item(1).AppendChild($xml.CreateTextNode([string]$data.message))
43
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
44
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('%AUMID%').Show($toast)
45
+ } catch {
46
+ Add-Type -AssemblyName System.Windows.Forms, System.Drawing
47
+ $icon = New-Object System.Windows.Forms.NotifyIcon
48
+ $icon.Icon = [System.Drawing.SystemIcons]::Information
49
+ $icon.Visible = $true
50
+ $icon.ShowBalloonTip(10000, [string]$data.title, [string]$data.message, [System.Windows.Forms.ToolTipIcon]::Info)
51
+ Start-Sleep -Seconds 8 # the balloon goes with its icon
52
+ $icon.Dispose()
53
+ }
54
+ } catch {
55
+ exit 1
56
+ }
57
+ """.replace("%AUMID%", WINDOWS_AUMID)
58
+
59
+
60
+ def windows_command(ps: str) -> list[str]:
61
+ """powershell.exe running WINDOWS_SCRIPT: -EncodedCommand (UTF-16LE, base64), so no quoting of the script is needed."""
62
+ import base64
63
+
64
+ return [ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand",
65
+ base64.b64encode(WINDOWS_SCRIPT.encode("utf-16-le")).decode("ascii")]
66
+
67
+
68
+ def _post_windows(title: str, message: str) -> bool:
69
+ import json
70
+
71
+ ps = shutil.which("powershell.exe") or shutil.which("powershell")
72
+ if not ps:
73
+ return False
74
+ try: # no console window flashes up when the background sync (pythonw) posts it
75
+ proc = subprocess.run(windows_command(ps), input=json.dumps({"title": title, "message": message}), text=True,
76
+ capture_output=True, timeout=30, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
77
+ except (OSError, subprocess.SubprocessError):
78
+ return False
79
+ return proc.returncode == 0
80
+
81
+
28
82
  def post(title: str, message: str) -> bool:
29
83
  """Show a desktop notification; False when this system has no way to (or it failed)."""
30
84
  if platform.system() == "Darwin":
31
85
  cmd = ["osascript", "-e", f"display notification {_applescript(message)} with title {_applescript(title)}"]
86
+ elif sys.platform == "win32" or platform.system() == "Windows":
87
+ return _post_windows(title, message)
32
88
  elif shutil.which("notify-send"):
33
89
  cmd = ["notify-send", "--app-name=Interlatch", title, message]
34
90
  else:
@@ -39,7 +39,7 @@ import threading
39
39
  import time
40
40
  from datetime import datetime, timedelta, timezone
41
41
 
42
- from .util import utcnow_iso
42
+ from .util import is_abs_path, normalize_project_path, utcnow_iso
43
43
 
44
44
  ROLES = ("admin", "member", "readonly")
45
45
  ROLE_RANK = {"readonly": 0, "member": 1, "admin": 2}
@@ -149,8 +149,8 @@ def clean_projects(projects) -> list[str] | None:
149
149
  out = set()
150
150
  for x in projects:
151
151
  x = str(x or "").strip()
152
- x = x.rstrip("/") or x
153
- if not x.startswith("/") or len(x) > 1024 or "\0" in x:
152
+ x = normalize_project_path(x)
153
+ if not is_abs_path(x) or len(x) > 1024 or "\0" in x:
154
154
  raise PeopleError("{project} is not a project on this hub", project=x or "''")
155
155
  out.add(x)
156
156
  return sorted(out)