@cargo-ai/cli 1.0.86 → 1.0.88

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.
@@ -14,8 +14,16 @@ import { ensureHarnessPnpmOnPath } from "./pnpmShim.js";
14
14
  * from Claude Code's own transcript. `CARGO_ASK_TIMING=1` writes the phase
15
15
  * boundaries — and the harness's own diagnostics — to stderr. Unset, every
16
16
  * call below is a no-op and nothing is added to the stream.
17
+ *
18
+ * `CARGO_ASK_TIMING=parts` adds the whole of each stream part next to its
19
+ * type. Verbose, and the only way to see what the bridge actually sends: the
20
+ * part names alone say a tool call arrived, not that its result came back
21
+ * empty or that the field a reader is looking for is spelled differently here
22
+ * than in the bridge that emitted it.
17
23
  */
18
- const ASK_TIMING_ENABLED = process.env["CARGO_ASK_TIMING"] === "1";
24
+ const ASK_TIMING_LEVEL = process.env["CARGO_ASK_TIMING"];
25
+ const ASK_TIMING_ENABLED = ASK_TIMING_LEVEL === "1" || ASK_TIMING_LEVEL === "parts";
26
+ const ASK_TIMING_DUMPS_PARTS = ASK_TIMING_LEVEL === "parts";
19
27
  function createPhaseLog(label) {
20
28
  if (ASK_TIMING_ENABLED === false) {
21
29
  return () => undefined;
@@ -54,12 +62,24 @@ async function* timeParts(parts, phase) {
54
62
  : "unknown";
55
63
  // Text arrives one delta per token, which would bury everything else.
56
64
  if (type.startsWith("text-delta") === false) {
57
- phase(`part ${String(seen)}: ${type}`);
65
+ phase(ASK_TIMING_DUMPS_PARTS === true
66
+ ? `part ${String(seen)}: ${type} ${dumpPart(part)}`
67
+ : `part ${String(seen)}: ${type}`);
58
68
  }
59
69
  yield part;
60
70
  }
61
71
  phase(`stream ended after ${String(seen)} parts`);
62
72
  }
73
+ /** One stream part, flattened onto its log line. Truncated: some are huge. */
74
+ function dumpPart(part) {
75
+ try {
76
+ const json = JSON.stringify(part, (_key, value) => value instanceof Error ? `Error(${value.message})` : value);
77
+ return json === undefined ? "" : json.slice(0, 900);
78
+ }
79
+ catch {
80
+ return "<unserializable>";
81
+ }
82
+ }
63
83
  const LOCAL_WORKING_COPY_INSTRUCTIONS = `You are working in the user's local working copy, not a sandbox. Prefer plan over deploy.
64
84
 
65
85
  The user confirms each Bash, Write, Edit and NotebookEdit call, and can only answer one at a time. Ask for a single one of those per step and wait for its result before asking for the next; two in one step leaves the second waiting on a confirmation it will never be offered. Reads (Read, Glob, Grep) and the Cargo \`mcp__cargo__*\` tools need no confirmation — batch those freely.
@@ -395,6 +415,11 @@ export async function createLocalHarness(input) {
395
415
  handlers: options,
396
416
  accumulator,
397
417
  unanswered,
418
+ // Interrupt Claude Code through the turn's abort signal so the
419
+ // bridge stops cleanly. Without this the host walks away from the
420
+ // stream and teardown looks like "bridge closed before the turn
421
+ // finished" rather than an intentional stop.
422
+ onStall: options === undefined ? undefined : options.onStall,
398
423
  });
399
424
  }
400
425
  catch (error) {
@@ -520,11 +545,14 @@ export async function runPromptTurn(input) {
520
545
  const stallTimeoutMs = input.stallTimeoutMs === undefined
521
546
  ? TURN_STALL_TIMEOUT_MS
522
547
  : input.stallTimeoutMs;
548
+ const toolStallTimeoutMs = input.toolStallTimeoutMs === undefined
549
+ ? TURN_TOOL_STALL_TIMEOUT_MS
550
+ : input.toolStallTimeoutMs;
523
551
  const assistantChunks = accumulator.chunks;
524
552
  const toolCalls = new Map();
525
553
  const turnState = {
526
554
  toolRanSinceText: false,
527
- inFlightToolCallIds: new Set(),
555
+ announced: [],
528
556
  };
529
557
  let stream = await input.driver.start();
530
558
  let round = 0;
@@ -537,6 +565,8 @@ export async function runPromptTurn(input) {
537
565
  usage: accumulator.usage,
538
566
  turnState,
539
567
  stallTimeoutMs,
568
+ toolStallTimeoutMs,
569
+ onStall: input.onStall,
540
570
  });
541
571
  if (approvals.length === 0) {
542
572
  break;
@@ -549,7 +579,6 @@ export async function runPromptTurn(input) {
549
579
  throw new Error(`Local Claude Code asked for tool approval ${String(MAX_APPROVAL_ROUNDS)} times in one turn without finishing. Stopping the turn.`);
550
580
  }
551
581
  const continuations = [];
552
- const approvedToolCallIds = [];
553
582
  for (const approval of approvals) {
554
583
  // Refused before the user is asked: not their call to make, and a
555
584
  // prompt for it could not say what it was granting.
@@ -561,8 +590,13 @@ export async function runPromptTurn(input) {
561
590
  : onApproval === undefined
562
591
  ? false
563
592
  : await onApproval(approval);
564
- if (approved === true) {
565
- approvedToolCallIds.push(approval.toolCallId);
593
+ // Recorded against the call so the retraction pass below knows it is
594
+ // settled: an approved one is running and must be left alone, a refused
595
+ // one is certainly not and needs no reprieve to find out. Absent when
596
+ // its result already arrived, which takes it off the list.
597
+ const announced = turnState.announced.find((call) => call.toolCallId === approval.toolCallId);
598
+ if (announced !== undefined) {
599
+ announced.status = approved === true ? "approved" : "refused";
566
600
  }
567
601
  // Answered, so the drain on a later turn must not deny it again.
568
602
  if (input.unanswered !== undefined) {
@@ -588,15 +622,7 @@ export async function runPromptTurn(input) {
588
622
  },
589
623
  });
590
624
  }
591
- // Only the tools just approved are actually going to run. The harness
592
- // surfaces one approval per stream, so a sibling the model announced in
593
- // the same stream was never asked about and will never report a result —
594
- // left in the set it would tell the stall watchdog a tool is in flight
595
- // for the rest of the turn, turning a silent stream into a hang.
596
- turnState.inFlightToolCallIds.clear();
597
- for (const toolCallId of approvedToolCallIds) {
598
- turnState.inFlightToolCallIds.add(toolCallId);
599
- }
625
+ retractAbandonedCalls(turnState.announced, handlers.onActivity);
600
626
  stream = await input.driver.continueWithApprovals(continuations);
601
627
  }
602
628
  const streamed = assistantChunks.join("");
@@ -612,14 +638,94 @@ export async function runPromptTurn(input) {
612
638
  }
613
639
  return fallback;
614
640
  }
641
+ /**
642
+ * Retract the calls the caller was told about and will never hear the end of,
643
+ * at the boundary where the answers to this round's approvals are known.
644
+ *
645
+ * Left on screen instead, an abandoned call outlives the work: no result is
646
+ * coming, so no `end` event is either, and the caller keeps showing a tool
647
+ * that stopped existing.
648
+ *
649
+ * `"approved"` is kept, and `"running"` is the one case that cannot be judged
650
+ * yet — the reason this is not a plain sweep. It is either the sibling whose
651
+ * approval request the harness lost (it surfaces one per stream) or a tool
652
+ * Claude Code ran without asking, whose result has simply not arrived. Both
653
+ * look identical here, so it gets one more stream to prove itself: a live one
654
+ * reports its result and leaves this list on its own.
655
+ *
656
+ * Everything else is certain and retracted — `"streaming"` never finished
657
+ * arriving, `"refused"` was told no, and `"abandoning"` already had its
658
+ * stream and stayed silent. Refusals matter especially: a tool
659
+ * `isAlwaysDenied` refuses never reaches `onApproval`, so this is the caller's
660
+ * only notice.
661
+ *
662
+ * The reprieve is for the caller's benefit only — `"abandoning"` is not an
663
+ * executing status, so the stall watchdog keeps its short deadline throughout
664
+ * it. Extending the reprieve to the watchdog would deadlock the very hang it
665
+ * exists to prevent: a dropped sibling would hold the timer off, the stream
666
+ * would stay silent, and the next boundary, the thing that resolves the
667
+ * ambiguity, would never arrive.
668
+ */
669
+ function retractAbandonedCalls(announced, onActivity) {
670
+ for (const call of [...announced]) {
671
+ if (call.status === "approved") {
672
+ continue;
673
+ }
674
+ if (call.status === "running") {
675
+ call.status = "abandoning";
676
+ continue;
677
+ }
678
+ announced.splice(announced.indexOf(call), 1);
679
+ if (onActivity !== undefined) {
680
+ onActivity({
681
+ kind: "dropped",
682
+ toolCallId: call.toolCallId,
683
+ toolName: call.toolName,
684
+ });
685
+ }
686
+ }
687
+ }
615
688
  /**
616
689
  * How long a turn may produce nothing at all while no tool is running.
617
690
  *
618
691
  * Generous on purpose: this is a deadlock detector, not a latency budget. The
619
692
  * clock only runs when nothing is in flight, so the wait is over a turn that
620
693
  * is neither working nor talking.
694
+ *
695
+ * `CARGO_ASK_STALL_TIMEOUT_MS` shortens it for the end-to-end tests, which
696
+ * would otherwise have to sit out the real deadline to prove a hang is caught.
621
697
  */
622
- const TURN_STALL_TIMEOUT_MS = 120_000;
698
+ const TURN_STALL_TIMEOUT_MS = readTimeoutEnv("CARGO_ASK_STALL_TIMEOUT_MS", 120_000);
699
+ /**
700
+ * The same deadline, for a turn that has a tool executing.
701
+ *
702
+ * A tool at work is silent by design — a test run says nothing for as long as
703
+ * it takes — so this cannot be the deadline above. But it cannot be *no*
704
+ * deadline either, which is what it used to be: a tool the user approved and
705
+ * that never reported back left the turn waiting forever, spinner and all,
706
+ * with nothing to do but kill the terminal. That is the shape of every "it got
707
+ * stuck on the tool" report.
708
+ *
709
+ * Twelve minutes, from Claude Code's own ten-minute ceiling on a Bash tool.
710
+ * That is the bound for the common case, not for every case: the ceiling is
711
+ * configurable, and it does not apply to an MCP tool at all. A `mcp__cargo__*`
712
+ * call that genuinely runs longer than this is cut off with its work lost, so
713
+ * `CARGO_ASK_TOOL_STALL_TIMEOUT_MS` raises it for a workspace that does that
714
+ * routinely. The trade is deliberate: a deadline that covers every conceivable
715
+ * tool is indistinguishable from the hang it replaced.
716
+ *
717
+ * Recoverable either way — the session is replaced and the chat, its history
718
+ * and the working copy all survive.
719
+ */
720
+ const TURN_TOOL_STALL_TIMEOUT_MS = readTimeoutEnv("CARGO_ASK_TOOL_STALL_TIMEOUT_MS", 12 * 60_000);
721
+ function readTimeoutEnv(name, fallbackMs) {
722
+ const raw = process.env[name];
723
+ if (raw === undefined) {
724
+ return fallbackMs;
725
+ }
726
+ const parsed = Number(raw);
727
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
728
+ }
623
729
  /**
624
730
  * How long teardown may take before the session is abandoned. Short, because
625
731
  * the caller is usually someone who just pressed Ctrl-C.
@@ -644,6 +750,29 @@ export class HarnessTurnStuck extends Error {
644
750
  this.name = "HarnessTurnStuck";
645
751
  }
646
752
  }
753
+ /**
754
+ * The turn produced nothing for `TURN_STALL_TIMEOUT_MS` while no tool was in
755
+ * flight. Common causes: a step that asked to run several confirmable tools at
756
+ * once (the harness only surfaces one approval), or the Claude Code bridge
757
+ * going silent. Recoverable — replace the session and ask again.
758
+ */
759
+ export class HarnessTurnStalled extends Error {
760
+ constructor(timeoutMs, whileToolRunning = false) {
761
+ const seconds = String(Math.round(timeoutMs / 1000));
762
+ super(whileToolRunning === true
763
+ ? `A tool has been running for ${seconds}s without reporting anything back, which is longer than Claude Code lets any tool run. Treating it as never coming back rather than waiting on it.`
764
+ : `Local Claude Code went quiet for ${seconds}s with no tool running. Often this is a step that asked to run several confirmable tools at once (only the first can be answered), or the coding session itself stalled.`);
765
+ this.name = "HarnessTurnStalled";
766
+ }
767
+ }
768
+ /**
769
+ * The Claude Code bridge dropped mid-turn. Same recovery as a stall: the
770
+ * process behind the session is gone, so the next question needs a new one.
771
+ */
772
+ export function isBridgeClosedError(error) {
773
+ return (error instanceof Error &&
774
+ error.message.includes("bridge closed before the turn finished") === true);
775
+ }
647
776
  /**
648
777
  * Fail a turn that has gone silent with no tool running, instead of hanging.
649
778
  *
@@ -654,11 +783,15 @@ export class HarnessTurnStuck extends Error {
654
783
  * `LOCAL_WORKING_COPY_INSTRUCTIONS` asks for one confirmable tool per step to
655
784
  * avoid it; this is the backstop for when the model does it anyway.
656
785
  *
657
- * The timer is armed only while no tool is in flight — a ten-minute test run
658
- * is silent too, and must not be cut off.
786
+ * Two deadlines rather than one, because a tool at work is silent by design
787
+ * and a ten-minute test run must not be cut off: `timeoutMs` for a stream with
788
+ * nothing executing, and the far longer `toolTimeoutMs` while a tool is. What
789
+ * there is never again is *no* deadline — a tool that never reports back used
790
+ * to leave the turn on the spinner forever.
659
791
  */
660
- async function* withStallTimeout(stream, timeoutMs, isToolInFlight) {
661
- if (Number.isFinite(timeoutMs) === false) {
792
+ async function* withStallTimeout(stream, timeoutMs, toolTimeoutMs, isToolInFlight, onStall) {
793
+ if (Number.isFinite(timeoutMs) === false &&
794
+ Number.isFinite(toolTimeoutMs) === false) {
662
795
  for await (const part of stream) {
663
796
  yield part;
664
797
  }
@@ -667,9 +800,11 @@ async function* withStallTimeout(stream, timeoutMs, isToolInFlight) {
667
800
  const iterator = stream[Symbol.asyncIterator]();
668
801
  for (;;) {
669
802
  const next = iterator.next();
670
- const result = await (isToolInFlight() === true
803
+ const running = isToolInFlight();
804
+ const budgetMs = running === true ? toolTimeoutMs : timeoutMs;
805
+ const result = await (Number.isFinite(budgetMs) === false
671
806
  ? next
672
- : rejectIfIdle(next, timeoutMs));
807
+ : rejectIfIdle(next, budgetMs, onStall, running));
673
808
  if (result.done === true) {
674
809
  return;
675
810
  }
@@ -699,14 +834,22 @@ export async function settleWithin(work, timeoutMs) {
699
834
  }
700
835
  }
701
836
  }
702
- async function rejectIfIdle(next, timeoutMs) {
837
+ async function rejectIfIdle(next, timeoutMs, onStall, whileToolRunning) {
703
838
  let timer;
704
839
  try {
705
840
  return await Promise.race([
706
841
  next,
707
842
  new Promise((_resolve, reject) => {
708
843
  timer = setTimeout(() => {
709
- reject(new Error(`Local Claude Code went quiet for ${String(Math.round(timeoutMs / 1000))}s with no tool running. A single step that asks to run several tools needing confirmation ends up here: only the first can be answered, and the rest leave the turn waiting. Ask again, or in smaller steps.`));
844
+ // Reject first so Promise.race settles on HarnessTurnStalled, not
845
+ // on an AbortError the interrupt below may race onto `next`.
846
+ reject(new HarnessTurnStalled(timeoutMs, whileToolRunning));
847
+ // Then stop the bridge so teardown is an intentional abort, not a
848
+ // mysterious "bridge closed before the turn finished" after we
849
+ // have already walked away from the stream.
850
+ if (onStall !== undefined) {
851
+ onStall();
852
+ }
710
853
  }, timeoutMs);
711
854
  }),
712
855
  ]);
@@ -759,19 +902,23 @@ export function isAutoApproved(toolName) {
759
902
  return AUTO_APPROVED_TOOL_NAMES.has(toolName);
760
903
  }
761
904
  /**
762
- * `Agent` spawns a subagent to run a task of its own. It does reach the
763
- * approval prompt — the bridge defaults an unlisted tool to `edit`, which
764
- * `allow-reads` asks about — but approving it is a different kind of decision
765
- * from approving a `Bash`: the subagent runs its own tools, can be asked for
766
- * with `mode: "bypassPermissions"`, and whether its calls come back here for
767
- * individual approval is not something we control or currently know.
905
+ * `Agent` spawns a subagent to run a task of its own, and approving one is a
906
+ * different kind of decision from approving a `Bash`: the subagent runs its
907
+ * own tools, can be asked for with `mode: "bypassPermissions"`, and whether
908
+ * its calls come back here for individual approval is not something we control
909
+ * or currently know.
768
910
  *
769
- * Until that is established, refuse it. A prompt that cannot describe what it
770
- * is granting is worse than not offering the capability, and nothing in the
771
- * adapter settings or the harness's `inactiveTools` can withhold this tool at
772
- * source — the approval is the only lever there is.
911
+ * The refusal that actually stops it is in `claudeConfig.ts`, as a deny rule
912
+ * in Claude Code's own settings — this never fired, because the approval
913
+ * request it waits for is never sent. The bridge writes `ask` rules only for
914
+ * the tools in its `NATIVE_TOOL_KINDS` map, the subagent tools are not in it,
915
+ * and so `allow-reads` left them running unattended.
916
+ *
917
+ * Kept as the second line: a user who points `CLAUDE_CONFIG_DIR` at their own
918
+ * Claude Code config gets no deny rule written for them, and a future bridge
919
+ * that does surface the approval should find an answer waiting.
773
920
  */
774
- const DENIED_TOOL_NAMES = new Set(["Agent"]);
921
+ const DENIED_TOOL_NAMES = new Set(["Agent", "Task"]);
775
922
  const DENIED_TOOL_REASON = "Spawning a subagent is not available in this session. Do the work in this turn instead, asking for each tool you need.";
776
923
  /** Exported for tests. */
777
924
  export function isAlwaysDenied(toolName) {
@@ -794,7 +941,7 @@ async function consumeTurn(input) {
794
941
  const approvals = [];
795
942
  const { handlers } = input;
796
943
  const pendingFiles = new Map();
797
- const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, () => input.turnState.inFlightToolCallIds.size > 0);
944
+ const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, input.toolStallTimeoutMs, () => input.turnState.announced.some((call) => call.status === "running" || call.status === "approved"), input.onStall);
798
945
  for await (const rawPart of parts) {
799
946
  if (typeof rawPart !== "object" || rawPart === null) {
800
947
  continue;
@@ -808,6 +955,43 @@ async function consumeTurn(input) {
808
955
  ? error
809
956
  : "Local Claude Code turn failed.");
810
957
  }
958
+ // Claude Code streams the tool name before the full call is assembled.
959
+ // Surface that early so the spinner is not stuck on "Asking…" while the
960
+ // input is still arriving — and so a later `tool-call` can fill in the
961
+ // summary without looking like a second tool.
962
+ if (part["type"] === "tool-input-start") {
963
+ const toolCallId = part["id"];
964
+ const toolName = part["toolName"];
965
+ if (typeof toolCallId === "string" && typeof toolName === "string") {
966
+ if (input.toolCalls.has(toolCallId) === false) {
967
+ input.toolCalls.set(toolCallId, {
968
+ toolName,
969
+ input: undefined,
970
+ });
971
+ input.turnState.toolRanSinceText = true;
972
+ // Announced, because the spinner is about to show this name and is
973
+ // owed a retraction if the call never happens — but `"streaming"`,
974
+ // not executing. The tool is not running yet, its input is still
975
+ // being generated, and the deltas that carry it are output the
976
+ // watchdog is entitled to expect; standing it down here would turn a
977
+ // bridge that dies mid-input into a hang.
978
+ input.turnState.announced.push({
979
+ toolCallId,
980
+ toolName,
981
+ status: "streaming",
982
+ });
983
+ if (handlers.onActivity !== undefined) {
984
+ handlers.onActivity({
985
+ kind: "start",
986
+ toolCallId,
987
+ toolName,
988
+ summary: "",
989
+ });
990
+ }
991
+ }
992
+ }
993
+ continue;
994
+ }
811
995
  if (part["type"] === "tool-call") {
812
996
  const toolCallId = part["toolCallId"];
813
997
  const toolName = part["toolName"];
@@ -817,10 +1001,26 @@ async function consumeTurn(input) {
817
1001
  input: part["input"],
818
1002
  });
819
1003
  input.turnState.toolRanSinceText = true;
820
- input.turnState.inFlightToolCallIds.add(toolCallId);
1004
+ // The full call is assembled, so something is executing it — that is
1005
+ // what stands the stall watchdog down, and it deliberately waits until
1006
+ // here rather than firing at `tool-input-start`.
1007
+ const announced = input.turnState.announced.find((call) => call.toolCallId === toolCallId);
1008
+ if (announced === undefined) {
1009
+ input.turnState.announced.push({
1010
+ toolCallId,
1011
+ toolName,
1012
+ status: "running",
1013
+ });
1014
+ }
1015
+ else {
1016
+ announced.status = "running";
1017
+ }
1018
+ // Re-emitted when `tool-input-start` already announced the name, so
1019
+ // the spinner can pick up the input summary once it is known.
821
1020
  if (handlers.onActivity !== undefined) {
822
1021
  handlers.onActivity({
823
1022
  kind: "start",
1023
+ toolCallId,
824
1024
  toolName,
825
1025
  summary: summarizeToolInput(part["input"]),
826
1026
  });
@@ -842,18 +1042,28 @@ async function consumeTurn(input) {
842
1042
  ? input.toolCalls.get(toolCallId)
843
1043
  : undefined;
844
1044
  const isError = part["type"] === "tool-error";
845
- if (handlers.onActivity !== undefined) {
1045
+ // A preliminary result is progress from a tool that is still running,
1046
+ // so it must not clear the spinner or tell the stall watchdog the tool
1047
+ // is done.
1048
+ const preliminary = part["preliminary"] === true;
1049
+ if (preliminary === false &&
1050
+ typeof toolCallId === "string" &&
1051
+ handlers.onActivity !== undefined) {
846
1052
  handlers.onActivity({
847
1053
  kind: "end",
1054
+ toolCallId,
848
1055
  toolName: recorded === undefined ? "tool" : recorded.toolName,
849
1056
  isError,
850
1057
  });
851
1058
  }
852
- if (typeof toolCallId === "string") {
853
- // A preliminary result is progress from a tool that is still running,
854
- // so it must not tell the stall watchdog the tool is done.
855
- if (part["preliminary"] !== true) {
856
- input.turnState.inFlightToolCallIds.delete(toolCallId);
1059
+ if (typeof toolCallId === "string" && preliminary === false) {
1060
+ // Off the list on the way out. The `end` above is the caller's
1061
+ // retraction, so a later approval round must not announce a second
1062
+ // one — and leaving under its own steam is how a tool that ran unasked
1063
+ // proves it was alive rather than abandoned.
1064
+ const at = input.turnState.announced.findIndex((call) => call.toolCallId === toolCallId);
1065
+ if (at !== -1) {
1066
+ input.turnState.announced.splice(at, 1);
857
1067
  }
858
1068
  const touched = pendingFiles.get(toolCallId);
859
1069
  pendingFiles.delete(toolCallId);
@@ -943,19 +1153,35 @@ function readCount(value) {
943
1153
  ? value
944
1154
  : 0;
945
1155
  }
946
- function readClaudeCodeCostUsd(part) {
947
- const harnessMetadata = part["harnessMetadata"];
948
- if (typeof harnessMetadata !== "object" || harnessMetadata === null) {
949
- return undefined;
950
- }
951
- const claudeCode = harnessMetadata["claude-code"];
952
- if (typeof claudeCode !== "object" || claudeCode === null) {
953
- return undefined;
1156
+ /**
1157
+ * What Claude Code says the turn cost, off the `finish` part.
1158
+ *
1159
+ * Two keys, because the value is renamed on its way here: the bridge emits
1160
+ * `harnessMetadata`, and `HarnessAgent` republishes the same object as
1161
+ * `providerMetadata` on the parts this code actually reads. Looking only for
1162
+ * the bridge's name found nothing, so every local turn was ingested at a cost
1163
+ * of zero and priced into no credits at all.
1164
+ *
1165
+ * Exported for tests.
1166
+ */
1167
+ export function readClaudeCodeCostUsd(part) {
1168
+ for (const key of ["providerMetadata", "harnessMetadata"]) {
1169
+ const metadata = part[key];
1170
+ if (typeof metadata !== "object" || metadata === null) {
1171
+ continue;
1172
+ }
1173
+ const claudeCode = metadata["claude-code"];
1174
+ if (typeof claudeCode !== "object" || claudeCode === null) {
1175
+ continue;
1176
+ }
1177
+ const costUsd = claudeCode["costUsd"];
1178
+ if (typeof costUsd === "number" &&
1179
+ Number.isFinite(costUsd) &&
1180
+ costUsd > 0) {
1181
+ return costUsd;
1182
+ }
954
1183
  }
955
- const costUsd = claudeCode["costUsd"];
956
- return typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd > 0
957
- ? costUsd
958
- : undefined;
1184
+ return undefined;
959
1185
  }
960
1186
  /**
961
1187
  * The AI SDK part nests the whole `toolCall`; the harness bridge part carries
@@ -16,5 +16,11 @@ export declare class AskInterrupted extends Error {
16
16
  * Enter submits (accepting the highlighted suggestion when the menu is open).
17
17
  * Empty Enter leaves. Tab inserts the highlight without submitting.
18
18
  */
19
- export declare function readAskLine(input: NodeJS.ReadableStream, cwd: string): Promise<string | undefined>;
19
+ export declare function readAskLine(input: NodeJS.ReadableStream, cwd: string,
20
+ /**
21
+ * What the user had already typed while the last turn was running. The line
22
+ * opens with it in place and the cursor after it, so a question started
23
+ * mid-reply is continued rather than retyped.
24
+ */
25
+ initialLine?: string): Promise<string | undefined>;
20
26
  //# sourceMappingURL=prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/prompt.ts"],"names":[],"mappings":";AAiBA;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,KAAK;;CAKxC;AASD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiQ7B"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/prompt.ts"],"names":[],"mappings":";AAiBA;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,KAAK;;CAKxC;AASD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,GAAG,EAAE,MAAM;AACX;;;;GAIG;AACH,WAAW,SAAK,GACf,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiQ7B"}
@@ -25,10 +25,16 @@ function isTTYStream(stream) {
25
25
  * Enter submits (accepting the highlighted suggestion when the menu is open).
26
26
  * Empty Enter leaves. Tab inserts the highlight without submitting.
27
27
  */
28
- export async function readAskLine(input, cwd) {
28
+ export async function readAskLine(input, cwd,
29
+ /**
30
+ * What the user had already typed while the last turn was running. The line
31
+ * opens with it in place and the cursor after it, so a question started
32
+ * mid-reply is continued rather than retyped.
33
+ */
34
+ initialLine = "") {
29
35
  return new Promise((resolvePromise, rejectPromise) => {
30
- let line = "";
31
- let cursor = 0;
36
+ let line = initialLine;
37
+ let cursor = initialLine.length;
32
38
  let selected = 0;
33
39
  let painted = false;
34
40
  let finished = false;
@@ -68,6 +68,13 @@ type ChatTurnClaim = {
68
68
  */
69
69
  export declare function claimChatTurn(api: Api, chatUuid: string, ownerId: string): Promise<ChatTurnClaim>;
70
70
  export declare function doesChatExist(api: Api, chatUuid: string): Promise<boolean>;
71
+ /**
72
+ * Greedy wrap on spaces. A word longer than the width keeps its own line
73
+ * rather than being cut mid-token.
74
+ *
75
+ * Exported for tests.
76
+ */
77
+ export declare function wrapLine(text: string, width: number): string[];
71
78
  /**
72
79
  * A path inside the session directory reads better relative to it; anything
73
80
  * outside stays absolute so it is obvious the turn left the project.
@@ -78,7 +85,7 @@ export declare function displayPath(path: string, dir: string): string;
78
85
  /**
79
86
  * Same tree grammar as the palette:
80
87
  * ◇ <your message>
81
- * 🧱 reply…
88
+ * ■ reply…
82
89
  * ◆
83
90
  * Empty Enter or /exit leaves to the parent palette.
84
91
  */
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AA0BxC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAoB1D;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAKT;AAiBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,SAAS,GAAG,WAAW,CAErE;AA8ED;;;;;;;;;GASG;AACH;;;;GAIG;AACH,KAAK,aAAa,GACd;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/C;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAkBxB;AAyCD,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAsSD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ7D;AAkGD;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CA0b7B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HhB"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAmCxC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAkC1D;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAKT;AAiBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,SAAS,GAAG,WAAW,CAErE;AA8ED;;;;;;;;;GASG;AACH;;;;GAIG;AACH,KAAK,aAAa,GACd;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/C;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAkBxB;AAyCD,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAeD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9D;AAicD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ7D;AA2ID;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CAoqB7B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HhB"}