specsmd 0.1.70 → 0.1.72

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.
@@ -114,6 +114,25 @@ function normalizeTimestamp(value) {
114
114
  return parsed.toISOString();
115
115
  }
116
116
 
117
+ function compareByCreatedAtThenId(a, b) {
118
+ const aTime = a?.createdAt ? Date.parse(a.createdAt) : NaN;
119
+ const bTime = b?.createdAt ? Date.parse(b.createdAt) : NaN;
120
+ const aHasTime = !Number.isNaN(aTime);
121
+ const bHasTime = !Number.isNaN(bTime);
122
+
123
+ if (aHasTime && bHasTime && aTime !== bTime) {
124
+ return aTime - bTime;
125
+ }
126
+ if (aHasTime && !bHasTime) {
127
+ return -1;
128
+ }
129
+ if (!aHasTime && bHasTime) {
130
+ return 1;
131
+ }
132
+
133
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
134
+ }
135
+
117
136
  function parseIntentFolderName(folderName) {
118
137
  const match = String(folderName).match(/^(\d{3})-(.+)$/);
119
138
  if (!match) {
@@ -272,7 +291,8 @@ function parseIntent(intentPath, warnings) {
272
291
  completedStories: storyStats.completed,
273
292
  inProgressStories: storyStats.inProgress,
274
293
  pendingStories: storyStats.pending,
275
- blockedStories: storyStats.blocked
294
+ blockedStories: storyStats.blocked,
295
+ createdAt: normalizeTimestamp(requirementsFrontmatter.created)
276
296
  };
277
297
  }
278
298
 
@@ -486,7 +506,7 @@ function parseAidlcDashboard(workspacePath) {
486
506
  const intents = intentFolders
487
507
  .map((intentFolder) => parseIntent(path.join(intentsPath, intentFolder), warnings))
488
508
  .filter(Boolean)
489
- .sort((a, b) => a.id.localeCompare(b.id));
509
+ .sort(compareByCreatedAtThenId);
490
510
 
491
511
  if (intentFolders.length === 0) {
492
512
  warnings.push('No intents found under memory-bank/intents.');
@@ -350,7 +350,8 @@ function buildPendingItems(intents) {
350
350
  mode: item.mode,
351
351
  complexity: item.complexity,
352
352
  dependencies: item.dependencies || [],
353
- filePath: item.filePath
353
+ filePath: item.filePath,
354
+ createdAt: item.createdAt
354
355
  });
355
356
  }
356
357
  }
@@ -360,6 +361,19 @@ function buildPendingItems(intents) {
360
361
  if (depDiff !== 0) {
361
362
  return depDiff;
362
363
  }
364
+ const aTime = a.createdAt ? Date.parse(a.createdAt) : NaN;
365
+ const bTime = b.createdAt ? Date.parse(b.createdAt) : NaN;
366
+ const aHasTime = !Number.isNaN(aTime);
367
+ const bHasTime = !Number.isNaN(bTime);
368
+ if (aHasTime && bHasTime && aTime !== bTime) {
369
+ return aTime - bTime;
370
+ }
371
+ if (aHasTime && !bHasTime) {
372
+ return -1;
373
+ }
374
+ if (!aHasTime && bHasTime) {
375
+ return 1;
376
+ }
363
377
  return a.id.localeCompare(b.id);
364
378
  });
365
379
 
@@ -12,7 +12,8 @@ const {
12
12
  calculateStats,
13
13
  parseDependencies,
14
14
  buildPendingItems,
15
- normalizeRunWorkItem
15
+ normalizeRunWorkItem,
16
+ normalizeTimestamp
16
17
  } = require('./model');
17
18
 
18
19
  const STANDARD_TYPES = [
@@ -80,6 +81,46 @@ function getFirstStringValue(record, keys) {
80
81
  return undefined;
81
82
  }
82
83
 
84
+ function compareByCreatedAtThenId(a, b) {
85
+ const aTime = a?.createdAt ? Date.parse(a.createdAt) : NaN;
86
+ const bTime = b?.createdAt ? Date.parse(b.createdAt) : NaN;
87
+ const aHasTime = !Number.isNaN(aTime);
88
+ const bHasTime = !Number.isNaN(bTime);
89
+
90
+ if (aHasTime && bHasTime && aTime !== bTime) {
91
+ return aTime - bTime;
92
+ }
93
+ if (aHasTime && !bHasTime) {
94
+ return -1;
95
+ }
96
+ if (!aHasTime && bHasTime) {
97
+ return 1;
98
+ }
99
+
100
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
101
+ }
102
+
103
+ function compareRunDatesDesc(a, b) {
104
+ const aDate = a?.completedAt || a?.startedAt;
105
+ const bDate = b?.completedAt || b?.startedAt;
106
+ const aTime = aDate ? Date.parse(aDate) : NaN;
107
+ const bTime = bDate ? Date.parse(bDate) : NaN;
108
+ const aHasTime = !Number.isNaN(aTime);
109
+ const bHasTime = !Number.isNaN(bTime);
110
+
111
+ if (aHasTime && bHasTime && aTime !== bTime) {
112
+ return bTime - aTime;
113
+ }
114
+ if (aHasTime && !bHasTime) {
115
+ return -1;
116
+ }
117
+ if (!aHasTime && bHasTime) {
118
+ return 1;
119
+ }
120
+
121
+ return String(b?.id || '').localeCompare(String(a?.id || ''));
122
+ }
123
+
83
124
  function parseRunLog(runLogPath) {
84
125
  const content = readFileSafe(runLogPath);
85
126
  if (!content) {
@@ -210,8 +251,8 @@ function scanWorkItems(intentPath, intentId, stateWorkItems, warnings) {
210
251
  filePath,
211
252
  description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
212
253
  dependencies,
213
- createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
214
- completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
254
+ createdAt: normalizeTimestamp(frontmatter.created),
255
+ completedAt: normalizeTimestamp(frontmatter.completed_at)
215
256
  };
216
257
  });
217
258
  }
@@ -252,8 +293,8 @@ function scanIntents(rootPath, normalizedState, warnings) {
252
293
  filePath: briefPath,
253
294
  description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
254
295
  workItems,
255
- createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
256
- completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
296
+ createdAt: normalizeTimestamp(frontmatter.created),
297
+ completedAt: normalizeTimestamp(frontmatter.completed_at)
257
298
  };
258
299
  });
259
300
  }
@@ -325,7 +366,8 @@ function buildActiveRuns(runs, normalizedState) {
325
366
 
326
367
  return (normalizedState.runs?.active || [])
327
368
  .map((active) => byId.get(active.id) || null)
328
- .filter(Boolean);
369
+ .filter(Boolean)
370
+ .sort(compareRunDatesDesc);
329
371
  }
330
372
 
331
373
  function buildCompletedRuns(runs) {
@@ -430,8 +472,8 @@ function parseFireDashboard(workspacePath) {
430
472
 
431
473
  const warnings = [];
432
474
  const normalizedState = normalizeState(rawState);
433
- const intents = scanIntents(rootPath, normalizedState, warnings);
434
- const runs = scanRuns(rootPath, normalizedState);
475
+ const intents = scanIntents(rootPath, normalizedState, warnings).sort(compareByCreatedAtThenId);
476
+ const runs = scanRuns(rootPath, normalizedState).sort(compareRunDatesDesc);
435
477
  const activeRuns = buildActiveRuns(runs, normalizedState);
436
478
  const completedRuns = buildCompletedRuns(runs);
437
479
  const standards = scanStandards(rootPath);
@@ -248,7 +248,8 @@ function buildSpecsData(snapshot) {
248
248
  path: intent.path,
249
249
  storiesComplete: units.reduce((sum, unit) => sum + unit.storiesComplete, 0),
250
250
  storiesTotal: units.reduce((sum, unit) => sum + unit.storiesTotal, 0),
251
- units
251
+ units,
252
+ createdAt: intent.createdAt
252
253
  };
253
254
  });
254
255
 
@@ -400,7 +401,8 @@ function buildFireViewData(snapshot) {
400
401
  mode: normalizeFireMode(item.mode),
401
402
  complexity: normalizeFireComplexity(item.complexity),
402
403
  filePath: item.filePath,
403
- dependencies: item.dependencies || []
404
+ dependencies: item.dependencies || [],
405
+ createdAt: item.createdAt
404
406
  }));
405
407
 
406
408
  const completedRuns = (snapshot.completedRuns || []).map((run) => ({
@@ -418,13 +420,15 @@ function buildFireViewData(snapshot) {
418
420
  status: normalizeFireStatus(intent.status),
419
421
  filePath: intent.filePath,
420
422
  description: intent.description,
423
+ createdAt: intent.createdAt,
421
424
  workItems: (intent.workItems || []).map((item) => ({
422
425
  id: item.id,
423
426
  title: item.title || item.id,
424
427
  status: normalizeFireStatus(item.status),
425
428
  mode: normalizeFireMode(item.mode),
426
429
  complexity: normalizeFireComplexity(item.complexity),
427
- filePath: item.filePath
430
+ filePath: item.filePath,
431
+ createdAt: item.createdAt
428
432
  }))
429
433
  }));
430
434
 
@@ -706,12 +710,16 @@ function createSetDataMessage(data) {
706
710
  };
707
711
  }
708
712
 
709
- const flowInfo = {
710
- id: data.flow,
711
- displayName: flowDisplayName(data.flow),
712
- icon: flowIcon(data.flow),
713
- rootFolder: flowRootFolder(data.flow)
714
- };
713
+ const availableFlows = (data.availableFlows && data.availableFlows.length > 0
714
+ ? data.availableFlows
715
+ : [data.flow])
716
+ .filter(Boolean)
717
+ .map((flow) => ({
718
+ id: flow,
719
+ displayName: flowDisplayName(flow),
720
+ icon: flowIcon(flow),
721
+ rootFolder: flowRootFolder(flow)
722
+ }));
715
723
 
716
724
  if (data.flow === 'fire') {
717
725
  return {
@@ -733,7 +741,7 @@ function createSetDataMessage(data) {
733
741
  specsHtml: '',
734
742
  overviewHtml: '',
735
743
  fireData: buildFireViewData(data.snapshot),
736
- availableFlows: [flowInfo],
744
+ availableFlows,
737
745
  activeFlowId: data.flow
738
746
  };
739
747
  }
@@ -757,7 +765,7 @@ function createSetDataMessage(data) {
757
765
  },
758
766
  specsHtml: getSpecsViewHtml(webviewData),
759
767
  overviewHtml: getOverviewViewHtml(webviewData),
760
- availableFlows: [flowInfo],
768
+ availableFlows,
761
769
  activeFlowId: data.flow
762
770
  };
763
771
  }
@@ -28,6 +28,103 @@
28
28
  document.documentElement.style.colorScheme = theme;
29
29
  }
30
30
 
31
+ function copyText(text) {
32
+ if (navigator.clipboard && navigator.clipboard.writeText) {
33
+ return navigator.clipboard.writeText(text);
34
+ }
35
+
36
+ var textarea = document.createElement('textarea');
37
+ textarea.value = text;
38
+ textarea.setAttribute('readonly', 'readonly');
39
+ textarea.style.position = 'fixed';
40
+ textarea.style.opacity = '0';
41
+ document.body.appendChild(textarea);
42
+ textarea.select();
43
+
44
+ try {
45
+ document.execCommand('copy');
46
+ return Promise.resolve();
47
+ } catch (error) {
48
+ return Promise.reject(error);
49
+ } finally {
50
+ textarea.remove();
51
+ }
52
+ }
53
+
54
+ function closeCommandDialog() {
55
+ var existing = document.querySelector('.specsmd-command-dialog');
56
+ if (existing) {
57
+ existing.remove();
58
+ }
59
+ }
60
+
61
+ function showCommandDialog(command) {
62
+ closeCommandDialog();
63
+
64
+ var overlay = document.createElement('div');
65
+ overlay.className = 'specsmd-command-dialog';
66
+ overlay.setAttribute('role', 'dialog');
67
+ overlay.setAttribute('aria-modal', 'true');
68
+ overlay.setAttribute('aria-label', 'Start FIRE run command');
69
+
70
+ var panel = document.createElement('div');
71
+ panel.className = 'specsmd-command-dialog-panel';
72
+
73
+ var title = document.createElement('div');
74
+ title.className = 'specsmd-command-dialog-title';
75
+ title.textContent = 'Start FIRE run';
76
+
77
+ var description = document.createElement('div');
78
+ description.className = 'specsmd-command-dialog-description';
79
+ description.textContent = 'Run this command from your project folder.';
80
+
81
+ var commandBox = document.createElement('textarea');
82
+ commandBox.className = 'specsmd-command-dialog-command';
83
+ commandBox.value = command;
84
+ commandBox.readOnly = true;
85
+ commandBox.rows = 2;
86
+
87
+ var actions = document.createElement('div');
88
+ actions.className = 'specsmd-command-dialog-actions';
89
+
90
+ var copyButton = document.createElement('button');
91
+ copyButton.type = 'button';
92
+ copyButton.className = 'specsmd-command-dialog-copy';
93
+ copyButton.textContent = 'Copy Command';
94
+
95
+ var closeButton = document.createElement('button');
96
+ closeButton.type = 'button';
97
+ closeButton.className = 'specsmd-command-dialog-close';
98
+ closeButton.textContent = 'Close';
99
+
100
+ copyButton.addEventListener('click', function () {
101
+ copyText(command).then(function () {
102
+ copyButton.textContent = 'Copied';
103
+ }).catch(function () {
104
+ commandBox.focus();
105
+ commandBox.select();
106
+ });
107
+ });
108
+
109
+ closeButton.addEventListener('click', closeCommandDialog);
110
+ overlay.addEventListener('click', function (event) {
111
+ if (event.target === overlay) {
112
+ closeCommandDialog();
113
+ }
114
+ });
115
+
116
+ actions.appendChild(copyButton);
117
+ actions.appendChild(closeButton);
118
+ panel.appendChild(title);
119
+ panel.appendChild(description);
120
+ panel.appendChild(commandBox);
121
+ panel.appendChild(actions);
122
+ overlay.appendChild(panel);
123
+ document.body.appendChild(overlay);
124
+ commandBox.focus();
125
+ commandBox.select();
126
+ }
127
+
31
128
  document.documentElement.dataset.host = 'dashboard-web';
32
129
  applyTheme(readTheme());
33
130
 
@@ -44,4 +141,18 @@
44
141
  document.documentElement.dataset.loaded = 'true';
45
142
  }
46
143
  });
144
+
145
+ window.addEventListener('specsmd-dashboard-command', function (event) {
146
+ if (!event.detail || !event.detail.command) {
147
+ return;
148
+ }
149
+
150
+ showCommandDialog(String(event.detail.command));
151
+ });
152
+
153
+ window.addEventListener('keydown', function (event) {
154
+ if (event.key === 'Escape') {
155
+ closeCommandDialog();
156
+ }
157
+ });
47
158
  }());
@@ -64,3 +64,77 @@ specsmd-app {
64
64
  width: 100vw;
65
65
  height: 100vh;
66
66
  }
67
+
68
+ .specsmd-command-dialog {
69
+ position: fixed;
70
+ inset: 0;
71
+ z-index: 1000;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ padding: 24px;
76
+ background: rgba(0, 0, 0, 0.45);
77
+ }
78
+
79
+ .specsmd-command-dialog-panel {
80
+ width: min(520px, 100%);
81
+ padding: 16px;
82
+ border: 1px solid var(--vscode-sideBarSectionHeader-border);
83
+ border-radius: 8px;
84
+ background: var(--vscode-sideBar-background);
85
+ color: var(--vscode-foreground);
86
+ box-shadow: 0 18px 56px rgba(0, 0, 0, 0.28);
87
+ }
88
+
89
+ .specsmd-command-dialog-title {
90
+ font-size: 15px;
91
+ font-weight: 700;
92
+ margin-bottom: 4px;
93
+ }
94
+
95
+ .specsmd-command-dialog-description {
96
+ margin-bottom: 12px;
97
+ color: var(--vscode-descriptionForeground);
98
+ font-size: 12px;
99
+ }
100
+
101
+ .specsmd-command-dialog-command {
102
+ box-sizing: border-box;
103
+ width: 100%;
104
+ min-height: 58px;
105
+ resize: vertical;
106
+ padding: 10px;
107
+ border: 1px solid var(--vscode-input-border);
108
+ border-radius: 6px;
109
+ background: var(--vscode-input-background);
110
+ color: var(--vscode-foreground);
111
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
112
+ }
113
+
114
+ .specsmd-command-dialog-actions {
115
+ display: flex;
116
+ justify-content: flex-end;
117
+ gap: 8px;
118
+ margin-top: 12px;
119
+ }
120
+
121
+ .specsmd-command-dialog-actions button {
122
+ min-width: 88px;
123
+ padding: 7px 10px;
124
+ border: 1px solid var(--vscode-input-border);
125
+ border-radius: 6px;
126
+ color: var(--vscode-foreground);
127
+ background: transparent;
128
+ font: inherit;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .specsmd-command-dialog-actions button:hover {
133
+ background: var(--vscode-list-hoverBackground);
134
+ }
135
+
136
+ .specsmd-command-dialog-copy {
137
+ border-color: var(--vscode-button-background) !important;
138
+ background: var(--vscode-button-background) !important;
139
+ color: var(--vscode-button-foreground) !important;
140
+ }
@@ -6167,6 +6167,31 @@
6167
6167
 
6168
6168
  <!-- Resources Footer -->
6169
6169
  <div class="resources-footer">
6170
+ <div class="fabriqa-card">
6171
+ <div class="fabriqa-brand">
6172
+ <div class="fabriqa-mark">FA</div>
6173
+ <div>
6174
+ <div class="fabriqa-title">specs.md by Fabriqa.AI</div>
6175
+ <div class="fabriqa-subtitle">Spec-native agentic development environment</div>
6176
+ </div>
6177
+ </div>
6178
+ <div class="fabriqa-copy">
6179
+ Use Fabriqa.AI with your existing AI subscription to design, run, and reuse agentic workflows around your specs. It is free to try.
6180
+ </div>
6181
+ <div class="fabriqa-actions">
6182
+ <div class="fabriqa-link" @click=${() => this._openExternal("https://fabriqa.ai")}>Explore Fabriqa.AI</div>
6183
+ <div class="fabriqa-link secondary" @click=${() => this._openExternal("https://specs.md")}>Open specs.md</div>
6184
+ </div>
6185
+ </div>
6186
+ <div class="dashboard-tip">
6187
+ <div class="dashboard-tip-title">Did you know?</div>
6188
+ <div class="dashboard-tip-copy">
6189
+ You can use the specs.md dashboard outside VS Code and VS Code variants. Run <code>npx specsmd@latest dashboard</code> from your project folder.
6190
+ </div>
6191
+ <div class="fabriqa-actions">
6192
+ <div class="fabriqa-link secondary" @click=${() => this._openExternal("https://specs.md/getting-started/cli-dashboard")}>Dashboard docs</div>
6193
+ </div>
6194
+ </div>
6170
6195
  <div class="resources-title">Links</div>
6171
6196
  <div class="resources-links">
6172
6197
  <div class="resource-link" @click=${() => this._openExternal("https://specs.md")} title="Website">
@@ -6497,6 +6522,104 @@
6497
6522
  .feedback-link:hover {
6498
6523
  opacity: 0.8;
6499
6524
  }
6525
+
6526
+ .fabriqa-card {
6527
+ padding: 12px;
6528
+ margin-bottom: 12px;
6529
+ border: 1px solid rgba(249, 115, 22, 0.35);
6530
+ border-radius: 6px;
6531
+ background: linear-gradient(135deg, rgba(249, 115, 22, 0.12), rgba(34, 197, 94, 0.08));
6532
+ }
6533
+
6534
+ .fabriqa-brand {
6535
+ display: flex;
6536
+ align-items: center;
6537
+ gap: 10px;
6538
+ margin-bottom: 8px;
6539
+ }
6540
+
6541
+ .fabriqa-mark {
6542
+ display: flex;
6543
+ align-items: center;
6544
+ justify-content: center;
6545
+ flex: 0 0 30px;
6546
+ height: 30px;
6547
+ border-radius: 6px;
6548
+ background: var(--status-active);
6549
+ color: #ffffff;
6550
+ font-size: 11px;
6551
+ font-weight: 700;
6552
+ }
6553
+
6554
+ .fabriqa-title {
6555
+ font-size: 13px;
6556
+ font-weight: 700;
6557
+ color: var(--foreground);
6558
+ }
6559
+
6560
+ .fabriqa-subtitle,
6561
+ .fabriqa-copy,
6562
+ .dashboard-tip-copy {
6563
+ font-size: 11px;
6564
+ line-height: 1.45;
6565
+ color: var(--description-foreground);
6566
+ }
6567
+
6568
+ .fabriqa-copy {
6569
+ margin-bottom: 10px;
6570
+ }
6571
+
6572
+ .fabriqa-actions {
6573
+ display: flex;
6574
+ flex-wrap: wrap;
6575
+ gap: 8px;
6576
+ }
6577
+
6578
+ .fabriqa-link {
6579
+ display: inline-flex;
6580
+ align-items: center;
6581
+ justify-content: center;
6582
+ padding: 6px 9px;
6583
+ border-radius: 5px;
6584
+ background: var(--status-active);
6585
+ color: #ffffff;
6586
+ font-size: 11px;
6587
+ font-weight: 600;
6588
+ cursor: pointer;
6589
+ }
6590
+
6591
+ .fabriqa-link.secondary {
6592
+ border: 1px solid var(--border-color);
6593
+ background: var(--editor-background);
6594
+ color: var(--foreground);
6595
+ }
6596
+
6597
+ .fabriqa-link:hover {
6598
+ opacity: 0.86;
6599
+ }
6600
+
6601
+ .dashboard-tip {
6602
+ padding: 10px;
6603
+ margin-bottom: 12px;
6604
+ border: 1px solid var(--border-color);
6605
+ border-radius: 6px;
6606
+ background: var(--editor-background);
6607
+ }
6608
+
6609
+ .dashboard-tip-title {
6610
+ margin-bottom: 4px;
6611
+ color: var(--foreground);
6612
+ font-size: 11px;
6613
+ font-weight: 700;
6614
+ }
6615
+
6616
+ .dashboard-tip code {
6617
+ padding: 1px 4px;
6618
+ border-radius: 4px;
6619
+ background: var(--background);
6620
+ color: var(--foreground);
6621
+ font-family: var(--font-family);
6622
+ }
6500
6623
  `
6501
6624
  ];
6502
6625
  __decorateClass([
@@ -6644,6 +6767,15 @@
6644
6767
  connectEvents();
6645
6768
  return {
6646
6769
  postMessage(message) {
6770
+ if (isStandaloneStartRunMessage(message)) {
6771
+ window.dispatchEvent(new CustomEvent("specsmd-dashboard-command", {
6772
+ detail: {
6773
+ command: buildFireStartRunCommand(message.workItemIds),
6774
+ workItemIds: message.workItemIds
6775
+ }
6776
+ }));
6777
+ return;
6778
+ }
6647
6779
  fetch("/api/message", {
6648
6780
  method: "POST",
6649
6781
  headers: { "content-type": "application/json" },
@@ -6665,6 +6797,13 @@
6665
6797
  }
6666
6798
  };
6667
6799
  }
6800
+ function isStandaloneStartRunMessage(message) {
6801
+ return typeof message === "object" && message !== null && message.type === "startRun" && Array.isArray(message.workItemIds);
6802
+ }
6803
+ function buildFireStartRunCommand(workItemIds) {
6804
+ const ids = workItemIds.map((id) => String(id).trim()).filter(Boolean);
6805
+ return ["/specsmd-fire-builder", ...ids].join(" ");
6806
+ }
6668
6807
  var vscode = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : createStandaloneApi();
6669
6808
 
6670
6809
  // src/webview/components/app.ts
@@ -7105,6 +7244,15 @@
7105
7244
  }
7106
7245
  }
7107
7246
  _handleFireFilterChange(e7) {
7247
+ if (this._fireData) {
7248
+ this._fireData = {
7249
+ ...this._fireData,
7250
+ intentsData: {
7251
+ ...this._fireData.intentsData,
7252
+ filter: e7.detail.filter
7253
+ }
7254
+ };
7255
+ }
7108
7256
  vscode.postMessage({ type: "fireIntentsFilter", filter: e7.detail.filter });
7109
7257
  }
7110
7258
  _handleFireToggleExpand(e7) {
@@ -5,7 +5,7 @@ const { URL } = require('url');
5
5
  const { spawn } = require('child_process');
6
6
  const crypto = require('crypto');
7
7
  const { createWatchRuntime } = require('../runtime/watch-runtime');
8
- const { detectFlow } = require('../flow-detect');
8
+ const { detectAvailableFlows, detectFlow } = require('../flow-detect');
9
9
  const { loadWebDashboardData } = require('./snapshot');
10
10
 
11
11
  const PUBLIC_DIR = path.join(__dirname, 'public');
@@ -179,9 +179,13 @@ async function startDashboardWeb(options = {}) {
179
179
  const clients = new Set();
180
180
  let watcher = null;
181
181
  let lastData = null;
182
+ let activeFlow = options.flow || null;
182
183
 
183
184
  async function loadAndBroadcast() {
184
- lastData = await loadWebDashboardData({ workspacePath, flow: options.flow });
185
+ lastData = await loadWebDashboardData({ workspacePath, flow: activeFlow });
186
+ if (lastData.flow) {
187
+ activeFlow = lastData.flow;
188
+ }
185
189
  const message = lastData.webviewMessage || lastData;
186
190
  const payload = `event: message\ndata: ${JSON.stringify(message)}\n\n`;
187
191
  for (const client of clients) {
@@ -218,6 +222,19 @@ async function startDashboardWeb(options = {}) {
218
222
  sendJson(res, 200, { ok: true, data });
219
223
  return;
220
224
  }
225
+ if (message.type === 'switchFlow') {
226
+ const availableFlows = detectAvailableFlows(workspacePath);
227
+ const requestedFlow = typeof message.flowId === 'string' ? message.flowId : null;
228
+ if (requestedFlow && availableFlows.includes(requestedFlow)) {
229
+ activeFlow = requestedFlow;
230
+ } else if (availableFlows.length > 0) {
231
+ const currentIndex = availableFlows.indexOf(activeFlow);
232
+ activeFlow = availableFlows[(currentIndex + 1) % availableFlows.length];
233
+ }
234
+ const data = await loadAndBroadcast();
235
+ sendJson(res, 200, { ok: true, data });
236
+ return;
237
+ }
221
238
  if (message.type === 'openExternal') {
222
239
  const opened = openExternal(message.url);
223
240
  sendJson(res, opened ? 200 : 400, opened
@@ -308,14 +325,9 @@ async function startDashboardWeb(options = {}) {
308
325
 
309
326
  const initialData = await loadAndBroadcast();
310
327
  if (options.watch !== false && initialData.flow) {
311
- let detection = null;
312
- try {
313
- detection = detectFlow(workspacePath, options.flow);
314
- } catch {
315
- detection = null;
316
- }
317
- const flow = detection?.flow || initialData.flow;
318
- const roots = buildWatchRoots(workspacePath, flow).filter((root) => fs.existsSync(root));
328
+ const roots = detectAvailableFlows(workspacePath)
329
+ .flatMap((flow) => buildWatchRoots(workspacePath, flow))
330
+ .filter((root) => fs.existsSync(root));
319
331
  if (roots.length > 0) {
320
332
  watcher = createWatchRuntime({
321
333
  rootPaths: roots,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {