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.
- package/dist/client/assets/{ModelPreview-BRdf66iZ.js → ModelPreview-BE_DJvsb.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-DQ-0BDwp.js → abnfDiagram-VRR7QNED-DDVHljsN.js} +1 -1
- package/dist/client/assets/{arc-DB996Jer.js → arc-D6pBXFrr.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B6NsgBfh.js → architectureDiagram-ZJ3FMSHR-p9hcK6Ad.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-Do66IX7G.js → blockDiagram-677ZJIJ3-B1urT6p3.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-DFbyVO2f.js → c4Diagram-LMCZKHZV-Dpe5RReJ.js} +1 -1
- package/dist/client/assets/channel-BRiTV3DL.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-Dh_AXqCF.js → chunk-2Q5K7J3B-C-tD3SG_.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-yne3sS0_.js → chunk-32BRIVSS-D0MIswPl.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CecVqJ7p.js → chunk-5VM5RSS4-DAI_IFTQ.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-eRs2LgWQ.js → chunk-EX3LRPZG-BjO-2Ere.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-BBZTbiyT.js → chunk-JWPE2WC7-Tf0wkqSP.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-DuFkjAz7.js → chunk-MOJQB5TN-CzaSpUfp.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-AxfTNIzW.js → chunk-RYQCIY6F-vDS4WcRF.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-CihoNZl2.js → chunk-V7JOEXUC-ChDJI57Y.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-CW3x8SlE.js → chunk-VR4S4FIN-BViPjRi0.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-CVtb44LI.js → chunk-XXDRQBXY-BYNNBhi1.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-DnaJPSQT.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-DnaJPSQT.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-DVjHLC29.js → cose-bilkent-JH36ORCC-NRzzu38i.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-J2WM-6GK.js → cynefin-VYW2F7L2-A1cOSvjC.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DKaXiodq.js → cynefinDiagram-TSTJHNR4-D89LBcJA.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-LVS1YgSh.js → dagre-VKFMJZFB-BvNcNIYu.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-de576wbx.js → diagram-FQU43EPY-BEleLyaK.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-BLxjsybw.js → diagram-G47NLZAW-EH-vodG7.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-Dxv-Zg98.js → diagram-NH7WQ7WH-BviXs1Rk.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-Mgc11w-m.js → diagram-OA4YK3LP-CBSEzkwZ.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-Baa0EBra.js → diagram-WEI45ONY-D53fsZH_.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-BhTSdJNM.js → ebnfDiagram-CCIWWBDH-fmFV_0A6.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-CVtTjVGi.js → erDiagram-Q63AITRT-DkfQmoKL.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-DGPww_Jl.js → flowDiagram-23GEKE2U-BhD66TbS.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-DshJtOOJ.js → ganttDiagram-NO4QXBWP-D8PCGrIp.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CSTs_Nee.js → gitGraphDiagram-IHSO6WYX-DxDbg6Z9.js} +1 -1
- package/dist/client/assets/index-iKTEk-ec.js +631 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-DGiFmvwN.js → infoDiagram-FWYZ7A6U-B4m3yZCU.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-DlD-mPMi.js → ishikawaDiagram-FXEZZL3T-DzuDKD39.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-CIjD41_7.js → journeyDiagram-5HDEW3XC-B28ab3y9.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-B9e14Q0L.js → kanban-definition-HUTT4EX6-DjuLbFdG.js} +1 -1
- package/dist/client/assets/{linear-NhwKYgiu.js → linear-f2KT1pvx.js} +1 -1
- package/dist/client/assets/{mermaid.core-DJ5djvRE.js → mermaid.core-DbZRSylt.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-ClrdpYfC.js → mindmap-definition-LN4V7U3C-BkDmzSTz.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-4ZH2WMrD.js → pegDiagram-2B236MQR-DlquSxs3.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-6gPwO6l7.js → pieDiagram-ENE6RG2P-CCyDzfxg.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CDFJZSMQ.js → quadrantDiagram-ABIIQ3AL-CnLxtqkM.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-Bcl-IeSD.js → railroadDiagram-RFXS5EU6-Cukk-SDc.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-CL9mu8yn.js → requirementDiagram-TGXJPOKE-FiE5UTFr.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-DuBHXJe5.js → sankeyDiagram-HTMAVEWB-CctGleg5.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-X63pU9Rp.js → sequenceDiagram-DBY2YBRQ-2aJXXmki.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-Dk05ejr_.js → sizeCapture-X5ZJPWSS-DENKfwB_.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-DaRstp_Q.js → stateDiagram-2N3HPSRC-BgsJ1GTE.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CPinTSJf.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-DhxRQvSI.js → swimlanes-5IMT3BWC-CIEu4jIM.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-B8dswh7X.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-NUb5nhN9.js → timeline-definition-FHXFAJF6-CaC5EEAj.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-DzhzWH4d.js → vennDiagram-L72KCM5P-BR3helYl.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-CGM1wELe.js → wardleyDiagram-EHGQE667-BPV3CRrw.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CN8reugP.js → xychartDiagram-FW5EYKEG-B--69fmJ.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/collaborationPrompt.js +16 -2
- package/dist/server/cli.js +8 -6
- package/dist/server/entry.js +5 -1
- package/dist/server/routes/terminal.js +67 -32
- package/dist/server/utils/settings.js +110 -0
- package/dist/server/utils/tmuxRecoveryCandidate.js +11 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-BsTnFQUg.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-DHP8oEqL.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-DHP8oEqL.js +0 -1
- package/dist/client/assets/index-C_AcUvk0.js +0 -631
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-B0qxTbxa.js +0 -1
- 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 {
|
|
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,
|
|
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
|
-
|
|
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
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
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-
|
|
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};
|