termdock 1.4.108 → 1.4.110

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 (76) hide show
  1. package/.desktop-dist/main.js +483 -24
  2. package/dist/client/assets/{ModelPreview-By3cPzJ9.js → ModelPreview-B40SLVdu.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-Dm9g0Pi3.js → abnfDiagram-VRR7QNED-D7p11Ssd.js} +1 -1
  4. package/dist/client/assets/{arc-XntYYR9t.js → arc-CsmLIp7i.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CW0kZlVv.js → architectureDiagram-ZJ3FMSHR-BjYttOye.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-DgMdh19W.js → blockDiagram-677ZJIJ3-BnWi8IVO.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-DJC4m2Y6.js → c4Diagram-LMCZKHZV-BFm2RcKX.js} +1 -1
  8. package/dist/client/assets/channel-ifcSKqN3.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-CgE5oTjY.js → chunk-2Q5K7J3B-D_OKw60X.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-B50lSI4-.js → chunk-32BRIVSS-D6k1lZV9.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-C984gGSO.js → chunk-5VM5RSS4-CEqvSDkW.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-C1jPbFiq.js → chunk-EX3LRPZG-Ca2Gmbvd.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-D4U9D5Kv.js → chunk-JWPE2WC7-CRRvRUQB.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-C4rLPnf_.js → chunk-MOJQB5TN-CeZ5qCFm.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-CnSX68rt.js → chunk-RYQCIY6F-Cx7sW5Rr.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-LTTPDSzs.js → chunk-V7JOEXUC-B3_grzN8.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-dCZ9yGOH.js → chunk-VR4S4FIN-g73M8USU.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-DiUZr3na.js → chunk-XXDRQBXY-9i-NV13r.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-CVu2gyed.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CVu2gyed.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-Dw73llr7.js → cose-bilkent-JH36ORCC-DGKauMxg.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-CsB9ZT7a.js → cynefin-VYW2F7L2-DKvEKvGd.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-NinMHYPB.js → cynefinDiagram-TSTJHNR4-CLXYMIpB.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-CmFL5D4c.js → dagre-VKFMJZFB-DFAR5nAp.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-D_C52SB6.js → diagram-FQU43EPY-DeRBoyeq.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-BLlpvNO5.js → diagram-G47NLZAW-CDEAgMuP.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-BNT2NQnR.js → diagram-NH7WQ7WH-4VFR2ZsJ.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-DlJ-T7tl.js → diagram-OA4YK3LP-DMKIgZlI.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-D7s_sZnL.js → diagram-WEI45ONY-zEjxP4io.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-PGksmUKG.js → ebnfDiagram-CCIWWBDH-BuErqR1l.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-CbfrT-bt.js → erDiagram-Q63AITRT-D4nbc2T3.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-CqUGdXdQ.js → flowDiagram-23GEKE2U-B2MiQAI4.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-DpH0ybM6.js → ganttDiagram-NO4QXBWP-hGTX0vsS.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-Cmc3LsBg.js → gitGraphDiagram-IHSO6WYX-BHHSVtvH.js} +1 -1
  35. package/dist/client/assets/index-CFXqRKcJ.css +1 -0
  36. package/dist/client/assets/index-D3kro0zp.js +456 -0
  37. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CHzl_SDV.js → infoDiagram-FWYZ7A6U-BZuNpsCG.js} +1 -1
  38. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-BUVwtCaN.js → ishikawaDiagram-FXEZZL3T-C0KwjZIv.js} +1 -1
  39. package/dist/client/assets/{journeyDiagram-5HDEW3XC-DrmO4THP.js → journeyDiagram-5HDEW3XC-mxy7-Irj.js} +1 -1
  40. package/dist/client/assets/{kanban-definition-HUTT4EX6-DTZoUd1n.js → kanban-definition-HUTT4EX6-DslZKv6b.js} +1 -1
  41. package/dist/client/assets/{linear-DgCNjduP.js → linear-BJXz3Iqq.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-DLidsBlx.js → mermaid.core-Btv5hFOv.js} +4 -4
  43. package/dist/client/assets/{mindmap-definition-LN4V7U3C-srFG-Dae.js → mindmap-definition-LN4V7U3C-BSbqyDyq.js} +1 -1
  44. package/dist/client/assets/{pegDiagram-2B236MQR-Qary-lxo.js → pegDiagram-2B236MQR-L2cCu4W1.js} +1 -1
  45. package/dist/client/assets/{pieDiagram-ENE6RG2P-D924FFIQ.js → pieDiagram-ENE6RG2P-CXdawbl4.js} +1 -1
  46. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-YFBMJKUv.js → quadrantDiagram-ABIIQ3AL-CwKwBctE.js} +1 -1
  47. package/dist/client/assets/{railroadDiagram-RFXS5EU6-C__7fBPw.js → railroadDiagram-RFXS5EU6-B6MBOnSp.js} +1 -1
  48. package/dist/client/assets/{requirementDiagram-TGXJPOKE-BtAq5f69.js → requirementDiagram-TGXJPOKE-CR9lns0Z.js} +1 -1
  49. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-BGULYk91.js → sankeyDiagram-HTMAVEWB-DLuhNvTd.js} +1 -1
  50. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BBjCjIFm.js → sequenceDiagram-DBY2YBRQ-BorPhsA5.js} +1 -1
  51. package/dist/client/assets/{sizeCapture-X5ZJPWSS-DjP0UZy6.js → sizeCapture-X5ZJPWSS-DPXho3_K.js} +1 -1
  52. package/dist/client/assets/{stateDiagram-2N3HPSRC-BTqu5zVt.js → stateDiagram-2N3HPSRC-DSbrRqVk.js} +1 -1
  53. package/dist/client/assets/stateDiagram-v2-6OUMAXLB--bVRWeRL.js +1 -0
  54. package/dist/client/assets/{swimlanes-5IMT3BWC-ClySuTz2.js → swimlanes-5IMT3BWC-D4A4axIk.js} +2 -2
  55. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CqWtgk0W.js +8 -0
  56. package/dist/client/assets/{timeline-definition-FHXFAJF6-DHURyh7H.js → timeline-definition-FHXFAJF6-D4ObRKT8.js} +1 -1
  57. package/dist/client/assets/{vennDiagram-L72KCM5P-BFdn5KML.js → vennDiagram-L72KCM5P-Bj_4-RtP.js} +1 -1
  58. package/dist/client/assets/{wardleyDiagram-EHGQE667-DKEHwCwO.js → wardleyDiagram-EHGQE667-BNvcAUoQ.js} +1 -1
  59. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CI5HzOMq.js → xychartDiagram-FW5EYKEG-BfGV9sTq.js} +1 -1
  60. package/dist/client/index.html +2 -2
  61. package/dist/client/sw.js +1 -1
  62. package/dist/server/agent/automationStore.js +12 -7
  63. package/dist/server/agent/sessionSearchStore.js +135 -5
  64. package/dist/server/entry.js +24 -1
  65. package/dist/server/routes/terminal.js +6 -2
  66. package/dist/server/utils/runtimeMonitor.js +287 -0
  67. package/dist/server/utils/settings.js +9 -0
  68. package/package.json +1 -1
  69. package/runtime-manifest.json +2 -2
  70. package/dist/client/assets/channel-D1KecDCp.js +0 -1
  71. package/dist/client/assets/classDiagram-OUVF2IWQ-BYJ3p4nE.js +0 -1
  72. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BYJ3p4nE.js +0 -1
  73. package/dist/client/assets/index-BwMQSNQq.js +0 -456
  74. package/dist/client/assets/index-RGd2Qu1X.css +0 -1
  75. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-D5FsphfH.js +0 -1
  76. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C1_CRJSE.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
- constructor(directory) {
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 temporaryPath = `${this.metadataPath()}.${process.pid}.tmp`;
88
- fs.writeFileSync(temporaryPath, JSON.stringify({ version: 1, sessions: [...this.metadata.values()] }, null, 2), { mode: 0o600 });
89
- fs.renameSync(temporaryPath, this.metadataPath());
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`; }
@@ -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 app = createApp({ port: options.onboardingPort ?? port, httpsCaPath: options.httpsCaPath, localApiToken: options.localApiToken });
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),
@@ -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
- writeTerminalInput(session, `${formatCollaborationDelivery(frontendSessionId, pending)}\r`);
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,287 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { monitorEventLoopDelay } from 'node:perf_hooks';
5
+ const SAMPLE_INTERVAL_MS = 10_000;
6
+ const HISTORY_INTERVAL_MS = 60_000;
7
+ const MAX_HISTORY_SAMPLES = 24 * 60;
8
+ function round(value, digits = 2) {
9
+ if (!Number.isFinite(value))
10
+ return 0;
11
+ const scale = 10 ** digits;
12
+ return Math.round(value * scale) / scale;
13
+ }
14
+ function percentile(values, quantile) {
15
+ if (values.length === 0)
16
+ return 0;
17
+ const ordered = [...values].sort((a, b) => a - b);
18
+ return ordered[Math.min(ordered.length - 1, Math.floor(ordered.length * quantile))] ?? 0;
19
+ }
20
+ function readProcessIo() {
21
+ if (process.platform !== 'linux')
22
+ return { readBytes: 0, writeBytes: 0 };
23
+ try {
24
+ const values = new Map(fs.readFileSync('/proc/self/io', 'utf8').trim().split('\n').map((line) => {
25
+ const separator = line.indexOf(':');
26
+ return [line.slice(0, separator), Number(line.slice(separator + 1).trim())];
27
+ }));
28
+ return { readBytes: values.get('read_bytes') ?? 0, writeBytes: values.get('write_bytes') ?? 0 };
29
+ }
30
+ catch {
31
+ return { readBytes: 0, writeBytes: 0 };
32
+ }
33
+ }
34
+ async function collectStorageSnapshot(root, maxEntries = 20_000) {
35
+ const totals = new Map();
36
+ const queue = [root];
37
+ let totalBytes = 0;
38
+ let fileCount = 0;
39
+ let visited = 0;
40
+ while (queue.length > 0 && visited < maxEntries) {
41
+ const directory = queue.shift();
42
+ let entries;
43
+ try {
44
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
45
+ }
46
+ catch {
47
+ continue;
48
+ }
49
+ for (const entry of entries) {
50
+ if (visited >= maxEntries)
51
+ break;
52
+ visited += 1;
53
+ const entryPath = path.join(directory, entry.name);
54
+ if (entry.isDirectory()) {
55
+ queue.push(entryPath);
56
+ continue;
57
+ }
58
+ if (!entry.isFile())
59
+ continue;
60
+ try {
61
+ const stat = await fs.promises.stat(entryPath);
62
+ totalBytes += stat.size;
63
+ fileCount += 1;
64
+ const topLevelName = path.relative(root, entryPath).split(path.sep)[0] || entry.name;
65
+ totals.set(topLevelName, (totals.get(topLevelName) ?? 0) + stat.size);
66
+ }
67
+ catch { /* The file may disappear while diagnostics walk the directory. */ }
68
+ }
69
+ }
70
+ let freeBytes = null;
71
+ let totalFilesystemBytes = null;
72
+ try {
73
+ const stat = await fs.promises.statfs(root);
74
+ freeBytes = stat.bavail * stat.bsize;
75
+ totalFilesystemBytes = stat.blocks * stat.bsize;
76
+ }
77
+ catch { /* Not available on every supported filesystem. */ }
78
+ return {
79
+ root, totalBytes, fileCount, truncated: queue.length > 0 || visited >= maxEntries,
80
+ freeBytes, totalFilesystemBytes,
81
+ topLevel: [...totals.entries()].map(([name, bytes]) => ({ name, bytes })).sort((a, b) => b.bytes - a.bytes).slice(0, 20),
82
+ };
83
+ }
84
+ export function evaluateRuntimeHealth(sample, storage) {
85
+ const findings = [];
86
+ const add = (severity, code, message, value, threshold) => {
87
+ findings.push({ severity, code, message, value: round(value), threshold });
88
+ };
89
+ if (sample.rssBytes >= 1024 ** 3)
90
+ add('critical', 'PROCESS_RSS_HIGH', 'Termdock 内存占用超过 1 GiB', sample.rssBytes, 1024 ** 3);
91
+ else if (sample.rssBytes >= 512 * 1024 ** 2)
92
+ add('warning', 'PROCESS_RSS_HIGH', 'Termdock 内存占用超过 512 MiB', sample.rssBytes, 512 * 1024 ** 2);
93
+ if (sample.eventLoopDelayP99Ms >= 500)
94
+ add('critical', 'EVENT_LOOP_STALLED', 'Node.js 事件循环 P99 延迟过高', sample.eventLoopDelayP99Ms, 500);
95
+ else if (sample.eventLoopDelayP99Ms >= 100)
96
+ add('warning', 'EVENT_LOOP_SLOW', 'Node.js 事件循环 P99 延迟偏高', sample.eventLoopDelayP99Ms, 100);
97
+ if (sample.diskWriteBytesPerSecond >= 25 * 1024 ** 2)
98
+ add('critical', 'DISK_WRITE_RATE_HIGH', 'Termdock 持续磁盘写入速率过高', sample.diskWriteBytesPerSecond, 25 * 1024 ** 2);
99
+ else if (sample.diskWriteBytesPerSecond >= 5 * 1024 ** 2)
100
+ add('warning', 'DISK_WRITE_RATE_HIGH', 'Termdock 磁盘写入速率偏高', sample.diskWriteBytesPerSecond, 5 * 1024 ** 2);
101
+ if (sample.activeConnections >= 1000)
102
+ add('critical', 'CONNECTION_COUNT_HIGH', 'Termdock 活跃连接数过高', sample.activeConnections, 1000);
103
+ else if (sample.activeConnections >= 200)
104
+ add('warning', 'CONNECTION_COUNT_HIGH', 'Termdock 活跃连接数偏高', sample.activeConnections, 200);
105
+ if (sample.requestsPerSecond >= 0.25 && sample.requestErrorRatio >= 0.25)
106
+ add('critical', 'REQUEST_ERROR_RATIO_HIGH', '最近采样窗口请求错误率过高', sample.requestErrorRatio, 0.25);
107
+ else if (sample.requestsPerSecond >= 0.25 && sample.requestErrorRatio >= 0.1)
108
+ add('warning', 'REQUEST_ERROR_RATIO_HIGH', '最近采样窗口请求错误率偏高', sample.requestErrorRatio, 0.1);
109
+ if (sample.requestLatencyP95Ms >= 5_000)
110
+ add('critical', 'REQUEST_LATENCY_HIGH', '请求 P95 延迟超过 5 秒', sample.requestLatencyP95Ms, 5_000);
111
+ else if (sample.requestLatencyP95Ms >= 1_000)
112
+ add('warning', 'REQUEST_LATENCY_HIGH', '请求 P95 延迟超过 1 秒', sample.requestLatencyP95Ms, 1_000);
113
+ if (storage.totalBytes >= 2 * 1024 ** 3)
114
+ add('critical', 'STATE_DIRECTORY_LARGE', 'Termdock 状态目录超过 2 GiB', storage.totalBytes, 2 * 1024 ** 3);
115
+ else if (storage.totalBytes >= 512 * 1024 ** 2)
116
+ add('warning', 'STATE_DIRECTORY_LARGE', 'Termdock 状态目录超过 512 MiB', storage.totalBytes, 512 * 1024 ** 2);
117
+ if (storage.freeBytes !== null && storage.totalFilesystemBytes) {
118
+ const freeRatio = storage.freeBytes / storage.totalFilesystemBytes;
119
+ if (storage.freeBytes <= 1024 ** 3 || freeRatio <= 0.05)
120
+ add('critical', 'DISK_SPACE_LOW', 'Termdock 所在磁盘剩余空间严重不足', freeRatio, 0.05);
121
+ else if (storage.freeBytes <= 5 * 1024 ** 3 || freeRatio <= 0.1)
122
+ add('warning', 'DISK_SPACE_LOW', 'Termdock 所在磁盘剩余空间偏低', freeRatio, 0.1);
123
+ }
124
+ return findings;
125
+ }
126
+ export class RuntimeMonitor {
127
+ stateDirectory;
128
+ historyPath;
129
+ sampleIntervalMs;
130
+ historyIntervalMs;
131
+ maxHistorySamples;
132
+ sockets = new Map();
133
+ eventLoopHistogram;
134
+ history = [];
135
+ current = null;
136
+ sampleTimer = null;
137
+ lastHistoryAt = 0;
138
+ previousAt = Date.now();
139
+ previousCpu = process.cpuUsage();
140
+ previousIo = readProcessIo();
141
+ closedSocketBytes = { read: 0, written: 0 };
142
+ previousSocketBytes = { read: 0, written: 0 };
143
+ requestDurations = [];
144
+ requestCount = 0;
145
+ requestErrors = 0;
146
+ constructor(options) {
147
+ this.stateDirectory = options.stateDirectory;
148
+ this.historyPath = options.historyPath ?? null;
149
+ this.sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
150
+ this.historyIntervalMs = options.historyIntervalMs ?? HISTORY_INTERVAL_MS;
151
+ this.maxHistorySamples = options.maxHistorySamples ?? MAX_HISTORY_SAMPLES;
152
+ this.eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });
153
+ this.loadHistory();
154
+ }
155
+ start() {
156
+ if (this.sampleTimer)
157
+ return;
158
+ this.eventLoopHistogram.enable();
159
+ this.sample();
160
+ this.sampleTimer = setInterval(() => this.sample(), this.sampleIntervalMs);
161
+ this.sampleTimer.unref?.();
162
+ }
163
+ stop() {
164
+ if (this.sampleTimer)
165
+ clearInterval(this.sampleTimer);
166
+ this.sampleTimer = null;
167
+ this.eventLoopHistogram.disable();
168
+ }
169
+ trackSocket(socket) {
170
+ if (this.sockets.has(socket))
171
+ return;
172
+ this.sockets.set(socket, { read: socket.bytesRead, written: socket.bytesWritten });
173
+ socket.once('close', () => {
174
+ const baseline = this.sockets.get(socket);
175
+ if (!baseline)
176
+ return;
177
+ this.closedSocketBytes.read += Math.max(0, socket.bytesRead - baseline.read);
178
+ this.closedSocketBytes.written += Math.max(0, socket.bytesWritten - baseline.written);
179
+ this.sockets.delete(socket);
180
+ });
181
+ }
182
+ recordRequest(statusCode, durationMs) {
183
+ this.requestCount += 1;
184
+ if (statusCode >= 400)
185
+ this.requestErrors += 1;
186
+ if (Number.isFinite(durationMs))
187
+ this.requestDurations.push(durationMs);
188
+ if (this.requestDurations.length > 10_000)
189
+ this.requestDurations.splice(0, this.requestDurations.length - 10_000);
190
+ }
191
+ sample(now = Date.now()) {
192
+ const elapsedSeconds = Math.max(0.001, (now - this.previousAt) / 1000);
193
+ const cpu = process.cpuUsage(this.previousCpu);
194
+ const cpuProcessPercent = (cpu.user + cpu.system) / 1_000_000 / elapsedSeconds * 100;
195
+ const io = readProcessIo();
196
+ const socketBytes = this.getSocketBytes();
197
+ const memory = process.memoryUsage();
198
+ const requestCount = this.requestCount;
199
+ const result = {
200
+ timestamp: now,
201
+ uptimeSeconds: round(process.uptime()),
202
+ cpuProcessPercent: round(cpuProcessPercent),
203
+ cpuHostCapacityPercent: round(cpuProcessPercent / Math.max(1, os.availableParallelism())),
204
+ rssBytes: memory.rss,
205
+ heapUsedBytes: memory.heapUsed,
206
+ heapTotalBytes: memory.heapTotal,
207
+ externalBytes: memory.external,
208
+ eventLoopDelayP99Ms: round(this.eventLoopHistogram.percentile(99) / 1_000_000),
209
+ diskReadBytesPerSecond: round(Math.max(0, io.readBytes - this.previousIo.readBytes) / elapsedSeconds),
210
+ diskWriteBytesPerSecond: round(Math.max(0, io.writeBytes - this.previousIo.writeBytes) / elapsedSeconds),
211
+ networkReadBytesPerSecond: round(Math.max(0, socketBytes.read - this.previousSocketBytes.read) / elapsedSeconds),
212
+ networkWriteBytesPerSecond: round(Math.max(0, socketBytes.written - this.previousSocketBytes.written) / elapsedSeconds),
213
+ activeConnections: this.sockets.size,
214
+ requestsPerSecond: round(requestCount / elapsedSeconds),
215
+ requestErrorRatio: round(requestCount > 0 ? this.requestErrors / requestCount : 0, 4),
216
+ requestLatencyP95Ms: round(percentile(this.requestDurations, 0.95)),
217
+ };
218
+ this.current = result;
219
+ this.previousAt = now;
220
+ this.previousCpu = process.cpuUsage();
221
+ this.previousIo = io;
222
+ this.previousSocketBytes = socketBytes;
223
+ this.requestCount = 0;
224
+ this.requestErrors = 0;
225
+ this.requestDurations = [];
226
+ this.eventLoopHistogram.reset();
227
+ if (now - this.lastHistoryAt >= this.historyIntervalMs) {
228
+ this.history.push(result);
229
+ this.history = this.history.slice(-this.maxHistorySamples);
230
+ this.lastHistoryAt = now;
231
+ this.appendHistory(result);
232
+ }
233
+ return result;
234
+ }
235
+ async diagnostics() {
236
+ const current = this.current ?? this.sample();
237
+ const storage = await collectStorageSnapshot(this.stateDirectory);
238
+ const findings = evaluateRuntimeHealth(current, storage);
239
+ return {
240
+ status: findings.some((item) => item.severity === 'critical') ? 'critical' : findings.length > 0 ? 'warning' : 'ok',
241
+ generatedAt: Date.now(), current, history: [...this.history], storage, findings,
242
+ scopes: { cpu: 'termdock-process', memory: 'termdock-process', diskIo: 'termdock-process', networkIo: 'termdock-http-sockets', storage: 'termdock-state-directory' },
243
+ };
244
+ }
245
+ getSocketBytes() {
246
+ let read = this.closedSocketBytes.read;
247
+ let written = this.closedSocketBytes.written;
248
+ for (const [socket, baseline] of this.sockets) {
249
+ read += Math.max(0, socket.bytesRead - baseline.read);
250
+ written += Math.max(0, socket.bytesWritten - baseline.written);
251
+ }
252
+ return { read, written };
253
+ }
254
+ loadHistory() {
255
+ if (!this.historyPath)
256
+ return;
257
+ try {
258
+ this.history = fs.readFileSync(this.historyPath, 'utf8').trim().split('\n').flatMap((line) => {
259
+ try {
260
+ const item = JSON.parse(line);
261
+ return Number.isFinite(item.timestamp) ? [item] : [];
262
+ }
263
+ catch {
264
+ return [];
265
+ }
266
+ }).slice(-this.maxHistorySamples);
267
+ this.lastHistoryAt = this.history.at(-1)?.timestamp ?? 0;
268
+ }
269
+ catch { /* First run or a missing/corrupt history file. */ }
270
+ }
271
+ appendHistory(sample) {
272
+ if (!this.historyPath)
273
+ return;
274
+ void fs.promises.mkdir(path.dirname(this.historyPath), { recursive: true })
275
+ .then(() => fs.promises.appendFile(this.historyPath, `${JSON.stringify(sample)}\n`, { mode: 0o600 }))
276
+ .then(async () => {
277
+ const stat = await fs.promises.stat(this.historyPath);
278
+ if (stat.size <= 2 * 1024 * 1024)
279
+ return;
280
+ const retained = this.history.map((item) => JSON.stringify(item)).join('\n') + '\n';
281
+ const temporary = `${this.historyPath}.${process.pid}.tmp`;
282
+ await fs.promises.writeFile(temporary, retained, { mode: 0o600 });
283
+ await fs.promises.rename(temporary, this.historyPath);
284
+ })
285
+ .catch(() => undefined);
286
+ }
287
+ }
@@ -105,6 +105,7 @@ function normalizeSettings(value) {
105
105
  ? raw.autoRenamePromptPayloadChars
106
106
  : 12_000,
107
107
  newSessionAgentSlug: normalizeNewSessionAgentSlug(raw.newSessionAgentSlug),
108
+ runningSessionButtonEnabled: raw.runningSessionButtonEnabled === true,
108
109
  updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : Date.now(),
109
110
  };
110
111
  }
@@ -399,3 +400,11 @@ export function setNewSessionAgentSlugSetting(slug) {
399
400
  settings.newSessionAgentSlug = normalizeNewSessionAgentSlug(slug);
400
401
  });
401
402
  }
403
+ export function getRunningSessionButtonEnabledSetting() {
404
+ return loadSettings().runningSessionButtonEnabled;
405
+ }
406
+ export function setRunningSessionButtonEnabledSetting(enabled) {
407
+ return updateSettings((settings) => {
408
+ settings.runningSessionButtonEnabled = enabled;
409
+ });
410
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.108",
3
+ "version": "1.4.110",
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.108",
4
+ "version": "1.4.110",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-3I9X7zF4J4BUYOvxi3lIuWSKfDy6S6GD61w8Ay95Ypw=",
9
- "serverBundleHash": "sha256-KSGNyOcLMhHQwPHxSz7noGVaFIWukO1ExTVBIqm61Zc=",
9
+ "serverBundleHash": "sha256-FzVprEN6mYO+IXk9xqujHpNv1s544akAZOVv28fGvww=",
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-DLidsBlx.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-LTTPDSzs.js";import{b as i}from"./mermaid.core-DLidsBlx.js";import"./chunk-5VM5RSS4-C984gGSO.js";import"./chunk-XXDRQBXY-DiUZr3na.js";import"./chunk-VR4S4FIN-dCZ9yGOH.js";import"./chunk-32BRIVSS-B50lSI4-.js";import"./purify.es-CC4Brkr_.js";import"./index-BwMQSNQq.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-LTTPDSzs.js";import{b as i}from"./mermaid.core-DLidsBlx.js";import"./chunk-5VM5RSS4-C984gGSO.js";import"./chunk-XXDRQBXY-DiUZr3na.js";import"./chunk-VR4S4FIN-dCZ9yGOH.js";import"./chunk-32BRIVSS-B50lSI4-.js";import"./purify.es-CC4Brkr_.js";import"./index-BwMQSNQq.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};