@aion0/forge 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE_NOTES.md +4 -4
- package/app/api/issue-scanner/route.ts +2 -2
- package/app/api/pipelines/route.ts +14 -0
- package/app/api/project-pipelines/route.ts +68 -0
- package/components/CodeViewer.tsx +11 -1
- package/components/Dashboard.tsx +10 -12
- package/components/DocsViewer.tsx +11 -1
- package/components/PipelineEditor.tsx +3 -1
- package/components/PipelineView.tsx +262 -129
- package/components/ProjectDetail.tsx +186 -233
- package/components/SessionView.tsx +9 -1
- package/components/SkillsPanel.tsx +9 -1
- package/hooks/useSidebarResize.ts +52 -0
- package/lib/help-docs/05-pipelines.md +22 -7
- package/lib/help-docs/09-issue-autofix.md +1 -1
- package/lib/init.ts +7 -1
- package/lib/issue-scanner.ts +2 -2
- package/lib/pipeline-scheduler.ts +239 -0
- package/lib/pipeline.ts +43 -87
- package/package.json +1 -1
- package/src/core/db/database.ts +24 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, memo } from 'react';
|
|
4
|
+
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
4
5
|
|
|
5
6
|
// ─── Syntax highlighting ─────────────────────────────────
|
|
6
7
|
const KEYWORDS = new Set([
|
|
@@ -49,6 +50,7 @@ interface GitInfo {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export default memo(function ProjectDetail({ projectPath, projectName, hasGit }: { projectPath: string; projectName: string; hasGit: boolean }) {
|
|
53
|
+
const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 208, minWidth: 120, maxWidth: 400 });
|
|
52
54
|
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null);
|
|
53
55
|
const [loading, setLoading] = useState(false);
|
|
54
56
|
const [commitMsg, setCommitMsg] = useState('');
|
|
@@ -64,15 +66,13 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
64
66
|
const [diffFile, setDiffFile] = useState<string | null>(null);
|
|
65
67
|
const [projectSkills, setProjectSkills] = useState<{ name: string; displayName: string; type: string; scope: string; version: string; installedVersion: string; hasUpdate: boolean; source: 'registry' | 'local' }[]>([]);
|
|
66
68
|
const [showSkillsDetail, setShowSkillsDetail] = useState(false);
|
|
67
|
-
const [projectTab, setProjectTab] = useState<'code' | 'skills' | 'claudemd' | '
|
|
68
|
-
//
|
|
69
|
-
const [
|
|
70
|
-
const [
|
|
71
|
-
const [
|
|
72
|
-
const [
|
|
73
|
-
const [
|
|
74
|
-
const [issueLastScan, setIssueLastScan] = useState<string | null>(null);
|
|
75
|
-
const [retryModal, setRetryModal] = useState<{ issueNumber: number; context: string } | null>(null);
|
|
69
|
+
const [projectTab, setProjectTab] = useState<'code' | 'skills' | 'claudemd' | 'pipelines'>('code');
|
|
70
|
+
// Pipeline bindings state
|
|
71
|
+
const [pipelineBindings, setPipelineBindings] = useState<{ id: number; workflowName: string; enabled: boolean; config: any; lastRunAt: string | null; nextRunAt: string | null }[]>([]);
|
|
72
|
+
const [pipelineRuns, setPipelineRuns] = useState<{ id: string; workflowName: string; pipelineId: string; status: string; summary: string; createdAt: string }[]>([]);
|
|
73
|
+
const [availableWorkflows, setAvailableWorkflows] = useState<{ name: string; description?: string; builtin?: boolean }[]>([]);
|
|
74
|
+
const [showAddPipeline, setShowAddPipeline] = useState(false);
|
|
75
|
+
const [triggerInput, setTriggerInput] = useState<Record<string, string>>({});
|
|
76
76
|
const [claudeMdContent, setClaudeMdContent] = useState('');
|
|
77
77
|
const [claudeMdExists, setClaudeMdExists] = useState(false);
|
|
78
78
|
const [claudeTemplates, setClaudeTemplates] = useState<{ id: string; name: string; description: string; tags: string[]; builtin: boolean; content: string }[]>([]);
|
|
@@ -220,57 +220,28 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
220
220
|
fetchProjectSkills();
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
-
const
|
|
223
|
+
const fetchPipelineBindings = useCallback(async () => {
|
|
224
224
|
try {
|
|
225
|
-
const res = await fetch(`/api/
|
|
225
|
+
const res = await fetch(`/api/project-pipelines?project=${encodeURIComponent(projectPath)}`);
|
|
226
|
+
if (!res.ok) return;
|
|
226
227
|
const data = await res.json();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
setIssueNextScan(data.nextScan || null);
|
|
228
|
+
setPipelineBindings(data.bindings || []);
|
|
229
|
+
setPipelineRuns(data.runs || []);
|
|
230
|
+
setAvailableWorkflows(data.workflows || []);
|
|
231
231
|
} catch {}
|
|
232
232
|
}, [projectPath]);
|
|
233
233
|
|
|
234
|
-
const
|
|
235
|
-
await fetch('/api/issue-scanner', {
|
|
236
|
-
method: 'POST',
|
|
237
|
-
headers: { 'Content-Type': 'application/json' },
|
|
238
|
-
body: JSON.stringify({ action: 'save-config', projectPath, projectName, ...config }),
|
|
239
|
-
});
|
|
240
|
-
fetchIssueConfig();
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const scanNow = async () => {
|
|
244
|
-
setIssueScanning(true);
|
|
234
|
+
const triggerProjectPipeline = async (workflowName: string, input?: Record<string, string>) => {
|
|
245
235
|
try {
|
|
246
|
-
const res = await fetch('/api/
|
|
236
|
+
const res = await fetch('/api/project-pipelines', {
|
|
247
237
|
method: 'POST',
|
|
248
238
|
headers: { 'Content-Type': 'application/json' },
|
|
249
|
-
body: JSON.stringify({ action: '
|
|
239
|
+
body: JSON.stringify({ action: 'trigger', projectPath, projectName, workflowName, input }),
|
|
250
240
|
});
|
|
251
241
|
const data = await res.json();
|
|
252
|
-
if (data.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
alert(`Triggered ${data.triggered} issue fix(es): #${data.issues.join(', #')}`);
|
|
256
|
-
} else {
|
|
257
|
-
alert(`Scanned ${data.total} open issues — no new issues to process`);
|
|
258
|
-
}
|
|
259
|
-
await fetchIssueConfig();
|
|
260
|
-
} catch (e) {
|
|
261
|
-
alert('Scan failed');
|
|
262
|
-
}
|
|
263
|
-
setIssueScanning(false);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const triggerIssue = async (issueId: string) => {
|
|
267
|
-
await fetch('/api/issue-scanner', {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
headers: { 'Content-Type': 'application/json' },
|
|
270
|
-
body: JSON.stringify({ action: 'trigger', projectPath, issueId, projectName }),
|
|
271
|
-
});
|
|
272
|
-
setIssueManualId('');
|
|
273
|
-
fetchIssueConfig();
|
|
242
|
+
if (data.ok) { fetchPipelineBindings(); }
|
|
243
|
+
else { alert(data.error || 'Failed'); }
|
|
244
|
+
} catch { alert('Failed to trigger pipeline'); }
|
|
274
245
|
};
|
|
275
246
|
|
|
276
247
|
const fetchClaudeMd = useCallback(async () => {
|
|
@@ -409,9 +380,9 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
409
380
|
// Lazy load tab-specific data only when switching to that tab
|
|
410
381
|
useEffect(() => {
|
|
411
382
|
if (projectTab === 'skills') fetchProjectSkills();
|
|
412
|
-
if (projectTab === '
|
|
383
|
+
if (projectTab === 'pipelines') fetchPipelineBindings();
|
|
413
384
|
if (projectTab === 'claudemd') fetchClaudeMd();
|
|
414
|
-
}, [projectTab, fetchProjectSkills,
|
|
385
|
+
}, [projectTab, fetchProjectSkills, fetchPipelineBindings, fetchClaudeMd]);
|
|
415
386
|
|
|
416
387
|
return (
|
|
417
388
|
<>
|
|
@@ -481,13 +452,13 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
481
452
|
{claudeMdExists && <span className="ml-1 text-[8px] text-[var(--green)]">•</span>}
|
|
482
453
|
</button>
|
|
483
454
|
<button
|
|
484
|
-
onClick={() => setProjectTab('
|
|
455
|
+
onClick={() => setProjectTab('pipelines')}
|
|
485
456
|
className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
|
|
486
|
-
projectTab === '
|
|
457
|
+
projectTab === 'pipelines' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
|
487
458
|
}`}
|
|
488
459
|
>
|
|
489
|
-
|
|
490
|
-
{
|
|
460
|
+
Pipelines
|
|
461
|
+
{pipelineBindings.length > 0 && <span className="ml-1 text-[8px] text-[var(--text-secondary)]">({pipelineBindings.length})</span>}
|
|
491
462
|
</button>
|
|
492
463
|
</div>
|
|
493
464
|
</div>
|
|
@@ -521,12 +492,18 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
521
492
|
{/* Code content area */}
|
|
522
493
|
{projectTab === 'code' && <div className="flex-1 flex min-h-0 overflow-hidden">
|
|
523
494
|
{/* File tree */}
|
|
524
|
-
<div
|
|
495
|
+
<div style={{ width: sidebarWidth }} className="overflow-y-auto p-1 shrink-0">
|
|
525
496
|
{fileTree.map((node: any) => (
|
|
526
497
|
<FileTreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} />
|
|
527
498
|
))}
|
|
528
499
|
</div>
|
|
529
500
|
|
|
501
|
+
{/* Sidebar resize handle */}
|
|
502
|
+
<div
|
|
503
|
+
onMouseDown={onSidebarDragStart}
|
|
504
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
|
|
505
|
+
/>
|
|
506
|
+
|
|
530
507
|
{/* File content */}
|
|
531
508
|
<div className="flex-1 min-w-0 overflow-auto bg-[var(--bg-primary)]" style={{ width: 0 }}>
|
|
532
509
|
{/* Diff view */}
|
|
@@ -574,7 +551,7 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
574
551
|
{projectTab === 'skills' && (
|
|
575
552
|
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
576
553
|
{/* Left: skill/command tree */}
|
|
577
|
-
<div
|
|
554
|
+
<div style={{ width: sidebarWidth }} className="overflow-y-auto p-1 shrink-0">
|
|
578
555
|
{projectSkills.length === 0 ? (
|
|
579
556
|
<p className="text-[9px] text-[var(--text-secondary)] p-2">No skills or commands installed</p>
|
|
580
557
|
) : (
|
|
@@ -622,6 +599,12 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
622
599
|
)}
|
|
623
600
|
</div>
|
|
624
601
|
|
|
602
|
+
{/* Sidebar resize handle */}
|
|
603
|
+
<div
|
|
604
|
+
onMouseDown={onSidebarDragStart}
|
|
605
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
|
|
606
|
+
/>
|
|
607
|
+
|
|
625
608
|
{/* Right: file content / editor */}
|
|
626
609
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden bg-[var(--bg-primary)]">
|
|
627
610
|
{skillActivePath ? (
|
|
@@ -689,7 +672,7 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
689
672
|
{projectTab === 'claudemd' && (
|
|
690
673
|
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
691
674
|
{/* Left: templates list */}
|
|
692
|
-
<div
|
|
675
|
+
<div style={{ width: sidebarWidth }} className="overflow-y-auto shrink-0 flex flex-col">
|
|
693
676
|
<button
|
|
694
677
|
onClick={() => { setClaudeSelectedTemplate(null); setClaudeEditing(false); }}
|
|
695
678
|
className={`w-full px-2 py-1.5 border-b border-[var(--border)] text-[10px] text-left flex items-center gap-1 ${
|
|
@@ -734,6 +717,12 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
734
717
|
</div>
|
|
735
718
|
</div>
|
|
736
719
|
|
|
720
|
+
{/* Sidebar resize handle */}
|
|
721
|
+
<div
|
|
722
|
+
onMouseDown={onSidebarDragStart}
|
|
723
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
|
|
724
|
+
/>
|
|
725
|
+
|
|
737
726
|
{/* Right: CLAUDE.md content or template preview */}
|
|
738
727
|
<div className="flex-1 min-w-0 flex flex-col overflow-hidden bg-[var(--bg-primary)]">
|
|
739
728
|
{/* Header bar */}
|
|
@@ -797,149 +786,157 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
797
786
|
</div>
|
|
798
787
|
)}
|
|
799
788
|
|
|
800
|
-
{/*
|
|
801
|
-
{projectTab === '
|
|
789
|
+
{/* Pipelines tab */}
|
|
790
|
+
{projectTab === 'pipelines' && (
|
|
802
791
|
<div className="flex-1 overflow-auto p-4 space-y-4">
|
|
803
|
-
{/*
|
|
792
|
+
{/* Bound workflows */}
|
|
804
793
|
<div className="space-y-3">
|
|
805
|
-
<div className="flex items-center gap-
|
|
806
|
-
<
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
className="accent-[var(--accent)]"
|
|
812
|
-
/>
|
|
813
|
-
<span className="text-[11px] text-[var(--text-primary)] font-semibold">Enable Issue Auto-fix</span>
|
|
814
|
-
</label>
|
|
815
|
-
{issueConfig.enabled && (<>
|
|
816
|
-
<button
|
|
817
|
-
onClick={() => scanNow()}
|
|
818
|
-
disabled={issueScanning}
|
|
819
|
-
className="text-[9px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white disabled:opacity-50"
|
|
820
|
-
>
|
|
821
|
-
{issueScanning ? 'Scanning...' : 'Scan Now'}
|
|
822
|
-
</button>
|
|
823
|
-
{issueLastScan && (
|
|
824
|
-
<span className="text-[8px] text-[var(--text-secondary)]">
|
|
825
|
-
Last: {new Date(issueLastScan).toLocaleTimeString()}
|
|
826
|
-
</span>
|
|
827
|
-
)}
|
|
828
|
-
{issueNextScan && (
|
|
829
|
-
<span className="text-[8px] text-[var(--text-secondary)]">
|
|
830
|
-
Next: {new Date(issueNextScan).toLocaleTimeString()}
|
|
831
|
-
</span>
|
|
832
|
-
)}
|
|
833
|
-
</> )}
|
|
794
|
+
<div className="flex items-center gap-2">
|
|
795
|
+
<span className="text-xs font-semibold text-[var(--text-primary)]">Bound Pipelines</span>
|
|
796
|
+
<button
|
|
797
|
+
onClick={() => setShowAddPipeline(v => !v)}
|
|
798
|
+
className="text-[9px] px-2 py-0.5 bg-[var(--accent)] text-white rounded hover:opacity-90 ml-auto"
|
|
799
|
+
>+ Add</button>
|
|
834
800
|
</div>
|
|
835
801
|
|
|
836
|
-
{
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
<
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
className="
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
<
|
|
860
|
-
|
|
861
|
-
value={issueConfig.labels.join(', ')}
|
|
862
|
-
onChange={e => setIssueConfig({ ...issueConfig, labels: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
|
|
863
|
-
placeholder="bug, fix"
|
|
864
|
-
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
|
|
865
|
-
/>
|
|
866
|
-
</div>
|
|
802
|
+
{/* Add pipeline form */}
|
|
803
|
+
{showAddPipeline && (
|
|
804
|
+
<div className="border border-[var(--border)] rounded p-2 space-y-2">
|
|
805
|
+
{availableWorkflows.filter(w => !pipelineBindings.find(b => b.workflowName === w.name)).map(w => (
|
|
806
|
+
<button
|
|
807
|
+
key={w.name}
|
|
808
|
+
onClick={async () => {
|
|
809
|
+
await fetch('/api/project-pipelines', {
|
|
810
|
+
method: 'POST',
|
|
811
|
+
headers: { 'Content-Type': 'application/json' },
|
|
812
|
+
body: JSON.stringify({ action: 'add', projectPath, projectName, workflowName: w.name }),
|
|
813
|
+
});
|
|
814
|
+
setShowAddPipeline(false);
|
|
815
|
+
fetchPipelineBindings();
|
|
816
|
+
}}
|
|
817
|
+
className="w-full text-left px-2 py-1.5 rounded hover:bg-[var(--bg-tertiary)] text-[10px] flex items-center gap-2"
|
|
818
|
+
>
|
|
819
|
+
{w.builtin && <span className="text-[7px] text-[var(--text-secondary)]">⚙</span>}
|
|
820
|
+
<span className="text-[var(--text-primary)]">{w.name}</span>
|
|
821
|
+
{w.description && <span className="text-[var(--text-secondary)] truncate ml-auto text-[8px]">{w.description}</span>}
|
|
822
|
+
</button>
|
|
823
|
+
))}
|
|
824
|
+
{availableWorkflows.filter(w => !pipelineBindings.find(b => b.workflowName === w.name)).length === 0 && (
|
|
825
|
+
<p className="text-[9px] text-[var(--text-secondary)] p-2">All workflows already bound</p>
|
|
826
|
+
)}
|
|
867
827
|
</div>
|
|
868
828
|
)}
|
|
869
|
-
<div className="mt-3">
|
|
870
|
-
<button
|
|
871
|
-
onClick={() => saveIssueConfig(issueConfig)}
|
|
872
|
-
className="text-[10px] px-4 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
873
|
-
>Save Configuration</button>
|
|
874
|
-
</div>
|
|
875
|
-
</div>
|
|
876
829
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
830
|
+
{/* Bound pipeline list */}
|
|
831
|
+
{pipelineBindings.length === 0 ? (
|
|
832
|
+
<p className="text-[10px] text-[var(--text-secondary)]">No pipelines bound. Click + Add to attach a workflow.</p>
|
|
833
|
+
) : (
|
|
834
|
+
pipelineBindings.map(b => (
|
|
835
|
+
<div key={b.workflowName} className="border border-[var(--border)] rounded p-3 space-y-2">
|
|
836
|
+
<div className="flex items-center gap-2">
|
|
837
|
+
<span className="text-[11px] font-semibold text-[var(--text-primary)]">{b.workflowName}</span>
|
|
838
|
+
<label className="flex items-center gap-1 text-[9px] text-[var(--text-secondary)] cursor-pointer ml-auto">
|
|
839
|
+
<input type="checkbox" checked={b.enabled} onChange={async (e) => {
|
|
840
|
+
await fetch('/api/project-pipelines', {
|
|
841
|
+
method: 'POST',
|
|
842
|
+
headers: { 'Content-Type': 'application/json' },
|
|
843
|
+
body: JSON.stringify({ action: 'update', projectPath, workflowName: b.workflowName, enabled: e.target.checked }),
|
|
844
|
+
});
|
|
845
|
+
fetchPipelineBindings();
|
|
846
|
+
}} className="accent-[var(--accent)]" />
|
|
847
|
+
Enabled
|
|
848
|
+
</label>
|
|
849
|
+
<button
|
|
850
|
+
onClick={() => triggerProjectPipeline(b.workflowName, triggerInput)}
|
|
851
|
+
className="text-[9px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white"
|
|
852
|
+
>Run</button>
|
|
853
|
+
<button
|
|
854
|
+
onClick={async () => {
|
|
855
|
+
if (!confirm(`Remove "${b.workflowName}" from this project?`)) return;
|
|
856
|
+
await fetch('/api/project-pipelines', {
|
|
857
|
+
method: 'POST',
|
|
858
|
+
headers: { 'Content-Type': 'application/json' },
|
|
859
|
+
body: JSON.stringify({ action: 'remove', projectPath, workflowName: b.workflowName }),
|
|
860
|
+
});
|
|
861
|
+
fetchPipelineBindings();
|
|
862
|
+
}}
|
|
863
|
+
className="text-[9px] text-[var(--red)] hover:underline"
|
|
864
|
+
>Remove</button>
|
|
865
|
+
</div>
|
|
866
|
+
{/* Schedule config */}
|
|
867
|
+
<div className="flex items-center gap-2 text-[9px]">
|
|
868
|
+
<span className="text-[var(--text-secondary)]">Schedule:</span>
|
|
869
|
+
<select
|
|
870
|
+
value={b.config.interval || 0}
|
|
871
|
+
onChange={async (e) => {
|
|
872
|
+
const interval = Number(e.target.value);
|
|
873
|
+
const newConfig = { ...b.config, interval };
|
|
874
|
+
await fetch('/api/project-pipelines', {
|
|
875
|
+
method: 'POST',
|
|
876
|
+
headers: { 'Content-Type': 'application/json' },
|
|
877
|
+
body: JSON.stringify({ action: 'update', projectPath, workflowName: b.workflowName, config: newConfig }),
|
|
878
|
+
});
|
|
879
|
+
fetchPipelineBindings();
|
|
880
|
+
}}
|
|
881
|
+
className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded px-1.5 py-0.5 text-[9px] text-[var(--text-primary)]"
|
|
882
|
+
>
|
|
883
|
+
<option value={0}>Manual only</option>
|
|
884
|
+
<option value={15}>Every 15 min</option>
|
|
885
|
+
<option value={30}>Every 30 min</option>
|
|
886
|
+
<option value={60}>Every 1 hour</option>
|
|
887
|
+
<option value={120}>Every 2 hours</option>
|
|
888
|
+
<option value={360}>Every 6 hours</option>
|
|
889
|
+
<option value={720}>Every 12 hours</option>
|
|
890
|
+
<option value={1440}>Every 24 hours</option>
|
|
891
|
+
</select>
|
|
892
|
+
{b.config.interval > 0 && b.nextRunAt && (
|
|
893
|
+
<span className="text-[8px] text-[var(--text-secondary)]">
|
|
894
|
+
Next: {new Date(b.nextRunAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
|
895
|
+
</span>
|
|
896
|
+
)}
|
|
897
|
+
{b.lastRunAt && (
|
|
898
|
+
<span className="text-[8px] text-[var(--text-secondary)] ml-auto">
|
|
899
|
+
Last: {new Date(b.lastRunAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
|
900
|
+
</span>
|
|
901
|
+
)}
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
))
|
|
905
|
+
)}
|
|
894
906
|
</div>
|
|
895
907
|
|
|
896
|
-
{/*
|
|
897
|
-
{
|
|
908
|
+
{/* Execution history */}
|
|
909
|
+
{pipelineRuns.length > 0 && (
|
|
898
910
|
<div className="border-t border-[var(--border)] pt-3">
|
|
899
|
-
<div className="text-[9px] text-[var(--text-secondary)] uppercase mb-2">
|
|
911
|
+
<div className="text-[9px] text-[var(--text-secondary)] uppercase mb-2">Execution History</div>
|
|
900
912
|
<div className="border border-[var(--border)] rounded overflow-hidden">
|
|
901
|
-
{
|
|
902
|
-
<div key={
|
|
903
|
-
<
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
{p.pipelineId && (
|
|
912
|
-
<button
|
|
913
|
-
onClick={() => {
|
|
914
|
-
const event = new CustomEvent('forge:view-pipeline', { detail: { pipelineId: p.pipelineId } });
|
|
915
|
-
window.dispatchEvent(event);
|
|
916
|
-
}}
|
|
917
|
-
className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--accent)] font-mono"
|
|
918
|
-
title="View pipeline"
|
|
919
|
-
>{p.pipelineId.slice(0, 8)}</button>
|
|
920
|
-
)}
|
|
921
|
-
<span className="text-[var(--text-secondary)] text-[8px]">{p.createdAt}</span>
|
|
922
|
-
<div className="ml-auto flex gap-1">
|
|
923
|
-
{(p.status === 'failed' || p.status === 'done' || p.status === 'processing') && (
|
|
924
|
-
<button
|
|
925
|
-
onClick={() => setRetryModal({ issueNumber: p.issueNumber, context: '' })}
|
|
926
|
-
className="text-[8px] text-[var(--accent)] hover:underline"
|
|
927
|
-
>Retry</button>
|
|
928
|
-
)}
|
|
929
|
-
<button
|
|
930
|
-
onClick={async () => {
|
|
931
|
-
if (!confirm(`Delete record for issue #${p.issueNumber}?`)) return;
|
|
932
|
-
await fetch('/api/issue-scanner', {
|
|
933
|
-
method: 'POST',
|
|
934
|
-
headers: { 'Content-Type': 'application/json' },
|
|
935
|
-
body: JSON.stringify({ action: 'reset', projectPath, issueId: p.issueNumber }),
|
|
936
|
-
});
|
|
937
|
-
fetchIssueConfig();
|
|
938
|
-
}}
|
|
939
|
-
className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--red)]"
|
|
940
|
-
>Delete</button>
|
|
913
|
+
{pipelineRuns.map(run => (
|
|
914
|
+
<div key={run.id} className="flex items-start gap-2 px-3 py-2 border-b border-[var(--border)]/30 last:border-b-0 text-[10px]">
|
|
915
|
+
<span className={`shrink-0 ${
|
|
916
|
+
run.status === 'done' ? 'text-green-400' : run.status === 'failed' ? 'text-red-400' : 'text-yellow-400'
|
|
917
|
+
}`}>●</span>
|
|
918
|
+
<div className="flex-1 min-w-0">
|
|
919
|
+
<div className="flex items-center gap-2">
|
|
920
|
+
<span className="text-[var(--text-primary)] font-medium">{run.workflowName}</span>
|
|
921
|
+
<span className="text-[8px] text-[var(--text-secondary)] font-mono">{run.pipelineId.slice(0, 8)}</span>
|
|
922
|
+
<span className="text-[8px] text-[var(--text-secondary)] ml-auto">{new Date(run.createdAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
|
|
941
923
|
</div>
|
|
924
|
+
{run.summary && (
|
|
925
|
+
<pre className="text-[9px] text-[var(--text-secondary)] mt-1 whitespace-pre-wrap break-words line-clamp-3">{run.summary}</pre>
|
|
926
|
+
)}
|
|
942
927
|
</div>
|
|
928
|
+
<button
|
|
929
|
+
onClick={async () => {
|
|
930
|
+
if (!confirm('Delete this run?')) return;
|
|
931
|
+
await fetch('/api/project-pipelines', {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: { 'Content-Type': 'application/json' },
|
|
934
|
+
body: JSON.stringify({ action: 'delete-run', id: run.id }),
|
|
935
|
+
});
|
|
936
|
+
fetchPipelineBindings();
|
|
937
|
+
}}
|
|
938
|
+
className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--red)] shrink-0"
|
|
939
|
+
>×</button>
|
|
943
940
|
</div>
|
|
944
941
|
))}
|
|
945
942
|
</div>
|
|
@@ -1026,50 +1023,6 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
1026
1023
|
</div>
|
|
1027
1024
|
)}
|
|
1028
1025
|
|
|
1029
|
-
{/* Retry modal */}
|
|
1030
|
-
{retryModal && (
|
|
1031
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setRetryModal(null)}>
|
|
1032
|
-
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl w-[420px] max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
1033
|
-
<div className="px-4 py-3 border-b border-[var(--border)]">
|
|
1034
|
-
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Retry Issue #{retryModal.issueNumber}</h3>
|
|
1035
|
-
<p className="text-[10px] text-[var(--text-secondary)] mt-0.5">Add context to help the AI fix the issue better this time.</p>
|
|
1036
|
-
</div>
|
|
1037
|
-
<div className="p-4">
|
|
1038
|
-
<textarea
|
|
1039
|
-
value={retryModal.context}
|
|
1040
|
-
onChange={e => setRetryModal({ ...retryModal, context: e.target.value })}
|
|
1041
|
-
placeholder="e.g. The previous fix caused a merge conflict. Rebase from main first, then fix only the validation logic in src/utils.ts..."
|
|
1042
|
-
className="w-full h-32 px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[11px] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]/50 resize-none focus:outline-none focus:border-[var(--accent)]"
|
|
1043
|
-
autoFocus
|
|
1044
|
-
/>
|
|
1045
|
-
</div>
|
|
1046
|
-
<div className="px-4 py-3 border-t border-[var(--border)] flex justify-end gap-2">
|
|
1047
|
-
<button
|
|
1048
|
-
onClick={() => setRetryModal(null)}
|
|
1049
|
-
className="text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
1050
|
-
>Cancel</button>
|
|
1051
|
-
<button
|
|
1052
|
-
onClick={async () => {
|
|
1053
|
-
await fetch('/api/issue-scanner', {
|
|
1054
|
-
method: 'POST',
|
|
1055
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1056
|
-
body: JSON.stringify({
|
|
1057
|
-
action: 'retry',
|
|
1058
|
-
projectPath,
|
|
1059
|
-
projectName,
|
|
1060
|
-
issueId: retryModal.issueNumber,
|
|
1061
|
-
context: retryModal.context,
|
|
1062
|
-
}),
|
|
1063
|
-
});
|
|
1064
|
-
setRetryModal(null);
|
|
1065
|
-
fetchIssueConfig();
|
|
1066
|
-
}}
|
|
1067
|
-
className="text-[11px] px-4 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
1068
|
-
>Retry</button>
|
|
1069
|
-
</div>
|
|
1070
|
-
</div>
|
|
1071
|
-
</div>
|
|
1072
|
-
)}
|
|
1073
1026
|
</>
|
|
1074
1027
|
);
|
|
1075
1028
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
4
5
|
import MarkdownContent from './MarkdownContent';
|
|
5
6
|
|
|
6
7
|
interface SessionEntry {
|
|
@@ -42,6 +43,7 @@ export default function SessionView({
|
|
|
42
43
|
projects: { name: string; path: string; language: string | null }[];
|
|
43
44
|
onOpenInTerminal?: (sessionId: string, projectPath: string) => void;
|
|
44
45
|
}) {
|
|
46
|
+
const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 288, minWidth: 160, maxWidth: 480 });
|
|
45
47
|
// Tree data: project → sessions
|
|
46
48
|
const [sessionTree, setSessionTree] = useState<Record<string, ClaudeSessionInfo[]>>({});
|
|
47
49
|
const [expandedProjects, setExpandedProjects] = useState<Set<string>>(new Set());
|
|
@@ -285,7 +287,7 @@ export default function SessionView({
|
|
|
285
287
|
return (
|
|
286
288
|
<div className="flex h-full">
|
|
287
289
|
{/* Left: tree view */}
|
|
288
|
-
<div
|
|
290
|
+
<div style={{ width: sidebarWidth }} className="flex flex-col shrink-0 overflow-hidden">
|
|
289
291
|
{/* Header */}
|
|
290
292
|
<div className="flex items-center justify-between p-2 border-b border-[var(--border)]">
|
|
291
293
|
<span className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase">Sessions</span>
|
|
@@ -451,6 +453,12 @@ export default function SessionView({
|
|
|
451
453
|
</div>
|
|
452
454
|
</div>
|
|
453
455
|
|
|
456
|
+
{/* Sidebar resize handle */}
|
|
457
|
+
<div
|
|
458
|
+
onMouseDown={onSidebarDragStart}
|
|
459
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
|
|
460
|
+
/>
|
|
461
|
+
|
|
454
462
|
{/* Right: session content */}
|
|
455
463
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
456
464
|
{activeSession && (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
4
5
|
|
|
5
6
|
type ItemType = 'skill' | 'command';
|
|
6
7
|
|
|
@@ -28,6 +29,7 @@ interface ProjectInfo {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export default function SkillsPanel({ projectFilter }: { projectFilter?: string }) {
|
|
32
|
+
const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 224, minWidth: 140, maxWidth: 400 });
|
|
31
33
|
const [skills, setSkills] = useState<Skill[]>([]);
|
|
32
34
|
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
33
35
|
const [syncing, setSyncing] = useState(false);
|
|
@@ -282,7 +284,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
282
284
|
) : (
|
|
283
285
|
<div className="flex-1 flex min-h-0">
|
|
284
286
|
{/* Left: skill list */}
|
|
285
|
-
<div
|
|
287
|
+
<div style={{ width: sidebarWidth }} className="overflow-y-auto shrink-0">
|
|
286
288
|
{/* Registry items */}
|
|
287
289
|
{filtered.map(skill => {
|
|
288
290
|
const isInstalled = skill.installedGlobal || skill.installedProjects.length > 0;
|
|
@@ -401,6 +403,12 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
401
403
|
)}
|
|
402
404
|
</div>
|
|
403
405
|
|
|
406
|
+
{/* Sidebar resize handle */}
|
|
407
|
+
<div
|
|
408
|
+
onMouseDown={onSidebarDragStart}
|
|
409
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
|
|
410
|
+
/>
|
|
411
|
+
|
|
404
412
|
{/* Right: detail panel */}
|
|
405
413
|
<div className="flex-1 flex flex-col min-w-0">
|
|
406
414
|
{expandedSkill ? (() => {
|