neural-loom 0.2.57 → 0.2.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neural-loom",
3
- "version": "0.2.57",
3
+ "version": "0.2.58",
4
4
  "bin": {
5
5
  "neural-loom": "./bin/cli.js"
6
6
  },
@@ -96,6 +96,57 @@ export async function GET(request: Request) {
96
96
  }
97
97
  }
98
98
 
99
+ if (action === "branches") {
100
+ const { stdout } = await execPromise("git branch --format=\"%(refname:short)|%(HEAD)\"", { cwd: targetRoot });
101
+ const branches = stdout.split(/\r?\n/).filter(Boolean).map(line => {
102
+ const [name, active] = line.split("|");
103
+ return {
104
+ name: name.trim(),
105
+ isActive: active === "*"
106
+ };
107
+ });
108
+ return NextResponse.json({ success: true, branches });
109
+ }
110
+
111
+ if (action === "log") {
112
+ try {
113
+ const { stdout } = await execPromise("git log --graph --abbrev-commit --decorate --format=format:\"%h||%p||%s||%an||%ar\" --all -n 100", { cwd: targetRoot });
114
+ const lines = stdout.split(/\r?\n/);
115
+ const logItems = lines.map(line => {
116
+ const parts = line.split("||");
117
+ if (parts.length >= 5) {
118
+ const firstPart = parts[0];
119
+ const hashMatch = firstPart.match(/([0-9a-f]{7,40})$/);
120
+ const hash = hashMatch ? hashMatch[1] : "";
121
+ const graph = firstPart.substring(0, firstPart.length - hash.length);
122
+
123
+ return {
124
+ isCommit: true,
125
+ graph,
126
+ hash,
127
+ parents: parts[1].trim().split(/\s+/).filter(Boolean),
128
+ subject: parts[2],
129
+ author: parts[3],
130
+ date: parts[4]
131
+ };
132
+ } else {
133
+ return {
134
+ isCommit: false,
135
+ graph: line,
136
+ hash: "",
137
+ parents: [],
138
+ subject: "",
139
+ author: "",
140
+ date: ""
141
+ };
142
+ }
143
+ });
144
+ return NextResponse.json({ success: true, log: logItems });
145
+ } catch (err) {
146
+ return NextResponse.json({ success: true, log: [] });
147
+ }
148
+ }
149
+
99
150
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
100
151
  } catch (error) {
101
152
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -189,6 +240,34 @@ export async function POST(request: Request) {
189
240
  return NextResponse.json({ success: true, output: stdout + stderr });
190
241
  }
191
242
 
243
+ if (action === "checkout") {
244
+ const { branchName } = body;
245
+ if (!branchName) {
246
+ return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
247
+ }
248
+ const { stdout, stderr } = await execPromise(`git checkout ${JSON.stringify(branchName)}`, { cwd: targetRoot });
249
+ return NextResponse.json({ success: true, output: stdout + stderr });
250
+ }
251
+
252
+ if (action === "create_branch") {
253
+ const { branchName } = body;
254
+ if (!branchName) {
255
+ return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
256
+ }
257
+ const { stdout, stderr } = await execPromise(`git checkout -b ${JSON.stringify(branchName)}`, { cwd: targetRoot });
258
+ return NextResponse.json({ success: true, output: stdout + stderr });
259
+ }
260
+
261
+ if (action === "delete_branch") {
262
+ const { branchName, force } = body;
263
+ if (!branchName) {
264
+ return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
265
+ }
266
+ const flag = force ? "-D" : "-d";
267
+ const { stdout, stderr } = await execPromise(`git branch ${flag} ${JSON.stringify(branchName)}`, { cwd: targetRoot });
268
+ return NextResponse.json({ success: true, output: stdout + stderr });
269
+ }
270
+
192
271
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
193
272
  } catch (error) {
194
273
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -115,6 +115,16 @@ interface GitFile {
115
115
  worktreeStatus?: string;
116
116
  }
117
117
 
118
+ interface GitLogItem {
119
+ isCommit: boolean;
120
+ graph: string;
121
+ hash: string;
122
+ parents: string[];
123
+ subject: string;
124
+ author: string;
125
+ date: string;
126
+ }
127
+
118
128
  interface SessionStatus {
119
129
  type?: string;
120
130
  status?: string;
@@ -310,6 +320,12 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
310
320
  const [gitBranch, setGitBranch] = useState("");
311
321
  const [gitSync, setGitSync] = useState({ ahead: 0, behind: 0 });
312
322
  const [commitMessage, setCommitMessage] = useState("");
323
+ const [gitBranches, setGitBranches] = useState<{ name: string; isActive: boolean }[]>([]);
324
+ const [gitLog, setGitLog] = useState<GitLogItem[]>([]);
325
+ const [isBranchesExpanded, setIsBranchesExpanded] = useState(false);
326
+ const [isHistoryExpanded, setIsHistoryExpanded] = useState(false);
327
+ const [newBranchName, setNewBranchName] = useState("");
328
+ const [branchActionLoading, setBranchActionLoading] = useState(false);
313
329
 
314
330
  // Status & Telemetry
315
331
  const [sessionStatus, setSessionStatus] = useState<SessionStatus | null>(null);
@@ -340,6 +356,34 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
340
356
  setGitFiles(data.files || []);
341
357
  setGitBranch(data.branch || "");
342
358
  setGitSync({ ahead: data.ahead || 0, behind: data.behind || 0 });
359
+
360
+ if (data.isGit) {
361
+ // Fetch branches
362
+ try {
363
+ const branchesRes = await fetch(`/api/git?action=branches&root=${encodeURIComponent(workspaceRoot)}`);
364
+ if (branchesRes.ok) {
365
+ const branchesData = await branchesRes.json();
366
+ if (branchesData.success) {
367
+ setGitBranches(branchesData.branches || []);
368
+ }
369
+ }
370
+ } catch (err) {
371
+ console.error("Failed to load git branches:", err);
372
+ }
373
+
374
+ // Fetch log
375
+ try {
376
+ const logRes = await fetch(`/api/git?action=log&root=${encodeURIComponent(workspaceRoot)}`);
377
+ if (logRes.ok) {
378
+ const logData = await logRes.json();
379
+ if (logData.success) {
380
+ setGitLog(logData.log || []);
381
+ }
382
+ }
383
+ } catch (err) {
384
+ console.error("Failed to load git log:", err);
385
+ }
386
+ }
343
387
  }
344
388
  }
345
389
  } catch (e) {
@@ -510,6 +554,102 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
510
554
  }
511
555
  };
512
556
 
557
+ const handleGitCheckout = async (branchName: string) => {
558
+ setBranchActionLoading(true);
559
+ try {
560
+ const res = await fetch("/api/git", {
561
+ method: "POST",
562
+ headers: { "Content-Type": "application/json" },
563
+ body: JSON.stringify({
564
+ action: "checkout",
565
+ branchName,
566
+ rootPath: workspaceRoot,
567
+ }),
568
+ });
569
+ const data = await res.json();
570
+ if (res.ok && data.success) {
571
+ await loadGitStatus();
572
+ } else {
573
+ alert(data.error || `Failed to checkout branch ${branchName}`);
574
+ }
575
+ } catch (e) {
576
+ console.error(e);
577
+ alert("Failed to checkout branch due to a network error.");
578
+ } finally {
579
+ setBranchActionLoading(false);
580
+ }
581
+ };
582
+
583
+ const handleGitCreateBranch = async (e?: React.FormEvent) => {
584
+ if (e) e.preventDefault();
585
+ if (!newBranchName.trim()) return;
586
+ setBranchActionLoading(true);
587
+ try {
588
+ const res = await fetch("/api/git", {
589
+ method: "POST",
590
+ headers: { "Content-Type": "application/json" },
591
+ body: JSON.stringify({
592
+ action: "create_branch",
593
+ branchName: newBranchName.trim(),
594
+ rootPath: workspaceRoot,
595
+ }),
596
+ });
597
+ const data = await res.json();
598
+ if (res.ok && data.success) {
599
+ setNewBranchName("");
600
+ await loadGitStatus();
601
+ } else {
602
+ alert(data.error || `Failed to create branch ${newBranchName}`);
603
+ }
604
+ } catch (e) {
605
+ console.error(e);
606
+ alert("Failed to create branch due to a network error.");
607
+ } finally {
608
+ setBranchActionLoading(false);
609
+ }
610
+ };
611
+
612
+ const handleGitDeleteBranch = async (branchName: string, force = false) => {
613
+ if (branchName === gitBranch) {
614
+ alert("Cannot delete the currently active branch.");
615
+ return;
616
+ }
617
+ const message = force
618
+ ? `Are you sure you want to FORCE delete the branch "${branchName}"? All unmerged changes will be lost.`
619
+ : `Are you sure you want to delete the branch "${branchName}"?`;
620
+ if (!confirm(message)) return;
621
+
622
+ setBranchActionLoading(true);
623
+ try {
624
+ const res = await fetch("/api/git", {
625
+ method: "POST",
626
+ headers: { "Content-Type": "application/json" },
627
+ body: JSON.stringify({
628
+ action: "delete_branch",
629
+ branchName,
630
+ force,
631
+ rootPath: workspaceRoot,
632
+ }),
633
+ });
634
+ const data = await res.json();
635
+ if (res.ok && data.success) {
636
+ await loadGitStatus();
637
+ } else {
638
+ // If it failed because it's not merged, offer force delete
639
+ if (!force && data.error && (data.error.includes("not fully merged") || data.error.includes("not merged"))) {
640
+ handleGitDeleteBranch(branchName, true);
641
+ } else {
642
+ alert(data.error || `Failed to delete branch ${branchName}`);
643
+ }
644
+ }
645
+ } catch (e) {
646
+ console.error(e);
647
+ alert("Failed to delete branch due to a network error.");
648
+ } finally {
649
+ setBranchActionLoading(false);
650
+ }
651
+ };
652
+
513
653
  const handleSelectGitFile = async (file: GitFile) => {
514
654
  try {
515
655
  const res = await fetch(`/api/git?action=diff&file=${encodeURIComponent(file.relativePath)}&root=${encodeURIComponent(workspaceRoot)}`);
@@ -1390,6 +1530,194 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1390
1530
  </div>
1391
1531
  )}
1392
1532
  </div>
1533
+
1534
+ {/* Branches Section */}
1535
+ <div style={{ borderTop: "1px solid var(--border)", paddingTop: "0.75rem" }}>
1536
+ <div
1537
+ onClick={() => setIsBranchesExpanded(!isBranchesExpanded)}
1538
+ style={{
1539
+ display: "flex",
1540
+ justifyContent: "space-between",
1541
+ alignItems: "center",
1542
+ cursor: "pointer",
1543
+ marginBottom: "0.5rem",
1544
+ userSelect: "none"
1545
+ }}
1546
+ >
1547
+ <span style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", display: "flex", alignItems: "center", gap: "0.25rem" }}>
1548
+ {isBranchesExpanded ? "▼" : "▶"} Branches ({gitBranches.length})
1549
+ </span>
1550
+ </div>
1551
+
1552
+ {isBranchesExpanded && (
1553
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
1554
+ {/* Create Branch Input */}
1555
+ <form onSubmit={handleGitCreateBranch} style={{ display: "flex", gap: "0.25rem" }}>
1556
+ <input
1557
+ type="text"
1558
+ placeholder="New branch name..."
1559
+ value={newBranchName}
1560
+ onChange={(e) => setNewBranchName(e.target.value)}
1561
+ disabled={branchActionLoading}
1562
+ style={{
1563
+ flex: 1,
1564
+ backgroundColor: "#05070a",
1565
+ border: "1px solid var(--border)",
1566
+ borderRadius: "4px",
1567
+ padding: "0.25rem 0.5rem",
1568
+ color: "#ffffff",
1569
+ fontSize: "0.7rem",
1570
+ outline: "none"
1571
+ }}
1572
+ />
1573
+ <button
1574
+ type="submit"
1575
+ disabled={branchActionLoading || !newBranchName.trim()}
1576
+ style={{
1577
+ backgroundColor: "rgba(59, 130, 246, 0.15)",
1578
+ border: "1px solid #3b82f6",
1579
+ color: "#3b82f6",
1580
+ padding: "0.25rem 0.5rem",
1581
+ borderRadius: "4px",
1582
+ fontSize: "0.7rem",
1583
+ fontWeight: 600,
1584
+ cursor: "pointer",
1585
+ opacity: (!newBranchName.trim() || branchActionLoading) ? 0.5 : 1
1586
+ }}
1587
+ >
1588
+ + Create
1589
+ </button>
1590
+ </form>
1591
+
1592
+ {/* Branch List */}
1593
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem", maxHeight: "150px", overflowY: "auto" }}>
1594
+ {gitBranches.map((br) => (
1595
+ <div
1596
+ key={br.name}
1597
+ style={{
1598
+ display: "flex",
1599
+ alignItems: "center",
1600
+ justifyContent: "space-between",
1601
+ padding: "0.35rem 0.5rem",
1602
+ borderRadius: "4px",
1603
+ backgroundColor: br.isActive ? "rgba(59, 130, 246, 0.08)" : "transparent",
1604
+ border: br.isActive ? "1px solid rgba(59, 130, 246, 0.2)" : "1px solid transparent",
1605
+ fontSize: "0.75rem",
1606
+ transition: "all 0.15s ease",
1607
+ }}
1608
+ >
1609
+ <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", fontWeight: br.isActive ? 700 : 500, color: br.isActive ? "var(--primary)" : "#cbd5e1", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>
1610
+ <span style={{ fontSize: "0.7rem" }}>{br.isActive ? "●" : "○"}</span>
1611
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={br.name}>{br.name}</span>
1612
+ </div>
1613
+ <div style={{ display: "flex", gap: "0.25rem" }}>
1614
+ {!br.isActive && (
1615
+ <>
1616
+ <button
1617
+ type="button"
1618
+ onClick={() => handleGitCheckout(br.name)}
1619
+ disabled={branchActionLoading}
1620
+ title={`Switch to ${br.name}`}
1621
+ style={{
1622
+ background: "none",
1623
+ border: "none",
1624
+ cursor: "pointer",
1625
+ fontSize: "0.7rem",
1626
+ padding: "2px 4px",
1627
+ color: "var(--muted)"
1628
+ }}
1629
+ >
1630
+
1631
+ </button>
1632
+ <button
1633
+ type="button"
1634
+ onClick={() => handleGitDeleteBranch(br.name)}
1635
+ disabled={branchActionLoading}
1636
+ title={`Delete ${br.name}`}
1637
+ style={{
1638
+ background: "none",
1639
+ border: "none",
1640
+ cursor: "pointer",
1641
+ fontSize: "0.7rem",
1642
+ padding: "2px 4px",
1643
+ color: "var(--muted)"
1644
+ }}
1645
+ >
1646
+
1647
+ </button>
1648
+ </>
1649
+ )}
1650
+ </div>
1651
+ </div>
1652
+ ))}
1653
+ </div>
1654
+ </div>
1655
+ )}
1656
+ </div>
1657
+
1658
+ {/* Commit History Section */}
1659
+ <div style={{ borderTop: "1px solid var(--border)", paddingTop: "0.75rem" }}>
1660
+ <div
1661
+ onClick={() => setIsHistoryExpanded(!isHistoryExpanded)}
1662
+ style={{
1663
+ display: "flex",
1664
+ justifyContent: "space-between",
1665
+ alignItems: "center",
1666
+ cursor: "pointer",
1667
+ marginBottom: "0.5rem",
1668
+ userSelect: "none"
1669
+ }}
1670
+ >
1671
+ <span style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", display: "flex", alignItems: "center", gap: "0.25rem" }}>
1672
+ {isHistoryExpanded ? "▼" : "▶"} Commit History
1673
+ </span>
1674
+ </div>
1675
+
1676
+ {isHistoryExpanded && (
1677
+ <div style={{
1678
+ backgroundColor: "#05070a",
1679
+ border: "1px solid var(--border)",
1680
+ borderRadius: "6px",
1681
+ padding: "0.5rem",
1682
+ maxHeight: "220px",
1683
+ overflowY: "auto",
1684
+ overflowX: "auto"
1685
+ }}>
1686
+ {gitLog.length === 0 ? (
1687
+ <div style={{ color: "var(--muted)", fontStyle: "italic", fontSize: "0.7rem", textAlign: "center", padding: "0.5rem" }}>
1688
+ No commit history found.
1689
+ </div>
1690
+ ) : (
1691
+ <div style={{ fontFamily: "var(--font-mono)", fontSize: "0.65rem", lineHeight: "1.4", whiteSpace: "pre" }}>
1692
+ {gitLog.map((item, idx) => (
1693
+ <div
1694
+ key={idx}
1695
+ style={{
1696
+ display: "flex",
1697
+ padding: "2px 0",
1698
+ borderBottom: "1px solid rgba(255,255,255,0.01)",
1699
+ alignItems: "center"
1700
+ }}
1701
+ >
1702
+ <span style={{ color: "#3b82f6", flexShrink: 0, fontWeight: 700 }}>{item.graph}</span>
1703
+ {item.isCommit && (
1704
+ <>
1705
+ <span style={{ color: "#eab308", marginRight: "6px", flexShrink: 0 }} title="Commit Hash">{item.hash}</span>
1706
+ <span style={{ flex: 1, textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap", color: "#cbd5e1" }} title={item.subject}>
1707
+ {item.subject}
1708
+ </span>
1709
+ <span style={{ color: "var(--muted)", fontSize: "0.55rem", marginLeft: "6px", flexShrink: 0 }} title={`${item.author} (${item.date})`}>
1710
+ {item.date}
1711
+ </span>
1712
+ </>
1713
+ )}
1714
+ </div>
1715
+ ))}
1716
+ </div>
1717
+ )}
1718
+ </div>
1719
+ )}
1720
+ </div>
1393
1721
  </div>
1394
1722
  ) : (
1395
1723
  <div style={{ padding: "1rem", color: "var(--muted)", fontSize: "0.75rem", fontStyle: "italic" }}>