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 });
|
||||
|
||||
Reference in new issue
Block a user