termdock 1.4.107 → 1.4.109
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/.desktop-dist/main.js +345 -1
- package/dist/client/assets/{ModelPreview-CoR41z2e.js → ModelPreview-Cxk4IZLv.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-CXFrMEcU.js → abnfDiagram-VRR7QNED-Bz2HWRRX.js} +1 -1
- package/dist/client/assets/{arc-DXT1yvki.js → arc-D3vf35f_.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B4Tl5gm4.js → architectureDiagram-ZJ3FMSHR-LI-OXsDp.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-D8OY0bG2.js → blockDiagram-677ZJIJ3-Dm1JtLIo.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-BHIhcBy3.js → c4Diagram-LMCZKHZV-DSkxlBnU.js} +1 -1
- package/dist/client/assets/channel-qZtjoGxW.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-NFF-sZCT.js → chunk-2Q5K7J3B-Wd2AO6yQ.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-Cysd2WNQ.js → chunk-32BRIVSS-CIBkFIAg.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-BRHAAr29.js → chunk-5VM5RSS4-9veDV0NJ.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BovDZYVQ.js → chunk-EX3LRPZG-DVi5mqrV.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-ByWUvKGO.js → chunk-JWPE2WC7-DZc_3_5i.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-GWk63RII.js → chunk-MOJQB5TN-Czq1YGEA.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-GvzHMCqG.js → chunk-RYQCIY6F-BN3UqH_7.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BoiSCBad.js → chunk-V7JOEXUC-BgDCEp_z.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-ByS_rjkq.js → chunk-VR4S4FIN-BnyRVZSn.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-D9fRuB0M.js → chunk-XXDRQBXY-KOa0rQIz.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CnWXW1tk.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CnWXW1tk.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-BVOwkeRc.js → cose-bilkent-JH36ORCC-B-P2BstB.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-8rNTW_mJ.js → cynefin-VYW2F7L2-DOSg0Q6x.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DHASHiYD.js → cynefinDiagram-TSTJHNR4-Bm8Ct13U.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-wWqVXFNE.js → dagre-VKFMJZFB-Cnw99n0y.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-Bp2E7omd.js → diagram-FQU43EPY-B742HZhK.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-DajrBIg7.js → diagram-G47NLZAW-BGXKr2u9.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-ByGTRgNg.js → diagram-NH7WQ7WH-DIyEYN64.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-bsRCtWPe.js → diagram-OA4YK3LP-BdRMWU80.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-9f1_0Bjd.js → diagram-WEI45ONY-Bu-180k8.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DyvJDQQ5.js → ebnfDiagram-CCIWWBDH-Djcej7Pq.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-DwLHRiMz.js → erDiagram-Q63AITRT-D70JxkGr.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U--9lOgA3z.js → flowDiagram-23GEKE2U-DovWUM1i.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-j9C0N_DS.js → ganttDiagram-NO4QXBWP-Izsfqq5i.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-lHdVffsh.js → gitGraphDiagram-IHSO6WYX-BTZo0N08.js} +1 -1
- package/dist/client/assets/index-BE4Js_ss.js +456 -0
- package/dist/client/assets/index-CRIZk117.css +1 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-D8kAsmdn.js → infoDiagram-FWYZ7A6U-Ina5uVvZ.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CTY9BLyE.js → ishikawaDiagram-FXEZZL3T-iUdzuACl.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-B3QO8wF2.js → journeyDiagram-5HDEW3XC-lh15NShH.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-CesXysl2.js → kanban-definition-HUTT4EX6-CKZooxMa.js} +1 -1
- package/dist/client/assets/{linear-wxKR9xNu.js → linear-BrmrQdSX.js} +1 -1
- package/dist/client/assets/{mermaid.core-DqsMKQ0n.js → mermaid.core-fbY4nFyi.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-CCfpEZEP.js → mindmap-definition-LN4V7U3C-BHUhjMUf.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-DEje4xdU.js → pegDiagram-2B236MQR-8qcHqxsE.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-B5guqQ7K.js → pieDiagram-ENE6RG2P-CpIkkExH.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-D58kwRrP.js → quadrantDiagram-ABIIQ3AL-hWzvYsWG.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-D9VFfFu0.js → railroadDiagram-RFXS5EU6-2nhrE9bK.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-B0q4l0cr.js → requirementDiagram-TGXJPOKE-B05mEN7w.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-DTsXXi0M.js → sankeyDiagram-HTMAVEWB-CXHkBpDN.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-3Xpi1nmi.js → sequenceDiagram-DBY2YBRQ-C4wG1r_I.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-C0jHd0eC.js → sizeCapture-X5ZJPWSS-D_3i42tQ.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-DlDm_dYg.js → stateDiagram-2N3HPSRC-DhEgczRA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-D9gqRttI.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-C128cuo6.js → swimlanes-5IMT3BWC-DTrpFviv.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CNOSFfLB.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-BHsLELm-.js → timeline-definition-FHXFAJF6-DLpl_8Za.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-DkdQFPIy.js → vennDiagram-L72KCM5P-CgnCFHj7.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-DWaahbD5.js → wardleyDiagram-EHGQE667-BwNfD-cz.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CSwOYmgm.js → xychartDiagram-FW5EYKEG--sZgo_Fw.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/automationStore.js +12 -7
- package/dist/server/agent/sessionSearchStore.js +135 -5
- package/dist/server/entry.js +24 -1
- package/dist/server/routes/filesystem.js +31 -6
- package/dist/server/routes/terminal.js +6 -2
- package/dist/server/utils/heicPreview.js +50 -0
- package/dist/server/utils/runtimeMonitor.js +287 -0
- package/dist/server/utils/settings.js +9 -0
- package/dist/server/workers/heicPreviewWorker.js +14 -0
- package/package.json +3 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/channel-CXw70OW3.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CNF7sVVd.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CNF7sVVd.js +0 -1
- package/dist/client/assets/index-B1FKcn-W.js +0 -451
- package/dist/client/assets/index-BloZ2WQg.css +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVOBevpU.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-Bf87D8dr.js +0 -8
|
@@ -2,6 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
4
4
|
const LOG_SEGMENT_BYTES = MAX_LOG_BYTES / 2;
|
|
5
|
+
const DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_MAX_SESSIONS = 50;
|
|
7
|
+
const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60_000;
|
|
8
|
+
const BUDGET_MAINTENANCE_INTERVAL_MS = 10 * 60_000;
|
|
5
9
|
const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
6
10
|
function clean(value) {
|
|
7
11
|
return value
|
|
@@ -12,23 +16,36 @@ function clean(value) {
|
|
|
12
16
|
}
|
|
13
17
|
export class SessionSearchStore {
|
|
14
18
|
directory;
|
|
19
|
+
limits;
|
|
15
20
|
metadata = new Map();
|
|
16
21
|
pending = new Map();
|
|
17
22
|
flushTimer = null;
|
|
18
|
-
|
|
23
|
+
metadataDirty = false;
|
|
24
|
+
metadataMustPersist = false;
|
|
25
|
+
lastMetadataWriteAt = 0;
|
|
26
|
+
lastBudgetMaintenanceAt = 0;
|
|
27
|
+
constructor(directory, limits = {}) {
|
|
19
28
|
this.directory = directory;
|
|
29
|
+
this.limits = limits;
|
|
20
30
|
this.loadMetadata();
|
|
31
|
+
this.enforceBudget();
|
|
21
32
|
}
|
|
22
33
|
append(metadata, output) {
|
|
23
34
|
const text = clean(output);
|
|
35
|
+
const previous = this.metadata.get(metadata.sessionId);
|
|
24
36
|
this.metadata.set(metadata.sessionId, { ...metadata, updatedAt: Date.now() });
|
|
37
|
+
this.metadataDirty = true;
|
|
38
|
+
this.metadataMustPersist ||= this.metadataChanged(previous, metadata);
|
|
25
39
|
if (text.trim())
|
|
26
40
|
this.pending.set(metadata.sessionId, [...(this.pending.get(metadata.sessionId) ?? []), text]);
|
|
27
41
|
if (!this.flushTimer)
|
|
28
42
|
this.flushTimer = setTimeout(() => this.flush(), 750);
|
|
29
43
|
}
|
|
30
44
|
update(metadata) {
|
|
45
|
+
const previous = this.metadata.get(metadata.sessionId);
|
|
31
46
|
this.metadata.set(metadata.sessionId, { ...metadata, updatedAt: Date.now() });
|
|
47
|
+
this.metadataDirty = true;
|
|
48
|
+
this.metadataMustPersist ||= this.metadataChanged(previous, metadata);
|
|
32
49
|
if (!this.flushTimer)
|
|
33
50
|
this.flushTimer = setTimeout(() => this.flush(), 750);
|
|
34
51
|
}
|
|
@@ -61,7 +78,7 @@ export class SessionSearchStore {
|
|
|
61
78
|
if (this.flushTimer)
|
|
62
79
|
clearTimeout(this.flushTimer);
|
|
63
80
|
this.flushTimer = null;
|
|
64
|
-
if (this.metadata.size === 0 && this.pending.size === 0)
|
|
81
|
+
if (this.metadata.size === 0 && this.pending.size === 0 && !this.metadataDirty)
|
|
65
82
|
return;
|
|
66
83
|
fs.mkdirSync(this.directory, { recursive: true });
|
|
67
84
|
for (const [sessionId, chunks] of this.pending) {
|
|
@@ -84,9 +101,63 @@ export class SessionSearchStore {
|
|
|
84
101
|
fs.appendFileSync(logPath, boundedChunk, { mode: 0o600 });
|
|
85
102
|
}
|
|
86
103
|
this.pending.clear();
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (this.metadataDirty && (this.metadataMustPersist || now - this.lastMetadataWriteAt >= 30_000))
|
|
106
|
+
this.persistMetadata();
|
|
107
|
+
if (now - this.lastBudgetMaintenanceAt >= BUDGET_MAINTENANCE_INTERVAL_MS)
|
|
108
|
+
this.enforceBudget(now);
|
|
109
|
+
}
|
|
110
|
+
enforceBudget(now = Date.now()) {
|
|
111
|
+
this.lastBudgetMaintenanceAt = now;
|
|
112
|
+
const maxSessions = this.limits.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
113
|
+
const maxAgeMs = this.limits.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
114
|
+
const maxTotalBytes = this.limits.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
115
|
+
const newest = [...this.metadata.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
116
|
+
const retained = new Set(newest.filter((item, index) => index < maxSessions && now - item.updatedAt <= maxAgeMs).map((item) => item.sessionId));
|
|
117
|
+
let changed = false;
|
|
118
|
+
for (const sessionId of this.metadata.keys()) {
|
|
119
|
+
if (retained.has(sessionId))
|
|
120
|
+
continue;
|
|
121
|
+
this.metadata.delete(sessionId);
|
|
122
|
+
this.pending.delete(sessionId);
|
|
123
|
+
this.removeSessionLogs(sessionId);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
let files = this.listLogFiles();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (retained.has(file.sessionId))
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
fs.rmSync(file.path, { force: true });
|
|
132
|
+
}
|
|
133
|
+
catch { /* best effort */ }
|
|
134
|
+
}
|
|
135
|
+
files = this.listLogFiles();
|
|
136
|
+
let totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
137
|
+
const removalOrder = [
|
|
138
|
+
...files.filter((file) => file.previous).sort((a, b) => a.mtimeMs - b.mtimeMs),
|
|
139
|
+
...files.filter((file) => !file.previous).sort((a, b) => a.mtimeMs - b.mtimeMs),
|
|
140
|
+
];
|
|
141
|
+
for (const file of removalOrder) {
|
|
142
|
+
if (totalBytes <= maxTotalBytes)
|
|
143
|
+
break;
|
|
144
|
+
try {
|
|
145
|
+
fs.rmSync(file.path, { force: true });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
totalBytes -= file.size;
|
|
151
|
+
if (!file.previous) {
|
|
152
|
+
this.metadata.delete(file.sessionId);
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (changed) {
|
|
157
|
+
this.metadataDirty = true;
|
|
158
|
+
this.metadataMustPersist = true;
|
|
159
|
+
this.persistMetadata();
|
|
160
|
+
}
|
|
90
161
|
}
|
|
91
162
|
loadMetadata() {
|
|
92
163
|
try {
|
|
@@ -95,6 +166,7 @@ export class SessionSearchStore {
|
|
|
95
166
|
if (item && typeof item.sessionId === 'string')
|
|
96
167
|
this.metadata.set(item.sessionId, item);
|
|
97
168
|
}
|
|
169
|
+
this.lastMetadataWriteAt = Date.now();
|
|
98
170
|
}
|
|
99
171
|
catch { /* first run */ }
|
|
100
172
|
}
|
|
@@ -118,6 +190,64 @@ export class SessionSearchStore {
|
|
|
118
190
|
return 0;
|
|
119
191
|
}
|
|
120
192
|
}
|
|
193
|
+
metadataChanged(previous, next) {
|
|
194
|
+
return !previous
|
|
195
|
+
|| previous.backendSessionId !== next.backendSessionId
|
|
196
|
+
|| previous.title !== next.title
|
|
197
|
+
|| previous.cwd !== next.cwd
|
|
198
|
+
|| previous.agentSlug !== next.agentSlug
|
|
199
|
+
|| previous.agentNativeSessionId !== next.agentNativeSessionId;
|
|
200
|
+
}
|
|
201
|
+
persistMetadata() {
|
|
202
|
+
fs.mkdirSync(this.directory, { recursive: true });
|
|
203
|
+
const temporaryPath = `${this.metadataPath()}.${process.pid}.tmp`;
|
|
204
|
+
fs.writeFileSync(temporaryPath, JSON.stringify({ version: 1, sessions: [...this.metadata.values()] }, null, 2), { mode: 0o600 });
|
|
205
|
+
fs.renameSync(temporaryPath, this.metadataPath());
|
|
206
|
+
this.lastMetadataWriteAt = Date.now();
|
|
207
|
+
this.metadataDirty = false;
|
|
208
|
+
this.metadataMustPersist = false;
|
|
209
|
+
}
|
|
210
|
+
removeSessionLogs(sessionId) {
|
|
211
|
+
try {
|
|
212
|
+
fs.rmSync(this.logPath(sessionId), { force: true });
|
|
213
|
+
}
|
|
214
|
+
catch { /* best effort */ }
|
|
215
|
+
try {
|
|
216
|
+
fs.rmSync(this.previousLogPath(sessionId), { force: true });
|
|
217
|
+
}
|
|
218
|
+
catch { /* best effort */ }
|
|
219
|
+
}
|
|
220
|
+
listLogFiles() {
|
|
221
|
+
let names;
|
|
222
|
+
try {
|
|
223
|
+
names = fs.readdirSync(this.directory);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
return names.flatMap((name) => {
|
|
229
|
+
const match = name.match(/^(.*)\.log(\.1)?$/);
|
|
230
|
+
if (!match)
|
|
231
|
+
return [];
|
|
232
|
+
let sessionId;
|
|
233
|
+
try {
|
|
234
|
+
sessionId = decodeURIComponent(match[1]);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
const filePath = path.join(this.directory, name);
|
|
240
|
+
try {
|
|
241
|
+
const stat = fs.statSync(filePath);
|
|
242
|
+
return stat.isFile()
|
|
243
|
+
? [{ path: filePath, sessionId, previous: Boolean(match[2]), size: stat.size, mtimeMs: stat.mtimeMs }]
|
|
244
|
+
: [];
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
121
251
|
metadataPath() { return path.join(this.directory, 'sessions.json'); }
|
|
122
252
|
logPath(sessionId) { return path.join(this.directory, `${encodeURIComponent(sessionId)}.log`); }
|
|
123
253
|
previousLogPath(sessionId) { return `${this.logPath(sessionId)}.1`; }
|
package/dist/server/entry.js
CHANGED
|
@@ -23,6 +23,7 @@ import { localAccessManager } from './utils/localAccess.js';
|
|
|
23
23
|
import { isAllowedHost, isUpgradeOriginAllowed, validateHostMiddleware, } from './utils/requestSecurity.js';
|
|
24
24
|
import { getCookieSecurityOptions, setSecureCookieMode } from './utils/cookieSecurity.js';
|
|
25
25
|
import { requestDeadlineMiddleware } from './utils/requestDeadline.js';
|
|
26
|
+
import { RuntimeMonitor } from './utils/runtimeMonitor.js';
|
|
26
27
|
import { startOnboardingServer, stopOnboardingServer, getOnboardingServerUrl } from './onboardingServer.js';
|
|
27
28
|
import { CertificateWatcher } from './certificateWatcher.js';
|
|
28
29
|
import { startTermdockLogMaintenance, writeDiffTraceLog, writeErrorLog, writeJsonLog, writeTextLog, } from './utils/serverLogger.js';
|
|
@@ -67,6 +68,8 @@ function getRouteFamily(pathname) {
|
|
|
67
68
|
return 'auth';
|
|
68
69
|
if (pathname.startsWith('/api/client-log'))
|
|
69
70
|
return 'client-log';
|
|
71
|
+
if (pathname.startsWith('/api/diagnostics'))
|
|
72
|
+
return 'diagnostics';
|
|
70
73
|
if (pathname === '/health')
|
|
71
74
|
return 'health';
|
|
72
75
|
if (pathname.startsWith('/assets/'))
|
|
@@ -288,6 +291,7 @@ export function createApp(options = {}) {
|
|
|
288
291
|
return;
|
|
289
292
|
logged = true;
|
|
290
293
|
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
294
|
+
options.runtimeMonitor?.recordRequest(res.statusCode, durationMs);
|
|
291
295
|
writeJsonLog('access.log', {
|
|
292
296
|
event,
|
|
293
297
|
method: req.method,
|
|
@@ -424,6 +428,13 @@ export function createApp(options = {}) {
|
|
|
424
428
|
const address = req.socket.remoteAddress ?? '';
|
|
425
429
|
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
426
430
|
};
|
|
431
|
+
app.get('/api/diagnostics/runtime', requireAuth({ bypass: isTrustedLocalCliRequest }), async (_req, res) => {
|
|
432
|
+
if (!options.runtimeMonitor) {
|
|
433
|
+
res.status(503).json({ error: 'Runtime monitor is not active', code: 'RUNTIME_MONITOR_INACTIVE' });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
res.json(await options.runtimeMonitor.diagnostics());
|
|
437
|
+
});
|
|
427
438
|
// 应用鉴权 + CSRF保护(在终端路由之前)
|
|
428
439
|
// The HTML preview route authenticates itself: document requests use the
|
|
429
440
|
// session cookie, then get redirected to a short-lived URL token so the
|
|
@@ -490,8 +501,20 @@ export function startServer(options = {}) {
|
|
|
490
501
|
const stopLogMaintenance = startTermdockLogMaintenance();
|
|
491
502
|
const port = options.port ?? Number(process.env.PORT || DEFAULT_PORT);
|
|
492
503
|
const host = options.host ?? (process.env.HOST || DEFAULT_HOST);
|
|
493
|
-
const
|
|
504
|
+
const runtimeMonitor = new RuntimeMonitor({
|
|
505
|
+
stateDirectory: path.join(homedir(), '.termdock'),
|
|
506
|
+
historyPath: path.join(homedir(), '.termdock', 'runtime-metrics.log'),
|
|
507
|
+
});
|
|
508
|
+
runtimeMonitor.start();
|
|
509
|
+
const app = createApp({
|
|
510
|
+
port: options.onboardingPort ?? port,
|
|
511
|
+
httpsCaPath: options.httpsCaPath,
|
|
512
|
+
localApiToken: options.localApiToken,
|
|
513
|
+
runtimeMonitor,
|
|
514
|
+
});
|
|
494
515
|
const { server, scheme } = createServerForApp(app, options);
|
|
516
|
+
server.on('connection', (socket) => runtimeMonitor.trackSocket(socket));
|
|
517
|
+
server.once('close', () => runtimeMonitor.stop());
|
|
495
518
|
setSecureCookieMode(scheme === 'https');
|
|
496
519
|
const certWatcher = new CertificateWatcher({
|
|
497
520
|
enabled: scheme === 'https' && Boolean(options.httpsCertPath && options.httpsKeyPath),
|
|
@@ -8,7 +8,7 @@ import watcher from '@parcel/watcher';
|
|
|
8
8
|
import busboy from 'busboy';
|
|
9
9
|
import { pathValidator } from '../utils/pathValidator.js';
|
|
10
10
|
import { isAuthEnabled, isRequestAuthenticated } from '../utils/authProtection.js';
|
|
11
|
-
import { getImageDimensions } from '../utils/imageDimensions.js';
|
|
11
|
+
import { getImageDimensions, parseImageDimensions } from '../utils/imageDimensions.js';
|
|
12
12
|
import { writeDiffTraceLog, writeErrorLog, writeJsonLog } from '../utils/serverLogger.js';
|
|
13
13
|
import { clearBranchAuditRecords, clearChangeAuditRecords, listBranchAuditRecords, listChangeAuditRecords, buildChangeAuditFingerprint } from '../utils/changeAuditStore.js';
|
|
14
14
|
import { getLanIPv4Addresses } from '../utils/localAccess.js';
|
|
@@ -17,6 +17,7 @@ import { GitApplyError, HUNK_APPLY_MODES, runGitApply, validateHunkPatch } from
|
|
|
17
17
|
import { deleteFilesystemFile } from '../utils/deleteFilesystemFile.js';
|
|
18
18
|
import { inspectKicadBoardPoint } from '../utils/kicadBoardInspection.js';
|
|
19
19
|
import { EdaPreviewCache, requestAcceptsEtag } from '../utils/edaPreviewCache.js';
|
|
20
|
+
import { convertHeicPreview, HEIC_PREVIEW_MIME_TYPES, isHeicPreviewPath } from '../utils/heicPreview.js';
|
|
20
21
|
const router = Router();
|
|
21
22
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
22
23
|
const MAX_IMAGE_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
|
|
@@ -46,6 +47,7 @@ const MAX_NESTED_GIT_REPOS = 32;
|
|
|
46
47
|
const NESTED_GIT_DISCOVERY_TIMEOUT_MS = 1_000;
|
|
47
48
|
const NESTED_GIT_DISCOVERY_CACHE_TTL_MS = 60_000;
|
|
48
49
|
const FS_ROUTE_TIMEOUT_MS = 6_000;
|
|
50
|
+
const HEIC_PREVIEW_TIMEOUT_MS = 30_000;
|
|
49
51
|
const EDA_PREVIEW_TIMEOUT_MS = 30_000;
|
|
50
52
|
const EDA_PREVIEW_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
51
53
|
const GIT_ROUTE_TIMEOUT_MS = 8_000;
|
|
@@ -431,6 +433,7 @@ const IMAGE_MIME_BY_EXT = {
|
|
|
431
433
|
'.bmp': 'image/bmp',
|
|
432
434
|
'.ico': 'image/x-icon',
|
|
433
435
|
'.svg': 'image/svg+xml',
|
|
436
|
+
...HEIC_PREVIEW_MIME_TYPES,
|
|
434
437
|
};
|
|
435
438
|
function getImageMimeType(filePath) {
|
|
436
439
|
return IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
|
|
@@ -3024,23 +3027,45 @@ router.get('/blob', async (req, res) => {
|
|
|
3024
3027
|
}
|
|
3025
3028
|
return { resolvedPath, stat, mimeType };
|
|
3026
3029
|
})(), FS_ROUTE_TIMEOUT_MS, 'Image preview took too long. The file may be on slow storage or currently blocked by another process.', 'FS_BLOB_TIMEOUT');
|
|
3027
|
-
|
|
3028
|
-
|
|
3030
|
+
let responseBody = null;
|
|
3031
|
+
let responseMimeType = mimeType;
|
|
3032
|
+
let responseFilename = path.basename(resolvedPath);
|
|
3033
|
+
if (isHeicPreviewPath(resolvedPath)) {
|
|
3034
|
+
responseBody = await withTimeout(convertHeicPreview(resolvedPath, controller.signal), HEIC_PREVIEW_TIMEOUT_MS, 'HEIC preview conversion took too long.', 'HEIC_PREVIEW_TIMEOUT', () => controller.abort(new OperationTimeoutError('HEIC preview conversion took too long.', 'HEIC_PREVIEW_TIMEOUT')));
|
|
3035
|
+
if (responseBody.length > MAX_IMAGE_PREVIEW_SIZE) {
|
|
3036
|
+
const error = new Error('Converted image is too large to preview');
|
|
3037
|
+
error.code = 'IMAGE_TOO_LARGE';
|
|
3038
|
+
error.status = 413;
|
|
3039
|
+
throw error;
|
|
3040
|
+
}
|
|
3041
|
+
responseMimeType = 'image/jpeg';
|
|
3042
|
+
responseFilename = `${path.basename(resolvedPath, path.extname(resolvedPath))}.jpg`;
|
|
3043
|
+
}
|
|
3044
|
+
const responseSize = responseBody?.length ?? stat.size;
|
|
3045
|
+
res.setHeader('Content-Type', responseMimeType);
|
|
3046
|
+
res.setHeader('Content-Length', responseSize.toString());
|
|
3029
3047
|
res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
3030
3048
|
res.setHeader('Cache-Control', 'no-store');
|
|
3031
3049
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
3032
|
-
res.setHeader('Content-Disposition', buildContentDisposition('inline',
|
|
3050
|
+
res.setHeader('Content-Disposition', buildContentDisposition('inline', responseFilename));
|
|
3033
3051
|
// Expose dimensions so markdown image previews can reserve the final
|
|
3034
3052
|
// display box before downloading (loading placeholder without a jump).
|
|
3035
3053
|
// Best-effort: parsing failure just omits the headers.
|
|
3036
|
-
const dimensions =
|
|
3054
|
+
const dimensions = responseBody
|
|
3055
|
+
? parseImageDimensions(responseBody, responseMimeType)
|
|
3056
|
+
: await getImageDimensions(resolvedPath, responseMimeType).catch(() => null);
|
|
3037
3057
|
if (dimensions) {
|
|
3038
3058
|
res.setHeader('X-Image-Width', String(dimensions.width));
|
|
3039
3059
|
res.setHeader('X-Image-Height', String(dimensions.height));
|
|
3040
3060
|
}
|
|
3061
|
+
if (responseBody) {
|
|
3062
|
+
logOnce('ok', { path: resolvedPath, bytes: responseSize, extra: { mimeType: responseMimeType, sourceMimeType: mimeType } });
|
|
3063
|
+
res.end(responseBody);
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3041
3066
|
const stream = fs.createReadStream(resolvedPath);
|
|
3042
3067
|
controller.signal.addEventListener('abort', () => stream.destroy(controller.signal.reason instanceof Error ? controller.signal.reason : undefined), { once: true });
|
|
3043
|
-
res.on('finish', () => logOnce('ok', { path: resolvedPath, bytes: stat.size, extra: { mimeType } }));
|
|
3068
|
+
res.on('finish', () => logOnce('ok', { path: resolvedPath, bytes: stat.size, extra: { mimeType: responseMimeType } }));
|
|
3044
3069
|
res.on('close', () => {
|
|
3045
3070
|
if (!res.writableEnded)
|
|
3046
3071
|
logOnce('error', { path: resolvedPath, bytes: stat.size, code: 'CLIENT_CLOSED', error: 'Client closed image preview request' });
|
|
@@ -14,7 +14,7 @@ import { getPtyHostManager } from '../ptyhost/manager.js';
|
|
|
14
14
|
import { pathValidator } from '../utils/pathValidator.js';
|
|
15
15
|
import { TERMINAL, TMUX } from '../config.js';
|
|
16
16
|
import { localAccessManager } from '../utils/localAccess.js';
|
|
17
|
-
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, } from '../utils/settings.js';
|
|
17
|
+
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, getRunningSessionButtonEnabledSetting, setRunningSessionButtonEnabledSetting, } from '../utils/settings.js';
|
|
18
18
|
import { loadContextDraft, saveContextDraft } from '../utils/contextDraft.js';
|
|
19
19
|
import { getOnboardingServerUrl } from '../onboardingServer.js';
|
|
20
20
|
import { getFocusSequence, removeClientFocusState, scanFocusTrackingMode, setClientFocusState, } from '../utils/tmuxFocus.js';
|
|
@@ -1309,7 +1309,7 @@ function tryDeliverCollaborationInbox(frontendSessionId) {
|
|
|
1309
1309
|
|| (session.agentSession?.rich && !['idle', 'done'].includes(session.agentSession.status))) {
|
|
1310
1310
|
return { delivered: [], pending: pending.length };
|
|
1311
1311
|
}
|
|
1312
|
-
|
|
1312
|
+
session.ptyProcess.write(buildBracketedSubmitBytes(formatCollaborationDelivery(frontendSessionId, pending)));
|
|
1313
1313
|
collaborationStore.markDelivered(pending.map((message) => message.id));
|
|
1314
1314
|
return { delivered: pending.map((message) => message.id), pending: 0 };
|
|
1315
1315
|
}
|
|
@@ -5185,6 +5185,7 @@ async function getSettingsPayload() {
|
|
|
5185
5185
|
autoRenamePromptPreference: getAutoRenamePromptPreferenceSetting(),
|
|
5186
5186
|
autoRenamePromptPayloadChars: getAutoRenamePromptPayloadCharsSetting(),
|
|
5187
5187
|
newSessionAgentSlug: getNewSessionAgentSlugSetting(),
|
|
5188
|
+
runningSessionButtonEnabled: getRunningSessionButtonEnabledSetting(),
|
|
5188
5189
|
localAccess: {
|
|
5189
5190
|
...localAccess,
|
|
5190
5191
|
interfaces,
|
|
@@ -5293,6 +5294,9 @@ router.put('/settings', async (req, res) => {
|
|
|
5293
5294
|
}
|
|
5294
5295
|
setNewSessionAgentSlugSetting(slug);
|
|
5295
5296
|
}
|
|
5297
|
+
if (typeof body.runningSessionButtonEnabled === 'boolean') {
|
|
5298
|
+
setRunningSessionButtonEnabledSetting(body.runningSessionButtonEnabled);
|
|
5299
|
+
}
|
|
5296
5300
|
if (body.localAccess && typeof body.localAccess === 'object') {
|
|
5297
5301
|
const localAccessBody = body.localAccess;
|
|
5298
5302
|
if (localAccessBody.reset === true) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
export const HEIC_PREVIEW_MIME_TYPES = {
|
|
3
|
+
'.heic': 'image/heic',
|
|
4
|
+
'.heif': 'image/heif',
|
|
5
|
+
};
|
|
6
|
+
export function isHeicPreviewPath(filePath) {
|
|
7
|
+
const normalized = filePath.toLowerCase();
|
|
8
|
+
return normalized.endsWith('.heic') || normalized.endsWith('.heif');
|
|
9
|
+
}
|
|
10
|
+
export function convertHeicPreview(filePath, signal) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const worker = new Worker(new URL('../workers/heicPreviewWorker.js', import.meta.url), {
|
|
13
|
+
workerData: { filePath },
|
|
14
|
+
});
|
|
15
|
+
let settled = false;
|
|
16
|
+
const finish = (callback) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
signal?.removeEventListener('abort', abort);
|
|
21
|
+
callback();
|
|
22
|
+
};
|
|
23
|
+
const abort = () => {
|
|
24
|
+
void worker.terminate();
|
|
25
|
+
finish(() => reject(signal?.reason instanceof Error ? signal.reason : new Error('HEIC preview conversion was cancelled')));
|
|
26
|
+
};
|
|
27
|
+
if (signal?.aborted) {
|
|
28
|
+
abort();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
32
|
+
worker.once('message', (message) => {
|
|
33
|
+
if (message.error) {
|
|
34
|
+
finish(() => reject(new Error(message.error)));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { output } = message;
|
|
38
|
+
if (!output) {
|
|
39
|
+
finish(() => reject(new Error('HEIC preview worker returned no image data')));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
finish(() => resolve(Buffer.from(output)));
|
|
43
|
+
});
|
|
44
|
+
worker.once('error', (error) => finish(() => reject(error)));
|
|
45
|
+
worker.once('exit', (code) => {
|
|
46
|
+
if (code !== 0)
|
|
47
|
+
finish(() => reject(new Error(`HEIC preview worker exited with code ${code}`)));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|