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:
7418 committed 2026-02-24 12:32:03 +08:00
1 parent 8ee8085930
commit 9e7ccfefaa
27 files changed
+622 -212

No files matched your search

+29
View File
@@ -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) });
}
}
+1
View File
@@ -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 = {
+29
View File
@@ -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 }));
}
});
});
}
+10 -8
View File
@@ -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 },
+3 -2
View File
@@ -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 });