@aion0/forge 0.3.3 → 0.3.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/CLAUDE.md +2 -1
- package/app/api/favorites/route.ts +26 -0
- package/app/api/git/route.ts +40 -35
- package/app/api/issue-scanner/route.ts +116 -0
- package/app/api/pipelines/route.ts +11 -3
- package/app/api/skills/route.ts +12 -2
- package/app/api/tabs/route.ts +25 -0
- package/bin/forge-server.mjs +81 -22
- package/cli/mw.ts +2 -1
- package/components/DocTerminal.tsx +3 -2
- package/components/DocsViewer.tsx +160 -3
- package/components/PipelineView.tsx +3 -2
- package/components/ProjectDetail.tsx +1115 -0
- package/components/ProjectManager.tsx +191 -836
- package/components/SkillsPanel.tsx +28 -6
- package/components/TabBar.tsx +46 -0
- package/components/WebTerminal.tsx +4 -3
- package/lib/cloudflared.ts +1 -1
- package/lib/init.ts +6 -0
- package/lib/issue-scanner.ts +298 -0
- package/lib/pipeline.ts +296 -28
- package/lib/settings.ts +2 -0
- package/lib/skills.ts +30 -2
- package/lib/task-manager.ts +39 -0
- package/lib/terminal-standalone.ts +5 -1
- package/next.config.ts +3 -1
- package/package.json +1 -1
- package/src/core/db/database.ts +13 -0
- package/src/types/index.ts +1 -1
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
|
|
4
4
|
import MarkdownContent from './MarkdownContent';
|
|
5
|
+
import TabBar from './TabBar';
|
|
5
6
|
|
|
6
7
|
const DocTerminal = lazy(() => import('./DocTerminal'));
|
|
7
8
|
|
|
9
|
+
interface DocTab {
|
|
10
|
+
id: number;
|
|
11
|
+
filePath: string;
|
|
12
|
+
fileName: string;
|
|
13
|
+
rootIdx: number;
|
|
14
|
+
content: string | null;
|
|
15
|
+
isImage: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function genTabId(): number { return Date.now() + Math.floor(Math.random() * 10000); }
|
|
19
|
+
|
|
8
20
|
interface FileNode {
|
|
9
21
|
name: string;
|
|
10
22
|
path: string;
|
|
@@ -89,6 +101,141 @@ export default function DocsViewer() {
|
|
|
89
101
|
const [saving, setSaving] = useState(false);
|
|
90
102
|
const dragRef = useRef<{ startY: number; startH: number } | null>(null);
|
|
91
103
|
|
|
104
|
+
// Doc tabs
|
|
105
|
+
const [docTabs, setDocTabs] = useState<DocTab[]>([]);
|
|
106
|
+
const [activeDocTabId, setActiveDocTabId] = useState(0);
|
|
107
|
+
const saveTimerRef = useRef<any>(null);
|
|
108
|
+
|
|
109
|
+
// Load tabs from DB on mount
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
fetch('/api/tabs?type=docs').then(r => r.json())
|
|
112
|
+
.then(data => {
|
|
113
|
+
if (Array.isArray(data.tabs) && data.tabs.length > 0) {
|
|
114
|
+
setDocTabs(data.tabs);
|
|
115
|
+
setActiveDocTabId(data.activeTabId || data.tabs[0].id);
|
|
116
|
+
// Set selectedFile to active tab's file
|
|
117
|
+
const activeId = data.activeTabId || data.tabs[0].id;
|
|
118
|
+
const active = data.tabs.find((t: any) => t.id === activeId);
|
|
119
|
+
if (active) {
|
|
120
|
+
setSelectedFile(active.filePath);
|
|
121
|
+
// Content not stored in DB, fetch it
|
|
122
|
+
if (!active.isImage) {
|
|
123
|
+
fetch(`/api/docs?root=${active.rootIdx}&file=${encodeURIComponent(active.filePath)}`)
|
|
124
|
+
.then(r => r.json())
|
|
125
|
+
.then(d => { setContent(d.content || null); })
|
|
126
|
+
.catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}).catch(() => {});
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
// Persist tabs (debounced)
|
|
134
|
+
const persistDocTabs = useCallback((tabs: DocTab[], activeId: number) => {
|
|
135
|
+
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
136
|
+
saveTimerRef.current = setTimeout(() => {
|
|
137
|
+
fetch('/api/tabs?type=docs', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
tabs: tabs.map(t => ({ id: t.id, filePath: t.filePath, fileName: t.fileName, rootIdx: t.rootIdx, isImage: t.isImage })),
|
|
142
|
+
activeTabId: activeId,
|
|
143
|
+
}),
|
|
144
|
+
}).catch(() => {});
|
|
145
|
+
}, 500);
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
// Open file in tab
|
|
149
|
+
const openFileInTab = useCallback(async (path: string) => {
|
|
150
|
+
setSelectedFile(path);
|
|
151
|
+
setEditing(false);
|
|
152
|
+
setFileWarning(null);
|
|
153
|
+
|
|
154
|
+
const isImg = isImageFile(path);
|
|
155
|
+
const fileName = path.split('/').pop() || path;
|
|
156
|
+
|
|
157
|
+
// Check if tab already exists (use functional update to get latest state)
|
|
158
|
+
let found = false;
|
|
159
|
+
setDocTabs(prev => {
|
|
160
|
+
const existing = prev.find(t => t.filePath === path);
|
|
161
|
+
if (existing) {
|
|
162
|
+
found = true;
|
|
163
|
+
setActiveDocTabId(existing.id);
|
|
164
|
+
setContent(existing.content);
|
|
165
|
+
persistDocTabs(prev, existing.id);
|
|
166
|
+
}
|
|
167
|
+
return prev;
|
|
168
|
+
});
|
|
169
|
+
if (found) return;
|
|
170
|
+
|
|
171
|
+
// Fetch content
|
|
172
|
+
let fileContent: string | null = null;
|
|
173
|
+
if (!isImg) {
|
|
174
|
+
setLoading(true);
|
|
175
|
+
const res = await fetch(`/api/docs?root=${activeRoot}&file=${encodeURIComponent(path)}`);
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
if (data.tooLarge) {
|
|
178
|
+
setFileWarning(`File too large (${data.sizeLabel})`);
|
|
179
|
+
} else {
|
|
180
|
+
fileContent = data.content || null;
|
|
181
|
+
}
|
|
182
|
+
setLoading(false);
|
|
183
|
+
}
|
|
184
|
+
setContent(fileContent);
|
|
185
|
+
|
|
186
|
+
const newTab: DocTab = { id: genTabId(), filePath: path, fileName, rootIdx: activeRoot, isImage: isImg, content: fileContent };
|
|
187
|
+
setDocTabs(prev => {
|
|
188
|
+
// Double-check no duplicate
|
|
189
|
+
if (prev.find(t => t.filePath === path)) return prev;
|
|
190
|
+
const updated = [...prev, newTab];
|
|
191
|
+
setActiveDocTabId(newTab.id);
|
|
192
|
+
persistDocTabs(updated, newTab.id);
|
|
193
|
+
return updated;
|
|
194
|
+
});
|
|
195
|
+
}, [activeRoot, persistDocTabs]);
|
|
196
|
+
|
|
197
|
+
const closeDocTab = useCallback((tabId: number) => {
|
|
198
|
+
setDocTabs(prev => {
|
|
199
|
+
const updated = prev.filter(t => t.id !== tabId);
|
|
200
|
+
let newActiveId = activeDocTabId;
|
|
201
|
+
if (tabId === activeDocTabId) {
|
|
202
|
+
const idx = prev.findIndex(t => t.id === tabId);
|
|
203
|
+
const next = updated[Math.min(idx, updated.length - 1)];
|
|
204
|
+
newActiveId = next?.id || 0;
|
|
205
|
+
if (next) { setSelectedFile(next.filePath); setContent(next.content); }
|
|
206
|
+
else { setSelectedFile(null); setContent(null); }
|
|
207
|
+
}
|
|
208
|
+
setActiveDocTabId(newActiveId);
|
|
209
|
+
persistDocTabs(updated, newActiveId);
|
|
210
|
+
return updated;
|
|
211
|
+
});
|
|
212
|
+
}, [activeDocTabId, persistDocTabs]);
|
|
213
|
+
|
|
214
|
+
const activateDocTab = useCallback(async (tabId: number) => {
|
|
215
|
+
const tab = docTabs.find(t => t.id === tabId);
|
|
216
|
+
if (tab) {
|
|
217
|
+
setActiveDocTabId(tabId);
|
|
218
|
+
setSelectedFile(tab.filePath);
|
|
219
|
+
setEditing(false);
|
|
220
|
+
if (tab.rootIdx !== activeRoot) setActiveRoot(tab.rootIdx);
|
|
221
|
+
persistDocTabs(docTabs, tabId);
|
|
222
|
+
|
|
223
|
+
// Use cached content or re-fetch
|
|
224
|
+
if (tab.content) {
|
|
225
|
+
setContent(tab.content);
|
|
226
|
+
} else if (!tab.isImage) {
|
|
227
|
+
setLoading(true);
|
|
228
|
+
const res = await fetch(`/api/docs?root=${tab.rootIdx}&file=${encodeURIComponent(tab.filePath)}`);
|
|
229
|
+
const data = await res.json();
|
|
230
|
+
const fetched = data.content || null;
|
|
231
|
+
setContent(fetched);
|
|
232
|
+
// Cache in tab
|
|
233
|
+
setDocTabs(prev => prev.map(t => t.id === tabId ? { ...t, content: fetched } : t));
|
|
234
|
+
setLoading(false);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, [docTabs, activeRoot, persistDocTabs]);
|
|
238
|
+
|
|
92
239
|
// Fetch tree
|
|
93
240
|
const fetchTree = useCallback(async (rootIdx: number) => {
|
|
94
241
|
const res = await fetch(`/api/docs?root=${rootIdx}`);
|
|
@@ -223,7 +370,7 @@ export default function DocsViewer() {
|
|
|
223
370
|
filtered.map(f => (
|
|
224
371
|
<button
|
|
225
372
|
key={f.path}
|
|
226
|
-
onClick={() => {
|
|
373
|
+
onClick={() => { openFileInTab(f.path); setSearch(''); }}
|
|
227
374
|
className={`w-full text-left px-2 py-1 rounded text-xs truncate ${
|
|
228
375
|
selectedFile === f.path ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
|
|
229
376
|
}`}
|
|
@@ -236,15 +383,25 @@ export default function DocsViewer() {
|
|
|
236
383
|
)
|
|
237
384
|
) : (
|
|
238
385
|
tree.map(node => (
|
|
239
|
-
<TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={
|
|
386
|
+
<TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFileInTab} />
|
|
240
387
|
))
|
|
241
388
|
)}
|
|
242
389
|
</div>
|
|
243
390
|
</aside>
|
|
244
391
|
)}
|
|
245
392
|
|
|
246
|
-
{/* Main content
|
|
393
|
+
{/* Main content */}
|
|
247
394
|
<main className="flex-1 flex flex-col min-w-0">
|
|
395
|
+
{/* Doc tab bar */}
|
|
396
|
+
{docTabs.length > 0 && (
|
|
397
|
+
<TabBar
|
|
398
|
+
tabs={docTabs.map(t => ({ id: t.id, label: t.fileName.replace(/\.md$/, '') }))}
|
|
399
|
+
activeId={activeDocTabId}
|
|
400
|
+
onActivate={activateDocTab}
|
|
401
|
+
onClose={closeDocTab}
|
|
402
|
+
/>
|
|
403
|
+
)}
|
|
404
|
+
|
|
248
405
|
{/* Top bar */}
|
|
249
406
|
<div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0 flex items-center gap-2">
|
|
250
407
|
<button
|
|
@@ -17,6 +17,7 @@ interface WorkflowNode {
|
|
|
17
17
|
interface Workflow {
|
|
18
18
|
name: string;
|
|
19
19
|
description?: string;
|
|
20
|
+
builtin?: boolean;
|
|
20
21
|
vars: Record<string, string>;
|
|
21
22
|
input: Record<string, string>;
|
|
22
23
|
nodes: Record<string, WorkflowNode>;
|
|
@@ -168,7 +169,7 @@ export default function PipelineView({ onViewTask }: { onViewTask?: (taskId: str
|
|
|
168
169
|
>
|
|
169
170
|
<option value="">Editor ▾</option>
|
|
170
171
|
<option value="">+ New workflow</option>
|
|
171
|
-
{workflows.map(w => <option key={w.name} value={w.name}>{w.name}</option>)}
|
|
172
|
+
{workflows.map(w => <option key={w.name} value={w.name}>{w.builtin ? '⚙ ' : ''}{w.name}</option>)}
|
|
172
173
|
</select>
|
|
173
174
|
<button
|
|
174
175
|
onClick={() => setShowCreate(v => !v)}
|
|
@@ -188,7 +189,7 @@ export default function PipelineView({ onViewTask }: { onViewTask?: (taskId: str
|
|
|
188
189
|
>
|
|
189
190
|
<option value="">Select workflow...</option>
|
|
190
191
|
{workflows.map(w => (
|
|
191
|
-
<option key={w.name} value={w.name}>{w.name}{w.description ? ` — ${w.description}` : ''}</option>
|
|
192
|
+
<option key={w.name} value={w.name}>{w.builtin ? '[Built-in] ' : ''}{w.name}{w.description ? ` — ${w.description}` : ''}</option>
|
|
192
193
|
))}
|
|
193
194
|
</select>
|
|
194
195
|
|