termdock 1.4.131 → 1.4.133

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 (73) hide show
  1. package/dist/client/assets/{ModelPreview-BRdf66iZ.js → ModelPreview-BE_DJvsb.js} +1 -1
  2. package/dist/client/assets/{abnfDiagram-VRR7QNED-DQ-0BDwp.js → abnfDiagram-VRR7QNED-DDVHljsN.js} +1 -1
  3. package/dist/client/assets/{arc-DB996Jer.js → arc-D6pBXFrr.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B6NsgBfh.js → architectureDiagram-ZJ3FMSHR-p9hcK6Ad.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-677ZJIJ3-Do66IX7G.js → blockDiagram-677ZJIJ3-B1urT6p3.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-LMCZKHZV-DFbyVO2f.js → c4Diagram-LMCZKHZV-Dpe5RReJ.js} +1 -1
  7. package/dist/client/assets/channel-BRiTV3DL.js +1 -0
  8. package/dist/client/assets/{chunk-2Q5K7J3B-Dh_AXqCF.js → chunk-2Q5K7J3B-C-tD3SG_.js} +1 -1
  9. package/dist/client/assets/{chunk-32BRIVSS-yne3sS0_.js → chunk-32BRIVSS-D0MIswPl.js} +1 -1
  10. package/dist/client/assets/{chunk-5VM5RSS4-CecVqJ7p.js → chunk-5VM5RSS4-DAI_IFTQ.js} +1 -1
  11. package/dist/client/assets/{chunk-EX3LRPZG-eRs2LgWQ.js → chunk-EX3LRPZG-BjO-2Ere.js} +1 -1
  12. package/dist/client/assets/{chunk-JWPE2WC7-BBZTbiyT.js → chunk-JWPE2WC7-Tf0wkqSP.js} +1 -1
  13. package/dist/client/assets/{chunk-MOJQB5TN-DuFkjAz7.js → chunk-MOJQB5TN-CzaSpUfp.js} +1 -1
  14. package/dist/client/assets/{chunk-RYQCIY6F-AxfTNIzW.js → chunk-RYQCIY6F-vDS4WcRF.js} +1 -1
  15. package/dist/client/assets/{chunk-V7JOEXUC-CihoNZl2.js → chunk-V7JOEXUC-ChDJI57Y.js} +1 -1
  16. package/dist/client/assets/{chunk-VR4S4FIN-CW3x8SlE.js → chunk-VR4S4FIN-BViPjRi0.js} +1 -1
  17. package/dist/client/assets/{chunk-XXDRQBXY-CVtb44LI.js → chunk-XXDRQBXY-BYNNBhi1.js} +1 -1
  18. package/dist/client/assets/classDiagram-OUVF2IWQ-DnaJPSQT.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DnaJPSQT.js +1 -0
  20. package/dist/client/assets/{cose-bilkent-JH36ORCC-DVjHLC29.js → cose-bilkent-JH36ORCC-NRzzu38i.js} +1 -1
  21. package/dist/client/assets/{cynefin-VYW2F7L2-J2WM-6GK.js → cynefin-VYW2F7L2-A1cOSvjC.js} +1 -1
  22. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DKaXiodq.js → cynefinDiagram-TSTJHNR4-D89LBcJA.js} +1 -1
  23. package/dist/client/assets/{dagre-VKFMJZFB-LVS1YgSh.js → dagre-VKFMJZFB-BvNcNIYu.js} +1 -1
  24. package/dist/client/assets/{diagram-FQU43EPY-de576wbx.js → diagram-FQU43EPY-BEleLyaK.js} +1 -1
  25. package/dist/client/assets/{diagram-G47NLZAW-BLxjsybw.js → diagram-G47NLZAW-EH-vodG7.js} +1 -1
  26. package/dist/client/assets/{diagram-NH7WQ7WH-Dxv-Zg98.js → diagram-NH7WQ7WH-BviXs1Rk.js} +1 -1
  27. package/dist/client/assets/{diagram-OA4YK3LP-Mgc11w-m.js → diagram-OA4YK3LP-CBSEzkwZ.js} +1 -1
  28. package/dist/client/assets/{diagram-WEI45ONY-Baa0EBra.js → diagram-WEI45ONY-D53fsZH_.js} +1 -1
  29. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-BhTSdJNM.js → ebnfDiagram-CCIWWBDH-fmFV_0A6.js} +1 -1
  30. package/dist/client/assets/{erDiagram-Q63AITRT-CVtTjVGi.js → erDiagram-Q63AITRT-DkfQmoKL.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-23GEKE2U-DGPww_Jl.js → flowDiagram-23GEKE2U-BhD66TbS.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-NO4QXBWP-DshJtOOJ.js → ganttDiagram-NO4QXBWP-D8PCGrIp.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CSTs_Nee.js → gitGraphDiagram-IHSO6WYX-DxDbg6Z9.js} +1 -1
  34. package/dist/client/assets/index-iKTEk-ec.js +631 -0
  35. package/dist/client/assets/{infoDiagram-FWYZ7A6U-DGiFmvwN.js → infoDiagram-FWYZ7A6U-B4m3yZCU.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-DlD-mPMi.js → ishikawaDiagram-FXEZZL3T-DzuDKD39.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-5HDEW3XC-CIjD41_7.js → journeyDiagram-5HDEW3XC-B28ab3y9.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-HUTT4EX6-B9e14Q0L.js → kanban-definition-HUTT4EX6-DjuLbFdG.js} +1 -1
  39. package/dist/client/assets/{linear-NhwKYgiu.js → linear-f2KT1pvx.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-DJ5djvRE.js → mermaid.core-DbZRSylt.js} +4 -4
  41. package/dist/client/assets/{mindmap-definition-LN4V7U3C-ClrdpYfC.js → mindmap-definition-LN4V7U3C-BkDmzSTz.js} +1 -1
  42. package/dist/client/assets/{pegDiagram-2B236MQR-4ZH2WMrD.js → pegDiagram-2B236MQR-DlquSxs3.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-ENE6RG2P-6gPwO6l7.js → pieDiagram-ENE6RG2P-CCyDzfxg.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CDFJZSMQ.js → quadrantDiagram-ABIIQ3AL-CnLxtqkM.js} +1 -1
  45. package/dist/client/assets/{railroadDiagram-RFXS5EU6-Bcl-IeSD.js → railroadDiagram-RFXS5EU6-Cukk-SDc.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-TGXJPOKE-CL9mu8yn.js → requirementDiagram-TGXJPOKE-FiE5UTFr.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-DuBHXJe5.js → sankeyDiagram-HTMAVEWB-CctGleg5.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-X63pU9Rp.js → sequenceDiagram-DBY2YBRQ-2aJXXmki.js} +1 -1
  49. package/dist/client/assets/{sizeCapture-X5ZJPWSS-Dk05ejr_.js → sizeCapture-X5ZJPWSS-DENKfwB_.js} +1 -1
  50. package/dist/client/assets/{stateDiagram-2N3HPSRC-DaRstp_Q.js → stateDiagram-2N3HPSRC-BgsJ1GTE.js} +1 -1
  51. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CPinTSJf.js +1 -0
  52. package/dist/client/assets/{swimlanes-5IMT3BWC-DhxRQvSI.js → swimlanes-5IMT3BWC-CIEu4jIM.js} +2 -2
  53. package/dist/client/assets/swimlanesDiagram-G3AALYLV-B8dswh7X.js +8 -0
  54. package/dist/client/assets/{timeline-definition-FHXFAJF6-NUb5nhN9.js → timeline-definition-FHXFAJF6-CaC5EEAj.js} +1 -1
  55. package/dist/client/assets/{vennDiagram-L72KCM5P-DzhzWH4d.js → vennDiagram-L72KCM5P-BR3helYl.js} +1 -1
  56. package/dist/client/assets/{wardleyDiagram-EHGQE667-CGM1wELe.js → wardleyDiagram-EHGQE667-BPV3CRrw.js} +1 -1
  57. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CN8reugP.js → xychartDiagram-FW5EYKEG-B--69fmJ.js} +1 -1
  58. package/dist/client/index.html +1 -1
  59. package/dist/client/sw.js +1 -1
  60. package/dist/server/agent/collaborationPrompt.js +16 -2
  61. package/dist/server/cli.js +8 -6
  62. package/dist/server/entry.js +5 -1
  63. package/dist/server/routes/terminal.js +67 -32
  64. package/dist/server/utils/settings.js +110 -0
  65. package/dist/server/utils/tmuxRecoveryCandidate.js +11 -0
  66. package/package.json +1 -1
  67. package/runtime-manifest.json +2 -2
  68. package/dist/client/assets/channel-BsTnFQUg.js +0 -1
  69. package/dist/client/assets/classDiagram-OUVF2IWQ-DHP8oEqL.js +0 -1
  70. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DHP8oEqL.js +0 -1
  71. package/dist/client/assets/index-C_AcUvk0.js +0 -631
  72. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-B0qxTbxa.js +0 -1
  73. package/dist/client/assets/swimlanesDiagram-G3AALYLV-Buq7T0KR.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 });
@@ -1237,6 +1252,7 @@ function orchestrationSessionSnapshot(record) {
1237
1252
  const latestPrompt = backend?.autoTitlePromptPayloads.at(-1)?.trim() ?? '';
1238
1253
  return {
1239
1254
  sessionId: record.sessionId,
1255
+ agentNativeSessionId: backend?.agentSession?.sessionId ?? record.agentResume?.sessionId ?? null,
1240
1256
  backendSessionId: record.backendSessionId ?? null,
1241
1257
  name: record.name,
1242
1258
  cwd: backend?.cwd ?? record.cwd ?? '',
@@ -1349,7 +1365,12 @@ function tryDeliverCollaborationInbox(frontendSessionId) {
1349
1365
  groups: collaborationStore.groupsForSession(frontendSessionId),
1350
1366
  sessions: globalSessionState.sessions.map((candidate) => {
1351
1367
  const snapshot = orchestrationSessionSnapshot(candidate);
1352
- return { sessionId: snapshot.sessionId, name: snapshot.name, status: snapshot.status };
1368
+ return {
1369
+ sessionId: snapshot.sessionId,
1370
+ agentNativeSessionId: snapshot.agentNativeSessionId,
1371
+ name: snapshot.name,
1372
+ status: snapshot.status,
1373
+ };
1353
1374
  }),
1354
1375
  });
1355
1376
  session.ptyProcess.write(buildBracketedSubmitBytes(prompt));
@@ -1817,6 +1838,7 @@ async function listLiveTmuxInventorySessions() {
1817
1838
  '#{@termdock-created-at}',
1818
1839
  '#{@termdock-last-active-at}',
1819
1840
  `#{${TERMDOCK_GUI_DETACHED_AT_OPTION}}`,
1841
+ `#{${TERMDOCK_FRONTEND_SESSION_ID_OPTION}}`,
1820
1842
  ].join(TMUX_DELIMITER);
1821
1843
  try {
1822
1844
  const raw = await runTmux(['list-sessions', '-F', format]);
@@ -1824,9 +1846,9 @@ async function listLiveTmuxInventorySessions() {
1824
1846
  .split('\n')
1825
1847
  .map((line) => line.trim())
1826
1848
  .filter(Boolean)
1827
- .map((line) => parseDelimitedRow(line, 14))
1849
+ .map((line) => parseDelimitedRow(line, 15))
1828
1850
  .filter((row) => row !== null)
1829
- .map(([name, windowsRaw, attachedRaw, friendlyName, program, cwd, label, clientCountRaw, host, pidRaw, version, createdAtRaw, lastActiveAtRaw, guiDetachedAtRaw,]) => ({
1851
+ .map(([name, windowsRaw, attachedRaw, friendlyName, program, cwd, label, clientCountRaw, host, pidRaw, version, createdAtRaw, lastActiveAtRaw, guiDetachedAtRaw, sourceFrontendSessionId,]) => ({
1830
1852
  name,
1831
1853
  windows: Number.parseInt(windowsRaw || '0', 10) || 0,
1832
1854
  attachedCount: Number.parseInt(attachedRaw || '0', 10) || 0,
@@ -1841,6 +1863,7 @@ async function listLiveTmuxInventorySessions() {
1841
1863
  createdAt: parseNumberOption(createdAtRaw),
1842
1864
  lastActiveAt: parseNumberOption(lastActiveAtRaw),
1843
1865
  guiDetachedAt: parseNumberOption(guiDetachedAtRaw),
1866
+ sourceFrontendSessionId: sourceFrontendSessionId || null,
1844
1867
  }))
1845
1868
  .sort((a, b) => {
1846
1869
  const aCreated = a.createdAt ?? Number.POSITIVE_INFINITY;
@@ -1976,32 +1999,6 @@ async function buildSessionInventory() {
1976
1999
  return tmux;
1977
2000
  }
1978
2001
  }));
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
2002
  const liveTmuxByName = new Map(refreshedTmuxSessions.map((session) => [session.name, session]));
2006
2003
  let synchronizedPersistedFields = false;
2007
2004
  globalSessionState = {
@@ -2101,6 +2098,12 @@ async function buildSessionInventory() {
2101
2098
  connected: bound?.connected === true,
2102
2099
  live: true,
2103
2100
  restorable: bound?.restorable === true,
2101
+ recoveryCandidate: isTmuxRecoveryCandidate({
2102
+ managedByTermdock: isTermdockManagedTmuxSession(tmux),
2103
+ sourceFrontendSessionId: tmux.sourceFrontendSessionId,
2104
+ guiDetachedAt: tmux.guiDetachedAt,
2105
+ boundFrontendSessionId: bound?.sessionId ?? null,
2106
+ }),
2104
2107
  friendlyName: tmux.friendlyName,
2105
2108
  label: tmux.label,
2106
2109
  program: tmux.program,
@@ -4855,6 +4858,7 @@ router.get('/tmux/sessions', async (_req, res) => {
4855
4858
  connected: session.connected,
4856
4859
  live: session.live,
4857
4860
  restorable: session.restorable,
4861
+ recoveryCandidate: session.recoveryCandidate,
4858
4862
  friendlyName: session.friendlyName,
4859
4863
  label: session.label,
4860
4864
  program: session.program,
@@ -5600,6 +5604,7 @@ async function getSettingsPayload() {
5600
5604
  newSessionAgentSlug: getNewSessionAgentSlugSetting(),
5601
5605
  runningSessionButtonEnabled: getRunningSessionButtonEnabledSetting(),
5602
5606
  fileSortModes: getFileSortModesSetting(),
5607
+ pinnedExplorerRoots: getPinnedExplorerRootsSetting(),
5603
5608
  localAccess: {
5604
5609
  ...localAccess,
5605
5610
  interfaces,
@@ -5641,6 +5646,7 @@ router.post('/update/restart', async (_req, res) => {
5641
5646
  });
5642
5647
  router.put('/settings', async (req, res) => {
5643
5648
  const body = req.body ?? {};
5649
+ let pinnedExplorerRootsChanged = false;
5644
5650
  if (typeof body.preventSleep === 'boolean') {
5645
5651
  caffeinateManager.setPreventSleep(body.preventSleep);
5646
5652
  }
@@ -5735,6 +5741,26 @@ router.put('/settings', async (req, res) => {
5735
5741
  }
5736
5742
  setFileSortModeSetting(preference.path, preference.mode);
5737
5743
  }
5744
+ if (body.pinnedExplorerRoots && typeof body.pinnedExplorerRoots === 'object' && !Array.isArray(body.pinnedExplorerRoots)) {
5745
+ setPinnedExplorerRootsSetting(body.pinnedExplorerRoots);
5746
+ pinnedExplorerRootsChanged = true;
5747
+ }
5748
+ if (body.pinnedExplorerRoot && typeof body.pinnedExplorerRoot === 'object') {
5749
+ const mutation = body.pinnedExplorerRoot;
5750
+ const validAbsolutePath = (value) => typeof value === 'string'
5751
+ && value.length > 0
5752
+ && value.length <= 4096
5753
+ && (value.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(value));
5754
+ if (!validAbsolutePath(mutation.rootPath)
5755
+ || !validAbsolutePath(mutation.path)
5756
+ || (mutation.kind !== 'file' && mutation.kind !== 'directory')
5757
+ || typeof mutation.pinned !== 'boolean') {
5758
+ res.status(400).json({ error: 'Invalid pinned explorer entry', code: 'PINNED_EXPLORER_ROOT_INVALID' });
5759
+ return;
5760
+ }
5761
+ setPinnedExplorerRootSetting(mutation.rootPath, mutation.path, mutation.kind, mutation.pinned);
5762
+ pinnedExplorerRootsChanged = true;
5763
+ }
5738
5764
  if (body.localAccess && typeof body.localAccess === 'object') {
5739
5765
  const localAccessBody = body.localAccess;
5740
5766
  if (localAccessBody.reset === true) {
@@ -5753,7 +5779,16 @@ router.put('/settings', async (req, res) => {
5753
5779
  }
5754
5780
  }
5755
5781
  }
5756
- res.json(await getSettingsPayload());
5782
+ const payload = await getSettingsPayload();
5783
+ if (pinnedExplorerRootsChanged) {
5784
+ broadcastControlEvent({
5785
+ type: 'pinned-explorer-roots',
5786
+ pinnedExplorerRoots: payload.pinnedExplorerRoots,
5787
+ updatedAt: Date.now(),
5788
+ origin: typeof body.pinnedExplorerRootsOrigin === 'string' ? body.pinnedExplorerRootsOrigin : null,
5789
+ });
5790
+ }
5791
+ res.json(payload);
5757
5792
  });
5758
5793
  // ── Context draft (cross-device realtime sync) ────────────────────────
5759
5794
  // 草稿内容存服务端(~/.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.131",
3
+ "version": "1.4.133",
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.131",
4
+ "version": "1.4.133",
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-WwHpiehjhEPAzSG3ezDohB/qAqt6gUyH1Xco/uMsKyo=",
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-DJ5djvRE.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-CihoNZl2.js";import{b as i}from"./mermaid.core-DJ5djvRE.js";import"./chunk-5VM5RSS4-CecVqJ7p.js";import"./chunk-XXDRQBXY-CVtb44LI.js";import"./chunk-VR4S4FIN-CW3x8SlE.js";import"./chunk-32BRIVSS-yne3sS0_.js";import"./purify.es-CC4Brkr_.js";import"./index-C_AcUvk0.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-CihoNZl2.js";import{b as i}from"./mermaid.core-DJ5djvRE.js";import"./chunk-5VM5RSS4-CecVqJ7p.js";import"./chunk-XXDRQBXY-CVtb44LI.js";import"./chunk-VR4S4FIN-CW3x8SlE.js";import"./chunk-32BRIVSS-yne3sS0_.js";import"./purify.es-CC4Brkr_.js";import"./index-C_AcUvk0.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};