aicodeman 1.46.0 → 1.48.0
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.
- package/README.md +3 -0
- package/dist/cli-notify.d.ts +34 -0
- package/dist/cli-notify.d.ts.map +1 -0
- package/dist/cli-notify.js +100 -0
- package/dist/cli-notify.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/cli-registry/patterns.d.ts +1 -1
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +4 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +6 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +62 -2
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +43 -26
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +47 -17
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/path-probe.d.ts +6 -0
- package/dist/config/path-probe.d.ts.map +1 -1
- package/dist/config/path-probe.js +6 -0
- package/dist/config/path-probe.js.map +1 -1
- package/dist/config-backup.d.ts +1 -1
- package/dist/config-backup.d.ts.map +1 -1
- package/dist/config-backup.js +2 -0
- package/dist/config-backup.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +32 -8
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js +10 -0
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +19 -5
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +88 -8
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +6 -0
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +92 -10
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/discord-bridge.d.ts +338 -0
- package/dist/discord-bridge.d.ts.map +1 -0
- package/dist/discord-bridge.js +1256 -0
- package/dist/discord-bridge.js.map +1 -0
- package/dist/edit-history-local-source.d.ts +83 -0
- package/dist/edit-history-local-source.d.ts.map +1 -0
- package/dist/edit-history-local-source.js +198 -0
- package/dist/edit-history-local-source.js.map +1 -0
- package/dist/edit-history-parse.d.ts +114 -0
- package/dist/edit-history-parse.d.ts.map +1 -0
- package/dist/edit-history-parse.js +312 -0
- package/dist/edit-history-parse.js.map +1 -0
- package/dist/edit-history-store.d.ts +209 -0
- package/dist/edit-history-store.d.ts.map +1 -0
- package/dist/edit-history-store.js +599 -0
- package/dist/edit-history-store.js.map +1 -0
- package/dist/git-clone.d.ts +6 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +79 -14
- package/dist/git-clone.js.map +1 -1
- package/dist/image-watcher.js +4 -4
- package/dist/image-watcher.js.map +1 -1
- package/dist/mux-interface.d.ts +10 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/ralph-fix-plan-watcher.d.ts +13 -1
- package/dist/ralph-fix-plan-watcher.d.ts.map +1 -1
- package/dist/ralph-fix-plan-watcher.js +21 -3
- package/dist/ralph-fix-plan-watcher.js.map +1 -1
- package/dist/ralph-tracker.d.ts +6 -0
- package/dist/ralph-tracker.d.ts.map +1 -1
- package/dist/ralph-tracker.js +8 -0
- package/dist/ralph-tracker.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +4 -0
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session.d.ts +70 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +147 -4
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +40 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +62 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/discord.d.ts +55 -0
- package/dist/types/discord.d.ts.map +1 -0
- package/dist/types/discord.js +18 -0
- package/dist/types/discord.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +13 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/case-path.d.ts +2 -0
- package/dist/web/case-path.d.ts.map +1 -1
- package/dist/web/case-path.js +3 -3
- package/dist/web/case-path.js.map +1 -1
- package/dist/web/edit-history-sync.d.ts +158 -0
- package/dist/web/edit-history-sync.d.ts.map +1 -0
- package/dist/web/edit-history-sync.js +445 -0
- package/dist/web/edit-history-sync.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +6 -0
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +6 -0
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +13 -0
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.3093527f.js → app.963b5ba8.js} +18 -18
- package/dist/web/public/app.963b5ba8.js.br +0 -0
- package/dist/web/public/app.963b5ba8.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
- package/dist/web/public/constants.782170e1.js.br +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +630 -0
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.33f930d7.js +2 -0
- package/dist/web/public/i18n.33f930d7.js.br +0 -0
- package/dist/web/public/i18n.33f930d7.js.gz +0 -0
- package/dist/web/public/{image-input.941386e7.js → image-input.a5d0f8f3.js} +18 -9
- package/dist/web/public/image-input.a5d0f8f3.js.br +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +162 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.4e8b20ac.js → keyboard-accessory.d4ec5957.js} +77 -2
- package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
- package/dist/web/public/keyboard-accessory.d4ec5957.js.gz +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.5ef2ce85.css +1 -0
- package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
- package/dist/web/public/mobile.5ef2ce85.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.b100727d.js} +2 -2
- package/dist/web/public/panels-ui.b100727d.js.br +0 -0
- package/dist/web/public/{panels-ui.4a56ee63.js.gz → panels-ui.b100727d.js.gz} +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
- package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/security-notices.json +1 -21
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.186f9419.js → session-ui.5ac82f2c.js} +30 -30
- package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js +106 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.77dc538f.css +1 -0
- package/dist/web/public/styles.77dc538f.css.br +0 -0
- package/dist/web/public/styles.77dc538f.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js +1 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +8 -6
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/{terminal-ui.7e787829.js → terminal-ui.d7c75ce8.js} +3 -3
- package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/request-body-parser.d.ts +23 -0
- package/dist/web/request-body-parser.d.ts.map +1 -0
- package/dist/web/request-body-parser.js +57 -0
- package/dist/web/request-body-parser.js.map +1 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +104 -8
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +5 -1
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
- package/dist/web/routes/config-backup-routes.js +4 -1
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/custom-model-routes.d.ts +7 -2
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +51 -6
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.d.ts +25 -0
- package/dist/web/routes/discord-routes.d.ts.map +1 -0
- package/dist/web/routes/discord-routes.js +206 -0
- package/dist/web/routes/discord-routes.js.map +1 -0
- package/dist/web/routes/edit-history-routes.d.ts +43 -0
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
- package/dist/web/routes/edit-history-routes.js +106 -0
- package/dist/web/routes/edit-history-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts +5 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +67 -3
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +45 -2
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +17 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +92 -37
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +4 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +68 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +77 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +15 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +43 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +237 -15
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.3093527f.js.br +0 -0
- package/dist/web/public/app.3093527f.js.gz +0 -0
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/i18n.70265257.js +0 -2
- package/dist/web/public/i18n.70265257.js.br +0 -0
- package/dist/web/public/i18n.70265257.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.br +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +0 -1
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/session-ui.186f9419.js.br +0 -0
- package/dist/web/public/session-ui.186f9419.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +0 -106
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/styles.7238eae8.css +0 -1
- package/dist/web/public/styles.7238eae8.css.br +0 -0
- package/dist/web/public/styles.7238eae8.css.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +0 -1
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Edit history: a header button (next to Attachments) and the floating panel it opens
|
|
3
|
+
* right below, listing every file edit the ACTIVE session's agent (and its subagents) made, newest
|
|
4
|
+
* first.
|
|
5
|
+
*
|
|
6
|
+
* OPTIONAL and per-device: `showEditHistory` (App Settings → Header & Panels → Header), default
|
|
7
|
+
* OFF. While it is off the button never shows and nothing is requested. The panel DRIVES the
|
|
8
|
+
* server's reading, which never runs in the background: it reads
|
|
9
|
+
* `GET /api/sessions/:id/edits?sync=1` when it opens and when the active session changes, and,
|
|
10
|
+
* only while it is open, re-reads (debounced) on the active session's `transcript:tool_end` for a
|
|
11
|
+
* tool that can change files (the existing transcript watcher's event) and on its `session:idle`
|
|
12
|
+
* (the end of a turn). No poll, and nothing at all while the panel is closed.
|
|
13
|
+
*
|
|
14
|
+
* - A code entry opens its diff inline (`GET /api/sessions/:id/edits/:entryId`), drawn with the Git
|
|
15
|
+
* panel's diff classes (`git-diff`, `git-diff-line--hunk|add|del`).
|
|
16
|
+
* - An image entry opens the shared file preview (panels-ui.js `openFilePreview`), as does a click
|
|
17
|
+
* on any path; inside the workspace it also shows a thumbnail. Both go through the existing file
|
|
18
|
+
* routes only: the thumbnail is the confined `file-raw`, and the preview of a file outside the
|
|
19
|
+
* workspace registers it on click with `notify: false` (`_registerExternalPreview`, which
|
|
20
|
+
* enforces the attachment guard and its extension allowlist). A path from the transcript is
|
|
21
|
+
* never served by a route of its own.
|
|
22
|
+
* - `backfillTruncated` offers "Load older" (`POST /api/sessions/:id/edits/older`).
|
|
23
|
+
*
|
|
24
|
+
* Paths, agent descriptions and diffs are untrusted text: they are only ever written with
|
|
25
|
+
* `textContent`, never `innerHTML`.
|
|
26
|
+
*
|
|
27
|
+
* @mixin Extends CodemanApp.prototype via Object.assign
|
|
28
|
+
* @dependency app.js (this.activeSessionId, this.sessions, this.loadAppSettingsFromStorage, this.getDefaultSettings, this.$)
|
|
29
|
+
* @dependency api-client.js (this._api, this._apiJson; resolved at call time)
|
|
30
|
+
* @dependency panels-ui.js (openFilePreview, _isExternalPreviewPath)
|
|
31
|
+
* @dependency i18n.js (window.codemanT)
|
|
32
|
+
* @loadorder 12.58 of 16, after git-status-ui.js, before entrance-animations.js
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** SSE bursts (one event per tool call) collapse into one read. */
|
|
36
|
+
const EDIT_HISTORY_SSE_DEBOUNCE_MS = 600;
|
|
37
|
+
/** Tools whose end can mean changed files (Agent/Task: a subagent's edits; SendUserFile: an image). */
|
|
38
|
+
const EDIT_HISTORY_FOLLOW_TOOLS = new Set([
|
|
39
|
+
'Edit',
|
|
40
|
+
'Write',
|
|
41
|
+
'MultiEdit',
|
|
42
|
+
'NotebookEdit',
|
|
43
|
+
'Bash',
|
|
44
|
+
'Agent',
|
|
45
|
+
'Task',
|
|
46
|
+
'SendUserFile',
|
|
47
|
+
]);
|
|
48
|
+
/** Per-device view toggles (a viewer convenience, so plain localStorage). */
|
|
49
|
+
const EDIT_HISTORY_GROUP_KEY = 'codeman-edit-history-group';
|
|
50
|
+
const EDIT_HISTORY_MAIN_ONLY_KEY = 'codeman-edit-history-main-only';
|
|
51
|
+
|
|
52
|
+
Object.assign(CodemanApp.prototype, {
|
|
53
|
+
/** Per-device setting, default OFF. */
|
|
54
|
+
isEditHistoryEnabled() {
|
|
55
|
+
const settings = this.loadAppSettingsFromStorage();
|
|
56
|
+
const defaults = this.getDefaultSettings();
|
|
57
|
+
return (settings.showEditHistory ?? defaults.showEditHistory ?? false) === true;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/** Translate an English source string (i18n.js), with `{var}` interpolation. */
|
|
61
|
+
_eht(source, vars) {
|
|
62
|
+
if (typeof window.codemanT === 'function') return window.codemanT(source, vars);
|
|
63
|
+
return vars ? source.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m)) : source;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/** A row badge ("new"/"edit") through its scoped i18n key; English shows the bare word. */
|
|
67
|
+
_ehBadge(word) {
|
|
68
|
+
const key = `Edit history badge: ${word}`;
|
|
69
|
+
const out = this._eht(key);
|
|
70
|
+
return out === key ? word : out;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Called from applyHeaderVisibilitySettings() (boot and every settings save). */
|
|
74
|
+
applyEditHistoryVisibility() {
|
|
75
|
+
const on = this.isEditHistoryEnabled();
|
|
76
|
+
// Marker class, not [hidden]: the base rule is display:inline-flex !important (src/web/CLAUDE.md).
|
|
77
|
+
this.$('editHistoryBtn')?.classList.toggle('btn-edit-history--hidden', !on);
|
|
78
|
+
if (!on) {
|
|
79
|
+
this.closeEditHistoryPanel();
|
|
80
|
+
this._editHistory = null;
|
|
81
|
+
this._editHistoryEpoch = (this._editHistoryEpoch || 0) + 1; // an in-flight read must not repaint
|
|
82
|
+
clearTimeout(this._editHistoryDebounce);
|
|
83
|
+
this._editHistoryDebounce = null;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// ── Per-device view toggles ─────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
_editHistoryPref(key) {
|
|
90
|
+
try {
|
|
91
|
+
return localStorage.getItem(key) === '1';
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
_setEditHistoryPref(key, on) {
|
|
98
|
+
try {
|
|
99
|
+
localStorage.setItem(key, on ? '1' : '0');
|
|
100
|
+
} catch {
|
|
101
|
+
/* private window / blocked storage: the toggle still works for this page */
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
isEditHistoryGrouped() {
|
|
106
|
+
return this._editHistoryGroupOn ?? (this._editHistoryGroupOn = this._editHistoryPref(EDIT_HISTORY_GROUP_KEY));
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
isEditHistoryMainOnly() {
|
|
110
|
+
return this._editHistoryMainOnly ?? (this._editHistoryMainOnly = this._editHistoryPref(EDIT_HISTORY_MAIN_ONLY_KEY));
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
setEditHistoryGrouped(on) {
|
|
114
|
+
this._editHistoryGroupOn = !!on;
|
|
115
|
+
this._setEditHistoryPref(EDIT_HISTORY_GROUP_KEY, !!on);
|
|
116
|
+
this._renderEditHistoryPanel();
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
setEditHistoryMainOnly(on) {
|
|
120
|
+
this._editHistoryMainOnly = !!on;
|
|
121
|
+
this._setEditHistoryPref(EDIT_HISTORY_MAIN_ONLY_KEY, !!on);
|
|
122
|
+
this._renderEditHistoryPanel();
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// ── Data ────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/** The data for the session on screen, or null. */
|
|
128
|
+
_currentEditHistory() {
|
|
129
|
+
const h = this._editHistory;
|
|
130
|
+
return h && h.sessionId === this.activeSessionId ? h : null;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
_editHistoryBase(sid) {
|
|
134
|
+
return `/api/sessions/${encodeURIComponent(sid)}/edits`;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read the active session's history and repaint. `older` asks the server for entries before the
|
|
139
|
+
* backfill window (POST), otherwise the normal read. Answers for another session are dropped.
|
|
140
|
+
*/
|
|
141
|
+
async refreshEditHistory({ older = false } = {}) {
|
|
142
|
+
if (!this.isEditHistoryEnabled()) return;
|
|
143
|
+
const sid = this.activeSessionId || null;
|
|
144
|
+
if (!sid) {
|
|
145
|
+
this._editHistory = null;
|
|
146
|
+
this._renderEditHistoryPanel();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const epoch = (this._editHistoryEpoch = (this._editHistoryEpoch || 0) + 1);
|
|
150
|
+
const prev = this._currentEditHistory();
|
|
151
|
+
if (!prev) {
|
|
152
|
+
// Another session's list must not stay on screen while this one loads.
|
|
153
|
+
this._editHistory = { sessionId: sid, state: 'loading', data: null, diffs: new Map(), open: new Set() };
|
|
154
|
+
this._renderEditHistoryPanel();
|
|
155
|
+
} else if (older) {
|
|
156
|
+
prev.loadingOlder = true;
|
|
157
|
+
this._renderEditHistoryPanel();
|
|
158
|
+
}
|
|
159
|
+
const base = this._editHistoryBase(sid);
|
|
160
|
+
const data = older
|
|
161
|
+
? await this._apiJson(`${base}/older`, { method: 'POST', body: {} })
|
|
162
|
+
: await this._apiJson(`${base}?sync=1`);
|
|
163
|
+
if (epoch !== this._editHistoryEpoch || sid !== this.activeSessionId || !this.isEditHistoryEnabled()) return;
|
|
164
|
+
const cur = this._currentEditHistory() || {
|
|
165
|
+
sessionId: sid,
|
|
166
|
+
diffs: new Map(),
|
|
167
|
+
open: new Set(),
|
|
168
|
+
};
|
|
169
|
+
cur.loadingOlder = false;
|
|
170
|
+
if (data && typeof data === 'object') {
|
|
171
|
+
cur.state = 'ok';
|
|
172
|
+
cur.data = data;
|
|
173
|
+
} else if (!cur.data) {
|
|
174
|
+
cur.state = 'error';
|
|
175
|
+
}
|
|
176
|
+
this._editHistory = cur;
|
|
177
|
+
this._renderEditHistoryPanel();
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
loadOlderEditHistory() {
|
|
181
|
+
return this.refreshEditHistory({ older: true });
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* SSE `transcript:tool_end` ({ sessionId, toolName }), and the end of a turn through
|
|
186
|
+
* `onEditHistorySessionIdle`: while the panel is open on that session, re-read after a tool that
|
|
187
|
+
* can change files, and when the turn ends (a parallel tool call reports only one tool end; a
|
|
188
|
+
* background subagent writes after the tool end that started it). Closed: nothing.
|
|
189
|
+
*/
|
|
190
|
+
_onEditHistoryTranscriptEvent(data) {
|
|
191
|
+
if (!data || data.sessionId !== this.activeSessionId || !this._isEditHistoryPanelOpen()) return;
|
|
192
|
+
if (data.toolName !== undefined && !EDIT_HISTORY_FOLLOW_TOOLS.has(data.toolName)) return;
|
|
193
|
+
if (!this.isEditHistoryEnabled()) return;
|
|
194
|
+
clearTimeout(this._editHistoryDebounce);
|
|
195
|
+
this._editHistoryDebounce = setTimeout(() => {
|
|
196
|
+
this._editHistoryDebounce = null;
|
|
197
|
+
if (this._isEditHistoryPanelOpen()) this.refreshEditHistory();
|
|
198
|
+
}, EDIT_HISTORY_SSE_DEBOUNCE_MS);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Called by app.js's `session:idle` handler ({ id }): the end of a turn. Claude's interactive
|
|
203
|
+
* transcript has no `result` line, so the transcript watcher's `transcript:complete` never fires
|
|
204
|
+
* for these sessions.
|
|
205
|
+
*/
|
|
206
|
+
onEditHistorySessionIdle(sessionId) {
|
|
207
|
+
this._onEditHistoryTranscriptEvent({ sessionId });
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/** Called by selectSession(): the open panel follows the active session. */
|
|
211
|
+
onEditHistoryActiveSession(sessionId) {
|
|
212
|
+
if (!this.isEditHistoryEnabled() || !this._isEditHistoryPanelOpen()) return;
|
|
213
|
+
if (this._editHistory?.sessionId === sessionId) return;
|
|
214
|
+
this.refreshEditHistory();
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// ── Panel ───────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
_isEditHistoryPanelOpen() {
|
|
220
|
+
return !!this.$('editHistoryPanel')?.classList.contains('visible');
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
toggleEditHistoryPanel() {
|
|
224
|
+
if (this._isEditHistoryPanelOpen()) {
|
|
225
|
+
this.closeEditHistoryPanel();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const panel = this.$('editHistoryPanel');
|
|
229
|
+
if (!panel) return;
|
|
230
|
+
panel.classList.add('visible');
|
|
231
|
+
this.$('editHistoryBtn')?.setAttribute('aria-expanded', 'true');
|
|
232
|
+
const group = this.$('editHistoryGroupToggle');
|
|
233
|
+
if (group) group.checked = this.isEditHistoryGrouped();
|
|
234
|
+
const main = this.$('editHistoryMainOnlyToggle');
|
|
235
|
+
if (main) main.checked = this.isEditHistoryMainOnly();
|
|
236
|
+
this._ensureEditHistoryPanelDrag();
|
|
237
|
+
this._renderEditHistoryPanel();
|
|
238
|
+
this.refreshEditHistory();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
closeEditHistoryPanel() {
|
|
242
|
+
const panel = this.$('editHistoryPanel');
|
|
243
|
+
if (panel) {
|
|
244
|
+
panel.classList.remove('visible');
|
|
245
|
+
// Reset a dragged position so it reopens at the default spot (a resized size is kept).
|
|
246
|
+
panel.style.left = panel.style.top = panel.style.right = panel.style.bottom = '';
|
|
247
|
+
}
|
|
248
|
+
this.$('editHistoryBtn')?.setAttribute('aria-expanded', 'false');
|
|
249
|
+
clearTimeout(this._editHistoryDebounce);
|
|
250
|
+
this._editHistoryDebounce = null;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/** Drag by the header (pointer events: mouse, pen and touch). One set of listeners per page. */
|
|
254
|
+
_ensureEditHistoryPanelDrag() {
|
|
255
|
+
const panel = this.$('editHistoryPanel');
|
|
256
|
+
const handle = panel?.querySelector('.edit-history-header');
|
|
257
|
+
if (!panel || !handle || handle._dragReady) return;
|
|
258
|
+
handle._dragReady = true;
|
|
259
|
+
let drag = null;
|
|
260
|
+
handle.addEventListener('pointerdown', (e) => {
|
|
261
|
+
if (e.target.closest('button')) return;
|
|
262
|
+
const rect = panel.getBoundingClientRect();
|
|
263
|
+
drag = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
|
|
264
|
+
// Switch from right/bottom anchoring to explicit left/top so the drag has one coordinate system.
|
|
265
|
+
panel.style.left = `${rect.left}px`;
|
|
266
|
+
panel.style.top = `${rect.top}px`;
|
|
267
|
+
panel.style.right = 'auto';
|
|
268
|
+
panel.style.bottom = 'auto';
|
|
269
|
+
// Pin the size too: the phone layout stretches between left and right, which a drag gives up.
|
|
270
|
+
panel.style.width = `${rect.width}px`;
|
|
271
|
+
panel.style.height = `${rect.height}px`;
|
|
272
|
+
handle.setPointerCapture?.(e.pointerId);
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
});
|
|
275
|
+
handle.addEventListener('pointermove', (e) => {
|
|
276
|
+
if (!drag) return;
|
|
277
|
+
const maxX = window.innerWidth - panel.offsetWidth - 4;
|
|
278
|
+
const maxY = window.innerHeight - panel.offsetHeight - 4;
|
|
279
|
+
panel.style.left = `${Math.max(4, Math.min(e.clientX - drag.dx, maxX))}px`;
|
|
280
|
+
panel.style.top = `${Math.max(4, Math.min(e.clientY - drag.dy, maxY))}px`;
|
|
281
|
+
});
|
|
282
|
+
const end = (e) => {
|
|
283
|
+
drag = null;
|
|
284
|
+
handle.releasePointerCapture?.(e.pointerId);
|
|
285
|
+
};
|
|
286
|
+
handle.addEventListener('pointerup', end);
|
|
287
|
+
handle.addEventListener('pointercancel', end);
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
_ehEl(tag, className, text) {
|
|
291
|
+
const el = document.createElement(tag);
|
|
292
|
+
if (className) el.className = className;
|
|
293
|
+
if (text !== undefined) el.textContent = text;
|
|
294
|
+
return el;
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/** "14:03:22", with the date in front when it is not today. */
|
|
298
|
+
_editHistoryTime(ts) {
|
|
299
|
+
const d = new Date(ts);
|
|
300
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
301
|
+
const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
302
|
+
return d.toDateString() === new Date().toDateString() ? time : `${d.toLocaleDateString()} ${time}`;
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
/** Open `path` in the shared file preview overlay (panels-ui.js). */
|
|
306
|
+
_openEditHistoryPreview(path) {
|
|
307
|
+
const sid = this.activeSessionId;
|
|
308
|
+
if (sid && path) this.openFilePreview?.(path, sid);
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
_editHistoryUrl(url) {
|
|
312
|
+
return typeof CodemanBase !== 'undefined' && CodemanBase.url ? CodemanBase.url(url) : url;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Thumbnail URL for an image entry inside the workspace: the confined `file-raw` route the file
|
|
317
|
+
* preview uses. An image outside the workspace gets none: showing it needs an attachment id
|
|
318
|
+
* (`POST …/attachments`, `notify: false`), and minting one per rendered row would push the
|
|
319
|
+
* session's real attachments out of its capped registry. Clicking it opens the file preview,
|
|
320
|
+
* which registers it like any other external file. SVGs get none either (`file-raw` serves
|
|
321
|
+
* them as a download).
|
|
322
|
+
*/
|
|
323
|
+
_editHistoryThumbSrc(path, sid) {
|
|
324
|
+
if (!path || /\.svg$/i.test(path) || this._isExternalPreviewPath?.(path, sid) !== false) return null;
|
|
325
|
+
return this._editHistoryUrl(`/api/sessions/${encodeURIComponent(sid)}/file-raw?path=${encodeURIComponent(path)}`);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
_renderEditHistoryPanel() {
|
|
329
|
+
const body = this.$('editHistoryBody');
|
|
330
|
+
const count = this.$('editHistoryCount');
|
|
331
|
+
if (!body || !this._isEditHistoryPanelOpen()) return;
|
|
332
|
+
const el = (tag, cls, text) => this._ehEl(tag, cls, text);
|
|
333
|
+
const t = (s, v) => this._eht(s, v);
|
|
334
|
+
const focusKey = body.contains(document.activeElement)
|
|
335
|
+
? document.activeElement.closest?.('[data-edit-key]')?.dataset.editKey
|
|
336
|
+
: null;
|
|
337
|
+
const scroll = body.scrollTop;
|
|
338
|
+
body.replaceChildren();
|
|
339
|
+
if (count) count.textContent = '';
|
|
340
|
+
|
|
341
|
+
if (!this.activeSessionId) {
|
|
342
|
+
body.append(el('div', 'edit-history-empty', t('Open a session to see its edits.')));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const h = this._currentEditHistory();
|
|
346
|
+
if (!h || h.state === 'loading') {
|
|
347
|
+
body.append(el('div', 'edit-history-empty', t('Reading the edit history…')));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (h.state === 'error' || !h.data) {
|
|
351
|
+
body.append(el('div', 'edit-history-empty', t('Could not read the edit history.')));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const data = h.data;
|
|
355
|
+
if (data.state === 'unsupported') {
|
|
356
|
+
body.append(el('div', 'edit-history-empty', t('Edit history is only available for local Claude sessions.')));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (data.state === 'unavailable') {
|
|
360
|
+
body.append(
|
|
361
|
+
el(
|
|
362
|
+
'div',
|
|
363
|
+
'edit-history-note edit-history-note--warn',
|
|
364
|
+
data.reason === 'no-transcript'
|
|
365
|
+
? t(
|
|
366
|
+
'No transcript found for this session yet. It is picked up with the next prompt; until then a CLAUDE_CONFIG_DIR set outside Codeman cannot be found.'
|
|
367
|
+
)
|
|
368
|
+
: t('Showing saved entries only: {reason}', { reason: data.reason || '' })
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const all = Array.isArray(data.entries) ? data.entries : [];
|
|
374
|
+
const entries = this.isEditHistoryMainOnly() ? all.filter((e) => !e.agentId) : all;
|
|
375
|
+
if (count) count.textContent = all.length ? String(entries.length) : '';
|
|
376
|
+
if (!all.length) {
|
|
377
|
+
body.append(el('div', 'edit-history-empty', t('No edits yet.')));
|
|
378
|
+
} else if (!entries.length) {
|
|
379
|
+
body.append(el('div', 'edit-history-empty', t('No edits by the main agent.')));
|
|
380
|
+
} else if (this.isEditHistoryGrouped()) {
|
|
381
|
+
const groups = new Map();
|
|
382
|
+
for (const e of entries) {
|
|
383
|
+
const key = e.path || e.relPath;
|
|
384
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
385
|
+
groups.get(key).push(e);
|
|
386
|
+
}
|
|
387
|
+
for (const items of groups.values()) body.append(this._editHistoryGroup(items, data, h));
|
|
388
|
+
} else {
|
|
389
|
+
const list = el('div', 'edit-history-list');
|
|
390
|
+
for (const e of entries) list.append(...this._editHistoryEntry(e, data, h, true));
|
|
391
|
+
body.append(list);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (data.backfillTruncated) {
|
|
395
|
+
const more = el(
|
|
396
|
+
'button',
|
|
397
|
+
'btn-toolbar btn-sm edit-history-older',
|
|
398
|
+
h.loadingOlder ? t('Loading older edits…') : t('Load older')
|
|
399
|
+
);
|
|
400
|
+
more.type = 'button';
|
|
401
|
+
more.disabled = !!h.loadingOlder;
|
|
402
|
+
more.addEventListener('click', () => this.loadOlderEditHistory());
|
|
403
|
+
body.append(more);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
body.scrollTop = scroll;
|
|
407
|
+
if (focusKey) {
|
|
408
|
+
const again = [...body.querySelectorAll('[data-edit-key]')].find((n) => n.dataset.editKey === focusKey);
|
|
409
|
+
again?.focus({ preventScroll: true });
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
/** One file with all its edits (newest first), for the "group by file" view. */
|
|
414
|
+
_editHistoryGroup(items, data, h) {
|
|
415
|
+
const el = (tag, cls, text) => this._ehEl(tag, cls, text);
|
|
416
|
+
const first = items[0];
|
|
417
|
+
const group = el('section', 'edit-history-group');
|
|
418
|
+
const head = el('div', 'edit-history-group-head');
|
|
419
|
+
head.append(this._editHistoryPathButton(first));
|
|
420
|
+
const code = items.filter((e) => e.kind !== 'image');
|
|
421
|
+
if (code.length) {
|
|
422
|
+
const added = code.reduce((s, e) => s + (Number(e.added) || 0), 0);
|
|
423
|
+
const removed = code.reduce((s, e) => s + (Number(e.removed) || 0), 0);
|
|
424
|
+
head.append(this._editHistoryStats(added, removed));
|
|
425
|
+
}
|
|
426
|
+
head.append(el('span', 'edit-history-group-count', String(items.length)));
|
|
427
|
+
group.append(head);
|
|
428
|
+
for (const e of items) group.append(...this._editHistoryEntry(e, data, h, false));
|
|
429
|
+
return group;
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The path as folder + file name: the folder is cut from its start when the row is narrow, so a
|
|
434
|
+
* long absolute path (an image in claude's scratchpad) still shows which file it is.
|
|
435
|
+
*/
|
|
436
|
+
_editHistoryPathButton(e) {
|
|
437
|
+
const shown = e.relPath || e.path || '';
|
|
438
|
+
const btn = this._ehEl('button', 'edit-history-path');
|
|
439
|
+
const cut = shown.lastIndexOf('/') + 1;
|
|
440
|
+
if (cut > 0) {
|
|
441
|
+
const dir = this._ehEl('span', 'edit-history-path-dir');
|
|
442
|
+
const bdi = this._ehEl('bdi', null, shown.slice(0, cut));
|
|
443
|
+
bdi.dir = 'ltr';
|
|
444
|
+
dir.append(bdi);
|
|
445
|
+
btn.append(dir);
|
|
446
|
+
}
|
|
447
|
+
btn.append(this._ehEl('span', 'edit-history-path-name', shown.slice(cut)));
|
|
448
|
+
btn.type = 'button';
|
|
449
|
+
btn.title = e.path || e.relPath || '';
|
|
450
|
+
btn.addEventListener('click', (ev) => {
|
|
451
|
+
ev.stopPropagation();
|
|
452
|
+
this._openEditHistoryPreview(e.path);
|
|
453
|
+
});
|
|
454
|
+
return btn;
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
_editHistoryStats(added, removed) {
|
|
458
|
+
const stats = this._ehEl('span', 'edit-history-stats');
|
|
459
|
+
stats.append(this._ehEl('span', 'edit-history-add', `+${added}`));
|
|
460
|
+
stats.append(this._ehEl('span', 'edit-history-del', `−${removed}`));
|
|
461
|
+
return stats;
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/** A row, followed by its diff when the row is open. `withPath` false inside a file group. */
|
|
465
|
+
_editHistoryEntry(e, data, h, withPath) {
|
|
466
|
+
const el = (tag, cls, text) => this._ehEl(tag, cls, text);
|
|
467
|
+
const t = (s, v) => this._eht(s, v);
|
|
468
|
+
const isImage = e.kind === 'image';
|
|
469
|
+
const row = el('div', `edit-history-entry edit-history-entry--${isImage ? 'image' : 'code'}`);
|
|
470
|
+
row.dataset.editKey = e.id;
|
|
471
|
+
row.tabIndex = 0;
|
|
472
|
+
row.setAttribute('role', 'button');
|
|
473
|
+
row.append(el('span', 'edit-history-time', this._editHistoryTime(e.ts)));
|
|
474
|
+
|
|
475
|
+
let badge;
|
|
476
|
+
if (isImage) {
|
|
477
|
+
badge = el('span', 'edit-history-badge edit-history-badge--image');
|
|
478
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
479
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
480
|
+
svg.setAttribute('width', '12');
|
|
481
|
+
svg.setAttribute('height', '12');
|
|
482
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
483
|
+
for (const [tag, attrs] of [
|
|
484
|
+
['rect', { x: '3', y: '4', width: '18', height: '16', rx: '2' }],
|
|
485
|
+
['circle', { cx: '9', cy: '10', r: '2' }],
|
|
486
|
+
['path', { d: 'M21 17l-5-5-9 8' }],
|
|
487
|
+
]) {
|
|
488
|
+
const n = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
|
489
|
+
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
|
|
490
|
+
svg.append(n);
|
|
491
|
+
}
|
|
492
|
+
badge.append(svg);
|
|
493
|
+
badge.title = t('Image');
|
|
494
|
+
} else if (e.created) {
|
|
495
|
+
badge = el('span', 'edit-history-badge edit-history-badge--new', this._ehBadge('new'));
|
|
496
|
+
badge.title = t('File created');
|
|
497
|
+
} else {
|
|
498
|
+
badge = el('span', 'edit-history-badge edit-history-badge--edit', this._ehBadge('edit'));
|
|
499
|
+
badge.title = t('File edited ({tool})', { tool: e.tool || '' });
|
|
500
|
+
}
|
|
501
|
+
row.append(badge);
|
|
502
|
+
|
|
503
|
+
if (withPath) row.append(this._editHistoryPathButton(e));
|
|
504
|
+
row.append(el('span', 'edit-history-spacer'));
|
|
505
|
+
if (!isImage) row.append(this._editHistoryStats(Number(e.added) || 0, Number(e.removed) || 0));
|
|
506
|
+
|
|
507
|
+
if (e.agentId) {
|
|
508
|
+
const agent = data.agents?.[e.agentId];
|
|
509
|
+
const chip = el('span', 'edit-history-agent', agent?.description || e.agentId);
|
|
510
|
+
const bits = [agent?.agentType, agent?.worktreeBranch].filter(Boolean);
|
|
511
|
+
chip.title = bits.join(' · ') || t('Subagent');
|
|
512
|
+
row.append(chip);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const out = [row];
|
|
516
|
+
if (isImage) {
|
|
517
|
+
row.title = t('Open in the file preview');
|
|
518
|
+
const open = () => this._openEditHistoryPreview(e.path);
|
|
519
|
+
row.addEventListener('click', open);
|
|
520
|
+
row.addEventListener('keydown', (ev) => {
|
|
521
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
522
|
+
ev.preventDefault();
|
|
523
|
+
open();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const src = this._editHistoryThumbSrc(e.path, h.sessionId);
|
|
527
|
+
if (src) {
|
|
528
|
+
const img = el('img', 'edit-history-thumb');
|
|
529
|
+
img.loading = 'lazy';
|
|
530
|
+
img.alt = e.relPath || '';
|
|
531
|
+
img.addEventListener('click', (ev) => {
|
|
532
|
+
ev.stopPropagation();
|
|
533
|
+
open();
|
|
534
|
+
});
|
|
535
|
+
img.addEventListener('error', () => img.remove());
|
|
536
|
+
img.src = src;
|
|
537
|
+
out.push(img);
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const isOpen = h.open.has(e.id);
|
|
543
|
+
row.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
544
|
+
row.title = isOpen ? t('Hide the diff') : t('Show what changed');
|
|
545
|
+
const toggle = () => this.toggleEditHistoryDiff(e.id);
|
|
546
|
+
row.addEventListener('click', toggle);
|
|
547
|
+
row.addEventListener('keydown', (ev) => {
|
|
548
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
549
|
+
ev.preventDefault();
|
|
550
|
+
toggle();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
if (isOpen) {
|
|
554
|
+
row.classList.add('edit-history-entry--open');
|
|
555
|
+
out.push(this._editHistoryDiffView(h.diffs.get(e.id)));
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
/** Open or close one entry's diff; the first open loads it, later ones reuse it. */
|
|
561
|
+
async toggleEditHistoryDiff(entryId) {
|
|
562
|
+
const h = this._currentEditHistory();
|
|
563
|
+
if (!h) return;
|
|
564
|
+
if (h.open.has(entryId)) {
|
|
565
|
+
h.open.delete(entryId);
|
|
566
|
+
this._renderEditHistoryPanel();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
h.open.add(entryId);
|
|
570
|
+
const cached = h.diffs.get(entryId);
|
|
571
|
+
if (cached && cached.state !== 'error') {
|
|
572
|
+
this._renderEditHistoryPanel();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const view = { state: 'loading' };
|
|
576
|
+
h.diffs.set(entryId, view);
|
|
577
|
+
this._renderEditHistoryPanel();
|
|
578
|
+
const res = await this._api(`${this._editHistoryBase(h.sessionId)}/${encodeURIComponent(entryId)}`);
|
|
579
|
+
let body = null;
|
|
580
|
+
try {
|
|
581
|
+
body = res ? await res.json() : null;
|
|
582
|
+
} catch {
|
|
583
|
+
/* fall through */
|
|
584
|
+
}
|
|
585
|
+
if (h.diffs.get(entryId) !== view) return;
|
|
586
|
+
if (res && res.ok && body?.success && typeof body.data?.diff === 'string') {
|
|
587
|
+
view.state = 'ok';
|
|
588
|
+
view.diff = body.data.diff;
|
|
589
|
+
view.truncated = !!body.data.truncated;
|
|
590
|
+
} else {
|
|
591
|
+
view.state = 'error';
|
|
592
|
+
view.error = body?.error || this._eht('Could not read the diff.');
|
|
593
|
+
}
|
|
594
|
+
if (this._editHistory === h) this._renderEditHistoryPanel();
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
/** The diff block under an open row, drawn with the Git panel's classes. */
|
|
598
|
+
_editHistoryDiffView(view) {
|
|
599
|
+
const el = (tag, cls, text) => this._ehEl(tag, cls, text);
|
|
600
|
+
const t = (s, v) => this._eht(s, v);
|
|
601
|
+
const wrap = el('div', 'edit-history-diff');
|
|
602
|
+
if (!view || view.state === 'loading') {
|
|
603
|
+
wrap.append(el('div', 'edit-history-empty', t('Reading the diff…')));
|
|
604
|
+
return wrap;
|
|
605
|
+
}
|
|
606
|
+
if (view.state === 'error') {
|
|
607
|
+
wrap.append(el('div', 'edit-history-empty', view.error));
|
|
608
|
+
return wrap;
|
|
609
|
+
}
|
|
610
|
+
if (!view.diff.trim()) {
|
|
611
|
+
wrap.append(el('div', 'edit-history-empty', t('No textual changes.')));
|
|
612
|
+
} else {
|
|
613
|
+
const pre = el('pre', 'git-diff');
|
|
614
|
+
const frag = document.createDocumentFragment();
|
|
615
|
+
for (const line of view.diff.replace(/\n$/, '').split('\n')) {
|
|
616
|
+
let cls = 'git-diff-line';
|
|
617
|
+
// The API sends hunks only (no `---`/`+++` headers), so a `---` line is a removed `--` line.
|
|
618
|
+
if (line.startsWith('@@')) cls += ' git-diff-line--hunk';
|
|
619
|
+
else if (line.startsWith('+')) cls += ' git-diff-line--add';
|
|
620
|
+
else if (line.startsWith('-')) cls += ' git-diff-line--del';
|
|
621
|
+
frag.append(el('span', cls, line + '\n'));
|
|
622
|
+
}
|
|
623
|
+
pre.append(frag);
|
|
624
|
+
wrap.append(pre);
|
|
625
|
+
}
|
|
626
|
+
if (view.truncated)
|
|
627
|
+
wrap.append(el('div', 'edit-history-more', t('Diff cut short: it is larger than the viewer shows.')));
|
|
628
|
+
return wrap;
|
|
629
|
+
},
|
|
630
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|