diff --git a/src/client/components/contextView.tsx b/src/client/components/contextView.tsx
index 273dce2..50f98af 100644
--- a/src/client/components/contextView.tsx
+++ b/src/client/components/contextView.tsx
@@ -220,9 +220,12 @@ export function makeContextView(
[briefList, convOf, filesBefore],
)
const locateFileOp = React.useCallback((op: FileOp): void => {
- const step = locateStepOf(requests, op.seq, op.gone)
+ // A nested Code-Mode op has no surface row of its own — it reveals on
+ // its parent run_code result (whose removal stamp the op carries).
+ const seq = op.parent ?? op.seq
+ const step = locateStepOf(requests, seq, op.gone)
if (step === null) return
- setNodeFocus({ step, seq: op.seq, cat: 'tool' })
+ setNodeFocus({ step, seq, cat: 'tool' })
}, [requests])
// A brief row's reveal target: inputs/opener live in the picked step's OWN assembled surface; the response node (seq === the
// request's) first appears in the NEXT step's surface — or the live surface when the last bar is picked.
diff --git a/src/client/components/fileCard.tsx b/src/client/components/fileCard.tsx
index 4624e3e..a885ac5 100644
--- a/src/client/components/fileCard.tsx
+++ b/src/client/components/fileCard.tsx
@@ -98,6 +98,11 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C
{op.time !== undefined ? fmtTime(op.time) : '—'}{op.tool}
{op.detail !== undefined ? {op.detail} : null}
+ {op.hits !== undefined ? {t('files.hits', { n: fmt(op.hits) })} : null}
+ {/* A nested PTC op with nothing else to say still tells why: its program's description. */}
+ {op.detail === undefined && op.hits === undefined && op.program !== undefined
+ ? {op.program}
+ : null}
{op.added + op.removed > 0 ? : null}
{op.err ? : null}
>
@@ -197,12 +202,15 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C
{open ? (
- {e.ops.map((op) => {
+ {e.ops.map((op, i) => {
const onLocate = props.onLocate
+ // A meta-attributed search rows one op per matched file off ONE
+ // result — the ops share that result's seq, so the key joins the index.
+ const key = `${op.seq}:${i}`
return onLocate !== undefined
? (
) : null}
diff --git a/src/client/fileActivity.ts b/src/client/fileActivity.ts
index da4fc82..67a0b48 100644
--- a/src/client/fileActivity.ts
+++ b/src/client/fileActivity.ts
@@ -3,11 +3,19 @@
* timeline's served tool-result nodes (live tail + archive) joined with the
* conversation snapshot for call arguments; no host or wire additions.
*
- * One tool-result surface node equals one executed call; a call whose
+ * One top-level tool-result surface node equals one executed call. Code Mode
+ * (PTC) runs nested calls instead — those ride the conversation node's
+ * `subCalls` tree, and each settled sub-dispatch folds as one op attributed
+ * to the nested tool and located on its parent run_code result. A call whose
* arguments have aged out of the conversation join names no target and is
* skipped — every op the card counts, it can also row. Line deltas are
- * estimates read off the call ARGUMENTS (an
- * edit's old/new strings, a write's content), never off result payloads.
+ * estimates read off the call ARGUMENTS (an edit's old/new strings, a write's
+ * content), never off result payloads.
+ *
+ * A search resolves further when its result's bounded presentation meta names
+ * the matched files: the ops then row per real file (with the match count),
+ * and only a capped or malformed meta falls back to the call's own target —
+ * the narrowing path, else the searched pattern.
*
* Scope: `before` is the EXCLUSIVE upper seq bound (the next request's seq),
* so the picked step's own calls — whose results land before the next
@@ -34,6 +42,16 @@ export interface FileOp {
removed: number
/** What was searched for, when a search named both a path and a pattern. */
detail?: string
+ /**
+ * Nested Code-Mode op only: the run_code result node the op ran under — the
+ * locate target, because the op's own seq names the dispatch event, which
+ * has no surface row. `gone` is the parent's stamp.
+ */
+ parent?: number
+ /** Nested Code-Mode op only: the run_code program's model-authored description — why the file was touched. */
+ program?: string
+ /** Meta-attributed search op only: matched lines the result reported for this file. */
+ hits?: number
}
/** One file's aggregated activity; `ops` newest first. */
@@ -72,6 +90,18 @@ export function kindOfTool(tool: string | undefined): FileOpKind | null {
return KIND_BY_TOOL[tool] ?? null
}
+/**
+ * The file purpose of one executed call. Like {@link kindOfTool} except for
+ * the one file tool whose purpose follows its arguments: `str_replace_editor`
+ * reads on `view` and writes on every other command (create / str_replace /
+ * insert — an unknown command writes too; the call failed and the row keeps
+ * its error flag).
+ */
+export function kindOfCall(tool: string, args: Record | null): FileOpKind | null {
+ if (tool === 'str_replace_editor') return args !== null && args.command === 'view' ? 'read' : 'write'
+ return kindOfTool(tool)
+}
+
/**
* The operation's target path: the path-ish argument of read/write tools;
* for searches the narrowing `path`, else the pattern itself (a pathless
@@ -100,24 +130,28 @@ export function linesOf(s: string): number {
return s.endsWith('\n') ? n : n + 1
}
+/** The added/removed pair of one content-bearing argument set, or zeros. */
+function pairOf(added: unknown, removed: unknown): { added: number; removed: number } {
+ return {
+ added: typeof added === 'string' ? linesOf(added) : 0,
+ removed: typeof removed === 'string' ? linesOf(removed) : 0,
+ }
+}
+
/**
* The signed line footprint of one call: an edit removes its old string and
* adds its new one; a write adds its content (the pre-existing body, if any,
- * is unknowable from the arguments — the estimate stays honest about that).
- * Callers reach here only with parsed args (a null parse yields no path).
+ * is unknowable from the arguments — the estimate stays honest about that);
+ * `str_replace_editor` splits the same shapes across its commands. Callers
+ * reach here only with parsed args (a null parse yields no path).
*/
function deltaOf(tool: string, args: Record): { added: number; removed: number } {
- if (tool === 'edit') {
- const oldS = args.old_string
- const newS = args.new_string
- return {
- added: typeof newS === 'string' ? linesOf(newS) : 0,
- removed: typeof oldS === 'string' ? linesOf(oldS) : 0,
- }
- }
- if (tool === 'write') {
- const content = args.content
- return { added: typeof content === 'string' ? linesOf(content) : 0, removed: 0 }
+ if (tool === 'edit') return pairOf(args.new_string, args.old_string)
+ if (tool === 'write') return pairOf(args.content, undefined)
+ if (tool === 'str_replace_editor') {
+ if (args.command === 'str_replace') return pairOf(args.new_str, args.old_str)
+ if (args.command === 'insert') return pairOf(args.new_str, undefined)
+ if (args.command === 'create') return pairOf(args.file_text, undefined)
}
return { added: 0, removed: 0 }
}
@@ -131,6 +165,138 @@ export function formOf(tool: string, path: string): FileForm {
return 'text'
}
+/**
+ * A search op's detail: the pattern, with the include filter appended when
+ * one narrowed the call. A patternless (malformed) search has no detail.
+ */
+function searchDetailOf(args: Record | null): string | undefined {
+ const pattern = args?.pattern
+ if (typeof pattern !== 'string' || pattern === '') return undefined
+ const include = args?.include
+ return typeof include === 'string' && include !== '' ? `${pattern} (${include})` : pattern
+}
+
+/**
+ * The files a search demonstrably reached, read off the result's bounded
+ * presentation meta (grep groups matched lines by file; glob lists paths).
+ * Only the COMPLETE list attributes: a capped search (`truncated`) names a
+ * partial file set, and a malformed meta names none — both fall back to the
+ * call's own target. Each entry carries the reported match count.
+ */
+function searchFilesOf(meta: unknown): { path: string; hits: number }[] | null {
+ if (meta === null || typeof meta !== 'object') return null
+ const m = meta as { shape?: unknown; truncated?: unknown; files?: unknown; paths?: unknown }
+ if (m.truncated !== false) return null
+ const files: { path: string; hits: number }[] = []
+ if (m.shape === 'matches' && Array.isArray(m.files)) {
+ for (const f of m.files) {
+ if (f === null || typeof f !== 'object') continue
+ const group = f as { path?: unknown; matches?: unknown }
+ if (typeof group.path === 'string' && group.path !== '' && Array.isArray(group.matches)) {
+ files.push({ path: group.path, hits: group.matches.length })
+ }
+ }
+ } else if (m.shape === 'paths' && Array.isArray(m.paths)) {
+ for (const p of m.paths) {
+ if (typeof p === 'string' && p !== '') files.push({ path: p, hits: 0 })
+ }
+ }
+ return files.length > 0 ? files : null
+}
+
+/** One settled nested call of a Code-Mode tree, as far as the fold consumes it. */
+interface SubCall {
+ name: string
+ argsRaw: string
+ seq: number
+ time?: number
+ err: boolean
+ subCalls?: readonly unknown[]
+}
+
+/**
+ * Narrow one block of a conversation node's `subCalls` tree to a settled
+ * nested call, or null. The join is defensive — a running call has no result
+ * kind yet, and any malformed block is dropped, never thrown. (Null and
+ * non-object blocks never reach here: the folding loop pre-filters them.)
+ */
+function subCallOf(block: unknown): SubCall | null {
+ const b = block as Record
+ if (b.kind !== 'tool-result') return null
+ if (b.call === null || typeof b.call !== 'object') return null
+ const call = b.call as { name?: unknown; argsRaw?: unknown }
+ if (typeof call.name !== 'string' || typeof call.argsRaw !== 'string') return null
+ if (typeof b.seq !== 'number' || !Number.isFinite(b.seq)) return null
+ return {
+ name: call.name,
+ argsRaw: call.argsRaw,
+ seq: b.seq,
+ ...(typeof b.time === 'number' ? { time: b.time } : {}),
+ ...(b.isError === true ? { err: true } : { err: false }),
+ ...(Array.isArray(b.subCalls) ? { subCalls: b.subCalls } : {}),
+ }
+}
+
+/** The run_code call's model-authored description — the nested ops' "why". */
+function programOf(conv: ConversationNodeLike | undefined): string | undefined {
+ const description = parseCallArgs(conv?.call?.argsRaw)?.description
+ return typeof description === 'string' && description !== '' ? description : undefined
+}
+
+/**
+ * Depth guard for nested Code-Mode trees. The SDK bindings exclude `run_code`
+ * itself, so a real tree is one level deep; the cap only bounds defensive
+ * re-entry over a malformed join.
+ */
+const SUBCALL_MAX_DEPTH = 8
+
+/**
+ * Fold one nested Code-Mode tree: every settled sub-dispatch whose arguments
+ * resolve to a file target books one op, attributed to the nested tool and
+ * located on the parent run_code result. `seen` holds the already-visited
+ * blocks, so a malformed (cyclic) join cannot loop.
+ */
+function foldSubCalls(
+ blocks: readonly unknown[],
+ parent: SurfaceNode,
+ program: string | undefined,
+ add: (op: FileOp, path: string) => void,
+ seen: Set