feat(sources v1): the agent locates files itself — ls/find/grep + tool-call cards
User pivot (2026-09-19): no attachments UI at all. Pasting paths is native for technical users and alien for everyone else - so the agent gets the tools to find what the learner merely NAMES. The learner communicates in messages; the agent locates and reads. Zero cross-stream seams: paths already live in tool parts, so the sources "list" is the transcript. - ls/find/grep: pi-verbatim schemas, descriptions, and output formats (fd's glob semantics, rg's path:line: match format, pi's truncation notices), ported to pure Node - no binaries, no shell-out, read-only by construction. Deltas from pi: home-anchored resolution (the harness has no cwd), .gitignore replaced by hard noise exclusions (node_modules/.git/Library/.Trash), 10s walk timeout with partial results. - read: description swapped to pi-verbatim (doctrine moved to the prompt); relative paths now resolve against the learner's home like the rest. - prompts/shared/05-learner-files.md: one minimal doctrine fragment composed into BOTH agents (composeInstructions grew a shared/ dir, before the agent's own fragments). teacher/30-jobs.md keeps only the pdf-dispatch addendum. - ToolCallCard: the generic quiet tool-call card the user specced - collapsed one-liner (tool name + headline argument), expandable to full input/output, errors red, image payloads never render. Registered for read/ls/find/grep. - ui-message.ts: TeacherUITools entries for the four tools (mechanical). - tests: fs-tools suite (glob semantics, pi formats, exclusions, limits), registry/card renders, shared-fragment composition pins, tool-registry key updates, read re-anchor.
This commit is contained in:
@@ -4,6 +4,7 @@ import { DemoCard } from "@/components/artifacts/demo-card";
|
||||
import { JobCard, PdfJobCard } from "@/components/artifacts/job-card";
|
||||
import { VisualCard } from "@/components/artifacts/visual-card";
|
||||
import { ProbeDoneCard } from "@/components/artifacts/probe-done-card";
|
||||
import { ToolCallCard } from "@/components/artifacts/tool-call-card";
|
||||
|
||||
/**
|
||||
* Artifact renderer registry — the extension point for tool-part UI.
|
||||
@@ -29,4 +30,11 @@ export const artifactRenderers: Record<string, ComponentType<ArtifactPartProps>>
|
||||
// The probe conclusion: the assessment belongs to the learner as much as
|
||||
// to the transcript (docs/probe-teach-split.md §2). Plain v1 card.
|
||||
probe_done: ProbeDoneCard,
|
||||
// The learner-filesystem set (sources v1): one quiet expandable
|
||||
// tool-call card each — collapsed one-liner (tool name + headline
|
||||
// argument), full input/output on expand.
|
||||
read: ToolCallCard,
|
||||
ls: ToolCallCard,
|
||||
find: ToolCallCard,
|
||||
grep: ToolCallCard,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import type { ArtifactPartProps } from "@/src/core";
|
||||
|
||||
/**
|
||||
* The generic tool-call card (sources v1, 2026-09-19): a quiet, collapsed
|
||||
* one-liner per synchronous tool call — the tool name plus the headline
|
||||
* argument — expandable to the full input and output. The user's spec:
|
||||
* "at least the tool name; for read, the file name; everything collapsed
|
||||
* by default; not distracting — background stuff." Unregistered tools
|
||||
* still render nothing (chat.tsx's registry default); this card is only
|
||||
* for the learner-filesystem set (read/ls/find/grep) where the call IS
|
||||
* worth a trace in the transcript.
|
||||
*/
|
||||
|
||||
interface ToolCallPart {
|
||||
type: string;
|
||||
state: string;
|
||||
toolCallId: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
errorText?: string;
|
||||
}
|
||||
|
||||
/** The collapsed headline argument per tool: path for read/ls, pattern for
|
||||
* find/grep — shown as given, never resolved (the absolute path lands in
|
||||
* the expanded view when present in the output). */
|
||||
function headlineArg(tool: string, input: unknown): string {
|
||||
const inp = (input ?? {}) as Record<string, unknown>;
|
||||
if (tool === "read" || tool === "ls") {
|
||||
const p = typeof inp.path === "string" && inp.path.length > 0 ? inp.path : "~";
|
||||
return p;
|
||||
}
|
||||
if (typeof inp.pattern === "string") return inp.pattern;
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Render a tool result body: plain strings verbatim, image-bearing read
|
||||
* results show their text + an attachment note (the image itself is the
|
||||
* model's — the learner sees only that it happened). */
|
||||
function outputText(output: unknown): string | null {
|
||||
if (output == null) return null;
|
||||
if (typeof output === "string") return output;
|
||||
if (typeof output === "object" && "text" in (output as Record<string, unknown>)) {
|
||||
const o = output as { text?: unknown; images?: unknown[] };
|
||||
const text = typeof o.text === "string" ? o.text : null;
|
||||
const n = Array.isArray(o.images) ? o.images.length : 0;
|
||||
return n > 0 ? `${text ?? ""}\n[${n} image${n > 1 ? "s" : ""} attached to the model]`.trim() : text;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(output, null, 2);
|
||||
} catch {
|
||||
return String(output);
|
||||
}
|
||||
}
|
||||
|
||||
export function ToolCallCard({ part }: ArtifactPartProps) {
|
||||
const p = part as ToolCallPart;
|
||||
const tool = p.type.replace(/^tool-/, "");
|
||||
const arg = headlineArg(tool, p.input);
|
||||
const out = outputText(p.output);
|
||||
const failed = p.state === "output-error" || typeof p.errorText === "string";
|
||||
return (
|
||||
<details
|
||||
data-testid="tool-call-card"
|
||||
data-tool={tool}
|
||||
className="group my-1 w-full max-w-2xl select-none"
|
||||
>
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 text-xs text-muted-foreground [&::-webkit-details-marker]:hidden">
|
||||
<span className={`font-medium uppercase tracking-wide ${failed ? "text-red-500" : ""}`}>
|
||||
{tool}
|
||||
</span>
|
||||
{arg && <span className="truncate">{arg}</span>}
|
||||
<ChevronRight className="size-3 shrink-0 opacity-50 transition-transform group-open:rotate-90" />
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2 rounded-xl border bg-card p-3 text-xs">
|
||||
{p.input != null && (
|
||||
<div>
|
||||
<div className="mb-1 font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Input
|
||||
</div>
|
||||
<pre className="max-h-32 overflow-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-relaxed">
|
||||
{JSON.stringify(p.input, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{typeof p.errorText === "string" ? (
|
||||
<div>
|
||||
<div className="mb-1 font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Error
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap break-all font-mono text-[11px] text-red-500">
|
||||
{p.errorText}
|
||||
</pre>
|
||||
</div>
|
||||
) : out != null ? (
|
||||
<div>
|
||||
<div className="mb-1 font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Output
|
||||
</div>
|
||||
<pre className="max-h-64 overflow-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-relaxed">
|
||||
{out}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
p.state !== "output-available" && (
|
||||
<div className="text-muted-foreground italic">running…</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user