@beastmode-develeap/beastmode 0.1.271 → 0.1.273

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.
@@ -15,7 +15,7 @@
15
15
  }
16
16
  </script>
17
17
  <!--BOARD_DATA-->
18
- <script>window.__BUILD_STAMP__ = "20260516-112549-29655a8";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260516-120540-608a18f";</script>
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -8514,6 +8514,18 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
8514
8514
  const [onboardingTier, setOnboardingTier] = useState('standard');
8515
8515
  const [showPicker, setShowPicker] = useState(false);
8516
8516
  const [adding, setAdding] = useState(false);
8517
+ // Per-project analysis state (status pill + last-analyzed timestamp).
8518
+ // Keyed by project name; value is the /analyze/status JSON response.
8519
+ const [analysisStatuses, setAnalysisStatuses] = useState({});
8520
+ // Which project card is currently expanded (only one at a time).
8521
+ const [expandedProject, setExpandedProject] = useState(null);
8522
+ // Cached guide content keyed by project name; each entry holds optional l1/l2 strings.
8523
+ const [guideContent, setGuideContent] = useState({});
8524
+ // Per-project transient errors surfaced inside the card body.
8525
+ const [guideErrors, setGuideErrors] = useState({});
8526
+ // Per-project "show full guide" toggle (l2 visible).
8527
+ const [showFullGuide, setShowFullGuide] = useState({});
8528
+ const pollTimersRef = useRef({});
8517
8529
 
8518
8530
  const fetchProjects = useCallback(() => {
8519
8531
  api('GET', '/api/projects')
@@ -8524,6 +8536,101 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
8524
8536
 
8525
8537
  useEffect(() => { fetchProjects(); }, []);
8526
8538
 
8539
+ const fetchAnalysisStatus = useCallback(async (name) => {
8540
+ try {
8541
+ const status = await api('GET', '/api/projects/' + name + '/analyze/status');
8542
+ setAnalysisStatuses(prev => ({ ...prev, [name]: status }));
8543
+ return status;
8544
+ } catch (e) {
8545
+ setAnalysisStatuses(prev => ({ ...prev, [name]: { status: 'idle', error: e.message } }));
8546
+ return null;
8547
+ }
8548
+ }, []);
8549
+
8550
+ // Refresh per-project analysis status whenever the project list changes.
8551
+ useEffect(() => {
8552
+ projects.forEach(p => { fetchAnalysisStatus(p.name); });
8553
+ }, [projects, fetchAnalysisStatus]);
8554
+
8555
+ // Clean up any running poll intervals on unmount.
8556
+ useEffect(() => () => {
8557
+ Object.values(pollTimersRef.current).forEach(t => clearInterval(t));
8558
+ pollTimersRef.current = {};
8559
+ }, []);
8560
+
8561
+ const fetchGuide = useCallback(async (name, level) => {
8562
+ try {
8563
+ const resp = await api('GET', '/api/projects/' + name + '/codebase-guide?level=' + level);
8564
+ setGuideContent(prev => ({
8565
+ ...prev,
8566
+ [name]: { ...(prev[name] || {}), [level]: resp.content || '' },
8567
+ }));
8568
+ setGuideErrors(prev => ({ ...prev, [name]: null }));
8569
+ return resp;
8570
+ } catch (e) {
8571
+ setGuideErrors(prev => ({ ...prev, [name]: e.message }));
8572
+ return null;
8573
+ }
8574
+ }, []);
8575
+
8576
+ const toggleExpand = useCallback(async (name) => {
8577
+ if (expandedProject === name) {
8578
+ setExpandedProject(null);
8579
+ return;
8580
+ }
8581
+ setExpandedProject(name);
8582
+ // Fetch L1 content lazily on first expand if analysis is complete.
8583
+ const st = analysisStatuses[name];
8584
+ if (st && st.status === 'complete' && !(guideContent[name] && guideContent[name].l1)) {
8585
+ await fetchGuide(name, 'l1');
8586
+ }
8587
+ }, [expandedProject, analysisStatuses, guideContent, fetchGuide]);
8588
+
8589
+ const stopPolling = useCallback((name) => {
8590
+ if (pollTimersRef.current[name]) {
8591
+ clearInterval(pollTimersRef.current[name]);
8592
+ delete pollTimersRef.current[name];
8593
+ }
8594
+ }, []);
8595
+
8596
+ const startPolling = useCallback((name) => {
8597
+ stopPolling(name);
8598
+ pollTimersRef.current[name] = setInterval(async () => {
8599
+ const st = await fetchAnalysisStatus(name);
8600
+ if (!st) return;
8601
+ if (st.status === 'complete') {
8602
+ stopPolling(name);
8603
+ // Drop stale L1/L2 caches so the next expand re-fetches them.
8604
+ setGuideContent(prev => { const n = { ...prev }; delete n[name]; return n; });
8605
+ setShowFullGuide(prev => ({ ...prev, [name]: false }));
8606
+ if (expandedProject === name) {
8607
+ await fetchGuide(name, 'l1');
8608
+ }
8609
+ } else if (st.status === 'failed' || st.status === 'idle') {
8610
+ stopPolling(name);
8611
+ }
8612
+ }, 5000);
8613
+ }, [fetchAnalysisStatus, fetchGuide, stopPolling, expandedProject]);
8614
+
8615
+ const reanalyze = useCallback(async (name) => {
8616
+ setGuideErrors(prev => ({ ...prev, [name]: null }));
8617
+ setAnalysisStatuses(prev => ({ ...prev, [name]: { status: 'running' } }));
8618
+ try {
8619
+ await api('POST', '/api/projects/' + name + '/analyze?force=true', {});
8620
+ startPolling(name);
8621
+ } catch (e) {
8622
+ setAnalysisStatuses(prev => ({ ...prev, [name]: { status: 'failed', error: e.message } }));
8623
+ }
8624
+ }, [startPolling]);
8625
+
8626
+ const toggleFullGuide = useCallback(async (name) => {
8627
+ const next = !showFullGuide[name];
8628
+ setShowFullGuide(prev => ({ ...prev, [name]: next }));
8629
+ if (next && !(guideContent[name] && guideContent[name].l2)) {
8630
+ await fetchGuide(name, 'l2');
8631
+ }
8632
+ }, [showFullGuide, guideContent, fetchGuide]);
8633
+
8527
8634
  const addProject = async () => {
8528
8635
  const isGithub = addMode === 'github';
8529
8636
  if (isGithub && !githubUrl.trim()) return;
@@ -8543,19 +8650,26 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
8543
8650
  setSuccess(isGithub ? `Cloned and added: ${registeredPath}` : 'Project added successfully');
8544
8651
 
8545
8652
  if (onboardingTier === 'standard' || onboardingTier === 'full') {
8546
- setSuccess('Project added — running codebase analysis...');
8547
8653
  try {
8548
- await api('POST', '/api/projects/' + projName + '/analyze', {});
8549
- setSuccess('Project added and analyzed');
8550
- } catch { setSuccess('Project added (analysis failed can retry later)'); }
8654
+ const analyzeResult = await api('POST', '/api/projects/' + projName + '/analyze', {});
8655
+ if (analyzeResult && analyzeResult.cached) {
8656
+ setSuccess('Project added — codebase already analyzed');
8657
+ } else {
8658
+ setSuccess('Project added — analyzing codebase…');
8659
+ // Begin polling so the operator sees the status pill flip to
8660
+ // "Analyzed" on the new project card in real time. Card may
8661
+ // render slightly later (after fetchProjects), but startPolling
8662
+ // is idempotent and the interval keeps refreshing status.
8663
+ startPolling(projName);
8664
+ }
8665
+ } catch {
8666
+ setSuccess('Project added (analysis failed — can retry later)');
8667
+ }
8551
8668
  }
8552
8669
 
8553
- if (onboardingTier === 'full') {
8554
- setTimeout(() => {
8555
- if (onProjectChange) onProjectChange(projName);
8556
- navigate('#/new-session');
8557
- }, 1000);
8558
- }
8670
+ // Full tier no longer auto-redirects. The operator stays on the
8671
+ // Projects page so they can watch the analysis status flip and
8672
+ // navigate to /new-session manually when ready.
8559
8673
 
8560
8674
  setTimeout(() => setSuccess(null), 5000);
8561
8675
  fetchProjects();
@@ -8649,22 +8763,127 @@ function ProjectsPage({ selectedProject, onProjectChange }) {
8649
8763
  icon="projects"
8650
8764
  title="No projects yet"
8651
8765
  description="Add a project path to start building." />`
8652
- : projects.map(proj => html`
8653
- <div class="ext-item" key=${proj.name} style="flex-wrap:wrap;">
8654
- <div class="ext-info">
8655
- <div class="ext-name">${proj.name}</div>
8656
- <div class="ext-meta">
8657
- ${proj.repo ? proj.repo + ' \u2014 ' : ''}
8658
- ${proj.stack ? proj.stack.detected || proj.stack.framework || '' : ''}
8659
- ${proj.path ? ' \u2014 ' + proj.path : ''}
8766
+ : projects.map(proj => {
8767
+ const st = analysisStatuses[proj.name] || { status: 'idle' };
8768
+ const expanded = expandedProject === proj.name;
8769
+ const pillState = st.status === 'complete' ? 'complete'
8770
+ : st.status === 'running' ? 'running'
8771
+ : st.status === 'failed' ? 'failed'
8772
+ : 'none';
8773
+ const pillLabel = pillState === 'complete' ? '\u2713 Analyzed'
8774
+ : pillState === 'running' ? '\u23f3 Analyzing\u2026'
8775
+ : pillState === 'failed' ? '\u2717 Failed'
8776
+ : '\u2014 Not analyzed';
8777
+ const pillColor = pillState === 'complete' ? '#22c55e'
8778
+ : pillState === 'running' ? '#f59e0b'
8779
+ : pillState === 'failed' ? '#ef4444'
8780
+ : '#94a3b8';
8781
+ const tsLabel = pillState === 'complete' && st.analyzed_at
8782
+ ? 'Analyzed ' + timeAgo(st.analyzed_at)
8783
+ : pillState === 'failed' ? 'Analysis failed'
8784
+ : pillState === 'running' ? 'Analyzing\u2026'
8785
+ : 'Not analyzed';
8786
+ const guide = guideContent[proj.name] || {};
8787
+ const guideErr = guideErrors[proj.name];
8788
+ const isBusy = pillState === 'running';
8789
+ return html`
8790
+ <div class="ext-item" key=${proj.name} style="flex-wrap:wrap;flex-direction:column;align-items:stretch;">
8791
+ <div style="display:flex;align-items:center;gap:12px;width:100%;cursor:pointer;"
8792
+ onClick=${() => toggleExpand(proj.name)}>
8793
+ <div style="flex:1;min-width:0;">
8794
+ <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
8795
+ <div class="ext-name">${proj.name}</div>
8796
+ <span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;
8797
+ background:${pillColor};color:#fff;white-space:nowrap;"
8798
+ title=${st.error || ''}
8799
+ data-testid="project-status-pill"
8800
+ data-state=${pillState}>${pillLabel}</span>
8801
+ ${proj.stack && (proj.stack.detected || proj.stack.framework)
8802
+ ? html`<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;
8803
+ background:var(--bg-subtle);color:var(--text-secondary);">
8804
+ ${proj.stack.detected || proj.stack.framework}
8805
+ </span>`
8806
+ : null}
8807
+ </div>
8808
+ <div class="ext-meta" style="margin-top:4px;">
8809
+ ${proj.repo ? proj.repo + ' \u2014 ' : ''}
8810
+ ${proj.path || ''}
8811
+ </div>
8812
+ <div class="ext-meta" style="margin-top:2px;font-size:11px;color:var(--text-secondary);"
8813
+ data-testid="project-analyzed-timestamp">${tsLabel}</div>
8814
+ </div>
8815
+ <div class="ext-actions" style="display:flex;gap:6px;flex-wrap:wrap;"
8816
+ onClick=${(e) => e.stopPropagation()}>
8817
+ <button class="btn btn-sm btn-secondary"
8818
+ onClick=${() => reanalyze(proj.name)}
8819
+ disabled=${isBusy}
8820
+ data-testid="project-reanalyze-btn">
8821
+ ${isBusy ? 'Analyzing\u2026' : 'Re-analyze'}
8822
+ </button>
8823
+ <button class="btn btn-sm btn-danger" onClick=${() => removeProject(proj.name)}>Remove</button>
8824
+ <button class="btn btn-sm btn-secondary"
8825
+ onClick=${() => toggleExpand(proj.name)}
8826
+ data-testid="project-expand-btn">${expanded ? '\u25b4' : '\u25be'}</button>
8660
8827
  </div>
8661
8828
  </div>
8662
- <div class="ext-actions">
8663
- <button class="btn btn-sm btn-danger" onClick=${() => removeProject(proj.name)}>Remove</button>
8664
- </div>
8829
+ ${expanded ? html`
8830
+ <div style="margin-top:12px;padding:12px;border-top:1px solid var(--border);"
8831
+ data-testid="codebase-summary-panel">
8832
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
8833
+ <strong style="font-size:13px;">Codebase summary</strong>
8834
+ ${pillState === 'complete' && guide.l1 ? html`
8835
+ <button class="btn btn-sm btn-secondary"
8836
+ onClick=${() => toggleFullGuide(proj.name)}
8837
+ data-testid="project-show-full-guide">
8838
+ ${showFullGuide[proj.name] ? 'Hide full guide' : 'Show full guide'}
8839
+ </button>
8840
+ ` : null}
8841
+ </div>
8842
+ ${guideErr ? html`<div class="error-msg" style="margin-bottom:8px;">${guideErr}</div>` : null}
8843
+ ${pillState === 'complete'
8844
+ ? (guide.l1 == null
8845
+ ? html`<div style="color:var(--text-secondary);font-size:12px;">Loading guide\u2026</div>`
8846
+ : html`
8847
+ <pre style="white-space:pre-wrap;font-family:inherit;font-size:13px;line-height:1.5;
8848
+ background:var(--bg-subtle);padding:10px;border-radius:6px;margin:0;"
8849
+ data-testid="codebase-guide-l1">${guide.l1}</pre>
8850
+ ${showFullGuide[proj.name]
8851
+ ? (guide.l2 == null
8852
+ ? html`<div style="margin-top:8px;color:var(--text-secondary);font-size:12px;">Loading full guide\u2026</div>`
8853
+ : html`
8854
+ <pre style="white-space:pre-wrap;font-family:inherit;font-size:12px;line-height:1.5;
8855
+ background:var(--bg-subtle);padding:10px;border-radius:6px;margin-top:8px;"
8856
+ data-testid="codebase-guide-l2">${guide.l2}</pre>
8857
+ `)
8858
+ : null}
8859
+ `)
8860
+ : pillState === 'running'
8861
+ ? html`<div style="display:flex;align-items:center;gap:8px;color:var(--text-secondary);">
8862
+ <span class="spinner" style="display:inline-block;width:12px;height:12px;border:2px solid var(--border);
8863
+ border-top-color:var(--accent);border-radius:50%;
8864
+ animation:spin 0.8s linear infinite;"></span>
8865
+ Analyzing codebase\u2026
8866
+ </div>`
8867
+ : pillState === 'failed'
8868
+ ? html`<div>
8869
+ <div class="error-msg" style="margin-bottom:8px;">${st.error || 'Analysis failed'}</div>
8870
+ <button class="btn btn-sm btn-primary"
8871
+ onClick=${() => reanalyze(proj.name)}
8872
+ data-testid="project-retry-btn">Retry</button>
8873
+ </div>`
8874
+ : html`<div style="color:var(--text-secondary);">
8875
+ Not analyzed yet \u2014
8876
+ <button class="btn btn-sm btn-primary"
8877
+ style="margin-left:6px;"
8878
+ onClick=${() => reanalyze(proj.name)}
8879
+ data-testid="project-run-analysis-btn">Run analysis</button>
8880
+ </div>`}
8881
+ </div>
8882
+ ` : null}
8665
8883
  <${EnvironmentPanel} projectName=${proj.name} />
8666
8884
  </div>
8667
- `)}
8885
+ `;
8886
+ })}
8668
8887
  </div>
8669
8888
  ${showPicker && html`<${DirectoryPicker} initialPath=${newPath || null} onClose=${() => setShowPicker(false)} onSelect=${(p) => { setNewPath(p); setShowPicker(false); }} />`}
8670
8889
  </div>
@@ -1 +1 @@
1
- 29655a83818304b93c65b4e13f17aba0a0e3866e
1
+ 608a18f813a2b545bf3548c8e1fe036dab59b814
@@ -1 +1 @@
1
- 20260516-112549-29655a8
1
+ 20260516-120540-608a18f
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.271",
3
+ "version": "0.1.273",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {