@openspecui/core 1.2.0 → 1.6.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.
package/dist/index.mjs CHANGED
@@ -1,12 +1,13 @@
1
+ import { n as toOpsxDisplayPath, t as VIRTUAL_PROJECT_DIRNAME } from "./opsx-display-path-DYIjMsbM.mjs";
1
2
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
2
3
  import { dirname, join } from "path";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
4
5
  import { readFile as readFile$1, readdir, stat } from "node:fs/promises";
5
6
  import { dirname as dirname$1, join as join$1, matchesGlob, relative, resolve, sep } from "node:path";
6
- import { existsSync, realpathSync, utimesSync } from "node:fs";
7
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
7
8
  import { z } from "zod";
8
- import { watch } from "fs";
9
9
  import { EventEmitter } from "events";
10
+ import { watch } from "fs";
10
11
  import { exec, spawn } from "child_process";
11
12
  import { promisify } from "util";
12
13
  import { parse } from "yaml";
@@ -220,14 +221,14 @@ var MarkdownParser = class {
220
221
  if (currentOperation === "RENAMED") {
221
222
  const fromMatch = line.match(/FROM:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
222
223
  const toMatch = line.match(/TO:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
223
- if (fromMatch) renameBuffer = {
224
- ...renameBuffer ?? {},
225
- from: fromMatch[1].trim()
226
- };
227
- if (toMatch) renameBuffer = {
228
- ...renameBuffer ?? {},
229
- to: toMatch[1].trim()
230
- };
224
+ if (fromMatch) {
225
+ if (!renameBuffer) renameBuffer = {};
226
+ renameBuffer.from = fromMatch[1].trim();
227
+ }
228
+ if (toMatch) {
229
+ if (!renameBuffer) renameBuffer = {};
230
+ renameBuffer.to = toMatch[1].trim();
231
+ }
231
232
  if (renameBuffer?.from && renameBuffer?.to) {
232
233
  deltas.push({
233
234
  spec: deltaSpec.specId,
@@ -312,86 +313,6 @@ var MarkdownParser = class {
312
313
  }
313
314
  };
314
315
 
315
- //#endregion
316
- //#region src/validator.ts
317
- /**
318
- * Validator for OpenSpec documents
319
- */
320
- var Validator = class {
321
- /**
322
- * Validate a spec document
323
- */
324
- validateSpec(spec) {
325
- const issues = [];
326
- if (!spec.overview || spec.overview.trim().length === 0) issues.push({
327
- severity: "ERROR",
328
- message: "Spec must have a Purpose/Overview section",
329
- path: "overview"
330
- });
331
- if (spec.requirements.length === 0) issues.push({
332
- severity: "ERROR",
333
- message: "Spec must have at least one requirement",
334
- path: "requirements"
335
- });
336
- for (const req of spec.requirements) {
337
- if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
338
- severity: "WARNING",
339
- message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
340
- path: `requirements.${req.id}`
341
- });
342
- if (req.scenarios.length === 0) issues.push({
343
- severity: "WARNING",
344
- message: `Requirement should have at least one scenario: ${req.id}`,
345
- path: `requirements.${req.id}.scenarios`
346
- });
347
- if (req.text.length > 1e3) issues.push({
348
- severity: "WARNING",
349
- message: `Requirement text is too long (max 1000 chars): ${req.id}`,
350
- path: `requirements.${req.id}.text`
351
- });
352
- }
353
- return {
354
- valid: issues.filter((i) => i.severity === "ERROR").length === 0,
355
- issues
356
- };
357
- }
358
- /**
359
- * Validate a change proposal
360
- */
361
- validateChange(change) {
362
- const issues = [];
363
- if (!change.why || change.why.length < 50) issues.push({
364
- severity: "ERROR",
365
- message: "Change \"Why\" section must be at least 50 characters",
366
- path: "why"
367
- });
368
- if (change.why && change.why.length > 500) issues.push({
369
- severity: "WARNING",
370
- message: "Change \"Why\" section should be under 500 characters",
371
- path: "why"
372
- });
373
- if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
374
- severity: "ERROR",
375
- message: "Change must have a \"What Changes\" section",
376
- path: "whatChanges"
377
- });
378
- if (change.deltas.length === 0) issues.push({
379
- severity: "WARNING",
380
- message: "Change should have at least one delta",
381
- path: "deltas"
382
- });
383
- if (change.deltas.length > 50) issues.push({
384
- severity: "WARNING",
385
- message: "Change has too many deltas (max 50)",
386
- path: "deltas"
387
- });
388
- return {
389
- valid: issues.filter((i) => i.severity === "ERROR").length === 0,
390
- issues
391
- };
392
- }
393
- };
394
-
395
316
  //#endregion
396
317
  //#region src/reactive-fs/reactive-state.ts
397
318
  /**
@@ -578,8 +499,10 @@ const DEFAULT_IGNORE = [
578
499
  ".git",
579
500
  "**/.DS_Store"
580
501
  ];
581
- /** 健康检查间隔 (ms) - 3秒 */
582
- const HEALTH_CHECK_INTERVAL_MS = 3e3;
502
+ /** 恢复重试间隔 (ms) */
503
+ const RECOVERY_INTERVAL_MS = 3e3;
504
+ /** 路径语义检查间隔 (ms) */
505
+ const PATH_LIVENESS_INTERVAL_MS = 3e3;
583
506
  /**
584
507
  * 项目监听器
585
508
  *
@@ -602,17 +525,25 @@ var ProjectWatcher = class {
602
525
  ignore;
603
526
  initialized = false;
604
527
  initPromise = null;
605
- healthCheckTimer = null;
606
- lastEventTime = 0;
607
- healthCheckPending = false;
608
- enableHealthCheck;
609
528
  reinitializeTimer = null;
610
529
  reinitializePending = false;
530
+ reinitializeReasonPending = null;
531
+ pathLivenessTimer = null;
532
+ projectDirFingerprint = null;
533
+ generation = 0;
534
+ reinitializeCount = 0;
535
+ lastReinitializeReason = null;
536
+ reinitializeReasonCounts = {
537
+ "drop-events": 0,
538
+ "watcher-error": 0,
539
+ "missing-project-dir": 0,
540
+ "project-dir-replaced": 0,
541
+ manual: 0
542
+ };
611
543
  constructor(projectDir, options = {}) {
612
544
  this.projectDir = getRealPath$1(projectDir);
613
545
  this.debounceMs = options.debounceMs ?? DEBOUNCE_MS$1;
614
546
  this.ignore = options.ignore ?? DEFAULT_IGNORE;
615
- this.enableHealthCheck = options.enableHealthCheck ?? true;
616
547
  }
617
548
  /**
618
549
  * 初始化 watcher
@@ -621,8 +552,11 @@ var ProjectWatcher = class {
621
552
  async init() {
622
553
  if (this.initialized) return;
623
554
  if (this.initPromise) return this.initPromise;
624
- this.initPromise = this.doInit();
625
- await this.initPromise;
555
+ this.initPromise = this.doInit().catch((error) => {
556
+ this.initPromise = null;
557
+ throw error;
558
+ });
559
+ return this.initPromise;
626
560
  }
627
561
  async doInit() {
628
562
  this.subscription = await (await import("@parcel/watcher")).subscribe(this.projectDir, (err, events) => {
@@ -633,43 +567,98 @@ var ProjectWatcher = class {
633
567
  this.handleEvents(events);
634
568
  }, { ignore: this.ignore });
635
569
  this.initialized = true;
636
- this.lastEventTime = Date.now();
637
- if (this.enableHealthCheck) this.startHealthCheck();
570
+ this.generation += 1;
571
+ this.projectDirFingerprint = this.getProjectDirFingerprint();
572
+ this.startPathLivenessMonitor();
638
573
  }
639
574
  /**
640
575
  * 处理 watcher 错误
641
- * 对于 FSEvents dropped 错误,触发延迟重建
576
+ * 统一走错误驱动重建流程
642
577
  */
643
578
  handleWatcherError(err) {
644
579
  if ((err.message || String(err)).includes("Events were dropped")) {
645
580
  if (!this.reinitializePending) {
646
581
  console.warn("[ProjectWatcher] FSEvents dropped events, scheduling reinitialize...");
647
- this.scheduleReinitialize();
582
+ this.scheduleReinitialize("drop-events");
648
583
  }
649
584
  return;
650
585
  }
651
- console.error("[ProjectWatcher] Error:", err);
586
+ console.error("[ProjectWatcher] Watcher error, scheduling reinitialize:", err);
587
+ this.scheduleReinitialize("watcher-error");
652
588
  }
653
589
  /**
654
590
  * 延迟重建 watcher(防抖,避免频繁重建)
655
591
  */
656
- scheduleReinitialize() {
592
+ scheduleReinitialize(reason) {
593
+ this.reinitializeReasonPending = reason;
657
594
  if (this.reinitializePending) return;
658
595
  this.reinitializePending = true;
659
596
  if (this.reinitializeTimer) clearTimeout(this.reinitializeTimer);
660
597
  this.reinitializeTimer = setTimeout(() => {
661
598
  this.reinitializeTimer = null;
662
599
  this.reinitializePending = false;
663
- console.log("[ProjectWatcher] Reinitializing due to FSEvents error...");
664
- this.reinitialize();
665
- }, 1e3);
600
+ const pendingReason = this.reinitializeReasonPending ?? reason;
601
+ this.reinitializeReasonPending = null;
602
+ console.log(`[ProjectWatcher] Reinitializing (reason: ${pendingReason})...`);
603
+ this.reinitialize(pendingReason);
604
+ }, RECOVERY_INTERVAL_MS);
605
+ this.reinitializeTimer.unref();
606
+ }
607
+ /**
608
+ * 读取项目目录指纹(目录不存在时返回 null)
609
+ * 用于检测 path 对应实体是否被替换(inode/dev 漂移)
610
+ */
611
+ getProjectDirFingerprint() {
612
+ try {
613
+ const stat$1 = lstatSync(this.projectDir);
614
+ return `${stat$1.dev}:${stat$1.ino}`;
615
+ } catch {
616
+ return null;
617
+ }
618
+ }
619
+ /**
620
+ * 启动路径语义监测(避免 watcher 绑定到已失效句柄)
621
+ */
622
+ startPathLivenessMonitor() {
623
+ this.stopPathLivenessMonitor();
624
+ this.pathLivenessTimer = setInterval(() => {
625
+ this.checkPathLiveness();
626
+ }, PATH_LIVENESS_INTERVAL_MS);
627
+ this.pathLivenessTimer.unref();
628
+ }
629
+ /**
630
+ * 停止路径语义监测
631
+ */
632
+ stopPathLivenessMonitor() {
633
+ if (this.pathLivenessTimer) {
634
+ clearInterval(this.pathLivenessTimer);
635
+ this.pathLivenessTimer = null;
636
+ }
637
+ }
638
+ /**
639
+ * 只读检查 projectDir 是否仍指向初始化时的目录实体
640
+ */
641
+ checkPathLiveness() {
642
+ if (!this.initialized || this.reinitializePending) return;
643
+ const current = this.getProjectDirFingerprint();
644
+ if (current === null) {
645
+ console.warn("[ProjectWatcher] Project directory missing, scheduling reinitialize...");
646
+ this.scheduleReinitialize("missing-project-dir");
647
+ return;
648
+ }
649
+ if (this.projectDirFingerprint === null) {
650
+ this.projectDirFingerprint = current;
651
+ return;
652
+ }
653
+ if (current !== this.projectDirFingerprint) {
654
+ console.warn("[ProjectWatcher] Project directory replaced, scheduling reinitialize...");
655
+ this.scheduleReinitialize("project-dir-replaced");
656
+ }
666
657
  }
667
658
  /**
668
659
  * 处理原始事件
669
660
  */
670
661
  handleEvents(events) {
671
- this.lastEventTime = Date.now();
672
- this.healthCheckPending = false;
673
662
  const watchEvents = events.map((e) => ({
674
663
  type: e.type,
675
664
  path: e.path
@@ -757,60 +746,29 @@ var ProjectWatcher = class {
757
746
  return this.initialized;
758
747
  }
759
748
  /**
760
- * 启动健康检查定时器
761
- */
762
- startHealthCheck() {
763
- this.stopHealthCheck();
764
- this.healthCheckTimer = setInterval(() => {
765
- this.performHealthCheck();
766
- }, HEALTH_CHECK_INTERVAL_MS);
767
- this.healthCheckTimer.unref();
768
- }
769
- /**
770
- * 停止健康检查定时器
749
+ * 获取 watcher 运行时状态
771
750
  */
772
- stopHealthCheck() {
773
- if (this.healthCheckTimer) {
774
- clearInterval(this.healthCheckTimer);
775
- this.healthCheckTimer = null;
776
- }
777
- this.healthCheckPending = false;
778
- }
779
- /**
780
- * 执行健康检查
781
- *
782
- * 工作流程:
783
- * 1. 如果最近有事件,无需检查
784
- * 2. 如果上次探测还在等待中,说明 watcher 可能失效,尝试重建
785
- * 3. 否则,创建临时文件触发事件,等待下次检查验证
786
- */
787
- async performHealthCheck() {
788
- if (Date.now() - this.lastEventTime < HEALTH_CHECK_INTERVAL_MS) {
789
- this.healthCheckPending = false;
790
- return;
791
- }
792
- if (this.healthCheckPending) {
793
- console.warn("[ProjectWatcher] Health check failed, watcher appears stale. Reinitializing...");
794
- await this.reinitialize();
795
- return;
796
- }
797
- this.healthCheckPending = true;
798
- this.sendProbe();
751
+ get runtimeStatus() {
752
+ return {
753
+ generation: this.generation,
754
+ reinitializeCount: this.reinitializeCount,
755
+ lastReinitializeReason: this.lastReinitializeReason,
756
+ reinitializeReasonCounts: { ...this.reinitializeReasonCounts }
757
+ };
799
758
  }
800
759
  /**
801
- * 发送探测:通过 utimesSync 修改项目目录的时间戳来触发 watcher 事件
760
+ * 记录重建统计
802
761
  */
803
- sendProbe() {
804
- try {
805
- const now = /* @__PURE__ */ new Date();
806
- utimesSync(this.projectDir, now, now);
807
- } catch {}
762
+ markReinitialized(reason) {
763
+ this.reinitializeCount += 1;
764
+ this.lastReinitializeReason = reason;
765
+ this.reinitializeReasonCounts[reason] += 1;
808
766
  }
809
767
  /**
810
768
  * 重新初始化 watcher
811
769
  */
812
- async reinitialize() {
813
- this.stopHealthCheck();
770
+ async reinitialize(reason) {
771
+ this.stopPathLivenessMonitor();
814
772
  if (this.subscription) {
815
773
  try {
816
774
  await this.subscription.unsubscribe();
@@ -819,38 +777,50 @@ var ProjectWatcher = class {
819
777
  }
820
778
  this.initialized = false;
821
779
  this.initPromise = null;
822
- this.healthCheckPending = false;
780
+ this.projectDirFingerprint = null;
823
781
  if (!existsSync(this.projectDir)) {
824
782
  console.warn("[ProjectWatcher] Project directory does not exist, waiting for it to be created...");
825
- this.waitForProjectDir();
783
+ this.waitForProjectDir("missing-project-dir");
826
784
  return;
827
785
  }
828
786
  try {
829
787
  await this.init();
788
+ this.markReinitialized(reason);
830
789
  console.log("[ProjectWatcher] Reinitialized successfully");
831
790
  } catch (err) {
832
791
  console.error("[ProjectWatcher] Failed to reinitialize:", err);
833
- setTimeout(() => this.reinitialize(), HEALTH_CHECK_INTERVAL_MS);
792
+ this.scheduleReinitialize(reason);
834
793
  }
835
794
  }
836
795
  /**
837
796
  * 等待项目目录被创建
838
797
  */
839
- waitForProjectDir() {
840
- const checkInterval = setInterval(() => {
841
- if (existsSync(this.projectDir)) {
842
- clearInterval(checkInterval);
843
- console.log("[ProjectWatcher] Project directory created, reinitializing...");
844
- this.reinitialize();
798
+ waitForProjectDir(reason) {
799
+ this.reinitializeReasonPending = reason;
800
+ this.reinitializePending = true;
801
+ if (this.reinitializeTimer) {
802
+ clearTimeout(this.reinitializeTimer);
803
+ this.reinitializeTimer = null;
804
+ }
805
+ this.reinitializeTimer = setTimeout(() => {
806
+ this.reinitializeTimer = null;
807
+ this.reinitializePending = false;
808
+ if (!existsSync(this.projectDir)) {
809
+ this.waitForProjectDir(reason);
810
+ return;
845
811
  }
846
- }, HEALTH_CHECK_INTERVAL_MS);
847
- checkInterval.unref();
812
+ const pendingReason = this.reinitializeReasonPending ?? reason;
813
+ this.reinitializeReasonPending = null;
814
+ console.log("[ProjectWatcher] Project directory created, reinitializing...");
815
+ this.reinitialize(pendingReason);
816
+ }, RECOVERY_INTERVAL_MS);
817
+ this.reinitializeTimer.unref();
848
818
  }
849
819
  /**
850
820
  * 关闭 watcher
851
821
  */
852
822
  async close() {
853
- this.stopHealthCheck();
823
+ this.stopPathLivenessMonitor();
854
824
  if (this.debounceTimer) {
855
825
  clearTimeout(this.debounceTimer);
856
826
  this.debounceTimer = null;
@@ -860,6 +830,7 @@ var ProjectWatcher = class {
860
830
  this.reinitializeTimer = null;
861
831
  }
862
832
  this.reinitializePending = false;
833
+ this.reinitializeReasonPending = null;
863
834
  if (this.subscription) {
864
835
  await this.subscription.unsubscribe();
865
836
  this.subscription = null;
@@ -868,6 +839,7 @@ var ProjectWatcher = class {
868
839
  this.pendingEvents = [];
869
840
  this.initialized = false;
870
841
  this.initPromise = null;
842
+ this.projectDirFingerprint = null;
871
843
  }
872
844
  };
873
845
  /**
@@ -1032,6 +1004,22 @@ function isWatcherPoolInitialized() {
1032
1004
  function getWatchedProjectDir() {
1033
1005
  return globalProjectDir;
1034
1006
  }
1007
+ /**
1008
+ * 获取 watcher 运行时状态
1009
+ */
1010
+ function getWatcherRuntimeStatus() {
1011
+ if (!globalProjectWatcher) return null;
1012
+ const runtime = globalProjectWatcher.runtimeStatus;
1013
+ return {
1014
+ projectDir: globalProjectDir,
1015
+ initialized: globalProjectWatcher.isInitialized,
1016
+ subscriptionCount: globalProjectWatcher.subscriptionCount,
1017
+ generation: runtime.generation,
1018
+ reinitializeCount: runtime.reinitializeCount,
1019
+ lastReinitializeReason: runtime.lastReinitializeReason,
1020
+ reinitializeReasonCounts: runtime.reinitializeReasonCounts
1021
+ };
1022
+ }
1035
1023
 
1036
1024
  //#endregion
1037
1025
  //#region src/reactive-fs/reactive-fs.ts
@@ -1229,6 +1217,86 @@ function getCacheSize() {
1229
1217
  return stateCache$1.size;
1230
1218
  }
1231
1219
 
1220
+ //#endregion
1221
+ //#region src/validator.ts
1222
+ /**
1223
+ * Validator for OpenSpec documents
1224
+ */
1225
+ var Validator = class {
1226
+ /**
1227
+ * Validate a spec document
1228
+ */
1229
+ validateSpec(spec) {
1230
+ const issues = [];
1231
+ if (!spec.overview || spec.overview.trim().length === 0) issues.push({
1232
+ severity: "ERROR",
1233
+ message: "Spec must have a Purpose/Overview section",
1234
+ path: "overview"
1235
+ });
1236
+ if (spec.requirements.length === 0) issues.push({
1237
+ severity: "ERROR",
1238
+ message: "Spec must have at least one requirement",
1239
+ path: "requirements"
1240
+ });
1241
+ for (const req of spec.requirements) {
1242
+ if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
1243
+ severity: "WARNING",
1244
+ message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
1245
+ path: `requirements.${req.id}`
1246
+ });
1247
+ if (req.scenarios.length === 0) issues.push({
1248
+ severity: "WARNING",
1249
+ message: `Requirement should have at least one scenario: ${req.id}`,
1250
+ path: `requirements.${req.id}.scenarios`
1251
+ });
1252
+ if (req.text.length > 1e3) issues.push({
1253
+ severity: "WARNING",
1254
+ message: `Requirement text is too long (max 1000 chars): ${req.id}`,
1255
+ path: `requirements.${req.id}.text`
1256
+ });
1257
+ }
1258
+ return {
1259
+ valid: issues.filter((i) => i.severity === "ERROR").length === 0,
1260
+ issues
1261
+ };
1262
+ }
1263
+ /**
1264
+ * Validate a change proposal
1265
+ */
1266
+ validateChange(change) {
1267
+ const issues = [];
1268
+ if (!change.why || change.why.length < 50) issues.push({
1269
+ severity: "ERROR",
1270
+ message: "Change \"Why\" section must be at least 50 characters",
1271
+ path: "why"
1272
+ });
1273
+ if (change.why && change.why.length > 500) issues.push({
1274
+ severity: "WARNING",
1275
+ message: "Change \"Why\" section should be under 500 characters",
1276
+ path: "why"
1277
+ });
1278
+ if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
1279
+ severity: "ERROR",
1280
+ message: "Change must have a \"What Changes\" section",
1281
+ path: "whatChanges"
1282
+ });
1283
+ if (change.deltas.length === 0) issues.push({
1284
+ severity: "WARNING",
1285
+ message: "Change should have at least one delta",
1286
+ path: "deltas"
1287
+ });
1288
+ if (change.deltas.length > 50) issues.push({
1289
+ severity: "WARNING",
1290
+ message: "Change has too many deltas (max 50)",
1291
+ path: "deltas"
1292
+ });
1293
+ return {
1294
+ valid: issues.filter((i) => i.severity === "ERROR").length === 0,
1295
+ issues
1296
+ };
1297
+ }
1298
+ };
1299
+
1232
1300
  //#endregion
1233
1301
  //#region src/adapter.ts
1234
1302
  /**
@@ -1890,6 +1958,11 @@ const CURSOR_STYLE_VALUES = [
1890
1958
  "underline",
1891
1959
  "bar"
1892
1960
  ];
1961
+ const TERMINAL_RENDERER_ENGINE_VALUES = ["xterm", "ghostty"];
1962
+ const TerminalRendererEngineSchema = z.enum(TERMINAL_RENDERER_ENGINE_VALUES);
1963
+ function isTerminalRendererEngine(value) {
1964
+ return TERMINAL_RENDERER_ENGINE_VALUES.includes(value);
1965
+ }
1893
1966
  const BASE_PACKAGE_MANAGER_RUNNERS = [
1894
1967
  {
1895
1968
  id: "npx",
@@ -2227,8 +2300,10 @@ const TerminalConfigSchema = z.object({
2227
2300
  fontFamily: z.string().default(""),
2228
2301
  cursorBlink: z.boolean().default(true),
2229
2302
  cursorStyle: z.enum(CURSOR_STYLE_VALUES).default("block"),
2230
- scrollback: z.number().min(0).max(1e5).default(1e3)
2303
+ scrollback: z.number().min(0).max(1e5).default(1e3),
2304
+ rendererEngine: z.string().default("xterm")
2231
2305
  });
2306
+ const DashboardConfigSchema = z.object({ trendPointLimit: z.number().int().min(20).max(500).default(100) });
2232
2307
  /**
2233
2308
  * OpenSpecUI 配置 Schema
2234
2309
  *
@@ -2240,13 +2315,15 @@ const OpenSpecUIConfigSchema = z.object({
2240
2315
  args: z.array(z.string()).optional()
2241
2316
  }).default({}),
2242
2317
  theme: z.enum(THEME_VALUES).default("system"),
2243
- terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
2318
+ terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
2319
+ dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
2244
2320
  });
2245
2321
  /** 默认配置(静态,用于测试和类型) */
2246
2322
  const DEFAULT_CONFIG = {
2247
2323
  cli: {},
2248
2324
  theme: "system",
2249
- terminal: TerminalConfigSchema.parse({})
2325
+ terminal: TerminalConfigSchema.parse({}),
2326
+ dashboard: DashboardConfigSchema.parse({})
2250
2327
  };
2251
2328
  /**
2252
2329
  * 配置管理器
@@ -2312,6 +2389,10 @@ var ConfigManager = class {
2312
2389
  terminal: {
2313
2390
  ...current.terminal,
2314
2391
  ...config.terminal
2392
+ },
2393
+ dashboard: {
2394
+ ...current.dashboard,
2395
+ ...config.dashboard
2315
2396
  }
2316
2397
  };
2317
2398
  const serialized = JSON.stringify(merged, null, 2);
@@ -3048,6 +3129,17 @@ async function isToolConfigured(projectDir, toolId) {
3048
3129
  return (await getConfiguredTools(projectDir)).includes(toolId);
3049
3130
  }
3050
3131
 
3132
+ //#endregion
3133
+ //#region src/dashboard-types.ts
3134
+ const DASHBOARD_METRIC_KEYS = [
3135
+ "specifications",
3136
+ "requirements",
3137
+ "activeChanges",
3138
+ "inProgressChanges",
3139
+ "completedChanges",
3140
+ "taskCompletionPercent"
3141
+ ];
3142
+
3051
3143
  //#endregion
3052
3144
  //#region src/opsx-types.ts
3053
3145
  /** Check if an outputPath contains glob pattern characters */
@@ -3135,17 +3227,20 @@ const SchemaResolutionSchema = z.object({
3135
3227
  "package"
3136
3228
  ]),
3137
3229
  path: z.string(),
3230
+ displayPath: z.string().optional(),
3138
3231
  shadows: z.array(z.object({
3139
3232
  source: z.enum([
3140
3233
  "project",
3141
3234
  "user",
3142
3235
  "package"
3143
3236
  ]),
3144
- path: z.string()
3237
+ path: z.string(),
3238
+ displayPath: z.string().optional()
3145
3239
  }))
3146
3240
  });
3147
3241
  const TemplatesSchema = z.record(z.object({
3148
3242
  path: z.string(),
3243
+ displayPath: z.string().optional(),
3149
3244
  source: z.enum([
3150
3245
  "project",
3151
3246
  "user",
@@ -3189,6 +3284,12 @@ function parseCliJson(raw, schema, label) {
3189
3284
  function toRelativePath(root, absolutePath) {
3190
3285
  return relative(root, absolutePath).split(sep).join("/");
3191
3286
  }
3287
+ function isAbsoluteFsPath(path) {
3288
+ return path.startsWith("/") || /^[A-Za-z]:\//.test(path);
3289
+ }
3290
+ function toAbsoluteProjectPath(projectDir, path) {
3291
+ return isAbsoluteFsPath(path.replace(/\\/g, "/")) ? path : resolve(projectDir, path);
3292
+ }
3192
3293
  async function readEntriesUnderRoot(root) {
3193
3294
  if (!(await reactiveStat(root))?.isDirectory) return [];
3194
3295
  const collectEntries = async (dir) => {
@@ -3627,7 +3728,21 @@ var OpsxKernel = class {
3627
3728
  await touchOpsxProjectDeps(this.projectDir);
3628
3729
  const result = await this.cliExecutor.schemaWhich(name);
3629
3730
  if (!result.success) throw new Error(result.stderr || `openspec schema which failed (exit ${result.exitCode ?? "null"})`);
3630
- return parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
3731
+ const parsed = parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
3732
+ return {
3733
+ ...parsed,
3734
+ displayPath: toOpsxDisplayPath(parsed.path, {
3735
+ source: parsed.source,
3736
+ projectDir: this.projectDir
3737
+ }),
3738
+ shadows: parsed.shadows.map((shadow) => ({
3739
+ ...shadow,
3740
+ displayPath: toOpsxDisplayPath(shadow.path, {
3741
+ source: shadow.source,
3742
+ projectDir: this.projectDir
3743
+ })
3744
+ }))
3745
+ };
3631
3746
  }
3632
3747
  async fetchSchemaDetail(name) {
3633
3748
  await touchOpsxProjectDeps(this.projectDir);
@@ -3651,7 +3766,15 @@ var OpsxKernel = class {
3651
3766
  await touchOpsxProjectDeps(this.projectDir);
3652
3767
  const result = await this.cliExecutor.templates(schema);
3653
3768
  if (!result.success) throw new Error(result.stderr || `openspec templates failed (exit ${result.exitCode ?? "null"})`);
3654
- return parseCliJson(result.stdout, TemplatesSchema, "openspec templates");
3769
+ const templates = parseCliJson(result.stdout, TemplatesSchema, "openspec templates");
3770
+ return Object.fromEntries(Object.entries(templates).map(([artifactId, info]) => [artifactId, {
3771
+ ...info,
3772
+ path: toAbsoluteProjectPath(this.projectDir, info.path),
3773
+ displayPath: toOpsxDisplayPath(info.path, {
3774
+ source: info.source,
3775
+ projectDir: this.projectDir
3776
+ })
3777
+ }]));
3655
3778
  }
3656
3779
  async fetchTemplateContents(schema) {
3657
3780
  await this.ensureTemplates(schema);
@@ -3660,6 +3783,10 @@ var OpsxKernel = class {
3660
3783
  return [artifactId, {
3661
3784
  content: await reactiveReadFile(info.path),
3662
3785
  path: info.path,
3786
+ displayPath: info.displayPath ?? toOpsxDisplayPath(info.path, {
3787
+ source: info.source,
3788
+ projectDir: this.projectDir
3789
+ }),
3663
3790
  source: info.source
3664
3791
  }];
3665
3792
  }));
@@ -3908,4 +4035,4 @@ const PtyServerMessageSchema = z.discriminatedUnion("type", [
3908
4035
  ]);
3909
4036
 
3910
4037
  //#endregion
3911
- export { AI_TOOLS, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliExecutor, ConfigManager, DEFAULT_CONFIG, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, MarkdownParser, OpenSpecAdapter, OpenSpecUIConfigSchema, OpenSpecWatcher, OpsxKernel, ProjectWatcher, PtyAttachMessageSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, ReactiveContext, ReactiveState, RequirementSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, SpecSchema, TaskSchema, TemplatesSchema, TerminalConfigSchema, Validator, acquireWatcher, buildCliRunnerCandidates, clearCache, closeAllProjectWatchers, closeAllWatchers, contextStorage, createCleanCliEnv, createFileChangeObservable, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getProjectWatcher, getToolById, getWatchedProjectDir, initWatcherPool, isGlobPattern, isToolConfigured, isWatcherPoolInitialized, parseCliCommand, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli };
4038
+ export { AI_TOOLS, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliExecutor, ConfigManager, DASHBOARD_METRIC_KEYS, DEFAULT_CONFIG, DashboardConfigSchema, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, MarkdownParser, OpenSpecAdapter, OpenSpecUIConfigSchema, OpenSpecWatcher, OpsxKernel, ProjectWatcher, PtyAttachMessageSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, ReactiveContext, ReactiveState, RequirementSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, SpecSchema, TaskSchema, TemplatesSchema, TerminalConfigSchema, TerminalRendererEngineSchema, VIRTUAL_PROJECT_DIRNAME, Validator, acquireWatcher, buildCliRunnerCandidates, clearCache, closeAllProjectWatchers, closeAllWatchers, contextStorage, createCleanCliEnv, createFileChangeObservable, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getProjectWatcher, getToolById, getWatchedProjectDir, getWatcherRuntimeStatus, initWatcherPool, isGlobPattern, isTerminalRendererEngine, isToolConfigured, isWatcherPoolInitialized, parseCliCommand, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli, toOpsxDisplayPath };