@workerdeck/ui 2.8.0 → 2.10.0

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 (42) hide show
  1. package/build/{SessionPanel-_lQLhWOw.d.mts → SessionPanel-BVbau2eR.d.mts} +68 -4
  2. package/build/index.d.mts +62 -9
  3. package/build/index.mjs +24 -10
  4. package/build/index.mjs.map +1 -1
  5. package/build/scoped.css +44 -0
  6. package/build/{SessionPanel-Dg4xf-ql.mjs → use-path-links-CvcxdUXL.mjs} +1080 -497
  7. package/build/use-path-links-CvcxdUXL.mjs.map +1 -0
  8. package/build/workspace.d.mts +3 -2
  9. package/build/workspace.mjs +5 -86
  10. package/build/workspace.mjs.map +1 -1
  11. package/package.json +4 -4
  12. package/src/components/agent/Composer.tsx +88 -83
  13. package/src/components/agent/PromptTokenText.tsx +11 -3
  14. package/src/components/agent/SessionInfoDialog.tsx +1 -1
  15. package/src/components/agent/SessionItem.tsx +12 -6
  16. package/src/components/agent/SessionList.tsx +1 -1
  17. package/src/components/agent/SessionPanel.tsx +308 -225
  18. package/src/components/agent/SessionWorkspace.tsx +3 -0
  19. package/src/components/agent/StatusBar.tsx +1 -1
  20. package/src/components/agent/Transcript.tsx +83 -79
  21. package/src/components/agent/TranscriptItemView.tsx +1 -1
  22. package/src/components/agent/UsageDialog.tsx +87 -6
  23. package/src/components/agent/UsageMeters.tsx +17 -4
  24. package/src/components/agent/composer-commands.ts +154 -0
  25. package/src/components/agent/use-path-links.ts +7 -26
  26. package/src/components/prompt-area/clipboard-helpers.ts +13 -4
  27. package/src/components/prompt-area/dom-helpers.ts +10 -1
  28. package/src/components/prompt-area/index.ts +10 -1
  29. package/src/components/prompt-area/prompt-area-engine.ts +23 -14
  30. package/src/components/prompt-area/types.ts +20 -0
  31. package/src/components/prompt-area/use-prompt-area-keydown.ts +2 -1
  32. package/src/components/prompt-area/use-prompt-area.ts +8 -2
  33. package/src/components/terminal/StatusLine.tsx +3 -2
  34. package/src/components/terminal/file-link.tsx +20 -0
  35. package/src/components/terminal/items.tsx +3 -2
  36. package/src/components/terminal/markdown.tsx +31 -5
  37. package/src/index.ts +14 -2
  38. package/src/lib/file-link.ts +70 -0
  39. package/src/lib/path-match.ts +51 -0
  40. package/src/lib/session-cost.ts +65 -0
  41. package/src/styles/terminal.css +15 -0
  42. package/build/SessionPanel-Dg4xf-ql.mjs.map +0 -1
@@ -5,11 +5,11 @@ import { clsx } from "clsx";
5
5
  import { extendTailwindMerge } from "tailwind-merge";
6
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Select } from "@base-ui/react/select";
8
- import { ArrowDown, ArrowDownCircle, ArrowLeft, ArrowRight, ArrowUp, AtSign, Brain, ChartPie, Check, CheckSquare, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, Circle, CircleAlert, ClipboardList, Clock, Code, Copy, Download, File, FileDiff, FileText, Folder, FolderSearch, FolderTree, Gauge, Globe, Hand, Image, Info, Link2, LoaderCircle, MessageCircleQuestion, MessageSquareText, MoreHorizontal, Paperclip, PencilLine, Plug, Power, PowerOff, Puzzle, RefreshCw, RotateCcw, RotateCw, Search, ShieldCheck, SlashSquare, Sparkles, Square, SquarePen, Terminal, TriangleAlert, UsersRound, WifiOff, Wrench, X, Zap } from "lucide-react";
8
+ import { ArrowDown, ArrowDownCircle, ArrowLeft, ArrowRight, ArrowUp, AtSign, Brain, ChartPie, Check, CheckSquare, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, Circle, CircleAlert, ClipboardList, Clock, Code, Copy, Download, File, FileDiff, FileText, Folder, FolderSearch, FolderTree, Gauge, Globe, Hand, Image, Info, Link2, LoaderCircle, MessageCircleQuestion, MessageSquareText, MoreHorizontal, Paperclip, PencilLine, Plug, Power, PowerOff, Puzzle, RefreshCw, RotateCcw, RotateCw, Search, ShieldCheck, SlashSquare, SlidersHorizontal, Sparkles, Square, SquarePen, Terminal, TriangleAlert, UsersRound, WifiOff, Wrench, X, Zap } from "lucide-react";
9
9
  import { Menu } from "@base-ui/react/menu";
10
10
  import { Dialog } from "@base-ui/react/dialog";
11
11
  import { Tooltip } from "@base-ui/react/tooltip";
12
- import { PROTOCOL_VERSION, mergeUsage, orderUsageWindows, parseTodoWriteInput, parseUserQuestions, sessionTasks, subagentLabel, taskCountLabel, taskSummary, toolTitle, usageInfos, visibleTasks } from "@workerdeck/protocol";
12
+ import { PRICING_NOTE, PROTOCOL_VERSION, UNPRICED_WARN_SHARE, canonicalModel, costOf, costOfByModel, mergeUsage, orderUsageWindows, parseTodoWriteInput, parseUserQuestions, rateFor, sessionTasks, subagentLabel, taskCountLabel, taskSummary, toolTitle, totalTokens, usageInfos, usageIsStale, visibleTasks } from "@workerdeck/protocol";
13
13
  import { recapLine, scanPromptTokens, summarizeSince, useAttachments, useClaudeSession, useDraft, useHostFileSearch, useProfileUsage, useToolCallHost } from "@workerdeck/react";
14
14
  import { Streamdown } from "streamdown";
15
15
  import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
@@ -464,13 +464,19 @@ function ProgressRing({ value, size = 13, strokeWidth = 2, className }) {
464
464
  //#endregion
465
465
  //#region src/components/prompt-area/prompt-area-engine.ts
466
466
  /**
467
+ * A chip's plain-text form: `{sigil ?? trigger}{displayText}` (e.g., "@Alice").
468
+ */
469
+ function chipPlainText(seg) {
470
+ return `${seg.sigil ?? seg.trigger}${seg.displayText}`;
471
+ }
472
+ /**
467
473
  * Converts an array of segments to a plain text string.
468
- * Chips are represented as `{trigger}{displayText}` (e.g., "@Alice").
474
+ * Chips are represented by {@link chipPlainText}.
469
475
  */
470
476
  function segmentsToPlainText(segments) {
471
477
  return segments.map((seg) => {
472
478
  if (seg.type === "text") return seg.text;
473
- return `${seg.trigger}${seg.displayText}`;
479
+ return chipPlainText(seg);
474
480
  }).join("");
475
481
  }
476
482
  /**
@@ -495,7 +501,7 @@ function truncateSegmentsToLength(segments, maxLength) {
495
501
  const result = [];
496
502
  let length = 0;
497
503
  for (const seg of segments) {
498
- const segLength = seg.type === "text" ? seg.text.length : seg.trigger.length + seg.displayText.length;
504
+ const segLength = seg.type === "text" ? seg.text.length : chipPlainText(seg).length;
499
505
  if (length + segLength <= maxLength) {
500
506
  result.push(seg);
501
507
  length += segLength;
@@ -597,13 +603,13 @@ function detectActiveTrigger(text, cursorPos, triggers) {
597
603
  * @param chip - The chip data (value, displayText, optional data)
598
604
  * @returns New segments array with the chip inserted, and the new cursor position
599
605
  */
600
- function resolveChip(segments, activeTrigger, chip) {
606
+ function resolveChip(segments, activeTrigger, chip, trailingText = " ") {
601
607
  const triggerStart = activeTrigger.startOffset;
602
608
  const triggerEnd = triggerStart + 1 + activeTrigger.query.length;
603
609
  const newSegments = [];
604
610
  let offset = 0;
605
611
  for (const seg of segments) if (seg.type === "chip") {
606
- const chipText = `${seg.trigger}${seg.displayText}`;
612
+ const chipText = chipPlainText(seg);
607
613
  const chipStart = offset;
608
614
  const chipEnd = offset + chipText.length;
609
615
  if (chipEnd <= triggerStart || chipStart >= triggerEnd) newSegments.push(seg);
@@ -625,17 +631,18 @@ function resolveChip(segments, activeTrigger, chip) {
625
631
  trigger: activeTrigger.config.char,
626
632
  value: chip.value,
627
633
  displayText: chip.displayText,
634
+ ...chip.sigil !== void 0 ? { sigil: chip.sigil } : {},
628
635
  ...chip.data !== void 0 ? { data: chip.data } : {},
629
636
  ...chip.autoResolved ? { autoResolved: true } : {}
630
637
  };
631
638
  newSegments.push(newChip);
632
639
  if (afterText) newSegments.push({
633
640
  type: "text",
634
- text: " " + afterText.replace(/^\s/, "")
641
+ text: trailingText + afterText.replace(/^\s/, "")
635
642
  });
636
643
  else newSegments.push({
637
644
  type: "text",
638
- text: " "
645
+ text: trailingText
639
646
  });
640
647
  }
641
648
  offset = textEnd;
@@ -645,12 +652,12 @@ function resolveChip(segments, activeTrigger, chip) {
645
652
  let runningOffset = 0;
646
653
  for (const seg of merged) if (seg.type === "text") runningOffset += seg.text.length;
647
654
  else {
648
- runningOffset += seg.trigger.length + seg.displayText.length;
655
+ runningOffset += chipPlainText(seg).length;
649
656
  if (seg.value === chip.value && seg.displayText === chip.displayText && seg.trigger === activeTrigger.config.char) lastChipEndOffset = runningOffset;
650
657
  }
651
658
  return {
652
659
  segments: merged,
653
- cursorOffset: lastChipEndOffset === -1 ? runningOffset : lastChipEndOffset + 1
660
+ cursorOffset: lastChipEndOffset === -1 ? runningOffset : lastChipEndOffset + trailingText.length
654
661
  };
655
662
  }
656
663
  /**
@@ -671,7 +678,7 @@ function resolveText(segments, activeTrigger, text) {
671
678
  let cursorOffset = triggerStart + text.length;
672
679
  for (const seg of segments) {
673
680
  if (seg.type === "chip") {
674
- const chipEnd = offset + `${seg.trigger}${seg.displayText}`.length;
681
+ const chipEnd = offset + chipPlainText(seg).length;
675
682
  if (chipEnd <= triggerStart || offset >= triggerEnd) newSegments.push(seg);
676
683
  offset = chipEnd;
677
684
  continue;
@@ -729,7 +736,7 @@ function revertChipAtIndex(segments, index) {
729
736
  if (index < 0 || index >= segments.length) return null;
730
737
  const seg = segments[index];
731
738
  if (seg.type !== "chip" || !seg.autoResolved) return null;
732
- const revertedText = `${seg.trigger}${seg.displayText}`;
739
+ const revertedText = chipPlainText(seg);
733
740
  return {
734
741
  segments: mergeAdjacentTextSegments([
735
742
  ...segments.slice(0, index),
@@ -826,7 +833,7 @@ function replaceTextRange(segments, start, end, replacement) {
826
833
  let offset = 0;
827
834
  let inserted = false;
828
835
  for (const seg of segments) if (seg.type === "chip") {
829
- const chipText = `${seg.trigger}${seg.displayText}`;
836
+ const chipText = chipPlainText(seg);
830
837
  const chipStart = offset;
831
838
  const chipEnd = offset + chipText.length;
832
839
  if (!inserted && start === end && chipStart === start) {
@@ -1388,6 +1395,10 @@ function getChipData(node) {
1388
1395
  if (!raw) return;
1389
1396
  return safeJsonParse(raw);
1390
1397
  }
1398
+ function getChipSigil(node) {
1399
+ if (!isChipElement(node)) return;
1400
+ return node.dataset.chipSigil;
1401
+ }
1391
1402
  /**
1392
1403
  * Length of a chip's plain-text representation (`trigger + displayText`).
1393
1404
  *
@@ -1397,7 +1408,7 @@ function getChipData(node) {
1397
1408
  * `textContent` for resilience against externally-mutated nodes.
1398
1409
  */
1399
1410
  function chipNodeTextLength(node) {
1400
- const trigger = node.dataset.chipTrigger ?? "";
1411
+ const trigger = node.dataset.chipSigil ?? node.dataset.chipTrigger ?? "";
1401
1412
  const display = node.dataset.chipDisplay ?? node.textContent ?? "";
1402
1413
  return trigger.length + display.length;
1403
1414
  }
@@ -1416,6 +1427,7 @@ function chipNodeToSegment(node) {
1416
1427
  const value = getChipValue(node);
1417
1428
  const displayText = getChipDisplay(node);
1418
1429
  if (!trigger || value === void 0 || !displayText) return null;
1430
+ const sigil = getChipSigil(node);
1419
1431
  const data = getChipData(node);
1420
1432
  const autoResolved = getChipAutoResolved(node);
1421
1433
  return {
@@ -1423,6 +1435,7 @@ function chipNodeToSegment(node) {
1423
1435
  trigger,
1424
1436
  value,
1425
1437
  displayText,
1438
+ ...sigil !== void 0 ? { sigil } : {},
1426
1439
  ...data !== void 0 ? { data } : {},
1427
1440
  ...autoResolved ? { autoResolved: true } : {}
1428
1441
  };
@@ -2052,7 +2065,7 @@ function serializeFragmentToPlainText(fragment) {
2052
2065
  text += value;
2053
2066
  },
2054
2067
  onChip: (node) => {
2055
- text += (getChipTrigger(node) ?? "") + (getChipDisplay(node) ?? "");
2068
+ text += (getChipSigil(node) ?? getChipTrigger(node) ?? "") + (getChipDisplay(node) ?? "");
2056
2069
  },
2057
2070
  onBreak: () => {
2058
2071
  text += "\n";
@@ -2106,6 +2119,7 @@ function parseSegmentsFromClipboard(json) {
2106
2119
  trigger: item.trigger,
2107
2120
  value: item.value,
2108
2121
  displayText: item.displayText,
2122
+ ...typeof item.sigil === "string" ? { sigil: item.sigil } : {},
2109
2123
  ...item.data !== void 0 ? { data: item.data } : {},
2110
2124
  ...item.autoResolved ? { autoResolved: true } : {}
2111
2125
  };
@@ -2140,7 +2154,7 @@ function insertSegmentsAtCursor(currentSegments, pastedSegments, editor) {
2140
2154
  };
2141
2155
  for (const seg of currentSegments) {
2142
2156
  if (seg.type === "chip") {
2143
- const chipLen = seg.trigger.length + seg.displayText.length;
2157
+ const chipLen = chipPlainText(seg).length;
2144
2158
  if (offset >= cursorOffset) insertOnce();
2145
2159
  result.push(seg);
2146
2160
  offset += chipLen;
@@ -2618,7 +2632,7 @@ function usePromptAreaKeydown({ editorRef, triggers, markdownEnabled, submitOnEn
2618
2632
  for (let i = 0; i < segIdx; i++) {
2619
2633
  const s = segments[i];
2620
2634
  if (s.type === "text") targetOffset += s.text.length;
2621
- else targetOffset += s.trigger.length + s.displayText.length;
2635
+ else targetOffset += chipPlainText(s).length;
2622
2636
  }
2623
2637
  targetOffset += result.revertedText.length;
2624
2638
  onChange(result.segments);
@@ -3229,6 +3243,7 @@ function usePromptArea({ value, onChange, triggers = NO_TRIGGERS, disabled = fal
3229
3243
  chip.dataset.chipTrigger = seg.trigger;
3230
3244
  chip.dataset.chipValue = seg.value;
3231
3245
  chip.dataset.chipDisplay = seg.displayText;
3246
+ if (seg.sigil !== void 0) chip.dataset.chipSigil = seg.sigil;
3232
3247
  if (seg.data !== void 0) {
3233
3248
  const json = safeJsonStringify(seg.data);
3234
3249
  if (json) chip.dataset.chipData = json;
@@ -3238,7 +3253,7 @@ function usePromptArea({ value, onChange, triggers = NO_TRIGGERS, disabled = fal
3238
3253
  const chipStyle = triggerConfig?.chipStyle ?? "pill";
3239
3254
  chip.dataset.chipStyle = chipStyle;
3240
3255
  chip.className = cn("prompt-area-chip", chipStyle === "inline" && "prompt-area-chip--inline", triggerConfig?.chipClassName);
3241
- chip.textContent = `${seg.trigger}${seg.displayText}`;
3256
+ chip.textContent = chipPlainText(seg);
3242
3257
  chip.setAttribute("role", "button");
3243
3258
  chip.setAttribute("tabindex", "-1");
3244
3259
  editor.appendChild(chip);
@@ -3483,9 +3498,11 @@ function usePromptArea({ value, onChange, triggers = NO_TRIGGERS, disabled = fal
3483
3498
  return;
3484
3499
  }
3485
3500
  const displayText = activeTrigger.config.onSelect?.(suggestion) ?? suggestion.label;
3501
+ const options = activeTrigger.config.chipOptions?.(suggestion);
3486
3502
  const chipData = {
3487
3503
  value: suggestion.value,
3488
3504
  displayText: displayText || suggestion.label,
3505
+ ...options?.sigil !== void 0 ? { sigil: options.sigil } : {},
3489
3506
  data: suggestion.data
3490
3507
  };
3491
3508
  if (chipEditing.trySelectEditingChip(segments, chipData, activeTrigger.config.char, events.pushUndo)) {
@@ -3495,7 +3512,7 @@ function usePromptArea({ value, onChange, triggers = NO_TRIGGERS, disabled = fal
3495
3512
  }, 0);
3496
3513
  return;
3497
3514
  }
3498
- const result = resolveChip(segments, activeTrigger, chipData);
3515
+ const result = resolveChip(segments, activeTrigger, chipData, options?.trailingText);
3499
3516
  onChange(result.segments);
3500
3517
  renderSegmentsToDOM(result.segments);
3501
3518
  onChipAdd?.({
@@ -4411,6 +4428,123 @@ function launchTrigger(opts) {
4411
4428
  };
4412
4429
  }
4413
4430
  //#endregion
4431
+ //#region src/components/agent/composer-commands.ts
4432
+ const KIND_ORDER = {
4433
+ command: 0,
4434
+ client: 1,
4435
+ skill: 2
4436
+ };
4437
+ function cleanCommandName(name) {
4438
+ return name.replace(/\s*\(MCP\)$/i, "");
4439
+ }
4440
+ function skillToken(skill) {
4441
+ return `$${skill.name}`;
4442
+ }
4443
+ function skillTrailingText(skill) {
4444
+ const token = skillToken(skill);
4445
+ const rest = (skill.defaultPrompt ?? "").split(token).join(" ").replace(/\s+/g, " ").trim();
4446
+ return rest ? ` ${rest} ` : " ";
4447
+ }
4448
+ function skillPrompt(skill) {
4449
+ return skillToken(skill) + skillTrailingText(skill);
4450
+ }
4451
+ function mergeComposerRows(sources) {
4452
+ const rows = [];
4453
+ const engineNames = /* @__PURE__ */ new Set();
4454
+ for (const command of sources.commands ?? []) {
4455
+ const name = cleanCommandName(command.name);
4456
+ if (engineNames.has(name)) continue;
4457
+ engineNames.add(name);
4458
+ for (const alias of command.aliases ?? []) engineNames.add(cleanCommandName(alias));
4459
+ rows.push({
4460
+ kind: "command",
4461
+ name,
4462
+ description: command.description,
4463
+ argumentHint: command.argumentHint,
4464
+ aliases: command.aliases
4465
+ });
4466
+ }
4467
+ for (const command of sources.clientCommands ?? []) {
4468
+ if (engineNames.has(command.name)) continue;
4469
+ rows.push({
4470
+ kind: "client",
4471
+ name: command.name,
4472
+ description: command.description,
4473
+ argumentHint: command.argumentHint,
4474
+ command
4475
+ });
4476
+ }
4477
+ for (const skill of sources.skills ?? []) rows.push({
4478
+ kind: "skill",
4479
+ name: skill.name,
4480
+ label: skill.displayName ?? skill.name,
4481
+ description: skill.shortDescription ?? skill.description,
4482
+ enabled: skill.enabled,
4483
+ scope: skill.scope,
4484
+ skill
4485
+ });
4486
+ return rows;
4487
+ }
4488
+ function haystacks(row) {
4489
+ if (row.kind === "skill") return [
4490
+ row.name,
4491
+ row.label,
4492
+ ...row.name.split(/[-:_]/)
4493
+ ];
4494
+ return [
4495
+ row.name,
4496
+ ...row.kind === "command" ? row.aliases ?? [] : [],
4497
+ ...row.name.split(":")
4498
+ ];
4499
+ }
4500
+ function matchScore(query, candidates) {
4501
+ const needle = query.toLowerCase();
4502
+ const lowered = candidates.map((s) => s.toLowerCase());
4503
+ if (lowered.some((h) => h.startsWith(needle))) return 2;
4504
+ return lowered.some((h) => h.includes(needle)) ? 1 : 0;
4505
+ }
4506
+ function rankComposerRows(query, rows) {
4507
+ const scored = [];
4508
+ rows.forEach((row, index) => {
4509
+ if (row.kind === "skill" && !row.enabled) return;
4510
+ const score = matchScore(query, haystacks(row));
4511
+ if (score > 0) scored.push({
4512
+ score,
4513
+ index,
4514
+ row
4515
+ });
4516
+ });
4517
+ scored.sort((a, b) => b.score - a.score || KIND_ORDER[a.row.kind] - KIND_ORDER[b.row.kind] || a.index - b.index);
4518
+ return scored.map(({ row }) => row);
4519
+ }
4520
+ function matchClientCommand(text, commands) {
4521
+ const match = /^\/([\w:-]+)[ \t]*([\s\S]*)$/.exec(text.trim());
4522
+ if (!match) return;
4523
+ const command = commands.find((c) => c.name === match[1]);
4524
+ return command ? {
4525
+ command,
4526
+ args: match[2].trim()
4527
+ } : void 0;
4528
+ }
4529
+ function composerCommandRows(rows) {
4530
+ return rows.map((row) => row.kind === "skill" ? {
4531
+ kind: row.kind,
4532
+ name: row.name,
4533
+ label: row.label,
4534
+ description: row.description,
4535
+ scope: row.scope,
4536
+ enabled: row.enabled,
4537
+ insertText: skillPrompt(row.skill)
4538
+ } : {
4539
+ kind: row.kind,
4540
+ name: row.name,
4541
+ label: `/${row.name}`,
4542
+ description: row.description,
4543
+ enabled: true,
4544
+ insertText: `/${row.name} `
4545
+ });
4546
+ }
4547
+ //#endregion
4414
4548
  //#region src/components/agent/transcript-variant.tsx
4415
4549
  const VariantContext = createContext("cards");
4416
4550
  function TranscriptVariantProvider({ value, children }) {
@@ -4531,6 +4665,38 @@ function usePulse(animated) {
4531
4665
  return PULSE_FRAMES[frame % PULSE_FRAMES.length];
4532
4666
  }
4533
4667
  //#endregion
4668
+ //#region src/components/agent/PromptTokenText.tsx
4669
+ const SkillNamesContext = createContext(void 0);
4670
+ function SkillNamesProvider({ names, children }) {
4671
+ return /* @__PURE__ */ jsx(SkillNamesContext.Provider, {
4672
+ value: names,
4673
+ children
4674
+ });
4675
+ }
4676
+ function PromptTokenText({ text, className }) {
4677
+ const skills = useContext(SkillNamesContext);
4678
+ const tokens = scanPromptTokens(text, { skills });
4679
+ if (tokens.length === 0) return /* @__PURE__ */ jsx("span", {
4680
+ className,
4681
+ children: text
4682
+ });
4683
+ const parts = [];
4684
+ let cursor = 0;
4685
+ for (const [index, token] of tokens.entries()) {
4686
+ if (token.start > cursor) parts.push(text.slice(cursor, token.start));
4687
+ parts.push(/* @__PURE__ */ jsx("span", {
4688
+ className: cn("font-mono", token.kind === "file" ? "text-accent" : "text-info"),
4689
+ children: token.text
4690
+ }, `${token.start}-${index}`));
4691
+ cursor = token.end;
4692
+ }
4693
+ if (cursor < text.length) parts.push(text.slice(cursor));
4694
+ return /* @__PURE__ */ jsx("span", {
4695
+ className,
4696
+ children: parts.map((part, index) => /* @__PURE__ */ jsx(Fragment, { children: part }, index))
4697
+ });
4698
+ }
4699
+ //#endregion
4534
4700
  //#region src/components/terminal/affordances.tsx
4535
4701
  const DEFAULTS = {
4536
4702
  hover: true,
@@ -4749,6 +4915,69 @@ function TerminalDiff({ patch }) {
4749
4915
  });
4750
4916
  }
4751
4917
  //#endregion
4918
+ //#region src/lib/file-link.ts
4919
+ const SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
4920
+ const TRAILING_LINE = /^(.*?):(\d+)(?::\d+)?$/;
4921
+ const FRAGMENT_LINE = /^L(\d+)/;
4922
+ function parseFileLink(href, cwd) {
4923
+ const raw = href?.trim();
4924
+ if (!raw || raw.startsWith("#") || raw.startsWith("//")) return;
4925
+ let target = raw;
4926
+ if (SCHEME.test(target)) {
4927
+ if (!target.startsWith("file:")) return;
4928
+ try {
4929
+ target = decodeURIComponent(new URL(target).pathname);
4930
+ } catch {
4931
+ return;
4932
+ }
4933
+ }
4934
+ let line;
4935
+ const hash = target.indexOf("#");
4936
+ if (hash !== -1) {
4937
+ line = readLine(FRAGMENT_LINE.exec(target.slice(hash + 1))?.[1]);
4938
+ target = target.slice(0, hash);
4939
+ }
4940
+ const suffix = TRAILING_LINE.exec(target);
4941
+ if (suffix) {
4942
+ target = suffix[1];
4943
+ line ??= readLine(suffix[2]);
4944
+ }
4945
+ if (!target) return;
4946
+ const path = target.startsWith("/") ? normalize(target) : cwd ? normalize(`${cwd}/${target}`) : void 0;
4947
+ return path ? {
4948
+ path,
4949
+ ...line === void 0 ? {} : { line }
4950
+ } : void 0;
4951
+ }
4952
+ function readLine(value) {
4953
+ const parsed = value === void 0 ? NaN : Number(value);
4954
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
4955
+ }
4956
+ function normalize(path) {
4957
+ const out = [];
4958
+ for (const part of path.split("/")) {
4959
+ if (part === "" || part === ".") continue;
4960
+ if (part === "..") {
4961
+ out.pop();
4962
+ continue;
4963
+ }
4964
+ out.push(part);
4965
+ }
4966
+ return `/${out.join("/")}`;
4967
+ }
4968
+ //#endregion
4969
+ //#region src/components/terminal/file-link.tsx
4970
+ const FileLinkContext = createContext(void 0);
4971
+ function FileLinkProvider({ value, children }) {
4972
+ return /* @__PURE__ */ jsx(FileLinkContext.Provider, {
4973
+ value,
4974
+ children
4975
+ });
4976
+ }
4977
+ function useFileLinks() {
4978
+ return useContext(FileLinkContext);
4979
+ }
4980
+ //#endregion
4752
4981
  //#region src/components/terminal/markdown.tsx
4753
4982
  function codeText(node) {
4754
4983
  if (node === null || node === void 0 || typeof node === "boolean") return "";
@@ -4788,6 +5017,29 @@ function heading(tone) {
4788
5017
  });
4789
5018
  };
4790
5019
  }
5020
+ function Link({ href, children }) {
5021
+ const links = useFileLinks();
5022
+ const file = links ? parseFileLink(href, links.cwd) : void 0;
5023
+ if (file) return /* @__PURE__ */ jsx("button", {
5024
+ type: "button",
5025
+ className: "term-link",
5026
+ "data-tone": "blue",
5027
+ title: file.line === void 0 ? file.path : `${file.path}:${file.line}`,
5028
+ onClick: (event) => {
5029
+ event.stopPropagation();
5030
+ links?.open(file.path, file.line);
5031
+ },
5032
+ children
5033
+ });
5034
+ return /* @__PURE__ */ jsx("a", {
5035
+ className: "term-link",
5036
+ "data-tone": "blue",
5037
+ href,
5038
+ target: "_blank",
5039
+ rel: "noreferrer",
5040
+ children
5041
+ });
5042
+ }
4791
5043
  const TERMINAL_COMPONENTS = {
4792
5044
  p: ({ children }) => /* @__PURE__ */ jsx("div", {
4793
5045
  className: "term-block",
@@ -4844,12 +5096,8 @@ const TERMINAL_COMPONENTS = {
4844
5096
  className: "term-em",
4845
5097
  children
4846
5098
  }),
4847
- a: ({ children, href }) => /* @__PURE__ */ jsx("a", {
4848
- className: "term-link",
4849
- "data-tone": "blue",
5099
+ a: ({ children, href }) => /* @__PURE__ */ jsx(Link, {
4850
5100
  href,
4851
- target: "_blank",
4852
- rel: "noreferrer",
4853
5101
  children
4854
5102
  }),
4855
5103
  table: ({ children }) => /* @__PURE__ */ jsx("div", {
@@ -5221,7 +5469,7 @@ function UserRow({ item }) {
5221
5469
  glyph: index === 0 ? "❯" : void 0,
5222
5470
  glyphTone: "dim",
5223
5471
  tone: "fg",
5224
- children: line || "\xA0"
5472
+ children: line ? /* @__PURE__ */ jsx(PromptTokenText, { text: line }) : "\xA0"
5225
5473
  }, index)) : null]
5226
5474
  })
5227
5475
  });
@@ -5453,7 +5701,7 @@ function TurnResultRow$1({ item }) {
5453
5701
  " · ",
5454
5702
  formatDuration(item.durationMs),
5455
5703
  " · ",
5456
- formatCost(item.totalCostUsd)
5704
+ formatCost(item.costUsd ?? item.totalCostUsd)
5457
5705
  ]
5458
5706
  }), item.errors?.map((message, index) => /* @__PURE__ */ jsx(Row, {
5459
5707
  tone: "red",
@@ -5557,20 +5805,7 @@ function TerminalSurface({ fontSize, lineHeight, bleed, affordances, className,
5557
5805
  //#region src/components/agent/Composer.tsx
5558
5806
  const SHELL_PLACEHOLDER = "Run a command on the host…";
5559
5807
  const SHELL_HINT = "! shell mode · esc to exit";
5560
- function cleanName(name) {
5561
- return name.replace(/\s*\(MCP\)$/i, "");
5562
- }
5563
- function skillPrompt(skill) {
5564
- const base = skill.defaultPrompt?.trim() || `$${skill.name}`;
5565
- return /\s$/.test(base) ? base : base + " ";
5566
- }
5567
- function matchScore(query, haystacks) {
5568
- const needle = query.toLowerCase();
5569
- const lowered = haystacks.map((s) => s.toLowerCase());
5570
- if (lowered.some((h) => h.startsWith(needle))) return 2;
5571
- return lowered.some((h) => h.includes(needle)) ? 1 : 0;
5572
- }
5573
- function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message the agent…", commands, skills, onSearchFiles, onShellCommand, attachments, toolbar, layout = "stacked", fontSize, lineHeight, affordances, draft, className, ref }) {
5808
+ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message the agent…", commands, skills, clientCommands, onSearchFiles, onShellCommand, attachments, toolbar, layout = "stacked", fontSize, lineHeight, affordances, draft, className, ref }) {
5574
5809
  const inline = layout === "inline";
5575
5810
  const terminal = useTranscriptVariant() === "terminal";
5576
5811
  const [initialDraft] = useState(() => draft?.initialText ?? "");
@@ -5578,6 +5813,7 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5578
5813
  const fileInput = useRef(null);
5579
5814
  const [dragging, setDragging] = useState(false);
5580
5815
  const [shellMode, setShellMode] = useState(false);
5816
+ const [helpOpen, setHelpOpen] = useState(false);
5581
5817
  useImperativeHandle(ref, () => ({
5582
5818
  insertText: (text) => {
5583
5819
  const prefix = plainText.length > 0 && !/\s$/.test(plainText) ? " " : "";
@@ -5592,71 +5828,42 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5592
5828
  ]);
5593
5829
  const triggers = useMemo(() => {
5594
5830
  const configured = [];
5595
- const usableSkills = (skills ?? []).filter((s) => s.enabled);
5596
- if (commands && commands.length > 0) {
5597
- const seen = /* @__PURE__ */ new Set();
5598
- const unique = commands.flatMap((c) => {
5599
- const name = cleanName(c.name);
5600
- if (seen.has(name)) return [];
5601
- seen.add(name);
5602
- return [{
5603
- ...c,
5604
- name
5605
- }];
5606
- });
5831
+ const rows = mergeComposerRows({
5832
+ commands,
5833
+ clientCommands,
5834
+ skills
5835
+ });
5836
+ if (rows.length > 0) {
5837
+ const byTag = new Map(rows.map((row) => [`${row.kind}:${row.name}`, row]));
5607
5838
  configured.push(commandTrigger({
5608
- onSearch: (query) => {
5609
- const scored = [];
5610
- for (const c of unique) {
5611
- const score = matchScore(query, [
5612
- c.name,
5613
- ...c.aliases ?? [],
5614
- ...c.name.split(":")
5615
- ]);
5616
- if (score === 0) continue;
5617
- scored.push({
5618
- score,
5619
- suggestion: {
5620
- value: c.name,
5621
- label: `/${c.name}${c.argumentHint ? ` ${c.argumentHint}` : ""}`,
5622
- description: c.description
5623
- }
5624
- });
5625
- }
5626
- scored.sort((a, b) => b.score - a.score);
5627
- return scored.map(({ suggestion }) => suggestion);
5839
+ onSearch: (query) => rankComposerRows(query, rows).map((row) => row.kind === "skill" ? {
5840
+ value: row.name,
5841
+ label: row.label,
5842
+ description: row.description ? `Skill · ${row.description}` : "Skill · inserts a message you can edit",
5843
+ icon: /* @__PURE__ */ jsx(Sparkles, { className: "size-3.5 text-fg-3" }),
5844
+ data: `${row.kind}:${row.name}`
5845
+ } : {
5846
+ value: row.name,
5847
+ label: `/${row.name}${row.argumentHint ? ` ${row.argumentHint}` : ""}`,
5848
+ description: row.description,
5849
+ icon: row.kind === "client" ? /* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-3.5 text-fg-3" }) : void 0,
5850
+ data: `${row.kind}:${row.name}`
5851
+ }),
5852
+ insertAsText: (suggestion) => {
5853
+ const row = byTag.get(String(suggestion.data));
5854
+ return row?.kind === "client" && row.command.requiresArgs ? `/${row.name} ` : void 0;
5855
+ },
5856
+ chipOptions: (suggestion) => {
5857
+ const row = byTag.get(String(suggestion.data));
5858
+ return row?.kind === "skill" ? {
5859
+ sigil: "$",
5860
+ trailingText: skillTrailingText(row.skill)
5861
+ } : void 0;
5628
5862
  },
5629
5863
  onSelect: (suggestion) => suggestion.value,
5630
5864
  chipClassName: "font-mono"
5631
5865
  }));
5632
5866
  }
5633
- if (usableSkills.length > 0) configured.push(commandTrigger({
5634
- char: "$",
5635
- accessibilityLabel: "skill",
5636
- onSearch: (query) => {
5637
- const scored = [];
5638
- for (const skill of usableSkills) {
5639
- const score = matchScore(query, [skill.name, ...skill.name.split(/[-:_]/)]);
5640
- if (score === 0) continue;
5641
- const summary = skill.shortDescription ?? skill.description;
5642
- scored.push({
5643
- score,
5644
- suggestion: {
5645
- value: skill.name,
5646
- label: skill.displayName ?? skill.name,
5647
- description: summary ? `Skill · ${summary}` : "Skill · inserts a message you can edit",
5648
- icon: /* @__PURE__ */ jsx(Sparkles, { className: "size-3.5 text-fg-3" })
5649
- }
5650
- });
5651
- }
5652
- scored.sort((a, b) => b.score - a.score);
5653
- return scored.map(({ suggestion }) => suggestion);
5654
- },
5655
- insertAsText: (suggestion) => {
5656
- const skill = usableSkills.find((s) => s.name === suggestion.value);
5657
- return skill ? skillPrompt(skill) : `$${suggestion.value} `;
5658
- }
5659
- }));
5660
5867
  if (onSearchFiles) configured.push(mentionTrigger({
5661
5868
  searchDebounceMs: 150,
5662
5869
  onSearch: async (query, options) => {
@@ -5680,6 +5887,7 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5680
5887
  }, [
5681
5888
  commands,
5682
5889
  skills,
5890
+ clientCommands,
5683
5891
  onSearchFiles,
5684
5892
  onShellCommand
5685
5893
  ]);
@@ -5711,6 +5919,24 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5711
5919
  setShellMode(false);
5712
5920
  focus();
5713
5921
  };
5922
+ const hints = [
5923
+ triggers?.some((t) => t.char === "/") ? {
5924
+ key: "/",
5925
+ what: "commands and skills"
5926
+ } : void 0,
5927
+ onSearchFiles ? {
5928
+ key: "@",
5929
+ what: "mention a file"
5930
+ } : void 0,
5931
+ onShellCommand ? {
5932
+ key: "!",
5933
+ what: "run a shell command"
5934
+ } : void 0,
5935
+ {
5936
+ key: "?",
5937
+ what: "this list, on an empty composer"
5938
+ }
5939
+ ].filter((h) => h !== void 0);
5714
5940
  const shellKeys = shellMode ? {
5715
5941
  onEscape: leaveShellMode,
5716
5942
  onKeyDown: (e) => {
@@ -5719,7 +5945,30 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5719
5945
  leaveShellMode();
5720
5946
  }
5721
5947
  }
5722
- } : {};
5948
+ } : {
5949
+ onEscape: helpOpen ? () => setHelpOpen(false) : void 0,
5950
+ onKeyDown: (e) => {
5951
+ if (e.key === "?" && isEmpty && !e.metaKey && !e.ctrlKey) {
5952
+ e.preventDefault();
5953
+ setHelpOpen(true);
5954
+ return;
5955
+ }
5956
+ if (helpOpen) setHelpOpen(false);
5957
+ }
5958
+ };
5959
+ const helpHints = helpOpen ? /* @__PURE__ */ jsx(Fragment$1, { children: hints.map((hint) => /* @__PURE__ */ jsxs("span", {
5960
+ className: "flex items-center gap-1",
5961
+ children: [/* @__PURE__ */ jsx("kbd", {
5962
+ className: "font-mono text-text",
5963
+ children: hint.key
5964
+ }), hint.what]
5965
+ }, hint.key)) }) : null;
5966
+ const helpRow = helpOpen ? /* @__PURE__ */ jsx("div", {
5967
+ role: "note",
5968
+ "aria-label": "Composer shortcuts",
5969
+ className: "mx-auto mb-1 flex w-full max-w-[var(--wd-transcript-max-width)] flex-wrap items-center gap-x-3 gap-y-1 text-label text-fg-3",
5970
+ children: helpHints
5971
+ }) : null;
5723
5972
  const pick = (files) => {
5724
5973
  if (files && files.length > 0) attachments?.add(files);
5725
5974
  };
@@ -5862,6 +6111,18 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5862
6111
  })
5863
6112
  ]
5864
6113
  }),
6114
+ helpOpen ? /* @__PURE__ */ jsxs("div", {
6115
+ role: "note",
6116
+ "aria-label": "Composer shortcuts",
6117
+ className: "term-row",
6118
+ children: [/* @__PURE__ */ jsx("span", {
6119
+ "aria-hidden": true,
6120
+ className: "term-gutter"
6121
+ }), /* @__PURE__ */ jsx("div", {
6122
+ className: "flex min-w-0 flex-wrap items-center gap-x-[2ch] text-fg-3",
6123
+ children: helpHints
6124
+ })]
6125
+ }) : null,
5865
6126
  shellMode ? /* @__PURE__ */ jsxs("div", {
5866
6127
  className: "term-row",
5867
6128
  children: [/* @__PURE__ */ jsx("span", {
@@ -5889,55 +6150,59 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
5889
6150
  return /* @__PURE__ */ jsxs("div", {
5890
6151
  "data-slot": "composer",
5891
6152
  className: cn("px-[var(--wd-composer-padding)] pb-[var(--wd-composer-padding)]", className),
5892
- children: [/* @__PURE__ */ jsxs("div", {
5893
- ...dropHandlers,
5894
- className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)] overflow-hidden border border-border bg-bg", "transition-colors rounded-lg shadow-(--shadow-xs)", "focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30", dragging && "border-ring ring-2 ring-ring/30", shellMode && "border-[var(--wd-shell-accent)] focus-within:border-[var(--wd-shell-accent)] focus-within:ring-[var(--wd-shell-accent)]/30", disabled && "opacity-60"),
5895
- children: [staged.length > 0 && attachments ? /* @__PURE__ */ jsx(AttachmentStrip, { attachments }) : null, inline ? /* @__PURE__ */ jsxs("div", {
5896
- className: "flex items-end gap-1 p-1",
5897
- children: [
5898
- shellMode ? /* @__PURE__ */ jsx(ShellBadge, { onLeave: leaveShellMode }) : attach,
5899
- /* @__PURE__ */ jsx(PromptArea, {
5900
- ...bind,
5901
- triggers,
5902
- markdown: false,
5903
- normalizeBullets: false,
5904
- onSubmit: submit,
5905
- disabled,
5906
- placeholder: disabled ? "Session ended" : shellMode ? SHELL_PLACEHOLDER : placeholder,
5907
- minHeight: 20,
5908
- maxHeight: 192,
5909
- "aria-label": shellMode ? "Run a shell command" : "Message the agent",
5910
- className: "min-w-0 flex-1 py-1 text-body-sm text-text",
5911
- onImagePaste: (file) => attachments?.add([file]),
5912
- ...shellKeys
5913
- }),
5914
- submitButton
5915
- ]
5916
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(PromptArea, {
5917
- ...bind,
5918
- triggers,
5919
- markdown: false,
5920
- normalizeBullets: false,
5921
- onSubmit: submit,
5922
- disabled,
5923
- placeholder: disabled ? "Session ended" : shellMode ? SHELL_PLACEHOLDER : placeholder,
5924
- minHeight: 28,
5925
- maxHeight: 192,
5926
- "aria-label": shellMode ? "Run a shell command" : "Message the agent",
5927
- className: "px-3 pt-2.5 pb-0 text-body-sm text-text",
5928
- onImagePaste: (file) => attachments?.add([file]),
5929
- ...shellKeys
5930
- }), /* @__PURE__ */ jsxs("div", {
5931
- className: "flex items-center justify-between gap-2 px-2 pb-2",
5932
- children: [/* @__PURE__ */ jsxs("div", {
5933
- className: "flex min-w-0 items-center gap-1",
5934
- children: [shellMode ? /* @__PURE__ */ jsx(ShellBadge, { onLeave: leaveShellMode }) : attach, shellMode ? /* @__PURE__ */ jsx("span", {
5935
- className: "text-label text-text-muted",
5936
- children: SHELL_HINT
5937
- }) : toolbar]
5938
- }), submitButton]
5939
- })] })]
5940
- }), errorRow]
6153
+ children: [
6154
+ helpRow,
6155
+ /* @__PURE__ */ jsxs("div", {
6156
+ ...dropHandlers,
6157
+ className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)] overflow-hidden border border-border bg-bg", "transition-colors rounded-lg shadow-(--shadow-xs)", "focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30", dragging && "border-ring ring-2 ring-ring/30", shellMode && "border-[var(--wd-shell-accent)] focus-within:border-[var(--wd-shell-accent)] focus-within:ring-[var(--wd-shell-accent)]/30", disabled && "opacity-60"),
6158
+ children: [staged.length > 0 && attachments ? /* @__PURE__ */ jsx(AttachmentStrip, { attachments }) : null, inline ? /* @__PURE__ */ jsxs("div", {
6159
+ className: "flex items-end gap-1 p-1",
6160
+ children: [
6161
+ shellMode ? /* @__PURE__ */ jsx(ShellBadge, { onLeave: leaveShellMode }) : attach,
6162
+ /* @__PURE__ */ jsx(PromptArea, {
6163
+ ...bind,
6164
+ triggers,
6165
+ markdown: false,
6166
+ normalizeBullets: false,
6167
+ onSubmit: submit,
6168
+ disabled,
6169
+ placeholder: disabled ? "Session ended" : shellMode ? SHELL_PLACEHOLDER : placeholder,
6170
+ minHeight: 20,
6171
+ maxHeight: 192,
6172
+ "aria-label": shellMode ? "Run a shell command" : "Message the agent",
6173
+ className: "min-w-0 flex-1 py-1 text-body-sm text-text",
6174
+ onImagePaste: (file) => attachments?.add([file]),
6175
+ ...shellKeys
6176
+ }),
6177
+ submitButton
6178
+ ]
6179
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(PromptArea, {
6180
+ ...bind,
6181
+ triggers,
6182
+ markdown: false,
6183
+ normalizeBullets: false,
6184
+ onSubmit: submit,
6185
+ disabled,
6186
+ placeholder: disabled ? "Session ended" : shellMode ? SHELL_PLACEHOLDER : placeholder,
6187
+ minHeight: 28,
6188
+ maxHeight: 192,
6189
+ "aria-label": shellMode ? "Run a shell command" : "Message the agent",
6190
+ className: "px-3 pt-2.5 pb-0 text-body-sm text-text",
6191
+ onImagePaste: (file) => attachments?.add([file]),
6192
+ ...shellKeys
6193
+ }), /* @__PURE__ */ jsxs("div", {
6194
+ className: "flex items-center justify-between gap-2 px-2 pb-2",
6195
+ children: [/* @__PURE__ */ jsxs("div", {
6196
+ className: "flex min-w-0 items-center gap-1",
6197
+ children: [shellMode ? /* @__PURE__ */ jsx(ShellBadge, { onLeave: leaveShellMode }) : attach, shellMode ? /* @__PURE__ */ jsx("span", {
6198
+ className: "text-label text-text-muted",
6199
+ children: SHELL_HINT
6200
+ }) : toolbar]
6201
+ }), submitButton]
6202
+ })] })]
6203
+ }),
6204
+ errorRow
6205
+ ]
5941
6206
  });
5942
6207
  }
5943
6208
  function GlyphButton({ tone, label, disabled, onClick, gutter, className, children }) {
@@ -8015,7 +8280,7 @@ function SessionInfoDialog({ state, client, sessionId, open, onOpenChange }) {
8015
8280
  /* @__PURE__ */ jsx(DialogRow, {
8016
8281
  label: "Cost",
8017
8282
  mono: true,
8018
- children: formatCost(state.totalCostUsd)
8283
+ children: formatCost(state.costUsd ?? (state.totalCostUsd || void 0))
8019
8284
  })
8020
8285
  ]
8021
8286
  }),
@@ -8325,7 +8590,7 @@ function StatusBar({ state, rateLimits, connected, connection, onOpenStatus, onO
8325
8590
  /* @__PURE__ */ jsx("span", { className: "flex-1" }),
8326
8591
  /* @__PURE__ */ jsx("span", {
8327
8592
  className: "font-mono text-label text-fg-3",
8328
- children: formatCost(state.totalCostUsd)
8593
+ children: formatCost(state.costUsd ?? (state.totalCostUsd || void 0))
8329
8594
  }),
8330
8595
  actions ? /* @__PURE__ */ jsx("span", {
8331
8596
  className: "self-center",
@@ -10101,30 +10366,6 @@ function MessageContent({ className, ...props }) {
10101
10366
  });
10102
10367
  }
10103
10368
  //#endregion
10104
- //#region src/components/agent/PromptTokenText.tsx
10105
- function PromptTokenText({ text, className }) {
10106
- const tokens = scanPromptTokens(text);
10107
- if (tokens.length === 0) return /* @__PURE__ */ jsx("span", {
10108
- className,
10109
- children: text
10110
- });
10111
- const parts = [];
10112
- let cursor = 0;
10113
- for (const [index, token] of tokens.entries()) {
10114
- if (token.start > cursor) parts.push(text.slice(cursor, token.start));
10115
- parts.push(/* @__PURE__ */ jsx("span", {
10116
- className: cn("font-mono", token.kind === "file" ? "text-accent" : "text-info"),
10117
- children: token.text
10118
- }, `${token.start}-${index}`));
10119
- cursor = token.end;
10120
- }
10121
- if (cursor < text.length) parts.push(text.slice(cursor));
10122
- return /* @__PURE__ */ jsx("span", {
10123
- className,
10124
- children: parts.map((part, index) => /* @__PURE__ */ jsx(Fragment, { children: part }, index))
10125
- });
10126
- }
10127
- //#endregion
10128
10369
  //#region src/components/agent/Reasoning.tsx
10129
10370
  function Reasoning({ children, isStreaming = false, defaultOpen, className }) {
10130
10371
  const [open, setOpen] = useState(defaultOpen ?? isStreaming);
@@ -10432,7 +10673,7 @@ function TurnResultRow({ item }) {
10432
10673
  " · ",
10433
10674
  formatDuration(item.durationMs),
10434
10675
  " · ",
10435
- formatCost(item.totalCostUsd)
10676
+ formatCost(item.costUsd ?? item.totalCostUsd)
10436
10677
  ]
10437
10678
  }),
10438
10679
  /* @__PURE__ */ jsx("div", { className: "h-px flex-1 bg-border" })
@@ -10905,85 +11146,116 @@ function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage,
10905
11146
  terminal,
10906
11147
  brief
10907
11148
  ]);
11149
+ const skillNames = useMemo(() => state.skills?.map((skill) => skill.name), [state.skills]);
10908
11150
  return /* @__PURE__ */ jsx(TranscriptVariantProvider, {
10909
11151
  value: variant,
10910
- children: /* @__PURE__ */ jsxs(Conversation, {
10911
- className: cn(replaying && "invisible", className),
10912
- children: [
10913
- /* @__PURE__ */ jsx(ConversationContent, {
10914
- className: cn(terminal && "gap-0 p-0"),
10915
- children: /* @__PURE__ */ jsxs(TerminalShell, {
10916
- active: terminal,
10917
- fontSize,
10918
- lineHeight,
10919
- affordances,
10920
- children: [
10921
- frame ? frameTask === void 0 && !replaying ? /* @__PURE__ */ jsx("div", {
10922
- className: cn(terminal ? "term-row text-fg-4" : "p-4 text-body-sm text-fg-4"),
10923
- children: "This sub-agent's work is not in this transcript."
10924
- }) : null : items.length === 0 && state.status !== "starting" ? emptyState !== void 0 ? emptyState : /* @__PURE__ */ jsx(SessionEmptyState, {
10925
- cwd: state.cwd,
10926
- hasCommands: !!state.commands?.length,
10927
- hasSkills: !!state.skills?.some((s) => s.enabled),
10928
- canBrowseFiles
10929
- }) : null,
10930
- frame && frameTask === void 0 && !replaying ? null : /* @__PURE__ */ jsx(TranscriptRows, {
10931
- rows,
10932
- boundary,
10933
- since: catchUp?.since,
10934
- terminal,
10935
- replaying,
10936
- stickyPrompt: !frame && stickyPrompt,
10937
- gap,
10938
- fontSize,
10939
- lineHeight,
10940
- items,
10941
- pendingApprovals: state.pendingApprovals,
10942
- scrubber,
10943
- bookmarks: frame ? void 0 : bookmarks,
10944
- affordances,
10945
- fileUrl,
10946
- attachmentUrl,
10947
- hostImage,
10948
- jumpToRecapRef,
10949
- repinRef,
10950
- reveal: frame ? void 0 : reveal,
10951
- frameParentId: frame?.parentToolUseId,
10952
- onOpenSubagent: frame ? void 0 : onOpenSubagent
10953
- }),
10954
- (frame ? frameTask !== void 0 && taskBusy(frameTask, items) : showLoader(state)) ? terminal ? /* @__PURE__ */ jsxs(Fragment$1, { children: [state.items.length > 0 ? /* @__PURE__ */ jsx("div", {
10955
- className: "term-blank",
10956
- "aria-hidden": true
10957
- }) : null, /* @__PURE__ */ jsx(WorkingRow, {
10958
- label: state.status === "starting" ? "Starting…" : "Working…",
10959
- startedAt: runStartedAt,
10960
- tokens: state.contextUsage?.totalTokens
10961
- })] }) : /* @__PURE__ */ jsx(Loader, { label: state.status === "starting" ? "Starting session…" : void 0 }) : null
10962
- ]
10963
- })
10964
- }),
10965
- /* @__PURE__ */ jsx(ConversationScrollButton, {}),
10966
- replaying ? /* @__PURE__ */ jsx("div", {
10967
- "data-slot": "transcript-hold",
10968
- "aria-hidden": true,
10969
- className: "wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden",
10970
- children: /* @__PURE__ */ jsx("div", {
10971
- className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)]", !terminal && "px-4 py-4"),
10972
- children: terminal ? /* @__PURE__ */ jsx(TerminalSurface, {
11152
+ children: /* @__PURE__ */ jsx(SkillNamesProvider, {
11153
+ names: skillNames,
11154
+ children: /* @__PURE__ */ jsxs(Conversation, {
11155
+ className: cn(replaying && "invisible", className),
11156
+ children: [
11157
+ /* @__PURE__ */ jsx(ConversationContent, {
11158
+ className: cn(terminal && "gap-0 p-0"),
11159
+ children: /* @__PURE__ */ jsxs(TerminalShell, {
11160
+ active: terminal,
10973
11161
  fontSize,
10974
11162
  lineHeight,
10975
- affordances: false,
10976
- bleed: "1ch",
10977
- className: "term-transcript",
10978
- children: /* @__PURE__ */ jsx(WorkingRow, { label: "Loading…" })
10979
- }) : /* @__PURE__ */ jsx(Loader, { label: "Loading session…" })
10980
- })
10981
- }) : null
10982
- ]
11163
+ affordances,
11164
+ children: [
11165
+ frame ? frameTask === void 0 && !replaying ? /* @__PURE__ */ jsx("div", {
11166
+ className: cn(terminal ? "term-row text-fg-4" : "p-4 text-body-sm text-fg-4"),
11167
+ children: "This sub-agent's work is not in this transcript."
11168
+ }) : null : items.length === 0 && state.status !== "starting" ? emptyState !== void 0 ? emptyState : /* @__PURE__ */ jsx(SessionEmptyState, {
11169
+ cwd: state.cwd,
11170
+ hasCommands: !!state.commands?.length,
11171
+ hasSkills: !!state.skills?.some((s) => s.enabled),
11172
+ canBrowseFiles
11173
+ }) : null,
11174
+ frame && frameTask === void 0 && !replaying ? null : /* @__PURE__ */ jsx(TranscriptRows, {
11175
+ rows,
11176
+ boundary,
11177
+ since: catchUp?.since,
11178
+ terminal,
11179
+ replaying,
11180
+ stickyPrompt: !frame && stickyPrompt,
11181
+ gap,
11182
+ fontSize,
11183
+ lineHeight,
11184
+ items,
11185
+ pendingApprovals: state.pendingApprovals,
11186
+ scrubber,
11187
+ bookmarks: frame ? void 0 : bookmarks,
11188
+ affordances,
11189
+ fileUrl,
11190
+ attachmentUrl,
11191
+ hostImage,
11192
+ jumpToRecapRef,
11193
+ repinRef,
11194
+ reveal: frame ? void 0 : reveal,
11195
+ frameParentId: frame?.parentToolUseId,
11196
+ onOpenSubagent: frame ? void 0 : onOpenSubagent
11197
+ }),
11198
+ (frame ? frameTask !== void 0 && taskBusy(frameTask, items) : showLoader(state)) ? terminal ? /* @__PURE__ */ jsxs(Fragment$1, { children: [state.items.length > 0 ? /* @__PURE__ */ jsx("div", {
11199
+ className: "term-blank",
11200
+ "aria-hidden": true
11201
+ }) : null, /* @__PURE__ */ jsx(WorkingRow, {
11202
+ label: state.status === "starting" ? "Starting…" : "Working…",
11203
+ startedAt: runStartedAt,
11204
+ tokens: state.contextUsage?.totalTokens
11205
+ })] }) : /* @__PURE__ */ jsx(Loader, { label: state.status === "starting" ? "Starting session…" : void 0 }) : null
11206
+ ]
11207
+ })
11208
+ }),
11209
+ /* @__PURE__ */ jsx(ConversationScrollButton, {}),
11210
+ replaying ? /* @__PURE__ */ jsx("div", {
11211
+ "data-slot": "transcript-hold",
11212
+ "aria-hidden": true,
11213
+ className: "wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden",
11214
+ children: /* @__PURE__ */ jsx("div", {
11215
+ className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)]", !terminal && "px-4 py-4"),
11216
+ children: terminal ? /* @__PURE__ */ jsx(TerminalSurface, {
11217
+ fontSize,
11218
+ lineHeight,
11219
+ affordances: false,
11220
+ bleed: "1ch",
11221
+ className: "term-transcript",
11222
+ children: /* @__PURE__ */ jsx(WorkingRow, { label: "Loading…" })
11223
+ }) : /* @__PURE__ */ jsx(Loader, { label: "Loading session…" })
11224
+ })
11225
+ }) : null
11226
+ ]
11227
+ })
10983
11228
  })
10984
11229
  });
10985
11230
  }
10986
11231
  //#endregion
11232
+ //#region src/lib/session-cost.ts
11233
+ const GAP_SHARE = .02;
11234
+ function sessionCost(input) {
11235
+ const byModel = input.usageByModel ?? {};
11236
+ const breakdown = costOfByModel(byModel);
11237
+ const computedUsd = input.costUsd ?? (breakdown.unpriced || Object.keys(byModel).length === 0 ? void 0 : breakdown.total);
11238
+ const reportedUsd = input.totalCostUsd;
11239
+ const rows = Object.entries(byModel).map(([model, usage]) => ({
11240
+ model: canonicalModel(model),
11241
+ usage,
11242
+ tokens: totalTokens(usage),
11243
+ costUsd: rateFor(model) ? costOf(usage, model).total : void 0
11244
+ })).sort((a, b) => b.tokens - a.tokens);
11245
+ const gap = computedUsd !== void 0 && reportedUsd !== void 0 ? reportedUsd - computedUsd : void 0;
11246
+ const worthShowing = gap !== void 0 && Math.abs(gap) > Math.max(.01, GAP_SHARE * Math.max(computedUsd ?? 0, reportedUsd ?? 0));
11247
+ return {
11248
+ displayUsd: computedUsd ?? reportedUsd,
11249
+ computedUsd,
11250
+ reportedUsd,
11251
+ breakdown,
11252
+ rows,
11253
+ unpricedShare: breakdown.unpricedShare,
11254
+ showUnpricedWarning: breakdown.unpricedShare > UNPRICED_WARN_SHARE,
11255
+ gapUsd: worthShowing ? gap : void 0
11256
+ };
11257
+ }
11258
+ //#endregion
10987
11259
  //#region src/components/agent/UsageMeters.tsx
10988
11260
  function useMinuteClock(active = true) {
10989
11261
  const [now, setNow] = useState(() => Date.now());
@@ -11009,6 +11281,7 @@ function UsageMeters({ windows, now, className }) {
11009
11281
  function UsageMeter({ window, now }) {
11010
11282
  const { key: windowKey, info, updatedAt, inferredReset } = window;
11011
11283
  const utilization = info.utilization ?? 0;
11284
+ const stale = usageIsStale(window, now);
11012
11285
  const resetsAtMs = info.resetsAt !== void 0 ? info.resetsAt * 1e3 : void 0;
11013
11286
  const duration = rateLimitWindowSeconds(windowKey);
11014
11287
  const remaining = resetsAtMs !== void 0 ? (resetsAtMs - now) / 1e3 : void 0;
@@ -11020,14 +11293,14 @@ function UsageMeter({ window, now }) {
11020
11293
  className: "truncate text-body-sm text-fg-1",
11021
11294
  children: formatRateLimitWindowLong(windowKey)
11022
11295
  }), /* @__PURE__ */ jsxs("span", {
11023
- className: cn("shrink-0 font-mono text-body-sm font-medium", info.status === "rejected" ? "text-danger" : "text-fg-1"),
11296
+ className: cn("shrink-0 font-mono text-body-sm font-medium", info.status === "rejected" ? "text-danger" : stale ? "text-fg-4" : "text-fg-1"),
11024
11297
  children: [utilization.toFixed(0), "% used"]
11025
11298
  })]
11026
11299
  }),
11027
11300
  /* @__PURE__ */ jsxs("div", {
11028
11301
  className: "relative mt-2 h-2 rounded-full bg-border",
11029
11302
  children: [/* @__PURE__ */ jsx("div", {
11030
- className: cn("h-full rounded-full", meterTintClass(utilization)),
11303
+ className: cn("h-full rounded-full", meterTintClass(utilization), stale && "opacity-40"),
11031
11304
  style: { width: `${Math.min(100, Math.max(2, utilization))}%` }
11032
11305
  }), pace !== void 0 ? /* @__PURE__ */ jsx("span", {
11033
11306
  "aria-hidden": true,
@@ -11056,15 +11329,144 @@ function UsageMeter({ window, now }) {
11056
11329
  children: "limit reached"
11057
11330
  }) : null,
11058
11331
  inferredReset ? /* @__PURE__ */ jsx("span", { children: "window reset · nothing reported since" }) : null,
11059
- updatedAt && !inferredReset ? /* @__PURE__ */ jsx("span", { children: formatAgoPrecise(updatedAt, now) }) : null
11332
+ updatedAt && !inferredReset ? /* @__PURE__ */ jsxs("span", {
11333
+ className: cn(stale && "text-warning"),
11334
+ children: [stale ? "last reported " : "", formatAgoPrecise(updatedAt, now)]
11335
+ }) : null
11060
11336
  ]
11061
11337
  })
11062
11338
  ] });
11063
11339
  }
11064
11340
  //#endregion
11065
11341
  //#region src/components/agent/UsageDialog.tsx
11066
- function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updatedAt, open, onOpenChange, className }) {
11342
+ function Line({ label, value, tone }) {
11343
+ return /* @__PURE__ */ jsxs("div", {
11344
+ className: "flex items-baseline justify-between gap-4",
11345
+ children: [/* @__PURE__ */ jsx("span", {
11346
+ className: tone === "muted" ? "text-label text-fg-4" : "text-label text-fg-3",
11347
+ children: label
11348
+ }), /* @__PURE__ */ jsx("span", {
11349
+ className: `font-mono text-body-sm ${tone === "muted" ? "text-fg-4" : "text-fg-1"}`,
11350
+ children: value
11351
+ })]
11352
+ });
11353
+ }
11354
+ function CostEvidence({ cost, engine }) {
11355
+ const { breakdown, rows } = cost;
11356
+ if (rows.length === 0) return /* @__PURE__ */ jsx(Line, {
11357
+ label: "This session has cost",
11358
+ value: formatCost(cost.displayUsd)
11359
+ });
11360
+ return /* @__PURE__ */ jsxs("div", {
11361
+ className: "space-y-3",
11362
+ children: [
11363
+ /* @__PURE__ */ jsxs("div", {
11364
+ className: "flex items-baseline justify-between gap-4",
11365
+ children: [/* @__PURE__ */ jsx("span", {
11366
+ className: "text-label text-fg-3",
11367
+ children: "This session has cost"
11368
+ }), /* @__PURE__ */ jsx("span", {
11369
+ className: "font-mono text-body text-fg-1",
11370
+ children: formatCost(cost.displayUsd)
11371
+ })]
11372
+ }),
11373
+ /* @__PURE__ */ jsxs("div", {
11374
+ className: "space-y-1 border-t border-border pt-2",
11375
+ children: [
11376
+ /* @__PURE__ */ jsx(Line, {
11377
+ label: "input",
11378
+ value: formatCost(breakdown.input),
11379
+ tone: "muted"
11380
+ }),
11381
+ /* @__PURE__ */ jsx(Line, {
11382
+ label: "output",
11383
+ value: formatCost(breakdown.output),
11384
+ tone: "muted"
11385
+ }),
11386
+ /* @__PURE__ */ jsx(Line, {
11387
+ label: "cache write",
11388
+ value: formatCost(breakdown.cacheWrite),
11389
+ tone: "muted"
11390
+ }),
11391
+ /* @__PURE__ */ jsx(Line, {
11392
+ label: "cache read",
11393
+ value: formatCost(breakdown.cacheRead),
11394
+ tone: "muted"
11395
+ })
11396
+ ]
11397
+ }),
11398
+ /* @__PURE__ */ jsx("div", {
11399
+ className: "space-y-1 border-t border-border pt-2",
11400
+ children: rows.map((row) => /* @__PURE__ */ jsxs("div", {
11401
+ className: "flex items-baseline justify-between gap-4",
11402
+ children: [/* @__PURE__ */ jsx("span", {
11403
+ className: "truncate text-label text-fg-4",
11404
+ children: row.model
11405
+ }), /* @__PURE__ */ jsxs("span", {
11406
+ className: "shrink-0 font-mono text-label text-fg-4",
11407
+ children: [
11408
+ formatTokens(row.tokens),
11409
+ " · ",
11410
+ row.costUsd === void 0 ? "no rate" : formatCost(row.costUsd)
11411
+ ]
11412
+ })]
11413
+ }, row.model))
11414
+ }),
11415
+ cost.showUnpricedWarning ? /* @__PURE__ */ jsxs("p", {
11416
+ className: "text-label text-warning",
11417
+ children: [Math.round(cost.unpricedShare * 100), "% of these tokens ran on a model with no bundled rate, so the total is an understatement rather than an estimate."]
11418
+ }) : null,
11419
+ cost.gapUsd !== void 0 ? /* @__PURE__ */ jsxs("p", {
11420
+ className: "text-label text-fg-4",
11421
+ children: [
11422
+ engine === "claude" ? "Claude Code" : engine,
11423
+ " reports ",
11424
+ formatCost(cost.reportedUsd),
11425
+ " for the same tokens."
11426
+ ]
11427
+ }) : null
11428
+ ]
11429
+ });
11430
+ }
11431
+ function ProfileSpendSection({ spend }) {
11432
+ const { subscription } = spend;
11433
+ return /* @__PURE__ */ jsxs("div", {
11434
+ className: "mt-4 space-y-1 border-t border-border pt-3",
11435
+ children: [
11436
+ /* @__PURE__ */ jsx(Line, {
11437
+ label: "This profile, last 7 days",
11438
+ value: formatCost(spend.weekUsd)
11439
+ }),
11440
+ /* @__PURE__ */ jsx(Line, {
11441
+ label: "Last 30 days",
11442
+ value: formatCost(spend.monthUsd),
11443
+ tone: "muted"
11444
+ }),
11445
+ subscription ? /* @__PURE__ */ jsxs("p", {
11446
+ className: "pt-1 text-label text-fg-4",
11447
+ children: [
11448
+ formatMultiple(subscription.ratio),
11449
+ " the ",
11450
+ formatCost(subscription.weeklyShareUsd),
11451
+ " weekly share of your",
11452
+ " ",
11453
+ formatCost(subscription.monthlyUsd),
11454
+ " a month. A subscription is a flat fee, so this is leverage, not a bill."
11455
+ ]
11456
+ }) : null
11457
+ ]
11458
+ });
11459
+ }
11460
+ function formatMultiple(ratio) {
11461
+ return ratio >= 10 ? `${Math.round(ratio)}x` : `${ratio.toFixed(1)}x`;
11462
+ }
11463
+ function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, costUsd, usageByModel, spend, updatedAt, open, onOpenChange, className }) {
11067
11464
  const now = useMinuteClock(open);
11465
+ const cost = sessionCost({
11466
+ costUsd,
11467
+ totalCostUsd,
11468
+ usageByModel
11469
+ });
11068
11470
  return /* @__PURE__ */ jsx(Dialog$1, {
11069
11471
  open,
11070
11472
  onOpenChange,
@@ -11086,15 +11488,17 @@ function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updat
11086
11488
  windows: rateLimits,
11087
11489
  now
11088
11490
  }),
11089
- /* @__PURE__ */ jsxs("div", {
11090
- className: "mt-5 flex items-baseline justify-between gap-4 border-t border-border pt-3",
11091
- children: [/* @__PURE__ */ jsx("span", {
11092
- className: "text-label text-fg-3",
11093
- children: "This session has cost"
11094
- }), /* @__PURE__ */ jsx("span", {
11095
- className: "font-mono text-body-sm text-fg-1",
11096
- children: formatCost(totalCostUsd)
11097
- })]
11491
+ /* @__PURE__ */ jsx("div", {
11492
+ className: "mt-5 border-t border-border pt-3",
11493
+ children: /* @__PURE__ */ jsx(CostEvidence, {
11494
+ cost,
11495
+ engine
11496
+ })
11497
+ }),
11498
+ spend ? /* @__PURE__ */ jsx(ProfileSpendSection, { spend }) : null,
11499
+ /* @__PURE__ */ jsx("p", {
11500
+ className: "mt-3 text-label text-fg-4",
11501
+ children: PRICING_NOTE
11098
11502
  }),
11099
11503
  updatedAt ? /* @__PURE__ */ jsxs("p", {
11100
11504
  className: "mt-2 text-label text-fg-4",
@@ -11135,7 +11539,7 @@ const INTERACTIVE = [
11135
11539
  "[role=\"radio\"]",
11136
11540
  "[role=\"tab\"]"
11137
11541
  ].join(",");
11138
- function SessionPanel({ client, sessionId, header, panelSurface = "internal", statusSurface = "internal", statusPlacement = "top", onOpenPanel, onVitals, transcriptVariant = "cards", transcriptDensity = "comfortable", transcriptFont = "sans", affordances, terminalMetrics, scrubber = false, bookmarks, onToggleBookmark, reveal, subagents, openSubagent, onSubagentChange, stickyPrompt = false, controlsSurface = "internal", onControls, focusComposerOnClick = false, unseen, readOnly = false, approvalPrompts, toolHost, clientTools, cacheTranscript, emptyState, onLinkClick, fontSize, className }) {
11542
+ function SessionPanel({ client, sessionId, header, panelSurface = "internal", statusSurface = "internal", statusPlacement = "top", onOpenPanel, onVitals, transcriptVariant = "cards", transcriptDensity = "comfortable", transcriptFont = "sans", affordances, terminalMetrics, scrubber = false, bookmarks, onToggleBookmark, reveal, subagents, openSubagent, onSubagentChange, stickyPrompt = false, controlsSurface = "internal", onControls, focusComposerOnClick = false, unseen, readOnly = false, approvalPrompts, toolHost, clientTools, cacheTranscript, emptyState, onLinkClick, onOpenFile, fontSize, className }) {
11139
11543
  const effectiveTermFontSize = terminalMetrics?.fontSize ?? fontSize;
11140
11544
  const effectiveTermLineHeight = terminalMetrics?.lineHeight ?? (fontSize !== void 0 ? Math.round(fontSize * (18 / 13)) : void 0);
11141
11545
  const external = panelSurface === "external";
@@ -11146,7 +11550,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11146
11550
  const [panel, setPanel] = useState();
11147
11551
  const [showCompletedTasks, setShowCompletedTasks] = useState(false);
11148
11552
  const [taskReveal, setTaskReveal] = useState();
11149
- const { state, connection, replaying, protocolMismatch, shell, models, effectiveModel, handle, send, runShell, approve, deny, interrupt, setModel, setPermissionMode, reconnectNow, loadFullResult } = useClaudeSession(client, sessionId, {
11553
+ const { state, connection, replaying, protocolMismatch, shell, models, effectiveModel, handle, send, runShell, approve, deny, interrupt, clearContext, setModel, setPermissionMode, reconnectNow, loadFullResult } = useClaudeSession(client, sessionId, {
11150
11554
  onProtocolError: setProtocolError,
11151
11555
  cacheTranscript
11152
11556
  });
@@ -11186,7 +11590,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11186
11590
  } : toolHost);
11187
11591
  const terminal = transcriptVariant === "terminal";
11188
11592
  const capabilities = state.capabilities;
11189
- const { usage: profileUsage } = useProfileUsage(client, state.session?.profile, { enabled: capabilities.rateLimits });
11593
+ const { usage: profileUsage, spend: profileSpend } = useProfileUsage(client, state.session?.profile, { enabled: capabilities.rateLimits });
11190
11594
  const usage = useMemo(() => mergeUsage({
11191
11595
  rateLimits: state.rateLimits,
11192
11596
  updatedAt: state.rateLimitsUpdatedAt
@@ -11208,6 +11612,86 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11208
11612
  subagents,
11209
11613
  state.session?.subagents
11210
11614
  ]);
11615
+ const clientCommands = useMemo(() => {
11616
+ const modes = capabilities.permissionModes;
11617
+ const built = [];
11618
+ if (models.length > 0) built.push({
11619
+ name: "model",
11620
+ description: "Switch the model for this session",
11621
+ argumentHint: "<model>",
11622
+ requiresArgs: true,
11623
+ run: (args) => {
11624
+ const wanted = args.split(/\s+/)[0];
11625
+ if (!wanted) return false;
11626
+ setModel(wanted);
11627
+ return true;
11628
+ }
11629
+ });
11630
+ if (modes.length > 1) built.push({
11631
+ name: "permissions",
11632
+ description: `Set the permission mode (${modes.join(", ")})`,
11633
+ argumentHint: "<mode>",
11634
+ requiresArgs: true,
11635
+ run: (args) => {
11636
+ const wanted = args.split(/\s+/)[0];
11637
+ if (!modes.includes(wanted)) return false;
11638
+ setPermissionMode(wanted);
11639
+ return true;
11640
+ }
11641
+ });
11642
+ if (capabilities.clearContext) built.push({
11643
+ name: "clear",
11644
+ description: "Clear the conversation - the session keeps running and the old one stays resumable",
11645
+ run: () => {
11646
+ clearContext();
11647
+ return true;
11648
+ }
11649
+ });
11650
+ if (capabilities.mcpStatus) built.push({
11651
+ name: "mcp",
11652
+ description: "MCP servers and their status",
11653
+ run: () => (openPanel("mcp"), true)
11654
+ });
11655
+ if (capabilities.contextUsage) built.push({
11656
+ name: "context",
11657
+ description: "Context window usage",
11658
+ run: () => (openPanel("context"), true)
11659
+ });
11660
+ built.push({
11661
+ name: "status",
11662
+ description: "Session details",
11663
+ run: () => (openPanel("info"), true)
11664
+ });
11665
+ if (capabilities.rateLimits) built.push({
11666
+ name: "usage",
11667
+ description: "Rate limits and spend",
11668
+ run: () => (openPanel("usage"), true)
11669
+ });
11670
+ if (capabilities.skillsList) built.push({
11671
+ name: "skills",
11672
+ description: "Browse the skills this session can use",
11673
+ run: () => (openPanel("skills"), true)
11674
+ });
11675
+ return built;
11676
+ }, [
11677
+ capabilities,
11678
+ models.length,
11679
+ openPanel,
11680
+ clearContext,
11681
+ setModel,
11682
+ setPermissionMode
11683
+ ]);
11684
+ const composerCommands = useMemo(() => composerCommandRows(mergeComposerRows({
11685
+ commands: capabilities.slashCommands ? state.commands : void 0,
11686
+ clientCommands,
11687
+ skills: capabilities.skillsList ? state.skills : void 0
11688
+ })), [
11689
+ capabilities.slashCommands,
11690
+ capabilities.skillsList,
11691
+ state.commands,
11692
+ state.skills,
11693
+ clientCommands
11694
+ ]);
11211
11695
  const onVitalsRef = useRef(onVitals);
11212
11696
  onVitalsRef.current = onVitals;
11213
11697
  const vitalsModel = effectiveModel ?? state.model;
@@ -11223,6 +11707,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11223
11707
  permissionMode: state.permissionMode,
11224
11708
  permissionModes,
11225
11709
  skills: state.skills,
11710
+ composerCommands,
11226
11711
  cwd: state.cwd,
11227
11712
  contextUsage: state.contextUsage,
11228
11713
  tasks,
@@ -11241,6 +11726,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11241
11726
  state.permissionMode,
11242
11727
  permissionModes,
11243
11728
  state.skills,
11729
+ composerCommands,
11244
11730
  state.cwd,
11245
11731
  state.contextUsage,
11246
11732
  tasks,
@@ -11296,26 +11782,10 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11296
11782
  const composerRef = useRef(null);
11297
11783
  const jumpToRecap = useRef(null);
11298
11784
  const repinTranscript = useRef(null);
11299
- const commands = useMemo(() => {
11300
- if (!state.commands) return;
11301
- if (state.commands.some((c) => c.name === "model")) return state.commands;
11302
- return [{
11303
- name: "model",
11304
- description: "Switch the model for this session",
11305
- argumentHint: "<model>"
11306
- }, ...state.commands];
11307
- }, [state.commands]);
11308
11785
  const handleSend = (text, attachmentIds) => {
11309
11786
  if (attachmentIds.length === 0) {
11310
- const modelCommand = /^\/model\s+(\S+)$/.exec(text);
11311
- if (modelCommand) {
11312
- setModel(modelCommand[1]);
11313
- return;
11314
- }
11315
- if (capabilities.mcpStatus && text.trim() === "/mcp") {
11316
- openPanel("mcp");
11317
- return;
11318
- }
11787
+ const local = matchClientCommand(text, clientCommands);
11788
+ if (local && local.command.run(local.args)) return;
11319
11789
  }
11320
11790
  setCaughtUp(true);
11321
11791
  repinTranscript.current?.();
@@ -11381,6 +11851,10 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11381
11851
  onOpenTasks: external && !onOpenPanel ? void 0 : () => openPanel("tasks"),
11382
11852
  actions: headerTakesActions ? void 0 : menu
11383
11853
  });
11854
+ const fileLinks = useMemo(() => onOpenFile ? {
11855
+ cwd: state.cwd,
11856
+ open: onOpenFile
11857
+ } : void 0, [onOpenFile, state.cwd]);
11384
11858
  const panelRef = useRef(null);
11385
11859
  useEffect(() => {
11386
11860
  if (!onLinkClick) return;
@@ -11409,224 +11883,231 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11409
11883
  value: transcriptVariant,
11410
11884
  children: /* @__PURE__ */ jsx(TranscriptDensityProvider, {
11411
11885
  value: transcriptDensity,
11412
- children: /* @__PURE__ */ jsx(ToolResultFetchProvider, {
11413
- value: loadFullResult,
11414
- children: /* @__PURE__ */ jsx(ToolTitleProvider, {
11415
- value: state.toolTitles,
11416
- children: /* @__PURE__ */ jsx(ToolResultImageProvider, {
11417
- value: resultImages,
11418
- children: /* @__PURE__ */ jsxs("div", {
11419
- ref: panelRef,
11420
- "data-slot": "session-panel",
11421
- "data-agent-font": transcriptFont,
11422
- onClick: handleClick,
11423
- className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-bg", className),
11424
- style: fontSize !== void 0 ? { "--wd-font-size": `${Math.round(fontSize)}px` } : void 0,
11425
- children: [
11426
- headerTakesActions ? header({ actions: menu }) : header,
11427
- statusPlacement === "top" ? statusBar : null,
11428
- protocolMismatch !== void 0 ? /* @__PURE__ */ jsxs(Notice, {
11429
- level: "warning",
11430
- children: [
11431
- "Server speaks protocol v",
11432
- protocolMismatch,
11433
- ", this build renders v",
11434
- PROTOCOL_VERSION,
11435
- ". Some events may not render."
11436
- ]
11437
- }) : null,
11438
- protocolError ? /* @__PURE__ */ jsx(Notice, {
11439
- level: "error",
11440
- onDismiss: () => setProtocolError(void 0),
11441
- children: protocolError
11442
- }) : null,
11443
- subagentId !== void 0 ? /* @__PURE__ */ jsx(SubagentStrip, {
11444
- task: subagentTask,
11445
- items: subagentFrameItems,
11446
- label: subagentFallbackLabel,
11447
- onBack: leaveSubagent,
11448
- terminal,
11449
- fontSize: effectiveTermFontSize,
11450
- lineHeight: effectiveTermLineHeight
11451
- }) : null,
11452
- /* @__PURE__ */ jsx(BookmarkProvider, {
11453
- value: bookmarkHandle,
11454
- children: /* @__PURE__ */ jsx(Transcript, {
11455
- state,
11456
- fileUrl: sessionId ? (path) => client.sessionFileUrl(sessionId, path) : void 0,
11457
- attachmentUrl: sessionId ? (id) => client.attachmentUrl(sessionId, id) : void 0,
11458
- canBrowseFiles: hostFiles.available,
11459
- hostImage,
11460
- variant: transcriptVariant,
11461
- density: transcriptDensity,
11462
- fontSize: effectiveTermFontSize,
11463
- lineHeight: effectiveTermLineHeight,
11464
- affordances,
11465
- stickyPrompt,
11466
- scrubber,
11467
- bookmarks,
11468
- replaying,
11469
- catchUp: catchUp && newCount > 0 ? {
11470
- from: catchUp.itemCount,
11471
- since: catchUp.since
11472
- } : void 0,
11473
- reveal: taskReveal ?? returnReveal ?? reveal,
11474
- frame: subagentId === void 0 ? void 0 : { parentToolUseId: subagentId },
11475
- onOpenSubagent: enterSubagent,
11476
- emptyState,
11477
- jumpToRecapRef: jumpToRecap,
11478
- repinRef: repinTranscript
11479
- }, subagentId ?? "session")
11480
- }),
11481
- catchUp && newCount > 0 && !replaying && subagentId === void 0 ? /* @__PURE__ */ jsx("div", {
11482
- className: "px-3 pb-1",
11483
- children: /* @__PURE__ */ jsxs("div", {
11484
- "data-slot": "catch-up",
11485
- className: "mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3",
11886
+ children: /* @__PURE__ */ jsx(FileLinkProvider, {
11887
+ value: fileLinks,
11888
+ children: /* @__PURE__ */ jsx(ToolResultFetchProvider, {
11889
+ value: loadFullResult,
11890
+ children: /* @__PURE__ */ jsx(ToolTitleProvider, {
11891
+ value: state.toolTitles,
11892
+ children: /* @__PURE__ */ jsx(ToolResultImageProvider, {
11893
+ value: resultImages,
11894
+ children: /* @__PURE__ */ jsxs("div", {
11895
+ ref: panelRef,
11896
+ "data-slot": "session-panel",
11897
+ "data-agent-font": transcriptFont,
11898
+ onClick: handleClick,
11899
+ className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-bg", className),
11900
+ style: fontSize !== void 0 ? { "--wd-font-size": `${Math.round(fontSize)}px` } : void 0,
11901
+ children: [
11902
+ headerTakesActions ? header({ actions: menu }) : header,
11903
+ statusPlacement === "top" ? statusBar : null,
11904
+ protocolMismatch !== void 0 ? /* @__PURE__ */ jsxs(Notice, {
11905
+ level: "warning",
11486
11906
  children: [
11487
- /* @__PURE__ */ jsx("span", {
11488
- "aria-hidden": true,
11489
- className: cn("select-none", terminal ? "text-fg-3" : "text-accent"),
11490
- children: "※"
11491
- }),
11492
- /* @__PURE__ */ jsxs("span", {
11493
- className: "min-w-0 flex-1 truncate",
11494
- children: [
11495
- newCount,
11496
- " new ",
11497
- newCount === 1 ? "row" : "rows",
11498
- catchUp.since !== void 0 ? ` since you were last here` : ""
11499
- ]
11500
- }),
11501
- /* @__PURE__ */ jsx("button", {
11502
- type: "button",
11503
- onClick: () => jumpToRecap.current?.(),
11504
- className: "shrink-0 underline-offset-2 hover:text-fg-1 hover:underline",
11505
- children: "jump"
11506
- }),
11507
- /* @__PURE__ */ jsx("button", {
11508
- type: "button",
11509
- onClick: () => setCaughtUp(true),
11510
- className: "shrink-0 underline-offset-2 hover:text-fg-1 hover:underline",
11511
- children: "dismiss"
11512
- })
11907
+ "Server speaks protocol v",
11908
+ protocolMismatch,
11909
+ ", this build renders v",
11910
+ PROTOCOL_VERSION,
11911
+ ". Some events may not render."
11513
11912
  ]
11514
- })
11515
- }) : null,
11516
- !readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? /* @__PURE__ */ jsx("div", {
11517
- className: cn(terminal ? "pb-2" : "px-3 pb-2"),
11518
- children: /* @__PURE__ */ jsx(PromptSurface, {
11913
+ }) : null,
11914
+ protocolError ? /* @__PURE__ */ jsx(Notice, {
11915
+ level: "error",
11916
+ onDismiss: () => setProtocolError(void 0),
11917
+ children: protocolError
11918
+ }) : null,
11919
+ subagentId !== void 0 ? /* @__PURE__ */ jsx(SubagentStrip, {
11920
+ task: subagentTask,
11921
+ items: subagentFrameItems,
11922
+ label: subagentFallbackLabel,
11923
+ onBack: leaveSubagent,
11519
11924
  terminal,
11520
- metrics: {
11925
+ fontSize: effectiveTermFontSize,
11926
+ lineHeight: effectiveTermLineHeight
11927
+ }) : null,
11928
+ /* @__PURE__ */ jsx(BookmarkProvider, {
11929
+ value: bookmarkHandle,
11930
+ children: /* @__PURE__ */ jsx(Transcript, {
11931
+ state,
11932
+ fileUrl: sessionId ? (path) => client.sessionFileUrl(sessionId, path) : void 0,
11933
+ attachmentUrl: sessionId ? (id) => client.attachmentUrl(sessionId, id) : void 0,
11934
+ canBrowseFiles: hostFiles.available,
11935
+ hostImage,
11936
+ variant: transcriptVariant,
11937
+ density: transcriptDensity,
11521
11938
  fontSize: effectiveTermFontSize,
11522
- lineHeight: effectiveTermLineHeight
11523
- },
11524
- affordances,
11525
- children: state.pendingApprovals.map((request) => {
11526
- const HostPrompt = approvalPrompts?.[request.toolName];
11527
- if (HostPrompt) return /* @__PURE__ */ jsx(HostPrompt, {
11528
- request,
11529
- onApprove: approve,
11530
- onDeny: deny
11531
- }, request.id);
11532
- const isQuestion = request.toolName === "AskUserQuestion" && parseUserQuestions(request.input).length > 0;
11533
- if (terminal) return isQuestion ? /* @__PURE__ */ jsx(TerminalQuestionPrompt, {
11534
- request,
11535
- onAnswer: approve,
11536
- onDismiss: (id) => deny(id, "Question dismissed by user")
11537
- }, request.id) : /* @__PURE__ */ jsx(TerminalPermissionPrompt, {
11538
- request,
11539
- onApprove: approve,
11540
- onDeny: deny
11541
- }, request.id);
11542
- return isQuestion ? /* @__PURE__ */ jsx(QuestionPrompt, {
11543
- request,
11544
- onAnswer: approve,
11545
- onDismiss: (id) => deny(id, "Question dismissed by user")
11546
- }, request.id) : /* @__PURE__ */ jsx(PermissionPrompt, {
11547
- request,
11548
- onApprove: approve,
11549
- onDeny: deny
11550
- }, request.id);
11551
- })
11552
- })
11553
- }) : null,
11554
- readOnly || subagentId !== void 0 ? null : /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(Composer, {
11555
- ref: composerRef,
11556
- onSend: handleSend,
11557
- onInterrupt: interrupt,
11558
- busy,
11559
- disabled: ended || !sessionId,
11560
- commands: capabilities.slashCommands ? commands : void 0,
11561
- skills: capabilities.skillsList ? state.skills : void 0,
11562
- attachments,
11563
- draft,
11564
- onSearchFiles: hostFiles.available ? searchComposerFiles : void 0,
11565
- onShellCommand: shell ? runShell : void 0,
11566
- layout: controlsExternal ? "inline" : "stacked",
11567
- toolbar: controlsExternal ? void 0 : sessionControls,
11568
- fontSize: effectiveTermFontSize,
11569
- lineHeight: effectiveTermLineHeight,
11570
- affordances
11571
- }) }),
11572
- statusPlacement === "bottom" ? statusBar : null,
11573
- !external ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
11574
- /* @__PURE__ */ jsx(SessionInfoDialog, {
11575
- state,
11576
- client,
11577
- sessionId,
11578
- open: panel === "info",
11579
- onOpenChange: (next) => setPanel(next ? "info" : void 0)
11580
- }),
11581
- /* @__PURE__ */ jsx(ContextDialog, {
11582
- usage: state.contextUsage,
11583
- engine: state.engine ?? "claude",
11584
- open: panel === "context",
11585
- onOpenChange: (next) => setPanel(next ? "context" : void 0)
11586
- }),
11587
- /* @__PURE__ */ jsx(UsageDialog, {
11588
- rateLimits: windows,
11589
- subscriptionType: state.subscriptionType,
11590
- engine: state.engine ?? "claude",
11591
- totalCostUsd: state.totalCostUsd,
11592
- updatedAt: usageUpdatedAt,
11593
- open: panel === "usage",
11594
- onOpenChange: (next) => setPanel(next ? "usage" : void 0)
11939
+ lineHeight: effectiveTermLineHeight,
11940
+ affordances,
11941
+ stickyPrompt,
11942
+ scrubber,
11943
+ bookmarks,
11944
+ replaying,
11945
+ catchUp: catchUp && newCount > 0 ? {
11946
+ from: catchUp.itemCount,
11947
+ since: catchUp.since
11948
+ } : void 0,
11949
+ reveal: taskReveal ?? returnReveal ?? reveal,
11950
+ frame: subagentId === void 0 ? void 0 : { parentToolUseId: subagentId },
11951
+ onOpenSubagent: enterSubagent,
11952
+ emptyState,
11953
+ jumpToRecapRef: jumpToRecap,
11954
+ repinRef: repinTranscript
11955
+ }, subagentId ?? "session")
11595
11956
  }),
11596
- /* @__PURE__ */ jsx(McpDialog, {
11597
- client,
11598
- sessionId,
11599
- canManageServers: capabilities.mcpServerActions,
11600
- open: panel === "mcp",
11601
- onOpenChange: (next) => setPanel(next ? "mcp" : void 0)
11602
- }),
11603
- /* @__PURE__ */ jsx(TasksDialog, {
11604
- tasks,
11605
- showCompleted: showCompletedTasks,
11606
- onShowCompletedChange: setShowCompletedTasks,
11607
- onSelectTask: (task) => {
11608
- if (task.toolUseId) setTaskReveal({
11609
- toolUseId: task.toolUseId,
11610
- nonce: Date.now()
11611
- });
11612
- },
11613
- open: panel === "tasks",
11614
- onOpenChange: (next) => setPanel(next ? "tasks" : void 0)
11615
- }),
11616
- /* @__PURE__ */ jsx(SkillsDialog, {
11617
- skills: state.skills,
11618
- open: panel === "skills",
11619
- onOpenChange: (next) => setPanel(next ? "skills" : void 0),
11620
- onUse: (skill) => composerRef.current?.insertText(skillPrompt(skill))
11621
- }),
11622
- /* @__PURE__ */ jsx(HostFilesDialog, {
11623
- client,
11624
- cwd: state.cwd,
11625
- open: panel === "files",
11626
- onOpenChange: (next) => setPanel(next ? "files" : void 0)
11627
- })
11628
- ] }) : null
11629
- ]
11957
+ catchUp && newCount > 0 && !replaying && subagentId === void 0 ? /* @__PURE__ */ jsx("div", {
11958
+ className: "px-3 pb-1",
11959
+ children: /* @__PURE__ */ jsxs("div", {
11960
+ "data-slot": "catch-up",
11961
+ className: "mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3",
11962
+ children: [
11963
+ /* @__PURE__ */ jsx("span", {
11964
+ "aria-hidden": true,
11965
+ className: cn("select-none", terminal ? "text-fg-3" : "text-accent"),
11966
+ children: "※"
11967
+ }),
11968
+ /* @__PURE__ */ jsxs("span", {
11969
+ className: "min-w-0 flex-1 truncate",
11970
+ children: [
11971
+ newCount,
11972
+ " new ",
11973
+ newCount === 1 ? "row" : "rows",
11974
+ catchUp.since !== void 0 ? ` since you were last here` : ""
11975
+ ]
11976
+ }),
11977
+ /* @__PURE__ */ jsx("button", {
11978
+ type: "button",
11979
+ onClick: () => jumpToRecap.current?.(),
11980
+ className: "shrink-0 underline-offset-2 hover:text-fg-1 hover:underline",
11981
+ children: "jump"
11982
+ }),
11983
+ /* @__PURE__ */ jsx("button", {
11984
+ type: "button",
11985
+ onClick: () => setCaughtUp(true),
11986
+ className: "shrink-0 underline-offset-2 hover:text-fg-1 hover:underline",
11987
+ children: "dismiss"
11988
+ })
11989
+ ]
11990
+ })
11991
+ }) : null,
11992
+ !readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? /* @__PURE__ */ jsx("div", {
11993
+ className: cn(terminal ? "pb-2" : "px-3 pb-2"),
11994
+ children: /* @__PURE__ */ jsx(PromptSurface, {
11995
+ terminal,
11996
+ metrics: {
11997
+ fontSize: effectiveTermFontSize,
11998
+ lineHeight: effectiveTermLineHeight
11999
+ },
12000
+ affordances,
12001
+ children: state.pendingApprovals.map((request) => {
12002
+ const HostPrompt = approvalPrompts?.[request.toolName];
12003
+ if (HostPrompt) return /* @__PURE__ */ jsx(HostPrompt, {
12004
+ request,
12005
+ onApprove: approve,
12006
+ onDeny: deny
12007
+ }, request.id);
12008
+ const isQuestion = request.toolName === "AskUserQuestion" && parseUserQuestions(request.input).length > 0;
12009
+ if (terminal) return isQuestion ? /* @__PURE__ */ jsx(TerminalQuestionPrompt, {
12010
+ request,
12011
+ onAnswer: approve,
12012
+ onDismiss: (id) => deny(id, "Question dismissed by user")
12013
+ }, request.id) : /* @__PURE__ */ jsx(TerminalPermissionPrompt, {
12014
+ request,
12015
+ onApprove: approve,
12016
+ onDeny: deny
12017
+ }, request.id);
12018
+ return isQuestion ? /* @__PURE__ */ jsx(QuestionPrompt, {
12019
+ request,
12020
+ onAnswer: approve,
12021
+ onDismiss: (id) => deny(id, "Question dismissed by user")
12022
+ }, request.id) : /* @__PURE__ */ jsx(PermissionPrompt, {
12023
+ request,
12024
+ onApprove: approve,
12025
+ onDeny: deny
12026
+ }, request.id);
12027
+ })
12028
+ })
12029
+ }) : null,
12030
+ readOnly || subagentId !== void 0 ? null : /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(Composer, {
12031
+ ref: composerRef,
12032
+ onSend: handleSend,
12033
+ onInterrupt: interrupt,
12034
+ busy,
12035
+ disabled: ended || !sessionId,
12036
+ commands: capabilities.slashCommands ? state.commands : void 0,
12037
+ skills: capabilities.skillsList ? state.skills : void 0,
12038
+ clientCommands,
12039
+ attachments,
12040
+ draft,
12041
+ onSearchFiles: hostFiles.available ? searchComposerFiles : void 0,
12042
+ onShellCommand: shell ? runShell : void 0,
12043
+ layout: controlsExternal ? "inline" : "stacked",
12044
+ toolbar: controlsExternal ? void 0 : sessionControls,
12045
+ fontSize: effectiveTermFontSize,
12046
+ lineHeight: effectiveTermLineHeight,
12047
+ affordances
12048
+ }) }),
12049
+ statusPlacement === "bottom" ? statusBar : null,
12050
+ !external ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
12051
+ /* @__PURE__ */ jsx(SessionInfoDialog, {
12052
+ state,
12053
+ client,
12054
+ sessionId,
12055
+ open: panel === "info",
12056
+ onOpenChange: (next) => setPanel(next ? "info" : void 0)
12057
+ }),
12058
+ /* @__PURE__ */ jsx(ContextDialog, {
12059
+ usage: state.contextUsage,
12060
+ engine: state.engine ?? "claude",
12061
+ open: panel === "context",
12062
+ onOpenChange: (next) => setPanel(next ? "context" : void 0)
12063
+ }),
12064
+ /* @__PURE__ */ jsx(UsageDialog, {
12065
+ rateLimits: windows,
12066
+ subscriptionType: state.subscriptionType,
12067
+ engine: state.engine ?? "claude",
12068
+ totalCostUsd: state.totalCostUsd,
12069
+ costUsd: state.costUsd,
12070
+ usageByModel: state.usageByModel,
12071
+ spend: profileSpend,
12072
+ updatedAt: usageUpdatedAt,
12073
+ open: panel === "usage",
12074
+ onOpenChange: (next) => setPanel(next ? "usage" : void 0)
12075
+ }),
12076
+ /* @__PURE__ */ jsx(McpDialog, {
12077
+ client,
12078
+ sessionId,
12079
+ canManageServers: capabilities.mcpServerActions,
12080
+ open: panel === "mcp",
12081
+ onOpenChange: (next) => setPanel(next ? "mcp" : void 0)
12082
+ }),
12083
+ /* @__PURE__ */ jsx(TasksDialog, {
12084
+ tasks,
12085
+ showCompleted: showCompletedTasks,
12086
+ onShowCompletedChange: setShowCompletedTasks,
12087
+ onSelectTask: (task) => {
12088
+ if (task.toolUseId) setTaskReveal({
12089
+ toolUseId: task.toolUseId,
12090
+ nonce: Date.now()
12091
+ });
12092
+ },
12093
+ open: panel === "tasks",
12094
+ onOpenChange: (next) => setPanel(next ? "tasks" : void 0)
12095
+ }),
12096
+ /* @__PURE__ */ jsx(SkillsDialog, {
12097
+ skills: state.skills,
12098
+ open: panel === "skills",
12099
+ onOpenChange: (next) => setPanel(next ? "skills" : void 0),
12100
+ onUse: (skill) => composerRef.current?.insertText(skillPrompt(skill))
12101
+ }),
12102
+ /* @__PURE__ */ jsx(HostFilesDialog, {
12103
+ client,
12104
+ cwd: state.cwd,
12105
+ open: panel === "files",
12106
+ onOpenChange: (next) => setPanel(next ? "files" : void 0)
12107
+ })
12108
+ ] }) : null
12109
+ ]
12110
+ })
11630
12111
  })
11631
12112
  })
11632
12113
  })
@@ -11696,6 +12177,108 @@ function Notice({ level, onDismiss, children }) {
11696
12177
  });
11697
12178
  }
11698
12179
  //#endregion
11699
- export { Ink as $, PERMISSION_MODES as A, DialogContent as At, TaskList as B, SelectContent as Bt, TerminalPermissionPrompt as C, copyText as Ct, SubagentStrip as D, Dialog$1 as Dt, parseUserQuestions as E, TooltipProvider as Et, SkillsDialog as F, MenuContent as Ft, TerminalSurface as G, PortalScope as Gt, contextNote as H, SelectItemText as Ht, McpDialog as I, MenuItem as It, TerminalMarkdown as J, Badge as Jt, ToolTitleProvider as K, Textarea as Kt, HostFilesDialog as L, MenuSeparator as Lt, permissionModeChoices as M, DialogRow as Mt, permissionModeMeta as N, DialogTrigger as Nt, PermissionPrompt as O, DialogBody as Ot, ModelSelect as P, Menu$1 as Pt, Blank as Q, cn as Qt, EntryIcon as R, MenuTrigger as Rt, TerminalQuestionPrompt as S, CopyButton as St, QuestionPrompt as T, TooltipContent as Tt, Composer as U, SelectTrigger as Ut, ContextDialog as V, SelectItem as Vt, skillPrompt as W, SelectValue as Wt, previewPatch as X, Button as Xt, TerminalDiff as Y, badgeVariants as Yt, Band as Z, buttonVariants as Zt, ConversationContent as _, segmentsToPlainText as _t, Transcript as a, toolIcon as at, STATUS_META as b, CodeBlock as bt, PromptTokenText as c, useTranscriptDensity as ct, FileCard as d, hashtagTrigger as dt, Row as et, TerminalItemView as f, launchTrigger as ft, Conversation as g, plainTextToSegments as gt, Loader as h, PromptArea as ht, useMinuteClock as i, isMutatingTool as it, PermissionModeSelect as j, DialogHeader as jt, Response as k, DialogClose as kt, Message as l, useTranscriptVariant as lt, SessionEmptyState as m, usePromptAreaState as mt, UsageDialog as n, WithActions as nt, ToolCallCard as o, TranscriptDensityProvider as ot, TerminalTranscript as p, mentionTrigger as pt, useToolTitle as q, Input as qt, UsageMeters as r, useAffordances as rt, Reasoning as s, TranscriptVariantProvider as st, SessionPanel as t, CopyAction as tt, MessageContent as u, commandTrigger as ut, ConversationScrollButton as v, ProgressRing as vt, QUESTION_BEHAVIORS as w, Tip as wt, SessionInfoDialog as x, Spinner as xt, StatusBar as y, Splitter as yt, TasksDialog as z, Select$1 as zt };
12180
+ //#region src/lib/path-match.ts
12181
+ const SEG = "[^\\s:'\"`()\\[\\]{}<>|]+";
12182
+ const PATH_PATTERN = new RegExp(`(?:^|[\\s'"\`([{<@,])((?:\\.\\.?)?(?:/${SEG})+|[^\\s:'"\`()\\[\\]{}<>|@,]+(?:/${SEG})+)(?::(\\d+))?`);
12183
+ const FILE_PATTERN = /^([\w.@-]+\.[A-Za-z0-9]{1,10})(?::(\d+))?$/;
12184
+ const TRAILING = /[.,;:!?)\]}'"`]+$/;
12185
+ const FILENAME = /\.[A-Za-z0-9]{1,10}$/;
12186
+ function matchPath(text, inCode = false) {
12187
+ const source = text ?? "";
12188
+ const match = PATH_PATTERN.exec(source);
12189
+ if (!match) return inCode ? matchFilename(source) : void 0;
12190
+ const path = match[1].replace(TRAILING, "");
12191
+ if (!path.includes("/") || path.endsWith("/")) return;
12192
+ if (!(path.startsWith("/") || path.startsWith("./") || path.startsWith("../")) && !FILENAME.test(path)) return;
12193
+ return {
12194
+ path,
12195
+ line: match[2] ? Number(match[2]) : void 0,
12196
+ length: path.length + (match[2] ? match[2].length + 1 : 0)
12197
+ };
12198
+ }
12199
+ function matchFilename(text) {
12200
+ const hit = FILE_PATTERN.exec(text.trim());
12201
+ if (!hit) return;
12202
+ const line = hit[2] ? Number(hit[2]) : void 0;
12203
+ return {
12204
+ path: hit[1],
12205
+ line,
12206
+ length: hit[0].length
12207
+ };
12208
+ }
12209
+ //#endregion
12210
+ //#region src/components/agent/use-path-links.ts
12211
+ const LINKISH = "wd-path-link";
12212
+ const COVERAGE = .6;
12213
+ function hitFor(element) {
12214
+ if (!element) return;
12215
+ const inCode = element.closest("code") !== null;
12216
+ const text = element.textContent?.trim() ?? "";
12217
+ const hit = matchPath(text, inCode);
12218
+ if (!hit) return;
12219
+ if (!inCode && hit.length < text.length * COVERAGE) return;
12220
+ return {
12221
+ path: hit.path,
12222
+ line: hit.line
12223
+ };
12224
+ }
12225
+ function usePathLinks({ container, onOpen, enabled = true, ignore }) {
12226
+ useEffect(() => {
12227
+ const root = container.current;
12228
+ if (!root || !enabled) return;
12229
+ const excluded = (element) => ignore !== void 0 && element?.closest(ignore) !== null && element !== void 0;
12230
+ let marked;
12231
+ const unmark = () => {
12232
+ marked?.classList.remove(LINKISH);
12233
+ marked = void 0;
12234
+ };
12235
+ const onClick = (event) => {
12236
+ if (!event.metaKey && !event.ctrlKey) return;
12237
+ const target = event.target instanceof HTMLElement ? event.target : void 0;
12238
+ if (excluded(target)) return;
12239
+ const hit = hitFor(target);
12240
+ if (!hit) return;
12241
+ event.preventDefault();
12242
+ event.stopPropagation();
12243
+ unmark();
12244
+ onOpen(hit);
12245
+ };
12246
+ const onMove = (event) => {
12247
+ if (!event.metaKey && !event.ctrlKey) return unmark();
12248
+ const element = event.target instanceof HTMLElement ? event.target : void 0;
12249
+ if (element === marked) return;
12250
+ unmark();
12251
+ if (!element || excluded(element) || !hitFor(element)) return;
12252
+ marked = element;
12253
+ element.classList.add(LINKISH);
12254
+ };
12255
+ const onKey = (event) => {
12256
+ if (!event.metaKey && !event.ctrlKey) unmark();
12257
+ };
12258
+ root.addEventListener("click", onClick, true);
12259
+ root.addEventListener("mousemove", onMove);
12260
+ document.addEventListener("keydown", onKey);
12261
+ document.addEventListener("keyup", onKey);
12262
+ window.addEventListener("blur", unmark);
12263
+ return () => {
12264
+ unmark();
12265
+ root.removeEventListener("click", onClick, true);
12266
+ root.removeEventListener("mousemove", onMove);
12267
+ document.removeEventListener("keydown", onKey);
12268
+ document.removeEventListener("keyup", onKey);
12269
+ window.removeEventListener("blur", unmark);
12270
+ };
12271
+ }, [
12272
+ container,
12273
+ onOpen,
12274
+ enabled,
12275
+ ignore
12276
+ ]);
12277
+ }
12278
+ function resolveAgainstCwd(path, cwd) {
12279
+ return parseFileLink(path, cwd)?.path ?? path;
12280
+ }
12281
+ //#endregion
12282
+ export { TerminalDiff as $, SelectValue as $t, PermissionPrompt as A, Spinner as At, EntryIcon as B, DialogHeader as Bt, SessionInfoDialog as C, usePromptAreaState as Ct, QuestionPrompt as D, ProgressRing as Dt, QUESTION_BEHAVIORS as E, segmentsToPlainText as Et, permissionModeMeta as F, TooltipProvider as Ft, Composer as G, MenuItem as Gt, TaskList as H, DialogTrigger as Ht, ModelSelect as I, Dialog$1 as It, useToolTitle as J, Select$1 as Jt, TerminalSurface as K, MenuSeparator as Kt, SkillsDialog as L, DialogBody as Lt, PERMISSION_MODES as M, copyText as Mt, PermissionModeSelect as N, Tip as Nt, parseUserQuestions as O, Splitter as Ot, permissionModeChoices as P, TooltipContent as Pt, parseFileLink as Q, SelectTrigger as Qt, McpDialog as R, DialogClose as Rt, STATUS_META as S, mentionTrigger as St, TerminalPermissionPrompt as T, plainTextToSegments as Tt, ContextDialog as U, Menu$1 as Ut, TasksDialog as V, DialogRow as Vt, contextNote as W, MenuContent as Wt, FileLinkProvider as X, SelectItem as Xt, TerminalMarkdown as Y, SelectContent as Yt, useFileLinks as Z, SelectItemText as Zt, Loader as _, rankComposerRows as _t, UsageDialog as a, Button as an, CopyAction as at, ConversationScrollButton as b, hashtagTrigger as bt, Transcript as c, PromptTokenText as ct, Message as d, TranscriptDensityProvider as dt, PortalScope as en, previewPatch as et, MessageContent as f, TranscriptVariantProvider as ft, SessionEmptyState as g, mergeComposerRows as gt, TerminalTranscript as h, matchClientCommand as ht, SessionPanel as i, badgeVariants as in, Row as it, Response as j, CopyButton as jt, SubagentStrip as k, CodeBlock as kt, ToolCallCard as l, isMutatingTool as lt, TerminalItemView as m, useTranscriptVariant as mt, usePathLinks as n, Input as nn, Blank as nt, UsageMeters as o, buttonVariants as on, WithActions as ot, FileCard as p, useTranscriptDensity as pt, ToolTitleProvider as q, MenuTrigger as qt, matchPath as r, Badge as rn, Ink as rt, useMinuteClock as s, cn as sn, useAffordances as st, resolveAgainstCwd as t, Textarea as tn, Band as tt, Reasoning as u, toolIcon as ut, Conversation as v, skillPrompt as vt, TerminalQuestionPrompt as w, PromptArea as wt, StatusBar as x, launchTrigger as xt, ConversationContent as y, commandTrigger as yt, HostFilesDialog as z, DialogContent as zt };
11700
12283
 
11701
- //# sourceMappingURL=SessionPanel-Dg4xf-ql.mjs.map
12284
+ //# sourceMappingURL=use-path-links-CvcxdUXL.mjs.map