aicodeman 1.48.0 → 1.49.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 +12 -3
- package/dist/config/cli-registry/schema.d.ts +1 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +5 -1
- 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 +71 -5
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +4 -4
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/foreign-tmux.d.ts +48 -0
- package/dist/config/foreign-tmux.d.ts.map +1 -0
- package/dist/config/foreign-tmux.js +48 -0
- package/dist/config/foreign-tmux.js.map +1 -0
- package/dist/custom-model-injection-apply.d.ts +4 -2
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +35 -21
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/docker-hosts.d.ts +8 -1
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +5 -5
- package/dist/docker-hosts.js.map +1 -1
- package/dist/foreign-tmux-discovery.d.ts +140 -0
- package/dist/foreign-tmux-discovery.d.ts.map +1 -0
- package/dist/foreign-tmux-discovery.js +366 -0
- package/dist/foreign-tmux-discovery.js.map +1 -0
- package/dist/foreign-tmux.d.ts +381 -0
- package/dist/foreign-tmux.d.ts.map +1 -0
- package/dist/foreign-tmux.js +775 -0
- package/dist/foreign-tmux.js.map +1 -0
- package/dist/git-fetch-scheduler.d.ts +2 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -1
- package/dist/git-fetch-scheduler.js +1 -1
- package/dist/git-fetch-scheduler.js.map +1 -1
- package/dist/git-fetch.d.ts +1 -1
- package/dist/git-fetch.d.ts.map +1 -1
- package/dist/git-workspace-status.d.ts +2 -2
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/hooks-config.d.ts +2 -1
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +2 -1
- package/dist/hooks-config.js.map +1 -1
- package/dist/live-worktree.d.ts +53 -0
- package/dist/live-worktree.d.ts.map +1 -0
- package/dist/live-worktree.js +97 -0
- package/dist/live-worktree.js.map +1 -0
- package/dist/mux-interface.d.ts +12 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/reboot-restore.d.ts +1 -1
- package/dist/reboot-restore.d.ts.map +1 -1
- package/dist/reboot-restore.js +9 -0
- package/dist/reboot-restore.js.map +1 -1
- package/dist/remote-files.d.ts +150 -5
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +505 -7
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-hosts.d.ts +15 -9
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +42 -20
- package/dist/remote-hosts.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +19 -8
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-env-clamp.d.ts +1 -1
- package/dist/session-env-clamp.d.ts.map +1 -1
- package/dist/session-env-clamp.js +4 -3
- package/dist/session-env-clamp.js.map +1 -1
- package/dist/session.d.ts +91 -21
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +190 -33
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +74 -12
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +256 -41
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/foreign-tmux.d.ts +101 -0
- package/dist/types/foreign-tmux.d.ts.map +1 -0
- package/dist/types/foreign-tmux.js +27 -0
- package/dist/types/foreign-tmux.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +106 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/update.d.ts +4 -1
- package/dist/types/update.d.ts.map +1 -1
- package/dist/web/edit-history-sync.d.ts +27 -7
- package/dist/web/edit-history-sync.d.ts.map +1 -1
- package/dist/web/edit-history-sync.js +35 -19
- package/dist/web/edit-history-sync.js.map +1 -1
- package/dist/web/launch-defaults.d.ts +1 -1
- package/dist/web/launch-defaults.js +2 -2
- package/dist/web/launch-defaults.js.map +1 -1
- package/dist/web/linked-cases-writer.d.ts +43 -0
- package/dist/web/linked-cases-writer.d.ts.map +1 -0
- package/dist/web/linked-cases-writer.js +107 -0
- package/dist/web/linked-cases-writer.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +4 -1
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +4 -2
- package/dist/web/paste-image-gc.js.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.963b5ba8.js → app.9ea37070.js} +6 -6
- package/dist/web/public/app.9ea37070.js.br +0 -0
- package/dist/web/public/app.9ea37070.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +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 +27 -14
- 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/foreign-sessions.js +476 -0
- package/dist/web/public/foreign-sessions.js.br +0 -0
- package/dist/web/public/foreign-sessions.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +5 -3
- package/dist/web/public/git-status-ui.js.br +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.a5e82c95.js +2 -0
- package/dist/web/public/i18n.a5e82c95.js.br +0 -0
- package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +36 -17
- 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.d4ec5957.js → keyboard-accessory.b941676c.js} +3 -1
- package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
- package/dist/web/public/{keyboard-accessory.d4ec5957.js.gz → keyboard-accessory.b941676c.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 +10 -0
- 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.5ef2ce85.css → mobile.67c8eb5b.css} +1 -1
- package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
- package/dist/web/public/{mobile.5ef2ce85.css.gz → mobile.67c8eb5b.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.b100727d.js → panels-ui.8bcd4473.js} +4 -4
- package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
- package/dist/web/public/panels-ui.8bcd4473.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.304ba0d4.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.5ac82f2c.js → session-ui.a7c8c095.js} +11 -11
- package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
- package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
- package/dist/web/public/{settings-ui.d1d4fb99.js → settings-ui.18f92052.js} +3 -3
- package/dist/web/public/settings-ui.18f92052.js.br +0 -0
- package/dist/web/public/settings-ui.18f92052.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 → styles.6a718331.css} +1 -1
- package/dist/web/public/styles.6a718331.css.br +0 -0
- package/dist/web/public/{styles.77dc538f.css.gz → styles.6a718331.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.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +2 -2
- 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.d7c75ce8.js → terminal-ui.a7883640.js} +2 -2
- package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
- package/dist/web/public/terminal-ui.a7883640.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/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +72 -53
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.js +1 -1
- package/dist/web/routes/discord-routes.js.map +1 -1
- package/dist/web/routes/edit-history-routes.d.ts +2 -2
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -1
- package/dist/web/routes/edit-history-routes.js +4 -3
- package/dist/web/routes/edit-history-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +213 -102
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +7 -2
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/mux-routes.d.ts +10 -3
- package/dist/web/routes/mux-routes.d.ts.map +1 -1
- package/dist/web/routes/mux-routes.js +59 -1
- package/dist/web/routes/mux-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +22 -0
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
- package/dist/web/routes/readmymind-routes.js +3 -2
- package/dist/web/routes/readmymind-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +24 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +11 -8
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +262 -29
- 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 +3 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +24 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +34 -9
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/self-update.d.ts +9 -0
- package/dist/web/self-update.d.ts.map +1 -1
- package/dist/web/self-update.js +19 -3
- package/dist/web/self-update.js.map +1 -1
- package/dist/web/server.d.ts +16 -3
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +84 -24
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +13 -0
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +16 -8
- package/dist/web/session-wait-registry.js.map +1 -1
- package/package.json +2 -2
- package/skills/codeman/SKILL.md +20 -4
- package/dist/web/public/app.963b5ba8.js.br +0 -0
- package/dist/web/public/app.963b5ba8.js.gz +0 -0
- package/dist/web/public/i18n.33f930d7.js +0 -2
- package/dist/web/public/i18n.33f930d7.js.br +0 -0
- package/dist/web/public/i18n.33f930d7.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
- package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
- package/dist/web/public/panels-ui.b100727d.js.br +0 -0
- package/dist/web/public/panels-ui.b100727d.js.gz +0 -0
- 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.br +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
- package/dist/web/public/styles.77dc538f.css.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* `GET /api/sessions/:id/edits?sync=1` when it opens and when the active session changes, and,
|
|
10
10
|
* only while it is open, re-reads (debounced) on the active session's `transcript:tool_end` for a
|
|
11
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.
|
|
12
|
+
* (the end of a turn). No poll, and nothing at all while the panel is closed. A remote session has
|
|
13
|
+
* no transcript watcher (no hooks reach back from its host), so it follows turn ends only.
|
|
13
14
|
*
|
|
14
15
|
* - A code entry opens its diff inline (`GET /api/sessions/:id/edits/:entryId`), drawn with the Git
|
|
15
16
|
* panel's diff classes (`git-diff`, `git-diff-line--hunk|add|del`).
|
|
16
17
|
* - An image entry opens the shared file preview (panels-ui.js `openFilePreview`), as does a click
|
|
17
|
-
* on any path; inside
|
|
18
|
+
* on any path; inside a local session's workspace it also shows a thumbnail (a remote one would
|
|
19
|
+
* stream every listed image over ssh). Both go through the existing file
|
|
18
20
|
* routes only: the thumbnail is the confined `file-raw`, and the preview of a file outside the
|
|
19
21
|
* workspace registers it on click with `notify: false` (`_registerExternalPreview`, which
|
|
20
22
|
* enforces the attachment guard and its extension allowlist). A path from the transcript is
|
|
@@ -318,10 +320,12 @@ Object.assign(CodemanApp.prototype, {
|
|
|
318
320
|
* (`POST …/attachments`, `notify: false`), and minting one per rendered row would push the
|
|
319
321
|
* session's real attachments out of its capped registry. Clicking it opens the file preview,
|
|
320
322
|
* which registers it like any other external file. SVGs get none either (`file-raw` serves
|
|
321
|
-
* them as a download)
|
|
323
|
+
* them as a download), nor does a remote session's image: its `file-raw` streams over ssh, and
|
|
324
|
+
* a list of generated images would open one ssh read per row; a click still previews it.
|
|
322
325
|
*/
|
|
323
326
|
_editHistoryThumbSrc(path, sid) {
|
|
324
|
-
if (!path || /\.svg$/i.test(path) || this.
|
|
327
|
+
if (!path || /\.svg$/i.test(path) || this.sessions?.get?.(sid)?.remote) return null;
|
|
328
|
+
if (this._isExternalPreviewPath?.(path, sid) !== false) return null;
|
|
325
329
|
return this._editHistoryUrl(`/api/sessions/${encodeURIComponent(sid)}/file-raw?path=${encodeURIComponent(path)}`);
|
|
326
330
|
},
|
|
327
331
|
|
|
@@ -353,21 +357,30 @@ Object.assign(CodemanApp.prototype, {
|
|
|
353
357
|
}
|
|
354
358
|
const data = h.data;
|
|
355
359
|
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
360
|
body.append(
|
|
361
361
|
el(
|
|
362
362
|
'div',
|
|
363
|
-
'edit-history-
|
|
364
|
-
data.reason === '
|
|
365
|
-
? t(
|
|
366
|
-
|
|
367
|
-
)
|
|
368
|
-
: t('Showing saved entries only: {reason}', { reason: data.reason || '' })
|
|
363
|
+
'edit-history-empty',
|
|
364
|
+
data.reason === 'adopted'
|
|
365
|
+
? t('Edit history is not available for a tmux session opened outside Codeman.')
|
|
366
|
+
: t('Edit history is only available for Claude sessions outside Docker.')
|
|
369
367
|
)
|
|
370
368
|
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (data.state === 'unavailable') {
|
|
372
|
+
// A remote host sends no hooks: its transcript is only ever found by the lookup over ssh.
|
|
373
|
+
const note =
|
|
374
|
+
data.reason !== 'no-transcript'
|
|
375
|
+
? t('Showing saved entries only: {reason}', { reason: data.reason || '' })
|
|
376
|
+
: this.sessions?.get?.(h.sessionId)?.remote
|
|
377
|
+
? t(
|
|
378
|
+
'No transcript found on the remote host yet (Claude writes it with the first prompt). A CLAUDE_CONFIG_DIR set only in a shell profile (.profile, .bash_profile, .zshrc) cannot be found.'
|
|
379
|
+
)
|
|
380
|
+
: t(
|
|
381
|
+
'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.'
|
|
382
|
+
);
|
|
383
|
+
body.append(el('div', 'edit-history-note edit-history-note--warn', note));
|
|
371
384
|
}
|
|
372
385
|
|
|
373
386
|
const all = Array.isArray(data.entries) ? data.entries : [];
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The "sessions someone opened by hand" block on the home screen.
|
|
3
|
+
*
|
|
4
|
+
* A tmux session a human started (`tmux new -s work`, then `claude`, or just a
|
|
5
|
+
* shell) is invisible to Codeman: it lives on the DEFAULT socket, not the
|
|
6
|
+
* instance-scoped one Codeman owns. `GET /api/mux/foreign` finds those, and one
|
|
7
|
+
* click wraps one in a Codeman tab (`POST /api/sessions/adopt`).
|
|
8
|
+
*
|
|
9
|
+
* ## One renderer, two containers
|
|
10
|
+
*
|
|
11
|
+
* The welcome screen and the phone overview both show this list. They call the
|
|
12
|
+
* SAME `renderForeignSessions(container)` rather than each building rows, because
|
|
13
|
+
* two renderers are how one surface ends up calling a session "shell" while the
|
|
14
|
+
* other calls it "bash". Same reason `CodemanSessionOrder` is shared.
|
|
15
|
+
*
|
|
16
|
+
* ## Polling only while the home screen is up
|
|
17
|
+
*
|
|
18
|
+
* The list is a poll, not an SSE stream: the server caches a local scan on a TTL,
|
|
19
|
+
* so N tabs polling costs one scan per TTL no matter how many are open. But an
|
|
20
|
+
* open tab that has LEFT the home screen must stop, otherwise every background
|
|
21
|
+
* tab keeps sweeping tmux sockets forever. `startForeignPolling` /
|
|
22
|
+
* `stopForeignPolling` are called from `showWelcome`/`hideWelcome`.
|
|
23
|
+
*
|
|
24
|
+
* A tab hidden while it shows the home screen skips its ticks as well. The TTL sits
|
|
25
|
+
* under the poll interval, so one tab never hits the cache: every tick is a full
|
|
26
|
+
* sweep, one fork or more per socket, for a list nobody is reading. It catches up
|
|
27
|
+
* on `visibilitychange`, and the cache is what bounds a flurry of tab switches.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ Docker and remote locations are NOT in that poll. Each costs a `docker exec`
|
|
30
|
+
* or a full ssh handshake per target, each with a ~12s timeout, so riding them
|
|
31
|
+
* along on an 8s tick stacks connections on a slow host, the one cost the
|
|
32
|
+
* server-side design explicitly refuses. The wide scan is therefore ONE-SHOT:
|
|
33
|
+
* turning the toggle on fetches once, and after that it is the user's Rescan
|
|
34
|
+
* button. The periodic poll stays local-only forever.
|
|
35
|
+
*
|
|
36
|
+
* The wide rows are kept separately (`_foreignWideSessions`) and merged into
|
|
37
|
+
* every local render, because a local-only poll's payload does not contain them
|
|
38
|
+
* and would otherwise wipe the container/remote rows off the list one tick after
|
|
39
|
+
* they appeared. Turning the toggle off discards them.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ Adoption creates a REAL session, so the button locks while in flight and the
|
|
42
|
+
* result goes through the app's normal idempotent create path
|
|
43
|
+
* (`_onSessionCreated` then `selectSession`), never by inserting a tab here.
|
|
44
|
+
*
|
|
45
|
+
* @mixin Extends CodemanApp.prototype via Object.assign
|
|
46
|
+
* @dependency app.js (this.sessions, _onSessionCreated, selectSession)
|
|
47
|
+
* @dependency api-client.js (_api)
|
|
48
|
+
* @dependency ralph-panel.js (formatRelativeTime)
|
|
49
|
+
* @dependency i18n.js (codemanT, optional)
|
|
50
|
+
* @loadorder 12.565 of 16, after home-sessions.js, before git-status-ui.js
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** Fallback poll cadence; the server sends its own in `pollIntervalMs`. */
|
|
54
|
+
const FOREIGN_POLL_FALLBACK_MS = 8000;
|
|
55
|
+
|
|
56
|
+
/** i18n.js's translator, or the English source where it has not loaded. */
|
|
57
|
+
function foreignT(source) {
|
|
58
|
+
return typeof codemanT === 'function' ? codemanT(source) : source;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A row's mode, in the registry's own word for it. Falls back to the id, never a list of ids here. */
|
|
62
|
+
function foreignModeLabel(mode) {
|
|
63
|
+
const catalog =
|
|
64
|
+
typeof window !== 'undefined' && Array.isArray(window.__codemanCliCatalog) ? window.__codemanCliCatalog : [];
|
|
65
|
+
return catalog.find((entry) => entry.id === mode)?.label || mode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const FOREIGN_LOCATION_LABEL = {
|
|
69
|
+
local: 'this machine',
|
|
70
|
+
docker: 'container',
|
|
71
|
+
remote: 'remote',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
Object.assign(CodemanApp.prototype, {
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════
|
|
76
|
+
// Lifecycle
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Begin polling. Idempotent: the home screen re-shows on several paths
|
|
81
|
+
* (boot, closing the last tab, a breakpoint change) and each would otherwise
|
|
82
|
+
* stack another interval on top of the last.
|
|
83
|
+
*/
|
|
84
|
+
startForeignPolling() {
|
|
85
|
+
if (this._foreignPollTimer || this._foreignRefused) return;
|
|
86
|
+
this._foreignPollTick();
|
|
87
|
+
this._foreignPollTimer = setInterval(
|
|
88
|
+
() => this._foreignPollTick(),
|
|
89
|
+
this._foreignPollMs || FOREIGN_POLL_FALLBACK_MS
|
|
90
|
+
);
|
|
91
|
+
// Stored, unlike a poller made once: this runs on every trip to the home screen.
|
|
92
|
+
this._foreignOnVisible = () => this._foreignPollTick();
|
|
93
|
+
document.addEventListener('visibilitychange', this._foreignOnVisible);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** Stop polling. MUST run on leaving the home screen, see @fileoverview. */
|
|
97
|
+
stopForeignPolling() {
|
|
98
|
+
if (!this._foreignPollTimer) return;
|
|
99
|
+
clearInterval(this._foreignPollTimer);
|
|
100
|
+
this._foreignPollTimer = null;
|
|
101
|
+
document.removeEventListener('visibilitychange', this._foreignOnVisible);
|
|
102
|
+
this._foreignOnVisible = null;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/** One poll, and the one gate every poll passes: a hidden tab skips it, see @fileoverview. */
|
|
106
|
+
_foreignPollTick() {
|
|
107
|
+
if (document.hidden) return;
|
|
108
|
+
// Local only: see @fileoverview. The wide scan never rides the timer.
|
|
109
|
+
void this.loadForeignSessions();
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/** Whether the user asked for the expensive docker/remote scan too. */
|
|
113
|
+
foreignScanRemote() {
|
|
114
|
+
return this._foreignScanRemote === true;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Flip the wide scan. ON fetches containers and hosts ONCE; OFF discards what
|
|
119
|
+
* that fetch found, because those rows are not re-confirmed by any later poll
|
|
120
|
+
* and leaving them up would show a container session that may be long gone.
|
|
121
|
+
*/
|
|
122
|
+
toggleForeignScanRemote() {
|
|
123
|
+
this._foreignScanRemote = !this._foreignScanRemote;
|
|
124
|
+
if (!this._foreignScanRemote) {
|
|
125
|
+
this._foreignWideSessions = [];
|
|
126
|
+
this._foreignWideNotes = [];
|
|
127
|
+
this.renderAllForeignSessions();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
void this.loadForeignSessions({ wide: true });
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/** Re-run the expensive scan on demand. The only way wide rows ever refresh. */
|
|
134
|
+
rescanForeignWide() {
|
|
135
|
+
if (!this.foreignScanRemote()) return;
|
|
136
|
+
void this.loadForeignSessions({ wide: true });
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════
|
|
140
|
+
// Data
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fetch the list. `wide` asks the server for containers and ssh hosts too, and
|
|
145
|
+
* ONLY an explicit user action ever passes it, never the interval.
|
|
146
|
+
*/
|
|
147
|
+
async loadForeignSessions({ wide = false } = {}) {
|
|
148
|
+
if (this._foreignRefused) return;
|
|
149
|
+
const qs = wide ? '?docker=1&remote=1' : '';
|
|
150
|
+
if (wide) this._foreignWideLoading = true;
|
|
151
|
+
this.renderAllForeignSessions();
|
|
152
|
+
const res = await this._api(`/api/mux/foreign${qs}`);
|
|
153
|
+
if (wide) this._foreignWideLoading = false;
|
|
154
|
+
// ⚠️ A 403 is an answer, not a failed poll. The list is admin-only in multi-user
|
|
155
|
+
// mode (it shows other people's processes), so a non-admin has nothing to see here
|
|
156
|
+
// and nothing later on this page changes that: stop asking and show no block,
|
|
157
|
+
// instead of "Could not reach the server" under a heading they cannot use.
|
|
158
|
+
if (res?.status === 403) {
|
|
159
|
+
this._foreignRefused = true;
|
|
160
|
+
this.stopForeignPolling();
|
|
161
|
+
this.renderAllForeignSessions();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const body = res?.ok ? await this._readJsonBody(res) : null;
|
|
165
|
+
const data = body?.success === true ? body.data : body?.success === false ? null : body;
|
|
166
|
+
if (!data) {
|
|
167
|
+
// A failed poll must not blank a list the user is looking at: keep the
|
|
168
|
+
// last good result and let the next tick recover.
|
|
169
|
+
this._foreignError = true;
|
|
170
|
+
this.renderAllForeignSessions();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this._foreignError = false;
|
|
174
|
+
const rows = Array.isArray(data.sessions) ? data.sessions : [];
|
|
175
|
+
const notes = Array.isArray(data.notes) ? data.notes : [];
|
|
176
|
+
// Every note opens with its location (foreign-tmux-discovery.ts). A local one is
|
|
177
|
+
// re-read on each poll; a container or host one only by the next explicit scan,
|
|
178
|
+
// so it has to outlive the local polls in between.
|
|
179
|
+
const isLocalNote = (note) => String(note).startsWith('local:');
|
|
180
|
+
this._foreignNotes = notes.filter(isLocalNote);
|
|
181
|
+
if (wide) {
|
|
182
|
+
// Split by location: the local rows refresh on every tick, the rest only
|
|
183
|
+
// here, so they have to be stored apart or the next local-only payload
|
|
184
|
+
// (which cannot contain them) would read as "those sessions are gone".
|
|
185
|
+
this._foreignSessions = rows.filter((r) => (r.location || 'local') === 'local');
|
|
186
|
+
this._foreignWideSessions = rows.filter((r) => (r.location || 'local') !== 'local');
|
|
187
|
+
this._foreignWideNotes = notes.filter((note) => !isLocalNote(note));
|
|
188
|
+
this._foreignWideAt = Date.now();
|
|
189
|
+
} else {
|
|
190
|
+
this._foreignSessions = rows;
|
|
191
|
+
}
|
|
192
|
+
this._foreignCanScanWide = data.canScanWide === true;
|
|
193
|
+
if (Number.isFinite(data.pollIntervalMs)) this._foreignPollMs = data.pollIntervalMs;
|
|
194
|
+
this.renderAllForeignSessions();
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** Local rows plus whatever the last wide scan found. The render's one input. */
|
|
198
|
+
_allForeignRows() {
|
|
199
|
+
const local = Array.isArray(this._foreignSessions) ? this._foreignSessions : [];
|
|
200
|
+
if (!this.foreignScanRemote()) return local;
|
|
201
|
+
const wide = Array.isArray(this._foreignWideSessions) ? this._foreignWideSessions : [];
|
|
202
|
+
return wide.length ? local.concat(wide) : local;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Adopt one candidate.
|
|
207
|
+
*
|
|
208
|
+
* The in-flight lock is global, one adoption at a time from this page: it only
|
|
209
|
+
* stops a double-click. The server holds the real one-wrapper-per-target
|
|
210
|
+
* guarantee.
|
|
211
|
+
*/
|
|
212
|
+
async adoptForeignSession(id, buttonEl) {
|
|
213
|
+
if (this._foreignAdoptInFlight) return;
|
|
214
|
+
this._foreignAdoptInFlight = true;
|
|
215
|
+
if (buttonEl) {
|
|
216
|
+
buttonEl.disabled = true;
|
|
217
|
+
buttonEl.textContent = foreignT('Opening…');
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const body = { id };
|
|
221
|
+
if (this.foreignScanRemote()) {
|
|
222
|
+
body.docker = true;
|
|
223
|
+
body.remote = true;
|
|
224
|
+
}
|
|
225
|
+
// ⚠️ `_api`, not `_apiJson`: the latter folds a network failure, a non-2xx
|
|
226
|
+
// and `success:false` all into null, and the server deliberately tells
|
|
227
|
+
// two different stories here: "that tmux session is no longer there" vs
|
|
228
|
+
// "could not reach it just now (…), try again". Collapsing them showed a
|
|
229
|
+
// live remote session as deleted every time the link flickered.
|
|
230
|
+
const res = await this._api('/api/sessions/adopt', { method: 'POST', body });
|
|
231
|
+
const payload = await this._readJsonBody(res);
|
|
232
|
+
const session = payload && payload.success !== false ? payload.data?.session : null;
|
|
233
|
+
if (!session) {
|
|
234
|
+
const fallback = foreignT('Could not open that session. Refreshing the list.');
|
|
235
|
+
this.showToast?.(payload?.error || fallback, 'error');
|
|
236
|
+
await this.loadForeignSessions();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const data = { session };
|
|
240
|
+
// Go through the app's normal create path so tab order, lineage lines and
|
|
241
|
+
// SSE-vs-POST ordering behave exactly as they do for a Run.
|
|
242
|
+
this._onSessionCreated?.(data.session);
|
|
243
|
+
await this.selectSession(data.session.id);
|
|
244
|
+
// Still one explicit user action, so a wide row's "Open" refreshes the
|
|
245
|
+
// wide list too, otherwise the row just adopted keeps reading as free.
|
|
246
|
+
await this.loadForeignSessions({ wide: this.foreignScanRemote() });
|
|
247
|
+
} finally {
|
|
248
|
+
this._foreignAdoptInFlight = false;
|
|
249
|
+
if (buttonEl) {
|
|
250
|
+
buttonEl.disabled = false;
|
|
251
|
+
buttonEl.textContent = foreignT('Open');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/** The error envelope, or null when there was no readable body at all. */
|
|
257
|
+
async _readJsonBody(res) {
|
|
258
|
+
if (!res) return null;
|
|
259
|
+
try {
|
|
260
|
+
return await res.json();
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// ═══════════════════════════════════════════════════════════════
|
|
267
|
+
// Render
|
|
268
|
+
// ═══════════════════════════════════════════════════════════════
|
|
269
|
+
|
|
270
|
+
/** Paint every mounted container (welcome + phone overview). */
|
|
271
|
+
renderAllForeignSessions() {
|
|
272
|
+
for (const id of ['foreignSessions', 'mobileForeignSessions']) {
|
|
273
|
+
const el = document.getElementById(id);
|
|
274
|
+
if (el) this.renderForeignSessions(el);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Render into one container. The ONLY row builder, see @fileoverview.
|
|
280
|
+
*
|
|
281
|
+
* Everything is built with DOM APIs and `textContent`: a session name and a
|
|
282
|
+
* command line come from another user's process, so they are never allowed
|
|
283
|
+
* near `innerHTML`.
|
|
284
|
+
*/
|
|
285
|
+
renderForeignSessions(container) {
|
|
286
|
+
const rows = this._allForeignRows();
|
|
287
|
+
container.innerHTML = '';
|
|
288
|
+
if (this._foreignRefused) {
|
|
289
|
+
container.hidden = true;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!rows.length) {
|
|
294
|
+
// Hidden rather than shown-empty: on a first run there is usually nothing,
|
|
295
|
+
// and an empty block on the welcome screen reads as a broken feature.
|
|
296
|
+
// A note is a REASON the list is empty, so a block carrying one must stay
|
|
297
|
+
// visible: hiding it is exactly how "where did my session go" becomes
|
|
298
|
+
// unanswerable.
|
|
299
|
+
const hasNotes =
|
|
300
|
+
(Array.isArray(this._foreignNotes) && this._foreignNotes.length > 0) ||
|
|
301
|
+
(Array.isArray(this._foreignWideNotes) && this._foreignWideNotes.length > 0);
|
|
302
|
+
// ⚠️ `canScanWide` must keep the block visible even with nothing to show.
|
|
303
|
+
// The scan toggle lives in the header, so hiding an empty block also hides
|
|
304
|
+
// the only control that could fill it: on a host with containers but no
|
|
305
|
+
// local tmux sessions that made the whole feature unreachable.
|
|
306
|
+
container.hidden = !this.foreignScanRemote() && !this._foreignError && !hasNotes && !this._foreignCanScanWide;
|
|
307
|
+
if (!container.hidden) {
|
|
308
|
+
container.appendChild(this._foreignHeader(0));
|
|
309
|
+
const empty = document.createElement('div');
|
|
310
|
+
empty.className = 'foreign-empty';
|
|
311
|
+
this._appendForeignNotes(container);
|
|
312
|
+
empty.textContent = this._foreignError
|
|
313
|
+
? foreignT('Could not reach the server.')
|
|
314
|
+
: foreignT('No sessions found outside Codeman.');
|
|
315
|
+
container.appendChild(empty);
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
container.hidden = false;
|
|
321
|
+
container.appendChild(this._foreignHeader(rows.length));
|
|
322
|
+
|
|
323
|
+
this._appendForeignNotes(container);
|
|
324
|
+
|
|
325
|
+
const list = document.createElement('div');
|
|
326
|
+
list.className = 'foreign-list';
|
|
327
|
+
// Adoptable first: an already-open one is not an action, it is a reminder.
|
|
328
|
+
const sorted = rows.slice().sort((a, b) => {
|
|
329
|
+
const aOpen = a.adoptedBy ? 1 : 0;
|
|
330
|
+
const bOpen = b.adoptedBy ? 1 : 0;
|
|
331
|
+
if (aOpen !== bOpen) return aOpen - bOpen;
|
|
332
|
+
return (b.createdAt || 0) - (a.createdAt || 0);
|
|
333
|
+
});
|
|
334
|
+
for (const row of sorted) list.appendChild(this._foreignRow(row));
|
|
335
|
+
container.appendChild(list);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Why a scan produced less than the user expected.
|
|
340
|
+
*
|
|
341
|
+
* Without this, a session skipped for an unsafe name or a host that could not
|
|
342
|
+
* be reached is simply ABSENT, and "my tmux session does not show up" has no
|
|
343
|
+
* answer anywhere in the product. Notes are server-authored strings, rendered
|
|
344
|
+
* as text.
|
|
345
|
+
*/
|
|
346
|
+
_appendForeignNotes(container) {
|
|
347
|
+
const local = Array.isArray(this._foreignNotes) ? this._foreignNotes : [];
|
|
348
|
+
const wide = this.foreignScanRemote() && Array.isArray(this._foreignWideNotes) ? this._foreignWideNotes : [];
|
|
349
|
+
const notes = wide.length ? local.concat(wide) : local;
|
|
350
|
+
if (!notes.length) return;
|
|
351
|
+
const box = document.createElement('div');
|
|
352
|
+
box.className = 'foreign-notes';
|
|
353
|
+
box.setAttribute('data-i18n-skip', '');
|
|
354
|
+
for (const n of notes) {
|
|
355
|
+
const line = document.createElement('div');
|
|
356
|
+
line.className = 'foreign-note';
|
|
357
|
+
line.textContent = n;
|
|
358
|
+
box.appendChild(line);
|
|
359
|
+
}
|
|
360
|
+
container.appendChild(box);
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
_foreignHeader(count) {
|
|
364
|
+
const header = document.createElement('div');
|
|
365
|
+
header.className = 'foreign-header';
|
|
366
|
+
|
|
367
|
+
const title = document.createElement('h3');
|
|
368
|
+
title.className = 'foreign-title';
|
|
369
|
+
title.textContent = foreignT('Opened outside Codeman');
|
|
370
|
+
header.appendChild(title);
|
|
371
|
+
|
|
372
|
+
if (count) {
|
|
373
|
+
const badge = document.createElement('span');
|
|
374
|
+
badge.className = 'foreign-count';
|
|
375
|
+
badge.setAttribute('data-i18n-skip', '');
|
|
376
|
+
badge.textContent = String(count);
|
|
377
|
+
header.appendChild(badge);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const scan = document.createElement('button');
|
|
381
|
+
scan.type = 'button';
|
|
382
|
+
scan.className = 'foreign-scan-toggle';
|
|
383
|
+
scan.dataset.foreignAction = 'toggle-scan';
|
|
384
|
+
scan.setAttribute('aria-pressed', String(this.foreignScanRemote()));
|
|
385
|
+
scan.title = foreignT('Also scan containers and remote hosts (slower)');
|
|
386
|
+
scan.textContent = this.foreignScanRemote() ? '⟳ all' : '⟳ local';
|
|
387
|
+
header.appendChild(scan);
|
|
388
|
+
|
|
389
|
+
// The wide scan is one-shot, so without this button its rows could only
|
|
390
|
+
// ever be refreshed by toggling off and on again.
|
|
391
|
+
if (this.foreignScanRemote()) {
|
|
392
|
+
const rescan = document.createElement('button');
|
|
393
|
+
rescan.type = 'button';
|
|
394
|
+
rescan.className = 'foreign-scan-toggle';
|
|
395
|
+
rescan.dataset.foreignAction = 'rescan-wide';
|
|
396
|
+
rescan.disabled = this._foreignWideLoading === true;
|
|
397
|
+
rescan.title = foreignT('Scan containers and remote hosts again');
|
|
398
|
+
rescan.textContent = this._foreignWideLoading ? foreignT('Scanning…') : foreignT('Rescan');
|
|
399
|
+
header.appendChild(rescan);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return header;
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
_foreignRow(row) {
|
|
406
|
+
const item = document.createElement('div');
|
|
407
|
+
item.className = 'foreign-row';
|
|
408
|
+
if (row.adoptedBy) item.classList.add('foreign-row--open');
|
|
409
|
+
|
|
410
|
+
const dot = document.createElement('span');
|
|
411
|
+
dot.className = `foreign-dot foreign-dot--${row.mode || 'shell'}`;
|
|
412
|
+
item.appendChild(dot);
|
|
413
|
+
|
|
414
|
+
const body = document.createElement('span');
|
|
415
|
+
body.className = 'foreign-row-body';
|
|
416
|
+
|
|
417
|
+
const line1 = document.createElement('span');
|
|
418
|
+
line1.className = 'foreign-row-name';
|
|
419
|
+
line1.setAttribute('data-i18n-skip', '');
|
|
420
|
+
line1.textContent = row.sessionName || '(unnamed)';
|
|
421
|
+
body.appendChild(line1);
|
|
422
|
+
|
|
423
|
+
const line2 = document.createElement('span');
|
|
424
|
+
line2.className = 'foreign-row-sub';
|
|
425
|
+
line2.setAttribute('data-i18n-skip', '');
|
|
426
|
+
const bits = [foreignModeLabel(row.mode)];
|
|
427
|
+
const where = row.hostLabel || foreignT(FOREIGN_LOCATION_LABEL[row.location] || row.location);
|
|
428
|
+
if (where) bits.push(where);
|
|
429
|
+
if (row.workingDir) bits.push(row.workingDir);
|
|
430
|
+
line2.textContent = bits.join(' · ');
|
|
431
|
+
line2.title = row.command || '';
|
|
432
|
+
body.appendChild(line2);
|
|
433
|
+
|
|
434
|
+
item.appendChild(body);
|
|
435
|
+
|
|
436
|
+
const action = document.createElement('button');
|
|
437
|
+
action.type = 'button';
|
|
438
|
+
action.className = 'foreign-open-btn';
|
|
439
|
+
if (row.adoptedBy) {
|
|
440
|
+
action.dataset.foreignAction = 'select';
|
|
441
|
+
action.dataset.foreignSession = row.adoptedBy;
|
|
442
|
+
action.textContent = foreignT('Go to tab');
|
|
443
|
+
} else {
|
|
444
|
+
action.dataset.foreignAction = 'adopt';
|
|
445
|
+
action.dataset.foreignId = row.id;
|
|
446
|
+
action.textContent = foreignT('Open');
|
|
447
|
+
}
|
|
448
|
+
item.appendChild(action);
|
|
449
|
+
|
|
450
|
+
return item;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* One delegated listener per container, wired once. Delegation matters here
|
|
455
|
+
* because the list is fully rebuilt on every poll: per-button listeners would
|
|
456
|
+
* leak one set per tick.
|
|
457
|
+
*/
|
|
458
|
+
wireForeignSessions() {
|
|
459
|
+
if (this._foreignWired) return;
|
|
460
|
+
this._foreignWired = true;
|
|
461
|
+
document.addEventListener('click', (event) => {
|
|
462
|
+
const el = event.target?.closest?.('[data-foreign-action]');
|
|
463
|
+
if (!el) return;
|
|
464
|
+
const action = el.dataset.foreignAction;
|
|
465
|
+
if (action === 'adopt') {
|
|
466
|
+
void this.adoptForeignSession(el.dataset.foreignId, el);
|
|
467
|
+
} else if (action === 'select') {
|
|
468
|
+
void this.selectSession(el.dataset.foreignSession);
|
|
469
|
+
} else if (action === 'toggle-scan') {
|
|
470
|
+
this.toggleForeignScanRemote();
|
|
471
|
+
} else if (action === 'rescan-wide') {
|
|
472
|
+
this.rescanForeignWide();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
});
|
|
Binary file
|
|
Binary file
|
|
@@ -425,9 +425,11 @@ Object.assign(CodemanApp.prototype, {
|
|
|
425
425
|
overview.state === 'not-a-repo'
|
|
426
426
|
? 'No git repository here: this session’s folder is not one, and none was found inside it (up to two levels down).'
|
|
427
427
|
: overview.state === 'unsupported'
|
|
428
|
-
? overview.reason === '
|
|
429
|
-
? 'Git status is not available for
|
|
430
|
-
:
|
|
428
|
+
? overview.reason === 'adopted'
|
|
429
|
+
? 'Git status is not available for a tmux session opened outside Codeman.'
|
|
430
|
+
: overview.reason === 'docker'
|
|
431
|
+
? 'Git status is not available for Docker sessions, or for folders inside a Docker case workspace.'
|
|
432
|
+
: 'Git status is not available for remote (SSH) sessions.'
|
|
431
433
|
: `Could not read the repository: ${overview.error || 'git failed'}`;
|
|
432
434
|
body.append(el('div', 'git-status-empty', why));
|
|
433
435
|
clearChrome();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|