Spaces:
Sleeping
Sleeping
Claw Web commited on
Commit Β·
6aeba60
1
Parent(s): ea87ddf
feat: complete P0 fixes + Manus UI overhaul phase 2
Browse filesP0 Fixes:
- tool_choice: pass through from caller instead of hardcoded 'auto' (llm.ts)
- glob matching: replaced custom regex with micromatch library (permissions.ts)
- graceful shutdown: SIGTERM/SIGINT handlers with DB close (index.ts, db.ts)
- Dockerfile: chmod 777 β 755 with proper chown
Manus UI:
- RightPanel: tabbed panel with Actions/Files/Preview tabs
- FileExplorer: inline file browser with directory navigation and file preview
- PreviewPanel: live viewport showing latest tool output (terminal/web/file)
- CodeDiffViewer: LCS-based diff viewer with Diff/Original/Modified tabs
- Integrated CodeDiffViewer into ActionTree for edit_file operations
Build: clean, 0 errors
- Dockerfile +1 -1
- client/src/components/ActionTree.tsx +51 -24
- client/src/components/CodeDiffViewer.tsx +190 -0
- client/src/components/RightPanel.tsx +395 -0
- client/src/pages/Home.tsx +2 -1
- package.json +2 -0
- pnpm-lock.yaml +63 -0
- server/_core/index.ts +24 -0
- server/_core/llm.ts +2 -1
- server/db.ts +15 -0
- server/runtime/permissions.ts +4 -18
Dockerfile
CHANGED
|
@@ -28,7 +28,7 @@ COPY . .
|
|
| 28 |
RUN pnpm build
|
| 29 |
|
| 30 |
# Create workspace directory for agent
|
| 31 |
-
RUN mkdir -p /home/ubuntu && chmod
|
| 32 |
|
| 33 |
# HF Spaces uses port 7860
|
| 34 |
ENV PORT=7860
|
|
|
|
| 28 |
RUN pnpm build
|
| 29 |
|
| 30 |
# Create workspace directory for agent
|
| 31 |
+
RUN mkdir -p /home/ubuntu && chown node:node /home/ubuntu && chmod 755 /home/ubuntu
|
| 32 |
|
| 33 |
# HF Spaces uses port 7860
|
| 34 |
ENV PORT=7860
|
client/src/components/ActionTree.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
| 23 |
Minimize2,
|
| 24 |
} from "lucide-react";
|
| 25 |
import type { ChatMessage } from "@/hooks/useChat";
|
|
|
|
| 26 |
|
| 27 |
// Tool category mapping for icons and colors
|
| 28 |
const TOOL_CATEGORIES: Record<string, { icon: typeof Terminal; color: string; label: string }> = {
|
|
@@ -181,32 +182,58 @@ function ActionNode({ tool, index, isLast }: ActionNodeProps) {
|
|
| 181 |
</pre>
|
| 182 |
</div>
|
| 183 |
|
| 184 |
-
{/* Output */}
|
| 185 |
{tool.result && (
|
| 186 |
<div className="relative group/block mt-2">
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
)}
|
| 212 |
</div>
|
|
|
|
| 23 |
Minimize2,
|
| 24 |
} from "lucide-react";
|
| 25 |
import type { ChatMessage } from "@/hooks/useChat";
|
| 26 |
+
import { CodeDiffViewer } from "./CodeDiffViewer";
|
| 27 |
|
| 28 |
// Tool category mapping for icons and colors
|
| 29 |
const TOOL_CATEGORIES: Record<string, { icon: typeof Terminal; color: string; label: string }> = {
|
|
|
|
| 182 |
</pre>
|
| 183 |
</div>
|
| 184 |
|
| 185 |
+
{/* Output β with CodeDiffViewer for edit/write operations */}
|
| 186 |
{tool.result && (
|
| 187 |
<div className="relative group/block mt-2">
|
| 188 |
+
{(tool.name === "edit_file" || tool.name === "multi_edit_file") && tool.result && !tool.isError ? (
|
| 189 |
+
(() => {
|
| 190 |
+
// Try to extract old/new content from edit result
|
| 191 |
+
try {
|
| 192 |
+
const parsed = JSON.parse(tool.arguments);
|
| 193 |
+
const fileName = parsed.path?.split("/").pop() || parsed.path;
|
| 194 |
+
// Show diff viewer with result as the "after" content
|
| 195 |
+
return (
|
| 196 |
+
<CodeDiffViewer
|
| 197 |
+
oldContent={parsed.old_string || parsed.old_text || ""}
|
| 198 |
+
newContent={parsed.new_string || parsed.new_text || parsed.old_string || ""}
|
| 199 |
+
fileName={fileName}
|
| 200 |
+
/>
|
| 201 |
+
);
|
| 202 |
+
} catch {
|
| 203 |
+
return null;
|
| 204 |
+
}
|
| 205 |
+
})() || (
|
| 206 |
+
<pre className="text-[11px] font-mono bg-secondary/30 rounded-md p-2 overflow-x-auto max-h-[300px] overflow-y-auto whitespace-pre-wrap break-all text-foreground/80">
|
| 207 |
+
{tool.result.length > 5000 ? tool.result.slice(0, 5000) + "\n\n[...truncated]" : tool.result}
|
| 208 |
+
</pre>
|
| 209 |
+
)
|
| 210 |
+
) : (
|
| 211 |
+
<>
|
| 212 |
+
<div className="text-[10px] text-muted-foreground/60 mb-1 flex items-center gap-1">
|
| 213 |
+
<Terminal className="size-2.5" /> Output
|
| 214 |
+
{tool.isError && (
|
| 215 |
+
<span className="text-destructive text-[9px]">ERROR</span>
|
| 216 |
+
)}
|
| 217 |
+
<button
|
| 218 |
+
onClick={() => handleCopy(tool.result!)}
|
| 219 |
+
className="ml-auto opacity-0 group-hover/block:opacity-100 transition-opacity"
|
| 220 |
+
>
|
| 221 |
+
<Copy className="size-2.5 text-muted-foreground hover:text-foreground" />
|
| 222 |
+
</button>
|
| 223 |
+
</div>
|
| 224 |
+
<pre
|
| 225 |
+
className={`text-[11px] font-mono rounded-md p-2 overflow-x-auto max-h-[300px] overflow-y-auto whitespace-pre-wrap break-all ${
|
| 226 |
+
tool.isError
|
| 227 |
+
? "bg-destructive/10 text-destructive/80 border border-destructive/20"
|
| 228 |
+
: "bg-secondary/30 text-foreground/80"
|
| 229 |
+
}`}
|
| 230 |
+
>
|
| 231 |
+
{tool.result.length > 5000
|
| 232 |
+
? tool.result.slice(0, 5000) + "\n\n[...truncated]"
|
| 233 |
+
: tool.result}
|
| 234 |
+
</pre>
|
| 235 |
+
</>
|
| 236 |
+
)}
|
| 237 |
</div>
|
| 238 |
)}
|
| 239 |
</div>
|
client/src/components/CodeDiffViewer.tsx
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CodeDiffViewer β Manus-style code diff viewer with tabs:
|
| 3 |
+
* Diff / Original / Modified
|
| 4 |
+
* Used in ActionTree when edit_file or write_file tool calls are shown.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { useState, useMemo } from "react";
|
| 8 |
+
import { cn } from "@/lib/utils";
|
| 9 |
+
|
| 10 |
+
type DiffTab = "diff" | "original" | "modified";
|
| 11 |
+
|
| 12 |
+
interface DiffLine {
|
| 13 |
+
type: "added" | "removed" | "context";
|
| 14 |
+
lineOld?: number;
|
| 15 |
+
lineNew?: number;
|
| 16 |
+
content: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Simple unified diff parser.
|
| 21 |
+
* Takes old text and new text, produces a line-by-line diff.
|
| 22 |
+
*/
|
| 23 |
+
function computeDiff(oldText: string, newText: string): DiffLine[] {
|
| 24 |
+
const oldLines = oldText.split("\n");
|
| 25 |
+
const newLines = newText.split("\n");
|
| 26 |
+
const result: DiffLine[] = [];
|
| 27 |
+
|
| 28 |
+
// Simple LCS-based diff
|
| 29 |
+
const m = oldLines.length;
|
| 30 |
+
const n = newLines.length;
|
| 31 |
+
|
| 32 |
+
// For large files, fall back to a simpler approach
|
| 33 |
+
if (m * n > 1_000_000) {
|
| 34 |
+
// Just show removed then added for very large files
|
| 35 |
+
oldLines.forEach((line, i) => {
|
| 36 |
+
result.push({ type: "removed", lineOld: i + 1, content: line });
|
| 37 |
+
});
|
| 38 |
+
newLines.forEach((line, i) => {
|
| 39 |
+
result.push({ type: "added", lineNew: i + 1, content: line });
|
| 40 |
+
});
|
| 41 |
+
return result;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Build LCS table
|
| 45 |
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
| 46 |
+
for (let i = 1; i <= m; i++) {
|
| 47 |
+
for (let j = 1; j <= n; j++) {
|
| 48 |
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
| 49 |
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
| 50 |
+
} else {
|
| 51 |
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Backtrack to produce diff
|
| 57 |
+
const diffLines: DiffLine[] = [];
|
| 58 |
+
let i = m, j = n;
|
| 59 |
+
while (i > 0 || j > 0) {
|
| 60 |
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
| 61 |
+
diffLines.unshift({ type: "context", lineOld: i, lineNew: j, content: oldLines[i - 1] });
|
| 62 |
+
i--; j--;
|
| 63 |
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
| 64 |
+
diffLines.unshift({ type: "added", lineNew: j, content: newLines[j - 1] });
|
| 65 |
+
j--;
|
| 66 |
+
} else {
|
| 67 |
+
diffLines.unshift({ type: "removed", lineOld: i, content: oldLines[i - 1] });
|
| 68 |
+
i--;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return diffLines;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
interface CodeDiffViewerProps {
|
| 76 |
+
oldContent: string;
|
| 77 |
+
newContent: string;
|
| 78 |
+
fileName?: string;
|
| 79 |
+
className?: string;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export function CodeDiffViewer({ oldContent, newContent, fileName, className }: CodeDiffViewerProps) {
|
| 83 |
+
const [activeTab, setActiveTab] = useState<DiffTab>("diff");
|
| 84 |
+
|
| 85 |
+
const diffLines = useMemo(
|
| 86 |
+
() => computeDiff(oldContent, newContent),
|
| 87 |
+
[oldContent, newContent]
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
const stats = useMemo(() => {
|
| 91 |
+
const added = diffLines.filter(l => l.type === "added").length;
|
| 92 |
+
const removed = diffLines.filter(l => l.type === "removed").length;
|
| 93 |
+
return { added, removed };
|
| 94 |
+
}, [diffLines]);
|
| 95 |
+
|
| 96 |
+
const tabs: { id: DiffTab; label: string }[] = [
|
| 97 |
+
{ id: "diff", label: "Diff" },
|
| 98 |
+
{ id: "original", label: "Original" },
|
| 99 |
+
{ id: "modified", label: "Modified" },
|
| 100 |
+
];
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
<div className={cn("flex flex-col rounded-md border border-border overflow-hidden", className)}>
|
| 104 |
+
{/* Header with tabs */}
|
| 105 |
+
<div className="flex items-center justify-between bg-secondary/30 border-b border-border">
|
| 106 |
+
<div className="flex">
|
| 107 |
+
{tabs.map((tab) => (
|
| 108 |
+
<button
|
| 109 |
+
key={tab.id}
|
| 110 |
+
onClick={() => setActiveTab(tab.id)}
|
| 111 |
+
className={cn(
|
| 112 |
+
"px-3 py-1.5 text-[11px] font-medium transition-colors border-b-2 -mb-px",
|
| 113 |
+
activeTab === tab.id
|
| 114 |
+
? "border-primary text-foreground bg-background/50"
|
| 115 |
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
| 116 |
+
)}
|
| 117 |
+
>
|
| 118 |
+
{tab.label}
|
| 119 |
+
</button>
|
| 120 |
+
))}
|
| 121 |
+
</div>
|
| 122 |
+
<div className="flex items-center gap-2 px-2">
|
| 123 |
+
{fileName && (
|
| 124 |
+
<span className="text-[10px] font-mono text-muted-foreground">
|
| 125 |
+
{fileName}
|
| 126 |
+
</span>
|
| 127 |
+
)}
|
| 128 |
+
<span className="text-[10px] font-mono text-green-500">+{stats.added}</span>
|
| 129 |
+
<span className="text-[10px] font-mono text-red-500">-{stats.removed}</span>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
{/* Content */}
|
| 134 |
+
<div className="overflow-auto max-h-[400px] bg-[#0d1117]">
|
| 135 |
+
{activeTab === "diff" && (
|
| 136 |
+
<table className="w-full text-[11px] font-mono border-collapse">
|
| 137 |
+
<tbody>
|
| 138 |
+
{diffLines.map((line, idx) => (
|
| 139 |
+
<tr
|
| 140 |
+
key={idx}
|
| 141 |
+
className={cn(
|
| 142 |
+
line.type === "added" && "bg-green-500/10",
|
| 143 |
+
line.type === "removed" && "bg-red-500/10"
|
| 144 |
+
)}
|
| 145 |
+
>
|
| 146 |
+
<td className="w-[1px] px-1 text-right text-muted-foreground/40 select-none border-r border-border/30 whitespace-nowrap">
|
| 147 |
+
{line.lineOld || ""}
|
| 148 |
+
</td>
|
| 149 |
+
<td className="w-[1px] px-1 text-right text-muted-foreground/40 select-none border-r border-border/30 whitespace-nowrap">
|
| 150 |
+
{line.lineNew || ""}
|
| 151 |
+
</td>
|
| 152 |
+
<td className="w-[1px] px-1 select-none font-bold">
|
| 153 |
+
<span className={cn(
|
| 154 |
+
line.type === "added" && "text-green-500",
|
| 155 |
+
line.type === "removed" && "text-red-500",
|
| 156 |
+
line.type === "context" && "text-muted-foreground/30"
|
| 157 |
+
)}>
|
| 158 |
+
{line.type === "added" ? "+" : line.type === "removed" ? "-" : " "}
|
| 159 |
+
</span>
|
| 160 |
+
</td>
|
| 161 |
+
<td className="px-2 whitespace-pre-wrap break-all">
|
| 162 |
+
<span className={cn(
|
| 163 |
+
line.type === "added" && "text-green-400/90",
|
| 164 |
+
line.type === "removed" && "text-red-400/90",
|
| 165 |
+
line.type === "context" && "text-foreground/60"
|
| 166 |
+
)}>
|
| 167 |
+
{line.content}
|
| 168 |
+
</span>
|
| 169 |
+
</td>
|
| 170 |
+
</tr>
|
| 171 |
+
))}
|
| 172 |
+
</tbody>
|
| 173 |
+
</table>
|
| 174 |
+
)}
|
| 175 |
+
|
| 176 |
+
{activeTab === "original" && (
|
| 177 |
+
<pre className="text-[11px] font-mono p-3 whitespace-pre-wrap break-all text-foreground/70">
|
| 178 |
+
{oldContent || "(empty)"}
|
| 179 |
+
</pre>
|
| 180 |
+
)}
|
| 181 |
+
|
| 182 |
+
{activeTab === "modified" && (
|
| 183 |
+
<pre className="text-[11px] font-mono p-3 whitespace-pre-wrap break-all text-foreground/70">
|
| 184 |
+
{newContent || "(empty)"}
|
| 185 |
+
</pre>
|
| 186 |
+
)}
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
);
|
| 190 |
+
}
|
client/src/components/RightPanel.tsx
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RightPanel β Manus-style tabbed right panel.
|
| 3 |
+
* Tabs: Actions (tool timeline), Files (explorer), Preview (live viewport).
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { useState, useCallback, useEffect, useRef } from "react";
|
| 7 |
+
import {
|
| 8 |
+
Activity,
|
| 9 |
+
FolderTree,
|
| 10 |
+
Monitor,
|
| 11 |
+
Folder,
|
| 12 |
+
File,
|
| 13 |
+
ChevronRight,
|
| 14 |
+
ChevronDown,
|
| 15 |
+
RefreshCw,
|
| 16 |
+
Home,
|
| 17 |
+
ArrowUp,
|
| 18 |
+
FileText,
|
| 19 |
+
FileCode,
|
| 20 |
+
FileJson,
|
| 21 |
+
Image as ImageIcon,
|
| 22 |
+
Loader2,
|
| 23 |
+
ExternalLink,
|
| 24 |
+
} from "lucide-react";
|
| 25 |
+
import { ActionTree } from "./ActionTree";
|
| 26 |
+
import type { ChatMessage } from "@/hooks/useChat";
|
| 27 |
+
import { cn } from "@/lib/utils";
|
| 28 |
+
|
| 29 |
+
// βββ Tab definitions ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
type TabId = "actions" | "files" | "preview";
|
| 32 |
+
|
| 33 |
+
const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
|
| 34 |
+
{ id: "actions", label: "Actions", icon: Activity },
|
| 35 |
+
{ id: "files", label: "Files", icon: FolderTree },
|
| 36 |
+
{ id: "preview", label: "Preview", icon: Monitor },
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
+
// βββ File Explorer types ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
|
| 41 |
+
interface FileNode {
|
| 42 |
+
name: string;
|
| 43 |
+
path: string;
|
| 44 |
+
isDirectory: boolean;
|
| 45 |
+
size?: number;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function getFileIcon(name: string) {
|
| 49 |
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
| 50 |
+
if (["ts", "tsx", "js", "jsx", "py", "rs", "go", "java", "c", "cpp", "rb", "sh"].includes(ext))
|
| 51 |
+
return FileCode;
|
| 52 |
+
if (["json", "yaml", "yml", "toml", "xml", "env"].includes(ext))
|
| 53 |
+
return FileJson;
|
| 54 |
+
if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(ext))
|
| 55 |
+
return ImageIcon;
|
| 56 |
+
if (["md", "txt", "log", "csv"].includes(ext))
|
| 57 |
+
return FileText;
|
| 58 |
+
return File;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function formatSize(bytes?: number): string {
|
| 62 |
+
if (bytes === undefined) return "";
|
| 63 |
+
if (bytes < 1024) return `${bytes}B`;
|
| 64 |
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
| 65 |
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// βββ Inline File Explorer βββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
|
| 70 |
+
function FileExplorer() {
|
| 71 |
+
const [currentPath, setCurrentPath] = useState("/home/user");
|
| 72 |
+
const [files, setFiles] = useState<FileNode[]>([]);
|
| 73 |
+
const [loading, setLoading] = useState(false);
|
| 74 |
+
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
| 75 |
+
const [fileContent, setFileContent] = useState<string | null>(null);
|
| 76 |
+
const [loadingContent, setLoadingContent] = useState(false);
|
| 77 |
+
|
| 78 |
+
const fetchFiles = useCallback(async (dirPath: string) => {
|
| 79 |
+
setLoading(true);
|
| 80 |
+
try {
|
| 81 |
+
const response = await fetch("/api/chat/slash", {
|
| 82 |
+
method: "POST",
|
| 83 |
+
headers: { "Content-Type": "application/json" },
|
| 84 |
+
body: JSON.stringify({ command: "/files", args: `list ${dirPath}` }),
|
| 85 |
+
});
|
| 86 |
+
const data = await response.json();
|
| 87 |
+
if (data.files) {
|
| 88 |
+
setFiles(data.files);
|
| 89 |
+
} else if (data.result) {
|
| 90 |
+
// Parse text listing
|
| 91 |
+
const lines = data.result.split("\n").filter((l: string) => l.trim());
|
| 92 |
+
const parsed: FileNode[] = lines
|
| 93 |
+
.filter((l: string) => !l.startsWith("##") && !l.startsWith("---"))
|
| 94 |
+
.map((l: string) => {
|
| 95 |
+
const isDir = l.includes("[DIR]") || l.endsWith("/");
|
| 96 |
+
const name = l.replace("[DIR]", "").replace(/\/$/, "").trim().split(/\s+/).pop() || l.trim();
|
| 97 |
+
return { name, path: `${dirPath}/${name}`.replace("//", "/"), isDirectory: isDir };
|
| 98 |
+
})
|
| 99 |
+
.filter((f: FileNode) => f.name && f.name !== "." && f.name !== "..");
|
| 100 |
+
setFiles(parsed);
|
| 101 |
+
}
|
| 102 |
+
} catch (err) {
|
| 103 |
+
console.error("Failed to fetch files:", err);
|
| 104 |
+
setFiles([]);
|
| 105 |
+
} finally {
|
| 106 |
+
setLoading(false);
|
| 107 |
+
}
|
| 108 |
+
}, []);
|
| 109 |
+
|
| 110 |
+
const fetchFileContent = useCallback(async (filePath: string) => {
|
| 111 |
+
setLoadingContent(true);
|
| 112 |
+
try {
|
| 113 |
+
const response = await fetch("/api/chat/slash", {
|
| 114 |
+
method: "POST",
|
| 115 |
+
headers: { "Content-Type": "application/json" },
|
| 116 |
+
body: JSON.stringify({ command: "/files", args: `read ${filePath}` }),
|
| 117 |
+
});
|
| 118 |
+
const data = await response.json();
|
| 119 |
+
setFileContent(data.content || data.result || "Unable to read file");
|
| 120 |
+
} catch {
|
| 121 |
+
setFileContent("Error reading file");
|
| 122 |
+
} finally {
|
| 123 |
+
setLoadingContent(false);
|
| 124 |
+
}
|
| 125 |
+
}, []);
|
| 126 |
+
|
| 127 |
+
useEffect(() => {
|
| 128 |
+
fetchFiles(currentPath);
|
| 129 |
+
}, [currentPath, fetchFiles]);
|
| 130 |
+
|
| 131 |
+
const navigateUp = () => {
|
| 132 |
+
const parent = currentPath.split("/").slice(0, -1).join("/") || "/";
|
| 133 |
+
setCurrentPath(parent);
|
| 134 |
+
setSelectedFile(null);
|
| 135 |
+
setFileContent(null);
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
const handleClick = (file: FileNode) => {
|
| 139 |
+
if (file.isDirectory) {
|
| 140 |
+
setCurrentPath(file.path);
|
| 141 |
+
setSelectedFile(null);
|
| 142 |
+
setFileContent(null);
|
| 143 |
+
} else {
|
| 144 |
+
setSelectedFile(file.path);
|
| 145 |
+
fetchFileContent(file.path);
|
| 146 |
+
}
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
return (
|
| 150 |
+
<div className="h-full flex flex-col">
|
| 151 |
+
{/* Path bar */}
|
| 152 |
+
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border bg-secondary/20">
|
| 153 |
+
<button
|
| 154 |
+
onClick={() => { setCurrentPath("/home/user"); setSelectedFile(null); setFileContent(null); }}
|
| 155 |
+
className="p-1 rounded hover:bg-accent/50"
|
| 156 |
+
title="Home"
|
| 157 |
+
>
|
| 158 |
+
<Home className="size-3 text-muted-foreground" />
|
| 159 |
+
</button>
|
| 160 |
+
<button
|
| 161 |
+
onClick={navigateUp}
|
| 162 |
+
className="p-1 rounded hover:bg-accent/50"
|
| 163 |
+
title="Up"
|
| 164 |
+
>
|
| 165 |
+
<ArrowUp className="size-3 text-muted-foreground" />
|
| 166 |
+
</button>
|
| 167 |
+
<div className="flex-1 text-[10px] font-mono text-muted-foreground truncate px-1">
|
| 168 |
+
{currentPath}
|
| 169 |
+
</div>
|
| 170 |
+
<button
|
| 171 |
+
onClick={() => fetchFiles(currentPath)}
|
| 172 |
+
className="p-1 rounded hover:bg-accent/50"
|
| 173 |
+
title="Refresh"
|
| 174 |
+
>
|
| 175 |
+
<RefreshCw className={cn("size-3 text-muted-foreground", loading && "animate-spin")} />
|
| 176 |
+
</button>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
{/* File list or content preview */}
|
| 180 |
+
{selectedFile && fileContent !== null ? (
|
| 181 |
+
<div className="flex-1 flex flex-col overflow-hidden">
|
| 182 |
+
<div className="flex items-center gap-1 px-2 py-1 border-b border-border bg-secondary/10">
|
| 183 |
+
<button
|
| 184 |
+
onClick={() => { setSelectedFile(null); setFileContent(null); }}
|
| 185 |
+
className="text-[10px] text-primary hover:underline"
|
| 186 |
+
>
|
| 187 |
+
Back
|
| 188 |
+
</button>
|
| 189 |
+
<span className="text-[10px] text-muted-foreground truncate">
|
| 190 |
+
{selectedFile.split("/").pop()}
|
| 191 |
+
</span>
|
| 192 |
+
</div>
|
| 193 |
+
<div className="flex-1 overflow-auto">
|
| 194 |
+
{loadingContent ? (
|
| 195 |
+
<div className="flex items-center justify-center h-full">
|
| 196 |
+
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
| 197 |
+
</div>
|
| 198 |
+
) : (
|
| 199 |
+
<pre className="text-[11px] font-mono p-2 whitespace-pre-wrap break-all text-foreground/80">
|
| 200 |
+
{fileContent.length > 10000
|
| 201 |
+
? fileContent.slice(0, 10000) + "\n\n[...truncated]"
|
| 202 |
+
: fileContent}
|
| 203 |
+
</pre>
|
| 204 |
+
)}
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
) : (
|
| 208 |
+
<div className="flex-1 overflow-y-auto">
|
| 209 |
+
{loading ? (
|
| 210 |
+
<div className="flex items-center justify-center h-32">
|
| 211 |
+
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
| 212 |
+
</div>
|
| 213 |
+
) : files.length === 0 ? (
|
| 214 |
+
<div className="flex items-center justify-center h-32 text-[11px] text-muted-foreground">
|
| 215 |
+
Empty directory
|
| 216 |
+
</div>
|
| 217 |
+
) : (
|
| 218 |
+
<div className="py-0.5">
|
| 219 |
+
{files
|
| 220 |
+
.sort((a, b) => {
|
| 221 |
+
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
| 222 |
+
return a.name.localeCompare(b.name);
|
| 223 |
+
})
|
| 224 |
+
.map((file) => {
|
| 225 |
+
const Icon = file.isDirectory ? Folder : getFileIcon(file.name);
|
| 226 |
+
return (
|
| 227 |
+
<button
|
| 228 |
+
key={file.path}
|
| 229 |
+
onClick={() => handleClick(file)}
|
| 230 |
+
className={cn(
|
| 231 |
+
"flex items-center gap-1.5 w-full px-2 py-1 text-left hover:bg-accent/50 transition-colors",
|
| 232 |
+
selectedFile === file.path && "bg-accent"
|
| 233 |
+
)}
|
| 234 |
+
>
|
| 235 |
+
<Icon
|
| 236 |
+
className={cn(
|
| 237 |
+
"size-3.5 shrink-0",
|
| 238 |
+
file.isDirectory ? "text-amber-400" : "text-muted-foreground"
|
| 239 |
+
)}
|
| 240 |
+
/>
|
| 241 |
+
<span className="text-[11px] truncate flex-1">{file.name}</span>
|
| 242 |
+
{!file.isDirectory && file.size !== undefined && (
|
| 243 |
+
<span className="text-[9px] text-muted-foreground/50 font-mono">
|
| 244 |
+
{formatSize(file.size)}
|
| 245 |
+
</span>
|
| 246 |
+
)}
|
| 247 |
+
{file.isDirectory && (
|
| 248 |
+
<ChevronRight className="size-3 text-muted-foreground/30" />
|
| 249 |
+
)}
|
| 250 |
+
</button>
|
| 251 |
+
);
|
| 252 |
+
})}
|
| 253 |
+
</div>
|
| 254 |
+
)}
|
| 255 |
+
</div>
|
| 256 |
+
)}
|
| 257 |
+
</div>
|
| 258 |
+
);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// βββ Preview Panel ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 262 |
+
|
| 263 |
+
function PreviewPanel({ messages }: { messages: ChatMessage[] }) {
|
| 264 |
+
// Extract the latest tool output that could be previewed
|
| 265 |
+
// (bash output, web fetch results, file contents)
|
| 266 |
+
const latestPreviewable = (() => {
|
| 267 |
+
for (let i = messages.length - 1; i >= 0; i--) {
|
| 268 |
+
const msg = messages[i];
|
| 269 |
+
if (msg.role === "assistant" && msg.toolCalls) {
|
| 270 |
+
for (let j = msg.toolCalls.length - 1; j >= 0; j--) {
|
| 271 |
+
const tc = msg.toolCalls[j];
|
| 272 |
+
if (tc.result && !tc.isError) {
|
| 273 |
+
if (tc.name === "bash" || tc.name === "PowerShell") {
|
| 274 |
+
return { type: "terminal" as const, name: tc.name, content: tc.result, command: (() => { try { return JSON.parse(tc.arguments).command; } catch { return ""; } })() };
|
| 275 |
+
}
|
| 276 |
+
if (tc.name === "WebFetch" || tc.name === "WebSearch") {
|
| 277 |
+
return { type: "web" as const, name: tc.name, content: tc.result, url: (() => { try { return JSON.parse(tc.arguments).url || JSON.parse(tc.arguments).query; } catch { return ""; } })() };
|
| 278 |
+
}
|
| 279 |
+
if (tc.name === "read_file") {
|
| 280 |
+
return { type: "file" as const, name: tc.name, content: tc.result, path: (() => { try { return JSON.parse(tc.arguments).path; } catch { return ""; } })() };
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
return null;
|
| 287 |
+
})();
|
| 288 |
+
|
| 289 |
+
if (!latestPreviewable) {
|
| 290 |
+
return (
|
| 291 |
+
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/40 gap-2 p-4">
|
| 292 |
+
<Monitor className="size-8" />
|
| 293 |
+
<p className="text-xs text-center">
|
| 294 |
+
Live preview will appear here when the agent runs commands or fetches content
|
| 295 |
+
</p>
|
| 296 |
+
</div>
|
| 297 |
+
);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return (
|
| 301 |
+
<div className="h-full flex flex-col">
|
| 302 |
+
{/* Preview header */}
|
| 303 |
+
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-secondary/20">
|
| 304 |
+
{latestPreviewable.type === "terminal" && (
|
| 305 |
+
<>
|
| 306 |
+
<div className="flex gap-1">
|
| 307 |
+
<div className="size-2 rounded-full bg-red-500/70" />
|
| 308 |
+
<div className="size-2 rounded-full bg-yellow-500/70" />
|
| 309 |
+
<div className="size-2 rounded-full bg-green-500/70" />
|
| 310 |
+
</div>
|
| 311 |
+
<span className="text-[10px] font-mono text-muted-foreground truncate">
|
| 312 |
+
$ {latestPreviewable.command?.slice(0, 60)}
|
| 313 |
+
</span>
|
| 314 |
+
</>
|
| 315 |
+
)}
|
| 316 |
+
{latestPreviewable.type === "web" && (
|
| 317 |
+
<>
|
| 318 |
+
<ExternalLink className="size-3 text-muted-foreground" />
|
| 319 |
+
<span className="text-[10px] font-mono text-muted-foreground truncate">
|
| 320 |
+
{latestPreviewable.url}
|
| 321 |
+
</span>
|
| 322 |
+
</>
|
| 323 |
+
)}
|
| 324 |
+
{latestPreviewable.type === "file" && (
|
| 325 |
+
<>
|
| 326 |
+
<FileText className="size-3 text-muted-foreground" />
|
| 327 |
+
<span className="text-[10px] font-mono text-muted-foreground truncate">
|
| 328 |
+
{latestPreviewable.path}
|
| 329 |
+
</span>
|
| 330 |
+
</>
|
| 331 |
+
)}
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
{/* Preview content */}
|
| 335 |
+
<div className="flex-1 overflow-auto bg-[#0d1117]">
|
| 336 |
+
<pre className="text-[11px] font-mono p-3 whitespace-pre-wrap break-all text-green-400/90">
|
| 337 |
+
{latestPreviewable.content.length > 15000
|
| 338 |
+
? latestPreviewable.content.slice(0, 15000) + "\n\n[...truncated]"
|
| 339 |
+
: latestPreviewable.content}
|
| 340 |
+
</pre>
|
| 341 |
+
</div>
|
| 342 |
+
</div>
|
| 343 |
+
);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// βββ Main RightPanel ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 347 |
+
|
| 348 |
+
interface RightPanelProps {
|
| 349 |
+
messages: ChatMessage[];
|
| 350 |
+
isStreaming: boolean;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
export function RightPanel({ messages, isStreaming }: RightPanelProps) {
|
| 354 |
+
const [activeTab, setActiveTab] = useState<TabId>("actions");
|
| 355 |
+
|
| 356 |
+
return (
|
| 357 |
+
<div className="h-full flex flex-col bg-background">
|
| 358 |
+
{/* Tab bar */}
|
| 359 |
+
<div className="flex border-b border-border bg-secondary/10">
|
| 360 |
+
{TABS.map((tab) => {
|
| 361 |
+
const Icon = tab.icon;
|
| 362 |
+
const isActive = activeTab === tab.id;
|
| 363 |
+
return (
|
| 364 |
+
<button
|
| 365 |
+
key={tab.id}
|
| 366 |
+
onClick={() => setActiveTab(tab.id)}
|
| 367 |
+
className={cn(
|
| 368 |
+
"flex items-center gap-1.5 px-3 py-2 text-[11px] font-medium transition-colors border-b-2 -mb-px",
|
| 369 |
+
isActive
|
| 370 |
+
? "border-primary text-foreground"
|
| 371 |
+
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
|
| 372 |
+
)}
|
| 373 |
+
>
|
| 374 |
+
<Icon className="size-3.5" />
|
| 375 |
+
{tab.label}
|
| 376 |
+
</button>
|
| 377 |
+
);
|
| 378 |
+
})}
|
| 379 |
+
</div>
|
| 380 |
+
|
| 381 |
+
{/* Tab content */}
|
| 382 |
+
<div className="flex-1 overflow-hidden">
|
| 383 |
+
{activeTab === "actions" && (
|
| 384 |
+
<ActionTree messages={messages} isStreaming={isStreaming} />
|
| 385 |
+
)}
|
| 386 |
+
{activeTab === "files" && (
|
| 387 |
+
<FileExplorer />
|
| 388 |
+
)}
|
| 389 |
+
{activeTab === "preview" && (
|
| 390 |
+
<PreviewPanel messages={messages} />
|
| 391 |
+
)}
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
);
|
| 395 |
+
}
|
client/src/pages/Home.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { BuddySprite } from "@/components/BuddySprite";
|
|
| 16 |
import { useBuddy } from "@/buddy";
|
| 17 |
import { useChat, type ChatMessage } from "@/hooks/useChat";
|
| 18 |
import { ActionTree } from "@/components/ActionTree";
|
|
|
|
| 19 |
import {
|
| 20 |
ResizablePanelGroup,
|
| 21 |
ResizablePanel,
|
|
@@ -1004,7 +1005,7 @@ export default function Home() {
|
|
| 1004 |
<ResizableHandle withHandle />
|
| 1005 |
<ResizablePanel defaultSize={40} minSize={25} maxSize={55}>
|
| 1006 |
<div className="h-full bg-background border-l border-border">
|
| 1007 |
-
<
|
| 1008 |
</div>
|
| 1009 |
</ResizablePanel>
|
| 1010 |
</>
|
|
|
|
| 16 |
import { useBuddy } from "@/buddy";
|
| 17 |
import { useChat, type ChatMessage } from "@/hooks/useChat";
|
| 18 |
import { ActionTree } from "@/components/ActionTree";
|
| 19 |
+
import { RightPanel } from "@/components/RightPanel";
|
| 20 |
import {
|
| 21 |
ResizablePanelGroup,
|
| 22 |
ResizablePanel,
|
|
|
|
| 1005 |
<ResizableHandle withHandle />
|
| 1006 |
<ResizablePanel defaultSize={40} minSize={25} maxSize={55}>
|
| 1007 |
<div className="h-full bg-background border-l border-border">
|
| 1008 |
+
<RightPanel messages={messages} isStreaming={isStreaming} />
|
| 1009 |
</div>
|
| 1010 |
</ResizablePanel>
|
| 1011 |
</>
|
package.json
CHANGED
|
@@ -65,6 +65,7 @@
|
|
| 65 |
"input-otp": "^1.4.2",
|
| 66 |
"jose": "6.1.0",
|
| 67 |
"lucide-react": "^0.453.0",
|
|
|
|
| 68 |
"mysql2": "^3.15.0",
|
| 69 |
"nanoid": "^5.1.5",
|
| 70 |
"next-themes": "^0.4.6",
|
|
@@ -90,6 +91,7 @@
|
|
| 90 |
"@types/diff": "^8.0.0",
|
| 91 |
"@types/express": "4.17.21",
|
| 92 |
"@types/google.maps": "^3.58.1",
|
|
|
|
| 93 |
"@types/node": "^24.7.0",
|
| 94 |
"@types/react": "^19.2.1",
|
| 95 |
"@types/react-dom": "^19.2.1",
|
|
|
|
| 65 |
"input-otp": "^1.4.2",
|
| 66 |
"jose": "6.1.0",
|
| 67 |
"lucide-react": "^0.453.0",
|
| 68 |
+
"micromatch": "^4.0.8",
|
| 69 |
"mysql2": "^3.15.0",
|
| 70 |
"nanoid": "^5.1.5",
|
| 71 |
"next-themes": "^0.4.6",
|
|
|
|
| 91 |
"@types/diff": "^8.0.0",
|
| 92 |
"@types/express": "4.17.21",
|
| 93 |
"@types/google.maps": "^3.58.1",
|
| 94 |
+
"@types/micromatch": "^4.0.10",
|
| 95 |
"@types/node": "^24.7.0",
|
| 96 |
"@types/react": "^19.2.1",
|
| 97 |
"@types/react-dom": "^19.2.1",
|
pnpm-lock.yaml
CHANGED
|
@@ -167,6 +167,9 @@ importers:
|
|
| 167 |
lucide-react:
|
| 168 |
specifier: ^0.453.0
|
| 169 |
version: 0.453.0(react@19.2.4)
|
|
|
|
|
|
|
|
|
|
| 170 |
mysql2:
|
| 171 |
specifier: ^3.15.0
|
| 172 |
version: 3.20.0(@types/node@24.12.0)
|
|
@@ -237,6 +240,9 @@ importers:
|
|
| 237 |
'@types/google.maps':
|
| 238 |
specifier: ^3.58.1
|
| 239 |
version: 3.58.1
|
|
|
|
|
|
|
|
|
|
| 240 |
'@types/node':
|
| 241 |
specifier: ^24.7.0
|
| 242 |
version: 24.12.0
|
|
@@ -2375,6 +2381,9 @@ packages:
|
|
| 2375 |
'@types/body-parser@1.19.6':
|
| 2376 |
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
| 2377 |
|
|
|
|
|
|
|
|
|
|
| 2378 |
'@types/connect@3.4.38':
|
| 2379 |
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
| 2380 |
|
|
@@ -2512,6 +2521,9 @@ packages:
|
|
| 2512 |
'@types/mdast@4.0.4':
|
| 2513 |
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
| 2514 |
|
|
|
|
|
|
|
|
|
|
| 2515 |
'@types/ms@2.1.0':
|
| 2516 |
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
| 2517 |
|
|
@@ -2664,6 +2676,10 @@ packages:
|
|
| 2664 |
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
| 2665 |
engines: {node: 18 || 20 || >=22}
|
| 2666 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2667 |
browserslist@4.28.2:
|
| 2668 |
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
| 2669 |
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
|
@@ -3284,6 +3300,10 @@ packages:
|
|
| 3284 |
file-uri-to-path@1.0.0:
|
| 3285 |
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
| 3286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3287 |
finalhandler@1.3.2:
|
| 3288 |
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
| 3289 |
engines: {node: '>= 0.8'}
|
|
@@ -3501,6 +3521,10 @@ packages:
|
|
| 3501 |
is-hexadecimal@2.0.1:
|
| 3502 |
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
| 3503 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3504 |
is-plain-obj@4.1.0:
|
| 3505 |
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
| 3506 |
engines: {node: '>=12'}
|
|
@@ -3855,6 +3879,10 @@ packages:
|
|
| 3855 |
micromark@4.0.2:
|
| 3856 |
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
|
| 3857 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3858 |
mime-db@1.52.0:
|
| 3859 |
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
| 3860 |
engines: {node: '>= 0.6'}
|
|
@@ -4008,6 +4036,10 @@ packages:
|
|
| 4008 |
picocolors@1.1.1:
|
| 4009 |
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
| 4010 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4011 |
picomatch@4.0.4:
|
| 4012 |
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
| 4013 |
engines: {node: '>=12'}
|
|
@@ -4427,6 +4459,10 @@ packages:
|
|
| 4427 |
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
| 4428 |
engines: {node: '>=14.0.0'}
|
| 4429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4430 |
toidentifier@1.0.1:
|
| 4431 |
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
| 4432 |
engines: {node: '>=0.6'}
|
|
@@ -6932,6 +6968,8 @@ snapshots:
|
|
| 6932 |
'@types/connect': 3.4.38
|
| 6933 |
'@types/node': 24.12.0
|
| 6934 |
|
|
|
|
|
|
|
| 6935 |
'@types/connect@3.4.38':
|
| 6936 |
dependencies:
|
| 6937 |
'@types/node': 24.12.0
|
|
@@ -7101,6 +7139,10 @@ snapshots:
|
|
| 7101 |
dependencies:
|
| 7102 |
'@types/unist': 3.0.3
|
| 7103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7104 |
'@types/ms@2.1.0': {}
|
| 7105 |
|
| 7106 |
'@types/node@24.12.0':
|
|
@@ -7278,6 +7320,10 @@ snapshots:
|
|
| 7278 |
dependencies:
|
| 7279 |
balanced-match: 4.0.4
|
| 7280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7281 |
browserslist@4.28.2:
|
| 7282 |
dependencies:
|
| 7283 |
baseline-browser-mapping: 2.10.13
|
|
@@ -7904,6 +7950,10 @@ snapshots:
|
|
| 7904 |
|
| 7905 |
file-uri-to-path@1.0.0: {}
|
| 7906 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7907 |
finalhandler@1.3.2:
|
| 7908 |
dependencies:
|
| 7909 |
debug: 2.6.9
|
|
@@ -8186,6 +8236,8 @@ snapshots:
|
|
| 8186 |
|
| 8187 |
is-hexadecimal@2.0.1: {}
|
| 8188 |
|
|
|
|
|
|
|
| 8189 |
is-plain-obj@4.1.0: {}
|
| 8190 |
|
| 8191 |
is-property@1.0.2: {}
|
|
@@ -8739,6 +8791,11 @@ snapshots:
|
|
| 8739 |
transitivePeerDependencies:
|
| 8740 |
- supports-color
|
| 8741 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8742 |
mime-db@1.52.0: {}
|
| 8743 |
|
| 8744 |
mime-types@2.1.35:
|
|
@@ -8872,6 +8929,8 @@ snapshots:
|
|
| 8872 |
|
| 8873 |
picocolors@1.1.1: {}
|
| 8874 |
|
|
|
|
|
|
|
| 8875 |
picomatch@4.0.4: {}
|
| 8876 |
|
| 8877 |
pkg-types@1.3.1:
|
|
@@ -9417,6 +9476,10 @@ snapshots:
|
|
| 9417 |
|
| 9418 |
tinyspy@3.0.2: {}
|
| 9419 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9420 |
toidentifier@1.0.1: {}
|
| 9421 |
|
| 9422 |
trim-lines@3.0.1: {}
|
|
|
|
| 167 |
lucide-react:
|
| 168 |
specifier: ^0.453.0
|
| 169 |
version: 0.453.0(react@19.2.4)
|
| 170 |
+
micromatch:
|
| 171 |
+
specifier: ^4.0.8
|
| 172 |
+
version: 4.0.8
|
| 173 |
mysql2:
|
| 174 |
specifier: ^3.15.0
|
| 175 |
version: 3.20.0(@types/node@24.12.0)
|
|
|
|
| 240 |
'@types/google.maps':
|
| 241 |
specifier: ^3.58.1
|
| 242 |
version: 3.58.1
|
| 243 |
+
'@types/micromatch':
|
| 244 |
+
specifier: ^4.0.10
|
| 245 |
+
version: 4.0.10
|
| 246 |
'@types/node':
|
| 247 |
specifier: ^24.7.0
|
| 248 |
version: 24.12.0
|
|
|
|
| 2381 |
'@types/body-parser@1.19.6':
|
| 2382 |
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
| 2383 |
|
| 2384 |
+
'@types/braces@3.0.5':
|
| 2385 |
+
resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==}
|
| 2386 |
+
|
| 2387 |
'@types/connect@3.4.38':
|
| 2388 |
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
| 2389 |
|
|
|
|
| 2521 |
'@types/mdast@4.0.4':
|
| 2522 |
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
| 2523 |
|
| 2524 |
+
'@types/micromatch@4.0.10':
|
| 2525 |
+
resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==}
|
| 2526 |
+
|
| 2527 |
'@types/ms@2.1.0':
|
| 2528 |
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
| 2529 |
|
|
|
|
| 2676 |
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
| 2677 |
engines: {node: 18 || 20 || >=22}
|
| 2678 |
|
| 2679 |
+
braces@3.0.3:
|
| 2680 |
+
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
| 2681 |
+
engines: {node: '>=8'}
|
| 2682 |
+
|
| 2683 |
browserslist@4.28.2:
|
| 2684 |
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
| 2685 |
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
|
|
|
| 3300 |
file-uri-to-path@1.0.0:
|
| 3301 |
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
| 3302 |
|
| 3303 |
+
fill-range@7.1.1:
|
| 3304 |
+
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
| 3305 |
+
engines: {node: '>=8'}
|
| 3306 |
+
|
| 3307 |
finalhandler@1.3.2:
|
| 3308 |
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
| 3309 |
engines: {node: '>= 0.8'}
|
|
|
|
| 3521 |
is-hexadecimal@2.0.1:
|
| 3522 |
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
| 3523 |
|
| 3524 |
+
is-number@7.0.0:
|
| 3525 |
+
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
| 3526 |
+
engines: {node: '>=0.12.0'}
|
| 3527 |
+
|
| 3528 |
is-plain-obj@4.1.0:
|
| 3529 |
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
| 3530 |
engines: {node: '>=12'}
|
|
|
|
| 3879 |
micromark@4.0.2:
|
| 3880 |
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
|
| 3881 |
|
| 3882 |
+
micromatch@4.0.8:
|
| 3883 |
+
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
| 3884 |
+
engines: {node: '>=8.6'}
|
| 3885 |
+
|
| 3886 |
mime-db@1.52.0:
|
| 3887 |
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
| 3888 |
engines: {node: '>= 0.6'}
|
|
|
|
| 4036 |
picocolors@1.1.1:
|
| 4037 |
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
| 4038 |
|
| 4039 |
+
picomatch@2.3.2:
|
| 4040 |
+
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
| 4041 |
+
engines: {node: '>=8.6'}
|
| 4042 |
+
|
| 4043 |
picomatch@4.0.4:
|
| 4044 |
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
| 4045 |
engines: {node: '>=12'}
|
|
|
|
| 4459 |
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
| 4460 |
engines: {node: '>=14.0.0'}
|
| 4461 |
|
| 4462 |
+
to-regex-range@5.0.1:
|
| 4463 |
+
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
| 4464 |
+
engines: {node: '>=8.0'}
|
| 4465 |
+
|
| 4466 |
toidentifier@1.0.1:
|
| 4467 |
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
| 4468 |
engines: {node: '>=0.6'}
|
|
|
|
| 6968 |
'@types/connect': 3.4.38
|
| 6969 |
'@types/node': 24.12.0
|
| 6970 |
|
| 6971 |
+
'@types/braces@3.0.5': {}
|
| 6972 |
+
|
| 6973 |
'@types/connect@3.4.38':
|
| 6974 |
dependencies:
|
| 6975 |
'@types/node': 24.12.0
|
|
|
|
| 7139 |
dependencies:
|
| 7140 |
'@types/unist': 3.0.3
|
| 7141 |
|
| 7142 |
+
'@types/micromatch@4.0.10':
|
| 7143 |
+
dependencies:
|
| 7144 |
+
'@types/braces': 3.0.5
|
| 7145 |
+
|
| 7146 |
'@types/ms@2.1.0': {}
|
| 7147 |
|
| 7148 |
'@types/node@24.12.0':
|
|
|
|
| 7320 |
dependencies:
|
| 7321 |
balanced-match: 4.0.4
|
| 7322 |
|
| 7323 |
+
braces@3.0.3:
|
| 7324 |
+
dependencies:
|
| 7325 |
+
fill-range: 7.1.1
|
| 7326 |
+
|
| 7327 |
browserslist@4.28.2:
|
| 7328 |
dependencies:
|
| 7329 |
baseline-browser-mapping: 2.10.13
|
|
|
|
| 7950 |
|
| 7951 |
file-uri-to-path@1.0.0: {}
|
| 7952 |
|
| 7953 |
+
fill-range@7.1.1:
|
| 7954 |
+
dependencies:
|
| 7955 |
+
to-regex-range: 5.0.1
|
| 7956 |
+
|
| 7957 |
finalhandler@1.3.2:
|
| 7958 |
dependencies:
|
| 7959 |
debug: 2.6.9
|
|
|
|
| 8236 |
|
| 8237 |
is-hexadecimal@2.0.1: {}
|
| 8238 |
|
| 8239 |
+
is-number@7.0.0: {}
|
| 8240 |
+
|
| 8241 |
is-plain-obj@4.1.0: {}
|
| 8242 |
|
| 8243 |
is-property@1.0.2: {}
|
|
|
|
| 8791 |
transitivePeerDependencies:
|
| 8792 |
- supports-color
|
| 8793 |
|
| 8794 |
+
micromatch@4.0.8:
|
| 8795 |
+
dependencies:
|
| 8796 |
+
braces: 3.0.3
|
| 8797 |
+
picomatch: 2.3.2
|
| 8798 |
+
|
| 8799 |
mime-db@1.52.0: {}
|
| 8800 |
|
| 8801 |
mime-types@2.1.35:
|
|
|
|
| 8929 |
|
| 8930 |
picocolors@1.1.1: {}
|
| 8931 |
|
| 8932 |
+
picomatch@2.3.2: {}
|
| 8933 |
+
|
| 8934 |
picomatch@4.0.4: {}
|
| 8935 |
|
| 8936 |
pkg-types@1.3.1:
|
|
|
|
| 9476 |
|
| 9477 |
tinyspy@3.0.2: {}
|
| 9478 |
|
| 9479 |
+
to-regex-range@5.0.1:
|
| 9480 |
+
dependencies:
|
| 9481 |
+
is-number: 7.0.0
|
| 9482 |
+
|
| 9483 |
toidentifier@1.0.1: {}
|
| 9484 |
|
| 9485 |
trim-lines@3.0.1: {}
|
server/_core/index.ts
CHANGED
|
@@ -80,6 +80,30 @@ async function startServer() {
|
|
| 80 |
server.listen(port, "0.0.0.0", () => {
|
| 81 |
console.log(`Server running on http://0.0.0.0:${port}/`);
|
| 82 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
startServer().catch(console.error);
|
|
|
|
| 80 |
server.listen(port, "0.0.0.0", () => {
|
| 81 |
console.log(`Server running on http://0.0.0.0:${port}/`);
|
| 82 |
});
|
| 83 |
+
|
| 84 |
+
// βββ Graceful shutdown βββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
+
const shutdown = (signal: string) => {
|
| 86 |
+
console.log(`\n[server] ${signal} received β shutting down gracefully...`);
|
| 87 |
+
server.close(() => {
|
| 88 |
+
console.log("[server] HTTP server closed");
|
| 89 |
+
// Close database connections
|
| 90 |
+
try {
|
| 91 |
+
const { closeDb } = require("../db");
|
| 92 |
+
if (typeof closeDb === "function") closeDb();
|
| 93 |
+
} catch {}
|
| 94 |
+
console.log("[server] Cleanup complete, exiting");
|
| 95 |
+
process.exit(0);
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
// Force exit after 10 seconds if graceful shutdown hangs
|
| 99 |
+
setTimeout(() => {
|
| 100 |
+
console.error("[server] Forced exit after timeout");
|
| 101 |
+
process.exit(1);
|
| 102 |
+
}, 10_000).unref();
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
| 106 |
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
| 107 |
}
|
| 108 |
|
| 109 |
startServer().catch(console.error);
|
server/_core/llm.ts
CHANGED
|
@@ -341,6 +341,7 @@ export async function invokeLLM(params: InvokeParams): Promise<InvokeResult> {
|
|
| 341 |
export interface StreamInvokeParams {
|
| 342 |
messages: Message[];
|
| 343 |
tools?: Tool[];
|
|
|
|
| 344 |
model?: string;
|
| 345 |
maxTokens?: number;
|
| 346 |
temperature?: number;
|
|
@@ -363,7 +364,7 @@ export async function invokeLLMStream(params: StreamInvokeParams): Promise<globa
|
|
| 363 |
model,
|
| 364 |
messages: params.messages.map(normalizeMessage),
|
| 365 |
tools: params.tools,
|
| 366 |
-
tool_choice: "auto",
|
| 367 |
max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS,
|
| 368 |
temperature: params.temperature ?? 0.7,
|
| 369 |
top_p: params.topP ?? 1,
|
|
|
|
| 341 |
export interface StreamInvokeParams {
|
| 342 |
messages: Message[];
|
| 343 |
tools?: Tool[];
|
| 344 |
+
tool_choice?: ToolChoice;
|
| 345 |
model?: string;
|
| 346 |
maxTokens?: number;
|
| 347 |
temperature?: number;
|
|
|
|
| 364 |
model,
|
| 365 |
messages: params.messages.map(normalizeMessage),
|
| 366 |
tools: params.tools,
|
| 367 |
+
tool_choice: params.tool_choice || "auto",
|
| 368 |
max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS,
|
| 369 |
temperature: params.temperature ?? 0.7,
|
| 370 |
top_p: params.topP ?? 1,
|
server/db.ts
CHANGED
|
@@ -397,3 +397,18 @@ export function deleteSessionState(sessionId: number, key?: string): void {
|
|
| 397 |
console.error(`[db] Failed to delete session state:`, err.message);
|
| 398 |
}
|
| 399 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
console.error(`[db] Failed to delete session state:`, err.message);
|
| 398 |
}
|
| 399 |
}
|
| 400 |
+
|
| 401 |
+
// βββ Graceful shutdown ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 402 |
+
|
| 403 |
+
export function closeDb() {
|
| 404 |
+
if (_sqlite) {
|
| 405 |
+
try {
|
| 406 |
+
_sqlite.close();
|
| 407 |
+
console.log("[db] SQLite connection closed");
|
| 408 |
+
} catch (err) {
|
| 409 |
+
console.error("[db] Error closing SQLite:", err);
|
| 410 |
+
}
|
| 411 |
+
_sqlite = null;
|
| 412 |
+
_db = null;
|
| 413 |
+
}
|
| 414 |
+
}
|
server/runtime/permissions.ts
CHANGED
|
@@ -154,19 +154,15 @@ export class PermissionPolicy {
|
|
| 154 |
}
|
| 155 |
}
|
| 156 |
|
|
|
|
|
|
|
| 157 |
// βββ Glob matching for tool names ββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
|
| 159 |
export function globMatchToolName(
|
| 160 |
pattern: string,
|
| 161 |
toolName: string
|
| 162 |
): boolean {
|
| 163 |
-
|
| 164 |
-
const regexStr = pattern
|
| 165 |
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
| 166 |
-
.replace(/\*/g, ".*")
|
| 167 |
-
.replace(/\?/g, ".");
|
| 168 |
-
const regex = new RegExp(`^${regexStr}$`);
|
| 169 |
-
return regex.test(toolName);
|
| 170 |
}
|
| 171 |
|
| 172 |
// βββ Glob matching for file paths ββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -175,17 +171,7 @@ export function globMatchPath(pattern: string, filePath: string): boolean {
|
|
| 175 |
// Normalize: remove trailing slashes
|
| 176 |
const normalizedPath = filePath.replace(/\/+$/, "");
|
| 177 |
const normalizedPattern = pattern.replace(/\/+$/, "");
|
| 178 |
-
|
| 179 |
-
// Convert glob pattern to regex
|
| 180 |
-
let regexStr = normalizedPattern
|
| 181 |
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
| 182 |
-
.replace(/\*\*\//g, "(?:.*\/)?")
|
| 183 |
-
.replace(/\/\*\*/g, "(?:\/.*)?")
|
| 184 |
-
.replace(/\*\*/g, ".*")
|
| 185 |
-
.replace(/\*/g, "[^/]*")
|
| 186 |
-
.replace(/\?/g, "[^/]");
|
| 187 |
-
const regex = new RegExp(`^${regexStr}$`);
|
| 188 |
-
return regex.test(normalizedPath);
|
| 189 |
}
|
| 190 |
|
| 191 |
// βββ Permission prompt text ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 154 |
}
|
| 155 |
}
|
| 156 |
|
| 157 |
+
import micromatch from "micromatch";
|
| 158 |
+
|
| 159 |
// βββ Glob matching for tool names ββββββββββββββββββββββββββββββββββββββββββββ
|
| 160 |
|
| 161 |
export function globMatchToolName(
|
| 162 |
pattern: string,
|
| 163 |
toolName: string
|
| 164 |
): boolean {
|
| 165 |
+
return micromatch.isMatch(toolName, pattern);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
// βββ Glob matching for file paths ββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 171 |
// Normalize: remove trailing slashes
|
| 172 |
const normalizedPath = filePath.replace(/\/+$/, "");
|
| 173 |
const normalizedPattern = pattern.replace(/\/+$/, "");
|
| 174 |
+
return micromatch.isMatch(normalizedPath, normalizedPattern, { dot: true });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
// βββ Permission prompt text ββββββββββββββββββββββββββββββββββββββββββββββββββ
|