@aion0/forge 0.5.31 → 0.5.32

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 CHANGED
@@ -1,8 +1,8 @@
1
- # Forge v0.5.31
1
+ # Forge v0.5.32
2
2
 
3
3
  Released: 2026-04-10
4
4
 
5
- ## Changes since v0.5.30
5
+ ## Changes since v0.5.31
6
6
 
7
7
 
8
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.30...v0.5.31
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.31...v0.5.32
@@ -28,14 +28,19 @@ interface FileNode {
28
28
 
29
29
  // ─── File Tree ───────────────────────────────────────────
30
30
 
31
- function TreeNode({ node, depth, selected, onSelect }: {
31
+ function TreeNode({ node, depth, selected, onSelect, collapseVersion = 0 }: {
32
32
  node: FileNode;
33
33
  depth: number;
34
34
  selected: string | null;
35
35
  onSelect: (path: string) => void;
36
+ collapseVersion?: number;
36
37
  }) {
37
38
  const [expanded, setExpanded] = useState(depth < 1);
38
39
 
40
+ useEffect(() => {
41
+ if (collapseVersion > 0) setExpanded(false);
42
+ }, [collapseVersion]);
43
+
39
44
  if (node.type === 'dir') {
40
45
  return (
41
46
  <div>
@@ -48,7 +53,7 @@ function TreeNode({ node, depth, selected, onSelect }: {
48
53
  <span className="text-[var(--text-primary)]">{node.name}</span>
49
54
  </button>
50
55
  {expanded && node.children?.map(child => (
51
- <TreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} />
56
+ <TreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} collapseVersion={collapseVersion} />
52
57
  ))}
53
58
  </div>
54
59
  );
@@ -107,6 +112,7 @@ export default function DocsViewer() {
107
112
  const [content, setContent] = useState<string | null>(null);
108
113
  const [loading, setLoading] = useState(false);
109
114
  const [search, setSearch] = useState('');
115
+ const [treeCollapseVersion, setTreeCollapseVersion] = useState(0);
110
116
  const [terminalHeight, setTerminalHeight] = useState(250);
111
117
  const [docsAgent, setDocsAgent] = useState('');
112
118
  const [sidebarOpen, setSidebarOpen] = useState(true);
@@ -387,14 +393,19 @@ export default function DocsViewer() {
387
393
  </div>
388
394
 
389
395
  {/* Search */}
390
- <div className="p-2 border-b border-[var(--border)]">
396
+ <div className="p-2 border-b border-[var(--border)] flex items-center gap-2">
391
397
  <input
392
398
  type="text"
393
399
  placeholder="Search..."
394
400
  value={search}
395
401
  onChange={e => setSearch(e.target.value)}
396
- className="w-full text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
402
+ className="flex-1 text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
397
403
  />
404
+ <button
405
+ onClick={() => setTreeCollapseVersion(v => v + 1)}
406
+ className="text-[10px] px-1.5 py-1 rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] shrink-0"
407
+ title="Collapse all folders"
408
+ >⇱</button>
398
409
  </div>
399
410
 
400
411
  {/* Tree / search results */}
@@ -419,7 +430,7 @@ export default function DocsViewer() {
419
430
  )
420
431
  ) : (
421
432
  (hideUnsupported ? filterTree(tree) : tree).map(node => (
422
- <TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFileInTab} />
433
+ <TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFileInTab} collapseVersion={treeCollapseVersion} />
423
434
  ))
424
435
  )}
425
436
  </div>
@@ -67,6 +67,7 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
67
67
  const [gitLoading, setGitLoading] = useState(false);
68
68
  const [gitResult, setGitResult] = useState<{ ok?: boolean; error?: string } | null>(null);
69
69
  const [fileTree, setFileTree] = useState<any[]>([]);
70
+ const [treeCollapseVersion, setTreeCollapseVersion] = useState(0);
70
71
  const [selectedFile, setSelectedFile] = useState<string | null>(null);
71
72
  const [fileContent, setFileContent] = useState<string | null>(null);
72
73
  const [fileImageUrl, setFileImageUrl] = useState<string | null>(null);
@@ -713,10 +714,19 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
713
714
  </div>
714
715
  )}
715
716
  {codeSearching && <div className="px-2 py-1 text-[9px] text-[var(--text-secondary)]">Searching...</div>}
717
+ {/* File tree header */}
718
+ <div className="px-2 py-0.5 border-b border-[var(--border)] flex items-center justify-between">
719
+ <span className="text-[8px] text-[var(--text-secondary)] uppercase">Files</span>
720
+ <button
721
+ onClick={() => setTreeCollapseVersion(v => v + 1)}
722
+ className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
723
+ title="Collapse all folders"
724
+ >⇱ collapse all</button>
725
+ </div>
716
726
  {/* File tree */}
717
727
  <div className="overflow-y-auto flex-1 p-1">
718
728
  {fileTree.map((node: any) => (
719
- <FileTreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} />
729
+ <FileTreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} collapseVersion={treeCollapseVersion} />
720
730
  ))}
721
731
  </div>
722
732
  </div>
@@ -1468,14 +1478,20 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
1468
1478
  });
1469
1479
 
1470
1480
  // Simple file tree node
1471
- const FileTreeNode = memo(function FileTreeNode({ node, depth, selected, onSelect }: {
1481
+ const FileTreeNode = memo(function FileTreeNode({ node, depth, selected, onSelect, collapseVersion }: {
1472
1482
  node: { name: string; path: string; type: string; children?: any[] };
1473
1483
  depth: number;
1474
1484
  selected: string | null;
1475
1485
  onSelect: (path: string) => void;
1486
+ collapseVersion: number;
1476
1487
  }) {
1477
1488
  const [expanded, setExpanded] = useState(depth < 1);
1478
1489
 
1490
+ // When parent bumps collapseVersion, collapse this node
1491
+ useEffect(() => {
1492
+ if (collapseVersion > 0) setExpanded(false);
1493
+ }, [collapseVersion]);
1494
+
1479
1495
  if (node.type === 'dir') {
1480
1496
  return (
1481
1497
  <div>
@@ -1488,7 +1504,7 @@ const FileTreeNode = memo(function FileTreeNode({ node, depth, selected, onSelec
1488
1504
  <span className="text-[var(--text-primary)]">{node.name}</span>
1489
1505
  </button>
1490
1506
  {expanded && node.children?.map((child: any) => (
1491
- <FileTreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} />
1507
+ <FileTreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} collapseVersion={collapseVersion} />
1492
1508
  ))}
1493
1509
  </div>
1494
1510
  );
@@ -319,6 +319,13 @@ export default function SessionView({
319
319
  <div className="flex items-center justify-between p-2 border-b border-[var(--border)]">
320
320
  <span className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase">Sessions</span>
321
321
  <div className="flex items-center gap-2">
322
+ {!singleProject && (
323
+ <button
324
+ onClick={() => setExpandedProjects(new Set())}
325
+ className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
326
+ title="Collapse all projects"
327
+ >⇱</button>
328
+ )}
322
329
  <button
323
330
  onClick={() => batchMode ? exitBatchMode() : setBatchMode(true)}
324
331
  className={`text-[9px] transition-colors ${batchMode ? 'text-[var(--accent)]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
@@ -64,23 +64,29 @@ function buildTree(files: { name: string; path: string; type: string }[]): TreeN
64
64
  return root;
65
65
  }
66
66
 
67
- function SkillFileTree({ files, activeFile, onSelect }: {
67
+ function SkillFileTree({ files, activeFile, onSelect, collapseVersion = 0 }: {
68
68
  files: { name: string; path: string; type: string }[];
69
69
  activeFile: string | null;
70
70
  onSelect: (path: string) => void;
71
+ collapseVersion?: number;
71
72
  }) {
72
73
  const tree = buildTree(files);
73
- return <TreeNodeList nodes={tree} depth={0} activeFile={activeFile} onSelect={onSelect} />;
74
+ return <TreeNodeList nodes={tree} depth={0} activeFile={activeFile} onSelect={onSelect} collapseVersion={collapseVersion} />;
74
75
  }
75
76
 
76
- function TreeNodeList({ nodes, depth, activeFile, onSelect }: {
77
- nodes: TreeNode[]; depth: number; activeFile: string | null; onSelect: (path: string) => void;
77
+ function TreeNodeList({ nodes, depth, activeFile, onSelect, collapseVersion = 0 }: {
78
+ nodes: TreeNode[]; depth: number; activeFile: string | null; onSelect: (path: string) => void; collapseVersion?: number;
78
79
  }) {
79
80
  const [expanded, setExpanded] = useState<Set<string>>(new Set(
80
81
  // Auto-expand first level
81
82
  nodes.filter(n => n.type === 'dir').map(n => n.path)
82
83
  ));
83
84
 
85
+ // Collapse all on version bump
86
+ useEffect(() => {
87
+ if (collapseVersion > 0) setExpanded(new Set());
88
+ }, [collapseVersion]);
89
+
84
90
  const toggle = (path: string) => {
85
91
  setExpanded(prev => {
86
92
  const next = new Set(prev);
@@ -103,7 +109,7 @@ function TreeNodeList({ nodes, depth, activeFile, onSelect }: {
103
109
  <span>📁 {node.name}</span>
104
110
  </button>
105
111
  {expanded.has(node.path) && (
106
- <TreeNodeList nodes={node.children} depth={depth + 1} activeFile={activeFile} onSelect={onSelect} />
112
+ <TreeNodeList nodes={node.children} depth={depth + 1} activeFile={activeFile} onSelect={onSelect} collapseVersion={collapseVersion} />
107
113
  )}
108
114
  </div>
109
115
  ) : (
@@ -152,6 +158,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
152
158
  const [expandedSkill, setExpandedSkill] = useState<string | null>(null);
153
159
  const [skillFiles, setSkillFiles] = useState<{ name: string; path: string; type: string }[]>([]);
154
160
  const [activeFile, setActiveFile] = useState<string | null>(null);
161
+ const [skillTreeCollapseVersion, setSkillTreeCollapseVersion] = useState(0);
155
162
  const [fileContent, setFileContent] = useState<string>('');
156
163
 
157
164
  const fetchSkills = useCallback(async () => {
@@ -742,7 +749,18 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
742
749
  {/* File browser */}
743
750
  <div className="flex-1 flex min-h-0 overflow-hidden">
744
751
  {/* File tree */}
745
- <div className="w-36 border-r border-[var(--border)] overflow-y-auto shrink-0">
752
+ <div className="w-36 border-r border-[var(--border)] flex flex-col shrink-0">
753
+ {skillFiles.length > 0 && (
754
+ <div className="px-2 py-0.5 border-b border-[var(--border)] flex items-center justify-between shrink-0">
755
+ <span className="text-[8px] text-[var(--text-secondary)] uppercase">Files</span>
756
+ <button
757
+ onClick={() => setSkillTreeCollapseVersion(v => v + 1)}
758
+ className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
759
+ title="Collapse all"
760
+ >⇱</button>
761
+ </div>
762
+ )}
763
+ <div className="overflow-y-auto flex-1">
746
764
  {skillFiles.length === 0 ? (
747
765
  <div className="p-2 text-[9px] text-[var(--text-secondary)]">Loading...</div>
748
766
  ) : (
@@ -750,8 +768,10 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
750
768
  files={skillFiles}
751
769
  activeFile={activeFile}
752
770
  onSelect={(path) => loadFile(itemName, path, isLocal, localItem?.type, localItem?.projectPath)}
771
+ collapseVersion={skillTreeCollapseVersion}
753
772
  />
754
773
  )}
774
+ </div>
755
775
  {skill?.sourceUrl && (
756
776
  <div className="border-t border-[var(--border)] p-2">
757
777
  <a
@@ -126,8 +126,13 @@ export default function WorkspaceTree({
126
126
  {/* Header */}
127
127
  <div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--border)] shrink-0">
128
128
  <span className="text-xs font-bold text-[var(--text-primary)]">Workspace</span>
129
+ <button onClick={() => setExpanded(new Set())}
130
+ className="text-[8px] px-1.5 py-0.5 rounded border border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] ml-auto"
131
+ title="Collapse all">
132
+
133
+ </button>
129
134
  <button onClick={onCreateWorkspace}
130
- className="text-[8px] px-1.5 py-0.5 rounded border border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] ml-auto">
135
+ className="text-[8px] px-1.5 py-0.5 rounded border border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">
131
136
  +
132
137
  </button>
133
138
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.31",
3
+ "version": "0.5.32",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {