@blackbelt-technology/pi-agent-dashboard 0.4.0 → 0.4.2

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 (129) hide show
  1. package/AGENTS.md +104 -35
  2. package/README.md +390 -494
  3. package/docs/architecture.md +423 -20
  4. package/package.json +11 -8
  5. package/packages/extension/package.json +11 -4
  6. package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
  7. package/packages/extension/src/__tests__/ask-user-tool.test.ts +91 -15
  8. package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
  9. package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
  10. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
  11. package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
  12. package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
  13. package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
  14. package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
  15. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
  16. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
  17. package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
  18. package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
  19. package/packages/extension/src/ask-user-tool.ts +170 -61
  20. package/packages/extension/src/bridge.ts +199 -19
  21. package/packages/extension/src/multiselect-decode.ts +40 -0
  22. package/packages/extension/src/multiselect-list.ts +146 -0
  23. package/packages/extension/src/multiselect-polyfill.ts +73 -0
  24. package/packages/extension/src/server-launcher.ts +15 -3
  25. package/packages/extension/src/ui-modules.ts +272 -0
  26. package/packages/server/package.json +11 -5
  27. package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
  28. package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
  29. package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
  30. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
  31. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
  32. package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
  33. package/packages/server/src/__tests__/directory-service.test.ts +174 -0
  34. package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
  35. package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
  36. package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
  37. package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
  38. package/packages/server/src/__tests__/package-routes.test.ts +136 -3
  39. package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
  40. package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
  41. package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
  42. package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
  43. package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
  44. package/packages/server/src/__tests__/pi-version-skew.test.ts +72 -0
  45. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
  46. package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
  47. package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
  48. package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
  49. package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
  50. package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
  51. package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
  52. package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
  53. package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
  54. package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
  55. package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
  56. package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
  57. package/packages/server/src/browse.ts +118 -13
  58. package/packages/server/src/browser-gateway.ts +19 -0
  59. package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
  60. package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
  61. package/packages/server/src/browser-handlers/handler-context.ts +15 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
  63. package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
  64. package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
  65. package/packages/server/src/cli.ts +61 -15
  66. package/packages/server/src/directory-service.ts +156 -15
  67. package/packages/server/src/event-wiring.ts +111 -10
  68. package/packages/server/src/installed-package-enricher.ts +143 -0
  69. package/packages/server/src/package-manager-wrapper.ts +305 -8
  70. package/packages/server/src/package-source-helpers.ts +104 -0
  71. package/packages/server/src/pending-attach-registry.ts +112 -0
  72. package/packages/server/src/pending-resume-intent-registry.ts +107 -0
  73. package/packages/server/src/pi-core-checker.ts +9 -14
  74. package/packages/server/src/pi-gateway.ts +14 -0
  75. package/packages/server/src/pi-version-skew.ts +12 -1
  76. package/packages/server/src/proposal-attach-naming.ts +47 -0
  77. package/packages/server/src/restart-helper.ts +13 -2
  78. package/packages/server/src/routes/file-routes.ts +29 -3
  79. package/packages/server/src/routes/package-routes.ts +72 -3
  80. package/packages/server/src/routes/plugin-config-routes.ts +129 -0
  81. package/packages/server/src/routes/system-routes.ts +2 -0
  82. package/packages/server/src/server.ts +339 -10
  83. package/packages/server/src/session-api.ts +30 -5
  84. package/packages/server/src/session-order-manager.ts +22 -0
  85. package/packages/server/src/session-scanner.ts +10 -1
  86. package/packages/shared/package.json +9 -2
  87. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
  88. package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
  89. package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
  90. package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
  91. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
  92. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
  93. package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
  94. package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
  95. package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
  96. package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
  97. package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
  98. package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
  99. package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
  100. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
  101. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
  102. package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
  103. package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
  104. package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
  105. package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
  106. package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
  107. package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
  108. package/packages/shared/src/browser-protocol.ts +110 -4
  109. package/packages/shared/src/config.ts +45 -0
  110. package/packages/shared/src/dashboard-plugin/index.ts +11 -0
  111. package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
  112. package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
  113. package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
  114. package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
  115. package/packages/shared/src/openspec-activity-detector.ts +18 -22
  116. package/packages/shared/src/openspec-design-evidence.ts +109 -0
  117. package/packages/shared/src/openspec-poller.ts +117 -3
  118. package/packages/shared/src/openspec-specs-evidence.ts +79 -0
  119. package/packages/shared/src/platform/binary-lookup.ts +96 -1
  120. package/packages/shared/src/platform/index.ts +1 -0
  121. package/packages/shared/src/platform/node-spawn.ts +154 -0
  122. package/packages/shared/src/plugin-bridge-register.ts +139 -0
  123. package/packages/shared/src/protocol.ts +79 -2
  124. package/packages/shared/src/recommended-extensions.ts +7 -1
  125. package/packages/shared/src/rest-api.ts +68 -3
  126. package/packages/shared/src/state-replay.ts +20 -1
  127. package/packages/shared/src/tool-registry/definitions.ts +92 -0
  128. package/packages/shared/src/tool-registry/strategies.ts +17 -3
  129. package/packages/shared/src/types.ts +160 -0
@@ -80,6 +80,12 @@ export interface KnownServer {
80
80
  addedAt: string; // ISO timestamp
81
81
  }
82
82
 
83
+ /**
84
+ * Plugin-specific config namespace.
85
+ * Lives at ~/.pi/dashboard/config.json#plugins.<id>.*
86
+ */
87
+ export type PluginsConfig = Record<string, Record<string, unknown>>;
88
+
83
89
  export interface DashboardConfig {
84
90
  port: number;
85
91
  piPort: number;
@@ -107,6 +113,13 @@ export interface DashboardConfig {
107
113
  electronMode: boolean;
108
114
  /** Persisted list of known remote servers */
109
115
  knownServers: KnownServer[];
116
+ /**
117
+ * Per-plugin config namespaces. Reserved top-level key.
118
+ * Each plugin's config lives at plugins.<id>.*
119
+ * Plugin-shaped legacy top-level keys (e.g. openspec.*) stay at top-level
120
+ * until each extract-*-as-plugin change migrates them.
121
+ */
122
+ plugins: PluginsConfig;
110
123
  }
111
124
 
112
125
  export interface CorsConfig {
@@ -117,6 +130,7 @@ export interface CorsConfig {
117
130
  const VALID_SPAWN_STRATEGIES: SpawnStrategy[] = ["tmux", "headless"];
118
131
 
119
132
  const DEFAULTS: DashboardConfig = {
133
+ plugins: {},
120
134
  port: 8000,
121
135
  piPort: 9999,
122
136
  autoStart: true,
@@ -237,6 +251,36 @@ function parseMemoryLimits(raw: any): MemoryLimitsConfig {
237
251
  };
238
252
  }
239
253
 
254
+ function parsePluginsConfig(raw: unknown): PluginsConfig {
255
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
256
+ const result: PluginsConfig = {};
257
+ for (const [id, val] of Object.entries(raw as Record<string, unknown>)) {
258
+ if (val && typeof val === "object" && !Array.isArray(val)) {
259
+ result[id] = val as Record<string, unknown>;
260
+ }
261
+ }
262
+ return result;
263
+ }
264
+
265
+ /**
266
+ * Get the plugins config block from a loaded DashboardConfig.
267
+ * Provides typed access to plugins.<id>.* namespaces.
268
+ */
269
+ export function getPluginsConfig(config: DashboardConfig): PluginsConfig {
270
+ return config.plugins ?? {};
271
+ }
272
+
273
+ /**
274
+ * Get a single plugin's config from a loaded DashboardConfig.
275
+ * Returns {} if the plugin has no stored config.
276
+ */
277
+ export function getPluginConfig(
278
+ config: DashboardConfig,
279
+ pluginId: string,
280
+ ): Record<string, unknown> {
281
+ return config.plugins?.[pluginId] ?? {};
282
+ }
283
+
240
284
  function parseKnownServers(raw: any): KnownServer[] {
241
285
  if (!Array.isArray(raw)) return [];
242
286
  return raw
@@ -299,6 +343,7 @@ export function loadConfig(): DashboardConfig {
299
343
  ...(typeof parsed.lastServer === "string" ? { lastServer: parsed.lastServer } : {}),
300
344
  electronMode: parsed.electronMode === true,
301
345
  knownServers: parseKnownServers(parsed.knownServers),
346
+ plugins: parsePluginsConfig(parsed.plugins),
302
347
  };
303
348
 
304
349
  // Compute resolvedTrustedNetworks: merge trustedNetworks + auth.bypassHosts
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Barrel export for dashboard plugin system shared types.
3
+ * Import from:
4
+ * @blackbelt-technology/pi-dashboard-shared/dashboard-plugin/index.js
5
+ * @blackbelt-technology/pi-dashboard-shared/dashboard-plugin/slot-types.js
6
+ * etc.
7
+ */
8
+ export * from "./slot-types.js";
9
+ export * from "./manifest-types.js";
10
+ export * from "./slot-props.js";
11
+ export * from "./plugin-status.js";
@@ -0,0 +1,58 @@
1
+ import type { SlotId, SettingsTab } from "./slot-types.js";
2
+
3
+ /**
4
+ * A single slot claim in a plugin manifest.
5
+ */
6
+ export interface PluginClaim {
7
+ /** The slot this claim targets. */
8
+ slot: SlotId;
9
+ /** Exported component name from the plugin's client entry (for React slots). */
10
+ component?: string;
11
+ /** Route command for "command-route" slot (e.g. "/specs"). */
12
+ command?: string;
13
+ /** Trigger id for "anchored-popover" slot. */
14
+ trigger?: string;
15
+ /** toolName for "tool-renderer" slot. */
16
+ toolName?: string;
17
+ /**
18
+ * For "settings-section" slot: which SettingsPanel tab to render in.
19
+ * Defaults to "general" if omitted.
20
+ */
21
+ tab?: SettingsTab;
22
+ /** Slot-specific extra config. */
23
+ config?: Record<string, unknown>;
24
+ /** Optional exported predicate function name for filtering contributions. */
25
+ predicate?: string;
26
+ }
27
+
28
+ /**
29
+ * The pi-dashboard-plugin manifest.
30
+ * Declared as the `pi-dashboard-plugin` field in a package.json,
31
+ * or as a top-level `dashboard-plugin.json` adjacent to package.json.
32
+ */
33
+ export interface PluginManifest {
34
+ /** Globally unique kebab-case plugin id. */
35
+ id: string;
36
+ /** Human-readable display name. */
37
+ displayName: string;
38
+ /**
39
+ * Lower number = rendered earlier in multi-contribution slots.
40
+ * Default 1000. First-party plugins use 100.
41
+ */
42
+ priority?: number;
43
+ /** Relative path to the bundled client entry (from package root). */
44
+ client?: string;
45
+ /** Optional relative path to the server entry. */
46
+ server?: string;
47
+ /** Optional relative path to a pi-extension/bridge entry. */
48
+ bridge?: string;
49
+ /** Optional relative path to a JSON Schema 7 file for plugin config validation. */
50
+ configSchema?: string;
51
+ /** Slot claims. */
52
+ claims: PluginClaim[];
53
+ /**
54
+ * When true, the plugin is a test fixture and SHALL be excluded from
55
+ * production bundles (NODE_ENV=production).
56
+ */
57
+ fixture?: boolean;
58
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Plugin status types used in /api/health.plugins[] and WebSocket broadcasts.
3
+ */
4
+
5
+ /** Status of a single discovered plugin, reported by /api/health. */
6
+ export interface PluginStatus {
7
+ id: string;
8
+ enabled: boolean;
9
+ loaded: boolean;
10
+ /** Error message if the plugin failed to load or has a conflict. */
11
+ error?: string;
12
+ /** Number of slot claims declared in the plugin's manifest. */
13
+ claims: number;
14
+ }
15
+
16
+ /** WebSocket broadcast sent to all browsers when a plugin's config changes. */
17
+ export interface PluginConfigUpdate {
18
+ type: "plugin_config_update";
19
+ /** Plugin id that was updated. */
20
+ id: string;
21
+ /**
22
+ * Only this plugin's namespace config (plugins.<id>.*).
23
+ * Never contains other plugins' config.
24
+ */
25
+ config: unknown;
26
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Typed prop contracts for each slot id.
3
+ *
4
+ * Every slot consumer passes exactly the props defined here to each contribution
5
+ * component. Plugins receive only the props for the slot they claim.
6
+ *
7
+ * NOTE: PluginContext is imported as a type-only forward reference so this
8
+ * shared package doesn't depend on the runtime package. The runtime package
9
+ * will re-export this map with the concrete PluginContext type filled in.
10
+ */
11
+ import type { DashboardSession } from "../types.js";
12
+ import type { SlotId } from "./slot-types.js";
13
+
14
+ /**
15
+ * Opaque marker type for PluginContext.
16
+ * The concrete type is defined in @blackbelt-technology/dashboard-plugin-runtime/context.
17
+ * Using `unknown` here keeps this shared types-only package free of runtime deps.
18
+ */
19
+ export type AnyPluginContext = unknown;
20
+
21
+ /** Folder descriptor passed to sidebar-folder-section slot. */
22
+ export interface FolderDescriptor {
23
+ cwd: string;
24
+ label?: string;
25
+ }
26
+
27
+ /** Map of slot id → props type for that slot's contributions. */
28
+ export interface SlotPropsMap {
29
+ "sidebar-folder-section": {
30
+ folder: FolderDescriptor;
31
+ pluginContext: AnyPluginContext;
32
+ };
33
+ "session-card-badge": {
34
+ session: DashboardSession;
35
+ pluginContext: AnyPluginContext;
36
+ };
37
+ "session-card-action-bar": {
38
+ session: DashboardSession;
39
+ pluginContext: AnyPluginContext;
40
+ };
41
+ "content-view": {
42
+ session: DashboardSession;
43
+ routeParams: Record<string, string>;
44
+ onClose: () => void;
45
+ pluginContext: AnyPluginContext;
46
+ };
47
+ "content-header-sticky": {
48
+ session: DashboardSession;
49
+ pluginContext: AnyPluginContext;
50
+ };
51
+ "content-inline-footer": {
52
+ session: DashboardSession;
53
+ pluginContext: AnyPluginContext;
54
+ };
55
+ "anchored-popover": {
56
+ anchorEl: HTMLElement;
57
+ onDismiss: () => void;
58
+ pluginContext: AnyPluginContext;
59
+ };
60
+ "command-route": {
61
+ session: DashboardSession;
62
+ routeParams: Record<string, string>;
63
+ onClose: () => void;
64
+ pluginContext: AnyPluginContext;
65
+ };
66
+ "settings-section": {
67
+ pluginContext: AnyPluginContext;
68
+ };
69
+ "tool-renderer": {
70
+ toolName: string;
71
+ toolInput: Record<string, unknown>;
72
+ sessionId: string;
73
+ pluginContext: AnyPluginContext;
74
+ };
75
+ // Descriptor-only slots don't have React props (consumed by extension-ui-system)
76
+ "management-modal": Record<string, unknown>;
77
+ "footer-segment": Record<string, unknown>;
78
+ "agent-metric": Record<string, unknown>;
79
+ "breadcrumb": Record<string, unknown>;
80
+ "gate": Record<string, unknown>;
81
+ "toast": Record<string, unknown>;
82
+ "rjsf-form": Record<string, unknown>;
83
+ }
84
+
85
+ /** Get the props type for a specific slot id. */
86
+ export type SlotProps<S extends SlotId> = SlotPropsMap[S];
87
+
88
+ // Type-level test: assert SlotPropsMap covers every SlotId.
89
+ // This will produce a TS error if any SlotId is not in SlotPropsMap.
90
+ type _AssertAllSlotsCovered = {
91
+ [K in SlotId]: SlotPropsMap[K];
92
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Frozen slot taxonomy for the dashboard plugin system.
3
+ * These ids and their payload contracts are versioned via
4
+ * @blackbelt-technology/pi-dashboard-shared.
5
+ *
6
+ * Adding a slot: minor (non-breaking).
7
+ * Removing or renaming a slot: major (breaking).
8
+ */
9
+
10
+ /** All valid slot ids (frozen for v0.x). */
11
+ export type SlotId =
12
+ // React-only slots
13
+ | "sidebar-folder-section"
14
+ | "session-card-action-bar"
15
+ | "content-inline-footer"
16
+ | "anchored-popover"
17
+ | "command-route"
18
+ | "tool-renderer"
19
+ // React-or-descriptor slots
20
+ | "session-card-badge"
21
+ | "content-view"
22
+ | "content-header-sticky"
23
+ | "settings-section"
24
+ // Descriptor-only slots (from extension-ui-system)
25
+ | "management-modal"
26
+ | "footer-segment"
27
+ | "agent-metric"
28
+ | "breadcrumb"
29
+ | "gate"
30
+ | "toast"
31
+ | "rjsf-form";
32
+
33
+ /** How many contributions a slot allows. */
34
+ export type Multiplicity = "one" | "many" | "one-active";
35
+
36
+ /** Which payload types the slot accepts. */
37
+ export type PayloadTier = "react-only" | "descriptor-only" | "react-or-descriptor";
38
+
39
+ export interface SlotDefinition {
40
+ multiplicity: Multiplicity;
41
+ payloadTier: PayloadTier;
42
+ description: string;
43
+ }
44
+
45
+ /** Frozen slot definitions map for v0.x. */
46
+ export const SLOT_DEFINITIONS: Record<SlotId, SlotDefinition> = {
47
+ "sidebar-folder-section": {
48
+ multiplicity: "many",
49
+ payloadTier: "react-only",
50
+ description: "Collapsible block above session list per workspace folder",
51
+ },
52
+ "session-card-badge": {
53
+ multiplicity: "many",
54
+ payloadTier: "react-or-descriptor",
55
+ description: "Compact info chip on a session card",
56
+ },
57
+ "session-card-action-bar": {
58
+ multiplicity: "many",
59
+ payloadTier: "react-only",
60
+ description: "Action buttons on a session card",
61
+ },
62
+ "content-view": {
63
+ multiplicity: "one-active",
64
+ payloadTier: "react-or-descriptor",
65
+ description: "Full-screen content area view for a session",
66
+ },
67
+ "content-header-sticky": {
68
+ multiplicity: "many",
69
+ payloadTier: "react-or-descriptor",
70
+ description: "Sticky header above the content view",
71
+ },
72
+ "content-inline-footer": {
73
+ multiplicity: "many",
74
+ payloadTier: "react-only",
75
+ description: "Inline footer below the content view (React-only)",
76
+ },
77
+ "anchored-popover": {
78
+ multiplicity: "one",
79
+ payloadTier: "react-only",
80
+ description: "Popover anchored to a UI trigger element",
81
+ },
82
+ "command-route": {
83
+ multiplicity: "many",
84
+ payloadTier: "react-only",
85
+ description: "Maps a slash command or URL route to a content view",
86
+ },
87
+ "settings-section": {
88
+ multiplicity: "many",
89
+ payloadTier: "react-or-descriptor",
90
+ description: "A section in the Settings page",
91
+ },
92
+ "tool-renderer": {
93
+ multiplicity: "many",
94
+ payloadTier: "react-only",
95
+ description: "Custom React renderer for a specific tool call by toolName",
96
+ },
97
+ // Descriptor-only (extension-ui-system)
98
+ "management-modal": {
99
+ multiplicity: "many",
100
+ payloadTier: "descriptor-only",
101
+ description: "Full-screen management modal (extension-ui-system)",
102
+ },
103
+ "footer-segment": {
104
+ multiplicity: "many",
105
+ payloadTier: "descriptor-only",
106
+ description: "Segment in the session footer bar (extension-ui-system)",
107
+ },
108
+ "agent-metric": {
109
+ multiplicity: "one",
110
+ payloadTier: "descriptor-only",
111
+ description: "Metric chip on an agent card (extension-ui-system)",
112
+ },
113
+ "breadcrumb": {
114
+ multiplicity: "many",
115
+ payloadTier: "descriptor-only",
116
+ description: "Breadcrumb item in the content header (extension-ui-system)",
117
+ },
118
+ "gate": {
119
+ multiplicity: "many",
120
+ payloadTier: "descriptor-only",
121
+ description: "Flow gate/checkpoint UI (extension-ui-system)",
122
+ },
123
+ "toast": {
124
+ multiplicity: "many",
125
+ payloadTier: "descriptor-only",
126
+ description: "Transient notification toast (extension-ui-system)",
127
+ },
128
+ "rjsf-form": {
129
+ multiplicity: "many",
130
+ payloadTier: "descriptor-only",
131
+ description: "JSON-Schema-driven form (extension-ui-system Phase 4)",
132
+ },
133
+ };
134
+
135
+ /** Valid settings tab ids in SettingsPanel. */
136
+ export type SettingsTab =
137
+ | "general"
138
+ | "servers"
139
+ | "packages"
140
+ | "providers"
141
+ | "security"
142
+ | "advanced";
143
+
144
+ export const VALID_SETTINGS_TABS: SettingsTab[] = [
145
+ "general",
146
+ "servers",
147
+ "packages",
148
+ "providers",
149
+ "security",
150
+ "advanced",
151
+ ];
@@ -83,29 +83,25 @@ export function detectOpenSpecActivity(
83
83
  }
84
84
 
85
85
  if (tool === "bash") {
86
- const command = args.command as string | undefined;
87
- if (!command || !command.includes("openspec")) return null;
88
-
89
- // Check for --change flag
90
- const flagMatch = command.match(CLI_CHANGE_FLAG_RE);
91
- if (flagMatch) {
92
- return { changeName: flagMatch[1], isActive: true };
93
- }
94
-
95
- // Check for openspec archive <name>
96
- const archiveMatch = command.match(CLI_ARCHIVE_RE);
97
- if (archiveMatch) {
98
- return { changeName: archiveMatch[1], isActive: true };
99
- }
100
-
101
- // Check for openspec new change "name"
102
- const newChangeMatch = command.match(CLI_NEW_CHANGE_RE);
103
- if (newChangeMatch) {
104
- return { changeName: newChangeMatch[1], isActive: true };
86
+ const command = args.command as string | undefined;
87
+ if (!command || !command.includes("openspec")) return null;
88
+
89
+ // Try each CLI regex in order; first match wins.
90
+ const match =
91
+ command.match(CLI_CHANGE_FLAG_RE) ??
92
+ command.match(CLI_ARCHIVE_RE) ??
93
+ command.match(CLI_NEW_CHANGE_RE);
94
+ if (!match) return null;
95
+
96
+ const name = match[1];
97
+ // Reject flag-shaped tokens (e.g. `--help`, `-h`). The CLI regex capture
98
+ // groups use `[^\s"']+` which would otherwise treat `--help` as a change
99
+ // name and trigger downstream auto-attach + auto-rename.
100
+ // See change: fix-openspec-flag-rename-bug.
101
+ if (name.startsWith("-")) return null;
102
+
103
+ return { changeName: name, isActive: true };
105
104
  }
106
105
 
107
- return null;
108
- }
109
-
110
106
  return null;
111
107
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Local-evidence override for the OpenSpec `design` artifact.
3
+ *
4
+ * The upstream `spec-driven` schema requires `design.md` as a hard,
5
+ * single-file dependency of `tasks`. Two real-world workflows fight that:
6
+ *
7
+ * • **Split design** — users put design content in `design-rendering.md`
8
+ * + `design-state.md`. The CLI doesn't see them; status reports
9
+ * `design: ready` forever; dashboard shows `[Continue] [FF]` instead of
10
+ * `[Apply]`.
11
+ * • **No-design changes** — trivial fixes that don't need a design doc.
12
+ * User writes `tasks.md`, starts implementing; CLI still says
13
+ * `design: ready`; dashboard buttons are wrong.
14
+ *
15
+ * This module computes a boolean "is design satisfied locally?" from
16
+ * file-system evidence the CLI ignores. It is consumed by:
17
+ *
18
+ * 1. `buildOpenSpecData` in `openspec-poller.ts` — promotes
19
+ * `artifacts[design].status` from "ready" to "done" when the rules fire.
20
+ * Promote-only; design-only; never demotes; never touches other artifacts.
21
+ *
22
+ * 2. `.pi/skills/openspec-shared/scripts/effective-status.sh` — the
23
+ * OpenSpec workflow skills invoke this wrapper instead of
24
+ * `openspec status --json` so skill-driven prompts and dashboard buttons
25
+ * cannot disagree.
26
+ *
27
+ * Three rules, evaluated in order with short-circuit:
28
+ *
29
+ * R1 any file matching ^design.*\.md$ exists in the change folder
30
+ * R2 a design/ subdirectory exists with at least one *.md inside
31
+ * R3 tasks.md exists AND contains at least one Markdown checkbox
32
+ * (^\s*-\s+\[[ xX]\]\s)
33
+ *
34
+ * R3 is heuristic but defensible: a user who wrote actionable tasks has
35
+ * already made the design decisions. The schema's hard dependency is
36
+ * paperwork we don't believe in for trivial changes.
37
+ *
38
+ * See change: fix-openspec-design-detection.
39
+ */
40
+
41
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
42
+ import path from "node:path";
43
+
44
+ /** Probe surface — kept tiny so unit tests can pass an in-memory stub. */
45
+ export interface DesignEvidenceProbe {
46
+ /** R1: any file matching `^design.*\.md$` in `changeDir`. */
47
+ hasDesignFile(changeDir: string): boolean;
48
+ /** R2: `<changeDir>/design/` exists and contains at least one `*.md`. */
49
+ hasDesignDirWithMd(changeDir: string): boolean;
50
+ /** R3: `<changeDir>/tasks.md` contains at least one Markdown checkbox. */
51
+ tasksHasCheckboxes(changeDir: string): boolean;
52
+ }
53
+
54
+ /** Pure rule evaluator. R1 → R2 → R3, short-circuits on first match. */
55
+ export function evaluateLocalDesignSatisfaction(
56
+ changeDir: string,
57
+ probe: DesignEvidenceProbe,
58
+ ): boolean {
59
+ if (probe.hasDesignFile(changeDir)) return true;
60
+ if (probe.hasDesignDirWithMd(changeDir)) return true;
61
+ if (probe.tasksHasCheckboxes(changeDir)) return true;
62
+ return false;
63
+ }
64
+
65
+ const DESIGN_FILE_RE = /^design.*\.md$/;
66
+ const CHECKBOX_RE = /^\s*-\s+\[[ xX]\]\s/m;
67
+
68
+ /** Production probe — backed by the real filesystem. Sync, defensive. */
69
+ export function createFsDesignEvidenceProbe(): DesignEvidenceProbe {
70
+ return {
71
+ hasDesignFile(changeDir) {
72
+ try {
73
+ const entries = readdirSync(changeDir, { withFileTypes: true });
74
+ for (const e of entries) {
75
+ if (e.isFile() && DESIGN_FILE_RE.test(e.name)) return true;
76
+ }
77
+ return false;
78
+ } catch {
79
+ return false;
80
+ }
81
+ },
82
+
83
+ hasDesignDirWithMd(changeDir) {
84
+ const dir = path.join(changeDir, "design");
85
+ try {
86
+ const st = statSync(dir);
87
+ if (!st.isDirectory()) return false;
88
+ const entries = readdirSync(dir, { withFileTypes: true });
89
+ for (const e of entries) {
90
+ if (e.isFile() && e.name.endsWith(".md")) return true;
91
+ }
92
+ return false;
93
+ } catch {
94
+ return false;
95
+ }
96
+ },
97
+
98
+ tasksHasCheckboxes(changeDir) {
99
+ const tasks = path.join(changeDir, "tasks.md");
100
+ if (!existsSync(tasks)) return false;
101
+ try {
102
+ const text = readFileSync(tasks, "utf8");
103
+ return CHECKBOX_RE.test(text);
104
+ } catch {
105
+ return false;
106
+ }
107
+ },
108
+ };
109
+ }