termdock 1.4.130 → 1.4.132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/client/assets/{ModelPreview-CWRqYrs3.js → ModelPreview-xIMB1gci.js} +1 -1
  2. package/dist/client/assets/{abnfDiagram-VRR7QNED-DmhLAHjm.js → abnfDiagram-VRR7QNED-eBFVeh4Q.js} +1 -1
  3. package/dist/client/assets/{arc-1HhVZ8o_.js → arc-DqGUkd0h.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-C6DRI6j7.js → architectureDiagram-ZJ3FMSHR-CtTvgRQg.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-677ZJIJ3-yWWx0IRp.js → blockDiagram-677ZJIJ3-B2oYlFoa.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-LMCZKHZV-CxFRqj7g.js → c4Diagram-LMCZKHZV-Dq6gd7UH.js} +1 -1
  7. package/dist/client/assets/channel-DX0MaZfr.js +1 -0
  8. package/dist/client/assets/{chunk-2Q5K7J3B-DiLHSUvK.js → chunk-2Q5K7J3B-Dh2wvvmq.js} +1 -1
  9. package/dist/client/assets/{chunk-32BRIVSS-BHUhbE-j.js → chunk-32BRIVSS-CaGo9PVx.js} +1 -1
  10. package/dist/client/assets/{chunk-5VM5RSS4-D8RUQOJJ.js → chunk-5VM5RSS4-DuuYWFWH.js} +1 -1
  11. package/dist/client/assets/{chunk-EX3LRPZG-DbCvvyES.js → chunk-EX3LRPZG-CAgvAwHI.js} +1 -1
  12. package/dist/client/assets/{chunk-JWPE2WC7-Bfmi19tX.js → chunk-JWPE2WC7-DOZPKXr-.js} +1 -1
  13. package/dist/client/assets/{chunk-MOJQB5TN-CIPAOJ3Z.js → chunk-MOJQB5TN-CNpQHgVD.js} +1 -1
  14. package/dist/client/assets/{chunk-RYQCIY6F-Dv26f911.js → chunk-RYQCIY6F-BPaHtABV.js} +1 -1
  15. package/dist/client/assets/{chunk-V7JOEXUC-BwYdTxxm.js → chunk-V7JOEXUC-NX3abn8T.js} +1 -1
  16. package/dist/client/assets/{chunk-VR4S4FIN-B9OzWHSy.js → chunk-VR4S4FIN-BxjIyRMT.js} +1 -1
  17. package/dist/client/assets/{chunk-XXDRQBXY-DhNCpjc0.js → chunk-XXDRQBXY-BuS0NHo0.js} +1 -1
  18. package/dist/client/assets/classDiagram-OUVF2IWQ-DGaq_x0x.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DGaq_x0x.js +1 -0
  20. package/dist/client/assets/{cose-bilkent-JH36ORCC-CH4J7-sj.js → cose-bilkent-JH36ORCC-DWbgHp1l.js} +1 -1
  21. package/dist/client/assets/{cynefin-VYW2F7L2-CNXXGzMQ.js → cynefin-VYW2F7L2-WxauVl6G.js} +1 -1
  22. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-2m8SlJ8B.js → cynefinDiagram-TSTJHNR4-BkoOZTFt.js} +1 -1
  23. package/dist/client/assets/{dagre-VKFMJZFB-B0SJJBJf.js → dagre-VKFMJZFB-bJ1Gd6jt.js} +1 -1
  24. package/dist/client/assets/{diagram-FQU43EPY-CM5-n6VV.js → diagram-FQU43EPY-C7lstq4q.js} +1 -1
  25. package/dist/client/assets/{diagram-G47NLZAW-BcrjleGT.js → diagram-G47NLZAW-CrEO4kM7.js} +1 -1
  26. package/dist/client/assets/{diagram-NH7WQ7WH-Bgr-DpEE.js → diagram-NH7WQ7WH-rik8NVse.js} +1 -1
  27. package/dist/client/assets/{diagram-OA4YK3LP-CIIR6wG7.js → diagram-OA4YK3LP-DSTOMqY-.js} +1 -1
  28. package/dist/client/assets/{diagram-WEI45ONY-N9F8sTNj.js → diagram-WEI45ONY-BO4F0yvc.js} +1 -1
  29. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DOnf9qid.js → ebnfDiagram-CCIWWBDH-FM1SO3Rz.js} +1 -1
  30. package/dist/client/assets/{erDiagram-Q63AITRT-DEiBoeG7.js → erDiagram-Q63AITRT-d7PNAH1d.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-23GEKE2U-B3bDnU-U.js → flowDiagram-23GEKE2U-HcnGJZyD.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-NO4QXBWP-f2t9atKu.js → ganttDiagram-NO4QXBWP-B_2eDiGU.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CUSsJhQW.js → gitGraphDiagram-IHSO6WYX-BEUx6JTA.js} +1 -1
  34. package/dist/client/assets/index-DJbO1cOV.js +631 -0
  35. package/dist/client/assets/{infoDiagram-FWYZ7A6U-DNgpMfOB.js → infoDiagram-FWYZ7A6U-o2XMkkgh.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-Bboc2-i6.js → ishikawaDiagram-FXEZZL3T-iW307QEg.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-5HDEW3XC-BXO2B4dQ.js → journeyDiagram-5HDEW3XC-BY25d5te.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-HUTT4EX6-Cmi1QYgY.js → kanban-definition-HUTT4EX6-CmB_pI7U.js} +1 -1
  39. package/dist/client/assets/{linear-DziuYRxh.js → linear-3xGaKGxN.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-BjZ5LK9C.js → mermaid.core-CAhvQysP.js} +4 -4
  41. package/dist/client/assets/{mindmap-definition-LN4V7U3C-cC33ypgg.js → mindmap-definition-LN4V7U3C-Cze4acOy.js} +1 -1
  42. package/dist/client/assets/{pegDiagram-2B236MQR-BmsTQ-0x.js → pegDiagram-2B236MQR-Csq4rLtQ.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-ENE6RG2P-B1qWnnLQ.js → pieDiagram-ENE6RG2P-DYOKoI_3.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DBXqWc58.js → quadrantDiagram-ABIIQ3AL-B_vAzn4r.js} +1 -1
  45. package/dist/client/assets/{railroadDiagram-RFXS5EU6-CsaUAxs4.js → railroadDiagram-RFXS5EU6-BvpuLHee.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-TGXJPOKE-TW7I0rAq.js → requirementDiagram-TGXJPOKE-RuA-sUmf.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-BHGgMFaC.js → sankeyDiagram-HTMAVEWB-BUXylWCj.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-RKAuMGPN.js → sequenceDiagram-DBY2YBRQ-AwIzUbeT.js} +1 -1
  49. package/dist/client/assets/{sizeCapture-X5ZJPWSS-gDMHFdCz.js → sizeCapture-X5ZJPWSS-Pf8HegXQ.js} +1 -1
  50. package/dist/client/assets/{stateDiagram-2N3HPSRC-eJR-FdYD.js → stateDiagram-2N3HPSRC-DM-4V3vF.js} +1 -1
  51. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CPn90WmF.js +1 -0
  52. package/dist/client/assets/{swimlanes-5IMT3BWC-BxUGW4OG.js → swimlanes-5IMT3BWC-C1FwQoin.js} +2 -2
  53. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CYaJCN3S.js +8 -0
  54. package/dist/client/assets/{timeline-definition-FHXFAJF6-K7UoQ_OF.js → timeline-definition-FHXFAJF6-CjsGCC_J.js} +1 -1
  55. package/dist/client/assets/{vennDiagram-L72KCM5P-BVoARFlW.js → vennDiagram-L72KCM5P-Cm6UHJnD.js} +1 -1
  56. package/dist/client/assets/{wardleyDiagram-EHGQE667-D8sZauvy.js → wardleyDiagram-EHGQE667-a4EEjtsO.js} +1 -1
  57. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CTTS4EdC.js → xychartDiagram-FW5EYKEG-CphZIXn9.js} +1 -1
  58. package/dist/client/index.html +1 -1
  59. package/dist/client/sw.js +1 -1
  60. package/dist/server/cli.js +8 -6
  61. package/dist/server/entry.js +5 -1
  62. package/dist/server/routes/terminal.js +60 -31
  63. package/dist/server/utils/settings.js +110 -0
  64. package/dist/server/utils/tmuxRecoveryCandidate.js +11 -0
  65. package/package.json +1 -1
  66. package/runtime-manifest.json +2 -2
  67. package/dist/client/assets/channel-BhCsHnkc.js +0 -1
  68. package/dist/client/assets/classDiagram-OUVF2IWQ-BhvLQDyG.js +0 -1
  69. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BhvLQDyG.js +0 -1
  70. package/dist/client/assets/index-CH4So_5W.js +0 -631
  71. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-B6ADeGOu.js +0 -1
  72. package/dist/client/assets/swimlanesDiagram-G3AALYLV-COTh3HeD.js +0 -8
@@ -15,7 +15,7 @@ import { getPtyHostManager } from '../ptyhost/manager.js';
15
15
  import { pathValidator } from '../utils/pathValidator.js';
16
16
  import { TERMINAL, TMUX } from '../config.js';
17
17
  import { localAccessManager } from '../utils/localAccess.js';
18
- import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, getRunningSessionButtonEnabledSetting, setRunningSessionButtonEnabledSetting, getFileSortModesSetting, setFileSortModesSetting, setFileSortModeSetting, } from '../utils/settings.js';
18
+ import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, getRunningSessionButtonEnabledSetting, setRunningSessionButtonEnabledSetting, getFileSortModesSetting, setFileSortModesSetting, setFileSortModeSetting, getPinnedExplorerRootsSetting, setPinnedExplorerRootsSetting, setPinnedExplorerRootSetting, watchPinnedExplorerRootsSetting, } from '../utils/settings.js';
19
19
  import { loadContextDraft, saveContextDraft } from '../utils/contextDraft.js';
20
20
  import { getOnboardingServerUrl } from '../onboardingServer.js';
21
21
  import { inspectLinuxInotifyUsage, isWatchResourceExhaustion } from '../utils/fileWatchPolicy.js';
@@ -25,6 +25,7 @@ import { agentBySlug, buildResumeCommand, detectAgentFromCommand, inferResumeSes
25
25
  import { applyAgentEvent, agentEventMatchesCurrentAgent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
26
26
  import { canRestoreDeadAgentShell, normalizePersistedAgentResumeInfo, } from '../agent/resumePersistence.js';
27
27
  import { AgentResumeHistoryStore } from '../agent/resumeHistory.js';
28
+ import { isTmuxRecoveryCandidate } from '../utils/tmuxRecoveryCandidate.js';
28
29
  import { AutomationStore, normalizeAutomationSchedule } from '../agent/automationStore.js';
29
30
  import { buildBracketedSubmitBytes, canDeliverPromptToAgent } from '../agent/promptDelivery.js';
30
31
  import { CollaborationStore } from '../agent/collaborationStore.js';
@@ -104,6 +105,7 @@ const TERMDOCK_VERSION = (() => {
104
105
  const TERMDOCK_HOST = os.hostname();
105
106
  const TERMDOCK_PID = String(process.pid);
106
107
  const TERMDOCK_GUI_DETACHED_AT_OPTION = '@termdock-gui-detached-at';
108
+ const TERMDOCK_FRONTEND_SESSION_ID_OPTION = '@termdock-frontend-session-id';
107
109
  // WebSocket clients per session (separate from SSE clients).
108
110
  const wsClients = new Map();
109
111
  // Sessions where copy-mode -e just auto-exited at the bottom.
@@ -234,6 +236,17 @@ function sendClientStatePayload(payload) {
234
236
  function broadcastControlEvent(payload) {
235
237
  sendClientStatePayload(JSON.stringify(payload));
236
238
  }
239
+ // Atomic settings writes replace the inode, so the utility watches the parent
240
+ // directory. Besides this process's own writes, it observes sibling Termdock
241
+ // server processes that share ~/.termdock and relays their pin list here.
242
+ watchPinnedExplorerRootsSetting((pinnedExplorerRoots) => {
243
+ broadcastControlEvent({
244
+ type: 'pinned-explorer-roots',
245
+ pinnedExplorerRoots,
246
+ updatedAt: Date.now(),
247
+ origin: null,
248
+ });
249
+ });
237
250
  let serverRestartScheduled = false;
238
251
  function requestServerRestartAfterUpdate() {
239
252
  if (serverRestartScheduled)
@@ -614,6 +627,7 @@ async function backfillPersistedTmuxMetadata() {
614
627
  '@termdock-version': TERMDOCK_VERSION,
615
628
  '@termdock-host': TERMDOCK_HOST,
616
629
  '@termdock-pid': TERMDOCK_PID,
630
+ [TERMDOCK_FRONTEND_SESSION_ID_OPTION]: s.sessionId,
617
631
  // Agent-hook plumbing (see ensureManagedTmuxSessionReady): backfilled
618
632
  // at startup so sessions created before this feature get it too.
619
633
  'allow-passthrough': 'on',
@@ -841,7 +855,7 @@ function parseNumberOption(raw) {
841
855
  return Number.isFinite(value) ? value : null;
842
856
  }
843
857
  function isTermdockManagedTmuxSession(session) {
844
- return !!(session.version || session.host || session.pid || session.createdAt || session.lastActiveAt || session.label || session.program || session.cwd || session.guiDetachedAt);
858
+ return !!(session.version || session.host || session.pid || session.createdAt || session.lastActiveAt || session.label || session.program || session.cwd || session.guiDetachedAt || session.sourceFrontendSessionId);
845
859
  }
846
860
  function normalizeMetadataProgram(program) {
847
861
  return normalizeTmuxMetadataProgram(program, { shellNames: shellNamesBackend });
@@ -1221,6 +1235,7 @@ async function openInventorySession(req, input) {
1221
1235
  '@termdock-version': TERMDOCK_VERSION,
1222
1236
  '@termdock-host': TERMDOCK_HOST,
1223
1237
  '@termdock-pid': TERMDOCK_PID,
1238
+ [TERMDOCK_FRONTEND_SESSION_ID_OPTION]: savedRecord.sessionId,
1224
1239
  });
1225
1240
  }
1226
1241
  const inventory = await getSessionInventorySnapshot({ refresh: true });
@@ -1817,6 +1832,7 @@ async function listLiveTmuxInventorySessions() {
1817
1832
  '#{@termdock-created-at}',
1818
1833
  '#{@termdock-last-active-at}',
1819
1834
  `#{${TERMDOCK_GUI_DETACHED_AT_OPTION}}`,
1835
+ `#{${TERMDOCK_FRONTEND_SESSION_ID_OPTION}}`,
1820
1836
  ].join(TMUX_DELIMITER);
1821
1837
  try {
1822
1838
  const raw = await runTmux(['list-sessions', '-F', format]);
@@ -1824,9 +1840,9 @@ async function listLiveTmuxInventorySessions() {
1824
1840
  .split('\n')
1825
1841
  .map((line) => line.trim())
1826
1842
  .filter(Boolean)
1827
- .map((line) => parseDelimitedRow(line, 14))
1843
+ .map((line) => parseDelimitedRow(line, 15))
1828
1844
  .filter((row) => row !== null)
1829
- .map(([name, windowsRaw, attachedRaw, friendlyName, program, cwd, label, clientCountRaw, host, pidRaw, version, createdAtRaw, lastActiveAtRaw, guiDetachedAtRaw,]) => ({
1845
+ .map(([name, windowsRaw, attachedRaw, friendlyName, program, cwd, label, clientCountRaw, host, pidRaw, version, createdAtRaw, lastActiveAtRaw, guiDetachedAtRaw, sourceFrontendSessionId,]) => ({
1830
1846
  name,
1831
1847
  windows: Number.parseInt(windowsRaw || '0', 10) || 0,
1832
1848
  attachedCount: Number.parseInt(attachedRaw || '0', 10) || 0,
@@ -1841,6 +1857,7 @@ async function listLiveTmuxInventorySessions() {
1841
1857
  createdAt: parseNumberOption(createdAtRaw),
1842
1858
  lastActiveAt: parseNumberOption(lastActiveAtRaw),
1843
1859
  guiDetachedAt: parseNumberOption(guiDetachedAtRaw),
1860
+ sourceFrontendSessionId: sourceFrontendSessionId || null,
1844
1861
  }))
1845
1862
  .sort((a, b) => {
1846
1863
  const aCreated = a.createdAt ?? Number.POSITIVE_INFINITY;
@@ -1976,32 +1993,6 @@ async function buildSessionInventory() {
1976
1993
  return tmux;
1977
1994
  }
1978
1995
  }));
1979
- let discoveredManagedTmuxSession = false;
1980
- for (const tmux of refreshedTmuxSessions) {
1981
- if (!isTermdockManagedTmuxSession(tmux))
1982
- continue;
1983
- if (tmux.guiDetachedAt !== null)
1984
- continue;
1985
- const exists = globalSessionState.sessions.some((session) => session.mode === 'tmux' && session.tmuxSessionName === tmux.name);
1986
- if (exists)
1987
- continue;
1988
- const liveBackend = findBackendSessionForTmux(tmux.name);
1989
- const friendlyName = tmux.friendlyName?.trim() || null;
1990
- upsertGlobalSessionRecord({
1991
- sessionId: randomUUID(),
1992
- name: friendlyName ?? `tmux:${tmux.name}`,
1993
- customName: friendlyName ? true : undefined,
1994
- backendSessionId: liveBackend?.[0] ?? null,
1995
- mode: 'tmux',
1996
- tmuxSessionName: tmux.name,
1997
- createdAt: tmux.createdAt ?? Date.now(),
1998
- lastActivity: tmux.lastActiveAt ?? Date.now(),
1999
- });
2000
- discoveredManagedTmuxSession = true;
2001
- }
2002
- if (discoveredManagedTmuxSession) {
2003
- schedulePersistGlobalState();
2004
- }
2005
1996
  const liveTmuxByName = new Map(refreshedTmuxSessions.map((session) => [session.name, session]));
2006
1997
  let synchronizedPersistedFields = false;
2007
1998
  globalSessionState = {
@@ -2101,6 +2092,12 @@ async function buildSessionInventory() {
2101
2092
  connected: bound?.connected === true,
2102
2093
  live: true,
2103
2094
  restorable: bound?.restorable === true,
2095
+ recoveryCandidate: isTmuxRecoveryCandidate({
2096
+ managedByTermdock: isTermdockManagedTmuxSession(tmux),
2097
+ sourceFrontendSessionId: tmux.sourceFrontendSessionId,
2098
+ guiDetachedAt: tmux.guiDetachedAt,
2099
+ boundFrontendSessionId: bound?.sessionId ?? null,
2100
+ }),
2104
2101
  friendlyName: tmux.friendlyName,
2105
2102
  label: tmux.label,
2106
2103
  program: tmux.program,
@@ -4855,6 +4852,7 @@ router.get('/tmux/sessions', async (_req, res) => {
4855
4852
  connected: session.connected,
4856
4853
  live: session.live,
4857
4854
  restorable: session.restorable,
4855
+ recoveryCandidate: session.recoveryCandidate,
4858
4856
  friendlyName: session.friendlyName,
4859
4857
  label: session.label,
4860
4858
  program: session.program,
@@ -5600,6 +5598,7 @@ async function getSettingsPayload() {
5600
5598
  newSessionAgentSlug: getNewSessionAgentSlugSetting(),
5601
5599
  runningSessionButtonEnabled: getRunningSessionButtonEnabledSetting(),
5602
5600
  fileSortModes: getFileSortModesSetting(),
5601
+ pinnedExplorerRoots: getPinnedExplorerRootsSetting(),
5603
5602
  localAccess: {
5604
5603
  ...localAccess,
5605
5604
  interfaces,
@@ -5641,6 +5640,7 @@ router.post('/update/restart', async (_req, res) => {
5641
5640
  });
5642
5641
  router.put('/settings', async (req, res) => {
5643
5642
  const body = req.body ?? {};
5643
+ let pinnedExplorerRootsChanged = false;
5644
5644
  if (typeof body.preventSleep === 'boolean') {
5645
5645
  caffeinateManager.setPreventSleep(body.preventSleep);
5646
5646
  }
@@ -5735,6 +5735,26 @@ router.put('/settings', async (req, res) => {
5735
5735
  }
5736
5736
  setFileSortModeSetting(preference.path, preference.mode);
5737
5737
  }
5738
+ if (body.pinnedExplorerRoots && typeof body.pinnedExplorerRoots === 'object' && !Array.isArray(body.pinnedExplorerRoots)) {
5739
+ setPinnedExplorerRootsSetting(body.pinnedExplorerRoots);
5740
+ pinnedExplorerRootsChanged = true;
5741
+ }
5742
+ if (body.pinnedExplorerRoot && typeof body.pinnedExplorerRoot === 'object') {
5743
+ const mutation = body.pinnedExplorerRoot;
5744
+ const validAbsolutePath = (value) => typeof value === 'string'
5745
+ && value.length > 0
5746
+ && value.length <= 4096
5747
+ && (value.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(value));
5748
+ if (!validAbsolutePath(mutation.rootPath)
5749
+ || !validAbsolutePath(mutation.path)
5750
+ || (mutation.kind !== 'file' && mutation.kind !== 'directory')
5751
+ || typeof mutation.pinned !== 'boolean') {
5752
+ res.status(400).json({ error: 'Invalid pinned explorer entry', code: 'PINNED_EXPLORER_ROOT_INVALID' });
5753
+ return;
5754
+ }
5755
+ setPinnedExplorerRootSetting(mutation.rootPath, mutation.path, mutation.kind, mutation.pinned);
5756
+ pinnedExplorerRootsChanged = true;
5757
+ }
5738
5758
  if (body.localAccess && typeof body.localAccess === 'object') {
5739
5759
  const localAccessBody = body.localAccess;
5740
5760
  if (localAccessBody.reset === true) {
@@ -5753,7 +5773,16 @@ router.put('/settings', async (req, res) => {
5753
5773
  }
5754
5774
  }
5755
5775
  }
5756
- res.json(await getSettingsPayload());
5776
+ const payload = await getSettingsPayload();
5777
+ if (pinnedExplorerRootsChanged) {
5778
+ broadcastControlEvent({
5779
+ type: 'pinned-explorer-roots',
5780
+ pinnedExplorerRoots: payload.pinnedExplorerRoots,
5781
+ updatedAt: Date.now(),
5782
+ origin: typeof body.pinnedExplorerRootsOrigin === 'string' ? body.pinnedExplorerRootsOrigin : null,
5783
+ });
5784
+ }
5785
+ res.json(payload);
5757
5786
  });
5758
5787
  // ── Context draft (cross-device realtime sync) ────────────────────────
5759
5788
  // 草稿内容存服务端(~/.termdock/context-draft.json),PUT 后通过 control
@@ -70,6 +70,39 @@ export function normalizeFileSortModes(value) {
70
70
  && (filePath.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(filePath))))
71
71
  .slice(-500));
72
72
  }
73
+ function isAbsoluteFilePath(value) {
74
+ return value.length > 0
75
+ && value.length <= 4096
76
+ && (value.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(value));
77
+ }
78
+ export function normalizePinnedExplorerRoots(value) {
79
+ if (!value || typeof value !== 'object' || Array.isArray(value))
80
+ return {};
81
+ const normalized = {};
82
+ for (const [rootPath, rawEntries] of Object.entries(value).slice(-200)) {
83
+ if (!isAbsoluteFilePath(rootPath) || !Array.isArray(rawEntries))
84
+ continue;
85
+ const seen = new Set();
86
+ const entries = [];
87
+ for (const rawEntry of rawEntries) {
88
+ if (!rawEntry || typeof rawEntry !== 'object' || Array.isArray(rawEntry))
89
+ continue;
90
+ const entry = rawEntry;
91
+ if (typeof entry.path !== 'string'
92
+ || !isAbsoluteFilePath(entry.path)
93
+ || (entry.kind !== 'file' && entry.kind !== 'directory')
94
+ || seen.has(entry.path))
95
+ continue;
96
+ seen.add(entry.path);
97
+ entries.push({ path: entry.path, kind: entry.kind });
98
+ if (entries.length >= 12)
99
+ break;
100
+ }
101
+ if (entries.length > 0)
102
+ normalized[rootPath] = entries;
103
+ }
104
+ return normalized;
105
+ }
73
106
  function normalizeSettings(value) {
74
107
  const raw = value && typeof value === 'object' && !Array.isArray(value)
75
108
  ? value
@@ -117,6 +150,7 @@ function normalizeSettings(value) {
117
150
  newSessionAgentSlug: normalizeNewSessionAgentSlug(raw.newSessionAgentSlug),
118
151
  runningSessionButtonEnabled: raw.runningSessionButtonEnabled === true,
119
152
  fileSortModes: normalizeFileSortModes(raw.fileSortModes),
153
+ pinnedExplorerRoots: normalizePinnedExplorerRoots(raw.pinnedExplorerRoots),
120
154
  updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : Date.now(),
121
155
  };
122
156
  }
@@ -437,3 +471,79 @@ export function setFileSortModeSetting(filePath, mode) {
437
471
  settings.fileSortModes = normalizeFileSortModes(next);
438
472
  });
439
473
  }
474
+ export function getPinnedExplorerRootsSetting() {
475
+ return loadSettings().pinnedExplorerRoots;
476
+ }
477
+ export function setPinnedExplorerRootsSetting(roots) {
478
+ return updateSettings((settings) => {
479
+ settings.pinnedExplorerRoots = normalizePinnedExplorerRoots(roots);
480
+ });
481
+ }
482
+ export function setPinnedExplorerRootSetting(rootPath, entryPath, kind, pinned) {
483
+ return updateSettings((settings) => {
484
+ const roots = { ...settings.pinnedExplorerRoots };
485
+ const current = roots[rootPath] ?? [];
486
+ if (pinned) {
487
+ if (!current.some((entry) => entry.path === entryPath)) {
488
+ roots[rootPath] = [{ path: entryPath, kind }, ...current].slice(0, 12);
489
+ }
490
+ }
491
+ else {
492
+ const next = current.filter((entry) => entry.path !== entryPath);
493
+ if (next.length > 0)
494
+ roots[rootPath] = next;
495
+ else
496
+ delete roots[rootPath];
497
+ }
498
+ settings.pinnedExplorerRoots = normalizePinnedExplorerRoots(roots);
499
+ });
500
+ }
501
+ /**
502
+ * Watch the settings file through its parent directory because atomic saves
503
+ * replace the file inode. This lets sibling Termdock server processes sharing
504
+ * the same home directory relay pin changes to their own connected clients.
505
+ */
506
+ export function watchPinnedExplorerRootsSetting(listener, settingsFile = SETTINGS_FILE) {
507
+ const initial = loadSettingsFile(settingsFile);
508
+ let previous = JSON.stringify(initial.pinnedExplorerRoots);
509
+ let timer = null;
510
+ let closed = false;
511
+ const refresh = () => {
512
+ timer = null;
513
+ if (closed)
514
+ return;
515
+ try {
516
+ const roots = loadSettingsFile(settingsFile).pinnedExplorerRoots;
517
+ const serialized = JSON.stringify(roots);
518
+ if (serialized === previous)
519
+ return;
520
+ previous = serialized;
521
+ listener(roots);
522
+ }
523
+ catch (error) {
524
+ console.warn('[settings] failed to refresh pinned explorer roots:', error);
525
+ }
526
+ };
527
+ let watcher;
528
+ try {
529
+ watcher = fs.watch(path.dirname(settingsFile), (_eventType, filename) => {
530
+ if (filename && filename.toString() !== path.basename(settingsFile))
531
+ return;
532
+ if (timer)
533
+ clearTimeout(timer);
534
+ timer = setTimeout(refresh, 40);
535
+ timer.unref?.();
536
+ });
537
+ watcher.unref();
538
+ }
539
+ catch (error) {
540
+ console.warn('[settings] failed to watch pinned explorer roots:', error);
541
+ return () => undefined;
542
+ }
543
+ return () => {
544
+ closed = true;
545
+ if (timer)
546
+ clearTimeout(timer);
547
+ watcher.close();
548
+ };
549
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A recovery entry is a Termdock GUI session whose frontend record vanished
3
+ * while its tmux process survived. Plain tmux sessions, CLI-created detached
4
+ * sessions, and sessions the user explicitly detached are not recoverable.
5
+ */
6
+ export function isTmuxRecoveryCandidate({ managedByTermdock, sourceFrontendSessionId, guiDetachedAt, boundFrontendSessionId, }) {
7
+ return managedByTermdock
8
+ && sourceFrontendSessionId !== null
9
+ && guiDetachedAt === null
10
+ && boundFrontendSessionId === null;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.130",
3
+ "version": "1.4.132",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.130",
4
+ "version": "1.4.132",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-3I9X7zF4J4BUYOvxi3lIuWSKfDy6S6GD61w8Ay95Ypw=",
9
- "serverBundleHash": "sha256-KCwumftUV9M9Hc3M/FVXnKudXTFT73UU7xByvp81cfM=",
9
+ "serverBundleHash": "sha256-ZCOkjm9hxiF2LH/SdwOiZM7pJUBM08L6PIA90iM0yaE=",
10
10
  "entrypoint": "dist/server/cli.js",
11
11
  "clientEntrypoint": "dist/client/index.html"
12
12
  }
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-BjZ5LK9C.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-BwYdTxxm.js";import{b as i}from"./mermaid.core-BjZ5LK9C.js";import"./chunk-5VM5RSS4-D8RUQOJJ.js";import"./chunk-XXDRQBXY-DhNCpjc0.js";import"./chunk-VR4S4FIN-B9OzWHSy.js";import"./chunk-32BRIVSS-BHUhbE-j.js";import"./purify.es-CC4Brkr_.js";import"./index-CH4So_5W.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-BwYdTxxm.js";import{b as i}from"./mermaid.core-BjZ5LK9C.js";import"./chunk-5VM5RSS4-D8RUQOJJ.js";import"./chunk-XXDRQBXY-DhNCpjc0.js";import"./chunk-VR4S4FIN-B9OzWHSy.js";import"./chunk-32BRIVSS-BHUhbE-j.js";import"./purify.es-CC4Brkr_.js";import"./index-CH4So_5W.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};