@hachej/boring-agent 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { ReactNode, ComponentType, HTMLAttributes, ComponentProps, FormEvent, El
4
4
  import * as ai from 'ai';
5
5
  import { UIMessage, FileUIPart, ChatStatus } from 'ai';
6
6
  import * as _ai_sdk_react from '@ai-sdk/react';
7
- import { S as SendMessageInput, d as SessionSummary } from '../harness-DpMCKbzh.js';
7
+ import { S as SendMessageInput, d as SessionSummary } from '../harness-DT3ZzdAN.js';
8
8
  import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, InputGroupButton, TooltipContent, InputGroupAddon, InputGroupTextarea } from '@hachej/boring-ui-kit';
9
9
  import { Streamdown } from 'streamdown';
10
10
  import { StickToBottom } from 'use-stick-to-bottom';
@@ -344,32 +344,61 @@ function useAgentChat(opts) {
344
344
 
345
345
  // src/front/splitFollowUp.ts
346
346
  function splitFollowUp(msgs, consumed, genId) {
347
- const targetIdx = msgs.findIndex(
347
+ let targetIdx = msgs.findIndex(
348
348
  (m) => m.role === "assistant" && m.parts?.some((p) => {
349
349
  const part = p;
350
350
  return part.type === "data-followup-consumed" || part.id?.startsWith("turn-");
351
351
  })
352
352
  );
353
+ if (targetIdx < 0) {
354
+ for (let i = msgs.length - 1; i >= 0; i--) {
355
+ if (msgs[i]?.role === "assistant") {
356
+ targetIdx = i;
357
+ break;
358
+ }
359
+ }
360
+ }
361
+ const displayTextFromMarker = msgs.flatMap((m) => m.parts ?? []).map((p) => p).find((p) => p.type === "data-followup-consumed" && typeof p.data?.text === "string")?.data?.text;
353
362
  const userMsg = {
354
363
  id: genId(),
355
364
  role: "user",
356
- parts: [...consumed.files, { type: "text", text: consumed.text }]
365
+ parts: [...consumed.files, { type: "text", text: displayTextFromMarker ?? consumed.text }]
357
366
  };
358
367
  if (targetIdx < 0) {
359
368
  return [...msgs, userMsg];
360
369
  }
361
370
  const target = msgs[targetIdx];
362
- const markerIdx = target.parts?.findIndex((p) => {
371
+ const parts = target.parts ?? [];
372
+ let markerIdx = parts.findIndex((p) => {
363
373
  const part = p;
364
374
  return part.type === "data-followup-consumed" || part.id?.startsWith("turn-");
365
- }) ?? -1;
366
- const markerIsPart = markerIdx >= 0 && (target.parts?.[markerIdx]).type === "data-followup-consumed";
367
- const turn1Parts = markerIdx >= 0 ? target.parts?.slice(0, markerIdx) ?? [] : target.parts ?? [];
368
- const turn2Parts = markerIdx >= 0 ? target.parts?.slice(markerIsPart ? markerIdx + 1 : markerIdx) ?? [] : [];
375
+ });
376
+ let markerIsPart = markerIdx >= 0 && parts[markerIdx].type === "data-followup-consumed";
377
+ if (markerIdx < 0) {
378
+ const textPartIndexes = parts.map((p, index) => p.type === "text" ? index : -1).filter((index) => index >= 0);
379
+ if (textPartIndexes.length > 1) {
380
+ markerIdx = textPartIndexes[textPartIndexes.length - 1];
381
+ markerIsPart = false;
382
+ } else {
383
+ return [...msgs.slice(0, targetIdx), userMsg, target, ...msgs.slice(targetIdx + 1)];
384
+ }
385
+ }
386
+ const turn1Parts = markerIdx >= 0 ? parts.slice(0, markerIdx) : parts;
387
+ const turn2Parts = markerIdx >= 0 ? parts.slice(markerIsPart ? markerIdx + 1 : markerIdx) : [];
369
388
  const asst1 = { ...target, parts: turn1Parts };
370
389
  const asst2 = { ...target, id: genId(), parts: turn2Parts };
371
390
  return [...msgs.slice(0, targetIdx), asst1, userMsg, asst2, ...msgs.slice(targetIdx + 1)];
372
391
  }
392
+ function splitFollowUpForDisplay(msgs, consumed, pending, ids) {
393
+ const draft = consumed ?? pending;
394
+ if (!draft || !ids) return msgs;
395
+ const hasFollowUpBoundary = msgs.some(
396
+ (m) => m.role === "assistant" && (m.parts?.some((p) => p.id?.startsWith("turn-") || p.type === "data-followup-consumed") || (m.parts?.filter((p) => p.type === "text").length ?? 0) > 1)
397
+ );
398
+ if (!hasFollowUpBoundary) return msgs;
399
+ let n = 0;
400
+ return splitFollowUp(msgs, draft, () => n++ === 0 ? ids.userId : ids.assistantId);
401
+ }
373
402
 
374
403
  // src/front/DebugDrawer.tsx
375
404
  import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
@@ -3778,6 +3807,8 @@ function ChatPanel(props) {
3778
3807
  const [debugWidth, setDebugWidth] = useState13(440);
3779
3808
  const [pendingMessage, setPendingMessage] = useState13(null);
3780
3809
  const pendingMessageRef = useRef10(null);
3810
+ const followUpPostedRef = useRef10(false);
3811
+ const followUpPostTimerRef = useRef10(null);
3781
3812
  const consumedFollowUpRef = useRef10(null);
3782
3813
  const followUpSplitIdsRef = useRef10(null);
3783
3814
  const {
@@ -3793,7 +3824,8 @@ function ChatPanel(props) {
3793
3824
  onData: (part) => {
3794
3825
  if (part?.type === "data-followup-consumed") {
3795
3826
  const pending = pendingMessageRef.current;
3796
- consumedFollowUpRef.current = pending ? { text: pending.text, files: pending.files } : null;
3827
+ const serverText = part?.data?.text;
3828
+ consumedFollowUpRef.current = pending ? { text: pending.text, files: pending.files } : typeof serverText === "string" ? { text: serverText, files: [] } : null;
3797
3829
  followUpSplitIdsRef.current = {
3798
3830
  userId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-followup-user`,
3799
3831
  assistantId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-followup-assistant`
@@ -3923,17 +3955,35 @@ function ChatPanel(props) {
3923
3955
  return () => globalThis.removeEventListener?.("boring:model-change", onChange);
3924
3956
  }, []);
3925
3957
  const isStreaming = status === "submitted" || status === "streaming";
3926
- const displayMessages = useMemo10(() => {
3927
- const consumed = consumedFollowUpRef.current ?? pendingMessageRef.current;
3928
- const ids = followUpSplitIdsRef.current;
3929
- if (!consumed || !ids) return messages;
3930
- const hasFollowUpPart = messages.some(
3931
- (m) => m.role === "assistant" && m.parts?.some((p) => p.id?.startsWith("turn-"))
3932
- );
3933
- if (!hasFollowUpPart) return messages;
3934
- let n = 0;
3935
- return splitFollowUp(messages, consumed, () => n++ === 0 ? ids.userId : ids.assistantId);
3936
- }, [messages]);
3958
+ const displayMessages = useMemo10(
3959
+ () => splitFollowUpForDisplay(
3960
+ messages,
3961
+ consumedFollowUpRef.current,
3962
+ pendingMessageRef.current,
3963
+ followUpSplitIdsRef.current
3964
+ ),
3965
+ [messages]
3966
+ );
3967
+ const postPendingFollowUp = useCallback12((pending) => {
3968
+ if (followUpPostedRef.current) return;
3969
+ followUpPostedRef.current = true;
3970
+ fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
3971
+ method: "POST",
3972
+ headers: {
3973
+ "Content-Type": "application/json",
3974
+ ...requestHeaders
3975
+ },
3976
+ body: JSON.stringify({ message: pending.serverMessage, displayText: pending.text, attachments: pending.attachments })
3977
+ }).catch(() => {
3978
+ followUpPostedRef.current = false;
3979
+ });
3980
+ }, [sessionId, requestHeaders]);
3981
+ useEffect11(() => {
3982
+ if (status !== "streaming") return;
3983
+ const pending = pendingMessageRef.current;
3984
+ if (!pending) return;
3985
+ postPendingFollowUp(pending);
3986
+ }, [status, postPendingFollowUp]);
3937
3987
  const prevStatusForQueue = useRef10(status);
3938
3988
  useEffect11(() => {
3939
3989
  const prev = prevStatusForQueue.current;
@@ -3946,6 +3996,9 @@ function ChatPanel(props) {
3946
3996
  consumedFollowUpRef.current = null;
3947
3997
  followUpSplitIdsRef.current = null;
3948
3998
  pendingMessageRef.current = null;
3999
+ followUpPostedRef.current = false;
4000
+ if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
4001
+ followUpPostTimerRef.current = null;
3949
4002
  setPendingMessage(null);
3950
4003
  let n = 0;
3951
4004
  const genId = () => ids ? n++ === 0 ? ids.userId : ids.assistantId : globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
@@ -3966,6 +4019,9 @@ function ChatPanel(props) {
3966
4019
  consumedFollowUpRef.current = null;
3967
4020
  followUpSplitIdsRef.current = null;
3968
4021
  pendingMessageRef.current = null;
4022
+ followUpPostedRef.current = false;
4023
+ if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
4024
+ followUpPostTimerRef.current = null;
3969
4025
  setPendingMessage(null);
3970
4026
  fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
3971
4027
  method: "DELETE",
@@ -4155,16 +4211,17 @@ ${content}
4155
4211
  attachments: resolvedAttachments
4156
4212
  };
4157
4213
  pendingMessageRef.current = nextPending;
4214
+ followUpPostedRef.current = false;
4158
4215
  setPendingMessage(nextPending);
4159
- fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
4160
- method: "POST",
4161
- headers: {
4162
- "Content-Type": "application/json",
4163
- ...requestHeaders
4164
- },
4165
- body: JSON.stringify({ message: serverMessage, attachments: resolvedAttachments })
4166
- }).catch(() => {
4167
- });
4216
+ if (status === "streaming") {
4217
+ postPendingFollowUp(nextPending);
4218
+ } else {
4219
+ if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
4220
+ followUpPostTimerRef.current = setTimeout(() => {
4221
+ const pending = pendingMessageRef.current;
4222
+ if (pending) postPendingFollowUp(pending);
4223
+ }, 1e3);
4224
+ }
4168
4225
  }
4169
4226
  return;
4170
4227
  }
@@ -51,7 +51,7 @@ interface AgentHarness {
51
51
  * A `data-followup-consumed` chunk is emitted before the follow-up turn so
52
52
  * the client can clear its pending-message bubble immediately.
53
53
  */
54
- followUp?(sessionId: string, text: string, attachments?: MessageAttachment[]): void;
54
+ followUp?(sessionId: string, text: string, attachments?: MessageAttachment[], displayText?: string): void | Promise<void>;
55
55
  /**
56
56
  * Discard any queued follow-up for this session (called by the Stop button).
57
57
  */
@@ -2242,13 +2242,25 @@ function timeoutResult(durationMs) {
2242
2242
  stderrEncoding: "utf-8"
2243
2243
  };
2244
2244
  }
2245
+ function toRemotePath(value) {
2246
+ if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
2247
+ if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
2248
+ return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
2249
+ }
2250
+ return value;
2251
+ }
2245
2252
  function toRemoteCwd(cwd) {
2246
2253
  if (!cwd) return cwd;
2247
- if (cwd === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
2248
- if (cwd.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
2249
- return `${VERCEL_SANDBOX_REMOTE_ROOT}${cwd.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
2250
- }
2251
- return cwd;
2254
+ return toRemotePath(cwd);
2255
+ }
2256
+ function toRemoteCommand(command) {
2257
+ return command.replaceAll(VERCEL_SANDBOX_WORKSPACE_ROOT, VERCEL_SANDBOX_REMOTE_ROOT);
2258
+ }
2259
+ function toRemoteEnv(env) {
2260
+ if (!env) return void 0;
2261
+ return Object.fromEntries(
2262
+ Object.entries(env).map(([key, value]) => [key, toRemotePath(value)])
2263
+ );
2252
2264
  }
2253
2265
  function createVercelSandboxExec(sandbox, execOpts = {}) {
2254
2266
  return {
@@ -2294,9 +2306,9 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
2294
2306
  }
2295
2307
  const result = await sandbox.runCommand({
2296
2308
  cmd: "sh",
2297
- args: ["-c", cmd],
2309
+ args: ["-c", toRemoteCommand(cmd)],
2298
2310
  cwd: toRemoteCwd(opts?.cwd),
2299
- env: opts?.env,
2311
+ env: toRemoteEnv(opts?.env),
2300
2312
  signal: controller.signal,
2301
2313
  stdout: createStreamWritable(stdoutCollector, captureState, opts?.onStdout),
2302
2314
  stderr: createStreamWritable(stderrCollector, captureState, opts?.onStderr)
@@ -3689,6 +3701,16 @@ function mergePiPackageSources(base = [], additional = []) {
3689
3701
  }
3690
3702
 
3691
3703
  // src/server/harness/pi-coding-agent/createHarness.ts
3704
+ function extractUserMessageText(message) {
3705
+ const record = message;
3706
+ if (record?.role !== "user") return "";
3707
+ if (typeof record.content === "string") return record.content;
3708
+ if (!Array.isArray(record.content)) return "";
3709
+ return record.content.map((part) => {
3710
+ const p = part;
3711
+ return p.type === "text" && typeof p.text === "string" ? p.text : "";
3712
+ }).join("");
3713
+ }
3692
3714
  function extractAssistantMessageText(message) {
3693
3715
  const record = message;
3694
3716
  const role = typeof record?.role === "string" ? record.role : void 0;
@@ -3874,16 +3896,18 @@ function createPiCodingAgentHarness(opts) {
3874
3896
  await originalDelete(ctx, sessionId);
3875
3897
  disposePiSession(sessionId);
3876
3898
  };
3877
- const harnessFollowUpQueues = /* @__PURE__ */ new Map();
3899
+ const nativeFollowUpPending = /* @__PURE__ */ new Set();
3878
3900
  return {
3879
3901
  id: "pi-coding-agent",
3880
3902
  placement: "server",
3881
3903
  sessions: sessionStore,
3882
- followUp(sessionId, text, attachments) {
3883
- harnessFollowUpQueues.set(sessionId, { message: text, attachments });
3904
+ async followUp(sessionId, text, _attachments, _displayText = text) {
3905
+ const handle = piSessions.get(sessionId);
3906
+ if (!handle) return;
3907
+ nativeFollowUpPending.add(sessionId);
3908
+ await handle.piSession.followUp(text);
3884
3909
  },
3885
- clearFollowUp(sessionId) {
3886
- harnessFollowUpQueues.delete(sessionId);
3910
+ clearFollowUp(_sessionId) {
3887
3911
  },
3888
3912
  /**
3889
3913
  * Pi exposes the resolved system prompt as a getter on AgentSession.
@@ -3900,7 +3924,6 @@ function createPiCodingAgentHarness(opts) {
3900
3924
  input,
3901
3925
  ctx
3902
3926
  );
3903
- harnessFollowUpQueues.delete(input.sessionId);
3904
3927
  const chunks = [];
3905
3928
  let done = false;
3906
3929
  let streamError = null;
@@ -3960,7 +3983,6 @@ function createPiCodingAgentHarness(opts) {
3960
3983
  return chunk2;
3961
3984
  });
3962
3985
  }
3963
- let pendingFollowUp;
3964
3986
  let promptSettled = false;
3965
3987
  let promptPromise = Promise.resolve();
3966
3988
  const unsubscribe = piSession.subscribe((event) => {
@@ -3982,7 +4004,19 @@ function createPiCodingAgentHarness(opts) {
3982
4004
  activeTools.delete(event.toolCallId);
3983
4005
  if (activeTools.size === 0) stopHeartbeat();
3984
4006
  }
3985
- let converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4007
+ let converted;
4008
+ if (event.type === "message_start" && event.message?.role === "user") {
4009
+ const text = extractUserMessageText(event.message);
4010
+ converted = text && text !== input.message ? [{ type: "data-followup-consumed", data: { text } }] : [];
4011
+ if (text && text !== input.message) {
4012
+ inlineTurnIndex += 1;
4013
+ nativeFollowUpPending.delete(input.sessionId);
4014
+ }
4015
+ } else if (event.type === "message_end" && event.message?.role === "user") {
4016
+ converted = [];
4017
+ } else {
4018
+ converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4019
+ }
3986
4020
  if (event.type === "message_update") {
3987
4021
  const ame = event.assistantMessageEvent;
3988
4022
  if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
@@ -4042,16 +4076,7 @@ function createPiCodingAgentHarness(opts) {
4042
4076
  assistantText += text;
4043
4077
  }
4044
4078
  }
4045
- const followUp = harnessFollowUpQueues.get(input.sessionId);
4046
- if (followUp !== void 0 && !ctx.abortSignal.aborted) {
4047
- harnessFollowUpQueues.delete(input.sessionId);
4048
- chunks.push({ type: "data-followup-consumed" });
4049
- inlineTurnIndex += 1;
4050
- sawTextChunk = false;
4051
- textStartSeen.clear();
4052
- textDeltaSeen.clear();
4053
- assistantText = "";
4054
- pendingFollowUp = followUp;
4079
+ if (nativeFollowUpPending.has(input.sessionId) && !ctx.abortSignal.aborted) {
4055
4080
  } else {
4056
4081
  done = true;
4057
4082
  stopHeartbeat();
@@ -4107,11 +4132,6 @@ ${attachmentNotes.join("\n")}` : message,
4107
4132
  const prepared = await prepareTurn(message, attachments);
4108
4133
  return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
4109
4134
  promptSettled = true;
4110
- if (pendingFollowUp !== void 0 && !ctx.abortSignal.aborted) {
4111
- const next = pendingFollowUp;
4112
- pendingFollowUp = void 0;
4113
- promptPromise = startTurn(next.message, next.attachments);
4114
- }
4115
4135
  }).catch((err) => {
4116
4136
  promptSettled = true;
4117
4137
  streamError = err;
@@ -4535,12 +4555,25 @@ function boundFs(workspaceRoot) {
4535
4555
 
4536
4556
  // src/server/tools/operations/vercel.ts
4537
4557
  import { relative as relative5 } from "path";
4558
+ function rootAliases(workspace) {
4559
+ const aliases = [workspace.root];
4560
+ if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
4561
+ if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
4562
+ return aliases;
4563
+ }
4538
4564
  function toRelPath(workspace, absolutePath) {
4539
- const rel = relative5(workspace.root, absolutePath);
4540
- if (rel.startsWith("..") || rel.startsWith("/")) {
4541
- throw new Error(`path "${absolutePath}" is outside workspace`);
4565
+ for (const root of rootAliases(workspace)) {
4566
+ const rel = relative5(root, absolutePath);
4567
+ if (!rel.startsWith("..") && !rel.startsWith("/")) return rel;
4568
+ }
4569
+ const skillMarker = "/.agents/skills/";
4570
+ const skillIndex = absolutePath.indexOf(skillMarker);
4571
+ if (skillIndex >= 0) {
4572
+ return `.agents/skills/${absolutePath.slice(skillIndex + skillMarker.length)}`;
4542
4573
  }
4543
- return rel;
4574
+ throw new Error(
4575
+ `path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
4576
+ );
4544
4577
  }
4545
4578
  function vercelBashOps(sandbox) {
4546
4579
  return {
@@ -4599,6 +4632,39 @@ function vercelEditOps(workspace) {
4599
4632
  }
4600
4633
  };
4601
4634
  }
4635
+ function toRemotePath2(value) {
4636
+ if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
4637
+ if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
4638
+ return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
4639
+ }
4640
+ return value;
4641
+ }
4642
+ function findPredicate(pattern) {
4643
+ const isPathShaped = pattern.includes("/") || pattern.includes("**");
4644
+ if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
4645
+ let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
4646
+ if (!translated.startsWith("*")) translated = `*${translated}`;
4647
+ return `-path ${shellEscape(translated)}`;
4648
+ }
4649
+ function findIgnoreArgs(cwd, ignore) {
4650
+ return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
4651
+ }
4652
+ function fallbackFindCommand(pattern, cwd, options) {
4653
+ return [
4654
+ "find",
4655
+ shellEscape(cwd),
4656
+ "-maxdepth 20",
4657
+ "-type f",
4658
+ findIgnoreArgs(cwd, options.ignore),
4659
+ findPredicate(pattern),
4660
+ `| head -n ${Math.max(1, Math.trunc(options.limit))}`
4661
+ ].filter(Boolean).join(" ");
4662
+ }
4663
+ function isFdMissing(result) {
4664
+ if (result.exitCode !== 127) return false;
4665
+ const stderr = Buffer.from(result.stderr).toString("utf-8");
4666
+ return /fd: not found|fd: command not found|not found/i.test(stderr);
4667
+ }
4602
4668
  function vercelFindOps(sandbox, workspace) {
4603
4669
  return {
4604
4670
  async exists(absolutePath) {
@@ -4617,18 +4683,25 @@ function vercelFindOps(sandbox, workspace) {
4617
4683
  return result.exitCode === 0;
4618
4684
  },
4619
4685
  async glob(pattern, cwd, options) {
4686
+ const remoteCwd = toRemotePath2(cwd);
4620
4687
  const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
4621
4688
  for (const ig of options.ignore) {
4622
4689
  args.push("--exclude", ig);
4623
4690
  }
4624
- args.push(pattern, cwd);
4625
- const result = await sandbox.exec(args.map(shellEscape).join(" "), {
4691
+ args.push(pattern, remoteCwd);
4692
+ let result = await sandbox.exec(args.map(shellEscape).join(" "), {
4626
4693
  timeoutMs: 3e4,
4627
4694
  maxOutputBytes: 1048576
4628
4695
  });
4696
+ if (isFdMissing(result)) {
4697
+ result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
4698
+ timeoutMs: 3e4,
4699
+ maxOutputBytes: 1048576
4700
+ });
4701
+ }
4629
4702
  if (result.exitCode !== 0 && result.exitCode !== 1) {
4630
4703
  const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
4631
- throw new Error(`fd failed (exit ${result.exitCode}): ${stderr}`);
4704
+ throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
4632
4705
  }
4633
4706
  const stdout = Buffer.from(result.stdout).toString("utf-8");
4634
4707
  return stdout.split("\n").filter(Boolean);
@@ -6158,7 +6231,12 @@ function chatRoutes(app, opts, done) {
6158
6231
  });
6159
6232
  }
6160
6233
  const runtime = await resolveRuntime(request);
6161
- runtime.harness.followUp?.(sessionId, body.message, parsedAttachments.data);
6234
+ await runtime.harness.followUp?.(
6235
+ sessionId,
6236
+ body.message,
6237
+ parsedAttachments.data,
6238
+ typeof body.displayText === "string" && body.displayText.length > 0 ? body.displayText : body.message
6239
+ );
6162
6240
  return reply.code(202).send({ queued: true });
6163
6241
  }
6164
6242
  );
@@ -6281,9 +6359,16 @@ import { randomUUID as randomUUID4 } from "crypto";
6281
6359
  import { z as z2 } from "zod";
6282
6360
  var DEFAULT_SESSION_TITLE2 = "New session";
6283
6361
  var DEFAULT_WORKSPACE_ID2 = "default";
6362
+ var MAX_ANALYSIS_TRANSCRIPT_CHARS = 12e4;
6284
6363
  var createSessionBodySchema = z2.object({
6285
6364
  title: z2.string().min(1).max(200).optional()
6286
6365
  }).optional();
6366
+ var analyzeSessionBodySchema = z2.object({
6367
+ instructions: z2.string().max(4e3).optional(),
6368
+ title: z2.string().min(1).max(200).optional(),
6369
+ run: z2.boolean().optional(),
6370
+ includeTranscript: z2.boolean().optional()
6371
+ }).optional();
6287
6372
  var SessionNotFoundError = class extends Error {
6288
6373
  constructor(sessionId) {
6289
6374
  super(`Session not found: ${sessionId}`);
@@ -6380,13 +6465,141 @@ function classifySessionError(err, reply) {
6380
6465
  }
6381
6466
  });
6382
6467
  }
6468
+ function stableStringify(value) {
6469
+ if (typeof value === "string") return value;
6470
+ try {
6471
+ return JSON.stringify(value, null, 2);
6472
+ } catch {
6473
+ return String(value);
6474
+ }
6475
+ }
6476
+ function partText(part) {
6477
+ const value = part;
6478
+ if (typeof value.text === "string") return value.text;
6479
+ if (typeof value.delta === "string") return value.delta;
6480
+ if (typeof value.content === "string") return value.content;
6481
+ return "";
6482
+ }
6483
+ function toolName(part) {
6484
+ if (typeof part.toolName === "string") return part.toolName;
6485
+ if (typeof part.type === "string" && part.type.startsWith("tool-")) {
6486
+ return part.type.slice("tool-".length);
6487
+ }
6488
+ return "tool";
6489
+ }
6490
+ function formatToolPart(part) {
6491
+ const name = toolName(part);
6492
+ const lines = [`[tool:${name}]`];
6493
+ if ("input" in part) {
6494
+ lines.push("input:", stableStringify(part.input));
6495
+ }
6496
+ if ("output" in part) {
6497
+ lines.push("output:", stableStringify(part.output));
6498
+ }
6499
+ if (typeof part.errorText === "string" && part.errorText.length > 0) {
6500
+ lines.push("error:", part.errorText);
6501
+ }
6502
+ return lines.join("\n");
6503
+ }
6504
+ function formatMessageForTranscript(message, index) {
6505
+ const msg = message;
6506
+ const role = String(msg.role ?? "unknown").toUpperCase();
6507
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
6508
+ const lines = [`## ${index + 1}. ${role}`];
6509
+ if (typeof msg.content === "string" && msg.content.trim()) {
6510
+ lines.push(msg.content.trim());
6511
+ }
6512
+ for (const part of parts) {
6513
+ const record = part;
6514
+ const type = typeof record.type === "string" ? record.type : "";
6515
+ if (type === "text") {
6516
+ const text2 = partText(record).trim();
6517
+ if (text2) lines.push(text2);
6518
+ continue;
6519
+ }
6520
+ if (type === "reasoning") {
6521
+ const text2 = partText(record).trim();
6522
+ if (text2) lines.push(`[reasoning]
6523
+ ${text2}`);
6524
+ continue;
6525
+ }
6526
+ if (type.startsWith("tool-")) {
6527
+ lines.push(formatToolPart(record));
6528
+ continue;
6529
+ }
6530
+ const text = partText(record).trim();
6531
+ if (text) lines.push(`[${type || "part"}]
6532
+ ${text}`);
6533
+ }
6534
+ if (lines.length === 1) lines.push("(no visible content)");
6535
+ return lines.join("\n\n");
6536
+ }
6537
+ function formatSessionTranscript(session) {
6538
+ const header = [
6539
+ `# Agent session transcript: ${session.title}`,
6540
+ "",
6541
+ `- Session: ${session.id}`,
6542
+ `- Created: ${session.createdAt}`,
6543
+ `- Updated: ${session.updatedAt}`,
6544
+ `- User turns: ${session.turnCount}`
6545
+ ];
6546
+ const body = session.messages.map(formatMessageForTranscript);
6547
+ return [...header, "", ...body].join("\n");
6548
+ }
6549
+ function truncateMiddle(value, maxChars) {
6550
+ if (value.length <= maxChars) return value;
6551
+ const keep = Math.floor((maxChars - 200) / 2);
6552
+ return `${value.slice(0, keep)}
6553
+
6554
+ [... transcript truncated: ${value.length - keep * 2} chars omitted ...]
6555
+
6556
+ ${value.slice(-keep)}`;
6557
+ }
6558
+ function buildAnalysisPrompt(session, transcript, instructions) {
6559
+ const boundedTranscript = truncateMiddle(transcript, MAX_ANALYSIS_TRANSCRIPT_CHARS);
6560
+ return [
6561
+ "You are an agent-session analyst. Analyze the transcript below and explain what is going on.",
6562
+ "",
6563
+ "Return a concise, evidence-backed report with these sections:",
6564
+ "1. User goal",
6565
+ "2. Current state",
6566
+ "3. What the agent did",
6567
+ "4. Failures, confusing behavior, or likely root causes",
6568
+ "5. Files/tools/commands that matter",
6569
+ "6. Risks and recommended next actions",
6570
+ "",
6571
+ "Rules:",
6572
+ "- Cite concrete transcript evidence when possible.",
6573
+ '- Say "unknown" when the transcript does not prove something.',
6574
+ "- Do not modify files unless explicitly asked in follow-up.",
6575
+ instructions ? `- Extra user instructions: ${instructions}` : "",
6576
+ "",
6577
+ `Source session: ${session.id} (${session.title})`,
6578
+ "",
6579
+ "<transcript>",
6580
+ boundedTranscript,
6581
+ "</transcript>"
6582
+ ].filter(Boolean).join("\n");
6583
+ }
6584
+ function analysisTextFromChunks(chunks) {
6585
+ return chunks.map((chunk2) => {
6586
+ const record = chunk2;
6587
+ return record.type === "text-delta" && typeof record.delta === "string" ? record.delta : "";
6588
+ }).join("").trim();
6589
+ }
6383
6590
  function sessionRoutes(app, opts, done) {
6384
6591
  const sessionStore = opts.sessionStore ?? new InMemorySessionStore();
6385
6592
  const validateCreateBody = createBodyValidator(createSessionBodySchema);
6593
+ const validateAnalyzeBody = createBodyValidator(analyzeSessionBodySchema);
6386
6594
  async function resolveSessionStore(request) {
6387
6595
  if (opts.getSessionStore) return await opts.getSessionStore(request);
6388
6596
  return sessionStore;
6389
6597
  }
6598
+ async function resolveRuntime(request) {
6599
+ if (opts.getRuntime) return await opts.getRuntime(request);
6600
+ if (opts.harness && opts.workdir) return { harness: opts.harness, workdir: opts.workdir };
6601
+ throw new Error("session analysis requires harness/workdir or getRuntime");
6602
+ }
6390
6603
  app.get("/api/v1/agent/sessions", async (request, reply) => {
6391
6604
  try {
6392
6605
  const store = await resolveSessionStore(request);
@@ -6421,6 +6634,113 @@ function sessionRoutes(app, opts, done) {
6421
6634
  return classifySessionError(err, reply);
6422
6635
  }
6423
6636
  });
6637
+ app.get("/api/v1/agent/sessions/:id/transcript", async (request, reply) => {
6638
+ const params = request.params;
6639
+ const sessionId = requireSessionId(params.id, reply);
6640
+ if (sessionId === null) return;
6641
+ const query = request.query;
6642
+ const format = query.format === "json" ? "json" : "markdown";
6643
+ try {
6644
+ const store = await resolveSessionStore(request);
6645
+ const session = await store.load(getSessionCtx(request), sessionId);
6646
+ const transcript = formatSessionTranscript(session);
6647
+ if (format === "json") {
6648
+ return {
6649
+ session: {
6650
+ id: session.id,
6651
+ title: session.title,
6652
+ createdAt: session.createdAt,
6653
+ updatedAt: session.updatedAt,
6654
+ turnCount: session.turnCount
6655
+ },
6656
+ transcript,
6657
+ messages: session.messages
6658
+ };
6659
+ }
6660
+ return reply.header("Content-Type", "text/markdown; charset=utf-8").send(transcript);
6661
+ } catch (err) {
6662
+ return classifySessionError(err, reply);
6663
+ }
6664
+ });
6665
+ app.post(
6666
+ "/api/v1/agent/sessions/:id/analysis",
6667
+ { preHandler: validateAnalyzeBody },
6668
+ async (request, reply) => {
6669
+ const params = request.params;
6670
+ const sourceSessionId = requireSessionId(params.id, reply);
6671
+ if (sourceSessionId === null) return;
6672
+ const body = request.body;
6673
+ const run2 = body?.run === true;
6674
+ const sessionCtx = getSessionCtx(request);
6675
+ try {
6676
+ const store = await resolveSessionStore(request);
6677
+ const sourceSession = await store.load(sessionCtx, sourceSessionId);
6678
+ const transcript = formatSessionTranscript(sourceSession);
6679
+ const prompt = buildAnalysisPrompt(sourceSession, transcript, body?.instructions);
6680
+ let runtime = null;
6681
+ if (run2) {
6682
+ try {
6683
+ runtime = await resolveRuntime(request);
6684
+ } catch {
6685
+ return reply.code(501).send({
6686
+ error: {
6687
+ code: ERROR_CODE_NOT_IMPLEMENTED,
6688
+ message: "session analysis runtime is not configured"
6689
+ }
6690
+ });
6691
+ }
6692
+ }
6693
+ const analysisSession = await store.create(sessionCtx, {
6694
+ title: body?.title ?? `Analysis: ${sourceSession.title}`.slice(0, 200)
6695
+ });
6696
+ if (!run2) {
6697
+ return {
6698
+ sourceSession: {
6699
+ id: sourceSession.id,
6700
+ title: sourceSession.title,
6701
+ createdAt: sourceSession.createdAt,
6702
+ updatedAt: sourceSession.updatedAt,
6703
+ turnCount: sourceSession.turnCount
6704
+ },
6705
+ analysisSession,
6706
+ prompt,
6707
+ transcriptUrl: `/api/v1/agent/sessions/${encodeURIComponent(sourceSession.id)}/transcript`,
6708
+ ...body?.includeTranscript ? { transcript } : {}
6709
+ };
6710
+ }
6711
+ if (!runtime) {
6712
+ throw new Error("session analysis runtime is not configured");
6713
+ }
6714
+ const abortController = new AbortController();
6715
+ const chunks = [];
6716
+ for await (const chunk2 of runtime.harness.sendMessage(
6717
+ { sessionId: analysisSession.id, message: prompt },
6718
+ {
6719
+ abortSignal: abortController.signal,
6720
+ workdir: runtime.workdir
6721
+ }
6722
+ )) {
6723
+ chunks.push(chunk2);
6724
+ }
6725
+ return {
6726
+ sourceSession: {
6727
+ id: sourceSession.id,
6728
+ title: sourceSession.title,
6729
+ createdAt: sourceSession.createdAt,
6730
+ updatedAt: sourceSession.updatedAt,
6731
+ turnCount: sourceSession.turnCount
6732
+ },
6733
+ analysisSession,
6734
+ prompt,
6735
+ transcriptUrl: `/api/v1/agent/sessions/${encodeURIComponent(sourceSession.id)}/transcript`,
6736
+ analysisText: analysisTextFromChunks(chunks),
6737
+ ...body?.includeTranscript ? { transcript } : {}
6738
+ };
6739
+ } catch (err) {
6740
+ return classifySessionError(err, reply);
6741
+ }
6742
+ }
6743
+ );
6424
6744
  app.delete("/api/v1/agent/sessions/:id", async (request, reply) => {
6425
6745
  const params = request.params;
6426
6746
  const sessionId = requireSessionId(params.id, reply);
@@ -6741,7 +7061,9 @@ async function createAgentApp(opts = {}) {
6741
7061
  sessionChangesTracker
6742
7062
  });
6743
7063
  await app.register(sessionRoutes, {
6744
- sessionStore: harness.sessions
7064
+ sessionStore: harness.sessions,
7065
+ harness,
7066
+ workdir: runtimeBundle.workspace.root
6745
7067
  });
6746
7068
  await app.register(systemPromptRoutes, { harness });
6747
7069
  await app.register(modelsRoutes);
@@ -7193,6 +7515,13 @@ var registerAgentRoutes = async (app, opts) => {
7193
7515
  getSessionStore: async (request) => {
7194
7516
  const binding = await getBindingForRequest(request);
7195
7517
  return binding.harness.sessions;
7518
+ },
7519
+ getRuntime: async (request) => {
7520
+ const binding = await getBindingForRequest(request);
7521
+ return {
7522
+ harness: binding.harness,
7523
+ workdir: binding.runtimeBundle.workspace.root
7524
+ };
7196
7525
  }
7197
7526
  });
7198
7527
  await app.register(systemPromptRoutes, {
@@ -1,4 +1,4 @@
1
- export { A as AgentHarness, R as RunContext, S as SendMessageInput, a as SessionCtx, b as SessionDetail, c as SessionStore, d as SessionSummary } from '../harness-DpMCKbzh.js';
1
+ export { A as AgentHarness, R as RunContext, S as SendMessageInput, a as SessionCtx, b as SessionDetail, c as SessionStore, d as SessionSummary } from '../harness-DT3ZzdAN.js';
2
2
  import { W as Workspace, S as Sandbox, F as FileSearch, A as AgentTool } from '../sandbox-handle-store-DCNEJJ6b.js';
3
3
  export { E as Entry, a as ExecOptions, b as ExecResult, I as IsolatedCodeInput, c as IsolatedCodeOutput, J as JSONSchema, d as SandboxCapability, e as SandboxHandleRecord, f as SandboxHandleStore, g as Stat, T as ToolExecContext, h as ToolResult } from '../sandbox-handle-store-DCNEJJ6b.js';
4
4
  export { UIMessage, UIMessageChunk } from 'ai';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
@@ -74,7 +74,7 @@
74
74
  "use-stick-to-bottom": "^1.1.3",
75
75
  "yaml": "^2.8.3",
76
76
  "zod": "^3.25.76",
77
- "@hachej/boring-ui-kit": "0.1.10"
77
+ "@hachej/boring-ui-kit": "0.1.12"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@opentelemetry/api": "^1.9.1",