feat: v0.12.0 — fix streaming tool UI persistence, file tree UX improvements, unify icon library
Streaming tool UI fix (ChatView.tsx): - Add toolUsesRef/toolResultsRef to reliably track tool data across closures - Serialize tool_use/tool_result into JSON content-blocks array when building assistant message after stream ends, matching backend storage format - MessageItem.parseToolBlocks() now renders tool UI from history messages - Clean up refs in finally block and timeout auto-retry path File tree UX (FileTree.tsx, file-tree.tsx): - Remove folder path header from right panel, merge search + refresh into one row - Remove file icon spacer (size-5) so file icons align flush with search box - Change folder icon color from blue to muted-foreground Chat list panel (ChatListPanel.tsx): - Folder header: expanded uses FolderOpenIcon, collapsed uses Folder01Icon - Add tooltip showing full working directory path on folder hover - Double-click folder header opens in system file manager Title bar folder name (chat/[id]/page.tsx): - Add tooltip with full path on project name hover - Click opens folder in Finder/Explorer via Electron IPC or API fallback Open folder support: - electron/main.ts: add shell:open-path IPC handler via shell.openPath - electron/preload.ts: expose electronAPI.shell.openPath to renderer - api/files/open/route.ts: fallback API route using open/explorer/xdg-open Unify icon library — migrate app-level components from Lucide to Hugeicons: - file-tree.tsx: folder, file, chevron, plus icons - tool-actions-group.tsx: chevron expand icon - MessageItem.tsx: copy, check, expand/collapse icons - MessageInput.tsx: stop button (SquareIcon → StopIcon) - FileCard.tsx: file icon - ImageLightbox.tsx: left/right navigation arrows - InstallWizard.tsx: status icons (check, x, minus, loader, circle, copy, download) - Keep Lucide in ui/ and ai-elements/ base layers (shadcn standard)
This commit is contained in:
1 parent
8ee8085930
commit
9e7ccfefaa
27 files changed
+622
-212
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getConversation } from '@/lib/conversation-registry';
|
||||
import type { PermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { sessionId, mode } = await request.json();
|
||||
|
||||
if (!sessionId || !mode) {
|
||||
return NextResponse.json({ error: 'sessionId and mode are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const conversation = getConversation(sessionId);
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ applied: false });
|
||||
}
|
||||
|
||||
const permissionMode: PermissionMode = mode === 'code' ? 'acceptEdits' : 'plan';
|
||||
await conversation.setPermissionMode(permissionMode);
|
||||
|
||||
return NextResponse.json({ applied: true });
|
||||
} catch (error) {
|
||||
console.error('[mode] Failed to switch mode:', error);
|
||||
return NextResponse.json({ applied: false, error: String(error) });
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
result = {
|
||||
behavior: 'allow',
|
||||
updatedPermissions: decision.updatedPermissions as unknown as PermissionUpdate[],
|
||||
...(decision.updatedInput ? { updatedInput: decision.updatedInput } : {}),
|
||||
};
|
||||
} else {
|
||||
result = {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { path } = await req.json();
|
||||
if (!path || typeof path !== 'string') {
|
||||
return NextResponse.json({ error: 'Missing path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const platform = process.platform;
|
||||
let cmd: string;
|
||||
if (platform === 'darwin') {
|
||||
cmd = `open "${path}"`;
|
||||
} else if (platform === 'win32') {
|
||||
cmd = `explorer "${path}"`;
|
||||
} else {
|
||||
cmd = `xdg-open "${path}"`;
|
||||
}
|
||||
|
||||
return new Promise<NextResponse>((resolve) => {
|
||||
exec(cmd, (err) => {
|
||||
if (err) {
|
||||
resolve(NextResponse.json({ error: err.message }, { status: 500 }));
|
||||
} else {
|
||||
resolve(NextResponse.json({ ok: true }));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -8,8 +8,8 @@ function getGlobalCommandsDir(): string {
|
||||
return path.join(os.homedir(), ".claude", "commands");
|
||||
}
|
||||
|
||||
function getProjectCommandsDir(): string {
|
||||
return path.join(process.cwd(), ".claude", "commands");
|
||||
function getProjectCommandsDir(cwd?: string): string {
|
||||
return path.join(cwd || process.cwd(), ".claude", "commands");
|
||||
}
|
||||
|
||||
function getInstalledSkillsDir(): string {
|
||||
@@ -149,7 +149,7 @@ function findInstalledSkillMatches(
|
||||
|
||||
function findSkillFile(
|
||||
name: string,
|
||||
options?: { installedSource?: InstalledSource; installedOnly?: boolean }
|
||||
options?: { installedSource?: InstalledSource; installedOnly?: boolean; cwd?: string }
|
||||
):
|
||||
| SkillMatch
|
||||
| { conflict: true; sources: InstalledSource[] }
|
||||
@@ -158,7 +158,7 @@ function findSkillFile(
|
||||
|
||||
if (!options?.installedOnly) {
|
||||
// Check project first, then global, then installed (~/.agents/skills/ and ~/.claude/skills/)
|
||||
const projectPath = path.join(getProjectCommandsDir(), `${name}.md`);
|
||||
const projectPath = path.join(getProjectCommandsDir(options?.cwd), `${name}.md`);
|
||||
if (fs.existsSync(projectPath)) {
|
||||
return { filePath: projectPath, source: "project" };
|
||||
}
|
||||
@@ -211,6 +211,7 @@ export async function GET(
|
||||
const { name } = await params;
|
||||
const url = new URL(_request.url);
|
||||
const sourceParam = url.searchParams.get("source");
|
||||
const cwdParam = url.searchParams.get("cwd") || undefined;
|
||||
const installedSource =
|
||||
sourceParam === "agents" || sourceParam === "claude"
|
||||
? (sourceParam as InstalledSource)
|
||||
@@ -223,8 +224,8 @@ export async function GET(
|
||||
}
|
||||
|
||||
const found = installedSource
|
||||
? findSkillFile(name, { installedSource, installedOnly: true })
|
||||
: findSkillFile(name);
|
||||
? findSkillFile(name, { installedSource, installedOnly: true, cwd: cwdParam })
|
||||
: findSkillFile(name, { cwd: cwdParam });
|
||||
if (found && "conflict" in found) {
|
||||
return NextResponse.json(
|
||||
{ error: "Multiple skills with different content", sources: found.sources },
|
||||
@@ -327,6 +328,7 @@ export async function DELETE(
|
||||
const { name } = await params;
|
||||
const url = new URL(_request.url);
|
||||
const sourceParam = url.searchParams.get("source");
|
||||
const cwdParam = url.searchParams.get("cwd") || undefined;
|
||||
const installedSource =
|
||||
sourceParam === "agents" || sourceParam === "claude"
|
||||
? (sourceParam as InstalledSource)
|
||||
@@ -339,8 +341,8 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
const found = installedSource
|
||||
? findSkillFile(name, { installedSource, installedOnly: true })
|
||||
: findSkillFile(name);
|
||||
? findSkillFile(name, { installedSource, installedOnly: true, cwd: cwdParam })
|
||||
: findSkillFile(name, { cwd: cwdParam });
|
||||
if (found && "conflict" in found) {
|
||||
return NextResponse.json(
|
||||
{ error: "Multiple skills with different content", sources: found.sources },
|
||||
|
||||
@@ -283,10 +283,11 @@ export async function GET(request: NextRequest) {
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, content, scope } = body as {
|
||||
const { name, content, scope, cwd } = body as {
|
||||
name: string;
|
||||
content: string;
|
||||
scope: "global" | "project";
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
if (!name || typeof name !== "string") {
|
||||
@@ -306,7 +307,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const dir =
|
||||
scope === "project" ? getProjectCommandsDir() : getGlobalCommandsDir();
|
||||
scope === "project" ? getProjectCommandsDir(cwd) : getGlobalCommandsDir();
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ChatView } from '@/components/chat/ChatView';
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Loading02Icon, PencilEdit01Icon } from "@hugeicons/core-free-icons";
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { usePanel } from '@/hooks/usePanel';
|
||||
|
||||
interface ChatSessionPageProps {
|
||||
@@ -23,6 +24,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
|
||||
const [sessionModel, setSessionModel] = useState<string>('');
|
||||
const [sessionMode, setSessionMode] = useState<string>('');
|
||||
const [projectName, setProjectName] = useState<string>('');
|
||||
const [sessionWorkingDir, setSessionWorkingDir] = useState<string>('');
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editTitle, setEditTitle] = useState('');
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -80,6 +82,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
|
||||
const data: { session: ChatSession } = await res.json();
|
||||
if (data.session.working_directory) {
|
||||
setWorkingDirectory(data.session.working_directory);
|
||||
setSessionWorkingDir(data.session.working_directory);
|
||||
localStorage.setItem("codepilot:last-working-directory", data.session.working_directory);
|
||||
window.dispatchEvent(new Event('refresh-file-tree'));
|
||||
}
|
||||
@@ -168,7 +171,34 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
|
||||
>
|
||||
{projectName && (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{projectName}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="text-xs text-muted-foreground shrink-0 hover:text-foreground transition-colors cursor-pointer"
|
||||
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
|
||||
onClick={() => {
|
||||
if (sessionWorkingDir) {
|
||||
const w = window as unknown as { electronAPI?: { shell?: { openPath: (p: string) => void } } };
|
||||
if (w.electronAPI?.shell?.openPath) {
|
||||
w.electronAPI.shell.openPath(sessionWorkingDir);
|
||||
} else {
|
||||
fetch('/api/files/open', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: sessionWorkingDir }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{projectName}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs break-all">{sessionWorkingDir || projectName}</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">Click to open in Finder</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-xs text-muted-foreground shrink-0">/</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user