batch3 T8: chat->file click, Session.tsx rewires to Workspace, sidebar polish

- MessageBubble & ToolCallCard: detect path-like strings in rendered text
  via regex requiring slash+extension; clicks dispatch open_file_in_browser
- Session.tsx: now renders <Workspace sessionId projectId>; on mount,
  emits session_loaded so sidebar can highlight even deep-linked sessions
  not in the recent_sessions cache
- ProjectSidebar: active project's chevron visually disabled (50% opacity,
  cursor-not-allowed) and click no-op; activeSession from useSidebar used
  as fallback when active session isn't in cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:55:52 +00:00
parent 60a0036850
commit eca4aa8382
4 changed files with 174 additions and 40 deletions

View File

@@ -1,12 +1,50 @@
import { useState } from 'react';
import type { ReactNode } from 'react';
import { ChevronRight, Wrench } from 'lucide-react';
import type { Message, ToolCall } from '@/api/types';
import { sessionEvents } from '@/hooks/sessionEvents';
interface Props {
message?: Message;
toolCall?: ToolCall;
}
// Same regex/heuristic as MessageBubble: paths ending in `.ext` with at
// least one `/`. Linkifies file paths emitted by tools like grep / find_files
// so they're clickable.
const PATH_REGEX = /([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/g;
function linkifyOutput(text: string): ReactNode[] {
const out: ReactNode[] = [];
let lastIdx = 0;
let idx = 0;
for (const match of text.matchAll(PATH_REGEX)) {
const matchedText = match[0];
const start = match.index ?? 0;
if (!matchedText.includes('/')) continue;
if (start > lastIdx) out.push(text.slice(lastIdx, start));
out.push(
<button
key={idx}
type="button"
onClick={() =>
sessionEvents.emit({
type: 'open_file_in_browser',
path: matchedText,
})
}
className="text-primary underline cursor-pointer hover:text-primary/80"
>
{matchedText}
</button>
);
lastIdx = start + matchedText.length;
idx += 1;
}
if (lastIdx < text.length) out.push(text.slice(lastIdx));
return out.length > 0 ? out : [text];
}
export function ToolCallCard({ message, toolCall }: Props) {
const [open, setOpen] = useState(false);
const tc = toolCall ?? message?.tool_calls?.[0];
@@ -48,7 +86,11 @@ export function ToolCallCard({ message, toolCall }: Props) {
</pre>
) : output !== undefined ? (
<pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto max-h-72 overflow-y-auto">
{typeof output === 'string' ? output : JSON.stringify(output, null, 2)}
{linkifyOutput(
typeof output === 'string'
? output
: JSON.stringify(output, null, 2)
)}
</pre>
) : (
<div className="text-xs text-muted-foreground">no result yet</div>