Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { coderFixtures } from '@/stories/fixtures/coder-fixtures'
import { directoryFixtures } from '@/stories/fixtures/directory-fixtures'
import { engineFixtures } from '@/stories/fixtures/engine-fixtures'
import { sandboxFixtures } from '@/stories/fixtures/sandbox-fixtures'
import { shellFixtures } from '@/stories/fixtures/shell-fixtures'
import { webFixtures } from '@/stories/fixtures/web-fixtures'
import { workerFixtures } from '@/stories/fixtures/worker-fixtures'
import type { FunctionCallMessage as FCallType } from '@/types/chat'
Expand Down Expand Up @@ -125,6 +126,11 @@ export const SandboxFamily: Story = {
render: () => <FamilyGallery fixtures={sandboxFixtures} />,
}

export const ShellFamily: Story = {
name: 'shell family',
render: () => <FamilyGallery fixtures={shellFixtures} />,
}

export const DirectoryFamily: Story = {
name: 'directory family',
render: () => <FamilyGallery fixtures={directoryFixtures} />,
Expand Down
10 changes: 8 additions & 2 deletions console/web/src/components/chat/FunctionCallMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SandboxFunctionIdLabel,
SandboxToolView,
} from '@/components/chat/sandbox'
import { ShellFunctionIdLabel, ShellToolView } from '@/components/chat/shell'
import { WebFunctionIdLabel, WebToolView } from '@/components/chat/web'
import { WorkerFunctionIdLabel, WorkerToolView } from '@/components/chat/worker'
import { AlwaysAllowButton } from '@/components/permissions/AlwaysAllowButton'
Expand Down Expand Up @@ -118,6 +119,9 @@ function FunctionIdLabel({ functionId }: { functionId: string }) {
if (SandboxToolView.isSandboxFunction(functionId)) {
return <SandboxFunctionIdLabel functionId={functionId} />
}
if (ShellToolView.isShellFunction(functionId)) {
return <ShellFunctionIdLabel functionId={functionId} />
}
return <span className="text-ink">{functionId}</span>
}

Expand All @@ -144,14 +148,16 @@ export function FunctionCallMessage({
DirectoryToolView.tryRenderPreview(message) ??
WorkerToolView.tryRenderPreview(message) ??
WebToolView.tryRenderPreview(message) ??
CoderToolView.tryRenderPreview(message)
CoderToolView.tryRenderPreview(message) ??
ShellToolView.tryRenderPreview(message)
const customTerminal = !pending
? (SandboxToolView.tryRender(message) ??
EngineToolView.tryRender(message) ??
DirectoryToolView.tryRender(message) ??
WorkerToolView.tryRender(message) ??
WebToolView.tryRender(message) ??
CoderToolView.tryRender(message))
CoderToolView.tryRender(message) ??
ShellToolView.tryRender(message))
: null
const hasCustomTerminal = customTerminal != null
const showRequestPaneAbove =
Expand Down
79 changes: 51 additions & 28 deletions console/web/src/components/chat/sandbox/FsGrepView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { renderWithHighlight } from './highlight'
import {
type FsMatch,
fsGrepRequestSchema,
fsGrepResponseSchema,
safeParseResponse,
Expand Down Expand Up @@ -35,35 +36,57 @@ export function FsGrepView({ input, output }: FsGrepViewProps) {
· no matches
</div>
) : (
<div className="font-mono text-[12px] leading-[1.55]">
{matches.map((m) => (
<div
/* path+line+content is collision-resistant in practice (two
hits on the same line of the same file produce identical
FsMatch records on the wire today). React will only warn
if the daemon ever sends true duplicates, which would
itself be a wire-shape bug worth surfacing. */
key={`${m.path}:${m.line}:${m.content}`}
className="border-b border-rule-2 last:border-b-0 px-3 py-1.5"
>
<div className="text-ink-faint">
<span className="text-accent">{m.path}</span>
<span className="text-ink-ghost">:</span>
<span className="tabular-nums">{m.line}</span>
</div>
<pre className="text-ink whitespace-pre-wrap break-words m-0 mt-0.5">
<code>
{/* sandbox grep patterns are always regexes on the wire */}
{renderWithHighlight(m.content, req.data.pattern, {
isRegex: true,
ignoreCase: !!req.data.ignore_case,
})}
</code>
</pre>
</div>
))}
</div>
<GrepMatchList
matches={matches}
pattern={req.data.pattern}
ignoreCase={!!req.data.ignore_case}
/>
)}
</div>
)
}

interface GrepMatchListProps {
matches: FsMatch[]
pattern: string
ignoreCase: boolean
}

/** Highlighted match list. Shared with the shell module's `fs::grep`
renderer — both wires speak `FsMatch` and regex-by-default patterns. */
export function GrepMatchList({
matches,
pattern,
ignoreCase,
}: GrepMatchListProps) {
return (
<div className="font-mono text-[12px] leading-[1.55]">
{matches.map((m) => (
<div
/* path+line+content is collision-resistant in practice (two
hits on the same line of the same file produce identical
FsMatch records on the wire today). React will only warn
if the daemon ever sends true duplicates, which would
itself be a wire-shape bug worth surfacing. */
key={`${m.path}:${m.line}:${m.content}`}
className="border-b border-rule-2 last:border-b-0 px-3 py-1.5"
>
<div className="text-ink-faint">
<span className="text-accent">{m.path}</span>
<span className="text-ink-ghost">:</span>
<span className="tabular-nums">{m.line}</span>
</div>
<pre className="text-ink whitespace-pre-wrap break-words m-0 mt-0.5">
<code>
{/* sandbox grep patterns are always regexes on the wire */}
{renderWithHighlight(m.content, pattern, {
isRegex: true,
ignoreCase,
})}
</code>
</pre>
</div>
))}
</div>
)
}
71 changes: 40 additions & 31 deletions console/web/src/components/chat/sandbox/FsLsView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { File, FileText, Folder, Link as LinkIcon } from 'lucide-react'
import { formatBytes, formatMode, formatMtime } from './format'
import {
type FsEntry,
fsLsRequestSchema,
fsLsResponseSchema,
safeParseResponse,
Expand Down Expand Up @@ -30,42 +31,50 @@ export function FsLsView({ input, output }: FsLsViewProps) {
· directory is empty
</div>
) : (
<table className="w-full font-mono text-[12px] text-ink">
<tbody>
{entries.map((e) => {
const Icon = e.is_symlink
? LinkIcon
: e.is_dir
? Folder
: iconForFile(e.name)
return (
<tr
key={`${e.name}:${e.size}:${e.mtime}`}
className="border-b border-rule-2 last:border-b-0"
>
<td className="pl-3 pr-1.5 py-1.5 w-5">
<Icon aria-hidden className="w-3.5 h-3.5 text-ink-faint" />
</td>
<td className="px-1.5 py-1.5 text-ink">{e.name}</td>
<td className="px-2 py-1.5 text-ink-faint tabular-nums text-right">
{e.is_dir ? '—' : formatBytes(e.size)}
</td>
<td className="px-2 py-1.5 text-ink-faint tabular-nums">
{`${e.is_dir ? 'd' : '-'}${formatMode(e.mode)}`}
</td>
<td className="pr-3 pl-2 py-1.5 text-ink-faint">
{formatMtime(e.mtime)}
</td>
</tr>
)
})}
</tbody>
</table>
<FsEntriesTable entries={entries} />
)}
</div>
)
}

/** Directory-listing table. Shared with the shell module's `fs::ls`
renderer — both wires speak `FsEntry`. */
export function FsEntriesTable({ entries }: { entries: FsEntry[] }) {
return (
<table className="w-full font-mono text-[12px] text-ink">
<tbody>
{entries.map((e) => {
const Icon = e.is_symlink
? LinkIcon
: e.is_dir
? Folder
: iconForFile(e.name)
return (
<tr
key={`${e.name}:${e.size}:${e.mtime}`}
className="border-b border-rule-2 last:border-b-0"
>
<td className="pl-3 pr-1.5 py-1.5 w-5">
<Icon aria-hidden className="w-3.5 h-3.5 text-ink-faint" />
</td>
<td className="px-1.5 py-1.5 text-ink">{e.name}</td>
<td className="px-2 py-1.5 text-ink-faint tabular-nums text-right">
{e.is_dir ? '—' : formatBytes(e.size)}
</td>
<td className="px-2 py-1.5 text-ink-faint tabular-nums">
{`${e.is_dir ? 'd' : '-'}${formatMode(e.mode)}`}
</td>
<td className="pr-3 pl-2 py-1.5 text-ink-faint">
{formatMtime(e.mtime)}
</td>
</tr>
)
})}
</tbody>
</table>
)
}

function iconForFile(name: string) {
const lower = name.toLowerCase()
if (/\.(md|txt|json|yml|yaml|toml|csv|log)$/.test(lower)) return FileText
Expand Down
121 changes: 68 additions & 53 deletions console/web/src/components/chat/sandbox/FsSedView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TooltipTrigger,
} from '@/components/ui/Tooltip'
import {
type FsSedFileResult,
fsSedRequestSchema,
fsSedResponseSchema,
safeParseResponse,
Expand Down Expand Up @@ -42,60 +43,74 @@ export function FsSedView({ input, output }: FsSedViewProps) {
· no files touched
</div>
) : (
<table className="w-full font-mono text-[12px] text-ink">
<thead>
<tr className="border-b border-rule-2 text-[11px] uppercase tracking-[0.06em] text-ink-faint">
<th className="text-left font-normal px-3 py-1.5">path</th>
<th className="text-left font-normal px-2 py-1.5 w-20 tabular-nums">
replacements
</th>
<th className="text-left font-normal px-3 py-1.5 w-8">status</th>
</tr>
</thead>
<tbody>
{results.map((r) => (
<tr
key={r.path}
className="border-b border-rule-2 last:border-b-0"
>
<td className="px-3 py-1.5 text-ink">{r.path}</td>
<td className="px-2 py-1.5 text-ink-faint tabular-nums">
{r.replacements}
</td>
<td className="px-3 py-1.5">
{r.success ? (
<span className="text-accent">ok</span>
) : r.error ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 text-warn cursor-help">
<TriangleAlert aria-hidden className="w-3.5 h-3.5" />
err
</span>
</TooltipTrigger>
<TooltipContent>{r.error}</TooltipContent>
</Tooltip>
) : (
<span className="text-warn">err</span>
)}
</td>
</tr>
))}
<tr className="bg-paper-2">
<td className="px-3 py-1.5 text-ink-faint uppercase tracking-[0.06em] text-[11px]">
total
</td>
<td className="px-2 py-1.5 tabular-nums" colSpan={2}>
<FooterPill
tone={total_replacements > 0 ? 'accent' : 'default'}
>
{`${total_replacements} replacements`}
</FooterPill>
</td>
</tr>
</tbody>
</table>
<SedResultsTable
results={results}
totalReplacements={total_replacements}
/>
)}
</div>
)
}

interface SedResultsTableProps {
results: FsSedFileResult[]
totalReplacements: number
}

/** Per-file replacement table with the total pill. Shared with the shell
module's `fs::sed` renderer — both wires speak `FsSedFileResult`. */
export function SedResultsTable({
results,
totalReplacements,
}: SedResultsTableProps) {
return (
<table className="w-full font-mono text-[12px] text-ink">
<thead>
<tr className="border-b border-rule-2 text-[11px] uppercase tracking-[0.06em] text-ink-faint">
<th className="text-left font-normal px-3 py-1.5">path</th>
<th className="text-left font-normal px-2 py-1.5 w-20 tabular-nums">
replacements
</th>
<th className="text-left font-normal px-3 py-1.5 w-8">status</th>
</tr>
</thead>
<tbody>
{results.map((r) => (
<tr key={r.path} className="border-b border-rule-2 last:border-b-0">
<td className="px-3 py-1.5 text-ink">{r.path}</td>
<td className="px-2 py-1.5 text-ink-faint tabular-nums">
{r.replacements}
</td>
<td className="px-3 py-1.5">
{r.success ? (
<span className="text-accent">ok</span>
) : r.error ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 text-warn cursor-help">
<TriangleAlert aria-hidden className="w-3.5 h-3.5" />
err
</span>
</TooltipTrigger>
<TooltipContent>{r.error}</TooltipContent>
</Tooltip>
) : (
<span className="text-warn">err</span>
)}
</td>
</tr>
))}
<tr className="bg-paper-2">
<td className="px-3 py-1.5 text-ink-faint uppercase tracking-[0.06em] text-[11px]">
total
</td>
<td className="px-2 py-1.5 tabular-nums" colSpan={2}>
<FooterPill tone={totalReplacements > 0 ? 'accent' : 'default'}>
{`${totalReplacements} replacements`}
</FooterPill>
</td>
</tr>
</tbody>
</table>
)
}
7 changes: 6 additions & 1 deletion console/web/src/components/chat/sandbox/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,12 @@ function invocationFromFunctionError(
}
}

function collectErrorCandidates(value: unknown): unknown[] {
/** Gather every sub-value that might carry an error payload: the value
itself, its unwrapped envelope, `value.error`, `error.details`,
`error.message`, and `content[]` text blocks. Shared with the shell
module's `parseShellErrorDisplay` — same harness wrapping, same
traversal. */
export function collectErrorCandidates(value: unknown): unknown[] {
const seen = new Set<unknown>()
const out: unknown[] = []
const push = (candidate: unknown) => {
Expand Down
Loading
Loading