aicodeman 1.40.0 → 1.41.1
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 +81 -13
- package/dist/attachment-registry.d.ts +2 -0
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +14 -3
- package/dist/attachment-registry.js.map +1 -1
- package/dist/cli-agent.d.ts +227 -0
- package/dist/cli-agent.d.ts.map +1 -0
- package/dist/cli-agent.js +771 -0
- package/dist/cli-agent.js.map +1 -0
- package/dist/cli.d.ts +25 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -41
- package/dist/cli.js.map +1 -1
- package/dist/codeman-credentials.d.ts +27 -0
- package/dist/codeman-credentials.d.ts.map +1 -0
- package/dist/codeman-credentials.js +66 -0
- package/dist/codeman-credentials.js.map +1 -0
- package/dist/config/buffer-limits.d.ts +9 -0
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +9 -0
- package/dist/config/buffer-limits.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +10 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +15 -0
- 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 +47 -0
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +47 -1
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/folder-listing.d.ts +94 -0
- package/dist/folder-listing.d.ts.map +1 -0
- package/dist/folder-listing.js +241 -0
- package/dist/folder-listing.js.map +1 -0
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +4 -0
- package/dist/git-clone.js.map +1 -1
- package/dist/hooks-config.d.ts +36 -16
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +45 -20
- package/dist/hooks-config.js.map +1 -1
- package/dist/html-view-capabilities.d.ts +54 -0
- package/dist/html-view-capabilities.d.ts.map +1 -0
- package/dist/html-view-capabilities.js +103 -0
- package/dist/html-view-capabilities.js.map +1 -0
- package/dist/image-watcher.d.ts.map +1 -1
- package/dist/image-watcher.js +6 -2
- package/dist/image-watcher.js.map +1 -1
- package/dist/mcp-server.d.ts +114 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +917 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-sync-targets.d.ts +34 -0
- package/dist/mcp-sync-targets.d.ts.map +1 -0
- package/dist/mcp-sync-targets.js +55 -0
- package/dist/mcp-sync-targets.js.map +1 -0
- package/dist/mcp-sync.d.ts +7 -1
- package/dist/mcp-sync.d.ts.map +1 -1
- package/dist/mcp-sync.js +113 -6
- package/dist/mcp-sync.js.map +1 -1
- package/dist/mux-interface.d.ts +36 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-files.d.ts +37 -0
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +103 -0
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-pane-history.d.ts +103 -0
- package/dist/remote-pane-history.d.ts.map +1 -0
- package/dist/remote-pane-history.js +258 -0
- package/dist/remote-pane-history.js.map +1 -0
- package/dist/remote-ssh-limiter.d.ts +16 -3
- package/dist/remote-ssh-limiter.d.ts.map +1 -1
- package/dist/remote-ssh-limiter.js +37 -6
- package/dist/remote-ssh-limiter.js.map +1 -1
- package/dist/session.d.ts +29 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +63 -4
- package/dist/session.js.map +1 -1
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +29 -6
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/tab-layout-legacy-order.d.ts.map +1 -1
- package/dist/tab-layout-legacy-order.js +72 -13
- package/dist/tab-layout-legacy-order.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -2
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +102 -7
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tab-layout.d.ts +9 -0
- package/dist/tab-layout.d.ts.map +1 -1
- package/dist/tab-layout.js +15 -2
- package/dist/tab-layout.js.map +1 -1
- package/dist/templates/case-template.md +2 -0
- package/dist/tmux-manager.d.ts +76 -2
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +216 -40
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tui/tui-client.d.ts +3 -17
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +3 -44
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/session.d.ts +53 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +44 -7
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +72 -7
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/process-exit-wait.d.ts +40 -0
- package/dist/utils/process-exit-wait.d.ts.map +1 -0
- package/dist/utils/process-exit-wait.js +68 -0
- package/dist/utils/process-exit-wait.js.map +1 -0
- package/dist/web/approval-inbox.d.ts +12 -0
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +1 -0
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +33 -0
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/paste-image-gc.d.ts +46 -6
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +106 -32
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +8 -1
- 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.24689381.js +43 -0
- package/dist/web/public/app.24689381.js.br +0 -0
- package/dist/web/public/app.24689381.js.gz +0 -0
- package/dist/web/public/approvals-ui.js +4 -0
- package/dist/web/public/approvals-ui.js.br +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.c0f8f9de.js → constants.17d77b65.js} +690 -35
- package/dist/web/public/constants.17d77b65.js.br +0 -0
- package/dist/web/public/constants.17d77b65.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js +490 -43
- package/dist/web/public/entrance-animations.js.br +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/gesture/gesture-codeman.js +112 -6
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/home-sessions.js +2 -1
- package/dist/web/public/home-sessions.js.br +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.9afd8e4d.js +2 -0
- package/dist/web/public/i18n.9afd8e4d.js.br +0 -0
- package/dist/web/public/i18n.9afd8e4d.js.gz +0 -0
- package/dist/web/public/image-input.9ed1d0d2.js +453 -0
- package/dist/web/public/image-input.9ed1d0d2.js.br +0 -0
- package/dist/web/public/image-input.9ed1d0d2.js.gz +0 -0
- package/dist/web/public/index.html +223 -46
- 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.c1e7c7b7.js → keyboard-accessory.c458eea8.js} +115 -35
- package/dist/web/public/keyboard-accessory.c458eea8.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c458eea8.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +11 -2
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.2dff1580.css +1 -0
- package/dist/web/public/mobile.2dff1580.css.br +0 -0
- package/dist/web/public/mobile.2dff1580.css.gz +0 -0
- package/dist/web/public/{notification-manager.50bb87d8.js → notification-manager.feb82c04.js} +19 -2
- package/dist/web/public/notification-manager.feb82c04.js.br +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.4735dfc2.js → panels-ui.73ec0137.js} +87 -87
- package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
- package/dist/web/public/panels-ui.73ec0137.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.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.f731602c.js → session-ui.dbc6819b.js} +33 -33
- package/dist/web/public/session-ui.dbc6819b.js.br +0 -0
- package/dist/web/public/session-ui.dbc6819b.js.gz +0 -0
- package/dist/web/public/settings-ui.81ac264e.js +97 -0
- package/dist/web/public/settings-ui.81ac264e.js.br +0 -0
- package/dist/web/public/settings-ui.81ac264e.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js +11 -0
- package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
- package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.900a28e4.css +1 -0
- package/dist/web/public/styles.900a28e4.css.br +0 -0
- package/dist/web/public/styles.900a28e4.css.gz +0 -0
- package/dist/web/public/{subagent-windows.e6ca799f.js → subagent-windows.07277a58.js} +7 -5
- package/dist/web/public/subagent-windows.07277a58.js.br +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +6 -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 +1 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +135 -38
- 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.858f5da5.js +2 -0
- package/dist/web/public/terminal-ui.858f5da5.js.br +0 -0
- package/dist/web/public/terminal-ui.858f5da5.js.gz +0 -0
- package/dist/web/public/tile-grid.js +769 -137
- package/dist/web/public/tile-grid.js.br +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 +9 -6
- package/dist/web/public/ultracode-windows.js.br +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.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +4 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.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 +12 -0
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/route-helpers.d.ts +8 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +8 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +4 -1
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/approval-routes.d.ts.map +1 -1
- package/dist/web/routes/approval-routes.js +6 -8
- package/dist/web/routes/approval-routes.js.map +1 -1
- package/dist/web/routes/case-routes.js +1 -1
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/cli-registry-routes.d.ts +23 -4
- package/dist/web/routes/cli-registry-routes.d.ts.map +1 -1
- package/dist/web/routes/cli-registry-routes.js +92 -7
- package/dist/web/routes/cli-registry-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts +46 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +214 -27
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +8 -1
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/html-view-routes.d.ts +60 -0
- package/dist/web/routes/html-view-routes.d.ts.map +1 -0
- package/dist/web/routes/html-view-routes.js +288 -0
- package/dist/web/routes/html-view-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/mcp-sync-routes.d.ts +7 -2
- package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -1
- package/dist/web/routes/mcp-sync-routes.js +10 -3
- package/dist/web/routes/mcp-sync-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +3 -3
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +1 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +345 -90
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +16 -2
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +32 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +74 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +7 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +77 -24
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +5 -2
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +5 -2
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +5 -4
- package/skills/codeman/SKILL.md +30 -7
- package/README.zh-CN.md +0 -1147
- package/dist/web/public/app.abadc970.js +0 -43
- package/dist/web/public/app.abadc970.js.br +0 -0
- package/dist/web/public/app.abadc970.js.gz +0 -0
- package/dist/web/public/constants.c0f8f9de.js.br +0 -0
- package/dist/web/public/constants.c0f8f9de.js.gz +0 -0
- package/dist/web/public/i18n.a31747a9.js +0 -2
- package/dist/web/public/i18n.a31747a9.js.br +0 -0
- package/dist/web/public/i18n.a31747a9.js.gz +0 -0
- package/dist/web/public/image-input.898a538b.js +0 -281
- package/dist/web/public/image-input.898a538b.js.br +0 -0
- package/dist/web/public/image-input.898a538b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/mobile.111b187d.css +0 -1
- package/dist/web/public/mobile.111b187d.css.br +0 -0
- package/dist/web/public/mobile.111b187d.css.gz +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.br +0 -0
- package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
- package/dist/web/public/panels-ui.4735dfc2.js.br +0 -0
- package/dist/web/public/panels-ui.4735dfc2.js.gz +0 -0
- package/dist/web/public/session-ui.f731602c.js.br +0 -0
- package/dist/web/public/session-ui.f731602c.js.gz +0 -0
- package/dist/web/public/settings-ui.a97ab16f.js +0 -97
- package/dist/web/public/settings-ui.a97ab16f.js.br +0 -0
- package/dist/web/public/settings-ui.a97ab16f.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
- package/dist/web/public/styles.8ee12666.css +0 -1
- package/dist/web/public/styles.8ee12666.css.br +0 -0
- package/dist/web/public/styles.8ee12666.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.br +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
- package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
- package/dist/web/public/terminal-ui.5059b30e.js +0 -2
- package/dist/web/public/terminal-ui.5059b30e.js.br +0 -0
- package/dist/web/public/terminal-ui.5059b30e.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +0 -4
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
|
|
24
24
|
// Per-device tile font size (a tile is a fraction of the screen).
|
|
25
25
|
const TILE_GRID_FONT_KEY = 'codeman-tile-font-size';
|
|
26
|
-
// The grid this
|
|
27
|
-
// `{ v: 1, open, ids, focused, zoomed, colFr, rowFr }`,
|
|
28
|
-
// in reading order with `null` for an empty one
|
|
29
|
-
//
|
|
26
|
+
// The grid this browser last had, as the user left it (sanitizeTileGridState,
|
|
27
|
+
// constants.js): `{ v: 1, open, ids, count, focused, zoomed, colFr, rowFr }`,
|
|
28
|
+
// `ids` being the cells in reading order with `null` for an empty one, `count`
|
|
29
|
+
// how many tiles the user's own last change left (a session that went away by
|
|
30
|
+
// itself does not lower it). Session ids and layout only, never content.
|
|
31
|
+
// Written on every change while the grid is open; `open: false` keeps it for
|
|
32
|
+
// the Tiles toggle, which brings it back exactly; a grid stored open is
|
|
33
|
+
// restored on reload inside handleInit. Never sent to the server.
|
|
30
34
|
const TILE_GRID_STORAGE_KEY = 'codeman:tile-grid';
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
35
|
+
// The count last picked in the Tiles button's right-click menu (2, 4 or 6;
|
|
36
|
+
// owner decision 10), per device: what a click opens when there is no stored
|
|
37
|
+
// grid to bring back, and what a pick re-forms the grid to.
|
|
34
38
|
const TILE_GRID_COUNT_KEY = 'codeman:tile-count';
|
|
35
39
|
// The leaving tiles' fade (styles.css .tile--leaving) plus slack: the still
|
|
36
40
|
// copy of a closing grid goes even if animationend never comes (a hidden tab,
|
|
@@ -52,6 +56,26 @@ const TILE_GRID_REFIT_MS = 150;
|
|
|
52
56
|
// the grid section's padding (styles.css .tile-grid), for the drag math.
|
|
53
57
|
const TILE_DIVIDER_PX = 6;
|
|
54
58
|
const TILE_GRID_PADDING_PX = 4;
|
|
59
|
+
// What a tile's header drag (and a popped-out window's title drag) carries:
|
|
60
|
+
// the session id under a type of its own, never text, so no text field or
|
|
61
|
+
// terminal takes it as typing. Another Codeman window reads it on drop.
|
|
62
|
+
const TILE_DRAG_TYPE = 'application/x-codeman-tile';
|
|
63
|
+
// Detach Tiles: a tile dropped outside every window waits this long before it
|
|
64
|
+
// opens as a window of its own, so another window that took it (a
|
|
65
|
+
// 'tile-adopted' message on the window channel) can still say so first.
|
|
66
|
+
const TILE_TEAR_OFF_SETTLE_MS = 120;
|
|
67
|
+
// How long after its drag ended a tile can still be claimed by another
|
|
68
|
+
// window's drop (the claim and the dragend come from two windows).
|
|
69
|
+
const TILE_ADOPT_WINDOW_MS = 5000;
|
|
70
|
+
// A session docked here from another window: its old window's 'detached'
|
|
71
|
+
// announcements this soon after are stale (it is closing), not a new pop-out.
|
|
72
|
+
const TILE_DOCK_GRACE_MS = 2000;
|
|
73
|
+
// The smallest window a detached tile opens as, and roughly how much browser
|
|
74
|
+
// chrome sits above a pop-out's content (its title and address bars), so the
|
|
75
|
+
// window's own header lands near where the tile was let go.
|
|
76
|
+
const TILE_WINDOW_MIN_W = 480;
|
|
77
|
+
const TILE_WINDOW_MIN_H = 320;
|
|
78
|
+
const TILE_WINDOW_CHROME_PX = 60;
|
|
55
79
|
|
|
56
80
|
/** `minmax(0, 1fr) 6px minmax(0, 2fr) ...`: tracks with a divider track between each. */
|
|
57
81
|
function tileGridTracks(fr) {
|
|
@@ -147,6 +171,11 @@ class TileGridModel {
|
|
|
147
171
|
// session id, or null for an empty cell. THE source of truth for where
|
|
148
172
|
// each tile is (owner: an empty cell can be any cell); `ids` derives from it.
|
|
149
173
|
this.cells = [];
|
|
174
|
+
// How many tiles the user's own last change left (open, add, remove, a
|
|
175
|
+
// count picked): a session that goes away by itself (deleted, popped out,
|
|
176
|
+
// refused) does not lower it, so the next time the grid opens the ranking
|
|
177
|
+
// fills that place. Stored with the grid (_persistTileGrid).
|
|
178
|
+
this.count = 0;
|
|
150
179
|
// id -> { tile: TerminalTile, el: HTMLElement }
|
|
151
180
|
this.tiles = new Map();
|
|
152
181
|
this.focusedId = null;
|
|
@@ -237,8 +266,15 @@ Object.assign(CodemanApp.prototype, {
|
|
|
237
266
|
if (state !== 'idle') this._setTileLoadingLabel(entry.body);
|
|
238
267
|
entry.el.classList.toggle('tile--loading', state !== 'idle');
|
|
239
268
|
// The first capture has landed (or failed): the terminal fades in,
|
|
240
|
-
// whole, instead of showing its replay scroll by
|
|
241
|
-
|
|
269
|
+
// whole, instead of showing its replay scroll by, or plays the
|
|
270
|
+
// terminal pane's entrance style (entrance-animations.js).
|
|
271
|
+
if (state === 'idle') {
|
|
272
|
+
entry.el.classList.remove('tile--revealing');
|
|
273
|
+
if (entry.screenOwed) {
|
|
274
|
+
entry.screenOwed = false;
|
|
275
|
+
this.playTileScreenEntrance?.(entry.body);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
242
278
|
},
|
|
243
279
|
});
|
|
244
280
|
}
|
|
@@ -308,12 +344,14 @@ Object.assign(CodemanApp.prototype, {
|
|
|
308
344
|
const sessionId = tileAt(e.target);
|
|
309
345
|
const files = Array.from(e.dataTransfer?.files || []);
|
|
310
346
|
if (!sessionId || files.length === 0) return;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
347
|
+
// The one classifier every pick and drop filter reads (image-input.js):
|
|
348
|
+
// MIME first, then the name's extension, so documents and videos count too.
|
|
349
|
+
const media = files.filter((f) => this._promptAttachKind?.(f));
|
|
350
|
+
if (media.length === 0) {
|
|
351
|
+
this.showToast?.('Unsupported file type', 'error');
|
|
314
352
|
return;
|
|
315
353
|
}
|
|
316
|
-
this._uploadAndInsertImages?.(
|
|
354
|
+
this._uploadAndInsertImages?.(media, { sessionId });
|
|
317
355
|
});
|
|
318
356
|
},
|
|
319
357
|
|
|
@@ -389,6 +427,9 @@ Object.assign(CodemanApp.prototype, {
|
|
|
389
427
|
wanted.forEach((id, k) => this._mountTile(id, { enterIndex: k }));
|
|
390
428
|
// Packed from the first cell (_applyTileLayout pads the shape with empty cells).
|
|
391
429
|
grid.cells = wanted.filter((id) => grid.tiles.has(id));
|
|
430
|
+
// What the user opened is the grid they want (_openStoredTileGrid keeps a
|
|
431
|
+
// stored count instead).
|
|
432
|
+
grid.count = grid.ids.length;
|
|
392
433
|
this._applyTileLayout();
|
|
393
434
|
// The tiles' frames paint first; their terminals are built one per frame
|
|
394
435
|
// after it, the focused tile's first, so its capture is the one the queue
|
|
@@ -412,11 +453,13 @@ Object.assign(CodemanApp.prototype, {
|
|
|
412
453
|
* Leaves the grid: every tile destroyed (sockets closed, xterms disposed,
|
|
413
454
|
* queued loads dropped), the main terminal unparked.
|
|
414
455
|
*
|
|
415
|
-
* `keepStored`
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
456
|
+
* `keepStored` (every caller) keeps the grid as it was, holes, count, sizes,
|
|
457
|
+
* focus and zoom, closed, for the Tiles toggle to bring back (a reload
|
|
458
|
+
* restores only a grid stored open); false would forget it. `reselect` shows the focused session in the single
|
|
459
|
+
* view through a forced reload; pass false when the caller selects
|
|
460
|
+
* something itself. `animate` (the Tiles toggle only, owner answer 4)
|
|
461
|
+
* leaves a still copy of the tiles over the stage until the single view has
|
|
462
|
+
* its content (_ghostTileGrid).
|
|
420
463
|
*
|
|
421
464
|
* The main terminal's cached content for EVERY tiled id is invalidated: it was
|
|
422
465
|
* written before the grid opened, possibly hours ago, and selectSession paints
|
|
@@ -457,6 +500,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
457
500
|
grid.colFr = [];
|
|
458
501
|
grid.rowFr = [];
|
|
459
502
|
grid.cells = [];
|
|
503
|
+
grid.count = 0;
|
|
460
504
|
grid.cols = 0;
|
|
461
505
|
grid.rows = 0;
|
|
462
506
|
grid.focusedId = null;
|
|
@@ -536,14 +580,28 @@ Object.assign(CodemanApp.prototype, {
|
|
|
536
580
|
// Zoomed, only the zoomed tile is on screen (the others are display: none
|
|
537
581
|
// under .tile-grid--zoomed, a rule the ghost layer does not carry).
|
|
538
582
|
const zoomed = section.classList.contains('tile-grid--zoomed');
|
|
583
|
+
const copies = [];
|
|
539
584
|
for (const id of grid.ids) {
|
|
540
585
|
const el = grid.tiles.get(id)?.el;
|
|
541
586
|
if (!el || (zoomed && !el.classList.contains('tile--zoomed'))) continue;
|
|
542
587
|
const ghost = el.cloneNode(true);
|
|
543
|
-
ghost.classList.remove(
|
|
588
|
+
ghost.classList.remove(
|
|
589
|
+
'tile--entering',
|
|
590
|
+
'tile--enter-themed',
|
|
591
|
+
'tile--enter-hold',
|
|
592
|
+
'tile--needs',
|
|
593
|
+
'tile--loading',
|
|
594
|
+
'tile--drop-target',
|
|
595
|
+
'tile--dragging'
|
|
596
|
+
);
|
|
597
|
+
ghost.querySelector?.('.tile-body')?.classList.remove('term-enter');
|
|
544
598
|
ghost.classList.add('tile--leaving');
|
|
545
599
|
layer.appendChild(ghost);
|
|
600
|
+
copies.push({ ghost, el, sessionId: id });
|
|
546
601
|
}
|
|
602
|
+
// The entrance style's own way out (entrance-animations.js): back into the
|
|
603
|
+
// tabs, a CRT switch-off. Placed while the real tiles are still measurable.
|
|
604
|
+
const leaveMs = this._stageTileExit?.(copies, { now: !hold }) || 0;
|
|
547
605
|
main.appendChild(layer);
|
|
548
606
|
let fallback = null;
|
|
549
607
|
let holdCap = null;
|
|
@@ -557,10 +615,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
557
615
|
if (fallback !== null || !layer.isConnected) return;
|
|
558
616
|
clearTimeout(holdCap);
|
|
559
617
|
layer.classList.add('tile-grid-ghosts--release');
|
|
560
|
-
fallback = setTimeout(done, TILE_GHOST_FALLBACK_MS);
|
|
618
|
+
fallback = setTimeout(done, Math.max(TILE_GHOST_FALLBACK_MS, leaveMs));
|
|
561
619
|
};
|
|
562
620
|
layer.addEventListener('animationend', (e) => {
|
|
563
|
-
if (
|
|
621
|
+
if (e.pseudoElement || e.target !== layer.lastElementChild) return;
|
|
622
|
+
if (/^tile-leave/.test(e.animationName)) done();
|
|
564
623
|
});
|
|
565
624
|
if (hold) holdCap = setTimeout(release, TILE_GHOST_HOLD_MAX_MS);
|
|
566
625
|
else release();
|
|
@@ -695,8 +754,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
695
754
|
|
|
696
755
|
/**
|
|
697
756
|
* The card's text, from the remembered count, the grid's state and what the
|
|
698
|
-
* window fits.
|
|
699
|
-
*
|
|
757
|
+
* window fits. With the grid closed and a stored grid to bring back, the
|
|
758
|
+
* click opens that grid as it was (never trimmed to the window), so the
|
|
759
|
+
* window line only says what fits. Compared with the last English text set,
|
|
760
|
+
* never the DOM (in zh-CN the DOM holds the translation; see
|
|
761
|
+
* _renderTileOverlay).
|
|
700
762
|
*/
|
|
701
763
|
_renderTileHint() {
|
|
702
764
|
const hint = this._tileHint;
|
|
@@ -712,10 +774,12 @@ Object.assign(CodemanApp.prototype, {
|
|
|
712
774
|
set(hint.title, 'titleText', `Tiles \u00B7 ${count}`);
|
|
713
775
|
set(hint.click, 'clickText', open ? 'Click: close the grid' : 'Click: open the grid');
|
|
714
776
|
const plural = capacity === 1 ? '' : 's';
|
|
777
|
+
const restoring = open ? null : this._storedTileGridSet();
|
|
778
|
+
const opens = restoring ? restoring.ids.length : count;
|
|
715
779
|
const fitsText =
|
|
716
|
-
|
|
780
|
+
opens <= capacity
|
|
717
781
|
? ''
|
|
718
|
-
: open
|
|
782
|
+
: open || restoring
|
|
719
783
|
? `This window fits ${capacity} tile${plural}`
|
|
720
784
|
: `This window fits ${capacity} tile${plural}: a click opens ${capacity}`;
|
|
721
785
|
set(hint.fits.text, 'fitsText', fitsText);
|
|
@@ -976,22 +1040,25 @@ Object.assign(CodemanApp.prototype, {
|
|
|
976
1040
|
|
|
977
1041
|
/**
|
|
978
1042
|
* A count picked in the menu: remembered for the click, then the grid opens
|
|
979
|
-
* with that many tiles (
|
|
980
|
-
*
|
|
981
|
-
* sessions than the count give
|
|
1043
|
+
* with that many tiles (a stored grid re-formed to it, its tiles first in
|
|
1044
|
+
* their cells), or the open grid is re-formed to it (_reformTileGrid).
|
|
1045
|
+
* Never more than the window fits; fewer open sessions than the count give
|
|
1046
|
+
* fewer tiles. Either way the grid's count is what the pick left.
|
|
982
1047
|
*/
|
|
983
1048
|
_pickTileCount(count) {
|
|
984
1049
|
this.closeTileCountMenu({ refocus: false });
|
|
985
1050
|
this._rememberTileGridCount(count);
|
|
986
1051
|
const grid = this._tileGrid;
|
|
987
1052
|
if (!grid?.open) {
|
|
988
|
-
this.
|
|
1053
|
+
this._activateTileGrid({ count });
|
|
989
1054
|
return;
|
|
990
1055
|
}
|
|
991
1056
|
const T = window.CodemanTileGrid;
|
|
992
1057
|
const n = Math.min(T.sanitizeTileCount(count), this._tileGridLimit().capacity);
|
|
993
|
-
|
|
994
|
-
this._reformTileGrid(T.tileGridSetForCount(grid.ids,
|
|
1058
|
+
// The tiles that join are the ranking's best (_tileGridRanking).
|
|
1059
|
+
this._reformTileGrid(T.tileGridSetForCount(grid.ids, this._tileGridRanking(), n, grid.focusedId));
|
|
1060
|
+
grid.count = grid.ids.length;
|
|
1061
|
+
this._persistTileGrid();
|
|
995
1062
|
},
|
|
996
1063
|
|
|
997
1064
|
/**
|
|
@@ -1126,14 +1193,18 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1126
1193
|
/**
|
|
1127
1194
|
* The Tiles button's click and Ctrl+Shift+G, one function so the two never
|
|
1128
1195
|
* drift (owner decision 8): opens the grid at once, no menu in the way, or
|
|
1129
|
-
* closes it to the single view of the focused session.
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1196
|
+
* closes it to the single view of the focused session. The grid this
|
|
1197
|
+
* browser last had comes back EXACTLY as the user left it (owner request:
|
|
1198
|
+
* "when I turn tiles off and on, always keep what the last setting was"):
|
|
1199
|
+
* its tiles in their cells, holes included, its count, divider sizes, focus
|
|
1200
|
+
* and zoom; a cell whose session no longer exists is filled from the
|
|
1201
|
+
* ranking (_openStoredTileGrid). With none of its sessions left (or nothing
|
|
1202
|
+
* stored), `tileGridOpenSet` (constants.js) takes an open split's two
|
|
1203
|
+
* sessions, else the open sessions as the ranking orders them
|
|
1204
|
+
* (_tileGridRanking: working, then needing input, then the most recent),
|
|
1205
|
+
* the active one always among them and focused, filled to the remembered
|
|
1206
|
+
* count (the right-click menu's last pick, default 6, at most what the
|
|
1207
|
+
* window fits; owner decision 10).
|
|
1137
1208
|
*/
|
|
1138
1209
|
toggleTileGrid() {
|
|
1139
1210
|
this.closeTileCountMenu();
|
|
@@ -1141,28 +1212,43 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1141
1212
|
this.closeTileGrid({ keepStored: true, reselect: true, animate: true });
|
|
1142
1213
|
return;
|
|
1143
1214
|
}
|
|
1144
|
-
|
|
1215
|
+
this._activateTileGrid();
|
|
1216
|
+
},
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Opens the grid as the toggle does (toggleTileGrid), or, with `count` (a
|
|
1220
|
+
* pick in the count menu while the grid is closed), with that many tiles: a
|
|
1221
|
+
* stored grid re-formed to it, its tiles first in their cells, the ones the
|
|
1222
|
+
* count adds filling its empty cells first, the rest from the ranking.
|
|
1223
|
+
*
|
|
1224
|
+
* @returns {boolean} whether the grid opened
|
|
1225
|
+
*/
|
|
1226
|
+
_activateTileGrid({ count = null } = {}) {
|
|
1227
|
+
if (!this.canOpenTileGrid()) return false;
|
|
1145
1228
|
const stored = this._readStoredTileGrid();
|
|
1146
|
-
const set = this._tileGridOpenSet(stored);
|
|
1229
|
+
const set = this._tileGridOpenSet(stored, count);
|
|
1147
1230
|
if (!set) {
|
|
1148
1231
|
this.showToast?.('No sessions to show as tiles', 'info');
|
|
1149
|
-
return;
|
|
1232
|
+
return false;
|
|
1150
1233
|
}
|
|
1151
|
-
if (set.source === 'stored') this._openStoredTileGrid(stored, set
|
|
1152
|
-
|
|
1234
|
+
if (set.source === 'stored') return this._openStoredTileGrid(stored, set, { keepCount: count === null });
|
|
1235
|
+
return this.openTileGrid(set.ids, { focusedId: set.focusedId });
|
|
1153
1236
|
},
|
|
1154
1237
|
|
|
1155
1238
|
/**
|
|
1156
|
-
* What the toggle would open now (see toggleTileGrid):
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1239
|
+
* What the toggle would open now (see toggleTileGrid): a stored grid as it
|
|
1240
|
+
* was, or `count` tiles (the remembered count by default), at most what the
|
|
1241
|
+
* window fits. With an explicit `count`, a stored grid is trimmed or filled
|
|
1242
|
+
* to it too. `stored` saves a second read.
|
|
1159
1243
|
*/
|
|
1160
|
-
_tileGridOpenSet(stored = this._readStoredTileGrid(), count =
|
|
1244
|
+
_tileGridOpenSet(stored = this._readStoredTileGrid(), count = null) {
|
|
1161
1245
|
const T = window.CodemanTileGrid;
|
|
1162
|
-
const n = Math.max(1, Math.min(count, this._tileGridLimit().capacity));
|
|
1246
|
+
const n = Math.max(1, Math.min(count ?? this._tileGridCount(), this._tileGridLimit().capacity));
|
|
1247
|
+
const ranked = this._tileGridRanking();
|
|
1163
1248
|
const set = T.tileGridOpenSet({
|
|
1164
1249
|
stored,
|
|
1165
1250
|
split: this._splitPane ? [this.activeSessionId, this._splitSessionId] : null,
|
|
1251
|
+
ranked,
|
|
1166
1252
|
sessions: this.sessions,
|
|
1167
1253
|
sessionOrder: this.sessionOrder,
|
|
1168
1254
|
detachedIds: this.detachedSessions,
|
|
@@ -1170,8 +1256,42 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1170
1256
|
limit: n,
|
|
1171
1257
|
});
|
|
1172
1258
|
if (!set) return null;
|
|
1173
|
-
|
|
1174
|
-
|
|
1259
|
+
// A stored grid comes back as it was: the count does not apply to it
|
|
1260
|
+
// unless one was asked for.
|
|
1261
|
+
if (set.source === 'stored' && count === null) return set;
|
|
1262
|
+
return { ...set, ids: T.tileGridSetForCount(set.ids, ranked, n, set.focusedId) };
|
|
1263
|
+
},
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* The open sessions the grid takes when nobody said which (the Tiles button
|
|
1267
|
+
* with nothing stored, and every tile it fills on its own), best first:
|
|
1268
|
+
* working (the most recently started turn first), then the ones needing
|
|
1269
|
+
* input (the red and yellow tab alerts), then the rest by most recent
|
|
1270
|
+
* activity, tab order breaking ties (rankTileSessions, constants.js). The
|
|
1271
|
+
* states and stamps are the home screens' own (`_mobileOverviewState()`,
|
|
1272
|
+
* mobile-overview.js). Detached sessions are never in it. Guarded like
|
|
1273
|
+
* the sorted rail: without the classifier (a stale cached
|
|
1274
|
+
* mobile-overview.js) it is plain tab order.
|
|
1275
|
+
*
|
|
1276
|
+
* @returns {string[]}
|
|
1277
|
+
*/
|
|
1278
|
+
_tileGridRanking() {
|
|
1279
|
+
const T = window.CodemanTileGrid;
|
|
1280
|
+
const open = T.buildTilePickerSessions(this.sessions, this.sessionOrder, this.detachedSessions);
|
|
1281
|
+
if (typeof this._mobileOverviewState !== 'function' || typeof T.rankTileSessions !== 'function') {
|
|
1282
|
+
return open.map((c) => c.id);
|
|
1283
|
+
}
|
|
1284
|
+
const rows = open.map(({ id }, orderIndex) => {
|
|
1285
|
+
const session = this.sessions.get(id);
|
|
1286
|
+
return {
|
|
1287
|
+
id,
|
|
1288
|
+
state: this._mobileOverviewState(session, this.pendingHooks?.get(id)),
|
|
1289
|
+
lastActivityAt: Number(session.lastActivityAt) || 0,
|
|
1290
|
+
lastSubmitAt: Number(session.lastSubmitAt) || 0,
|
|
1291
|
+
orderIndex,
|
|
1292
|
+
};
|
|
1293
|
+
});
|
|
1294
|
+
return T.rankTileSessions(rows);
|
|
1175
1295
|
},
|
|
1176
1296
|
|
|
1177
1297
|
/** Alt+Shift+Arrows: a human selection of the tile in that direction. */
|
|
@@ -1242,6 +1362,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1242
1362
|
if (grid.ids.length >= window.CodemanTileGrid.TILE_GRID_MAX) return false;
|
|
1243
1363
|
if (!this._mountTile(sessionId, { enterIndex })) return false;
|
|
1244
1364
|
this._placeTile(sessionId, cell);
|
|
1365
|
+
grid.count = grid.ids.length;
|
|
1245
1366
|
// A tile added while one is zoomed by hand is meant to be seen.
|
|
1246
1367
|
if (grid.zoomedId && !grid.autoZoom) grid.zoomedId = null;
|
|
1247
1368
|
this._applyTileLayout();
|
|
@@ -1286,17 +1407,20 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1286
1407
|
* moves focus to the neighbouring tile (next in grid order, else previous),
|
|
1287
1408
|
* as the app's choice (`auto`: no idle alert is spent); `focus: false` keeps
|
|
1288
1409
|
* DOM focus where it is (an app-driven removal: a socket the server closed),
|
|
1289
|
-
* so keystrokes never land in the neighbour's PTY unasked.
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
* the
|
|
1410
|
+
* so keystrokes never land in the neighbour's PTY unasked. `gone`: the
|
|
1411
|
+
* session went away by itself (deleted, popped out, its socket refused), not
|
|
1412
|
+
* by the user's hand, so the grid's count stays and the next time it opens
|
|
1413
|
+
* the ranking fills that place. The last tile leaving closes the grid, kept
|
|
1414
|
+
* as it was: with `refocus` the single view then shows that session (or,
|
|
1415
|
+
* popped out, the next one: _selectAfterTileGrid), without it the caller
|
|
1416
|
+
* decides what comes next.
|
|
1293
1417
|
*/
|
|
1294
|
-
removeTile(sessionId, { refocus = true, focus = true } = {}) {
|
|
1418
|
+
removeTile(sessionId, { refocus = true, focus = true, gone = false } = {}) {
|
|
1295
1419
|
const grid = this._tileGrid;
|
|
1296
1420
|
const entry = grid?.open ? grid.tiles.get(sessionId) : null;
|
|
1297
1421
|
if (!entry) return false;
|
|
1298
1422
|
if (grid.ids.length === 1) {
|
|
1299
|
-
this.closeTileGrid({ keepStored:
|
|
1423
|
+
this.closeTileGrid({ keepStored: true, reselect: refocus });
|
|
1300
1424
|
return true;
|
|
1301
1425
|
}
|
|
1302
1426
|
const wasFocused = grid.focusedId === sessionId;
|
|
@@ -1312,6 +1436,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1312
1436
|
entry.el.remove();
|
|
1313
1437
|
grid.tiles.delete(sessionId);
|
|
1314
1438
|
grid.cells[grid.cells.indexOf(sessionId)] = null;
|
|
1439
|
+
if (!gone) grid.count = grid.ids.length;
|
|
1315
1440
|
// Before the layout, which may zoom the focused tile on a small window.
|
|
1316
1441
|
if (wasFocused) grid.focusedId = null;
|
|
1317
1442
|
this._applyTileLayout();
|
|
@@ -1323,11 +1448,10 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1323
1448
|
|
|
1324
1449
|
/**
|
|
1325
1450
|
* Builds one tile (header, body, TerminalTile); where it goes is the
|
|
1326
|
-
* caller's (grid.cells). It enters
|
|
1327
|
-
* (
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
* Opacity and transform only, never anything its fit reads.
|
|
1451
|
+
* caller's (grid.cells). It enters as the `enterIndex`-th of a staggered
|
|
1452
|
+
* group (_beginTileEntrance), and its terminal stays transparent until its
|
|
1453
|
+
* first capture lands (.tile--revealing, cleared by the load queue, with a
|
|
1454
|
+
* backstop timer). Opacity and transform only, never anything its fit reads.
|
|
1331
1455
|
*/
|
|
1332
1456
|
_mountTile(sessionId, { enterIndex = 0 } = {}) {
|
|
1333
1457
|
const grid = this._tileGrid;
|
|
@@ -1336,16 +1460,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1336
1460
|
const el = document.createElement('div');
|
|
1337
1461
|
el.className = 'tile tile--revealing';
|
|
1338
1462
|
setTimeout(() => el.classList.remove('tile--revealing'), TILE_REVEAL_FALLBACK_MS);
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
el.removeEventListener('animationend', onEnd);
|
|
1345
|
-
el.classList.remove('tile--entering');
|
|
1346
|
-
};
|
|
1347
|
-
el.addEventListener('animationend', onEnd);
|
|
1348
|
-
}
|
|
1463
|
+
// Its screen plays the terminal pane's entrance once its first capture
|
|
1464
|
+
// lands, when its frame entered in a tile style (Tile Animations): never
|
|
1465
|
+
// with the default `settle`, nor on a reload's restore, which settles.
|
|
1466
|
+
const entered = this._beginTileEntrance(el, sessionId, enterIndex);
|
|
1467
|
+
const screenOwed = !!entered && entered !== 'settle';
|
|
1349
1468
|
el.dataset.sessionId = sessionId;
|
|
1350
1469
|
// Header and body are siblings: the chrome is refreshed in place
|
|
1351
1470
|
// (_renderTileHeader), never by rewriting the tile, which would take the
|
|
@@ -1391,12 +1510,61 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1391
1510
|
renaming: false,
|
|
1392
1511
|
// The pid this tile last saw, so a pane that starts later is noticed.
|
|
1393
1512
|
pid: this.sessions.get(sessionId)?.pid ?? null,
|
|
1513
|
+
// Its screen plays the terminal entrance once its first capture lands.
|
|
1514
|
+
screenOwed,
|
|
1394
1515
|
});
|
|
1395
1516
|
// Where it goes is the caller's (grid.cells).
|
|
1396
1517
|
this._renderTileHeader(sessionId);
|
|
1397
1518
|
return true;
|
|
1398
1519
|
},
|
|
1399
1520
|
|
|
1521
|
+
/**
|
|
1522
|
+
* A tile's frame enters as the `enterIndex`-th of a staggered group, in the
|
|
1523
|
+
* entrance style (entrance-animations.js, App Settings → Entrance
|
|
1524
|
+
* Animations). `settle` is the grid's own fade and settle (styles.css
|
|
1525
|
+
* .tile--entering), and what a reload restores with whatever the theme:
|
|
1526
|
+
* nothing animates on page load. Any other style is timed by the entrance
|
|
1527
|
+
* module a frame later, once the layout is final (_stageTileEntrance).
|
|
1528
|
+
* Opacity and transform only, never anything the fit reads.
|
|
1529
|
+
*
|
|
1530
|
+
* @returns {string|null} the style it enters with, or null when it does not animate
|
|
1531
|
+
*/
|
|
1532
|
+
_beginTileEntrance(el, sessionId, enterIndex = 0) {
|
|
1533
|
+
const style = this._tileEnterQuiet ? 'settle' : this.tileEntranceStyle?.() || 'settle';
|
|
1534
|
+
if (!this._tileMotionAllowed() || style === 'off') return null;
|
|
1535
|
+
el.classList.add('tile--entering');
|
|
1536
|
+
el.style.setProperty('--tile-enter-index', String(enterIndex));
|
|
1537
|
+
let backstop = null;
|
|
1538
|
+
const finish = () => {
|
|
1539
|
+
clearTimeout(backstop);
|
|
1540
|
+
el.removeEventListener('animationend', onEnd);
|
|
1541
|
+
el.classList.remove('tile--entering', 'tile--enter-themed', 'tile--enter-hold');
|
|
1542
|
+
if (el._tileEnterFinish === finish) el._tileEnterFinish = null;
|
|
1543
|
+
};
|
|
1544
|
+
// The tile's own entrance (`tile-enter`, or a themed `tile-enter-*`),
|
|
1545
|
+
// never a child's animation or a wash on its ::before.
|
|
1546
|
+
const onEnd = (e) => {
|
|
1547
|
+
if (e.target !== el || e.pseudoElement || !/^tile-enter/.test(e.animationName || '')) return;
|
|
1548
|
+
finish();
|
|
1549
|
+
};
|
|
1550
|
+
el.addEventListener('animationend', onEnd);
|
|
1551
|
+
el._tileEnterFinish = finish;
|
|
1552
|
+
if (style !== 'settle') {
|
|
1553
|
+
this._stageTileEntrance?.(el, sessionId, (ms) => {
|
|
1554
|
+
clearTimeout(backstop);
|
|
1555
|
+
backstop = setTimeout(finish, ms);
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return style;
|
|
1559
|
+
},
|
|
1560
|
+
|
|
1561
|
+
/** Plays a mounted tile's entrance again (the entrance lab's replay): nothing is remounted or refitted. */
|
|
1562
|
+
_replayTileEntrance(el, sessionId, enterIndex = 0) {
|
|
1563
|
+
el._tileEnterFinish?.();
|
|
1564
|
+
void el.offsetWidth;
|
|
1565
|
+
return this._beginTileEntrance(el, sessionId, enterIndex);
|
|
1566
|
+
},
|
|
1567
|
+
|
|
1400
1568
|
/**
|
|
1401
1569
|
* Makes `el` a drop target for a session tab dragged from the strip (the
|
|
1402
1570
|
* strip's own drag sets `draggedTabId`) and for a tile dragged by its header
|
|
@@ -1406,8 +1574,10 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1406
1574
|
* `accepts(id)` is the target's own rule (a tile takes any session but its
|
|
1407
1575
|
* own; an empty cell takes any). A session it does not accept
|
|
1408
1576
|
* is held there too, but refused (`dropEffect: 'none'`, no highlight, so no
|
|
1409
|
-
* drop follows).
|
|
1410
|
-
*
|
|
1577
|
+
* drop follows). With Detach Tiles on, a tile dragged from ANOTHER Codeman
|
|
1578
|
+
* window (no drag of this page's own, the tile type in the drag) is taken
|
|
1579
|
+
* too and docks here (_dockForeignTile). Any other drag (a file) is left to
|
|
1580
|
+
* the grid section's own guard (_installTileFileDrop).
|
|
1411
1581
|
*/
|
|
1412
1582
|
_acceptTabDrops(el, onDrop, { accepts = () => true } = {}) {
|
|
1413
1583
|
const dragged = () => (this._tileGrid?.open ? this.draggedTabId || this._draggedTileId || null : null);
|
|
@@ -1415,7 +1585,16 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1415
1585
|
'dragover',
|
|
1416
1586
|
(e) => {
|
|
1417
1587
|
const id = dragged();
|
|
1418
|
-
if (!id)
|
|
1588
|
+
if (!id) {
|
|
1589
|
+
// A tile from another Codeman window (Detach Tiles). Its id can
|
|
1590
|
+
// only be read on drop, so it is held here whatever it is.
|
|
1591
|
+
if (!this._isForeignTileDrag(e)) return;
|
|
1592
|
+
e.preventDefault?.();
|
|
1593
|
+
e.stopPropagation?.();
|
|
1594
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
1595
|
+
el.classList.add('tile--drop-target');
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1419
1598
|
e.preventDefault?.();
|
|
1420
1599
|
e.stopPropagation?.();
|
|
1421
1600
|
const ok = accepts(id);
|
|
@@ -1432,7 +1611,13 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1432
1611
|
(e) => {
|
|
1433
1612
|
el.classList.remove('tile--drop-target');
|
|
1434
1613
|
const id = dragged();
|
|
1435
|
-
if (!id)
|
|
1614
|
+
if (!id) {
|
|
1615
|
+
if (!this._isForeignTileDrag(e)) return;
|
|
1616
|
+
e.preventDefault?.();
|
|
1617
|
+
e.stopPropagation?.();
|
|
1618
|
+
this._dockForeignTile(e.dataTransfer?.getData?.(TILE_DRAG_TYPE), onDrop);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1436
1621
|
e.preventDefault?.();
|
|
1437
1622
|
e.stopPropagation?.();
|
|
1438
1623
|
if (accepts(id)) onDrop(id);
|
|
@@ -1575,10 +1760,12 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1575
1760
|
* Ctrl/Cmd+click on a tab: that session joins the grid and takes focus (a
|
|
1576
1761
|
* human selection: the user clicked its tab). With the grid closed it opens
|
|
1577
1762
|
* what the Tiles toggle would, with this session among the tiles and
|
|
1578
|
-
* focused
|
|
1579
|
-
* questions
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1763
|
+
* focused, never past the remembered count (owner answer to decision 10's
|
|
1764
|
+
* questions: N, not N+1): it joins while the grid holds fewer than the
|
|
1765
|
+
* count (a stored grid's first empty cell), else it takes the last tile's
|
|
1766
|
+
* place. A stored grid keeps its cells, sizes and holes around it.
|
|
1767
|
+
* Returns false when the grid cannot open here (narrow or solo window), so
|
|
1768
|
+
* the click is an ordinary one.
|
|
1582
1769
|
*/
|
|
1583
1770
|
addSessionToTiles(sessionId) {
|
|
1584
1771
|
if (!this.canOpenTileGrid() || !this.sessions.has(sessionId) || this.detachedSessions?.has(sessionId)) {
|
|
@@ -1599,7 +1786,26 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1599
1786
|
return true;
|
|
1600
1787
|
}
|
|
1601
1788
|
const n = Math.max(1, Math.min(this._tileGridCount(), capacity));
|
|
1602
|
-
const
|
|
1789
|
+
const stored = this._readStoredTileGrid();
|
|
1790
|
+
const plan = this._tileGridOpenSet(stored);
|
|
1791
|
+
if (plan?.source === 'stored') {
|
|
1792
|
+
let { ids, cells } = plan;
|
|
1793
|
+
if (!ids.includes(sessionId)) {
|
|
1794
|
+
if (ids.length < n) {
|
|
1795
|
+
ids = [...ids, sessionId];
|
|
1796
|
+
} else {
|
|
1797
|
+
const last = ids.at(-1);
|
|
1798
|
+
ids = ids.map((id) => (id === last ? sessionId : id));
|
|
1799
|
+
cells = cells.map((id) => (id === last ? sessionId : id));
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return this._openStoredTileGrid(
|
|
1803
|
+
stored,
|
|
1804
|
+
{ ids, cells, focusedId: sessionId },
|
|
1805
|
+
{ focusedId: sessionId, auto: false, keepCount: false }
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
const base = plan?.ids || [];
|
|
1603
1809
|
const ids = [...base.filter((id) => id !== sessionId).slice(0, n - 1), sessionId];
|
|
1604
1810
|
// Exactly these: an open split is already in `base` (tileGridOpenSet seeds
|
|
1605
1811
|
// it), and merging it again went past the count (N+1) and the window.
|
|
@@ -1642,7 +1848,8 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1642
1848
|
_replaceTileGrid(ids) {
|
|
1643
1849
|
const focus = ids.includes(this.activeSessionId) ? this.activeSessionId : ids[0];
|
|
1644
1850
|
if (this._tilesOwnTerminal()) {
|
|
1645
|
-
|
|
1851
|
+
// Kept until the group's grid, opened next, takes its place.
|
|
1852
|
+
this.closeTileGrid({ keepStored: true, reselect: false });
|
|
1646
1853
|
this.activeSessionId = null;
|
|
1647
1854
|
}
|
|
1648
1855
|
// Exactly the group: an open split closes without joining it (merged, its
|
|
@@ -1912,7 +2119,10 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1912
2119
|
* input starts no drag. dragstart's target is the header whatever was
|
|
1913
2120
|
* pressed, so where the press landed is noted in the capture phase, ahead of
|
|
1914
2121
|
* the buttons' own stopPropagation. `draggable` is off while moving is
|
|
1915
|
-
* (_paintTileHandle: a zoomed grid, a single tile, a rename in progress)
|
|
2122
|
+
* (_paintTileHandle: a zoomed grid, a single tile, a rename in progress),
|
|
2123
|
+
* unless Detach Tiles is on: then a drop outside the window pops the tile
|
|
2124
|
+
* out, and a drop on another Codeman window's tiles moves it there (see the
|
|
2125
|
+
* Detach Tiles section); a rename still turns it off.
|
|
1916
2126
|
*/
|
|
1917
2127
|
_installTileMoveDrag(sessionId, header, actions) {
|
|
1918
2128
|
let pressedOnControl = false;
|
|
@@ -1926,7 +2136,10 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1926
2136
|
);
|
|
1927
2137
|
header.addEventListener('dragstart', (e) => {
|
|
1928
2138
|
const entry = this._tileGrid?.open ? this._tileGrid.tiles.get(sessionId) : null;
|
|
1929
|
-
|
|
2139
|
+
// Detach Tiles lets a tile that cannot move (alone, or zoomed) still be
|
|
2140
|
+
// dragged, out of the window; no drop target here takes it then.
|
|
2141
|
+
const detachable = this._tileDetachAllowed();
|
|
2142
|
+
if (!entry || pressedOnControl || entry.renaming || !(this._tilesMovable() || detachable)) {
|
|
1930
2143
|
e.preventDefault?.();
|
|
1931
2144
|
return;
|
|
1932
2145
|
}
|
|
@@ -1934,11 +2147,16 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1934
2147
|
entry.el.classList.add('tile--dragging');
|
|
1935
2148
|
if (e.dataTransfer) {
|
|
1936
2149
|
e.dataTransfer.effectAllowed = 'move';
|
|
1937
|
-
// Firefox starts no drag without data.
|
|
1938
|
-
|
|
2150
|
+
// Firefox starts no drag without data. Another Codeman window reads
|
|
2151
|
+
// the id from it (Detach Tiles).
|
|
2152
|
+
e.dataTransfer.setData(TILE_DRAG_TYPE, sessionId);
|
|
1939
2153
|
}
|
|
2154
|
+
if (detachable) this._beginTileTearOff(sessionId, entry, e);
|
|
2155
|
+
});
|
|
2156
|
+
header.addEventListener('dragend', (e) => {
|
|
2157
|
+
this._settleTileTearOff(sessionId, e);
|
|
2158
|
+
this._endTileMoveDrag();
|
|
1940
2159
|
});
|
|
1941
|
-
header.addEventListener('dragend', () => this._endTileMoveDrag());
|
|
1942
2160
|
},
|
|
1943
2161
|
|
|
1944
2162
|
/**
|
|
@@ -1949,6 +2167,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1949
2167
|
*/
|
|
1950
2168
|
_endTileMoveDrag() {
|
|
1951
2169
|
this._draggedTileId = null;
|
|
2170
|
+
this._stopTileTearOffTracking();
|
|
1952
2171
|
const grid = this._tileGrid;
|
|
1953
2172
|
for (const { el } of grid?.tiles.values() || []) el.classList.remove('tile--dragging', 'tile--drop-target');
|
|
1954
2173
|
for (const slot of grid?.slots || []) slot.classList.remove('tile--drop-target');
|
|
@@ -1956,19 +2175,25 @@ Object.assign(CodemanApp.prototype, {
|
|
|
1956
2175
|
|
|
1957
2176
|
/**
|
|
1958
2177
|
* The header as a handle: `draggable` while the tile can move (not while a
|
|
1959
|
-
* tile is zoomed, alone, or being renamed),
|
|
1960
|
-
*
|
|
1961
|
-
* the
|
|
2178
|
+
* tile is zoomed, alone, or being renamed) or, with Detach Tiles on, be
|
|
2179
|
+
* dragged out of the window (then also alone or zoomed), and its tooltip,
|
|
2180
|
+
* the state and how long ("working 3m") plus what a drag does. Diffs on the
|
|
2181
|
+
* last English text, never the DOM (translated in zh-CN; see
|
|
1962
2182
|
* _renderTileOverlay).
|
|
1963
2183
|
*/
|
|
1964
2184
|
_paintTileHandle(entry) {
|
|
1965
2185
|
const movable = this._tilesMovable();
|
|
1966
|
-
const
|
|
2186
|
+
const detachable = this._tileDetachAllowed();
|
|
2187
|
+
let hint = '';
|
|
2188
|
+
if (movable && detachable) hint = 'Drag to move the tile, or out of the window to open it on its own';
|
|
2189
|
+
else if (movable) hint = 'Drag to move the tile';
|
|
2190
|
+
else if (detachable) hint = 'Drag out of the window to open the tile on its own';
|
|
2191
|
+
const title = [entry.stateLabel, hint].filter(Boolean).join('\n');
|
|
1967
2192
|
if (entry.headerLabel !== title) {
|
|
1968
2193
|
entry.headerLabel = title;
|
|
1969
2194
|
entry.header.title = title;
|
|
1970
2195
|
}
|
|
1971
|
-
const draggable = movable && !entry.renaming ? 'true' : 'false';
|
|
2196
|
+
const draggable = (movable || detachable) && !entry.renaming ? 'true' : 'false';
|
|
1972
2197
|
if (entry.header.getAttribute('draggable') !== draggable) entry.header.setAttribute('draggable', draggable);
|
|
1973
2198
|
},
|
|
1974
2199
|
|
|
@@ -2127,7 +2352,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2127
2352
|
this._renderTileOverlay(sessionId);
|
|
2128
2353
|
return;
|
|
2129
2354
|
}
|
|
2130
|
-
this.removeTile(sessionId, { focus: false });
|
|
2355
|
+
this.removeTile(sessionId, { focus: false, gone: true });
|
|
2131
2356
|
},
|
|
2132
2357
|
|
|
2133
2358
|
/**
|
|
@@ -2481,17 +2706,20 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2481
2706
|
this._persistTileGrid();
|
|
2482
2707
|
},
|
|
2483
2708
|
|
|
2484
|
-
// ── Persistence (codeman:tile-grid, per
|
|
2709
|
+
// ── Persistence (codeman:tile-grid, per browser: ids and layout, never content) ──
|
|
2485
2710
|
|
|
2486
2711
|
/**
|
|
2487
|
-
* Writes the open grid
|
|
2488
|
-
*
|
|
2489
|
-
*
|
|
2490
|
-
* window
|
|
2712
|
+
* Writes the open grid as it is, on every change (a move, a divider drag, a
|
|
2713
|
+
* tile added or removed, a count picked, a focus, a zoom): its cells, its
|
|
2714
|
+
* count, focus, a zoom the user chose (an automatic one is worked out again
|
|
2715
|
+
* from the window) and the divider fractions. Never content, never to the
|
|
2716
|
+
* server. `open: false` is the closed-but-remembered state. Never in a solo
|
|
2717
|
+
* window, nor while a stored grid is being put back (_openStoredTileGrid
|
|
2718
|
+
* writes once it is); a storage failure only costs the convenience.
|
|
2491
2719
|
*/
|
|
2492
2720
|
_persistTileGrid({ open = true } = {}) {
|
|
2493
2721
|
const grid = this._tileGrid;
|
|
2494
|
-
if (this.isSoloWindow || !grid || grid.ids.length === 0) return;
|
|
2722
|
+
if (this.isSoloWindow || this._tilePersistHold || !grid || grid.ids.length === 0) return;
|
|
2495
2723
|
if (open && !grid.open) return;
|
|
2496
2724
|
const state = {
|
|
2497
2725
|
v: 1,
|
|
@@ -2500,6 +2728,9 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2500
2728
|
// (sanitizeTileGridState; a build before cells drops the nulls and reads
|
|
2501
2729
|
// the tiles packed, as it always did).
|
|
2502
2730
|
ids: grid.cells.slice(),
|
|
2731
|
+
// Never fewer than the tiles shown (a build before `count` derives it
|
|
2732
|
+
// from `ids`, and ignores it).
|
|
2733
|
+
count: Math.min(Math.max(grid.count || 0, grid.ids.length), window.CodemanTileGrid.TILE_GRID_MAX),
|
|
2503
2734
|
focused: grid.focusedId,
|
|
2504
2735
|
zoomed: grid.autoZoom ? null : grid.zoomedId,
|
|
2505
2736
|
colFr: grid.colFr.slice(),
|
|
@@ -2534,47 +2765,74 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2534
2765
|
},
|
|
2535
2766
|
|
|
2536
2767
|
/**
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
* or filled to the remembered count, a shape change the cell model's rule
|
|
2542
|
-
* handles (reformTileCells).
|
|
2768
|
+
* The stored grid as it would come back now (restoreTileGridCells): its
|
|
2769
|
+
* cells as stored, a cell whose session no longer exists filled from the
|
|
2770
|
+
* ranking while the grid holds fewer than its count. Null when nothing is
|
|
2771
|
+
* stored or none of its sessions survive.
|
|
2543
2772
|
*/
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2773
|
+
_storedTileGridSet(stored = this._readStoredTileGrid()) {
|
|
2774
|
+
if (!stored) return null;
|
|
2775
|
+
return window.CodemanTileGrid.restoreTileGridCells(stored, this._tileGridRanking());
|
|
2776
|
+
},
|
|
2777
|
+
|
|
2778
|
+
/**
|
|
2779
|
+
* Opens a stored grid as the user left it: `set` (the stored cells with any
|
|
2780
|
+
* freed cell filled, _storedTileGridSet, by default; the Tiles button may
|
|
2781
|
+
* pass it trimmed or filled to a picked count, or with a Ctrl/Cmd+clicked
|
|
2782
|
+
* session in it) goes back into its cells, holes included, a shape change
|
|
2783
|
+
* following the cell model's rule (reformTileCells: the tiles keep their row
|
|
2784
|
+
* and column when all fit, the ones that join fill the empty cells in
|
|
2785
|
+
* reading order); then the fractions it had (only if they still match the
|
|
2786
|
+
* layout) and a zoom the user chose on the focused tile. A grid larger than
|
|
2787
|
+
* the window fits is never trimmed: the focused tile shows alone until the
|
|
2788
|
+
* window fits it (_applyTileLayout), and the arrangement stays. `auto`: the
|
|
2789
|
+
* app is putting it back, so no idle alert is spent. `keepCount`: the
|
|
2790
|
+
* stored count stays the grid's (a session that went away may be filled the
|
|
2791
|
+
* next time); false makes it what opens (a pick, a Ctrl/Cmd+click). Nothing
|
|
2792
|
+
* is written until the grid is back, so a stored grid is never overwritten
|
|
2793
|
+
* by a half-built one.
|
|
2794
|
+
*/
|
|
2795
|
+
_openStoredTileGrid(
|
|
2796
|
+
stored,
|
|
2797
|
+
set = this._storedTileGridSet(stored),
|
|
2798
|
+
{ focusedId = null, auto = true, keepCount = true } = {}
|
|
2799
|
+
) {
|
|
2800
|
+
if (!set?.ids?.length) return false;
|
|
2550
2801
|
const T = window.CodemanTileGrid;
|
|
2551
|
-
|
|
2552
|
-
//
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2802
|
+
const focus = focusedId || set.focusedId;
|
|
2803
|
+
// An open split closes without joining it (decision 8, case a).
|
|
2804
|
+
this._tilePersistHold = true;
|
|
2805
|
+
let opened = false;
|
|
2806
|
+
try {
|
|
2807
|
+
opened = this.openTileGrid(set.ids, { focusedId: focus, auto, mergeSplit: false });
|
|
2808
|
+
} finally {
|
|
2809
|
+
this._tilePersistHold = false;
|
|
2810
|
+
}
|
|
2811
|
+
if (!opened) return false;
|
|
2812
|
+
const grid = this._tileGrid;
|
|
2813
|
+
// openTileGrid packed the tiles from the first cell. They go back to their
|
|
2814
|
+
// cells (a session gone since leaves its cell empty, unless the ranking
|
|
2815
|
+
// filled it).
|
|
2816
|
+
const base = (set.cells || []).map((id) => (id && grid.tiles.has(id) ? id : null));
|
|
2817
|
+
const kept = base.filter(Boolean);
|
|
2818
|
+
const cells = T.reformTileCells(
|
|
2819
|
+
base,
|
|
2820
|
+
T.tileCellCols(base.length),
|
|
2821
|
+
kept,
|
|
2822
|
+
grid.ids.filter((id) => !kept.includes(id)),
|
|
2823
|
+
grid.cols,
|
|
2824
|
+
grid.rows
|
|
2825
|
+
);
|
|
2569
2826
|
if (cells.length === grid.cols * grid.rows && cells.filter(Boolean).length === grid.tiles.size) {
|
|
2570
2827
|
grid.cells = cells;
|
|
2571
2828
|
}
|
|
2829
|
+
grid.count = keepCount ? Math.min(Math.max(grid.ids.length, stored.count || 0), T.TILE_GRID_MAX) : grid.ids.length;
|
|
2572
2830
|
// openTileGrid laid the grid out with equal tracks. The stored ones go back
|
|
2573
2831
|
// on; _applyTileLayout drops them again if they do not match the column or
|
|
2574
2832
|
// row count (the window may have changed the layout since).
|
|
2575
2833
|
if (stored.colFr) grid.colFr = stored.colFr.slice();
|
|
2576
2834
|
if (stored.rowFr) grid.rowFr = stored.rowFr.slice();
|
|
2577
|
-
if (stored.zoomed && grid.tiles.has(stored.zoomed)) {
|
|
2835
|
+
if (stored.zoomed && stored.zoomed === grid.focusedId && grid.tiles.has(stored.zoomed)) {
|
|
2578
2836
|
grid.zoomedId = stored.zoomed;
|
|
2579
2837
|
grid.autoZoom = false;
|
|
2580
2838
|
}
|
|
@@ -2598,17 +2856,27 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2598
2856
|
if (this._tilesOwnTerminal()) return false;
|
|
2599
2857
|
const stored = this._readStoredTileGrid();
|
|
2600
2858
|
if (!stored?.open || stored.ids.length === 0) return false;
|
|
2601
|
-
|
|
2859
|
+
// Put back, not opened: the tiles settle in whatever the entrance theme.
|
|
2860
|
+
this._tileEnterQuiet = true;
|
|
2861
|
+
try {
|
|
2862
|
+
return this._openStoredTileGrid(stored);
|
|
2863
|
+
} finally {
|
|
2864
|
+
this._tileEnterQuiet = false;
|
|
2865
|
+
}
|
|
2602
2866
|
},
|
|
2603
2867
|
|
|
2604
|
-
/**
|
|
2868
|
+
/**
|
|
2869
|
+
* A followed `#session=` link took the screen on load: the stored grid stays
|
|
2870
|
+
* remembered, closed. Only `open` changes: the value is written back as it
|
|
2871
|
+
* was stored, so a session gone since still frees its cell for the ranking
|
|
2872
|
+
* the next time the grid opens.
|
|
2873
|
+
*/
|
|
2605
2874
|
_closeStoredTileGrid() {
|
|
2606
2875
|
const stored = this._readStoredTileGrid();
|
|
2607
2876
|
if (!stored?.open) return;
|
|
2608
|
-
// Stored as it was read, holes included (`ids` carries the cells).
|
|
2609
|
-
const { cells, ...rest } = stored;
|
|
2610
2877
|
try {
|
|
2611
|
-
localStorage.
|
|
2878
|
+
const raw = JSON.parse(localStorage.getItem(TILE_GRID_STORAGE_KEY));
|
|
2879
|
+
localStorage.setItem(TILE_GRID_STORAGE_KEY, JSON.stringify({ ...raw, open: false }));
|
|
2612
2880
|
} catch {
|
|
2613
2881
|
/* Per-device convenience only. */
|
|
2614
2882
|
}
|
|
@@ -2637,7 +2905,7 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2637
2905
|
const grid = this._tileGrid;
|
|
2638
2906
|
if (!grid?.open) return false;
|
|
2639
2907
|
for (const id of grid.ids.slice()) {
|
|
2640
|
-
if (!this.sessions.has(id) || this.detachedSessions?.has(id)) this.removeTile(id, { refocus: false });
|
|
2908
|
+
if (!this.sessions.has(id) || this.detachedSessions?.has(id)) this.removeTile(id, { refocus: false, gone: true });
|
|
2641
2909
|
}
|
|
2642
2910
|
if (!grid.open) return false;
|
|
2643
2911
|
// Only a focus that is gone moves: re-selecting the same tile would hide an
|
|
@@ -2648,6 +2916,370 @@ Object.assign(CodemanApp.prototype, {
|
|
|
2648
2916
|
for (const { tile } of grid.tiles.values()) tile.reconnectNow();
|
|
2649
2917
|
return true;
|
|
2650
2918
|
},
|
|
2919
|
+
|
|
2920
|
+
// ── Detach Tiles (per-device `tileDetachEnabled`, default OFF) ───────────
|
|
2921
|
+
//
|
|
2922
|
+
// A tile leaves the grid for a window of its own, and comes back into any
|
|
2923
|
+
// Codeman window's grid, by dragging:
|
|
2924
|
+
//
|
|
2925
|
+
// - its header dropped OUTSIDE every window opens it as a pop-out window
|
|
2926
|
+
// (detachSession, the tab's own "Open in a new window"), at the drop point
|
|
2927
|
+
// and the tile's size;
|
|
2928
|
+
// - its header dropped on ANOTHER Codeman window's tile or empty cell moves
|
|
2929
|
+
// it there: that window docks it and says so on the window channel
|
|
2930
|
+
// ('tile-adopted'), and this one lets the tile go;
|
|
2931
|
+
// - a pop-out window's title dropped on a window's tile or empty cell docks
|
|
2932
|
+
// the session there, and the pop-out closes.
|
|
2933
|
+
//
|
|
2934
|
+
// Nothing new on the server: a pop-out is another client of the same session
|
|
2935
|
+
// (see detachSession), and a tile is a TerminalTile in whichever window has it.
|
|
2936
|
+
// A session stays in ONE place in this browser, as with every pop-out.
|
|
2937
|
+
|
|
2938
|
+
/** Whether Detach Tiles is on for this device. */
|
|
2939
|
+
tileDetachEnabled() {
|
|
2940
|
+
return this.loadAppSettingsFromStorage?.()?.tileDetachEnabled === true;
|
|
2941
|
+
},
|
|
2942
|
+
|
|
2943
|
+
/** Detach Tiles applies here: on, and a dashboard (a pop-out has no grid to drag from). */
|
|
2944
|
+
_tileDetachAllowed() {
|
|
2945
|
+
return !this.isSoloWindow && this.tileDetachEnabled();
|
|
2946
|
+
},
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* A drag over this page that is a tile from another Codeman window: none of
|
|
2950
|
+
* this page's own drags is in flight, Detach Tiles is on and the drag
|
|
2951
|
+
* carries the tile type (its id is only readable on drop).
|
|
2952
|
+
*/
|
|
2953
|
+
_isForeignTileDrag(e) {
|
|
2954
|
+
if (this.draggedTabId || this._draggedTileId || !this._tileGrid?.open || !this._tileDetachAllowed()) return false;
|
|
2955
|
+
const types = e?.dataTransfer?.types;
|
|
2956
|
+
return !!types && Array.from(types).includes(TILE_DRAG_TYPE);
|
|
2957
|
+
},
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* A tile from another window dropped on one of this grid's targets: it docks
|
|
2961
|
+
* through the target's own drop (`place`: a tile it replaces or trades
|
|
2962
|
+
* places with, an empty cell it fills), and takes focus there (a human
|
|
2963
|
+
* selection: the user just put it there). A session that was popped out comes
|
|
2964
|
+
* back first (its window is asked to close). Once it is a tile here, every
|
|
2965
|
+
* other window hears 'tile-adopted', and the one it was dragged from lets
|
|
2966
|
+
* its tile go (_onTileAdoptedElsewhere). An id this page does not know (a
|
|
2967
|
+
* session closed meanwhile, another user's) docks nowhere.
|
|
2968
|
+
*
|
|
2969
|
+
* @returns {boolean} whether the session is a tile here afterwards
|
|
2970
|
+
*/
|
|
2971
|
+
_dockForeignTile(sessionId, place) {
|
|
2972
|
+
const grid = this._tileGrid;
|
|
2973
|
+
if (!grid?.open || typeof sessionId !== 'string' || !this.sessions.has(sessionId)) return false;
|
|
2974
|
+
const poppedOut = !!this.detachedSessions?.has(sessionId);
|
|
2975
|
+
// A pop-out window owns its session's place: take it back before mounting
|
|
2976
|
+
// (a detached session never mounts).
|
|
2977
|
+
if (poppedOut) this._redock?.(sessionId);
|
|
2978
|
+
// Already a tile here too (two windows showed it): it moves to where it
|
|
2979
|
+
// was dropped, as a drag inside this window would move it.
|
|
2980
|
+
place(sessionId);
|
|
2981
|
+
if (!grid.has(sessionId)) return false;
|
|
2982
|
+
(this._tileDockedAt ||= new Map()).set(sessionId, Date.now());
|
|
2983
|
+
// Asked unconditionally: a pop-out this window never heard of (opened from
|
|
2984
|
+
// another dashboard) closes too. Dashboards ignore it; only that id's
|
|
2985
|
+
// pop-out reacts.
|
|
2986
|
+
this._postWindowMessage?.({ type: 'close-request', id: sessionId });
|
|
2987
|
+
this._postWindowMessage?.({ type: 'tile-adopted', id: sessionId, by: this._wsTabNonce });
|
|
2988
|
+
return true;
|
|
2989
|
+
},
|
|
2990
|
+
|
|
2991
|
+
/**
|
|
2992
|
+
* True for a session this window docked from another one a moment ago: a
|
|
2993
|
+
* 'detached' announcement from its old pop-out is that window on its way
|
|
2994
|
+
* out, never a new pop-out (it would take the tile straight back off).
|
|
2995
|
+
*/
|
|
2996
|
+
_tileDockedRecently(sessionId) {
|
|
2997
|
+
const at = this._tileDockedAt?.get(sessionId);
|
|
2998
|
+
return typeof at === 'number' && Date.now() - at < TILE_DOCK_GRACE_MS;
|
|
2999
|
+
},
|
|
3000
|
+
|
|
3001
|
+
/**
|
|
3002
|
+
* Another window docked `sessionId` ('tile-adopted'). Only the window it
|
|
3003
|
+
* was dragged FROM acts: its drag is still settling or ended a moment ago.
|
|
3004
|
+
* The tile goes without taking the keyboard anywhere (the user is in the
|
|
3005
|
+
* other window now). The last tile leaving closes the grid onto the welcome
|
|
3006
|
+
* screen, never onto that session: two windows sizing one PTY garble it.
|
|
3007
|
+
*/
|
|
3008
|
+
_onTileAdoptedElsewhere(sessionId) {
|
|
3009
|
+
const recent = this._lastTileDrag;
|
|
3010
|
+
// A header drag counts even without a tear-off (Detach Tiles off here): the
|
|
3011
|
+
// other window can take the tile before this drag ends.
|
|
3012
|
+
const dragging = this._tileTearOff?.id === sessionId || this._draggedTileId === sessionId;
|
|
3013
|
+
const pending = this._tileTearOffPending?.id === sessionId;
|
|
3014
|
+
const justEnded = recent?.id === sessionId && Date.now() - recent.at < TILE_ADOPT_WINDOW_MS;
|
|
3015
|
+
if (!dragging && !pending && !justEnded) return false;
|
|
3016
|
+
this._cancelTileTearOff();
|
|
3017
|
+
this._lastTileDrag = null;
|
|
3018
|
+
const grid = this._tileGrid;
|
|
3019
|
+
if (!grid?.has(sessionId)) return false;
|
|
3020
|
+
// One stored grid per browser, and the window that docked the tile has
|
|
3021
|
+
// just written it: this window losing the tile is no change of the
|
|
3022
|
+
// user's to remember over that one (nor to forget it).
|
|
3023
|
+
this._tilePersistHold = true;
|
|
3024
|
+
try {
|
|
3025
|
+
if (grid.ids.length > 1) return this.removeTile(sessionId, { focus: false });
|
|
3026
|
+
this.closeTileGrid({ keepStored: true, reselect: false });
|
|
3027
|
+
} finally {
|
|
3028
|
+
this._tilePersistHold = false;
|
|
3029
|
+
}
|
|
3030
|
+
this.activeSessionId = null;
|
|
3031
|
+
try {
|
|
3032
|
+
localStorage.removeItem('codeman-active-session');
|
|
3033
|
+
} catch {
|
|
3034
|
+
/* Nothing stored. */
|
|
3035
|
+
}
|
|
3036
|
+
this.terminal?.clear();
|
|
3037
|
+
this.showWelcome?.();
|
|
3038
|
+
return true;
|
|
3039
|
+
},
|
|
3040
|
+
|
|
3041
|
+
/**
|
|
3042
|
+
* A header drag starts with Detach Tiles on: note the tile's size and where
|
|
3043
|
+
* in it the press landed (the pop-out opens that size, under the pointer),
|
|
3044
|
+
* and watch whether the pointer leaves the page. Two signals, since
|
|
3045
|
+
* browsers differ in what dragend reports for a drop outside the window:
|
|
3046
|
+
* its coordinates, and the page's own dragleave at the viewport's edge
|
|
3047
|
+
* with nothing entered (cleared again by any dragover inside).
|
|
3048
|
+
*/
|
|
3049
|
+
_beginTileTearOff(sessionId, entry, e) {
|
|
3050
|
+
this._stopTileTearOffTracking();
|
|
3051
|
+
const rect = entry.el.getBoundingClientRect();
|
|
3052
|
+
const within = (v, max) => (Number.isFinite(v) ? Math.max(0, Math.min(v, max)) : 0);
|
|
3053
|
+
const drag = {
|
|
3054
|
+
id: sessionId,
|
|
3055
|
+
width: rect.width,
|
|
3056
|
+
height: rect.height,
|
|
3057
|
+
offsetX: within(e?.clientX - rect.left, rect.width),
|
|
3058
|
+
offsetY: within(e?.clientY - rect.top, rect.height),
|
|
3059
|
+
outside: false,
|
|
3060
|
+
stop: null,
|
|
3061
|
+
};
|
|
3062
|
+
const onOver = () => {
|
|
3063
|
+
drag.outside = false;
|
|
3064
|
+
};
|
|
3065
|
+
const onLeave = (ev) => {
|
|
3066
|
+
if (ev.relatedTarget) return;
|
|
3067
|
+
const x = ev.clientX;
|
|
3068
|
+
const y = ev.clientY;
|
|
3069
|
+
if (x <= 0 || y <= 0 || x >= window.innerWidth - 1 || y >= window.innerHeight - 1) drag.outside = true;
|
|
3070
|
+
};
|
|
3071
|
+
document.addEventListener('dragover', onOver, true);
|
|
3072
|
+
document.addEventListener('dragleave', onLeave, true);
|
|
3073
|
+
drag.stop = () => {
|
|
3074
|
+
document.removeEventListener('dragover', onOver, true);
|
|
3075
|
+
document.removeEventListener('dragleave', onLeave, true);
|
|
3076
|
+
};
|
|
3077
|
+
this._tileTearOff = drag;
|
|
3078
|
+
},
|
|
3079
|
+
|
|
3080
|
+
/** The header drag is over (or the grid or tile went mid-drag): stop watching the pointer. */
|
|
3081
|
+
_stopTileTearOffTracking() {
|
|
3082
|
+
const drag = this._tileTearOff;
|
|
3083
|
+
if (!drag) return;
|
|
3084
|
+
this._tileTearOff = null;
|
|
3085
|
+
drag.stop?.();
|
|
3086
|
+
},
|
|
3087
|
+
|
|
3088
|
+
/**
|
|
3089
|
+
* dragend of a tile's header with Detach Tiles on. Taken by a drop target
|
|
3090
|
+
* (here or in another window: `dropEffect` is not 'none'), it stays where
|
|
3091
|
+
* that put it. Let go OUTSIDE the window (the dragend point, or the page's
|
|
3092
|
+
* own record of the pointer leaving), it opens as a window of its own after
|
|
3093
|
+
* TILE_TEAR_OFF_SETTLE_MS, unless another window claims it first. Let go
|
|
3094
|
+
* inside, on nothing, nothing happens, as before.
|
|
3095
|
+
*/
|
|
3096
|
+
_settleTileTearOff(sessionId, e) {
|
|
3097
|
+
const drag = this._tileTearOff?.id === sessionId ? this._tileTearOff : null;
|
|
3098
|
+
this._lastTileDrag = { id: sessionId, at: Date.now() };
|
|
3099
|
+
if (!drag) return false;
|
|
3100
|
+
this._stopTileTearOffTracking();
|
|
3101
|
+
const effect = e?.dataTransfer?.dropEffect;
|
|
3102
|
+
if (effect && effect !== 'none') return false;
|
|
3103
|
+
const cx = e?.clientX;
|
|
3104
|
+
const cy = e?.clientY;
|
|
3105
|
+
// (0, 0) is what a browser reports when it does not know where the drop was.
|
|
3106
|
+
const known = Number.isFinite(cx) && Number.isFinite(cy) && !(cx === 0 && cy === 0);
|
|
3107
|
+
const outside = known ? cx < 0 || cy < 0 || cx >= window.innerWidth || cy >= window.innerHeight : drag.outside;
|
|
3108
|
+
if (!outside) return false;
|
|
3109
|
+
const sx = e?.screenX;
|
|
3110
|
+
const sy = e?.screenY;
|
|
3111
|
+
const placed = Number.isFinite(sx) && Number.isFinite(sy) && !(sx === 0 && sy === 0);
|
|
3112
|
+
const placement = {
|
|
3113
|
+
width: drag.width,
|
|
3114
|
+
height: drag.height,
|
|
3115
|
+
...(placed ? { screenX: sx, screenY: sy, offsetX: drag.offsetX, offsetY: drag.offsetY } : {}),
|
|
3116
|
+
};
|
|
3117
|
+
this._cancelTileTearOff();
|
|
3118
|
+
const pending = { id: sessionId, timer: null };
|
|
3119
|
+
pending.timer = setTimeout(() => {
|
|
3120
|
+
if (this._tileTearOffPending !== pending) return;
|
|
3121
|
+
this._tileTearOffPending = null;
|
|
3122
|
+
this.detachTile(sessionId, placement);
|
|
3123
|
+
}, TILE_TEAR_OFF_SETTLE_MS);
|
|
3124
|
+
this._tileTearOffPending = pending;
|
|
3125
|
+
return true;
|
|
3126
|
+
},
|
|
3127
|
+
|
|
3128
|
+
_cancelTileTearOff() {
|
|
3129
|
+
const pending = this._tileTearOffPending;
|
|
3130
|
+
if (!pending) return;
|
|
3131
|
+
this._tileTearOffPending = null;
|
|
3132
|
+
clearTimeout(pending.timer);
|
|
3133
|
+
},
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Opens a tile as a window of its own (detachSession, which takes it out of
|
|
3137
|
+
* the grid), the tile's size (at least TILE_WINDOW_MIN_W x MIN_H). Placed
|
|
3138
|
+
* with its header under a screen point when given one (`screenX`/`screenY`
|
|
3139
|
+
* and where in the tile it was held, `offsetX`/`offsetY`: a drop outside the
|
|
3140
|
+
* window, a hand let go), otherwise over the tile itself (the ⋯ menu).
|
|
3141
|
+
*
|
|
3142
|
+
* @returns {boolean} whether the session is popped out afterwards
|
|
3143
|
+
*/
|
|
3144
|
+
detachTile(sessionId, { screenX, screenY, offsetX = 0, offsetY = 0, width, height } = {}) {
|
|
3145
|
+
const entry = this._tileGrid?.open ? this._tileGrid.tiles.get(sessionId) : null;
|
|
3146
|
+
if (!entry || !this._tileDetachAllowed()) return false;
|
|
3147
|
+
const rect = entry.el.getBoundingClientRect();
|
|
3148
|
+
const availW = window.screen?.availWidth || Infinity;
|
|
3149
|
+
const availH = window.screen?.availHeight || Infinity;
|
|
3150
|
+
const size = (v, min, max) => Math.round(Math.min(Math.max(Number.isFinite(v) ? v : min, min), max));
|
|
3151
|
+
const placement = {
|
|
3152
|
+
width: size(width ?? rect.width, TILE_WINDOW_MIN_W, availW),
|
|
3153
|
+
height: size(height ?? rect.height, TILE_WINDOW_MIN_H, availH),
|
|
3154
|
+
};
|
|
3155
|
+
if (Number.isFinite(screenX) && Number.isFinite(screenY)) {
|
|
3156
|
+
placement.left = Math.round(screenX - offsetX);
|
|
3157
|
+
placement.top = Math.round(screenY - offsetY - TILE_WINDOW_CHROME_PX);
|
|
3158
|
+
} else if (Number.isFinite(window.screenX) && Number.isFinite(window.screenY)) {
|
|
3159
|
+
// Over the tile: the page's own chrome sits above its viewport.
|
|
3160
|
+
const chrome = Math.max(0, (window.outerHeight || 0) - (window.innerHeight || 0));
|
|
3161
|
+
placement.left = Math.round(window.screenX + rect.left);
|
|
3162
|
+
placement.top = Math.round(window.screenY + chrome + rect.top - TILE_WINDOW_CHROME_PX);
|
|
3163
|
+
}
|
|
3164
|
+
this.detachSession?.(sessionId, placement);
|
|
3165
|
+
return !!this.detachedSessions?.has(sessionId);
|
|
3166
|
+
},
|
|
3167
|
+
|
|
3168
|
+
// ── For the gesture overlay (packages/gesture-control, src/codeman/entry.ts)
|
|
3169
|
+
//
|
|
3170
|
+
// A hand is no native drag: the overlay asks where it is and what is under
|
|
3171
|
+
// it, and every move still goes through the paths a mouse drag takes
|
|
3172
|
+
// (dropSessionOnTile / dropSessionOnSlot, and so _reorderTiles), the
|
|
3173
|
+
// pop-out through detachTile.
|
|
3174
|
+
|
|
3175
|
+
/** The tiled session whose tile is at viewport point (x, y), or null. */
|
|
3176
|
+
tileAtPoint(x, y) {
|
|
3177
|
+
const grid = this._tileGrid;
|
|
3178
|
+
if (!grid?.open) return null;
|
|
3179
|
+
const hit = document.elementFromPoint?.(x, y);
|
|
3180
|
+
if (!hit) return null;
|
|
3181
|
+
for (const [id, entry] of grid.tiles) if (entry.el.contains?.(hit)) return id;
|
|
3182
|
+
return null;
|
|
3183
|
+
},
|
|
3184
|
+
|
|
3185
|
+
/** Whether a hand may pick up this tile: it can move, or (Detach Tiles) leave the window. */
|
|
3186
|
+
canGrabTile(sessionId) {
|
|
3187
|
+
return !!this._tileGrid?.has(sessionId) && (this._tilesMovable() || this._tileDetachAllowed());
|
|
3188
|
+
},
|
|
3189
|
+
|
|
3190
|
+
/**
|
|
3191
|
+
* What a session dragged to viewport point (x, y) would drop onto, by the
|
|
3192
|
+
* mouse drag's own rules: another tile (it takes that tile's place, or the
|
|
3193
|
+
* two trade places), an empty cell (it moves or joins there). A tiled
|
|
3194
|
+
* session only while tiles can move (not zoomed). Null for anything else,
|
|
3195
|
+
* a popped-out session included.
|
|
3196
|
+
*
|
|
3197
|
+
* @returns {{kind: 'tile', id: string, el: HTMLElement}|{kind: 'cell', cell: number, el: HTMLElement}|null}
|
|
3198
|
+
*/
|
|
3199
|
+
tileDropTargetAt(x, y, draggedId) {
|
|
3200
|
+
const grid = this._tileGrid;
|
|
3201
|
+
if (!grid?.open || !this.sessions.has(draggedId) || this.detachedSessions?.has(draggedId)) return null;
|
|
3202
|
+
if (grid.tiles.has(draggedId) && !this._tilesMovable()) return null;
|
|
3203
|
+
const hit = document.elementFromPoint?.(x, y);
|
|
3204
|
+
if (!hit) return null;
|
|
3205
|
+
for (const [id, entry] of grid.tiles) {
|
|
3206
|
+
if (entry.el.contains?.(hit)) return id === draggedId ? null : { kind: 'tile', id, el: entry.el };
|
|
3207
|
+
}
|
|
3208
|
+
for (const slot of grid.slots || []) {
|
|
3209
|
+
if (slot.contains?.(hit)) return { kind: 'cell', cell: Number(slot.dataset.cell), el: slot };
|
|
3210
|
+
}
|
|
3211
|
+
return null;
|
|
3212
|
+
},
|
|
3213
|
+
|
|
3214
|
+
/**
|
|
3215
|
+
* Drops a session on a target tileDropTargetAt returned (the hand let go).
|
|
3216
|
+
*
|
|
3217
|
+
* @returns {boolean} whether the session is a tile afterwards
|
|
3218
|
+
*/
|
|
3219
|
+
dropOnTileTarget(draggedId, target) {
|
|
3220
|
+
if (!target || !this._tileGrid?.open) return false;
|
|
3221
|
+
if (target.kind === 'tile') this.dropSessionOnTile(draggedId, target.id);
|
|
3222
|
+
else if (target.kind === 'cell') this.dropSessionOnSlot(draggedId, target.cell);
|
|
3223
|
+
return !!this._tileGrid?.has(draggedId);
|
|
3224
|
+
},
|
|
3225
|
+
|
|
3226
|
+
/** Whether viewport point (x, y) is over the open grid (a hand let go outside it pops the tile out). */
|
|
3227
|
+
isOverTileGrid(x, y) {
|
|
3228
|
+
if (!this._tileGrid?.open) return false;
|
|
3229
|
+
const rect = document.getElementById('tileGrid')?.getBoundingClientRect?.();
|
|
3230
|
+
return !!rect && x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
|
3231
|
+
},
|
|
3232
|
+
|
|
3233
|
+
/**
|
|
3234
|
+
* A hand let a tile go outside the grid (Detach Tiles): it opens as a window
|
|
3235
|
+
* of its own with its header near that point. A script opening a window
|
|
3236
|
+
* with no click behind it is what popup blockers stop; detachSession then
|
|
3237
|
+
* says so and the tile stays.
|
|
3238
|
+
*/
|
|
3239
|
+
detachTileAtPoint(sessionId, x, y, { offsetX = 0, offsetY = 0 } = {}) {
|
|
3240
|
+
const chrome = Math.max(0, (window.outerHeight || 0) - (window.innerHeight || 0));
|
|
3241
|
+
const sx = (window.screenX || 0) + x;
|
|
3242
|
+
const sy = (window.screenY || 0) + chrome + y;
|
|
3243
|
+
return this.detachTile(sessionId, { screenX: sx, screenY: sy, offsetX, offsetY });
|
|
3244
|
+
},
|
|
3245
|
+
|
|
3246
|
+
/**
|
|
3247
|
+
* A pop-out window's title, as a handle (Detach Tiles): dragged onto a
|
|
3248
|
+
* Codeman window's tile or empty cell, the session docks there as a tile
|
|
3249
|
+
* and this window closes (_dockForeignTile, which asks it to). The same
|
|
3250
|
+
* drag type as a tile's header; on or off follows the setting, also when it
|
|
3251
|
+
* changes in another window.
|
|
3252
|
+
*/
|
|
3253
|
+
_installSoloTileHandle() {
|
|
3254
|
+
const title = document.getElementById('soloSessionTitle');
|
|
3255
|
+
if (!this.isSoloWindow || !title || title._codemanTileHandle) return;
|
|
3256
|
+
title._codemanTileHandle = true;
|
|
3257
|
+
const sync = () => {
|
|
3258
|
+
this._cachedAppSettings = null;
|
|
3259
|
+
const on = this.tileDetachEnabled();
|
|
3260
|
+
title.setAttribute('draggable', on ? 'true' : 'false');
|
|
3261
|
+
title.classList.toggle('solo-session-title--handle', on);
|
|
3262
|
+
const label = on ? "Drag onto a Codeman window's tiles to dock this session there" : '';
|
|
3263
|
+
if (title._codemanTileHandleLabel !== label) {
|
|
3264
|
+
title._codemanTileHandleLabel = label;
|
|
3265
|
+
title.title = label;
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
sync();
|
|
3269
|
+
window.addEventListener('storage', (e) => {
|
|
3270
|
+
if (!e.key || e.key === this.getSettingsStorageKey?.()) sync();
|
|
3271
|
+
});
|
|
3272
|
+
title.addEventListener('dragstart', (e) => {
|
|
3273
|
+
if (!this.tileDetachEnabled() || !this.soloSessionId) {
|
|
3274
|
+
e.preventDefault?.();
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
if (e.dataTransfer) {
|
|
3278
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
3279
|
+
e.dataTransfer.setData(TILE_DRAG_TYPE, this.soloSessionId);
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
},
|
|
2651
3283
|
});
|
|
2652
3284
|
|
|
2653
3285
|
// A tiled session deleted (here or elsewhere) loses its tile; if it held focus,
|
|
@@ -2665,7 +3297,7 @@ CodemanApp.prototype._onSessionDeleted = function (data) {
|
|
|
2665
3297
|
if (grid?.has(data.id)) {
|
|
2666
3298
|
const wasFocused = grid.focusedId === data.id;
|
|
2667
3299
|
const neighbor = window.CodemanTileGrid.tileNeighbor(grid.ids, data.id);
|
|
2668
|
-
this.removeTile(data.id, { refocus: false });
|
|
3300
|
+
this.removeTile(data.id, { refocus: false, gone: true });
|
|
2669
3301
|
if (wasFocused && grid.open && neighbor && !this._closingSessions?.has(data.id)) {
|
|
2670
3302
|
this._selectTiledSession(neighbor, { auto: true, focus: false });
|
|
2671
3303
|
}
|