Skip to content

Commit 9e7ccfe

Browse files
author
7418
committed
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)
1 parent 8ee8085 commit 9e7ccfe

27 files changed

+621
-211
lines changed

electron/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, BrowserWindow, nativeImage, dialog, session, utilityProcess, ipcMain } from 'electron';
1+
import { app, BrowserWindow, nativeImage, dialog, session, utilityProcess, ipcMain, shell } from 'electron';
22
import path from 'path';
33
import { execFileSync, spawn, ChildProcess } from 'child_process';
44
import fs from 'fs';
@@ -707,6 +707,11 @@ app.whenReady().then(async () => {
707707

708708
// --- End install wizard IPC handlers ---
709709

710+
// Open a folder in the system file manager (Finder / Explorer)
711+
ipcMain.handle('shell:open-path', async (_event: Electron.IpcMainInvokeEvent, folderPath: string) => {
712+
return shell.openPath(folderPath);
713+
});
714+
710715
try {
711716
let port: number;
712717

electron/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
77
node: process.versions.node,
88
chrome: process.versions.chrome,
99
},
10+
shell: {
11+
openPath: (folderPath: string) => ipcRenderer.invoke('shell:open-path', folderPath),
12+
},
1013
install: {
1114
checkPrerequisites: () => ipcRenderer.invoke('install:check-prerequisites'),
1215
start: (options?: { includeNode?: boolean }) => ipcRenderer.invoke('install:start', options),

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.11.0",
3+
"version": "0.12.0",
44
"private": true,
55
"author": {
66
"name": "op7418",

src/app/api/chat/mode/route.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getConversation } from '@/lib/conversation-registry';
3+
import type { PermissionMode } from '@anthropic-ai/claude-agent-sdk';
4+
5+
export const runtime = 'nodejs';
6+
export const dynamic = 'force-dynamic';
7+
8+
export async function POST(request: NextRequest) {
9+
try {
10+
const { sessionId, mode } = await request.json();
11+
12+
if (!sessionId || !mode) {
13+
return NextResponse.json({ error: 'sessionId and mode are required' }, { status: 400 });
14+
}
15+
16+
const conversation = getConversation(sessionId);
17+
if (!conversation) {
18+
return NextResponse.json({ applied: false });
19+
}
20+
21+
const permissionMode: PermissionMode = mode === 'code' ? 'acceptEdits' : 'plan';
22+
await conversation.setPermissionMode(permissionMode);
23+
24+
return NextResponse.json({ applied: true });
25+
} catch (error) {
26+
console.error('[mode] Failed to switch mode:', error);
27+
return NextResponse.json({ applied: false, error: String(error) });
28+
}
29+
}

src/app/api/chat/permission/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function POST(request: NextRequest) {
2323
result = {
2424
behavior: 'allow',
2525
updatedPermissions: decision.updatedPermissions as unknown as PermissionUpdate[],
26+
...(decision.updatedInput ? { updatedInput: decision.updatedInput } : {}),
2627
};
2728
} else {
2829
result = {

src/app/api/files/open/route.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { exec } from 'child_process';
3+
4+
export async function POST(req: NextRequest) {
5+
const { path } = await req.json();
6+
if (!path || typeof path !== 'string') {
7+
return NextResponse.json({ error: 'Missing path' }, { status: 400 });
8+
}
9+
10+
const platform = process.platform;
11+
let cmd: string;
12+
if (platform === 'darwin') {
13+
cmd = `open "${path}"`;
14+
} else if (platform === 'win32') {
15+
cmd = `explorer "${path}"`;
16+
} else {
17+
cmd = `xdg-open "${path}"`;
18+
}
19+
20+
return new Promise<NextResponse>((resolve) => {
21+
exec(cmd, (err) => {
22+
if (err) {
23+
resolve(NextResponse.json({ error: err.message }, { status: 500 }));
24+
} else {
25+
resolve(NextResponse.json({ ok: true }));
26+
}
27+
});
28+
});
29+
}

src/app/api/skills/[name]/route.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ function getGlobalCommandsDir(): string {
88
return path.join(os.homedir(), ".claude", "commands");
99
}
1010

11-
function getProjectCommandsDir(): string {
12-
return path.join(process.cwd(), ".claude", "commands");
11+
function getProjectCommandsDir(cwd?: string): string {
12+
return path.join(cwd || process.cwd(), ".claude", "commands");
1313
}
1414

1515
function getInstalledSkillsDir(): string {
@@ -149,7 +149,7 @@ function findInstalledSkillMatches(
149149

150150
function findSkillFile(
151151
name: string,
152-
options?: { installedSource?: InstalledSource; installedOnly?: boolean }
152+
options?: { installedSource?: InstalledSource; installedOnly?: boolean; cwd?: string }
153153
):
154154
| SkillMatch
155155
| { conflict: true; sources: InstalledSource[] }
@@ -158,7 +158,7 @@ function findSkillFile(
158158

159159
if (!options?.installedOnly) {
160160
// Check project first, then global, then installed (~/.agents/skills/ and ~/.claude/skills/)
161-
const projectPath = path.join(getProjectCommandsDir(), `${name}.md`);
161+
const projectPath = path.join(getProjectCommandsDir(options?.cwd), `${name}.md`);
162162
if (fs.existsSync(projectPath)) {
163163
return { filePath: projectPath, source: "project" };
164164
}
@@ -211,6 +211,7 @@ export async function GET(
211211
const { name } = await params;
212212
const url = new URL(_request.url);
213213
const sourceParam = url.searchParams.get("source");
214+
const cwdParam = url.searchParams.get("cwd") || undefined;
214215
const installedSource =
215216
sourceParam === "agents" || sourceParam === "claude"
216217
? (sourceParam as InstalledSource)
@@ -223,8 +224,8 @@ export async function GET(
223224
}
224225

225226
const found = installedSource
226-
? findSkillFile(name, { installedSource, installedOnly: true })
227-
: findSkillFile(name);
227+
? findSkillFile(name, { installedSource, installedOnly: true, cwd: cwdParam })
228+
: findSkillFile(name, { cwd: cwdParam });
228229
if (found && "conflict" in found) {
229230
return NextResponse.json(
230231
{ error: "Multiple skills with different content", sources: found.sources },
@@ -327,6 +328,7 @@ export async function DELETE(
327328
const { name } = await params;
328329
const url = new URL(_request.url);
329330
const sourceParam = url.searchParams.get("source");
331+
const cwdParam = url.searchParams.get("cwd") || undefined;
330332
const installedSource =
331333
sourceParam === "agents" || sourceParam === "claude"
332334
? (sourceParam as InstalledSource)
@@ -339,8 +341,8 @@ export async function DELETE(
339341
}
340342

341343
const found = installedSource
342-
? findSkillFile(name, { installedSource, installedOnly: true })
343-
: findSkillFile(name);
344+
? findSkillFile(name, { installedSource, installedOnly: true, cwd: cwdParam })
345+
: findSkillFile(name, { cwd: cwdParam });
344346
if (found && "conflict" in found) {
345347
return NextResponse.json(
346348
{ error: "Multiple skills with different content", sources: found.sources },

src/app/api/skills/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,11 @@ export async function GET(request: NextRequest) {
283283
export async function POST(request: Request) {
284284
try {
285285
const body = await request.json();
286-
const { name, content, scope } = body as {
286+
const { name, content, scope, cwd } = body as {
287287
name: string;
288288
content: string;
289289
scope: "global" | "project";
290+
cwd?: string;
290291
};
291292

292293
if (!name || typeof name !== "string") {
@@ -306,7 +307,7 @@ export async function POST(request: Request) {
306307
}
307308

308309
const dir =
309-
scope === "project" ? getProjectCommandsDir() : getGlobalCommandsDir();
310+
scope === "project" ? getProjectCommandsDir(cwd) : getGlobalCommandsDir();
310311

311312
if (!fs.existsSync(dir)) {
312313
fs.mkdirSync(dir, { recursive: true });

src/app/chat/[id]/page.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ChatView } from '@/components/chat/ChatView';
77
import { HugeiconsIcon } from "@hugeicons/react";
88
import { Loading02Icon, PencilEdit01Icon } from "@hugeicons/core-free-icons";
99
import { Input } from '@/components/ui/input';
10+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
1011
import { usePanel } from '@/hooks/usePanel';
1112

1213
interface ChatSessionPageProps {
@@ -23,6 +24,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
2324
const [sessionModel, setSessionModel] = useState<string>('');
2425
const [sessionMode, setSessionMode] = useState<string>('');
2526
const [projectName, setProjectName] = useState<string>('');
27+
const [sessionWorkingDir, setSessionWorkingDir] = useState<string>('');
2628
const [isEditingTitle, setIsEditingTitle] = useState(false);
2729
const [editTitle, setEditTitle] = useState('');
2830
const titleInputRef = useRef<HTMLInputElement>(null);
@@ -80,6 +82,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
8082
const data: { session: ChatSession } = await res.json();
8183
if (data.session.working_directory) {
8284
setWorkingDirectory(data.session.working_directory);
85+
setSessionWorkingDir(data.session.working_directory);
8386
localStorage.setItem("codepilot:last-working-directory", data.session.working_directory);
8487
window.dispatchEvent(new Event('refresh-file-tree'));
8588
}
@@ -168,7 +171,34 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
168171
>
169172
{projectName && (
170173
<>
171-
<span className="text-xs text-muted-foreground shrink-0">{projectName}</span>
174+
<Tooltip>
175+
<TooltipTrigger asChild>
176+
<button
177+
className="text-xs text-muted-foreground shrink-0 hover:text-foreground transition-colors cursor-pointer"
178+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
179+
onClick={() => {
180+
if (sessionWorkingDir) {
181+
const w = window as unknown as { electronAPI?: { shell?: { openPath: (p: string) => void } } };
182+
if (w.electronAPI?.shell?.openPath) {
183+
w.electronAPI.shell.openPath(sessionWorkingDir);
184+
} else {
185+
fetch('/api/files/open', {
186+
method: 'POST',
187+
headers: { 'Content-Type': 'application/json' },
188+
body: JSON.stringify({ path: sessionWorkingDir }),
189+
}).catch(() => {});
190+
}
191+
}
192+
}}
193+
>
194+
{projectName}
195+
</button>
196+
</TooltipTrigger>
197+
<TooltipContent>
198+
<p className="text-xs break-all">{sessionWorkingDir || projectName}</p>
199+
<p className="text-[10px] text-muted-foreground mt-0.5">Click to open in Finder</p>
200+
</TooltipContent>
201+
</Tooltip>
172202
<span className="text-xs text-muted-foreground shrink-0">/</span>
173203
</>
174204
)}

0 commit comments

Comments
 (0)