openspecui 1.3.0 → 1.5.0

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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.mjs +9 -6
  3. package/dist/index.mjs +1 -1
  4. package/dist/{open-BVmQScxd.mjs → open-DDagk2eo.mjs} +2 -2
  5. package/dist/{src-E2ERj6H4.mjs → src-16GA3our.mjs} +1014 -233
  6. package/package.json +3 -3
  7. package/web/assets/{BufferResource-8LHM3mct.js → BufferResource-Bn1UWy0D.js} +1 -1
  8. package/web/assets/{CanvasRenderer-BSV3tOcF.js → CanvasRenderer-D8NiU8la.js} +1 -1
  9. package/web/assets/{Filter-C5FD597y.js → Filter-CRwq487x.js} +1 -1
  10. package/web/assets/{RenderTargetSystem-DlUN0zsW.js → RenderTargetSystem-CtoB_qTm.js} +1 -1
  11. package/web/assets/{WebGLRenderer-QgEdU6ZZ.js → WebGLRenderer-BgKO8R0a.js} +1 -1
  12. package/web/assets/{WebGPURenderer-D7lHEg9G.js → WebGPURenderer-CQeL2efC.js} +1 -1
  13. package/web/assets/{browserAll-C7nnv_eJ.js → browserAll-DP6sOYev.js} +1 -1
  14. package/web/assets/ghostty-web-evxujSxm.js +13 -0
  15. package/web/assets/{index-Ar3cmjnK.js → index-4MAU81Qk.js} +1 -1
  16. package/web/assets/{index-Byo82vIc.js → index-B0IbsqHi.js} +1 -1
  17. package/web/assets/{index-Nzl3W_bm.js → index-B147AOgf.js} +1 -1
  18. package/web/assets/{index-CDAgriaJ.js → index-BMashGQn.js} +1 -1
  19. package/web/assets/{index-B0Q8Tr0G.js → index-BPZ3nG0r.js} +1 -1
  20. package/web/assets/{index-C8uGGt6w.js → index-BejnsZfY.js} +1 -1
  21. package/web/assets/{index-Dte58iQe.js → index-BnT52DZ8.js} +1 -1
  22. package/web/assets/{index-BFbdtMlr.js → index-CBCPR3Qb.js} +1 -1
  23. package/web/assets/{index-CYijlhdV.js → index-D2Tp4F9B.js} +1 -1
  24. package/web/assets/{index-JGA2Yc2F.js → index-D6ardy54.js} +1 -1
  25. package/web/assets/{index-DwUe2J4w.js → index-DJqmTRAR.js} +1 -1
  26. package/web/assets/{index-DYX6cNJ6.js → index-DTeOcXKn.js} +1 -1
  27. package/web/assets/{index-BkWa50ks.js → index-DcXyAs0z.js} +1 -1
  28. package/web/assets/{index-BJlwU-uQ.js → index-T8xoxmUb.js} +222 -219
  29. package/web/assets/index-Ys2MTD3W.css +1 -0
  30. package/web/assets/{index-C_c_g9pe.js → index-dSf1u0YV.js} +1 -1
  31. package/web/assets/{index-BdGDc8qr.js → index-f0QdJSzm.js} +1 -1
  32. package/web/assets/{webworkerAll-CEAZP48d.js → webworkerAll-DA2HufNb.js} +1 -1
  33. package/web/index.html +2 -2
  34. package/web/assets/index-ZMhgIxpu.css +0 -1
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
- import { createServer } from "http";
2
+ import { createServer } from "node:net";
3
+ import { createServer as createServer$1 } from "http";
3
4
  import { Http2ServerRequest } from "http2";
4
5
  import { Readable } from "stream";
5
6
  import crypto from "crypto";
@@ -8,14 +9,15 @@ import { dirname, join } from "path";
8
9
  import { AsyncLocalStorage } from "node:async_hooks";
9
10
  import { mkdir as mkdir$1, readFile as readFile$1, readdir, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
10
11
  import { dirname as dirname$1, join as join$1, matchesGlob, relative as relative$1, resolve as resolve$1, sep } from "node:path";
11
- import { existsSync, readFileSync, realpathSync, statSync, utimesSync } from "node:fs";
12
- import { watch } from "fs";
12
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
13
13
  import { EventEmitter } from "events";
14
+ import { watch } from "fs";
14
15
  import { exec, spawn } from "child_process";
15
16
  import { promisify } from "util";
16
- import { createServer as createServer$1 } from "node:net";
17
17
  import * as pty from "@lydell/node-pty";
18
+ import { execFile } from "node:child_process";
18
19
  import { EventEmitter as EventEmitter$1 } from "node:events";
20
+ import { promisify as promisify$1 } from "node:util";
19
21
  import { Worker as Worker$1 } from "node:worker_threads";
20
22
  import { fileURLToPath } from "node:url";
21
23
 
@@ -49,6 +51,41 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
49
51
  }) : target, mod));
50
52
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
51
53
 
54
+ //#endregion
55
+ //#region ../server/src/port-utils.ts
56
+ /**
57
+ * Check if a port is available by trying to listen on it.
58
+ * Uses default binding (both IPv4 and IPv6) to detect conflicts.
59
+ */
60
+ function isPortAvailable(port) {
61
+ return new Promise((resolve$2) => {
62
+ const server = createServer();
63
+ server.once("error", () => {
64
+ resolve$2(false);
65
+ });
66
+ server.once("listening", () => {
67
+ server.close(() => resolve$2(true));
68
+ });
69
+ server.listen(port);
70
+ });
71
+ }
72
+ /**
73
+ * Find an available port starting from the given port.
74
+ * Will try up to maxAttempts ports sequentially.
75
+ *
76
+ * @param startPort - The preferred port to start checking from
77
+ * @param maxAttempts - Maximum number of ports to try (default: 10)
78
+ * @returns The first available port found
79
+ * @throws Error if no available port is found in the range
80
+ */
81
+ async function findAvailablePort(startPort, maxAttempts = 10) {
82
+ for (let i = 0; i < maxAttempts; i++) {
83
+ const port = startPort + i;
84
+ if (await isPortAvailable(port)) return port;
85
+ }
86
+ throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
87
+ }
88
+
52
89
  //#endregion
53
90
  //#region ../../node_modules/.pnpm/@hono+node-server@1.19.6_hono@4.10.6/node_modules/@hono/node-server/dist/index.mjs
54
91
  var RequestError = class extends Error {
@@ -476,7 +513,7 @@ var createAdaptorServer = (options) => {
476
513
  overrideGlobalObjects: options.overrideGlobalObjects,
477
514
  autoCleanupIncoming: options.autoCleanupIncoming
478
515
  });
479
- return (options.createServer || createServer)(options.serverOptions || {}, requestListener);
516
+ return (options.createServer || createServer$1)(options.serverOptions || {}, requestListener);
480
517
  };
481
518
  var serve = (options, listeningListener) => {
482
519
  const server = createAdaptorServer(options);
@@ -697,14 +734,14 @@ var MarkdownParser = class {
697
734
  if (currentOperation === "RENAMED") {
698
735
  const fromMatch = line.match(/FROM:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
699
736
  const toMatch = line.match(/TO:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
700
- if (fromMatch) renameBuffer = {
701
- ...renameBuffer ?? {},
702
- from: fromMatch[1].trim()
703
- };
704
- if (toMatch) renameBuffer = {
705
- ...renameBuffer ?? {},
706
- to: toMatch[1].trim()
707
- };
737
+ if (fromMatch) {
738
+ if (!renameBuffer) renameBuffer = {};
739
+ renameBuffer.from = fromMatch[1].trim();
740
+ }
741
+ if (toMatch) {
742
+ if (!renameBuffer) renameBuffer = {};
743
+ renameBuffer.to = toMatch[1].trim();
744
+ }
708
745
  if (renameBuffer?.from && renameBuffer?.to) {
709
746
  deltas.push({
710
747
  spec: deltaSpec.specId,
@@ -789,86 +826,6 @@ var MarkdownParser = class {
789
826
  }
790
827
  };
791
828
 
792
- //#endregion
793
- //#region ../core/src/validator.ts
794
- /**
795
- * Validator for OpenSpec documents
796
- */
797
- var Validator = class {
798
- /**
799
- * Validate a spec document
800
- */
801
- validateSpec(spec) {
802
- const issues = [];
803
- if (!spec.overview || spec.overview.trim().length === 0) issues.push({
804
- severity: "ERROR",
805
- message: "Spec must have a Purpose/Overview section",
806
- path: "overview"
807
- });
808
- if (spec.requirements.length === 0) issues.push({
809
- severity: "ERROR",
810
- message: "Spec must have at least one requirement",
811
- path: "requirements"
812
- });
813
- for (const req of spec.requirements) {
814
- if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
815
- severity: "WARNING",
816
- message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
817
- path: `requirements.${req.id}`
818
- });
819
- if (req.scenarios.length === 0) issues.push({
820
- severity: "WARNING",
821
- message: `Requirement should have at least one scenario: ${req.id}`,
822
- path: `requirements.${req.id}.scenarios`
823
- });
824
- if (req.text.length > 1e3) issues.push({
825
- severity: "WARNING",
826
- message: `Requirement text is too long (max 1000 chars): ${req.id}`,
827
- path: `requirements.${req.id}.text`
828
- });
829
- }
830
- return {
831
- valid: issues.filter((i) => i.severity === "ERROR").length === 0,
832
- issues
833
- };
834
- }
835
- /**
836
- * Validate a change proposal
837
- */
838
- validateChange(change) {
839
- const issues = [];
840
- if (!change.why || change.why.length < 50) issues.push({
841
- severity: "ERROR",
842
- message: "Change \"Why\" section must be at least 50 characters",
843
- path: "why"
844
- });
845
- if (change.why && change.why.length > 500) issues.push({
846
- severity: "WARNING",
847
- message: "Change \"Why\" section should be under 500 characters",
848
- path: "why"
849
- });
850
- if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
851
- severity: "ERROR",
852
- message: "Change must have a \"What Changes\" section",
853
- path: "whatChanges"
854
- });
855
- if (change.deltas.length === 0) issues.push({
856
- severity: "WARNING",
857
- message: "Change should have at least one delta",
858
- path: "deltas"
859
- });
860
- if (change.deltas.length > 50) issues.push({
861
- severity: "WARNING",
862
- message: "Change has too many deltas (max 50)",
863
- path: "deltas"
864
- });
865
- return {
866
- valid: issues.filter((i) => i.severity === "ERROR").length === 0,
867
- issues
868
- };
869
- }
870
- };
871
-
872
829
  //#endregion
873
830
  //#region ../core/src/reactive-fs/reactive-state.ts
874
831
  /**
@@ -1055,8 +1012,10 @@ const DEFAULT_IGNORE = [
1055
1012
  ".git",
1056
1013
  "**/.DS_Store"
1057
1014
  ];
1058
- /** 健康检查间隔 (ms) - 3秒 */
1059
- const HEALTH_CHECK_INTERVAL_MS = 3e3;
1015
+ /** 恢复重试间隔 (ms) */
1016
+ const RECOVERY_INTERVAL_MS = 3e3;
1017
+ /** 路径语义检查间隔 (ms) */
1018
+ const PATH_LIVENESS_INTERVAL_MS = 3e3;
1060
1019
  /**
1061
1020
  * 项目监听器
1062
1021
  *
@@ -1079,17 +1038,25 @@ var ProjectWatcher = class {
1079
1038
  ignore;
1080
1039
  initialized = false;
1081
1040
  initPromise = null;
1082
- healthCheckTimer = null;
1083
- lastEventTime = 0;
1084
- healthCheckPending = false;
1085
- enableHealthCheck;
1086
1041
  reinitializeTimer = null;
1087
1042
  reinitializePending = false;
1043
+ reinitializeReasonPending = null;
1044
+ pathLivenessTimer = null;
1045
+ projectDirFingerprint = null;
1046
+ generation = 0;
1047
+ reinitializeCount = 0;
1048
+ lastReinitializeReason = null;
1049
+ reinitializeReasonCounts = {
1050
+ "drop-events": 0,
1051
+ "watcher-error": 0,
1052
+ "missing-project-dir": 0,
1053
+ "project-dir-replaced": 0,
1054
+ manual: 0
1055
+ };
1088
1056
  constructor(projectDir, options = {}) {
1089
1057
  this.projectDir = getRealPath$1(projectDir);
1090
1058
  this.debounceMs = options.debounceMs ?? DEBOUNCE_MS$1;
1091
1059
  this.ignore = options.ignore ?? DEFAULT_IGNORE;
1092
- this.enableHealthCheck = options.enableHealthCheck ?? true;
1093
1060
  }
1094
1061
  /**
1095
1062
  * 初始化 watcher
@@ -1098,8 +1065,11 @@ var ProjectWatcher = class {
1098
1065
  async init() {
1099
1066
  if (this.initialized) return;
1100
1067
  if (this.initPromise) return this.initPromise;
1101
- this.initPromise = this.doInit();
1102
- await this.initPromise;
1068
+ this.initPromise = this.doInit().catch((error) => {
1069
+ this.initPromise = null;
1070
+ throw error;
1071
+ });
1072
+ return this.initPromise;
1103
1073
  }
1104
1074
  async doInit() {
1105
1075
  this.subscription = await (await import("@parcel/watcher")).subscribe(this.projectDir, (err, events) => {
@@ -1110,43 +1080,98 @@ var ProjectWatcher = class {
1110
1080
  this.handleEvents(events);
1111
1081
  }, { ignore: this.ignore });
1112
1082
  this.initialized = true;
1113
- this.lastEventTime = Date.now();
1114
- if (this.enableHealthCheck) this.startHealthCheck();
1083
+ this.generation += 1;
1084
+ this.projectDirFingerprint = this.getProjectDirFingerprint();
1085
+ this.startPathLivenessMonitor();
1115
1086
  }
1116
1087
  /**
1117
1088
  * 处理 watcher 错误
1118
- * 对于 FSEvents dropped 错误,触发延迟重建
1089
+ * 统一走错误驱动重建流程
1119
1090
  */
1120
1091
  handleWatcherError(err) {
1121
1092
  if ((err.message || String(err)).includes("Events were dropped")) {
1122
1093
  if (!this.reinitializePending) {
1123
1094
  console.warn("[ProjectWatcher] FSEvents dropped events, scheduling reinitialize...");
1124
- this.scheduleReinitialize();
1095
+ this.scheduleReinitialize("drop-events");
1125
1096
  }
1126
1097
  return;
1127
1098
  }
1128
- console.error("[ProjectWatcher] Error:", err);
1099
+ console.error("[ProjectWatcher] Watcher error, scheduling reinitialize:", err);
1100
+ this.scheduleReinitialize("watcher-error");
1129
1101
  }
1130
1102
  /**
1131
1103
  * 延迟重建 watcher(防抖,避免频繁重建)
1132
1104
  */
1133
- scheduleReinitialize() {
1105
+ scheduleReinitialize(reason) {
1106
+ this.reinitializeReasonPending = reason;
1134
1107
  if (this.reinitializePending) return;
1135
1108
  this.reinitializePending = true;
1136
1109
  if (this.reinitializeTimer) clearTimeout(this.reinitializeTimer);
1137
1110
  this.reinitializeTimer = setTimeout(() => {
1138
1111
  this.reinitializeTimer = null;
1139
1112
  this.reinitializePending = false;
1140
- console.log("[ProjectWatcher] Reinitializing due to FSEvents error...");
1141
- this.reinitialize();
1142
- }, 1e3);
1113
+ const pendingReason = this.reinitializeReasonPending ?? reason;
1114
+ this.reinitializeReasonPending = null;
1115
+ console.log(`[ProjectWatcher] Reinitializing (reason: ${pendingReason})...`);
1116
+ this.reinitialize(pendingReason);
1117
+ }, RECOVERY_INTERVAL_MS);
1118
+ this.reinitializeTimer.unref();
1119
+ }
1120
+ /**
1121
+ * 读取项目目录指纹(目录不存在时返回 null)
1122
+ * 用于检测 path 对应实体是否被替换(inode/dev 漂移)
1123
+ */
1124
+ getProjectDirFingerprint() {
1125
+ try {
1126
+ const stat$1 = lstatSync(this.projectDir);
1127
+ return `${stat$1.dev}:${stat$1.ino}`;
1128
+ } catch {
1129
+ return null;
1130
+ }
1131
+ }
1132
+ /**
1133
+ * 启动路径语义监测(避免 watcher 绑定到已失效句柄)
1134
+ */
1135
+ startPathLivenessMonitor() {
1136
+ this.stopPathLivenessMonitor();
1137
+ this.pathLivenessTimer = setInterval(() => {
1138
+ this.checkPathLiveness();
1139
+ }, PATH_LIVENESS_INTERVAL_MS);
1140
+ this.pathLivenessTimer.unref();
1141
+ }
1142
+ /**
1143
+ * 停止路径语义监测
1144
+ */
1145
+ stopPathLivenessMonitor() {
1146
+ if (this.pathLivenessTimer) {
1147
+ clearInterval(this.pathLivenessTimer);
1148
+ this.pathLivenessTimer = null;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * 只读检查 projectDir 是否仍指向初始化时的目录实体
1153
+ */
1154
+ checkPathLiveness() {
1155
+ if (!this.initialized || this.reinitializePending) return;
1156
+ const current = this.getProjectDirFingerprint();
1157
+ if (current === null) {
1158
+ console.warn("[ProjectWatcher] Project directory missing, scheduling reinitialize...");
1159
+ this.scheduleReinitialize("missing-project-dir");
1160
+ return;
1161
+ }
1162
+ if (this.projectDirFingerprint === null) {
1163
+ this.projectDirFingerprint = current;
1164
+ return;
1165
+ }
1166
+ if (current !== this.projectDirFingerprint) {
1167
+ console.warn("[ProjectWatcher] Project directory replaced, scheduling reinitialize...");
1168
+ this.scheduleReinitialize("project-dir-replaced");
1169
+ }
1143
1170
  }
1144
1171
  /**
1145
1172
  * 处理原始事件
1146
1173
  */
1147
1174
  handleEvents(events) {
1148
- this.lastEventTime = Date.now();
1149
- this.healthCheckPending = false;
1150
1175
  const watchEvents = events.map((e) => ({
1151
1176
  type: e.type,
1152
1177
  path: e.path
@@ -1234,60 +1259,29 @@ var ProjectWatcher = class {
1234
1259
  return this.initialized;
1235
1260
  }
1236
1261
  /**
1237
- * 启动健康检查定时器
1262
+ * 获取 watcher 运行时状态
1238
1263
  */
1239
- startHealthCheck() {
1240
- this.stopHealthCheck();
1241
- this.healthCheckTimer = setInterval(() => {
1242
- this.performHealthCheck();
1243
- }, HEALTH_CHECK_INTERVAL_MS);
1244
- this.healthCheckTimer.unref();
1245
- }
1246
- /**
1247
- * 停止健康检查定时器
1248
- */
1249
- stopHealthCheck() {
1250
- if (this.healthCheckTimer) {
1251
- clearInterval(this.healthCheckTimer);
1252
- this.healthCheckTimer = null;
1253
- }
1254
- this.healthCheckPending = false;
1255
- }
1256
- /**
1257
- * 执行健康检查
1258
- *
1259
- * 工作流程:
1260
- * 1. 如果最近有事件,无需检查
1261
- * 2. 如果上次探测还在等待中,说明 watcher 可能失效,尝试重建
1262
- * 3. 否则,创建临时文件触发事件,等待下次检查验证
1263
- */
1264
- async performHealthCheck() {
1265
- if (Date.now() - this.lastEventTime < HEALTH_CHECK_INTERVAL_MS) {
1266
- this.healthCheckPending = false;
1267
- return;
1268
- }
1269
- if (this.healthCheckPending) {
1270
- console.warn("[ProjectWatcher] Health check failed, watcher appears stale. Reinitializing...");
1271
- await this.reinitialize();
1272
- return;
1273
- }
1274
- this.healthCheckPending = true;
1275
- this.sendProbe();
1264
+ get runtimeStatus() {
1265
+ return {
1266
+ generation: this.generation,
1267
+ reinitializeCount: this.reinitializeCount,
1268
+ lastReinitializeReason: this.lastReinitializeReason,
1269
+ reinitializeReasonCounts: { ...this.reinitializeReasonCounts }
1270
+ };
1276
1271
  }
1277
1272
  /**
1278
- * 发送探测:通过 utimesSync 修改项目目录的时间戳来触发 watcher 事件
1273
+ * 记录重建统计
1279
1274
  */
1280
- sendProbe() {
1281
- try {
1282
- const now = /* @__PURE__ */ new Date();
1283
- utimesSync(this.projectDir, now, now);
1284
- } catch {}
1275
+ markReinitialized(reason) {
1276
+ this.reinitializeCount += 1;
1277
+ this.lastReinitializeReason = reason;
1278
+ this.reinitializeReasonCounts[reason] += 1;
1285
1279
  }
1286
1280
  /**
1287
1281
  * 重新初始化 watcher
1288
1282
  */
1289
- async reinitialize() {
1290
- this.stopHealthCheck();
1283
+ async reinitialize(reason) {
1284
+ this.stopPathLivenessMonitor();
1291
1285
  if (this.subscription) {
1292
1286
  try {
1293
1287
  await this.subscription.unsubscribe();
@@ -1296,38 +1290,50 @@ var ProjectWatcher = class {
1296
1290
  }
1297
1291
  this.initialized = false;
1298
1292
  this.initPromise = null;
1299
- this.healthCheckPending = false;
1293
+ this.projectDirFingerprint = null;
1300
1294
  if (!existsSync(this.projectDir)) {
1301
1295
  console.warn("[ProjectWatcher] Project directory does not exist, waiting for it to be created...");
1302
- this.waitForProjectDir();
1296
+ this.waitForProjectDir("missing-project-dir");
1303
1297
  return;
1304
1298
  }
1305
1299
  try {
1306
1300
  await this.init();
1301
+ this.markReinitialized(reason);
1307
1302
  console.log("[ProjectWatcher] Reinitialized successfully");
1308
1303
  } catch (err) {
1309
1304
  console.error("[ProjectWatcher] Failed to reinitialize:", err);
1310
- setTimeout(() => this.reinitialize(), HEALTH_CHECK_INTERVAL_MS);
1305
+ this.scheduleReinitialize(reason);
1311
1306
  }
1312
1307
  }
1313
1308
  /**
1314
1309
  * 等待项目目录被创建
1315
1310
  */
1316
- waitForProjectDir() {
1317
- const checkInterval = setInterval(() => {
1318
- if (existsSync(this.projectDir)) {
1319
- clearInterval(checkInterval);
1320
- console.log("[ProjectWatcher] Project directory created, reinitializing...");
1321
- this.reinitialize();
1311
+ waitForProjectDir(reason) {
1312
+ this.reinitializeReasonPending = reason;
1313
+ this.reinitializePending = true;
1314
+ if (this.reinitializeTimer) {
1315
+ clearTimeout(this.reinitializeTimer);
1316
+ this.reinitializeTimer = null;
1317
+ }
1318
+ this.reinitializeTimer = setTimeout(() => {
1319
+ this.reinitializeTimer = null;
1320
+ this.reinitializePending = false;
1321
+ if (!existsSync(this.projectDir)) {
1322
+ this.waitForProjectDir(reason);
1323
+ return;
1322
1324
  }
1323
- }, HEALTH_CHECK_INTERVAL_MS);
1324
- checkInterval.unref();
1325
+ const pendingReason = this.reinitializeReasonPending ?? reason;
1326
+ this.reinitializeReasonPending = null;
1327
+ console.log("[ProjectWatcher] Project directory created, reinitializing...");
1328
+ this.reinitialize(pendingReason);
1329
+ }, RECOVERY_INTERVAL_MS);
1330
+ this.reinitializeTimer.unref();
1325
1331
  }
1326
1332
  /**
1327
1333
  * 关闭 watcher
1328
1334
  */
1329
1335
  async close() {
1330
- this.stopHealthCheck();
1336
+ this.stopPathLivenessMonitor();
1331
1337
  if (this.debounceTimer) {
1332
1338
  clearTimeout(this.debounceTimer);
1333
1339
  this.debounceTimer = null;
@@ -1337,6 +1343,7 @@ var ProjectWatcher = class {
1337
1343
  this.reinitializeTimer = null;
1338
1344
  }
1339
1345
  this.reinitializePending = false;
1346
+ this.reinitializeReasonPending = null;
1340
1347
  if (this.subscription) {
1341
1348
  await this.subscription.unsubscribe();
1342
1349
  this.subscription = null;
@@ -1345,6 +1352,7 @@ var ProjectWatcher = class {
1345
1352
  this.pendingEvents = [];
1346
1353
  this.initialized = false;
1347
1354
  this.initPromise = null;
1355
+ this.projectDirFingerprint = null;
1348
1356
  }
1349
1357
  };
1350
1358
  /**
@@ -1472,6 +1480,22 @@ function acquireWatcher(path$1, onChange, options = {}) {
1472
1480
  function isWatcherPoolInitialized() {
1473
1481
  return globalProjectWatcher !== null && globalProjectWatcher.isInitialized;
1474
1482
  }
1483
+ /**
1484
+ * 获取 watcher 运行时状态
1485
+ */
1486
+ function getWatcherRuntimeStatus() {
1487
+ if (!globalProjectWatcher) return null;
1488
+ const runtime = globalProjectWatcher.runtimeStatus;
1489
+ return {
1490
+ projectDir: globalProjectDir,
1491
+ initialized: globalProjectWatcher.isInitialized,
1492
+ subscriptionCount: globalProjectWatcher.subscriptionCount,
1493
+ generation: runtime.generation,
1494
+ reinitializeCount: runtime.reinitializeCount,
1495
+ lastReinitializeReason: runtime.lastReinitializeReason,
1496
+ reinitializeReasonCounts: runtime.reinitializeReasonCounts
1497
+ };
1498
+ }
1475
1499
 
1476
1500
  //#endregion
1477
1501
  //#region ../core/src/reactive-fs/reactive-fs.ts
@@ -1646,6 +1670,86 @@ async function reactiveStat(path$1) {
1646
1670
  return state.get();
1647
1671
  }
1648
1672
 
1673
+ //#endregion
1674
+ //#region ../core/src/validator.ts
1675
+ /**
1676
+ * Validator for OpenSpec documents
1677
+ */
1678
+ var Validator = class {
1679
+ /**
1680
+ * Validate a spec document
1681
+ */
1682
+ validateSpec(spec) {
1683
+ const issues = [];
1684
+ if (!spec.overview || spec.overview.trim().length === 0) issues.push({
1685
+ severity: "ERROR",
1686
+ message: "Spec must have a Purpose/Overview section",
1687
+ path: "overview"
1688
+ });
1689
+ if (spec.requirements.length === 0) issues.push({
1690
+ severity: "ERROR",
1691
+ message: "Spec must have at least one requirement",
1692
+ path: "requirements"
1693
+ });
1694
+ for (const req of spec.requirements) {
1695
+ if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
1696
+ severity: "WARNING",
1697
+ message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
1698
+ path: `requirements.${req.id}`
1699
+ });
1700
+ if (req.scenarios.length === 0) issues.push({
1701
+ severity: "WARNING",
1702
+ message: `Requirement should have at least one scenario: ${req.id}`,
1703
+ path: `requirements.${req.id}.scenarios`
1704
+ });
1705
+ if (req.text.length > 1e3) issues.push({
1706
+ severity: "WARNING",
1707
+ message: `Requirement text is too long (max 1000 chars): ${req.id}`,
1708
+ path: `requirements.${req.id}.text`
1709
+ });
1710
+ }
1711
+ return {
1712
+ valid: issues.filter((i) => i.severity === "ERROR").length === 0,
1713
+ issues
1714
+ };
1715
+ }
1716
+ /**
1717
+ * Validate a change proposal
1718
+ */
1719
+ validateChange(change) {
1720
+ const issues = [];
1721
+ if (!change.why || change.why.length < 50) issues.push({
1722
+ severity: "ERROR",
1723
+ message: "Change \"Why\" section must be at least 50 characters",
1724
+ path: "why"
1725
+ });
1726
+ if (change.why && change.why.length > 500) issues.push({
1727
+ severity: "WARNING",
1728
+ message: "Change \"Why\" section should be under 500 characters",
1729
+ path: "why"
1730
+ });
1731
+ if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
1732
+ severity: "ERROR",
1733
+ message: "Change must have a \"What Changes\" section",
1734
+ path: "whatChanges"
1735
+ });
1736
+ if (change.deltas.length === 0) issues.push({
1737
+ severity: "WARNING",
1738
+ message: "Change should have at least one delta",
1739
+ path: "deltas"
1740
+ });
1741
+ if (change.deltas.length > 50) issues.push({
1742
+ severity: "WARNING",
1743
+ message: "Change has too many deltas (max 50)",
1744
+ path: "deltas"
1745
+ });
1746
+ return {
1747
+ valid: issues.filter((i) => i.severity === "ERROR").length === 0,
1748
+ issues
1749
+ };
1750
+ }
1751
+ };
1752
+
1649
1753
  //#endregion
1650
1754
  //#region ../core/src/adapter.ts
1651
1755
  /**
@@ -1825,17 +1929,17 @@ var OpenSpecAdapter = class {
1825
1929
  const fullPath = join(dir, name);
1826
1930
  const statInfo = await reactiveStat(fullPath);
1827
1931
  if (!statInfo) continue;
1828
- const relativePath = fullPath.slice(root.length + 1);
1932
+ const relativePath$1 = fullPath.slice(root.length + 1);
1829
1933
  if (statInfo.isDirectory) {
1830
1934
  files.push({
1831
- path: relativePath,
1935
+ path: relativePath$1,
1832
1936
  type: "directory"
1833
1937
  });
1834
1938
  files.push(...await this.collectChangeFiles(root, fullPath));
1835
1939
  } else {
1836
1940
  const content = await reactiveReadFile(fullPath);
1837
1941
  files.push({
1838
- path: relativePath,
1942
+ path: relativePath$1,
1839
1943
  type: "file",
1840
1944
  content: content ?? void 0
1841
1945
  });
@@ -5835,6 +5939,8 @@ const CURSOR_STYLE_VALUES = [
5835
5939
  "underline",
5836
5940
  "bar"
5837
5941
  ];
5942
+ const TERMINAL_RENDERER_ENGINE_VALUES = ["xterm", "ghostty"];
5943
+ const TerminalRendererEngineSchema = enumType(TERMINAL_RENDERER_ENGINE_VALUES);
5838
5944
  const BASE_PACKAGE_MANAGER_RUNNERS = [
5839
5945
  {
5840
5946
  id: "npx",
@@ -6172,8 +6278,10 @@ const TerminalConfigSchema = objectType({
6172
6278
  fontFamily: stringType().default(""),
6173
6279
  cursorBlink: booleanType().default(true),
6174
6280
  cursorStyle: enumType(CURSOR_STYLE_VALUES).default("block"),
6175
- scrollback: numberType().min(0).max(1e5).default(1e3)
6281
+ scrollback: numberType().min(0).max(1e5).default(1e3),
6282
+ rendererEngine: stringType().default("xterm")
6176
6283
  });
6284
+ const DashboardConfigSchema = objectType({ trendPointLimit: numberType().int().min(20).max(500).default(100) });
6177
6285
  /**
6178
6286
  * OpenSpecUI 配置 Schema
6179
6287
  *
@@ -6185,13 +6293,15 @@ const OpenSpecUIConfigSchema = objectType({
6185
6293
  args: arrayType(stringType()).optional()
6186
6294
  }).default({}),
6187
6295
  theme: enumType(THEME_VALUES).default("system"),
6188
- terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
6296
+ terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
6297
+ dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
6189
6298
  });
6190
6299
  /** 默认配置(静态,用于测试和类型) */
6191
6300
  const DEFAULT_CONFIG = {
6192
6301
  cli: {},
6193
6302
  theme: "system",
6194
- terminal: TerminalConfigSchema.parse({})
6303
+ terminal: TerminalConfigSchema.parse({}),
6304
+ dashboard: DashboardConfigSchema.parse({})
6195
6305
  };
6196
6306
  /**
6197
6307
  * 配置管理器
@@ -6257,6 +6367,10 @@ var ConfigManager = class {
6257
6367
  terminal: {
6258
6368
  ...current.terminal,
6259
6369
  ...config.terminal
6370
+ },
6371
+ dashboard: {
6372
+ ...current.dashboard,
6373
+ ...config.dashboard
6260
6374
  }
6261
6375
  };
6262
6376
  const serialized = JSON.stringify(merged, null, 2);
@@ -6969,6 +7083,17 @@ async function getConfiguredTools(projectDir) {
6969
7083
  return state.get();
6970
7084
  }
6971
7085
 
7086
+ //#endregion
7087
+ //#region ../core/src/dashboard-types.ts
7088
+ const DASHBOARD_METRIC_KEYS = [
7089
+ "specifications",
7090
+ "requirements",
7091
+ "activeChanges",
7092
+ "inProgressChanges",
7093
+ "completedChanges",
7094
+ "taskCompletionPercent"
7095
+ ];
7096
+
6972
7097
  //#endregion
6973
7098
  //#region ../core/src/opsx-types.ts
6974
7099
  const ArtifactStatusSchema = objectType({
@@ -13768,10 +13893,10 @@ async function readEntriesUnderRoot(root) {
13768
13893
  const fullPath = join$1(dir, name);
13769
13894
  const statInfo = await reactiveStat(fullPath);
13770
13895
  if (!statInfo) continue;
13771
- const relativePath = toRelativePath(root, fullPath);
13896
+ const relativePath$1 = toRelativePath(root, fullPath);
13772
13897
  if (statInfo.isDirectory) {
13773
13898
  entries.push({
13774
- path: relativePath,
13899
+ path: relativePath$1,
13775
13900
  type: "directory"
13776
13901
  });
13777
13902
  entries.push(...await collectEntries(fullPath));
@@ -13779,7 +13904,7 @@ async function readEntriesUnderRoot(root) {
13779
13904
  const content = await reactiveReadFile(fullPath);
13780
13905
  const size = content ? Buffer.byteLength(content, "utf-8") : void 0;
13781
13906
  entries.push({
13782
- path: relativePath,
13907
+ path: relativePath$1,
13783
13908
  type: "file",
13784
13909
  content: content ?? void 0,
13785
13910
  size
@@ -22589,41 +22714,6 @@ var import_sender = /* @__PURE__ */ __toESM$1(require_sender(), 1);
22589
22714
  var import_websocket = /* @__PURE__ */ __toESM$1(require_websocket(), 1);
22590
22715
  var import_websocket_server = /* @__PURE__ */ __toESM$1(require_websocket_server(), 1);
22591
22716
 
22592
- //#endregion
22593
- //#region ../server/src/port-utils.ts
22594
- /**
22595
- * Check if a port is available by trying to listen on it.
22596
- * Uses default binding (both IPv4 and IPv6) to detect conflicts.
22597
- */
22598
- function isPortAvailable(port) {
22599
- return new Promise((resolve$2) => {
22600
- const server = createServer$1();
22601
- server.once("error", () => {
22602
- resolve$2(false);
22603
- });
22604
- server.once("listening", () => {
22605
- server.close(() => resolve$2(true));
22606
- });
22607
- server.listen(port);
22608
- });
22609
- }
22610
- /**
22611
- * Find an available port starting from the given port.
22612
- * Will try up to maxAttempts ports sequentially.
22613
- *
22614
- * @param startPort - The preferred port to start checking from
22615
- * @param maxAttempts - Maximum number of ports to try (default: 10)
22616
- * @returns The first available port found
22617
- * @throws Error if no available port is found in the range
22618
- */
22619
- async function findAvailablePort(startPort, maxAttempts = 10) {
22620
- for (let i = 0; i < maxAttempts; i++) {
22621
- const port = startPort + i;
22622
- if (await isPortAvailable(port)) return port;
22623
- }
22624
- throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
22625
- }
22626
-
22627
22717
  //#endregion
22628
22718
  //#region ../server/src/pty-manager.ts
22629
22719
  const DEFAULT_SCROLLBACK = 1e3;
@@ -22994,7 +23084,7 @@ function createPtyWebSocketHandler(ptyManager) {
22994
23084
  }
22995
23085
 
22996
23086
  //#endregion
22997
- //#region ../search/dist/worker-source-CMPZlh9-.mjs
23087
+ //#region ../search/src/protocol.ts
22998
23088
  const SearchDocumentKindSchema = enumType([
22999
23089
  "spec",
23000
23090
  "change",
@@ -23060,6 +23150,9 @@ const SearchWorkerResponseSchema = discriminatedUnionType("type", [
23060
23150
  message: stringType()
23061
23151
  })
23062
23152
  ]);
23153
+
23154
+ //#endregion
23155
+ //#region ../search/src/worker-source.ts
23063
23156
  const sharedRuntimeSource = String.raw`
23064
23157
  const DEFAULT_LIMIT = 50;
23065
23158
  const MAX_LIMIT = 200;
@@ -23277,6 +23370,368 @@ function createCliStreamObservable(startStream) {
23277
23370
  });
23278
23371
  }
23279
23372
 
23373
+ //#endregion
23374
+ //#region ../server/src/dashboard-git-snapshot.ts
23375
+ const execFileAsync$1 = promisify$1(execFile);
23376
+ const EMPTY_DIFF = {
23377
+ files: 0,
23378
+ insertions: 0,
23379
+ deletions: 0
23380
+ };
23381
+ async function defaultRunGit(cwd, args) {
23382
+ try {
23383
+ const { stdout } = await execFileAsync$1("git", args, {
23384
+ cwd,
23385
+ encoding: "utf8",
23386
+ maxBuffer: 8 * 1024 * 1024
23387
+ });
23388
+ return {
23389
+ ok: true,
23390
+ stdout
23391
+ };
23392
+ } catch {
23393
+ return {
23394
+ ok: false,
23395
+ stdout: ""
23396
+ };
23397
+ }
23398
+ }
23399
+ function parseShortStat(output) {
23400
+ const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
23401
+ const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
23402
+ const deletions = Number(/(\d+)\s+deletions?\(-\)/.exec(output)?.[1] ?? 0);
23403
+ return {
23404
+ files: Number.isFinite(files) ? files : 0,
23405
+ insertions: Number.isFinite(insertions) ? insertions : 0,
23406
+ deletions: Number.isFinite(deletions) ? deletions : 0
23407
+ };
23408
+ }
23409
+ function parseNumStat(output) {
23410
+ let files = 0;
23411
+ let insertions = 0;
23412
+ let deletions = 0;
23413
+ for (const line of output.split("\n")) {
23414
+ const trimmed = line.trim();
23415
+ if (!trimmed) continue;
23416
+ const [addRaw, deleteRaw] = trimmed.split(" ");
23417
+ if (!addRaw || !deleteRaw) continue;
23418
+ files += 1;
23419
+ if (addRaw !== "-") insertions += Number(addRaw) || 0;
23420
+ if (deleteRaw !== "-") deletions += Number(deleteRaw) || 0;
23421
+ }
23422
+ return {
23423
+ files,
23424
+ insertions,
23425
+ deletions
23426
+ };
23427
+ }
23428
+ function normalizeGitPath(path$1) {
23429
+ return path$1.replace(/\\/g, "/").replace(/^\.\//, "");
23430
+ }
23431
+ function relativePath(fromDir, target) {
23432
+ const rel = relative$1(fromDir, target);
23433
+ if (!rel || rel.length === 0) return ".";
23434
+ return rel;
23435
+ }
23436
+ function parseBranchName(branchRef, detached) {
23437
+ if (detached) return "(detached)";
23438
+ if (!branchRef) return "(unknown)";
23439
+ return branchRef.replace(/^refs\/heads\//, "");
23440
+ }
23441
+ function parseWorktreeList(porcelain) {
23442
+ const entries = [];
23443
+ let current = null;
23444
+ const flush = () => {
23445
+ if (!current) return;
23446
+ entries.push(current);
23447
+ current = null;
23448
+ };
23449
+ for (const line of porcelain.split("\n")) {
23450
+ if (line.startsWith("worktree ")) {
23451
+ flush();
23452
+ current = {
23453
+ path: line.slice(9).trim(),
23454
+ branchRef: null,
23455
+ detached: false
23456
+ };
23457
+ continue;
23458
+ }
23459
+ if (!current) continue;
23460
+ if (line.startsWith("branch ")) {
23461
+ current.branchRef = line.slice(7).trim();
23462
+ continue;
23463
+ }
23464
+ if (line === "detached") {
23465
+ current.detached = true;
23466
+ continue;
23467
+ }
23468
+ }
23469
+ flush();
23470
+ return entries;
23471
+ }
23472
+ function parseRelatedChanges(paths) {
23473
+ const related = /* @__PURE__ */ new Set();
23474
+ for (const path$1 of paths) {
23475
+ const normalized = normalizeGitPath(path$1);
23476
+ const activeMatch = /^openspec\/changes\/([^/]+)\//.exec(normalized);
23477
+ if (activeMatch?.[1]) {
23478
+ related.add(activeMatch[1]);
23479
+ continue;
23480
+ }
23481
+ const archiveMatch = /^openspec\/changes\/archive\/([^/]+)\//.exec(normalized);
23482
+ if (archiveMatch?.[1]) {
23483
+ const fullName = archiveMatch[1];
23484
+ related.add(fullName.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
23485
+ }
23486
+ }
23487
+ return [...related].sort((a, b) => a.localeCompare(b));
23488
+ }
23489
+ async function resolveDefaultBranch(projectDir, runGit) {
23490
+ const remoteHead = await runGit(projectDir, [
23491
+ "symbolic-ref",
23492
+ "--quiet",
23493
+ "--short",
23494
+ "refs/remotes/origin/HEAD"
23495
+ ]);
23496
+ const remoteRef = remoteHead.stdout.trim();
23497
+ if (remoteHead.ok && remoteRef) return remoteRef;
23498
+ const localHead = await runGit(projectDir, [
23499
+ "rev-parse",
23500
+ "--abbrev-ref",
23501
+ "HEAD"
23502
+ ]);
23503
+ const localRef = localHead.stdout.trim();
23504
+ if (localHead.ok && localRef && localRef !== "HEAD") return localRef;
23505
+ return "main";
23506
+ }
23507
+ async function collectCommitEntries(options) {
23508
+ const { worktreePath, defaultBranch, maxCommitEntries, runGit } = options;
23509
+ const entries = [];
23510
+ const commits = await runGit(worktreePath, [
23511
+ "log",
23512
+ "--format=%H%x1f%s",
23513
+ `-n${maxCommitEntries}`,
23514
+ `${defaultBranch}..HEAD`
23515
+ ]);
23516
+ if (commits.ok) for (const line of commits.stdout.split("\n")) {
23517
+ if (!line.trim()) continue;
23518
+ const [hash, title = ""] = line.split("");
23519
+ if (!hash) continue;
23520
+ const diffResult = await runGit(worktreePath, [
23521
+ "show",
23522
+ "--numstat",
23523
+ "--format=",
23524
+ hash
23525
+ ]);
23526
+ const changedFiles = (await runGit(worktreePath, [
23527
+ "show",
23528
+ "--name-only",
23529
+ "--format=",
23530
+ hash
23531
+ ])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
23532
+ entries.push({
23533
+ type: "commit",
23534
+ hash,
23535
+ title: title.trim() || hash.slice(0, 7),
23536
+ relatedChanges: parseRelatedChanges(changedFiles),
23537
+ diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
23538
+ });
23539
+ }
23540
+ const trackedResult = await runGit(worktreePath, [
23541
+ "diff",
23542
+ "--numstat",
23543
+ "HEAD"
23544
+ ]);
23545
+ const trackedFilesResult = await runGit(worktreePath, [
23546
+ "diff",
23547
+ "--name-only",
23548
+ "HEAD"
23549
+ ]);
23550
+ const untrackedResult = await runGit(worktreePath, [
23551
+ "ls-files",
23552
+ "--others",
23553
+ "--exclude-standard"
23554
+ ]);
23555
+ const trackedFiles = trackedFilesResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
23556
+ const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
23557
+ const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
23558
+ const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
23559
+ entries.push({
23560
+ type: "uncommitted",
23561
+ title: "Uncommitted",
23562
+ relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
23563
+ diff: {
23564
+ files: allUncommittedFiles.size,
23565
+ insertions: trackedDiff.insertions,
23566
+ deletions: trackedDiff.deletions
23567
+ }
23568
+ });
23569
+ return entries;
23570
+ }
23571
+ async function collectWorktree(options) {
23572
+ const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries } = options;
23573
+ const worktreePath = resolve$1(worktree.path);
23574
+ const resolvedProjectDir = resolve$1(projectDir);
23575
+ const aheadBehindResult = await runGit(worktreePath, [
23576
+ "rev-list",
23577
+ "--left-right",
23578
+ "--count",
23579
+ `${defaultBranch}...HEAD`
23580
+ ]);
23581
+ let ahead = 0;
23582
+ let behind = 0;
23583
+ if (aheadBehindResult.ok) {
23584
+ const [behindRaw, aheadRaw] = aheadBehindResult.stdout.trim().split(/\s+/);
23585
+ ahead = Number(aheadRaw) || 0;
23586
+ behind = Number(behindRaw) || 0;
23587
+ }
23588
+ const diffResult = await runGit(worktreePath, [
23589
+ "diff",
23590
+ "--shortstat",
23591
+ `${defaultBranch}...HEAD`
23592
+ ]);
23593
+ const diff = diffResult.ok ? parseShortStat(diffResult.stdout) : EMPTY_DIFF;
23594
+ const entries = await collectCommitEntries({
23595
+ worktreePath,
23596
+ defaultBranch,
23597
+ maxCommitEntries,
23598
+ runGit
23599
+ });
23600
+ return {
23601
+ path: worktreePath,
23602
+ relativePath: relativePath(resolvedProjectDir, worktreePath),
23603
+ branchName: parseBranchName(worktree.branchRef, worktree.detached),
23604
+ isCurrent: resolvedProjectDir === worktreePath,
23605
+ ahead,
23606
+ behind,
23607
+ diff,
23608
+ entries
23609
+ };
23610
+ }
23611
+ async function buildDashboardGitSnapshot(options) {
23612
+ const runGit = options.runGit ?? defaultRunGit;
23613
+ const maxCommitEntries = options.maxCommitEntries ?? 8;
23614
+ const resolvedProjectDir = resolve$1(options.projectDir);
23615
+ const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
23616
+ const worktreeResult = await runGit(resolvedProjectDir, [
23617
+ "worktree",
23618
+ "list",
23619
+ "--porcelain"
23620
+ ]);
23621
+ const parsed = worktreeResult.ok ? parseWorktreeList(worktreeResult.stdout) : [];
23622
+ const baseWorktrees = parsed.length > 0 ? parsed : [{
23623
+ path: resolvedProjectDir,
23624
+ branchRef: null,
23625
+ detached: false
23626
+ }];
23627
+ const worktrees = await Promise.all(baseWorktrees.map((worktree) => collectWorktree({
23628
+ projectDir: resolvedProjectDir,
23629
+ worktree,
23630
+ defaultBranch,
23631
+ runGit,
23632
+ maxCommitEntries
23633
+ })));
23634
+ worktrees.sort((a, b) => {
23635
+ if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
23636
+ return a.branchName.localeCompare(b.branchName);
23637
+ });
23638
+ return {
23639
+ defaultBranch,
23640
+ worktrees
23641
+ };
23642
+ }
23643
+
23644
+ //#endregion
23645
+ //#region ../server/src/dashboard-time-trends.ts
23646
+ const MIN_TREND_POINT_LIMIT = 20;
23647
+ const MAX_TREND_POINT_LIMIT = 500;
23648
+ const DEFAULT_TREND_POINT_LIMIT = 100;
23649
+ const TARGET_TREND_BARS = 20;
23650
+ const DAY_MS = 1440 * 60 * 1e3;
23651
+ function clampPointLimit(pointLimit) {
23652
+ if (!Number.isFinite(pointLimit)) return DEFAULT_TREND_POINT_LIMIT;
23653
+ return Math.max(MIN_TREND_POINT_LIMIT, Math.min(MAX_TREND_POINT_LIMIT, Math.trunc(pointLimit)));
23654
+ }
23655
+ function createEmptyTrendSeries() {
23656
+ return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
23657
+ }
23658
+ function normalizeEvents(events, pointLimit) {
23659
+ return events.filter((event) => Number.isFinite(event.ts) && event.ts > 0 && Number.isFinite(event.value)).sort((a, b) => a.ts - b.ts).slice(-pointLimit);
23660
+ }
23661
+ function buildTimeWindow(options) {
23662
+ const { probeEvents, targetBars, rightEdgeTs } = options;
23663
+ if (probeEvents.length === 0) return null;
23664
+ const probeEnd = probeEvents[probeEvents.length - 1].ts;
23665
+ const end = typeof rightEdgeTs === "number" && Number.isFinite(rightEdgeTs) && rightEdgeTs > 0 ? Math.max(probeEnd, rightEdgeTs) : probeEnd;
23666
+ const probeStart = probeEvents[0].ts;
23667
+ const rangeMs = Math.max(1, end - probeStart);
23668
+ const bucketMs = rangeMs >= DAY_MS ? Math.max(DAY_MS, Math.ceil(rangeMs / targetBars / DAY_MS) * DAY_MS) : Math.max(1, Math.ceil(rangeMs / targetBars));
23669
+ const windowStart = end - bucketMs * targetBars;
23670
+ return {
23671
+ windowStart,
23672
+ bucketMs,
23673
+ bucketEnds: Array.from({ length: targetBars }, (_, index) => windowStart + bucketMs * (index + 1))
23674
+ };
23675
+ }
23676
+ function bucketizeTrend(events, reducer, rightEdgeTs) {
23677
+ if (events.length === 0) return [];
23678
+ const timeWindow = buildTimeWindow({
23679
+ probeEvents: events,
23680
+ targetBars: TARGET_TREND_BARS,
23681
+ rightEdgeTs
23682
+ });
23683
+ if (!timeWindow) return [];
23684
+ const { windowStart, bucketMs, bucketEnds } = timeWindow;
23685
+ const sums = Array.from({ length: bucketEnds.length }, () => 0);
23686
+ const counts = Array.from({ length: bucketEnds.length }, () => 0);
23687
+ let baseline = 0;
23688
+ for (const event of events) {
23689
+ if (event.ts <= windowStart) {
23690
+ if (reducer === "sum-cumulative") baseline += event.value;
23691
+ continue;
23692
+ }
23693
+ const offset = event.ts - windowStart;
23694
+ const index = Math.max(0, Math.min(bucketEnds.length - 1, Math.ceil(offset / bucketMs) - 1));
23695
+ sums[index] += event.value;
23696
+ counts[index] += 1;
23697
+ }
23698
+ let cumulative = baseline;
23699
+ let carry = baseline !== 0 ? baseline : events[0].value;
23700
+ return bucketEnds.map((ts, index) => {
23701
+ if (reducer === "sum") return {
23702
+ ts,
23703
+ value: sums[index]
23704
+ };
23705
+ if (reducer === "sum-cumulative") {
23706
+ cumulative += sums[index];
23707
+ return {
23708
+ ts,
23709
+ value: cumulative
23710
+ };
23711
+ }
23712
+ if (counts[index] > 0) carry = sums[index] / counts[index];
23713
+ return {
23714
+ ts,
23715
+ value: carry
23716
+ };
23717
+ });
23718
+ }
23719
+ function buildDashboardTimeTrends(options) {
23720
+ const pointLimit = clampPointLimit(options.pointLimit);
23721
+ const trends = createEmptyTrendSeries();
23722
+ for (const metric of DASHBOARD_METRIC_KEYS) {
23723
+ if (options.availability[metric].state !== "ok") continue;
23724
+ trends[metric] = bucketizeTrend(normalizeEvents(options.events[metric], pointLimit), options.reducers?.[metric] ?? "sum", options.rightEdgeTs);
23725
+ }
23726
+ return {
23727
+ trends,
23728
+ trendMeta: {
23729
+ pointLimit,
23730
+ lastUpdatedAt: options.timestamp
23731
+ }
23732
+ };
23733
+ }
23734
+
23280
23735
  //#endregion
23281
23736
  //#region ../server/src/reactive-kv.ts
23282
23737
  /**
@@ -23397,6 +23852,76 @@ function createReactiveSubscriptionWithInput(task) {
23397
23852
  const t = initTRPC.context().create();
23398
23853
  const router = t.router;
23399
23854
  const publicProcedure = t.procedure;
23855
+ const execFileAsync = promisify$1(execFile);
23856
+ const dashboardGitTaskStatusEmitter = new EventEmitter$1();
23857
+ dashboardGitTaskStatusEmitter.setMaxListeners(200);
23858
+ const dashboardGitTaskStatus = {
23859
+ running: false,
23860
+ inFlight: 0,
23861
+ lastStartedAt: null,
23862
+ lastFinishedAt: null,
23863
+ lastReason: null,
23864
+ lastError: null
23865
+ };
23866
+ function getDashboardGitTaskStatus() {
23867
+ return { ...dashboardGitTaskStatus };
23868
+ }
23869
+ function emitDashboardGitTaskStatus() {
23870
+ dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
23871
+ }
23872
+ function beginDashboardGitTask(reason) {
23873
+ dashboardGitTaskStatus.inFlight += 1;
23874
+ dashboardGitTaskStatus.running = true;
23875
+ dashboardGitTaskStatus.lastStartedAt = Date.now();
23876
+ dashboardGitTaskStatus.lastReason = reason;
23877
+ dashboardGitTaskStatus.lastError = null;
23878
+ emitDashboardGitTaskStatus();
23879
+ }
23880
+ function endDashboardGitTask(error) {
23881
+ dashboardGitTaskStatus.inFlight = Math.max(0, dashboardGitTaskStatus.inFlight - 1);
23882
+ dashboardGitTaskStatus.running = dashboardGitTaskStatus.inFlight > 0;
23883
+ dashboardGitTaskStatus.lastFinishedAt = Date.now();
23884
+ if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
23885
+ emitDashboardGitTaskStatus();
23886
+ }
23887
+ function parseGitDirFromDotGitFile(content) {
23888
+ const line = content.split(/\r?\n/).map((item) => item.trim()).find((item) => item.startsWith("gitdir:"));
23889
+ if (!line) return null;
23890
+ const rawPath = line.slice(7).trim();
23891
+ return rawPath.length > 0 ? rawPath : null;
23892
+ }
23893
+ function getDashboardGitRefreshStampPath(projectDir) {
23894
+ return join$1(projectDir, "openspec", ".openspecui-dashboard-git-refresh.stamp");
23895
+ }
23896
+ async function touchDashboardGitRefreshStamp(projectDir, reason) {
23897
+ const stampPath = getDashboardGitRefreshStampPath(projectDir);
23898
+ await mkdir$1(dirname$1(stampPath), { recursive: true });
23899
+ await writeFile$1(stampPath, `${Date.now()} ${reason}\n`, "utf8");
23900
+ }
23901
+ async function registerDashboardGitReactiveDeps(projectDir) {
23902
+ await reactiveReadDir(projectDir, {
23903
+ includeHidden: true,
23904
+ exclude: ["node_modules"]
23905
+ });
23906
+ await reactiveReadFile(getDashboardGitRefreshStampPath(projectDir));
23907
+ const dotGitPath = join$1(projectDir, ".git");
23908
+ if (!await reactiveExists(dotGitPath)) return;
23909
+ const dotGitFileContent = await reactiveReadFile(dotGitPath);
23910
+ if (dotGitFileContent !== null) {
23911
+ const gitDirRaw = parseGitDirFromDotGitFile(dotGitFileContent);
23912
+ if (!gitDirRaw) return;
23913
+ const gitDirPath = resolve$1(projectDir, gitDirRaw);
23914
+ await reactiveReadDir(gitDirPath, { includeHidden: true });
23915
+ await reactiveReadFile(join$1(gitDirPath, "HEAD"));
23916
+ await reactiveReadFile(join$1(gitDirPath, "index"));
23917
+ await reactiveReadFile(join$1(gitDirPath, "packed-refs"));
23918
+ return;
23919
+ }
23920
+ await reactiveReadDir(dotGitPath, { includeHidden: true });
23921
+ await reactiveReadFile(join$1(dotGitPath, "HEAD"));
23922
+ await reactiveReadFile(join$1(dotGitPath, "index"));
23923
+ await reactiveReadFile(join$1(dotGitPath, "packed-refs"));
23924
+ }
23400
23925
  function requireChangeId(changeId) {
23401
23926
  if (!changeId) throw new Error("change is required");
23402
23927
  return changeId;
@@ -23468,6 +23993,210 @@ async function fetchOpsxTemplateContents(ctx, schema$6) {
23468
23993
  await ctx.kernel.ensureTemplateContents(schema$6);
23469
23994
  return ctx.kernel.getTemplateContents(schema$6);
23470
23995
  }
23996
+ function buildSystemStatus(ctx) {
23997
+ const runtime = getWatcherRuntimeStatus();
23998
+ return {
23999
+ projectDir: ctx.projectDir,
24000
+ watcherEnabled: runtime?.initialized ?? false,
24001
+ watcherGeneration: runtime?.generation ?? 0,
24002
+ watcherReinitializeCount: runtime?.reinitializeCount ?? 0,
24003
+ watcherLastReinitializeReason: runtime?.lastReinitializeReason ?? null
24004
+ };
24005
+ }
24006
+ function resolveTrendTimestamp(primary, secondary) {
24007
+ if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) return primary;
24008
+ if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) return secondary;
24009
+ return null;
24010
+ }
24011
+ function parseDatedIdTimestamp(id) {
24012
+ const match$1 = /^(\d{4})-(\d{2})-(\d{2})(?:-|$)/.exec(id);
24013
+ if (!match$1) return null;
24014
+ const year = Number(match$1[1]);
24015
+ const month = Number(match$1[2]);
24016
+ const day = Number(match$1[3]);
24017
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
24018
+ if (month < 1 || month > 12) return null;
24019
+ if (day < 1 || day > 31) return null;
24020
+ const ts = Date.UTC(year, month - 1, day);
24021
+ return Number.isFinite(ts) ? ts : null;
24022
+ }
24023
+ function createEmptyTriColorTrends() {
24024
+ return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
24025
+ }
24026
+ async function readLatestCommitTimestamp(projectDir) {
24027
+ try {
24028
+ const { stdout } = await execFileAsync("git", [
24029
+ "log",
24030
+ "-1",
24031
+ "--format=%ct"
24032
+ ], {
24033
+ cwd: projectDir,
24034
+ maxBuffer: 1024 * 1024,
24035
+ encoding: "utf8"
24036
+ });
24037
+ const seconds = Number(stdout.trim());
24038
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
24039
+ } catch {
24040
+ return null;
24041
+ }
24042
+ }
24043
+ async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
24044
+ if (contextStorage.getStore()) await registerDashboardGitReactiveDeps(ctx.projectDir);
24045
+ const now = Date.now();
24046
+ const [specMetas, changeMetas, archiveMetas] = await Promise.all([
24047
+ ctx.adapter.listSpecsWithMeta(),
24048
+ ctx.adapter.listChangesWithMeta(),
24049
+ ctx.adapter.listArchivedChangesWithMeta()
24050
+ ]);
24051
+ const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
24052
+ const change = await ctx.adapter.readArchivedChange(meta.id);
24053
+ if (!change) return null;
24054
+ return {
24055
+ id: meta.id,
24056
+ createdAt: meta.createdAt,
24057
+ updatedAt: meta.updatedAt,
24058
+ tasksCompleted: change.tasks.filter((task) => task.completed).length
24059
+ };
24060
+ }))).filter((item) => item !== null);
24061
+ const specifications = (await Promise.all(specMetas.map(async (meta) => {
24062
+ const spec = await ctx.adapter.readSpec(meta.id);
24063
+ if (!spec) return null;
24064
+ return {
24065
+ id: meta.id,
24066
+ name: meta.name,
24067
+ requirements: spec.requirements.length,
24068
+ updatedAt: meta.updatedAt
24069
+ };
24070
+ }))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
24071
+ const activeChanges = changeMetas.map((change) => ({
24072
+ id: change.id,
24073
+ name: change.name,
24074
+ progress: change.progress,
24075
+ updatedAt: change.updatedAt
24076
+ }));
24077
+ const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
24078
+ const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
24079
+ const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
24080
+ const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
24081
+ const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
24082
+ const inProgressChanges = activeChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
24083
+ const specificationTrendEvents = specMetas.flatMap((spec) => {
24084
+ const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
24085
+ return ts === null ? [] : [{
24086
+ ts,
24087
+ value: 1
24088
+ }];
24089
+ });
24090
+ const completedTrendEvents = archivedChanges.flatMap((archive) => {
24091
+ const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
24092
+ return ts === null ? [] : [{
24093
+ ts,
24094
+ value: archive.tasksCompleted
24095
+ }];
24096
+ });
24097
+ const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
24098
+ const requirementTrendEvents = specifications.flatMap((spec) => {
24099
+ const meta = specMetaById.get(spec.id);
24100
+ const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
24101
+ return ts === null ? [] : [{
24102
+ ts,
24103
+ value: spec.requirements
24104
+ }];
24105
+ });
24106
+ const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || specifications.length === 0;
24107
+ const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
24108
+ const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
24109
+ const config = await ctx.configManager.readConfig();
24110
+ beginDashboardGitTask(reason);
24111
+ let latestCommitTs = null;
24112
+ let git;
24113
+ try {
24114
+ const gitSnapshotPromise = buildDashboardGitSnapshot({ projectDir: ctx.projectDir }).catch(() => ({
24115
+ defaultBranch: "main",
24116
+ worktrees: []
24117
+ }));
24118
+ latestCommitTs = await readLatestCommitTimestamp(ctx.projectDir);
24119
+ git = await gitSnapshotPromise;
24120
+ } catch (error) {
24121
+ endDashboardGitTask(error);
24122
+ throw error;
24123
+ }
24124
+ endDashboardGitTask(null);
24125
+ const cardAvailability = {
24126
+ specifications: hasObjectiveSpecificationTrend ? { state: "ok" } : {
24127
+ state: "invalid",
24128
+ reason: "objective-history-unavailable"
24129
+ },
24130
+ requirements: hasObjectiveRequirementTrend ? { state: "ok" } : {
24131
+ state: "invalid",
24132
+ reason: "objective-history-unavailable"
24133
+ },
24134
+ activeChanges: {
24135
+ state: "invalid",
24136
+ reason: "objective-history-unavailable"
24137
+ },
24138
+ inProgressChanges: {
24139
+ state: "invalid",
24140
+ reason: "objective-history-unavailable"
24141
+ },
24142
+ completedChanges: hasObjectiveCompletedTrend ? { state: "ok" } : {
24143
+ state: "invalid",
24144
+ reason: "objective-history-unavailable"
24145
+ },
24146
+ taskCompletionPercent: {
24147
+ state: "invalid",
24148
+ reason: taskCompletionPercent === null ? "semantic-uncomputable" : "objective-history-unavailable"
24149
+ }
24150
+ };
24151
+ const trendKinds = {
24152
+ specifications: "monotonic",
24153
+ requirements: "monotonic",
24154
+ activeChanges: "bidirectional",
24155
+ inProgressChanges: "bidirectional",
24156
+ completedChanges: "monotonic",
24157
+ taskCompletionPercent: "bidirectional"
24158
+ };
24159
+ const { trends: baselineTrends, trendMeta } = buildDashboardTimeTrends({
24160
+ pointLimit: config.dashboard.trendPointLimit,
24161
+ timestamp: now,
24162
+ rightEdgeTs: latestCommitTs,
24163
+ availability: cardAvailability,
24164
+ events: {
24165
+ specifications: specificationTrendEvents,
24166
+ requirements: requirementTrendEvents,
24167
+ activeChanges: [],
24168
+ inProgressChanges: [],
24169
+ completedChanges: completedTrendEvents,
24170
+ taskCompletionPercent: []
24171
+ },
24172
+ reducers: {
24173
+ specifications: "sum",
24174
+ requirements: "sum",
24175
+ completedChanges: "sum"
24176
+ }
24177
+ });
24178
+ return {
24179
+ summary: {
24180
+ specifications: specifications.length,
24181
+ requirements,
24182
+ activeChanges: activeChanges.length,
24183
+ inProgressChanges,
24184
+ completedChanges: archiveMetas.length,
24185
+ archivedTasksCompleted,
24186
+ tasksTotal,
24187
+ tasksCompleted,
24188
+ taskCompletionPercent
24189
+ },
24190
+ trends: baselineTrends,
24191
+ triColorTrends: createEmptyTriColorTrends(),
24192
+ trendKinds,
24193
+ cardAvailability,
24194
+ trendMeta,
24195
+ specifications,
24196
+ activeChanges,
24197
+ git
24198
+ };
24199
+ }
23471
24200
  /**
23472
24201
  * Spec router - spec CRUD operations
23473
24202
  */
@@ -23675,25 +24404,17 @@ const configRouter = router({
23675
24404
  "dark",
23676
24405
  "system"
23677
24406
  ]).optional(),
23678
- terminal: objectType({
23679
- fontSize: numberType().min(8).max(32).optional(),
23680
- fontFamily: stringType().optional(),
23681
- cursorBlink: booleanType().optional(),
23682
- cursorStyle: enumType([
23683
- "block",
23684
- "underline",
23685
- "bar"
23686
- ]).optional(),
23687
- scrollback: numberType().min(0).max(1e5).optional()
23688
- }).optional()
24407
+ terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
24408
+ dashboard: DashboardConfigSchema.partial().optional()
23689
24409
  })).mutation(async ({ ctx, input }) => {
23690
24410
  const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
23691
24411
  const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
23692
24412
  if (hasCliCommand && !hasCliArgs) {
23693
24413
  await ctx.configManager.setCliCommand(input.cli?.command ?? "");
23694
- if (input.theme !== void 0 || input.terminal !== void 0) await ctx.configManager.writeConfig({
24414
+ if (input.theme !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
23695
24415
  theme: input.theme,
23696
- terminal: input.terminal
24416
+ terminal: input.terminal,
24417
+ dashboard: input.dashboard
23697
24418
  });
23698
24419
  return { success: true };
23699
24420
  }
@@ -24130,9 +24851,63 @@ const searchRouter = router({
24130
24851
  })
24131
24852
  });
24132
24853
  /**
24854
+ * System router - runtime status and heartbeat-friendly subscription
24855
+ */
24856
+ const systemRouter = router({
24857
+ status: publicProcedure.query(({ ctx }) => {
24858
+ return buildSystemStatus(ctx);
24859
+ }),
24860
+ subscribe: publicProcedure.subscription(({ ctx }) => {
24861
+ return observable((emit) => {
24862
+ emit.next(buildSystemStatus(ctx));
24863
+ const timer = setInterval(() => {
24864
+ emit.next(buildSystemStatus(ctx));
24865
+ }, 3e3);
24866
+ timer.unref();
24867
+ return () => {
24868
+ clearInterval(timer);
24869
+ };
24870
+ });
24871
+ })
24872
+ });
24873
+ /**
24874
+ * Dashboard router - objective project overview for UI
24875
+ */
24876
+ const dashboardRouter = router({
24877
+ get: publicProcedure.query(async ({ ctx }) => {
24878
+ return fetchDashboardOverview(ctx, "dashboard.get");
24879
+ }),
24880
+ subscribe: publicProcedure.subscription(({ ctx }) => {
24881
+ return createReactiveSubscription(async () => {
24882
+ return fetchDashboardOverview(ctx, "dashboard.subscribe");
24883
+ });
24884
+ }),
24885
+ refreshGitSnapshot: publicProcedure.input(objectType({ reason: stringType().optional() }).optional()).mutation(async ({ ctx, input }) => {
24886
+ const reason = input?.reason?.trim() || "manual-refresh";
24887
+ await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
24888
+ return { success: true };
24889
+ }),
24890
+ gitTaskStatus: publicProcedure.query(() => {
24891
+ return getDashboardGitTaskStatus();
24892
+ }),
24893
+ subscribeGitTaskStatus: publicProcedure.subscription(() => {
24894
+ return observable((emit) => {
24895
+ emit.next(getDashboardGitTaskStatus());
24896
+ const handler = (status) => {
24897
+ emit.next(status);
24898
+ };
24899
+ dashboardGitTaskStatusEmitter.on("change", handler);
24900
+ return () => {
24901
+ dashboardGitTaskStatusEmitter.off("change", handler);
24902
+ };
24903
+ });
24904
+ })
24905
+ });
24906
+ /**
24133
24907
  * Main app router
24134
24908
  */
24135
24909
  const appRouter = router({
24910
+ dashboard: dashboardRouter,
24136
24911
  spec: specRouter,
24137
24912
  change: changeRouter,
24138
24913
  archive: archiveRouter,
@@ -24142,11 +24917,12 @@ const appRouter = router({
24142
24917
  cli: cliRouter,
24143
24918
  opsx: opsxRouter,
24144
24919
  kv: kvRouter,
24145
- search: searchRouter
24920
+ search: searchRouter,
24921
+ system: systemRouter
24146
24922
  });
24147
24923
 
24148
24924
  //#endregion
24149
- //#region ../search/dist/node.mjs
24925
+ //#region ../search/src/node-worker-provider.ts
24150
24926
  function requestId() {
24151
24927
  return Math.random().toString(36).slice(2);
24152
24928
  }
@@ -24448,7 +25224,12 @@ async function createWebSocketServer(server, httpServer, config) {
24448
25224
  const handler = applyWSSHandler({
24449
25225
  wss,
24450
25226
  router: appRouter,
24451
- createContext: server.createContext
25227
+ createContext: server.createContext,
25228
+ keepAlive: {
25229
+ enabled: true,
25230
+ pingMs: 3e4,
25231
+ pongWaitMs: 5e3
25232
+ }
24452
25233
  });
24453
25234
  const ptyManager = new PtyManager(config.projectDir);
24454
25235
  const ptyWss = new import_websocket_server.default({ noServer: true });
@@ -24589,4 +25370,4 @@ async function startServer$1(options = {}) {
24589
25370
  }
24590
25371
 
24591
25372
  //#endregion
24592
- export { SchemaInfoSchema as a, CliExecutor as c, __commonJS$1 as d, __toESM$1 as f, SchemaDetailSchema as i, ConfigManager as l, createServer$2 as n, SchemaResolutionSchema as o, require_dist as r, TemplatesSchema as s, startServer$1 as t, OpenSpecAdapter as u };
25373
+ export { SchemaInfoSchema as a, CliExecutor as c, OpenSpecAdapter as d, __commonJS$1 as f, SchemaDetailSchema as i, ConfigManager as l, createServer$2 as n, SchemaResolutionSchema as o, __toESM$1 as p, require_dist as r, TemplatesSchema as s, startServer$1 as t, DEFAULT_CONFIG as u };