lsd-pi 1.3.9 → 1.3.11

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.
Files changed (53) hide show
  1. package/dist/loader.js +0 -0
  2. package/dist/resources/agents/scout.md +1 -0
  3. package/dist/resources/extensions/mcp-client/index.js +191 -83
  4. package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
  5. package/dist/resources/extensions/slash-commands/plan.js +67 -13
  6. package/dist/resources/extensions/subagent/agents.js +7 -0
  7. package/dist/resources/extensions/subagent/index.js +25 -8
  8. package/dist/resources/extensions/subagent/model-resolution.js +1 -0
  9. package/package.json +1 -1
  10. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +104 -2
  11. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
  12. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +39 -2
  13. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  14. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +135 -18
  15. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  16. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +21 -2
  17. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
  18. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +146 -8
  19. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
  20. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +51 -13
  21. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
  22. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  23. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +75 -4
  24. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  25. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
  26. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  29. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +31 -2
  31. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/packages/pi-coding-agent/package.json +1 -1
  33. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +129 -2
  34. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +158 -18
  35. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +163 -9
  36. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +60 -13
  37. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +86 -5
  38. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  39. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -2
  40. package/pkg/package.json +1 -1
  41. package/src/resources/agents/scout.md +1 -0
  42. package/src/resources/extensions/mcp-client/index.ts +212 -90
  43. package/src/resources/extensions/mcp-client/mcp-manager-component.ts +256 -0
  44. package/src/resources/extensions/mcp-client/tests/mcp-manager-component.test.ts +141 -0
  45. package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +18 -2
  46. package/src/resources/extensions/slash-commands/plan.ts +70 -13
  47. package/src/resources/extensions/subagent/agents.ts +9 -0
  48. package/src/resources/extensions/subagent/index.ts +30 -8
  49. package/src/resources/extensions/subagent/model-resolution.ts +1 -0
  50. package/src/resources/extensions/voice/tests/linux-ready.test.ts +34 -1
  51. package/dist/headless-query.d.ts +0 -40
  52. package/dist/headless-query.js +0 -77
  53. package/src/resources/extensions/gsd/tests/test-helpers.ts +0 -61
@@ -277,7 +277,7 @@ async function setModelIfNeeded(pi, ctx, modelRef) {
277
277
  return true;
278
278
  }
279
279
  function buildExecutionKickoffMessage(options) {
280
- const { permissionMode, executeWithSubagent = false } = options;
280
+ const { permissionMode, executeWithSubagent = false, executionNote } = options;
281
281
  const task = state.task.trim();
282
282
  if (!executeWithSubagent) {
283
283
  const details = [
@@ -287,6 +287,9 @@ function buildExecutionKickoffMessage(options) {
287
287
  details.push(`Original task: ${task}`);
288
288
  if (state.latestPlanPath)
289
289
  details.push(`Use the approved plan artifact at ${state.latestPlanPath} as the execution plan.`);
290
+ if (executionNote)
291
+ details.push(`User execution note: ${executionNote}`);
292
+ details.push("After implementation: guide the user through verification by presenting a concise checklist based on the plan's Acceptance Criteria and Verification Plan. Run applicable checks (build, lint, tests) and report results.");
290
293
  return details.join(" ");
291
294
  }
292
295
  const codingModel = readPlanModeCodingModel();
@@ -303,13 +306,24 @@ function buildExecutionKickoffMessage(options) {
303
306
  details.push(`Original task: ${task}`);
304
307
  if (state.latestPlanPath)
305
308
  details.push(`Primary plan artifact: ${state.latestPlanPath}`);
309
+ if (executionNote) {
310
+ details.push(`User execution note: ${executionNote}`);
311
+ // If the note contains a model request, surface it explicitly so the agent
312
+ // doesn't silently drop it. The model override will be resolved by
313
+ // normalizeSubagentModel when the subagent tool is invoked.
314
+ const modelMatch = executionNote.match(/\b(?:model|use\s+model)\s+["']?([\w.-]+(?:\/[\w.-]+)?)["']?\b/i);
315
+ if (modelMatch?.[1]) {
316
+ details.push(`The user explicitly requested model "${modelMatch[1]}". You MUST pass model="${modelMatch[1]}" in the subagent tool call. If normalizeSubagentModel cannot resolve this model, report the error to the user instead of silently falling back.`);
317
+ }
318
+ }
306
319
  details.push("Important: if the plan is large and you estimate it would exceed a single subagent's context window (~200k tokens), " +
307
320
  "split execution across multiple sequential subagents instead of one. " +
308
321
  "Use the subagent tool's chain mode: pass a \"chain\" array where each entry covers one self-contained phase or group of steps from the plan. " +
309
322
  "Each chain entry should include the agent name, a focused task description for that phase, and may reference {previous} to receive the prior phase's output as handoff context. " +
310
323
  "Only split when genuinely needed — prefer a single subagent for plans that fit comfortably.");
311
324
  details.push("After all subagents complete: (1) do a quick review of the implementation — check that the plan steps were actually carried out, spot obvious issues or missed pieces, and verify the code compiles/passes lint if applicable. " +
312
- "(2) Then summarize what was done, what (if anything) needs follow-up, and flag any concerns found during review.");
325
+ "(2) Then guide the user through verification: present a concise checklist based on the plan's Acceptance Criteria and Verification Plan sections. Run applicable checks (build, lint, tests) and report results. " +
326
+ "(3) Summarize what was done, what (if anything) needs follow-up, and flag any concerns found during review.");
313
327
  return details.join(" ");
314
328
  }
315
329
  let pendingNewSession = null;
@@ -331,7 +345,7 @@ function scheduleNewSession(pi, ctx) {
331
345
  // Must use the /prefix so tryExecuteExtensionCommand parses the name correctly.
332
346
  pi.executeSlashCommand("/plan-execute-new-session");
333
347
  }
334
- async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false) {
348
+ async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false, executionNote) {
335
349
  // Do NOT switch to reasoning model during execution.
336
350
  // The reasoning model is only for plan-mode investigation, not execution.
337
351
  // If a coding model is configured and we're using a subagent, the explicit
@@ -347,7 +361,7 @@ async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false)
347
361
  // subagent tool with the default session model BEFORE it ever sees the
348
362
  // explicit model="<planModeCodingModel>" instruction. Steering ensures the
349
363
  // configured plan-mode coding model reaches the subagent invocation.
350
- await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent }), { deliverAs: "steer" });
364
+ await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent, executionNote }), { deliverAs: "steer" });
351
365
  }
352
366
  async function cancelPlan(pi, ctx, clearTask = true) {
353
367
  const restoreMode = state.previousMode ?? "accept-on-edit";
@@ -359,10 +373,12 @@ async function cancelPlan(pi, ctx, clearTask = true) {
359
373
  function buildPlanModeSystemPrompt() {
360
374
  const details = [
361
375
  "You are currently in plan mode.",
376
+ "Terse output. All technical substance stays. Only fluff dies. Fragments OK.",
362
377
  "Investigate, clarify scope, and produce a persisted execution plan before making source changes.",
363
378
  "If requirements are ambiguous or constraints are missing, ask concise clarifying questions before drafting or saving a plan.",
364
379
  `Before writing or updating a plan artifact, make sure your confidence is at least ${MIN_PLAN_CONFIDENCE}/10. If confidence is lower, investigate more or ask clarifying questions first.`,
365
380
  "Include an explicit confidence line in every saved plan, for example: \"Confidence: 8/10\" or higher.",
381
+ "Every saved plan MUST include explicit \"Acceptance Criteria\" and \"Verification Plan\" sections. Plans missing these sections will be rejected for approval.",
366
382
  "When adjusting an existing saved plan, prefer the edit tool for targeted changes. Rewrite the whole file only when the structure changes substantially or an exact edit is impractical.",
367
383
  "Do not modify source files or run side-effect commands while plan mode is active.",
368
384
  "Persist plan artifacts under .lsd/plan/.",
@@ -411,6 +427,29 @@ function buildApprovalActionInstructions() {
411
427
  function buildApprovalDialogInstructions() {
412
428
  return buildApprovalActionInstructions();
413
429
  }
430
+ /** Required heading patterns for plan artifacts. Matches common variants. */
431
+ const REQUIRED_PLAN_SECTIONS = [
432
+ { pattern: /\b(acceptance\s*criteria|success\s*criteria|done\s*criteria)\b/i, label: "Acceptance Criteria" },
433
+ { pattern: /\b(verification\s*(plan|steps|strategy)|how\s+to\s+verify|testing\s*plan)\b/i, label: "Verification Plan" },
434
+ ];
435
+ function validatePlanArtifact(markdown) {
436
+ const missing = [];
437
+ for (const section of REQUIRED_PLAN_SECTIONS) {
438
+ if (!section.pattern.test(markdown)) {
439
+ missing.push(section.label);
440
+ }
441
+ }
442
+ return { valid: missing.length === 0, missing };
443
+ }
444
+ function buildPlanValidationSteeringMessage(planPath, missing) {
445
+ const missingList = missing.map((m) => `- **${m}**`).join("\n");
446
+ return [
447
+ `Plan artifact saved at ${planPath}.`,
448
+ `The plan is missing required sections before it can be approved for implementation:`,
449
+ missingList,
450
+ `Please revise the plan to include these sections, then re-save. Do not ask for approval until all required sections are present.`,
451
+ ].join("\n\n");
452
+ }
414
453
  function buildApprovalSteeringMessage(planPath) {
415
454
  return [
416
455
  `Plan artifact saved at ${planPath}.`,
@@ -621,13 +660,26 @@ export default function planCommand(pi) {
621
660
  return;
622
661
  }
623
662
  const planMarkdown = readPlanArtifact(path);
624
- pi.sendMessage({
625
- customType: "plan-mode-preview",
626
- content: buildPlanPreviewMessage(path, planMarkdown),
627
- display: true,
628
- });
629
- ctx.ui?.notify?.("/plan to show plan", "info");
630
- pi.sendUserMessage(buildApprovalSteeringMessage(path), { deliverAs: "steer" });
663
+ if (!planMarkdown) {
664
+ ctx.ui?.notify?.("Plan artifact could not be read for validation", "warning");
665
+ pi.sendUserMessage(buildApprovalSteeringMessage(path), { deliverAs: "steer" });
666
+ }
667
+ else {
668
+ const validation = validatePlanArtifact(planMarkdown);
669
+ if (!validation.valid) {
670
+ ctx.ui?.notify?.("Plan missing required sections — see guidance below", "warning");
671
+ pi.sendUserMessage(buildPlanValidationSteeringMessage(path, validation.missing), { deliverAs: "steer" });
672
+ }
673
+ else {
674
+ pi.sendMessage({
675
+ customType: "plan-mode-preview",
676
+ content: buildPlanPreviewMessage(path, planMarkdown),
677
+ display: true,
678
+ });
679
+ ctx.ui?.notify?.("/plan to show plan", "info");
680
+ pi.sendUserMessage(buildApprovalSteeringMessage(path), { deliverAs: "steer" });
681
+ }
682
+ }
631
683
  }
632
684
  return;
633
685
  }
@@ -676,12 +728,13 @@ export default function planCommand(pi) {
676
728
  permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
677
729
  executeWithSubagent: false,
678
730
  };
731
+ const permissionNote = getAnswerNote(permissionAnswer);
679
732
  state = { ...state, targetPermissionMode: executionMode.permissionMode };
680
733
  if (executionMode.executeWithSubagent) {
681
734
  const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
682
735
  ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
683
736
  }
684
- await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
737
+ await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent, permissionNote);
685
738
  return;
686
739
  }
687
740
  // ── First question answered (action) ──────────────────────────────────
@@ -699,12 +752,13 @@ export default function planCommand(pi) {
699
752
  permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
700
753
  executeWithSubagent: false,
701
754
  };
755
+ const actionNote = getAnswerNote(actionAnswer);
702
756
  state = { ...state, targetPermissionMode: executionMode.permissionMode };
703
757
  if (executionMode.executeWithSubagent) {
704
758
  const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
705
759
  ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
706
760
  }
707
- await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
761
+ await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent, actionNote);
708
762
  return;
709
763
  }
710
764
  if (actionSelection.includes(REVIEW_LABEL)) {
@@ -6,6 +6,8 @@ import * as path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent";
8
8
  const PROJECT_AGENT_DIR_CANDIDATES = [".lsd", ".gsd", ".pi"];
9
+ /** Fixed read-only tool set for the reserved `scout` agent. */
10
+ const SCOUT_ALLOWED_TOOLS = ["read", "lsp", "grep", "find", "ls"];
9
11
  function normalizeAgentModel(model) {
10
12
  const trimmed = model?.trim();
11
13
  if (!trimmed)
@@ -117,6 +119,11 @@ export function discoverAgents(cwd, scope) {
117
119
  else {
118
120
  addAgents(projectAgents);
119
121
  }
122
+ // Enforce reserved agent tool policies — scout is always read-only
123
+ const scout = agentMap.get("scout");
124
+ if (scout) {
125
+ scout.tools = [...SCOUT_ALLOWED_TOOLS];
126
+ }
120
127
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
121
128
  }
122
129
  export function formatAgentList(agents, maxItems) {
@@ -997,11 +997,13 @@ export default function (pi) {
997
997
  const hasTasks = (params.tasks?.length ?? 0) > 0;
998
998
  const hasSingle = Boolean(params.agent && params.task);
999
999
  const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
1000
- const makeDetails = (mode) => (results) => ({
1000
+ const makeDetails = (mode) => (results, opts) => ({
1001
1001
  mode,
1002
1002
  agentScope,
1003
1003
  projectAgentsDir: discovery.projectAgentsDir,
1004
1004
  results,
1005
+ totalChainSteps: opts?.totalSteps,
1006
+ currentChainStep: opts?.currentStep,
1005
1007
  });
1006
1008
  const trackInProcessDepth = (started, depth, ancestry) => {
1007
1009
  const sessionId = started.handle.sessionId;
@@ -1090,7 +1092,8 @@ export default function (pi) {
1090
1092
  for (let i = 0; i < params.chain.length; i++) {
1091
1093
  const step = params.chain[i];
1092
1094
  const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
1093
- // Create update callback that includes all previous results
1095
+ // Create update callback that includes all previous results + chain progress
1096
+ const chainTotalSteps = params.chain.length;
1094
1097
  const chainUpdate = onUpdate
1095
1098
  ? (partial) => {
1096
1099
  // Combine completed results with current streaming result
@@ -1099,7 +1102,10 @@ export default function (pi) {
1099
1102
  const allResults = [...results, currentResult];
1100
1103
  onUpdate({
1101
1104
  content: partial.content,
1102
- details: makeDetails("chain")(allResults),
1105
+ details: makeDetails("chain")(allResults, {
1106
+ totalSteps: chainTotalSteps,
1107
+ currentStep: i + 1,
1108
+ }),
1103
1109
  });
1104
1110
  }
1105
1111
  }
@@ -1117,7 +1123,7 @@ export default function (pi) {
1117
1123
  const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
1118
1124
  return {
1119
1125
  content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
1120
- details: makeDetails("chain")(results),
1126
+ details: makeDetails("chain")(results, { totalSteps: params.chain.length }),
1121
1127
  isError: true,
1122
1128
  };
1123
1129
  }
@@ -1125,7 +1131,7 @@ export default function (pi) {
1125
1131
  }
1126
1132
  return {
1127
1133
  content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
1128
- details: makeDetails("chain")(results),
1134
+ details: makeDetails("chain")(results, { totalSteps: params.chain.length }),
1129
1135
  };
1130
1136
  }
1131
1137
  if (params.tasks && params.tasks.length > 0) {
@@ -1653,13 +1659,24 @@ export default function (pi) {
1653
1659
  };
1654
1660
  if (details.mode === "chain") {
1655
1661
  const successCount = details.results.filter((r) => r.exitCode === 0).length;
1656
- const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
1662
+ const isRunning = details.currentChainStep != null;
1663
+ const icon = !isRunning && successCount === details.results.length
1664
+ ? theme.fg("success", "✓")
1665
+ : isRunning
1666
+ ? theme.fg("accent", "◉")
1667
+ : theme.fg("error", "✗");
1668
+ const totalLabel = details.totalChainSteps != null
1669
+ ? `${details.results.length}/${details.totalChainSteps} steps`
1670
+ : `${details.results.length} steps`;
1671
+ const progressLabel = isRunning
1672
+ ? ` (step ${details.currentChainStep} running)`
1673
+ : "";
1657
1674
  if (expanded) {
1658
1675
  const container = new Container();
1659
1676
  container.addChild(new Text(icon +
1660
1677
  " " +
1661
1678
  theme.fg("toolTitle", theme.bold("chain ")) +
1662
- theme.fg("accent", `${successCount}/${details.results.length} steps`), 0, 0));
1679
+ theme.fg("accent", `${totalLabel}${progressLabel}`), 0, 0));
1663
1680
  for (const r of details.results) {
1664
1681
  const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1665
1682
  const displayItems = getDisplayItems(r.messages);
@@ -1693,7 +1710,7 @@ export default function (pi) {
1693
1710
  let text = icon +
1694
1711
  " " +
1695
1712
  theme.fg("toolTitle", theme.bold("chain ")) +
1696
- theme.fg("accent", `${successCount}/${details.results.length} steps`);
1713
+ theme.fg("accent", `${totalLabel}${progressLabel}`);
1697
1714
  for (const r of details.results) {
1698
1715
  const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
1699
1716
  const displayItems = getDisplayItems(r.messages);
@@ -11,6 +11,7 @@ const BARE_MODEL_PROVIDER_RULES = [
11
11
  matches: (modelId) => /^(mistral-|ministral-|codestral-)/.test(modelId),
12
12
  },
13
13
  { provider: "groq", matches: (modelId) => modelId.startsWith("llama-") || modelId.startsWith("mixtral-") },
14
+ { provider: "zhipu", matches: (modelId) => modelId.startsWith("glm-") },
14
15
  ];
15
16
  export function inferProviderForBareModel(modelId) {
16
17
  const normalizedModelId = modelId.trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsd-pi",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "description": "LSD — Looks Sort of Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import stripAnsi from "strip-ansi";
4
- import { ToolSummaryLine } from "../tool-summary-line.js";
4
+ import { ToolSummaryLine, extractToolLabel } from "../tool-summary-line.js";
5
5
  import { initTheme } from "../../theme/theme.js";
6
6
  initTheme("dark");
7
7
  describe("ToolSummaryLine", () => {
@@ -13,7 +13,8 @@ describe("ToolSummaryLine", () => {
13
13
  assert.match(rendered, /^ ● /);
14
14
  assert.ok(rendered.includes("reading 2 files · 0.8s"));
15
15
  assert.equal(summary.canGroupWith("read"), true);
16
- assert.equal(summary.canGroupWith("find"), false);
16
+ assert.equal(summary.canGroupWith("find"), true);
17
+ assert.equal(summary.canGroupWith("bash"), false);
17
18
  assert.equal(rendered.includes("collapsed tools"), false);
18
19
  assert.equal(rendered.includes("⎯"), false);
19
20
  });
@@ -30,5 +31,106 @@ describe("ToolSummaryLine", () => {
30
31
  summary.setHidden(true);
31
32
  assert.deepEqual(summary.render(80), []);
32
33
  });
34
+ it("renders spinner and label line when pending tools exist", () => {
35
+ const summary = new ToolSummaryLine();
36
+ summary.addPendingTool("t1", "read", { path: "src/foo/bar.ts" });
37
+ const rendered = stripAnsi(summary.render(160).join("\n"));
38
+ assert.ok(!rendered.startsWith(" ●"));
39
+ assert.ok(rendered.includes(" └ bar.ts"));
40
+ assert.ok(rendered.includes("bar.ts"));
41
+ });
42
+ it("keeps last tool label after pending tool completes", () => {
43
+ const summary = new ToolSummaryLine();
44
+ summary.addPendingTool("t1", "read", { path: "file.ts" });
45
+ assert.equal(summary.hasPendingTools(), true);
46
+ summary.removePendingTool("t1");
47
+ summary.addTool("read", 500);
48
+ assert.equal(summary.hasPendingTools(), false);
49
+ const rendered = stripAnsi(summary.render(160).join("\n"));
50
+ assert.ok(rendered.includes("●"));
51
+ assert.ok(rendered.includes("0.5s"));
52
+ assert.ok(rendered.includes("└ file.ts"));
53
+ });
54
+ it("aggregates completed and pending tools in summary text", () => {
55
+ const summary = new ToolSummaryLine();
56
+ summary.addTool("read", 300);
57
+ summary.addPendingTool("t1", "grep", { pattern: "TODO" });
58
+ const rendered = stripAnsi(summary.render(160).join("\n"));
59
+ assert.ok(rendered.includes("1 file"));
60
+ assert.ok(rendered.includes("1 pattern"));
61
+ assert.ok(rendered.includes("…"));
62
+ assert.ok(rendered.includes("TODO"));
63
+ });
64
+ it("shows expand hint when set and tools are pending", () => {
65
+ const summary = new ToolSummaryLine();
66
+ summary.setExpandHint("(ctrl+o to expand)");
67
+ summary.addPendingTool("t1", "read", { path: "test.ts" });
68
+ const rendered = stripAnsi(summary.render(160).join("\n"));
69
+ assert.ok(rendered.includes("ctrl+o to expand"));
70
+ });
71
+ it("does not show expand hint when no pending tools", () => {
72
+ const summary = new ToolSummaryLine();
73
+ summary.setExpandHint("(ctrl+o to expand)");
74
+ summary.addTool("read", 300);
75
+ const rendered = stripAnsi(summary.render(160).join("\n"));
76
+ assert.ok(!rendered.includes("ctrl+o to expand"));
77
+ });
78
+ it("updates label when pending tool args change", () => {
79
+ const summary = new ToolSummaryLine();
80
+ summary.addPendingTool("t1", "read", { path: "old.ts" });
81
+ let rendered = stripAnsi(summary.render(160).join("\n"));
82
+ assert.ok(rendered.includes("old.ts"));
83
+ summary.updatePendingToolArgs("t1", { path: "new.ts" });
84
+ rendered = stripAnsi(summary.render(160).join("\n"));
85
+ assert.ok(rendered.includes("new.ts"));
86
+ });
87
+ it("clears pending spinner without removing last label", () => {
88
+ const summary = new ToolSummaryLine();
89
+ summary.addPendingTool("t1", "read", { path: "file.ts" });
90
+ summary.clearPendingTools();
91
+ const rendered = stripAnsi(summary.render(160).join("\n"));
92
+ assert.ok(!rendered.includes("◯"));
93
+ assert.ok(!rendered.includes("◔"));
94
+ assert.ok(!rendered.includes("◑"));
95
+ assert.ok(!rendered.includes("◕"));
96
+ assert.ok(!rendered.includes("● Listing"));
97
+ assert.ok(!rendered.includes("…"));
98
+ assert.ok(rendered === "" || rendered.includes("file.ts"));
99
+ });
100
+ it("canGroupWith considers pending tools", () => {
101
+ const summary = new ToolSummaryLine();
102
+ summary.addPendingTool("t1", "read", { path: "a.ts" });
103
+ assert.equal(summary.canGroupWith("read"), true);
104
+ assert.equal(summary.canGroupWith("grep"), true);
105
+ assert.equal(summary.canGroupWith("bash"), false);
106
+ });
107
+ });
108
+ describe("extractToolLabel", () => {
109
+ it("extracts basename for read tool", () => {
110
+ assert.equal(extractToolLabel("read", { path: "src/foo/bar.ts" }), "bar.ts");
111
+ assert.equal(extractToolLabel("read", { file_path: "/abs/path/file.json" }), "file.json");
112
+ assert.equal(extractToolLabel("read", {}), "read");
113
+ });
114
+ it("extracts pattern for grep tool", () => {
115
+ assert.equal(extractToolLabel("grep", { pattern: "TODO" }), "TODO");
116
+ assert.equal(extractToolLabel("grep", {}), "grep");
117
+ });
118
+ it("extracts pattern for find tool", () => {
119
+ assert.equal(extractToolLabel("find", { pattern: "*.ts" }), "*.ts");
120
+ assert.equal(extractToolLabel("find", {}), "find");
121
+ });
122
+ it("extracts path for ls tool", () => {
123
+ assert.equal(extractToolLabel("ls", { path: "src/components" }), "components");
124
+ assert.equal(extractToolLabel("ls", {}), ".");
125
+ });
126
+ it("extracts symbol or file for lsp tool", () => {
127
+ assert.equal(extractToolLabel("lsp", { symbol: "MyClass" }), "MyClass");
128
+ assert.equal(extractToolLabel("lsp", { file: "src/index.ts" }), "index.ts");
129
+ assert.equal(extractToolLabel("lsp", { symbol: "foo", file: "bar.ts" }), "foo");
130
+ assert.equal(extractToolLabel("lsp", {}), "lsp");
131
+ });
132
+ it("returns tool name for unknown tools", () => {
133
+ assert.equal(extractToolLabel("custom_tool", { whatever: "value" }), "custom_tool");
134
+ });
33
135
  });
34
136
  //# sourceMappingURL=tool-summary-line.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-summary-line.test.js","sourceRoot":"","sources":["../../../../../src/modes/interactive/components/__tests__/tool-summary-line.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,SAAS,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,SAAS,CAAC,MAAM,CAAC,CAAC;AAElB,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACrE,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE7B,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/B,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QAClD,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QAEpC,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC/C,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAEzC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport stripAnsi from \"strip-ansi\";\n\nimport { ToolSummaryLine } from \"../tool-summary-line.js\";\nimport { initTheme } from \"../../theme/theme.js\";\n\ninitTheme(\"dark\");\n\ndescribe(\"ToolSummaryLine\", () => {\n\tit(\"renders action-based summaries for grouped identical tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"read\", 600);\n\t\tsummary.addTool(\"read\", 150);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.match(rendered, /^ ● /);\n\t\tassert.ok(rendered.includes(\"reading 2 files · 0.8s\"));\n\t\tassert.equal(summary.canGroupWith(\"read\"), true);\n\t\tassert.equal(summary.canGroupWith(\"find\"), false);\n\t\tassert.equal(rendered.includes(\"collapsed tools\"), false);\n\t\tassert.equal(rendered.includes(\"⎯\"), false);\n\t});\n\n\tit(\"keeps fallback format for unknown tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"custom_tool\", 100);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"custom_tool · 0.1s\"));\n\t});\n\n\tit(\"renders nothing when empty or hidden\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tassert.deepEqual(summary.render(80), []);\n\n\t\tsummary.addTool(\"grep\", 100);\n\t\tsummary.setHidden(true);\n\t\tassert.deepEqual(summary.render(80), []);\n\t});\n});\n"]}
1
+ {"version":3,"file":"tool-summary-line.test.js","sourceRoot":"","sources":["../../../../../src/modes/interactive/components/__tests__/tool-summary-line.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport stripAnsi from \"strip-ansi\";\n\nimport { ToolSummaryLine, extractToolLabel } from \"../tool-summary-line.js\";\nimport { initTheme } from \"../../theme/theme.js\";\n\ninitTheme(\"dark\");\n\ndescribe(\"ToolSummaryLine\", () => {\n\tit(\"renders action-based summaries for grouped identical tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"read\", 600);\n\t\tsummary.addTool(\"read\", 150);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.match(rendered, /^ ● /);\n\t\tassert.ok(rendered.includes(\"reading 2 files · 0.8s\"));\n\t\tassert.equal(summary.canGroupWith(\"read\"), true);\n\t\tassert.equal(summary.canGroupWith(\"find\"), true);\n\t\tassert.equal(summary.canGroupWith(\"bash\"), false);\n\t\tassert.equal(rendered.includes(\"collapsed tools\"), false);\n\t\tassert.equal(rendered.includes(\"⎯\"), false);\n\t});\n\n\tit(\"keeps fallback format for unknown tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"custom_tool\", 100);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"custom_tool · 0.1s\"));\n\t});\n\n\tit(\"renders nothing when empty or hidden\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tassert.deepEqual(summary.render(80), []);\n\n\t\tsummary.addTool(\"grep\", 100);\n\t\tsummary.setHidden(true);\n\t\tassert.deepEqual(summary.render(80), []);\n\t});\n\n\tit(\"renders spinner and label line when pending tools exist\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addPendingTool(\"t1\", \"read\", { path: \"src/foo/bar.ts\" });\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(!rendered.startsWith(\" ●\"));\n\t\tassert.ok(rendered.includes(\" └ bar.ts\"));\n\t\tassert.ok(rendered.includes(\"bar.ts\"));\n\t});\n\n\tit(\"keeps last tool label after pending tool completes\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addPendingTool(\"t1\", \"read\", { path: \"file.ts\" });\n\t\tassert.equal(summary.hasPendingTools(), true);\n\n\t\tsummary.removePendingTool(\"t1\");\n\t\tsummary.addTool(\"read\", 500);\n\t\tassert.equal(summary.hasPendingTools(), false);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"●\"));\n\t\tassert.ok(rendered.includes(\"0.5s\"));\n\t\tassert.ok(rendered.includes(\"└ file.ts\"));\n\t});\n\n\tit(\"aggregates completed and pending tools in summary text\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"read\", 300);\n\t\tsummary.addPendingTool(\"t1\", \"grep\", { pattern: \"TODO\" });\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"1 file\"));\n\t\tassert.ok(rendered.includes(\"1 pattern\"));\n\t\tassert.ok(rendered.includes(\"…\"));\n\t\tassert.ok(rendered.includes(\"TODO\"));\n\t});\n\n\tit(\"shows expand hint when set and tools are pending\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.setExpandHint(\"(ctrl+o to expand)\");\n\t\tsummary.addPendingTool(\"t1\", \"read\", { path: \"test.ts\" });\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"ctrl+o to expand\"));\n\t});\n\n\tit(\"does not show expand hint when no pending tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.setExpandHint(\"(ctrl+o to expand)\");\n\t\tsummary.addTool(\"read\", 300);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(!rendered.includes(\"ctrl+o to expand\"));\n\t});\n\n\tit(\"updates label when pending tool args change\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addPendingTool(\"t1\", \"read\", { path: \"old.ts\" });\n\n\t\tlet rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"old.ts\"));\n\n\t\tsummary.updatePendingToolArgs(\"t1\", { path: \"new.ts\" });\n\t\trendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"new.ts\"));\n\t});\n\n\tit(\"clears pending spinner without removing last label\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addPendingTool(\"t1\", \"read\", { path: \"file.ts\" });\n\n\t\tsummary.clearPendingTools();\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(!rendered.includes(\"◯\"));\n\t\tassert.ok(!rendered.includes(\"◔\"));\n\t\tassert.ok(!rendered.includes(\"◑\"));\n\t\tassert.ok(!rendered.includes(\"◕\"));\n\t\tassert.ok(!rendered.includes(\"● Listing\"));\n\t\tassert.ok(!rendered.includes(\"…\"));\n\t\tassert.ok(rendered === \"\" || rendered.includes(\"file.ts\"));\n\t});\n\n\tit(\"canGroupWith considers pending tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addPendingTool(\"t1\", \"read\", { path: \"a.ts\" });\n\n\t\tassert.equal(summary.canGroupWith(\"read\"), true);\n\t\tassert.equal(summary.canGroupWith(\"grep\"), true);\n\t\tassert.equal(summary.canGroupWith(\"bash\"), false);\n\t});\n});\n\ndescribe(\"extractToolLabel\", () => {\n\tit(\"extracts basename for read tool\", () => {\n\t\tassert.equal(extractToolLabel(\"read\", { path: \"src/foo/bar.ts\" }), \"bar.ts\");\n\t\tassert.equal(extractToolLabel(\"read\", { file_path: \"/abs/path/file.json\" }), \"file.json\");\n\t\tassert.equal(extractToolLabel(\"read\", {}), \"read\");\n\t});\n\n\tit(\"extracts pattern for grep tool\", () => {\n\t\tassert.equal(extractToolLabel(\"grep\", { pattern: \"TODO\" }), \"TODO\");\n\t\tassert.equal(extractToolLabel(\"grep\", {}), \"grep\");\n\t});\n\n\tit(\"extracts pattern for find tool\", () => {\n\t\tassert.equal(extractToolLabel(\"find\", { pattern: \"*.ts\" }), \"*.ts\");\n\t\tassert.equal(extractToolLabel(\"find\", {}), \"find\");\n\t});\n\n\tit(\"extracts path for ls tool\", () => {\n\t\tassert.equal(extractToolLabel(\"ls\", { path: \"src/components\" }), \"components\");\n\t\tassert.equal(extractToolLabel(\"ls\", {}), \".\");\n\t});\n\n\tit(\"extracts symbol or file for lsp tool\", () => {\n\t\tassert.equal(extractToolLabel(\"lsp\", { symbol: \"MyClass\" }), \"MyClass\");\n\t\tassert.equal(extractToolLabel(\"lsp\", { file: \"src/index.ts\" }), \"index.ts\");\n\t\tassert.equal(extractToolLabel(\"lsp\", { symbol: \"foo\", file: \"bar.ts\" }), \"foo\");\n\t\tassert.equal(extractToolLabel(\"lsp\", {}), \"lsp\");\n\t});\n\n\tit(\"returns tool name for unknown tools\", () => {\n\t\tassert.equal(extractToolLabel(\"custom_tool\", { whatever: \"value\" }), \"custom_tool\");\n\t});\n});\n"]}
@@ -1,8 +1,30 @@
1
1
  import type { AssistantMessage } from "@gsd/pi-ai";
2
- import { Container, type MarkdownTheme } from "@gsd/pi-tui";
2
+ import type { Component } from "@gsd/pi-tui";
3
+ import { Container, Markdown, type MarkdownTheme, Text } from "@gsd/pi-tui";
3
4
  import { type TimestampFormat } from "./timestamp.js";
4
5
  /**
5
- * Component that renders a complete assistant message
6
+ * Create a Markdown component for an assistant text block.
7
+ * @param text - Text content (should be trimmed by caller)
8
+ * @param withMarker - Whether to prefix with the response marker
9
+ * @param markdownTheme - Markdown theme
10
+ */
11
+ export declare function createTextMarkdown(text: string, withMarker: boolean, markdownTheme: MarkdownTheme): Markdown;
12
+ /**
13
+ * Create a Markdown component for a thinking block.
14
+ */
15
+ export declare function createThinkingMarkdown(thinking: string, markdownTheme: MarkdownTheme): Markdown;
16
+ /**
17
+ * Create an error/abort Text component.
18
+ */
19
+ export declare function createErrorText(message: string): Text;
20
+ /**
21
+ * Component that renders a complete assistant message.
22
+ *
23
+ * Supports two rendering modes:
24
+ * 1. Legacy: `updateContent(message)` renders all text/thinking into a contentContainer.
25
+ * Tool rows are expected to be added as siblings in the parent container.
26
+ * 2. Interleaved: `updateContentOrdered(message, toolComponents)` renders text/thinking
27
+ * AND tool components in content order. Tool components become children of this container.
6
28
  */
7
29
  export declare class AssistantMessageComponent extends Container {
8
30
  private contentContainer;
@@ -15,6 +37,21 @@ export declare class AssistantMessageComponent extends Container {
15
37
  invalidate(): void;
16
38
  setHideThinkingBlock(hide: boolean): void;
17
39
  setThinkingLevel(level: string): void;
40
+ /**
41
+ * Legacy rendering: renders text/thinking blocks into contentContainer.
42
+ * Stops rendering at the first tool-type block (toolCall/serverToolUse).
43
+ * Post-tool text blocks are handled by the chat-controller to preserve
44
+ * content ordering relative to tool rows.
45
+ */
18
46
  updateContent(message: AssistantMessage): void;
47
+ /**
48
+ * Interleaved rendering: renders text/thinking AND tool components in content order.
49
+ * Tool components become children of this container, preserving visual ordering.
50
+ *
51
+ * @param message - The assistant message
52
+ * @param toolComponents - Map of content block ID → pre-created Component (ToolExecutionComponent etc.)
53
+ * @returns Map of content block ID → the tool Component that was placed (for pending tool tracking)
54
+ */
55
+ updateContentOrdered(message: AssistantMessage, toolComponents?: Map<string, Component>): Map<string, Component>;
19
56
  }
20
57
  //# sourceMappingURL=assistant-message.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,aAAa,CAAC;AAEpF,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEvE;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,eAAe,CAAkB;gBAGxC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,eAAe,GAAE,eAAiC,EAClD,aAAa,SAAQ;IAkBb,UAAU,IAAI,IAAI;IAO3B,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAIzC,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;CAoF9C"}
1
+ {"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAU,IAAI,EAAE,MAAM,aAAa,CAAC;AAEpF,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAUvE;;;;;GAKG;AACH,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,OAAO,EACnB,aAAa,EAAE,aAAa,GAC1B,QAAQ,CAGV;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,aAAa,GAC1B,QAAQ,CAKV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAErD;AAED;;;;;;;;GAQG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,eAAe,CAAkB;gBAGxC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,eAAe,GAAE,eAAiC,EAClD,aAAa,SAAQ;IAkBb,UAAU,IAAI,IAAI;IAO3B,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAIzC,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC;;;;;OAKG;IACH,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IA8E9C;;;;;;;OAOG;IACH,oBAAoB,CACnB,OAAO,EAAE,gBAAgB,EACzB,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACrC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC;CA+EzB"}