claude-autopm 3.22.1 → 3.23.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.
@@ -252,10 +252,12 @@ function getProjectDiagrams() {
252
252
  for (const f of entries) {
253
253
  if (!f.endsWith('.mmd')) continue;
254
254
  try {
255
- diagrams.push({
256
- name: f.replace(/\.mmd$/, ''),
257
- content: fs.readFileSync(path.join(diagramsDir, f), 'utf8')
258
- });
255
+ const name = f.replace(/\.mmd$/, '');
256
+ const d = { name, content: fs.readFileSync(path.join(diagramsDir, f), 'utf8') };
257
+ // Load metadata if available
258
+ const metaPath = path.join(diagramsDir, name + '.meta.json');
259
+ try { const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); d.type = meta.type || 'custom'; } catch { d.type = 'custom'; }
260
+ diagrams.push(d);
259
261
  } catch { /* skip unreadable */ }
260
262
  }
261
263
  return diagrams;
@@ -288,6 +290,7 @@ function renderHTML() {
288
290
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
289
291
  <title>AutoPM Config Dashboard</title>
290
292
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><\/script>
293
+ <script src="https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js"><\/script>
291
294
  <style>
292
295
  * { margin: 0; padding: 0; box-sizing: border-box; }
293
296
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; }
@@ -365,42 +368,84 @@ function renderHTML() {
365
368
  .toast.ok { background: #238636; }
366
369
  .toast.err { background: #da3633; }
367
370
  footer { margin-top: 24px; text-align: center; color: #484f58; font-size: 12px; }
368
- /* Diagram cards */
369
- .diagram-card { position: relative; }
370
- .diagram-card .diagram-toolbar { display: flex; gap: 6px; position: absolute; top: 12px; right: 12px; z-index: 2; }
371
- .diagram-card .diagram-toolbar button {
371
+ /* Diagram list */
372
+ .diagram-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
373
+ .diagram-grid-item {
374
+ background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 16px; cursor: pointer; transition: border-color 0.15s;
375
+ }
376
+ .diagram-grid-item:hover { border-color: #58a6ff; }
377
+ .diagram-grid-item h4 { color: #58a6ff; margin-bottom: 4px; }
378
+ .diagram-grid-item .diagram-meta { color: #8b949e; font-size: 11px; }
379
+ .diagram-grid-item .diagram-preview { height: 100px; overflow: hidden; opacity: 0.6; margin-top: 8px; }
380
+ .diagram-grid-item .diagram-preview svg { max-height: 100px; width: 100%; }
381
+ /* Diagram split editor */
382
+ .diagram-editor { display: none; }
383
+ .diagram-editor.active { display: flex; flex-direction: column; }
384
+ .diagram-editor-toolbar {
385
+ display: flex; justify-content: space-between; align-items: center;
386
+ padding: 8px 0; margin-bottom: 8px;
387
+ }
388
+ .diagram-editor-toolbar .left { display: flex; align-items: center; gap: 12px; }
389
+ .diagram-editor-toolbar .right { display: flex; gap: 6px; }
390
+ .diagram-editor-toolbar button {
372
391
  background: #21262d; color: #8b949e; border: 1px solid #30363d;
373
- padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
392
+ padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;
374
393
  }
375
- .diagram-card .diagram-toolbar button:hover { background: #30363d; color: #c9d1d9; }
376
- .diagram-card .mermaid-wrap { overflow: auto; max-height: 400px; cursor: pointer; }
377
- .diagram-card .mermaid-wrap:hover { outline: 1px solid #30363d; border-radius: 4px; }
378
- /* Fullscreen modal */
379
- .diagram-modal {
380
- display: none; position: fixed; inset: 0; z-index: 1000;
381
- background: rgba(0,0,0,0.85); justify-content: center; align-items: center;
394
+ .diagram-editor-toolbar button:hover { background: #30363d; color: #c9d1d9; }
395
+ .diagram-editor-toolbar .btn-save { background: #238636; color: #fff; border-color: #238636; }
396
+ .diagram-editor-toolbar .btn-save:hover { background: #2ea043; }
397
+ .diagram-editor-toolbar .btn-back { color: #58a6ff; }
398
+ .diagram-split {
399
+ display: flex; gap: 0; flex: 1; min-height: 500px;
400
+ border: 1px solid #30363d; border-radius: 8px; overflow: hidden;
382
401
  }
383
- .diagram-modal.open { display: flex; }
384
- .diagram-modal .modal-inner {
385
- background: #161b22; border: 1px solid #30363d; border-radius: 12px;
386
- width: 95vw; height: 92vh; display: flex; flex-direction: column; position: relative;
402
+ .diagram-split .code-pane {
403
+ flex: 1; display: flex; flex-direction: column; border-right: 1px solid #30363d; min-width: 0;
387
404
  }
388
- .diagram-modal .modal-header {
389
- display: flex; justify-content: space-between; align-items: center;
390
- padding: 12px 20px; border-bottom: 1px solid #30363d;
405
+ .diagram-split .code-pane .pane-header {
406
+ background: #161b22; padding: 6px 12px; font-size: 11px; color: #8b949e;
407
+ border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center;
408
+ }
409
+ .diagram-split .code-pane textarea {
410
+ flex: 1; background: #0d1117; color: #c9d1d9; border: none; padding: 12px;
411
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 13px;
412
+ line-height: 1.5; resize: none; outline: none; tab-size: 2;
413
+ }
414
+ .diagram-split .preview-pane {
415
+ flex: 1; display: flex; flex-direction: column; background: #161b22; min-width: 0;
416
+ }
417
+ .diagram-split .preview-pane .pane-header {
418
+ background: #161b22; padding: 6px 12px; font-size: 11px; color: #8b949e;
419
+ border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center;
420
+ }
421
+ .diagram-split .preview-pane .pane-header button {
422
+ background: none; border: none; color: #8b949e; cursor: pointer; font-size: 11px; padding: 2px 6px;
423
+ }
424
+ .diagram-split .preview-pane .pane-header button:hover { color: #c9d1d9; }
425
+ .diagram-split .preview-container {
426
+ flex: 1; overflow: hidden; position: relative; background: #0d1117;
391
427
  }
392
- .diagram-modal .modal-header h3 { color: #c9d1d9; font-size: 14px; }
393
- .diagram-modal .modal-header .modal-actions { display: flex; gap: 8px; }
394
- .diagram-modal .modal-header button {
428
+ .diagram-split .preview-container svg { width: 100%; height: 100%; }
429
+ .panzoom-hint {
430
+ position: absolute; bottom: 8px; right: 12px; color: #484f58; font-size: 10px; pointer-events: none; z-index: 1;
431
+ }
432
+ /* Diagram card actions */
433
+ .diagram-grid-item .card-actions {
434
+ display: flex; gap: 6px; margin-top: 8px; border-top: 1px solid #21262d; padding-top: 8px;
435
+ }
436
+ .diagram-grid-item .card-actions button {
395
437
  background: #21262d; color: #8b949e; border: 1px solid #30363d;
396
- padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px;
438
+ padding: 3px 10px; border-radius: 4px; cursor: pointer; font-size: 11px;
397
439
  }
398
- .diagram-modal .modal-header button:hover { background: #30363d; color: #c9d1d9; }
399
- .diagram-modal .modal-body {
400
- flex: 1; overflow: auto; padding: 20px; display: flex;
401
- justify-content: center; align-items: flex-start;
440
+ .diagram-grid-item .card-actions button:hover { background: #30363d; color: #c9d1d9; }
441
+ .diagram-grid-item .card-actions .btn-remove { color: #f85149; }
442
+ .diagram-grid-item .card-actions .btn-remove:hover { background: #3d1116; border-color: #f85149; }
443
+ /* Fullscreen editor */
444
+ .diagram-editor.fullscreen {
445
+ position: fixed; inset: 0; z-index: 998; background: #0d1117;
446
+ padding: 12px; display: flex; flex-direction: column;
402
447
  }
403
- .diagram-modal .modal-body svg { max-width: 100%; height: auto; }
448
+ .diagram-editor.fullscreen .diagram-split { min-height: 0; flex: 1; }
404
449
  </style>
405
450
  </head>
406
451
  <body>
@@ -531,30 +576,63 @@ function renderHTML() {
531
576
 
532
577
  <!-- Diagrams Tab -->
533
578
  <div id="tab-diagrams" class="tab-content">
534
- <div id="diagram-epic-card" class="card diagram-card" style="display:none;">
535
- <h3>Epic Flow <span style="color:#8b949e;font-size:11px;font-weight:normal;margin-left:8px;">PRD → Epic → Tasks workflow</span></h3>
536
- <div class="diagram-toolbar">
537
- <button onclick="downloadMermaid('diagram-epic')">↓ .mmd</button>
538
- <button onclick="openDiagramModal('diagram-epic','Epic Flow')">⤢ Fullscreen</button>
579
+ <!-- Diagram list view -->
580
+ <div id="diagram-list-view">
581
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
582
+ <p class="desc" style="margin:0;">Project diagrams stored in <code>.claude/pm/diagrams/</code></p>
583
+ <button class="btn" onclick="showNewDiagramForm()">+ New Diagram</button>
539
584
  </div>
540
- <div class="mermaid-wrap" onclick="openDiagramModal('diagram-epic','Epic Flow')">
541
- <pre class="mermaid" id="diagram-epic"></pre>
585
+ <!-- New diagram form (hidden by default) -->
586
+ <div id="new-diagram-form" class="card" style="display:none;margin-bottom:16px;">
587
+ <div class="row">
588
+ <div><label>Name (slug)</label><input type="text" id="new-diagram-name" placeholder="e.g. frontend, backend, database"></div>
589
+ <div><label>Type</label><select id="new-diagram-type">
590
+ <option value="architecture">architecture</option>
591
+ <option value="modules">modules</option>
592
+ <option value="data-flow">data-flow</option>
593
+ <option value="dependencies">dependencies</option>
594
+ <option value="custom">custom</option>
595
+ </select></div>
596
+ </div>
597
+ <label>Description</label>
598
+ <input type="text" id="new-diagram-desc" placeholder="e.g. Frontend React component tree">
599
+ <div style="margin-top:12px;">
600
+ <button class="btn" onclick="createNewDiagram()">Create</button>
601
+ <button class="btn" style="background:#21262d;margin-left:6px;" onclick="hideNewDiagramForm()">Cancel</button>
602
+ </div>
542
603
  </div>
604
+ <div id="diagram-grid" class="diagram-grid"></div>
543
605
  </div>
544
- <div id="project-diagrams"></div>
545
- </div>
546
-
547
- <!-- Diagram fullscreen modal -->
548
- <div class="diagram-modal" id="diagram-modal" role="dialog" aria-modal="true" aria-labelledby="modal-diagram-title" onclick="if(event.target===this)closeDiagramModal()">
549
- <div class="modal-inner">
550
- <div class="modal-header">
551
- <h3 id="modal-diagram-title"></h3>
552
- <div class="modal-actions">
553
- <button onclick="downloadMermaid(currentModalDiagram)">↓ Download .mmd</button>
554
- <button onclick="closeDiagramModal()">✕ Close</button>
606
+ <!-- Diagram editor view (split: code | preview) -->
607
+ <div id="diagram-editor-view" class="diagram-editor">
608
+ <div class="diagram-editor-toolbar">
609
+ <div class="left">
610
+ <button class="btn-back" onclick="closeDiagramEditor()">← Back</button>
611
+ <strong id="editor-diagram-name" style="color:#c9d1d9;font-size:14px;"></strong>
612
+ <span id="editor-unsaved" style="color:#d29922;font-size:11px;display:none;">● unsaved</span>
613
+ </div>
614
+ <div class="right">
615
+ <button onclick="downloadCurrentDiagram()">↓ .mmd</button>
616
+ <button onclick="toggleEditorFullscreen()">⤢ Fullscreen</button>
617
+ <button class="btn-save" onclick="saveDiagram()">Save</button>
618
+ </div>
619
+ </div>
620
+ <div class="diagram-split" id="diagram-split">
621
+ <div class="code-pane">
622
+ <div class="pane-header"><span>Mermaid Source</span></div>
623
+ <textarea id="diagram-code" spellcheck="false"></textarea>
624
+ </div>
625
+ <div class="preview-pane">
626
+ <div class="pane-header">
627
+ <span>Preview</span>
628
+ <button onclick="resetPanZoom()">Reset zoom</button>
629
+ </div>
630
+ <div class="preview-container" id="diagram-preview-container">
631
+ <div id="diagram-preview-render"></div>
632
+ <div class="panzoom-hint">Scroll to zoom · Drag to pan</div>
633
+ </div>
555
634
  </div>
556
635
  </div>
557
- <div class="modal-body" id="modal-diagram-body"></div>
558
636
  </div>
559
637
  </div>
560
638
 
@@ -589,140 +667,235 @@ const headers = { 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'applicati
589
667
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
590
668
 
591
669
  function showTab(name, btn) {
670
+ // Close diagram editor if leaving diagrams tab
671
+ if (currentDiagramName && name !== 'diagrams') closeDiagramEditor();
592
672
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
593
673
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
594
674
  document.getElementById('tab-' + name).classList.add('active');
595
675
  btn.classList.add('active');
596
- if (name === 'diagrams') renderMermaid();
676
+ if (name === 'diagrams') renderDiagramsTab();
677
+ // Pause auto-refresh on tabs with editable forms
678
+ editingLock = (name === 'mcp' || name === 'diagrams');
597
679
  }
598
680
 
599
- let diagramsRendered = false;
600
- const diagramSources = {};
601
- const diagramNames = {};
602
- let currentModalDiagram = null;
681
+ // --- Diagram editor state ---
682
+ let diagramsLoaded = false;
683
+ let allDiagrams = [];
684
+ let currentDiagramName = null;
685
+ let currentDiagramSaved = '';
686
+ let panzoomInstance = null;
687
+ let previewDebounce = null;
603
688
 
604
- function renderMermaid() {
605
- if (diagramsRendered) return;
689
+ function renderDiagramsTab() {
690
+ if (diagramsLoaded) return;
606
691
  fetch('/api/diagrams', { headers: { 'Authorization': 'Bearer ' + TOKEN } })
607
692
  .then(r => r.json())
608
693
  .then(data => {
609
- // Epic Flow — only show if there are actual epics/PRDs
610
- const epicCard = document.getElementById('diagram-epic-card');
611
- const epicEl = document.getElementById('diagram-epic');
612
- if (data.epicFlowHasData) {
613
- epicCard.style.display = '';
614
- epicEl.className = 'mermaid';
615
- epicEl.textContent = data.epicFlow;
616
- diagramSources['diagram-epic'] = data.epicFlow;
617
- diagramNames['diagram-epic'] = 'epic-flow';
618
- } else {
619
- epicCard.style.display = 'none';
620
- epicEl.className = '';
621
- epicEl.textContent = '';
622
- }
623
- document.getElementById('diagram-plugins').textContent = data.pluginGraph;
624
- document.getElementById('diagram-agents').textContent = data.agentTree;
625
- diagramSources['diagram-plugins'] = data.pluginGraph;
626
- diagramSources['diagram-agents'] = data.agentTree;
627
- diagramNames['diagram-plugins'] = 'plugins';
628
- diagramNames['diagram-agents'] = 'agents';
629
- // Render project diagrams dynamically
630
- const container = document.getElementById('project-diagrams');
631
- container.innerHTML = '';
632
- if (data.projectDiagrams && data.projectDiagrams.length > 0) {
633
- data.projectDiagrams.forEach((d, i) => {
634
- const id = 'proj-diagram-' + i;
635
- diagramSources[id] = d.content;
636
- diagramNames[id] = d.name;
637
- const card = document.createElement('div');
638
- card.className = 'card diagram-card';
639
- const h3 = document.createElement('h3');
640
- h3.textContent = d.name;
641
- card.appendChild(h3);
642
- const toolbar = document.createElement('div');
643
- toolbar.className = 'diagram-toolbar';
644
- const dlBtn = document.createElement('button');
645
- dlBtn.textContent = '↓ .mmd';
646
- dlBtn.onclick = function(e) { e.stopPropagation(); downloadMermaid(id); };
647
- const fsBtn = document.createElement('button');
648
- fsBtn.textContent = '⤢ Fullscreen';
649
- fsBtn.onclick = function(e) { e.stopPropagation(); openDiagramModal(id, d.name); };
650
- toolbar.appendChild(dlBtn);
651
- toolbar.appendChild(fsBtn);
652
- card.appendChild(toolbar);
653
- const wrap = document.createElement('div');
654
- wrap.className = 'mermaid-wrap';
655
- wrap.onclick = function() { openDiagramModal(id, d.name); };
656
- const pre = document.createElement('pre');
657
- pre.className = 'mermaid';
658
- pre.id = id;
659
- pre.textContent = d.content;
660
- wrap.appendChild(pre);
661
- card.appendChild(wrap);
662
- container.appendChild(card);
663
- });
664
- } else {
665
- const msg = document.createElement('div');
666
- msg.className = 'card';
667
- msg.innerHTML = '<p class="desc">No project diagrams yet. Run <code>/pm:diagram-new</code> to create your first diagram.</p>';
668
- container.appendChild(msg);
669
- }
670
- if (typeof mermaid !== 'undefined' && mermaid.run) {
671
- mermaid.run();
672
- } else {
673
- const errorMsg = 'Mermaid library unavailable. Check network connection.';
674
- ['diagram-epic','diagram-plugins','diagram-agents'].forEach(id => {
675
- const el = document.getElementById(id);
676
- if (el) el.textContent = errorMsg;
677
- });
678
- document.querySelectorAll('#project-diagrams .mermaid').forEach(el => {
679
- el.textContent = errorMsg;
694
+ allDiagrams = [];
695
+ if (data.projectDiagrams) {
696
+ data.projectDiagrams.forEach(d => {
697
+ allDiagrams.push({ name: d.name, content: d.content, type: d.type || 'custom' });
680
698
  });
681
699
  }
682
- diagramsRendered = true;
700
+ renderDiagramGrid();
701
+ diagramsLoaded = true;
683
702
  });
684
703
  }
685
704
 
686
- function openDiagramModal(diagramId, title) {
687
- currentModalDiagram = diagramId;
688
- document.getElementById('modal-diagram-title').textContent = title || diagramId;
689
- const body = document.getElementById('modal-diagram-body');
690
- body.textContent = '';
691
- const source = diagramSources[diagramId];
692
- if (source) {
705
+ function renderDiagramGrid() {
706
+ const grid = document.getElementById('diagram-grid');
707
+ grid.textContent = '';
708
+ if (allDiagrams.length === 0) {
709
+ const msg = document.createElement('p');
710
+ msg.className = 'desc';
711
+ msg.textContent = 'No diagrams yet. Click "+ New Diagram" or run /pm:diagram-new <name>.';
712
+ grid.appendChild(msg);
713
+ return;
714
+ }
715
+ allDiagrams.forEach(d => {
716
+ const item = document.createElement('div');
717
+ item.className = 'diagram-grid-item';
718
+ item.setAttribute('tabindex', '0');
719
+ const h4 = document.createElement('h4');
720
+ h4.textContent = d.name;
721
+ const meta = document.createElement('div');
722
+ meta.className = 'diagram-meta';
723
+ meta.textContent = d.type;
724
+ const preview = document.createElement('div');
725
+ preview.className = 'diagram-preview';
693
726
  const pre = document.createElement('pre');
694
727
  pre.className = 'mermaid';
695
- pre.textContent = source;
696
- body.appendChild(pre);
697
- if (typeof mermaid !== 'undefined' && mermaid.run) {
698
- mermaid.run({ nodes: body.querySelectorAll('.mermaid') });
699
- }
700
- } else {
701
- body.textContent = 'Diagram not available.';
728
+ pre.textContent = d.content;
729
+ preview.appendChild(pre);
730
+ // Action buttons
731
+ const actions = document.createElement('div');
732
+ actions.className = 'card-actions';
733
+ const editBtn = document.createElement('button');
734
+ editBtn.textContent = 'Edit';
735
+ editBtn.onclick = function(e) { e.stopPropagation(); openDiagramEditor(d.name); };
736
+ const dlBtn = document.createElement('button');
737
+ dlBtn.textContent = '↓ .mmd';
738
+ dlBtn.onclick = function(e) { e.stopPropagation(); downloadDiagram(d.name, d.content); };
739
+ const rmBtn = document.createElement('button');
740
+ rmBtn.textContent = 'Remove';
741
+ rmBtn.className = 'btn-remove';
742
+ rmBtn.onclick = function(e) { e.stopPropagation(); removeDiagram(d.name); };
743
+ actions.appendChild(editBtn);
744
+ actions.appendChild(dlBtn);
745
+ actions.appendChild(rmBtn);
746
+ item.appendChild(h4);
747
+ item.appendChild(meta);
748
+ item.appendChild(preview);
749
+ item.appendChild(actions);
750
+ // Click card body (not actions) opens editor
751
+ item.onclick = function(e) { if (!e.target.closest('.card-actions')) openDiagramEditor(d.name); };
752
+ item.onkeydown = function(e) {
753
+ if (e.target && e.target.closest('.card-actions')) return;
754
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDiagramEditor(d.name); }
755
+ };
756
+ grid.appendChild(item);
757
+ });
758
+ if (typeof mermaid !== 'undefined' && mermaid.run) {
759
+ mermaid.run({ nodes: grid.querySelectorAll('.mermaid') });
702
760
  }
703
- const modal = document.getElementById('diagram-modal');
704
- modal.classList.add('open');
705
- modalPrevFocus = document.activeElement;
706
- const closeBtn = modal.querySelector('.modal-actions button:last-child');
707
- if (closeBtn) closeBtn.focus();
708
- document.addEventListener('keydown', modalEscHandler);
709
761
  }
710
762
 
711
- let modalPrevFocus = null;
712
- function closeDiagramModal() {
713
- document.getElementById('diagram-modal').classList.remove('open');
714
- document.removeEventListener('keydown', modalEscHandler);
715
- currentModalDiagram = null;
716
- if (modalPrevFocus) { modalPrevFocus.focus(); modalPrevFocus = null; }
763
+ // New diagram form
764
+ function showNewDiagramForm() { document.getElementById('new-diagram-form').style.display = ''; }
765
+ function hideNewDiagramForm() { document.getElementById('new-diagram-form').style.display = 'none'; }
766
+
767
+ async function createNewDiagram() {
768
+ const name = document.getElementById('new-diagram-name').value.trim().replace(/[^a-zA-Z0-9_-]/g, '-');
769
+ if (!name) { toast('Name is required', false); return; }
770
+ if (allDiagrams.find(d => d.name === name)) { toast('Diagram "' + name + '" already exists', false); return; }
771
+ const type = document.getElementById('new-diagram-type').value;
772
+ const desc = document.getElementById('new-diagram-desc').value.trim();
773
+ const template = 'graph TD\\n A[' + esc(desc || name) + ']';
774
+ try {
775
+ await api('POST', '/api/diagrams/' + encodeURIComponent(name), { content: template, type: type, description: desc });
776
+ hideNewDiagramForm();
777
+ document.getElementById('new-diagram-name').value = '';
778
+ document.getElementById('new-diagram-desc').value = '';
779
+ diagramsLoaded = false;
780
+ renderDiagramsTab();
781
+ toast('Diagram "' + name + '" created', true);
782
+ } catch (e) { /* api() already shows toast */ }
783
+ }
784
+
785
+ async function removeDiagram(name) {
786
+ if (!confirm('Remove diagram "' + name + '"?')) return;
787
+ try {
788
+ await api('DELETE', '/api/diagrams/' + encodeURIComponent(name));
789
+ allDiagrams = allDiagrams.filter(d => d.name !== name);
790
+ renderDiagramGrid();
791
+ toast('Diagram removed', true);
792
+ } catch (e) { /* api() already shows toast */ }
717
793
  }
718
794
 
719
- function modalEscHandler(e) { if (e.key === 'Escape') closeDiagramModal(); }
795
+ function downloadDiagram(name, content) {
796
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_') + '.mmd';
797
+ const blob = new Blob([content], { type: 'text/plain' });
798
+ const a = document.createElement('a');
799
+ const url = URL.createObjectURL(blob);
800
+ a.href = url; a.download = safeName; a.style.display = 'none';
801
+ document.body.appendChild(a);
802
+ try { a.click(); } finally { document.body.removeChild(a); setTimeout(function() { URL.revokeObjectURL(url); }, 100); }
803
+ }
720
804
 
721
- function downloadMermaid(diagramId) {
722
- const src = diagramSources[diagramId];
723
- if (!src) return;
724
- const label = diagramNames[diagramId] || diagramId.replace('proj-diagram-', 'diagram-').replace('diagram-', '');
725
- const safeName = label.replace(/[^a-zA-Z0-9_-]/g, '_') + '.mmd';
805
+ function openDiagramEditor(name) {
806
+ const d = allDiagrams.find(x => x.name === name);
807
+ if (!d) return;
808
+ currentDiagramName = name;
809
+ currentDiagramSaved = d.content;
810
+ document.getElementById('editor-diagram-name').textContent = name;
811
+ document.getElementById('editor-unsaved').style.display = 'none';
812
+ const codeEl = document.getElementById('diagram-code');
813
+ codeEl.value = d.content;
814
+ codeEl.readOnly = false;
815
+ document.getElementById('diagram-list-view').style.display = 'none';
816
+ document.getElementById('diagram-editor-view').classList.add('active');
817
+ renderPreview(d.content);
818
+ }
819
+
820
+ function closeDiagramEditor() {
821
+ clearTimeout(previewDebounce); previewDebounce = null;
822
+ if (panzoomInstance) { panzoomInstance.dispose(); panzoomInstance = null; }
823
+ const editor = document.getElementById('diagram-editor-view');
824
+ editor.classList.remove('active', 'fullscreen');
825
+ document.getElementById('diagram-list-view').style.display = '';
826
+ currentDiagramName = null;
827
+ }
828
+
829
+ function toggleEditorFullscreen() {
830
+ document.getElementById('diagram-editor-view').classList.toggle('fullscreen');
831
+ // Re-render preview to fit new size
832
+ if (panzoomInstance) { panzoomInstance.dispose(); panzoomInstance = null; }
833
+ setTimeout(function() { renderPreview(document.getElementById('diagram-code').value); }, 100);
834
+ }
835
+
836
+ function renderPreview(source) {
837
+ const container = document.getElementById('diagram-preview-render');
838
+ container.textContent = '';
839
+ if (panzoomInstance) { panzoomInstance.dispose(); panzoomInstance = null; }
840
+ if (!source || !source.trim()) { container.textContent = 'Empty diagram'; return; }
841
+ const pre = document.createElement('pre');
842
+ pre.className = 'mermaid';
843
+ pre.textContent = source;
844
+ container.appendChild(pre);
845
+ if (typeof mermaid !== 'undefined' && mermaid.run) {
846
+ mermaid.run({ nodes: [pre] }).then(function() {
847
+ const svg = container.querySelector('svg');
848
+ if (svg && typeof panzoom !== 'undefined') {
849
+ svg.style.width = '100%';
850
+ svg.style.height = '100%';
851
+ panzoomInstance = panzoom(svg, { maxZoom: 10, minZoom: 0.1, smoothScroll: false });
852
+ }
853
+ }).catch(function() {
854
+ container.textContent = 'Mermaid syntax error — check your code';
855
+ });
856
+ }
857
+ }
858
+
859
+ function resetPanZoom() {
860
+ if (panzoomInstance) {
861
+ panzoomInstance.moveTo(0, 0);
862
+ panzoomInstance.zoomAbs(0, 0, 1);
863
+ }
864
+ }
865
+
866
+ // Live preview on code change
867
+ document.addEventListener('DOMContentLoaded', function() {
868
+ const codeEl = document.getElementById('diagram-code');
869
+ if (codeEl) {
870
+ codeEl.addEventListener('input', function() {
871
+ document.getElementById('editor-unsaved').style.display = codeEl.value !== currentDiagramSaved ? '' : 'none';
872
+ clearTimeout(previewDebounce);
873
+ previewDebounce = setTimeout(function() { renderPreview(codeEl.value); }, 600);
874
+ });
875
+ }
876
+ });
877
+
878
+ async function saveDiagram() {
879
+ if (!currentDiagramName) return;
880
+ const d = allDiagrams.find(x => x.name === currentDiagramName);
881
+ const content = document.getElementById('diagram-code').value;
882
+ try {
883
+ await api('POST', '/api/diagrams/' + encodeURIComponent(currentDiagramName), { content: content });
884
+ currentDiagramSaved = content;
885
+ if (d) d.content = content;
886
+ document.getElementById('editor-unsaved').style.display = 'none';
887
+ // Re-render grid so mini previews reflect saved content
888
+ diagramsLoaded = false;
889
+ toast('Diagram saved', true);
890
+ } catch (e) {
891
+ /* api() already shows toast */
892
+ }
893
+ }
894
+
895
+ function downloadCurrentDiagram() {
896
+ const src = document.getElementById('diagram-code').value;
897
+ if (!src || !currentDiagramName) return;
898
+ const safeName = currentDiagramName.replace(/[^a-zA-Z0-9_-]/g, '_') + '.mmd';
726
899
  const blob = new Blob([src], { type: 'text/plain' });
727
900
  const a = document.createElement('a');
728
901
  const url = URL.createObjectURL(blob);
@@ -1113,19 +1286,8 @@ function addEnvSuggestion(key, val, hint) {
1113
1286
  addEnvRow(key, val);
1114
1287
  }
1115
1288
 
1116
- // Pause auto-refresh while user is editing MCP or env forms
1289
+ // Pause auto-refresh while MCP & Keys tab is active (editing forms)
1117
1290
  let editingLock = false;
1118
- document.addEventListener('focusin', function(e) {
1119
- if (e.target.closest('#mcp-list, #env-list, .mcp-entry, .env-row')) editingLock = true;
1120
- });
1121
- document.addEventListener('focusout', function(e) {
1122
- if (e.target.closest('#mcp-list, #env-list, .mcp-entry, .env-row')) {
1123
- setTimeout(function() {
1124
- const active = document.activeElement;
1125
- if (!active || !active.closest('#mcp-list, #env-list, .mcp-entry, .env-row')) editingLock = false;
1126
- }, 200);
1127
- }
1128
- });
1129
1291
 
1130
1292
  async function refresh() {
1131
1293
  if (editingLock) return;
@@ -1195,6 +1357,43 @@ const server = http.createServer(async (req, res) => {
1195
1357
  return json(res, 200, getDiagramsData());
1196
1358
  }
1197
1359
 
1360
+ // Save diagram content
1361
+ const diagramMatch = parsedUrl.pathname.match(/^\/api\/diagrams\/([a-zA-Z0-9_-]+)$/);
1362
+ if (req.method === 'POST' && diagramMatch) {
1363
+ const name = decodeURIComponent(diagramMatch[1]);
1364
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) return json(res, 400, { error: 'Invalid diagram name' });
1365
+ const { body, err } = await parseJSONBody(req, res);
1366
+ if (err) return;
1367
+ if (typeof body.content !== 'string') return json(res, 400, { error: 'content must be a string' });
1368
+ if (body.content.length > 500000) return json(res, 400, { error: 'content too large (max 500KB)' });
1369
+ const diagramsDir = path.join(basePath, '.claude', 'pm', 'diagrams');
1370
+ if (!fs.existsSync(diagramsDir)) fs.mkdirSync(diagramsDir, { recursive: true });
1371
+ fs.writeFileSync(path.join(diagramsDir, name + '.mmd'), body.content, 'utf8');
1372
+ // Update metadata timestamp if exists, or create
1373
+ const metaPath = path.join(diagramsDir, name + '.meta.json');
1374
+ let meta = {};
1375
+ try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch {}
1376
+ meta.name = meta.name || name;
1377
+ if (typeof body.type === 'string') meta.type = body.type.slice(0, 50);
1378
+ if (typeof body.description === 'string') meta.description = body.description.slice(0, 500);
1379
+ meta.updated = new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');
1380
+ if (!meta.created) meta.created = meta.updated;
1381
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
1382
+ return json(res, 200, { ok: true });
1383
+ }
1384
+
1385
+ const diagramDeleteMatch = parsedUrl.pathname.match(/^\/api\/diagrams\/([a-zA-Z0-9_-]+)$/);
1386
+ if (req.method === 'DELETE' && diagramDeleteMatch) {
1387
+ const name = decodeURIComponent(diagramDeleteMatch[1]);
1388
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) return json(res, 400, { error: 'Invalid diagram name' });
1389
+ const diagramsDir = path.join(basePath, '.claude', 'pm', 'diagrams');
1390
+ const mmdPath = path.join(diagramsDir, name + '.mmd');
1391
+ const metaPath = path.join(diagramsDir, name + '.meta.json');
1392
+ try { fs.unlinkSync(mmdPath); } catch {}
1393
+ try { fs.unlinkSync(metaPath); } catch {}
1394
+ return json(res, 200, { ok: true });
1395
+ }
1396
+
1198
1397
  if (req.method === 'GET' && parsedUrl.pathname === '/api/tests') {
1199
1398
  return json(res, 200, getTestsData());
1200
1399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-autopm",
3
- "version": "3.22.1",
3
+ "version": "3.23.0",
4
4
  "description": "Autonomous Project Management Framework for Claude Code - Advanced AI-powered development automation",
5
5
  "main": "bin/autopm.js",
6
6
  "workspaces": [