@xenosystem/agent-conversation 0.1.35 → 0.1.37

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 (86) hide show
  1. package/dist/adapters/xenoAgentHostCatalog.d.ts.map +1 -1
  2. package/dist/adapters/xenoAgentHostCatalog.js +20 -5
  3. package/dist/adapters/xenoAgentHostCatalog.js.map +1 -1
  4. package/dist/components/agent/AgentView.d.ts +9 -0
  5. package/dist/components/agent/AgentView.d.ts.map +1 -1
  6. package/dist/components/agent/AgentView.js +883 -1045
  7. package/dist/components/agent/AgentView.js.map +1 -1
  8. package/dist/components/agent/GhostTextArea.d.ts +6 -0
  9. package/dist/components/agent/GhostTextArea.d.ts.map +1 -1
  10. package/dist/components/agent/GhostTextArea.js +15 -5
  11. package/dist/components/agent/GhostTextArea.js.map +1 -1
  12. package/dist/components/agent/LiveConversationCollaboration.d.ts +3 -1
  13. package/dist/components/agent/LiveConversationCollaboration.d.ts.map +1 -1
  14. package/dist/components/agent/LiveConversationCollaboration.js +3 -1
  15. package/dist/components/agent/LiveConversationCollaboration.js.map +1 -1
  16. package/dist/components/agent/MessageText.d.ts +36 -0
  17. package/dist/components/agent/MessageText.d.ts.map +1 -0
  18. package/dist/components/agent/MessageText.js +76 -0
  19. package/dist/components/agent/MessageText.js.map +1 -0
  20. package/dist/components/agent/SdkSessionPanel.d.ts +2 -0
  21. package/dist/components/agent/SdkSessionPanel.d.ts.map +1 -1
  22. package/dist/components/agent/SdkSessionPanel.js +1 -1
  23. package/dist/components/agent/SdkSessionPanel.js.map +1 -1
  24. package/dist/components/agent/composer/Composer.d.ts +282 -0
  25. package/dist/components/agent/composer/Composer.d.ts.map +1 -0
  26. package/dist/components/agent/composer/Composer.js +393 -0
  27. package/dist/components/agent/composer/Composer.js.map +1 -0
  28. package/dist/components/agent/composer/EffortCells.d.ts +12 -0
  29. package/dist/components/agent/composer/EffortCells.d.ts.map +1 -0
  30. package/dist/components/agent/composer/EffortCells.js +146 -0
  31. package/dist/components/agent/composer/EffortCells.js.map +1 -0
  32. package/dist/components/agent/composer/Tip.d.ts +19 -0
  33. package/dist/components/agent/composer/Tip.d.ts.map +1 -0
  34. package/dist/components/agent/composer/Tip.js +78 -0
  35. package/dist/components/agent/composer/Tip.js.map +1 -0
  36. package/dist/components/agent/composer/TypedMenu.d.ts +56 -0
  37. package/dist/components/agent/composer/TypedMenu.d.ts.map +1 -0
  38. package/dist/components/agent/composer/TypedMenu.js +156 -0
  39. package/dist/components/agent/composer/TypedMenu.js.map +1 -0
  40. package/dist/components/agent/mainAssistant.d.ts +9 -2
  41. package/dist/components/agent/mainAssistant.d.ts.map +1 -1
  42. package/dist/components/agent/mainAssistant.js +9 -2
  43. package/dist/components/agent/mainAssistant.js.map +1 -1
  44. package/dist/components/agent/toolCallDerive.d.ts +82 -0
  45. package/dist/components/agent/toolCallDerive.d.ts.map +1 -0
  46. package/dist/components/agent/toolCallDerive.js +741 -0
  47. package/dist/components/agent/toolCallDerive.js.map +1 -0
  48. package/dist/components/agent/transcript/Transcript.d.ts +127 -0
  49. package/dist/components/agent/transcript/Transcript.d.ts.map +1 -0
  50. package/dist/components/agent/transcript/Transcript.js +544 -0
  51. package/dist/components/agent/transcript/Transcript.js.map +1 -0
  52. package/dist/components/agent/transcript/glyphs.d.ts +83 -0
  53. package/dist/components/agent/transcript/glyphs.d.ts.map +1 -0
  54. package/dist/components/agent/transcript/glyphs.js +124 -0
  55. package/dist/components/agent/transcript/glyphs.js.map +1 -0
  56. package/dist/components/agent/transcript/stepModel.d.ts +163 -0
  57. package/dist/components/agent/transcript/stepModel.d.ts.map +1 -0
  58. package/dist/components/agent/transcript/stepModel.js +701 -0
  59. package/dist/components/agent/transcript/stepModel.js.map +1 -0
  60. package/dist/components/agent/transcript/turnModel.d.ts +124 -0
  61. package/dist/components/agent/transcript/turnModel.d.ts.map +1 -0
  62. package/dist/components/agent/transcript/turnModel.js +166 -0
  63. package/dist/components/agent/transcript/turnModel.js.map +1 -0
  64. package/dist/components/agent/transcript/useAskActions.d.ts +23 -0
  65. package/dist/components/agent/transcript/useAskActions.d.ts.map +1 -0
  66. package/dist/components/agent/transcript/useAskActions.js +92 -0
  67. package/dist/components/agent/transcript/useAskActions.js.map +1 -0
  68. package/dist/parityFixture.d.ts +54 -0
  69. package/dist/parityFixture.d.ts.map +1 -0
  70. package/dist/parityFixture.js +195 -0
  71. package/dist/parityFixture.js.map +1 -0
  72. package/dist/platformApiTypes.d.ts +10 -1
  73. package/dist/platformApiTypes.d.ts.map +1 -1
  74. package/dist/stores/agentChatStore.d.ts +61 -1
  75. package/dist/stores/agentChatStore.d.ts.map +1 -1
  76. package/dist/stores/agentChatStore.js +63 -8
  77. package/dist/stores/agentChatStore.js.map +1 -1
  78. package/dist/types/agentPermissions.d.ts +3 -1
  79. package/dist/types/agentPermissions.d.ts.map +1 -1
  80. package/dist/utils/acpExecutionAuthority.d.ts.map +1 -1
  81. package/dist/utils/acpExecutionAuthority.js +2 -0
  82. package/dist/utils/acpExecutionAuthority.js.map +1 -1
  83. package/package.json +6 -5
  84. package/src/composer.css +334 -0
  85. package/src/styles.css +7 -0
  86. package/src/transcript.css +585 -0
@@ -30,6 +30,11 @@ import { AgentNotificationBanner } from './AgentNotificationBanner.js';
30
30
  import { SlashCommandAutocomplete } from './SlashCommandAutocomplete.js';
31
31
  import { AssistantTurnErrorNotice, AssistantTurnStatusNotice } from './AssistantTurnStatusNotice.js';
32
32
  import { commandPreviewText, compactCommandPreview, deriveToolInteractionState, hideDuplicateToolTitle, isCommandToolCall } from './toolCardDisplay.js';
33
+ import { deriveToolCallFilePath, humanizeToolName, isToolDisplayNoise, deriveToolCardDisplay, summarizeCommand, parseStructuredCommandContent, formatToolDuration, countTextLines, deriveEditorDiffStats, deriveToolDiffStats, deriveToolCallFileName, deriveFileToolPrimaryText, deriveFileToolActionLabel, buildInlineDiffPreviewLines, mapWorkspaceChoice, deriveTerminalToolOutput, readToolStringParam, isPseudoPermissionDirectory, deriveShellCommandNames, deriveApprovalCardInfo, toolCallLabel, buildSurfaceApprovalRule, canBuildSurfaceApprovalRule, } from './toolCallDerive.js';
34
+ import { AgentsTray, BackgroundTray, Glyph, TasksTray, TranscriptTurn } from './transcript/Transcript.js';
35
+ import { CtxAction, CtxDiff, CtxGrow, CtxMeter, CtxSeg, CtxSplit, CtxStrip, CtxVr, EffortCells, LaneGroup, ModeSegment, PermChip, ComposerDock, DockIcon, Notice, SendSquare, Tiles, Tip, TurnStatus, TypedMenu, XaMenu, XaMenuSearch, XaRow, postureOf, tokenAt, } from './composer/Composer.js';
36
+ import { useAskActions } from './transcript/useAskActions.js';
37
+ import { deriveStepRow } from './transcript/stepModel.js';
33
38
  import { deriveWebJobProgressView, webJobStopAvailability, WEB_JOB_STOP_LABEL, WEB_JOB_STOP_TITLE } from './webJobProgressView.js';
34
39
  import { cloudModelName, cloudModelSortScore } from '../../utils/cloudModels.js';
35
40
  import { isSelectableLiveModel } from '../../utils/modelAvailability.js';
@@ -44,8 +49,21 @@ import { extractUserFileCreateRequest, extractUserFileEditRequest, isAbsoluteOrR
44
49
  import { buildRuntimeAttachedContextPrompt } from '../../utils/agentAttachedContextPrompt.js';
45
50
  import { buildWorkspaceSelectionContinuationPrompt } from '../../utils/workspaceSelectionContinuation.js';
46
51
  import { isAgentComposerInputLocked } from './nativeTuiComposerInput.js';
52
+ import { MessageCopyButton, MessageText } from './MessageText.js';
47
53
  import { AgentProviderSelectorController } from '../../providerSelector.js';
48
54
  import { loadHubAgentProviderCatalog, refreshHubAgentProviderCatalog, subscribeHubAgentProviderCatalog, } from '../../adapters/xenoAgentHostCatalog.js';
55
+ /**
56
+ * The seed used before any catalog has loaded — and NOT a claim that this model exists.
57
+ *
58
+ * ⚠️ It is a retired id: measured 2026-09-13 the live platform serves `gpt-5.6-*`, and this
59
+ * `gpt-5.5-high` is served by nothing. That is survivable only because the live catalog's own
60
+ * default replaces it the moment one arrives (see the reconciliation beside
61
+ * `cloudModelSelectionMissing`). Before that reconciliation existed, this constant was why a
62
+ * signed-in person with ten working models was told "Cloud is unavailable · Choose a model".
63
+ *
64
+ * 🔴 So do not treat a hardcoded model id as a default. Upstream renames models, and every
65
+ * hardcoded id here is a future outage that presents as a broken lane rather than a stale string.
66
+ */
49
67
  const AGENT_GPT55_DEFAULT_MODEL = 'gpt-5.5-high';
50
68
  const AGENT_MODEL_MODE_SWITCH_ENABLED = true;
51
69
  const isAgentCloudModel = (id) => !!id && !id.startsWith('local:');
@@ -166,46 +184,6 @@ function selectAllowedDirectoryForPath(filePath, allowedDirectories, fallback) {
166
184
  });
167
185
  return matchingRoot || fallback || allowedDirectories[0] || '';
168
186
  }
169
- function countNormalizedTextLines(content) {
170
- const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
171
- if (!normalized.length)
172
- return 0;
173
- return normalized.replace(/\n$/, '').split('\n').length;
174
- }
175
- function normalizeEditorCandidatePath(raw) {
176
- return raw
177
- .trim()
178
- .replace(/^["'`]+|["'`]+$/g, '')
179
- .replace(/[)\].,;:]+$/g, '');
180
- }
181
- function extractPathsFromResultText(text) {
182
- return extractWindowsFilePathsFromToolResult(text)
183
- .map(normalizeEditorCandidatePath)
184
- .filter(Boolean);
185
- }
186
- function deriveToolCallFilePath(toolCall) {
187
- if (toolCall.diff?.path)
188
- return normalizeEditorCandidatePath(toolCall.diff.path);
189
- const artifactPath = toolCall.artifacts?.find((artifact) => artifact.type === 'file' && artifact.openInEditor)?.value;
190
- if (artifactPath)
191
- return normalizeEditorCandidatePath(artifactPath);
192
- const params = toolCall.params || {};
193
- const paramCandidates = [
194
- params.path,
195
- params.file_path,
196
- params.targetPath,
197
- params.resolvedTargetPath,
198
- ];
199
- for (const candidate of paramCandidates) {
200
- if (typeof candidate === 'string') {
201
- const normalized = normalizeEditorCandidatePath(candidate);
202
- if (/^[A-Za-z]:\\/.test(normalized))
203
- return normalized;
204
- }
205
- }
206
- const resultCandidates = extractPathsFromResultText(toolCall.result || '');
207
- return resultCandidates[0] || null;
208
- }
209
187
  function collectMessageFileReferences(toolCalls) {
210
188
  if (!toolCalls || toolCalls.length === 0)
211
189
  return [];
@@ -658,146 +636,6 @@ function computeMsgParts(msg) {
658
636
  }
659
637
  return withTurnStatus(parts);
660
638
  }
661
- function humanizeToolName(toolName) {
662
- return toolName
663
- .replace(/_/g, ' ')
664
- .replace(/([a-z])([A-Z])/g, '$1 $2')
665
- .replace(/\b\w/g, (char) => char.toUpperCase());
666
- }
667
- function stripToolDisplayPrefix(value) {
668
- return value
669
- .replace(/^(Selected folder:|Selecting folder:?|Workspace placement:|Running:|Searching:|Reading:|Writing:|Editing:)\s*/i, '')
670
- .trim();
671
- }
672
- function isToolDisplayNoise(value) {
673
- const trimmed = (value || '').trim();
674
- if (!trimmed)
675
- return true;
676
- if (/^[{}\[\],:]+$/.test(trimmed))
677
- return true;
678
- if (trimmed === '{}' || trimmed === '[]' || trimmed === 'null')
679
- return true;
680
- try {
681
- const parsed = JSON.parse(trimmed);
682
- if (parsed === null)
683
- return true;
684
- if (Array.isArray(parsed))
685
- return parsed.length === 0;
686
- if (typeof parsed === 'object')
687
- return Object.keys(parsed).length === 0;
688
- }
689
- catch {
690
- // Not JSON, so only the punctuation-only check above applies.
691
- }
692
- return false;
693
- }
694
- function deriveToolCardDisplay(toolCall) {
695
- const params = toolCall.params || {};
696
- const normalizedToolName = toolCall.toolName.toLowerCase();
697
- const fallbackLabel = isToolDisplayNoise(toolCall.summary)
698
- ? stripToolDisplayPrefix(toolCallLabel(toolCall.toolName, params))
699
- : stripToolDisplayPrefix(toolCall.summary || '');
700
- const pr = toolCall.permissionRequest;
701
- const fileName = deriveToolCallFileName(toolCall);
702
- const fileActionLabel = deriveFileToolActionLabel(toolCall);
703
- const filePrimaryText = deriveFileToolPrimaryText(toolCall);
704
- const query = typeof params.query === 'string' ? params.query.trim() : '';
705
- const app = typeof params.app === 'string'
706
- ? params.app.trim()
707
- : (typeof params.appPath === 'string' ? params.appPath.trim() : '');
708
- const directoryArtifact = toolCall.artifacts?.find((artifact) => artifact.type === 'directory')?.value;
709
- const directory = typeof params.directory === 'string' && params.directory.trim()
710
- ? params.directory.trim()
711
- : directoryArtifact;
712
- const choice = typeof params.choice === 'string' ? mapWorkspaceChoice(params.choice.trim()) : '';
713
- const command = typeof params.command === 'string' ? params.command.trim() : '';
714
- if (pr?.status === 'pending') {
715
- return hideDuplicateToolTitle({
716
- eyebrow: 'Approval required',
717
- title: normalizedToolName === 'choosedirectory'
718
- ? 'Open folder picker'
719
- : (pr.directory || pr.path || fallbackLabel || humanizeToolName(toolCall.toolName)),
720
- });
721
- }
722
- if (fileActionLabel) {
723
- return hideDuplicateToolTitle({
724
- eyebrow: filePrimaryText || `${fileActionLabel} file`,
725
- title: `${fileActionLabel} ${fileName}`,
726
- });
727
- }
728
- if (isCommandToolCall(toolCall)) {
729
- return hideDuplicateToolTitle({
730
- eyebrow: toolCall.status === 'running' ? 'Running command' : 'Command',
731
- title: command ? compactCommandPreview(command) : 'Terminal command',
732
- });
733
- }
734
- switch (normalizedToolName) {
735
- case 'choosedirectory':
736
- return hideDuplicateToolTitle({
737
- eyebrow: toolCall.status === 'running' ? 'Selecting folder' : 'Selected folder',
738
- title: toolCall.status === 'running'
739
- ? (directory || 'Selecting folder')
740
- : (directory || fallbackLabel || 'Choose a folder'),
741
- });
742
- case 'workspaceselection':
743
- return hideDuplicateToolTitle({
744
- eyebrow: 'Workspace action',
745
- title: choice || fallbackLabel || 'Update workspace placement',
746
- });
747
- case 'chooseworkspaceplacement':
748
- return {
749
- eyebrow: toolCall.status === 'running' ? 'Choose workspace placement' : 'Workspace placement',
750
- title: '',
751
- };
752
- case 'xenoworkspacemanage':
753
- return hideDuplicateToolTitle({
754
- eyebrow: 'Workspace registry',
755
- title: fallbackLabel || 'Manage workspace hierarchy',
756
- });
757
- case 'glob':
758
- return hideDuplicateToolTitle({
759
- eyebrow: toolCall.status === 'running' ? 'Finding files' : 'Glob',
760
- title: '',
761
- });
762
- case 'terminallist':
763
- return hideDuplicateToolTitle({
764
- eyebrow: toolCall.status === 'running' ? 'Terminal command' : 'Terminal command',
765
- title: '',
766
- });
767
- case 'terminalreadoutput':
768
- return hideDuplicateToolTitle({
769
- eyebrow: toolCall.status === 'running' ? 'Terminal command' : 'Terminal command',
770
- title: '',
771
- });
772
- case 'terminalwriteinput':
773
- return hideDuplicateToolTitle({
774
- eyebrow: 'Terminal command',
775
- title: '',
776
- });
777
- case 'askuser':
778
- return hideDuplicateToolTitle({
779
- eyebrow: 'User input',
780
- title: '',
781
- });
782
- case 'websearch':
783
- case 'web_search':
784
- return hideDuplicateToolTitle({
785
- eyebrow: toolCall.status === 'running' ? 'Searching web' : 'Web search',
786
- title: query || fallbackLabel || 'Search query',
787
- });
788
- case 'openapp':
789
- case 'open_app':
790
- return hideDuplicateToolTitle({
791
- eyebrow: toolCall.status === 'running' ? 'Opening app' : 'Opened app',
792
- title: app || fallbackLabel || 'App launch',
793
- });
794
- default:
795
- return hideDuplicateToolTitle({
796
- eyebrow: humanizeToolName(toolCall.toolName),
797
- title: fallbackLabel || humanizeToolName(toolCall.toolName),
798
- });
799
- }
800
- }
801
639
  function assistantLaneMarkerKind(part) {
802
640
  switch (part.kind) {
803
641
  case 'tool-calls':
@@ -1012,36 +850,6 @@ function parseWebSearchResultBlocks(content) {
1012
850
  }
1013
851
  return resultBlocks;
1014
852
  }
1015
- function summarizeCommand(cmd) {
1016
- const lower = cmd.toLowerCase();
1017
- if (lower.startsWith('web_search:')) {
1018
- const query = cmd.slice(11).trim();
1019
- return { icon: 'search', label: `Searched web: ${query}`, detail: `Web search for "${query}"` };
1020
- }
1021
- if (lower.includes('select-string') || lower.includes('grep') || lower.includes('findstr')) {
1022
- const patternMatch = cmd.match(/[-"]Pattern["']?\s+["']?([^"'\s]+)/i) || cmd.match(/Select-String.*["']([^"']+)["']/i);
1023
- const pathMatch = cmd.match(/-Path\s+["']?([^"'\s|]+)/i);
1024
- const caseSensitive = lower.includes('casesensitive:$false') ? 'Case insensitive' : '';
1025
- const detail = `Searched with query ${patternMatch?.[1] || 'pattern'}${pathMatch ? ` in ${pathMatch[1]}` : ''}${caseSensitive ? `\n${caseSensitive}` : ''}`;
1026
- return { icon: 'search', label: `Searched ${patternMatch?.[1] || 'pattern'}`, detail };
1027
- }
1028
- if (lower.includes('get-childitem') || lower.includes('ls') || lower.includes('dir')) {
1029
- const pathMatch = cmd.match(/-Path\s+["']?([^"'\s|]+)/i) || cmd.match(/(?:ls|dir)\s+["']?([^"'\s|]+)/i);
1030
- const path = pathMatch?.[1] || '';
1031
- const folder = path.split(/[/\\]/).filter(Boolean).pop() || path;
1032
- const flags = [lower.includes('-recurse') && 'Recursive', lower.includes('-force') && 'Including hidden'].filter(Boolean).join(', ');
1033
- const detail = `Listed directory ${path}${flags ? `\n${flags}` : ''}`;
1034
- return { icon: 'file', label: `Listed ${folder || 'directory'}`, detail };
1035
- }
1036
- if (lower.includes('get-content') || lower.includes('cat') || lower.includes('type')) {
1037
- const pathMatch = cmd.match(/-Path\s+["']?([^"'\s|]+)/i) || cmd.match(/(?:cat|type)\s+["']?([^"'\s|]+)/i);
1038
- const path = pathMatch?.[1] || '';
1039
- const file = path.split(/[/\\]/).pop() || 'file';
1040
- return { icon: 'file', label: `Read ${file}`, detail: `Read file ${path}` };
1041
- }
1042
- const short = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
1043
- return { icon: 'terminal', label: short, detail: cmd };
1044
- }
1045
853
  function countResults(content) {
1046
854
  const lines = content.trim().split('\n').filter(l => l.trim());
1047
855
  if (lines.length === 0)
@@ -1050,67 +858,6 @@ function countResults(content) {
1050
858
  return 'error';
1051
859
  return `${lines.length} result${lines.length === 1 ? '' : 's'}`;
1052
860
  }
1053
- function parseStructuredCommandContent(content) {
1054
- const raw = content.replace(/^```\n?/gm, '').replace(/```$/gm, '').trim();
1055
- if (!raw) {
1056
- return {
1057
- command: '',
1058
- cwd: '',
1059
- stdout: '',
1060
- stderr: '',
1061
- error: '',
1062
- exit: '',
1063
- outputLineCount: 0,
1064
- };
1065
- }
1066
- const lines = raw.split('\n');
1067
- let command = '';
1068
- let cwd = '';
1069
- let error = '';
1070
- let exit = '';
1071
- let inStderr = false;
1072
- const stdoutLines = [];
1073
- const stderrLines = [];
1074
- for (const line of lines) {
1075
- if (!command && line.startsWith('$ ')) {
1076
- command = line.slice(2).trim();
1077
- continue;
1078
- }
1079
- if (!cwd && line.startsWith('cwd: ')) {
1080
- cwd = line.slice(5).trim();
1081
- continue;
1082
- }
1083
- if (line === '[stderr]') {
1084
- inStderr = true;
1085
- continue;
1086
- }
1087
- if (!error && line.startsWith('[error]')) {
1088
- error = line.replace(/^\[error\]\s*/i, '').trim();
1089
- continue;
1090
- }
1091
- if (!exit && line.startsWith('exit: ')) {
1092
- exit = line.replace(/^exit:\s*/i, 'exit ').trim();
1093
- continue;
1094
- }
1095
- if (inStderr) {
1096
- stderrLines.push(line);
1097
- }
1098
- else {
1099
- stdoutLines.push(line);
1100
- }
1101
- }
1102
- const stdout = stdoutLines.join('\n').trim();
1103
- const stderr = stderrLines.join('\n').trim();
1104
- return {
1105
- command,
1106
- cwd,
1107
- stdout,
1108
- stderr,
1109
- error,
1110
- exit,
1111
- outputLineCount: stdout ? stdout.split(/\r?\n/).length : 0,
1112
- };
1113
- }
1114
861
  function CommandResultBlock({ command, content, isError }) {
1115
862
  const [isOpen, setIsOpen] = useState(false);
1116
863
  const summary = summarizeCommand(command);
@@ -1163,13 +910,6 @@ function CommandResultBlock({ command, content, isError }) {
1163
910
  })()] }));
1164
911
  }
1165
912
  /** Renders tool calls from the agentic loop (function calling) */
1166
- function formatToolDuration(durationMs) {
1167
- if (typeof durationMs !== 'number' || Number.isNaN(durationMs))
1168
- return null;
1169
- if (durationMs < 1000)
1170
- return null;
1171
- return `${(durationMs / 1000).toFixed(durationMs >= 10_000 ? 0 : 1)}s`;
1172
- }
1173
913
  function normalizeToolDisplayText(value) {
1174
914
  return (value || '')
1175
915
  .replace(/\s+/g, ' ')
@@ -1181,119 +921,6 @@ function normalizeToolDisplayText(value) {
1181
921
  function isEmptyToolResult(value) {
1182
922
  return isToolDisplayNoise(value);
1183
923
  }
1184
- function countTextLines(value) {
1185
- if (!value)
1186
- return 0;
1187
- return value.split(/\r?\n/).length;
1188
- }
1189
- function deriveEditorDiffStats(diff) {
1190
- // Older persisted diff payloads counted empty original content as one removed
1191
- // line because ''.split('\n') returns ['']. Normalize display stats so a new
1192
- // file reads as +N -0 and the inline preview matches the counters.
1193
- if (diff.originalContent.length === 0) {
1194
- return {
1195
- added: countNormalizedTextLines(diff.modifiedContent),
1196
- removed: 0,
1197
- };
1198
- }
1199
- if (diff.modifiedContent.length === 0) {
1200
- return {
1201
- added: 0,
1202
- removed: countNormalizedTextLines(diff.originalContent),
1203
- };
1204
- }
1205
- return {
1206
- added: diff.addedLines,
1207
- removed: diff.removedLines,
1208
- };
1209
- }
1210
- function parseReplacementCount(result) {
1211
- const match = result?.match(/Replaced\s+(\d+)\s+occurrence/i);
1212
- const count = match ? Number.parseInt(match[1], 10) : 1;
1213
- return Number.isFinite(count) && count > 0 ? count : 1;
1214
- }
1215
- function deriveToolDiffStats(toolCall) {
1216
- if (toolCall.status !== 'done' || toolCall.success === false) {
1217
- return null;
1218
- }
1219
- if (toolCall.diff) {
1220
- return deriveEditorDiffStats(toolCall.diff);
1221
- }
1222
- const toolName = toolCall.toolName.toLowerCase();
1223
- const params = toolCall.params || {};
1224
- if (toolName === 'edit' || toolName === 'edit_file') {
1225
- const oldString = typeof params.old_string === 'string' ? params.old_string : '';
1226
- const newString = typeof params.new_string === 'string' ? params.new_string : '';
1227
- if (!oldString && !newString)
1228
- return null;
1229
- const replacements = parseReplacementCount(toolCall.result);
1230
- return {
1231
- added: countTextLines(newString) * replacements,
1232
- removed: countTextLines(oldString) * replacements
1233
- };
1234
- }
1235
- if (toolName === 'write' || toolName === 'write_file') {
1236
- const content = typeof params.content === 'string' ? params.content : '';
1237
- if (!content)
1238
- return null;
1239
- return {
1240
- added: countTextLines(content),
1241
- removed: 0
1242
- };
1243
- }
1244
- return null;
1245
- }
1246
- function deriveToolCallFileName(toolCall) {
1247
- const filePath = deriveToolCallFilePath(toolCall);
1248
- if (filePath) {
1249
- const normalized = filePath.replace(/[\\/]+$/, '');
1250
- const parts = normalized.split(/[/\\]/).filter(Boolean);
1251
- if (parts.length > 0)
1252
- return parts[parts.length - 1];
1253
- }
1254
- if (toolCall.diff?.name)
1255
- return toolCall.diff.name;
1256
- return 'file';
1257
- }
1258
- function deriveFileToolPrimaryText(toolCall) {
1259
- const toolName = toolCall.toolName.toLowerCase();
1260
- if (!['write', 'write_file', 'edit', 'edit_file', 'read', 'read_file'].includes(toolName)) {
1261
- return null;
1262
- }
1263
- if (toolCall.status === 'running') {
1264
- return null;
1265
- }
1266
- if (toolCall.success === false) {
1267
- return null;
1268
- }
1269
- if (toolName === 'read' || toolName === 'read_file') {
1270
- return 'Read file';
1271
- }
1272
- if (toolName === 'edit' || toolName === 'edit_file') {
1273
- return 'Edited file';
1274
- }
1275
- if (toolCall.diff) {
1276
- const original = toolCall.diff.originalContent || '';
1277
- const { removed } = deriveEditorDiffStats(toolCall.diff);
1278
- if (!original.trim() && removed === 0) {
1279
- return 'Created file';
1280
- }
1281
- return 'Edited file';
1282
- }
1283
- return 'Created file';
1284
- }
1285
- function deriveFileToolActionLabel(toolCall) {
1286
- const toolName = toolCall.toolName.toLowerCase();
1287
- if (toolName === 'writefiles')
1288
- return 'Write';
1289
- if (toolName === 'write' || toolName === 'write_file')
1290
- return 'Write';
1291
- if (toolName === 'edit' || toolName === 'edit_file')
1292
- return 'Edit';
1293
- if (toolName === 'read' || toolName === 'read_file')
1294
- return 'Read';
1295
- return null;
1296
- }
1297
924
  function deriveFileToolGroupLabel(toolCall) {
1298
925
  const primaryText = deriveFileToolPrimaryText(toolCall);
1299
926
  if (!primaryText)
@@ -2108,92 +1735,6 @@ function buildConversationDebugSnapshot(params) {
2108
1735
  }
2109
1736
  return lines.join('\n').trim();
2110
1737
  }
2111
- function splitPreviewLines(value) {
2112
- if (!value)
2113
- return [];
2114
- return value.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
2115
- }
2116
- function buildInlineDiffPreviewLines(diff, maxChangedLines = 12) {
2117
- const originalLines = splitPreviewLines(diff.originalContent);
2118
- const modifiedLines = splitPreviewLines(diff.modifiedContent);
2119
- if (originalLines.length === 0 && modifiedLines.length === 0) {
2120
- return [];
2121
- }
2122
- let prefix = 0;
2123
- while (prefix < originalLines.length &&
2124
- prefix < modifiedLines.length &&
2125
- originalLines[prefix] === modifiedLines[prefix]) {
2126
- prefix += 1;
2127
- }
2128
- let suffix = 0;
2129
- while (suffix < originalLines.length - prefix &&
2130
- suffix < modifiedLines.length - prefix &&
2131
- originalLines[originalLines.length - 1 - suffix] === modifiedLines[modifiedLines.length - 1 - suffix]) {
2132
- suffix += 1;
2133
- }
2134
- const originalChanged = originalLines.slice(prefix, originalLines.length - suffix);
2135
- const modifiedChanged = modifiedLines.slice(prefix, modifiedLines.length - suffix);
2136
- const contextBeforeStart = Math.max(0, prefix - 2);
2137
- const contextBefore = originalLines.slice(contextBeforeStart, prefix);
2138
- const contextAfterStart = modifiedLines.length - suffix;
2139
- const contextAfter = modifiedLines.slice(contextAfterStart, Math.min(modifiedLines.length, contextAfterStart + 2));
2140
- const lines = [];
2141
- for (let index = 0; index < contextBefore.length; index += 1) {
2142
- const oldLineNumber = contextBeforeStart + index + 1;
2143
- lines.push({
2144
- kind: 'context',
2145
- oldLineNumber,
2146
- newLineNumber: oldLineNumber,
2147
- text: contextBefore[index],
2148
- });
2149
- }
2150
- const changedLineCount = originalChanged.length + modifiedChanged.length;
2151
- const truncateChanged = changedLineCount > maxChangedLines;
2152
- const allowedChangedLines = truncateChanged
2153
- ? Math.max(2, Math.floor(maxChangedLines / 2))
2154
- : Number.MAX_SAFE_INTEGER;
2155
- const removedToShow = truncateChanged ? originalChanged.slice(0, allowedChangedLines) : originalChanged;
2156
- const addedToShow = truncateChanged ? modifiedChanged.slice(0, allowedChangedLines) : modifiedChanged;
2157
- for (let index = 0; index < removedToShow.length; index += 1) {
2158
- lines.push({
2159
- kind: 'removed',
2160
- oldLineNumber: prefix + index + 1,
2161
- text: removedToShow[index],
2162
- });
2163
- }
2164
- for (let index = 0; index < addedToShow.length; index += 1) {
2165
- lines.push({
2166
- kind: 'added',
2167
- newLineNumber: prefix + index + 1,
2168
- text: addedToShow[index],
2169
- });
2170
- }
2171
- if (truncateChanged) {
2172
- lines.push({
2173
- kind: 'ellipsis',
2174
- text: `... ${changedLineCount - (removedToShow.length + addedToShow.length)} more changed lines`
2175
- });
2176
- }
2177
- for (let index = 0; index < contextAfter.length; index += 1) {
2178
- const newLineNumber = contextAfterStart + index + 1;
2179
- lines.push({
2180
- kind: 'context',
2181
- oldLineNumber: newLineNumber,
2182
- newLineNumber,
2183
- text: contextAfter[index],
2184
- });
2185
- }
2186
- if (lines.length === 0) {
2187
- const fallbackLines = modifiedLines.length > 0 ? modifiedLines : originalLines;
2188
- return fallbackLines.slice(0, 6).map((line, index) => ({
2189
- kind: 'context',
2190
- oldLineNumber: index + 1,
2191
- newLineNumber: index + 1,
2192
- text: line,
2193
- }));
2194
- }
2195
- return lines;
2196
- }
2197
1738
  function trimToolPreviewText(value, maxLines = 10, maxChars = 1200) {
2198
1739
  const normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
2199
1740
  if (!normalized)
@@ -2305,18 +1846,6 @@ function ToolCommandDetails({ toolCall }) {
2305
1846
  return null;
2306
1847
  return (_jsxs("div", { "data-command-tool-details": true, className: "overflow-hidden", children: [command && (_jsxs("div", { className: "border-b border-white/[0.04]", children: [_jsx("div", { className: "px-2.5 pt-1.5 text-[10px] uppercase tracking-[0.12em] text-white/20", children: "Full command" }), _jsx("div", { children: _jsx("pre", { className: `px-2.5 pb-2 pt-1 text-[11px] font-mono whitespace-pre-wrap break-words ${isFailed ? 'text-red-300/70' : 'text-white/50'}`, children: command }) })] })), output && (_jsxs("div", { children: [_jsx("div", { className: "px-2.5 py-1 text-[10px] uppercase tracking-[0.12em] text-white/20", children: isFailed ? 'Error' : 'Output' }), _jsx("div", { children: _jsx("pre", { className: `px-2.5 pb-2 text-[11px] font-mono whitespace-pre-wrap break-words ${isFailed ? 'text-[#ff8b8b]' : 'text-white/40'}`, children: output }) })] })), !output && (_jsx("div", { className: "px-2.5 py-2 text-[11px] text-white/24", children: isRunning ? 'Waiting for output...' : 'No output' }))] }));
2307
1848
  }
2308
- function mapWorkspaceChoice(value) {
2309
- switch (value) {
2310
- case 'inbox':
2311
- return 'Keep in Inbox';
2312
- case 'reuse':
2313
- return 'Move to existing workspace';
2314
- case 'create':
2315
- return 'Create new workspace';
2316
- default:
2317
- return value;
2318
- }
2319
- }
2320
1849
  function deriveToolDetailRows(toolCall) {
2321
1850
  const params = toolCall.params || {};
2322
1851
  const toolName = toolCall.toolName.toLowerCase();
@@ -2415,30 +1944,6 @@ function deriveToolDetailRows(toolCall) {
2415
1944
  }
2416
1945
  return rows;
2417
1946
  }
2418
- function deriveTerminalToolOutput(toolCall) {
2419
- const toolName = toolCall.toolName.toLowerCase();
2420
- if (toolName !== 'terminalwriteinput' && toolName !== 'terminalreadoutput')
2421
- return null;
2422
- const raw = (toolCall.result || '').trim();
2423
- if (!raw)
2424
- return null;
2425
- try {
2426
- const parsed = JSON.parse(raw);
2427
- const output = typeof parsed.output === 'string' ? parsed.output : '';
2428
- if (!output.trim())
2429
- return null;
2430
- return {
2431
- output,
2432
- terminalId: typeof parsed.terminalId === 'string' ? parsed.terminalId : undefined,
2433
- cwd: typeof parsed.cwd === 'string' ? parsed.cwd : undefined,
2434
- };
2435
- }
2436
- catch {
2437
- if (/^Wrote \d+ characters to terminal/i.test(raw))
2438
- return null;
2439
- return { output: raw };
2440
- }
2441
- }
2442
1947
  function TerminalOutputBlock({ output, terminalId, cwd }) {
2443
1948
  return (_jsxs("div", { className: "border-t border-white/[0.04]", children: [_jsxs("div", { className: "flex h-[22px] items-center justify-between gap-2 px-2.5 text-[10px] uppercase tracking-[0.14em] text-white/20", children: [_jsx("span", { children: "Output" }), _jsx("span", { className: "min-w-0 truncate normal-case tracking-normal font-mono text-[10px] text-white/18", children: cwd || terminalId || '' })] }), _jsx("div", { className: "max-h-[220px] overflow-auto bg-[#050506] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/10 [&::-webkit-scrollbar-thumb]:rounded-[1px] [&::-webkit-scrollbar-thumb]:hover:bg-white/20", children: _jsx("pre", { className: "px-2.5 pb-2 pt-1 text-[11px] leading-relaxed font-mono whitespace-pre-wrap break-words text-white/55", children: output }) })] }));
2444
1949
  }
@@ -2492,179 +1997,6 @@ function WebContextJobProgressRow({ toolCall, onStop, }) {
2492
1997
  onStop();
2493
1998
  }, title: WEB_JOB_STOP_TITLE, "aria-label": WEB_JOB_STOP_TITLE, className: "h-[18px] flex-shrink-0 rounded-[2px] border border-white/[0.08] bg-white/[0.04] px-1.5 text-[9px] uppercase tracking-[0.14em] text-white/45 transition-colors hover:border-white/[0.16] hover:bg-white/[0.07] hover:text-white/75", children: WEB_JOB_STOP_LABEL }))] }) }));
2494
1999
  }
2495
- function readToolStringParam(params, keys) {
2496
- if (!params)
2497
- return '';
2498
- for (const key of keys) {
2499
- const value = params[key];
2500
- if (typeof value === 'string' && value.trim())
2501
- return value.trim();
2502
- }
2503
- return '';
2504
- }
2505
- function isPseudoPermissionDirectory(directory) {
2506
- return /^__.*__$/.test((directory || '').trim());
2507
- }
2508
- function tokenizeShellCommand(command) {
2509
- return (command.match(/"[^"]*"|'[^']*'|[^\s]+/g) || [])
2510
- .map((token) => token.replace(/^["']|["']$/g, '').trim())
2511
- .filter(Boolean);
2512
- }
2513
- function deriveShellCommandNames(command) {
2514
- const tokens = tokenizeShellCommand(command);
2515
- const commands = [];
2516
- let expectCommand = true;
2517
- for (const token of tokens) {
2518
- const normalized = token.toLowerCase();
2519
- if (['&&', '||', '|', ';'].includes(normalized)) {
2520
- expectCommand = true;
2521
- continue;
2522
- }
2523
- if (!expectCommand)
2524
- continue;
2525
- if (/^[a-zA-Z][\w.-]*$/.test(token)) {
2526
- commands.push(token);
2527
- expectCommand = false;
2528
- }
2529
- }
2530
- return Array.from(new Set(commands));
2531
- }
2532
- function deriveApprovalCardInfo(toolCall, pr) {
2533
- const params = toolCall.params || {};
2534
- const normalizedToolName = toolCall.toolName.toLowerCase();
2535
- const command = readToolStringParam(params, ['command']);
2536
- const terminalInput = readToolStringParam(params, ['data']);
2537
- const browserUrl = readToolStringParam(params, ['url']);
2538
- const browserSelector = readToolStringParam(params, ['selector']);
2539
- const browserText = readToolStringParam(params, ['text']);
2540
- const browserKey = readToolStringParam(params, ['key']);
2541
- const permissionText = (pr.path || pr.directory || '').trim();
2542
- const body = (() => {
2543
- if (command)
2544
- return command;
2545
- if (terminalInput)
2546
- return terminalInput.replace(/\r/g, '\\r').replace(/\n/g, '\\n');
2547
- if (browserUrl)
2548
- return browserUrl;
2549
- if (browserSelector && browserText)
2550
- return `${browserSelector}\n${browserText}`;
2551
- if (browserSelector)
2552
- return browserSelector;
2553
- if (browserKey)
2554
- return browserKey;
2555
- return permissionText || humanizeToolName(toolCall.toolName);
2556
- })();
2557
- const description = (() => {
2558
- if (permissionText)
2559
- return permissionText;
2560
- if (normalizedToolName === 'bash')
2561
- return 'Run shell command';
2562
- if (normalizedToolName === 'terminalwriteinput')
2563
- return 'Write to PowerShell terminal';
2564
- if (normalizedToolName.startsWith('browser'))
2565
- return humanizeToolName(toolCall.toolName);
2566
- if (normalizedToolName === 'choosedirectory')
2567
- return 'Open folder picker';
2568
- return humanizeToolName(toolCall.toolName);
2569
- })();
2570
- const shellNames = deriveShellCommandNames(command || terminalInput);
2571
- const summary = shellNames.length > 0
2572
- ? shellNames.slice(0, 3).join(', ')
2573
- : humanizeToolName(toolCall.toolName);
2574
- const allowlistLabel = shellNames.length > 0
2575
- ? `Allowlist '${shellNames[0]}'${shellNames.length > 1 ? ` + ${shellNames.length - 1}` : ''}`
2576
- : `Allowlist ${humanizeToolName(toolCall.toolName)}`;
2577
- return {
2578
- description,
2579
- summary,
2580
- body,
2581
- prompt: normalizedToolName === 'bash' || normalizedToolName === 'terminalwriteinput' ? '$ ' : '',
2582
- allowlistLabel,
2583
- runLabel: normalizedToolName === 'choosedirectory' ? 'Open' : 'Run',
2584
- };
2585
- }
2586
- function normalizeTerminalApprovalCommand(raw) {
2587
- return raw
2588
- .replace(/\r/g, '\n')
2589
- .split('\n')
2590
- .map((line) => line.trim())
2591
- .filter(Boolean)
2592
- .join(' && ');
2593
- }
2594
- function safeUrlOrigin(raw) {
2595
- try {
2596
- const url = new URL(raw);
2597
- return url.protocol === 'http:' || url.protocol === 'https:' ? url.origin : '';
2598
- }
2599
- catch {
2600
- return '';
2601
- }
2602
- }
2603
- function buildSurfaceApprovalRule(toolCall, pr) {
2604
- const params = toolCall.params || {};
2605
- const action = toolCall.toolName;
2606
- if (action === 'TerminalWriteInput') {
2607
- const command = normalizeTerminalApprovalCommand(readToolStringParam(params, ['data', 'command']));
2608
- if (!command)
2609
- return null;
2610
- const names = deriveShellCommandNames(command);
2611
- return {
2612
- surface: 'terminal',
2613
- action,
2614
- matcher: command,
2615
- command,
2616
- label: names.length > 0 ? names.slice(0, 3).join(', ') : command.slice(0, 80),
2617
- };
2618
- }
2619
- if (action === 'BrowserGoto') {
2620
- const url = readToolStringParam(params, ['url']);
2621
- const origin = safeUrlOrigin(url);
2622
- if (!origin)
2623
- return null;
2624
- return {
2625
- surface: 'browser',
2626
- action,
2627
- matcher: origin,
2628
- browserOrigin: origin,
2629
- urlPattern: origin,
2630
- label: origin,
2631
- };
2632
- }
2633
- if (action === 'BrowserClick' || action === 'BrowserType') {
2634
- const selector = readToolStringParam(params, ['selector']);
2635
- if (!selector)
2636
- return null;
2637
- const browserOrigin = pr.browserOrigin || '';
2638
- return {
2639
- surface: 'browser',
2640
- action,
2641
- matcher: browserOrigin ? `${browserOrigin} ${selector}` : selector,
2642
- browserOrigin: browserOrigin || undefined,
2643
- selector,
2644
- label: browserOrigin ? `${humanizeToolName(toolCall.toolName)} ${browserOrigin}` : `${humanizeToolName(toolCall.toolName)} ${selector}`,
2645
- };
2646
- }
2647
- if (action === 'BrowserKey') {
2648
- const key = readToolStringParam(params, ['key']);
2649
- if (!key)
2650
- return null;
2651
- const browserOrigin = pr.browserOrigin || '';
2652
- return {
2653
- surface: 'browser',
2654
- action,
2655
- matcher: browserOrigin ? `${browserOrigin} ${key}` : key,
2656
- browserOrigin: browserOrigin || undefined,
2657
- key,
2658
- label: browserOrigin ? `Browser key ${key} on ${browserOrigin}` : `Browser key ${key}`,
2659
- };
2660
- }
2661
- if (!pr.directory || isPseudoPermissionDirectory(pr.directory))
2662
- return null;
2663
- return null;
2664
- }
2665
- function canBuildSurfaceApprovalRule(toolCall, pr) {
2666
- return !!buildSurfaceApprovalRule(toolCall, pr);
2667
- }
2668
2000
  function ToolApprovalCard({ toolCall, permission, canPersistAllowlist, onSkip, onRun, onAllowlist, onDenyAlways, }) {
2669
2001
  const info = deriveApprovalCardInfo(toolCall, permission);
2670
2002
  const [expanded, setExpanded] = useState(true);
@@ -3109,58 +2441,6 @@ export function ToolCallsBlock({ calls, requestId, onAddApprovalRule, onOpenEdit
3109
2441
  }, className: "min-w-0 flex flex-1 items-center gap-1.5 text-left", "aria-label": `Collapse ${fileActionLabel} ${filePath}`, children: [_jsx("span", { className: "inline-flex h-3 w-3 flex-shrink-0 items-center justify-center", children: _jsx("span", { className: "block h-1.5 w-1.5 rounded-[2px] bg-[var(--xeno-marker-solid)]" }) }), _jsxs("span", { className: "min-w-0 flex-1 inline-flex items-center gap-1.5", children: [_jsx("span", { className: "flex-shrink-0 text-[10px] uppercase tracking-[0.14em] text-white/20", children: fileActionLabel }), _jsx("span", { className: "min-w-0 truncate text-[12px] leading-3 font-mono text-white/50", title: filePath, children: deriveToolCallFileName(tc) })] })] }), _jsxs("span", { className: "flex items-center gap-2 flex-shrink-0", children: [handleOpenDiff && tc.diff && (_jsx("button", { type: "button", onClick: () => handleOpenDiff(tc.diff), className: "text-[10px] text-white/35 hover:text-white/55 transition-colors", children: "Preview diff" })), tc.diff && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-[10px] text-emerald-300", children: ["+", diffStats?.added ?? 0] }), _jsxs("span", { className: "text-[10px] text-red-300", children: ["-", diffStats?.removed ?? 0] })] })), handleOpenPath && (_jsx("button", { type: "button", onClick: () => handleOpenPath(filePath), className: "text-[10px] text-white/20 hover:text-white/40 transition-colors", children: "Open file" }))] })] }) })), _jsx(ToolInlineDiffCard, { toolCall: tc, onPeekEditorDiff: handleOpenDiff, onPeekEditorPath: handleOpenPath, embedded: true, showHeader: false, showHeaderActions: false, showHeaderStats: false })] }) }) })), !hasPendingPermission && showsInlineCommandCard && (_jsx("div", { className: `${detailRows.length > 0 || webSearchResults.length > 0 || showsInlineDiffCard ? 'border-t border-white/[0.04]' : ''}`, children: _jsx(ToolCommandDetails, { toolCall: tc }) }))] }), hasDetailsFooter && (_jsxs("div", { "data-tool-details-footer": true, className: "absolute inset-x-0 bottom-0 z-10 flex h-6 items-center gap-x-3 border-t border-white/[0.04] bg-[var(--xeno-tool-expanded-bg)] px-2.5 text-[10px] text-white/20", children: [durationLabel && _jsxs("span", { className: "flex-none", children: ["Duration ", durationLabel] }), tc.riskLevel === 'high' && _jsx("span", { className: "flex-none", children: "Risk high" }), footerCwd && _jsxs("span", { className: "min-w-0 flex-1 truncate", children: ["cwd ", footerCwd] })] }))] }) }) }))] }, tc.id));
3110
2442
  }) }));
3111
2443
  }
3112
- function toolCallLabel(toolName, params) {
3113
- switch (toolName.toLowerCase()) {
3114
- case 'choosedirectory': {
3115
- const directory = String(params?.directory || params?.path || '');
3116
- return directory
3117
- ? `Selected folder: ${directory}`
3118
- : 'Selected folder for this conversation';
3119
- }
3120
- case 'workspaceselection': {
3121
- const choice = String(params?.choice || '');
3122
- return choice ? `Workspace placement: ${choice}` : 'Workspace placement selected';
3123
- }
3124
- case 'xenoworkspacemanage': {
3125
- const action = String(params?.action || 'manage');
3126
- return `Workspace registry: ${action}`;
3127
- }
3128
- case 'bash':
3129
- case 'run_command': {
3130
- const cmd = String(params?.command || '');
3131
- return cmd.length > 60 ? `Running: ${cmd.slice(0, 57)}...` : `Running: ${cmd}`;
3132
- }
3133
- case 'runcommand':
3134
- case 'shell':
3135
- case 'shell_command':
3136
- case 'shellcommand':
3137
- case 'terminal': {
3138
- const cmd = String(params?.command || '');
3139
- return cmd ? `Running: ${compactCommandPreview(cmd, 60)}` : 'Terminal command';
3140
- }
3141
- case 'websearch':
3142
- case 'web_search':
3143
- return `Searching: ${String(params?.query || '')}`;
3144
- case 'read':
3145
- case 'read_file':
3146
- return `Reading: ${String(params?.path || '').split(/[/\\]/).pop() || 'file'}`;
3147
- case 'write':
3148
- case 'write_file':
3149
- return `Writing: ${String(params?.file_path || params?.path || '').split(/[/\\]/).pop() || 'file'}`;
3150
- case 'edit':
3151
- case 'edit_file':
3152
- return `Editing: ${String(params?.file_path || params?.path || '').split(/[/\\]/).pop() || 'file'}`;
3153
- case 'glob':
3154
- return `Finding files in ${String(params?.path || '.')}`;
3155
- case 'grep':
3156
- return `Searching files for ${String(params?.pattern || params?.query || '')}`;
3157
- case 'openapp':
3158
- case 'open_app':
3159
- return `Opening: ${String(params?.app || params?.appPath || '')}`;
3160
- default:
3161
- return `${toolName}(${Object.keys(params || {}).join(', ')})`;
3162
- }
3163
- }
3164
2444
  function buildSystemPrompt(props, authDisplayName, runtimeKind = 'sdk-native') {
3165
2445
  const { agentName, systemPrompt, templateName, workspaceName, teamName, model, capabilities, allowedDirectories, allowedApps, teamAgents, isMainAssistant } = props;
3166
2446
  const userName = authDisplayName?.trim() || 'User';
@@ -3360,11 +2640,14 @@ export function AgentView(props) {
3360
2640
  ? (effectiveRuntimeSelection.acpAgentConfigId || effectiveRuntimeSelection.providerId)
3361
2641
  : undefined;
3362
2642
  const acpAgentsForRuntime = useMemo(() => acpAgentSummariesForRuntime.map((agent) => acpRuntimeAgentDetails[agent.id] || agent), [acpAgentSummariesForRuntime, acpRuntimeAgentDetails]);
2643
+ // The composer's approvals chip (D9) chooses a posture for THIS conversation; the agent's configured
2644
+ // profile is the default. Every turn — send, rerun, ACP — carries the same one.
2645
+ const effectivePermissionProfile = conversation?.sessionUi?.permissionProfile || props.permissionProfile || 'standard';
3363
2646
  const runtimeProvider = useMemo(() => resolveHubAgentRuntimeProvider({
3364
2647
  selection: effectiveRuntimeSelection,
3365
- permissionProfile: props.permissionProfile || 'standard',
2648
+ permissionProfile: effectivePermissionProfile,
3366
2649
  acpAgents: acpAgentsForRuntime,
3367
- }), [acpAgentsForRuntime, effectiveRuntimeSelection, props.permissionProfile]);
2650
+ }), [acpAgentsForRuntime, effectiveRuntimeSelection, effectivePermissionProfile]);
3368
2651
  const runtimeMode = acpModePending || runtimeProvider.kind === 'acp-provider' ? 'acp' : inferenceMode;
3369
2652
  const selectableAcpAgents = useMemo(() => {
3370
2653
  const availableConfigIds = availableAcpConfigIds(sharedRuntimeProviders);
@@ -3537,11 +2820,9 @@ export function AgentView(props) {
3537
2820
  setLastCloudModel(isAgentCloudModel(selectedModel) ? selectedModel : AGENT_GPT55_DEFAULT_MODEL);
3538
2821
  setInferenceMode('cloud');
3539
2822
  }, [selectedModel]);
3540
- const messagesEndRef = useRef(null);
3541
2823
  const messagesScrollRef = useRef(null);
3542
2824
  const textareaRef = useRef(null);
3543
2825
  const fileInputRef = useRef(null);
3544
- const modeRef = useRef(null);
3545
2826
  const modelListRef = useRef(null);
3546
2827
  const effortSelectorRef = useRef(null);
3547
2828
  const acpCliListRef = useRef(null);
@@ -3572,8 +2853,22 @@ export function AgentView(props) {
3572
2853
  return score || a.name.localeCompare(b.name);
3573
2854
  });
3574
2855
  }, [cloudRuntimeProvider]);
2856
+ /**
2857
+ * 🔴 "Cloud" is the MODEL ROUTE, never where the work runs — and this dock contains BOTH ideas
2858
+ * at once, so the distinction has to survive into the words.
2859
+ *
2860
+ * lane = Cloud → the model is served through the XENO API (this control)
2861
+ * host = Hosted sandbox / This machine → where the agent's work EXECUTES (the context strip)
2862
+ *
2863
+ * The artifact keeps the visible label short ("Cloud") and does the disambiguating in the
2864
+ * tooltip: "The XENO API can't take this turn right now. Pick another place to run it." So the
2865
+ * fallback reason below says XENO API, not "cloud" — a reader who sees only this sentence must
2866
+ * not conclude that hosted execution is down.
2867
+ */
2868
+ const cloudCatalogState = cloudRuntimeProvider?.catalogState;
2869
+ const cloudCatalogRecovering = cloudCatalogState?.state === 'loading' || !!cloudCatalogState?.failure?.retryable;
3575
2870
  const cloudCatalogUnavailableReason = cloudRuntimeProvider?.safeUnavailableReason
3576
- || 'The live XENO model catalog is unavailable.';
2871
+ || 'The XENO API can’t take this turn right now.';
3577
2872
  const availableModels = useXenoChatStore(s => s.availableModels);
3578
2873
  const fetchModels = useXenoChatStore(s => s.fetchModels);
3579
2874
  const localModels = useMemo(() => availableModels.filter(m => m.provider === 'local'), [availableModels]);
@@ -3726,16 +3021,43 @@ export function AgentView(props) {
3726
3021
  return isThinkingVariant ? `${cloud.name} (Thinking)` : cloud.name;
3727
3022
  }
3728
3023
  if (inferenceMode === 'cloud' && sharedProviderCatalogLoaded) {
3729
- return cloudCatalogUnavailableReason.startsWith('Sign in') ? 'Sign in for Cloud' : 'Cloud unavailable';
3024
+ if (cloudCatalogState?.state === 'loading')
3025
+ return 'Loading models…';
3026
+ if (cloudCatalogRecovering)
3027
+ return 'Retrying models…';
3028
+ return cloudCatalogState ? (cloudCatalogState.failure?.code === 'auth-required' ? 'Sign in for Cloud' : 'Cloud unavailable')
3029
+ : cloudCatalogUnavailableReason.startsWith('Sign in') ? 'Sign in for Cloud' : 'Cloud unavailable';
3730
3030
  }
3731
3031
  const local = availableModels.find(m => m.id === actualModelId);
3732
3032
  return local?.name || actualModelId;
3733
- }, [cloudModels, cloudRuntimeProvider, availableModels, selectedModel, actualModelId, selectedCloudEffortFamily, isThinkingVariant, runtimeProvider.kind, runtimeProvider.displayName, acpProviderModelDisplayName, inferenceMode, sharedProviderCatalogLoaded, cloudCatalogUnavailableReason]);
3033
+ }, [cloudModels, cloudRuntimeProvider, availableModels, selectedModel, actualModelId, selectedCloudEffortFamily, isThinkingVariant, runtimeProvider.kind, runtimeProvider.displayName, acpProviderModelDisplayName, inferenceMode, sharedProviderCatalogLoaded, cloudCatalogUnavailableReason, cloudCatalogState, cloudCatalogRecovering]);
3734
3034
  const currentCloudModel = useMemo(() => cloudModels.find(m => m.id === actualModelId), [cloudModels, actualModelId]);
3735
3035
  const cloudModelSelectionMissing = runtimeProvider.kind !== 'acp-provider'
3736
3036
  && inferenceMode === 'cloud'
3737
3037
  && cloudModels.length > 0
3738
3038
  && !currentCloudModel;
3039
+ /**
3040
+ * A selection the live catalog no longer serves is REPLACED, not reported as an outage.
3041
+ *
3042
+ * 🔴 Measured 2026-09-13 in the running app: the catalog returned ten real models and the dock
3043
+ * still said "Cloud is unavailable · Choose a model". The selection was `gpt-5.5-high` — the
3044
+ * hardcoded {@link AGENT_GPT55_DEFAULT_MODEL} — and the platform now serves the `gpt-5.6-*`
3045
+ * line, so the id had simply been retired upstream. `cloudModelSelectionMissing` was telling
3046
+ * the truth about a model, and the notice read as a failure of the whole lane.
3047
+ *
3048
+ * The catalog's own default already resolves through `preferredGpt55Model`; it just had no path
3049
+ * that repaired an EXISTING selection. The reconciliation that did exist only ran when a person
3050
+ * switched INTO cloud, so a conversation that opened already on cloud with a retired id stayed
3051
+ * stuck — which is every conversation, after any upstream model rename.
3052
+ *
3053
+ * Deliberately narrow: it acts only once the catalog has actually loaded and offers something,
3054
+ * so a network failure still surfaces as an outage instead of silently rewriting the choice.
3055
+ */
3056
+ useEffect(() => {
3057
+ if (!cloudModelSelectionMissing || !preferredGpt55Model || (cloudCatalogState && cloudCatalogState.state !== 'fresh'))
3058
+ return;
3059
+ setSelectedModel(preferredGpt55Model);
3060
+ }, [cloudModelSelectionMissing, preferredGpt55Model, cloudCatalogState]);
3739
3061
  const narrationModelId = useMemo(() => {
3740
3062
  if (actualModelId.startsWith('local:'))
3741
3063
  return actualModelId;
@@ -3744,12 +3066,26 @@ export function AgentView(props) {
3744
3066
  const cloudModelUnavailable = runtimeProvider.kind !== 'acp-provider'
3745
3067
  && inferenceMode === 'cloud'
3746
3068
  && ((sharedProviderCatalogLoaded && cloudModels.length === 0) || cloudModelSelectionMissing);
3747
- const cloudModelDegraded = runtimeProvider.kind !== 'acp-provider' && inferenceMode === 'cloud' && currentCloudModel?.availability === 'degraded';
3748
- const cloudModelStatusReason = cloudModelSelectionMissing
3749
- ? 'The selected model is not present in the live model catalog. Choose an available model.'
3750
- : (cloudModels.length === 0 && sharedProviderCatalogLoaded
3751
- ? cloudCatalogUnavailableReason
3752
- : (currentCloudModel?.availabilityReason || null));
3069
+ const cloudModelDegraded = runtimeProvider.kind !== 'acp-provider' && inferenceMode === 'cloud' && (currentCloudModel?.availability === 'degraded' || cloudCatalogState?.state === 'stale');
3070
+ /**
3071
+ * Can this conversation actually be moved to the local lane? The prototype's notice offers
3072
+ * "Use Local" unconditionally because its three lanes are fixtures; here it is only an honest
3073
+ * offer when a local model is installed — otherwise the button would resolve nothing, which is
3074
+ * the failure the notice exists to avoid.
3075
+ */
3076
+ const canRunLocally = localModels.length > 0;
3077
+ // Recovery-aware hosts supply a typed reason. Keep the legacy text fallback only for old hosts;
3078
+ // a timeout or permission denial must not send an already signed-in person through OAuth again.
3079
+ const cloudNeedsSignIn = cloudCatalogState ? cloudCatalogState.failure?.code === 'auth-required' : cloudCatalogUnavailableReason.startsWith('Sign in');
3080
+ /** The lane control is already saying it. A notice that repeats a control is noise. */
3081
+ const cloudOutageStated = cloudModelUnavailable || (runtimeMode === 'cloud' && sharedProviderCatalogLoaded && cloudModels.length === 0);
3082
+ const cloudModelStatusReason = cloudCatalogState?.state === 'stale'
3083
+ ? 'Showing the last known model list. Availability will be checked when you send.'
3084
+ : cloudModelSelectionMissing
3085
+ ? 'The selected model is not present in the live model catalog. Choose an available model.'
3086
+ : (cloudModels.length === 0 && sharedProviderCatalogLoaded
3087
+ ? cloudCatalogUnavailableReason
3088
+ : (currentCloudModel?.availabilityReason || null));
3753
3089
  const renderModelSearch = () => (_jsx("div", { className: "shrink-0 border-b border-white/[0.06] bg-[#111111]", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3 w-3 -translate-y-1/2 text-white/25" }), _jsx("input", { autoFocus: true, type: "text", value: modelSearchQuery, onChange: (event) => setModelSearchQuery(event.target.value), onKeyDown: (event) => {
3754
3090
  if (event.key === 'Escape') {
3755
3091
  setModelListOpen(false);
@@ -3995,7 +3331,21 @@ export function AgentView(props) {
3995
3331
  return;
3996
3332
  const handleClick = (e) => {
3997
3333
  const target = e.target;
3998
- if (modeRef.current?.contains(target))
3334
+ /**
3335
+ * 🔴 The popovers are PORTALED (`XaMenu` → `createPortal(node, dock)`), so their rows are not
3336
+ * DOM descendants of the wrapper `<div ref=…>` that surrounds each `<XaMenu>` in the tree —
3337
+ * and DOM `.contains()` does not follow React portals. Every ref check below therefore read a
3338
+ * click on a menu ROW as "outside": the menu closed on mousedown, the row was unmounted, and
3339
+ * the click that followed landed on a detached node. Measured 2026-09-14 in the packaged app
3340
+ * with a real mouse: 14 rows visible, click a row, 0 rows, pill unchanged. `element.click()`
3341
+ * skips mousedown, which is why the first regression test passed while every human failed.
3342
+ *
3343
+ * A target inside any open popover is inside, whatever wrapper it was portaled out of.
3344
+ */
3345
+ if (target instanceof Element && target.closest('.xa-menu'))
3346
+ return;
3347
+ // the model group is its own click-away region — the bench's one element, with no wrapper to hold a ref
3348
+ if (target instanceof Element && target.closest('.xa-mgrp'))
3999
3349
  return;
4000
3350
  if (modelListRef.current?.contains(target))
4001
3351
  return;
@@ -4104,6 +3454,8 @@ export function AgentView(props) {
4104
3454
  const [sessionWorkbenchHeight, setSessionWorkbenchHeight] = useState(0);
4105
3455
  const truncateToMessage = useAgentChatStore(s => s.truncateToMessage);
4106
3456
  const switchBranch = useAgentChatStore(s => s.switchBranch);
3457
+ const rateMessage = useAgentChatStore(s => s.rateMessage);
3458
+ const rememberThinkingTime = useAgentChatStore(s => s.rememberThinkingTime);
4107
3459
  const getBranchInfo = useAgentChatStore(s => s.getBranchInfo);
4108
3460
  const activeNotificationsMap = useAgentChatStore(s => s.activeNotifications);
4109
3461
  const dismissNotification = useAgentChatStore(s => s.dismissNotification);
@@ -4185,6 +3537,101 @@ export function AgentView(props) {
4185
3537
  ? getConversationRuntimeProjection(conversationId) || projectConversationRuntimeState(conversation)
4186
3538
  : projectConversationRuntimeState(conversation)), [conversation, conversationId, getConversationRuntimeProjection]);
4187
3539
  const runtimeProjectedMessages = runtimeProjectedConversation.messages;
3540
+ /**
3541
+ * The turn has stopped at a question and is waiting for the PERSON. Nothing is running, so there
3542
+ * is nothing to stop: the send square goes back to being a send square (bench `.send.idle`), and
3543
+ * the way to end the turn is the card's own Cancel, where the question is.
3544
+ */
3545
+ const agentIsAsking = useMemo(() => {
3546
+ const last = runtimeProjectedMessages[runtimeProjectedMessages.length - 1];
3547
+ if (!last || last.role !== 'assistant')
3548
+ return false;
3549
+ return (last.toolCalls || []).some((call) => call.permissionRequest?.status === 'pending' || call.askUserRequest?.status === 'pending');
3550
+ }, [runtimeProjectedMessages]);
3551
+ /**
3552
+ * D11 — which envelope the composer is in. Machine is a chat-profile conversation the person has
3553
+ * pointed at this computer (`sessionUi.machine`); an agent conversation is Agent whatever the flag
3554
+ * says, because a project folder is a stronger fact than a remembered choice.
3555
+ */
3556
+ const composerMode = workspaceBinding.mode === 'agent' ? 'agent' : sessionUi?.machine ? 'machine' : 'chat';
3557
+ /*
3558
+ * Whether this computer can be acted on — a MEASUREMENT (did a XENO Use device answer?), never a
3559
+ * flag. A surface that already knows says so through `props.machine`; otherwise the conversation
3560
+ * asks the host itself, once, so every surface that mounts it gets the same answer without each
3561
+ * one repeating the question. `undefined` while the host is still being asked.
3562
+ */
3563
+ const [machineProbe, setMachineProbe] = useState(undefined);
3564
+ useEffect(() => {
3565
+ if (props.machine || !conversationId)
3566
+ return;
3567
+ const machine = getOptionalAgentPlatformBridge()?.machine;
3568
+ if (!machine) {
3569
+ setMachineProbe({ available: false, reason: 'This surface cannot reach this computer.' });
3570
+ return;
3571
+ }
3572
+ let cancelled = false;
3573
+ void machine.devices({ conversationId }).then((result) => {
3574
+ if (cancelled)
3575
+ return;
3576
+ if (!result.success) {
3577
+ setMachineProbe({ available: false, reason: result.error });
3578
+ return;
3579
+ }
3580
+ setMachineProbe(result.devices.length ? { available: true } : { available: false, reason: 'No device answered. Start XENO Use on this computer.' });
3581
+ }).catch((error) => { if (!cancelled)
3582
+ setMachineProbe({ available: false, reason: error instanceof Error ? error.message : 'This computer did not answer.' }); });
3583
+ return () => { cancelled = true; };
3584
+ }, [conversationId, props.machine]);
3585
+ const machineOffer = props.machine ?? machineProbe;
3586
+ /*
3587
+ * What this machine conversation HOLDS, for the strip: the host's live grants first (the ones
3588
+ * the agent can actually act through), else the app grant the transcript is asking for right now.
3589
+ * A pending ask is shown too — "Discord · this session" with Revoke meaning "no" — because the
3590
+ * strip is where the person expects to find the answer to "what can it touch?", asked or held.
3591
+ */
3592
+ const [hostMachineGrants, setHostMachineGrants] = useState([]);
3593
+ useEffect(() => {
3594
+ if (composerMode !== 'machine' || !conversationId) {
3595
+ setHostMachineGrants([]);
3596
+ return;
3597
+ }
3598
+ const machine = getOptionalAgentPlatformBridge()?.machine;
3599
+ if (!machine)
3600
+ return;
3601
+ let cancelled = false;
3602
+ void machine.grants({ conversationId }).then((result) => {
3603
+ if (cancelled || !result.success)
3604
+ return;
3605
+ setHostMachineGrants(result.grants.filter((grant) => !grant.revokedAt).map((grant) => ({ grantId: grant.grantId, name: grant.target.name, scope: grant.scope })));
3606
+ }).catch(() => { });
3607
+ return () => { cancelled = true; };
3608
+ }, [composerMode, conversationId, runtimeProjectedMessages.length]);
3609
+ const machineGrant = useMemo(() => {
3610
+ if (composerMode !== 'machine')
3611
+ return null;
3612
+ const held = hostMachineGrants[hostMachineGrants.length - 1];
3613
+ if (held)
3614
+ return { name: held.name, scope: held.scope, pending: false, grantId: held.grantId };
3615
+ for (let i = runtimeProjectedMessages.length - 1; i >= 0; i -= 1) {
3616
+ const message = runtimeProjectedMessages[i];
3617
+ for (const call of message.toolCalls || []) {
3618
+ const name = (call.toolName || '').toLowerCase();
3619
+ if ((name === 'openapp' || name === 'open_app') && call.permissionRequest?.status === 'pending') {
3620
+ const app = typeof call.params?.app === 'string' ? call.params.app : typeof call.params?.appPath === 'string' ? call.params.appPath : 'an app';
3621
+ return { name: app, scope: 'session', pending: true, requestId: message.id, permission: call.permissionRequest };
3622
+ }
3623
+ }
3624
+ }
3625
+ return null;
3626
+ }, [composerMode, hostMachineGrants, runtimeProjectedMessages]);
3627
+ /**
3628
+ * The agent is WORKING — as opposed to waiting for the person. The composer folds its model group,
3629
+ * dims the mode segment and shows a stop square only for the first; a turn stopped at a question
3630
+ * leaves every control live (bench: the frames all show an unfolded dock beside a pending ask),
3631
+ * because the person may well change the model, the mode or the posture before answering.
3632
+ */
3633
+ const agentWorking = isStreaming && !agentIsAsking;
3634
+ const controlsLocked = agentWorking || isRunningCommand;
4188
3635
  const latestRuntimePermissionProfile = runtimeProjectedConversation.latestPermissionProfile || null;
4189
3636
  const runtimeCheckpointEvents = runtimeProjectedConversation.checkpointEvents;
4190
3637
  const conversationTasks = useMemo(() => {
@@ -4195,6 +3642,10 @@ export function AgentView(props) {
4195
3642
  return (a.createdAt || 0) - (b.createdAt || 0);
4196
3643
  });
4197
3644
  }, [runtimeProjectedConversation.tasks]);
3645
+ const planAwaitingApproval = useMemo(() => runtimeProjectedMessages.some(message => (message.toolCalls || []).some(tool => {
3646
+ const ask = deriveStepRow(tool).ask;
3647
+ return ask?.kind === 'plan' && ask.status !== 'yes' && conversationTasks.some(task => task.sourceMessageId === message.id);
3648
+ })), [runtimeProjectedMessages, conversationTasks]);
4198
3649
  const taskCounts = useMemo(() => {
4199
3650
  const pending = conversationTasks.filter((task) => task.status === 'pending').length;
4200
3651
  const inProgress = conversationTasks.filter((task) => task.status === 'in_progress').length;
@@ -4294,7 +3745,31 @@ export function AgentView(props) {
4294
3745
  selectedModel,
4295
3746
  selectedReasoningOptionId,
4296
3747
  ]);
4297
- const skipConversationModelPersistRef = useRef(false);
3748
+ /**
3749
+ * Suppress the model persist ONLY in the commit where the conversation restore ran.
3750
+ *
3751
+ * 🔴 This used to be a one-shot boolean the restore effect set and the persist effect cleared —
3752
+ * and the two effects do not share their dependencies. The restore effect also depends on
3753
+ * `cloudModels`, which gets a new identity on every catalog publish, and opening the Cloud model
3754
+ * list force-refreshes the catalog. So opening the list re-ran the restore effect ALONE, the flag
3755
+ * stayed armed, and the person's next choice was the "next persist" it swallowed: the pick was
3756
+ * never saved, and the following refresh restored the old model into the pill. Measured
3757
+ * 2026-09-14 against the real component (`AgentView.modelSelection.dom.test.ts`): pill showed
3758
+ * "Model B" with the conversation still `model-a`, then snapped back to "Model A".
3759
+ *
3760
+ * ⚠️ The suppression itself is load-bearing and is kept, scoped correctly. When the conversation
3761
+ * changes, both effects run in the SAME commit and the persist effect still sees the previous
3762
+ * conversation's `selectedModel` — writing it would stamp the old model onto the new
3763
+ * conversation. So the skip applies to exactly that commit and can never outlive it.
3764
+ *
3765
+ * The counter effect is declared BEFORE both effects on purpose: effects run in declaration
3766
+ * order, so within one commit it advances first and both effects read the same value.
3767
+ */
3768
+ const commitSequenceRef = useRef(0);
3769
+ const modelRestoreCommitRef = useRef(-1);
3770
+ useEffect(() => {
3771
+ commitSequenceRef.current += 1;
3772
+ });
4298
3773
  useEffect(() => {
4299
3774
  if (!conversationId)
4300
3775
  return;
@@ -4434,14 +3909,16 @@ export function AgentView(props) {
4434
3909
  window.removeEventListener('resize', measure);
4435
3910
  };
4436
3911
  }, [renderWorkbenchPanel, sessionPanelOpen, sessionPanelTab, terminalHistory.length, backgroundTasks.length, checkpoints.length, pendingTurns.length, selectedTraceMessageId, activeTerminalCommand, isRunningTerminalCommand]);
3912
+ /**
3913
+ * Clearance for what still OVERLAYS the thread. The dock no longer does — the frame is a column and
3914
+ * the thread ends above it — so only an open workbench panel, which floats over the thread's end,
3915
+ * needs the thread to pad itself. Zero otherwise, and the bench's own padding applies.
3916
+ */
4437
3917
  const messageViewportBottomPadding = useMemo(() => {
4438
- const messageBottomBreathingRoom = 120;
4439
- const composerClearance = Math.max(footerDockHeight, 160) + messageBottomBreathingRoom;
4440
3918
  if (!renderWorkbenchPanel || !sessionPanelOpen)
4441
- return composerClearance;
4442
- const workbenchClearance = 96 + Math.max(sessionWorkbenchHeight, 0) + messageBottomBreathingRoom;
4443
- return Math.max(composerClearance, workbenchClearance);
4444
- }, [footerDockHeight, renderWorkbenchPanel, sessionPanelOpen, sessionWorkbenchHeight]);
3919
+ return 0;
3920
+ return Math.max(sessionWorkbenchHeight, 0) + 16;
3921
+ }, [renderWorkbenchPanel, sessionPanelOpen, sessionWorkbenchHeight]);
4445
3922
  const isTransientLocalRuntimeError = useCallback((error) => {
4446
3923
  const normalized = (error || '').toLowerCase();
4447
3924
  return normalized.includes('timed out')
@@ -4513,7 +3990,7 @@ export function AgentView(props) {
4513
3990
  && !requestedModel.endsWith('-thinking')
4514
3991
  ? `${reconciledModel}:thinking`
4515
3992
  : reconciledModel;
4516
- skipConversationModelPersistRef.current = true;
3993
+ modelRestoreCommitRef.current = commitSequenceRef.current;
4517
3994
  if (storedRuntime) {
4518
3995
  setChatRuntimeSelection(storedRuntime.selection);
4519
3996
  setAcpModePending(false);
@@ -4553,10 +4030,9 @@ export function AgentView(props) {
4553
4030
  updateConversation,
4554
4031
  ]);
4555
4032
  useEffect(() => {
4556
- if (skipConversationModelPersistRef.current) {
4557
- skipConversationModelPersistRef.current = false;
4033
+ // Same commit as a restore: `selectedModel` may still belong to the previous conversation.
4034
+ if (modelRestoreCommitRef.current === commitSequenceRef.current)
4558
4035
  return;
4559
- }
4560
4036
  if (!conversationId)
4561
4037
  return;
4562
4038
  const runtimeSelection = effectiveRuntimeSelection || runtimeSelectionForSdk('migration');
@@ -4733,10 +4209,75 @@ export function AgentView(props) {
4733
4209
  setIsStartingLocalRuntime(false);
4734
4210
  }
4735
4211
  }, [isStartingLocalRuntime, selectedModel]);
4736
- // Auto-scroll on new messages or streaming content
4212
+ /**
4213
+ * The thread FOLLOWS while you are at its end, and stays put while you read (the bench's `pinned`).
4214
+ *
4215
+ * 🔴 This replaces an effect that ran `scrollIntoView` on EVERY change to `messages` — which, while a
4216
+ * turn streams, is every token: scrolling up to read an earlier step was undone by the next word.
4217
+ * Now: less than 48 px from the end is "pinned"; a pinned view follows every growth of the thread (a
4218
+ * row, a card, a streamed word, a fold in flight, observed rather than inferred from the store);
4219
+ * scrolled further up, nothing moves you, and the jump button appears past 120 px.
4220
+ */
4221
+ const threadRef = useRef(null);
4222
+ const pinnedRef = useRef(true);
4223
+ const [toBottom, setToBottom] = useState({ show: false, bottom: 0 });
4224
+ useEffect(() => {
4225
+ const scroller = messagesScrollRef.current;
4226
+ if (!scroller)
4227
+ return;
4228
+ const onScroll = () => {
4229
+ const off = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
4230
+ pinnedRef.current = off < 48;
4231
+ const show = off > 120;
4232
+ const bottom = (footerDockRef.current?.offsetHeight ?? 0) + 8;
4233
+ setToBottom((previous) => (previous.show === show && previous.bottom === bottom ? previous : { show, bottom }));
4234
+ };
4235
+ scroller.addEventListener('scroll', onScroll, { passive: true });
4236
+ const dock = footerDockRef.current;
4237
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => {
4238
+ const bottom = (dock?.offsetHeight ?? 0) + 8;
4239
+ setToBottom(previous => previous.bottom === bottom ? previous : { ...previous, bottom });
4240
+ });
4241
+ if (dock)
4242
+ observer?.observe(dock);
4243
+ onScroll();
4244
+ return () => { scroller.removeEventListener('scroll', onScroll); observer?.disconnect(); };
4245
+ }, [conversationHydrating]);
4246
+ useEffect(() => {
4247
+ const thread = threadRef.current;
4248
+ const scroller = messagesScrollRef.current;
4249
+ if (!thread || !scroller || typeof ResizeObserver === 'undefined')
4250
+ return;
4251
+ const observer = new ResizeObserver(() => { if (pinnedRef.current)
4252
+ scroller.scrollTop = scroller.scrollHeight; });
4253
+ observer.observe(thread);
4254
+ return () => observer.disconnect();
4255
+ }, [conversationHydrating]);
4256
+ // Opening a conversation, or sending into one, puts you at its end — those are the two moments the
4257
+ // person has asked for the latest, whatever they were reading before.
4258
+ const lastMessageRole = messages[messages.length - 1]?.role;
4259
+ useEffect(() => {
4260
+ pinnedRef.current = true;
4261
+ const scroller = messagesScrollRef.current;
4262
+ if (scroller)
4263
+ scroller.scrollTop = scroller.scrollHeight;
4264
+ }, [conversationId]);
4737
4265
  useEffect(() => {
4738
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
4739
- }, [messages]);
4266
+ if (lastMessageRole !== 'user')
4267
+ return;
4268
+ pinnedRef.current = true;
4269
+ const scroller = messagesScrollRef.current;
4270
+ if (scroller)
4271
+ scroller.scrollTop = scroller.scrollHeight;
4272
+ }, [messages.length, lastMessageRole]);
4273
+ const jumpToLatest = useCallback(() => {
4274
+ pinnedRef.current = true;
4275
+ const scroller = messagesScrollRef.current;
4276
+ if (!scroller)
4277
+ return;
4278
+ const still = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
4279
+ scroller.scrollTo({ top: scroller.scrollHeight, behavior: still ? 'auto' : 'smooth' });
4280
+ }, []);
4740
4281
  const keepExpandedToolAboveComposer = useCallback((trigger) => {
4741
4282
  const toolCall = trigger.closest('[data-command-tool-call="true"]');
4742
4283
  if (!toolCall)
@@ -4852,8 +4393,13 @@ export function AgentView(props) {
4852
4393
  const handleSelectWorkspaceMode = useCallback(async (mode) => {
4853
4394
  if (isBusy || props.externalInputLockReason || isSelectingConversationWorkspace)
4854
4395
  return;
4855
- if (mode === 'chat') {
4396
+ if (mode === 'chat' || mode === 'machine') {
4397
+ // Machine IS a chat-profile conversation — private workspace, no project — that the person has
4398
+ // pointed at this computer; the grants that make it act are the host's, taken one ask at a time
4856
4399
  chooseConversationChatOnly();
4400
+ const targetConversationId = ensureConversationId(conversationId);
4401
+ const current = useAgentChatStore.getState().conversations[targetConversationId];
4402
+ updateConversation(targetConversationId, { sessionUi: { ...(current?.sessionUi || {}), machine: mode === 'machine' } });
4857
4403
  return;
4858
4404
  }
4859
4405
  const current = conversationId ? useAgentChatStore.getState().conversations[conversationId] : undefined;
@@ -5350,7 +4896,7 @@ export function AgentView(props) {
5350
4896
  enableThinking: thinkingEnabled,
5351
4897
  initialToolCalls: options?.initialToolCalls,
5352
4898
  capabilities,
5353
- permissionProfile: props.permissionProfile || 'standard',
4899
+ permissionProfile: effectivePermissionProfile,
5354
4900
  allowedDirectories: options?.allowedDirectoriesOverride || effectiveAllowedDirectories,
5355
4901
  approvalRules: effectiveApprovalRules,
5356
4902
  runtime: currentRuntimeBinding,
@@ -5391,7 +4937,7 @@ export function AgentView(props) {
5391
4937
  fullSystemPrompt,
5392
4938
  thinkingEnabled,
5393
4939
  capabilities,
5394
- props.permissionProfile,
4940
+ effectivePermissionProfile,
5395
4941
  effectiveRuntimeSelection,
5396
4942
  runtimeProvider.kind,
5397
4943
  runtimeProvider.status,
@@ -6558,38 +6104,20 @@ export function AgentView(props) {
6558
6104
  dispatchSend(trimmed, attachmentsToSend, targetConversationId);
6559
6105
  };
6560
6106
  const handleKeyDown = (e) => {
6561
- if (showSlashCommands) {
6562
- const cleanFilter = input.slice(1).toLowerCase();
6563
- const filtered = activeCommandsList.filter((cmd) => {
6564
- const nameWithoutSlash = cmd.name.startsWith('/') ? cmd.name.slice(1).toLowerCase() : cmd.name.toLowerCase();
6565
- return nameWithoutSlash.includes(cleanFilter) || (cmd.description && cmd.description.toLowerCase().includes(cleanFilter));
6566
- });
6567
- if (filtered.length > 0) {
6568
- if (e.key === 'ArrowDown') {
6569
- e.preventDefault();
6570
- setSlashCommandIndex((prev) => (prev + 1) % filtered.length);
6571
- return;
6572
- }
6573
- if (e.key === 'ArrowUp') {
6574
- e.preventDefault();
6575
- setSlashCommandIndex((prev) => (prev - 1 + filtered.length) % filtered.length);
6576
- return;
6577
- }
6578
- if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) {
6579
- e.preventDefault();
6580
- const chosen = filtered[slashCommandIndex] || filtered[0];
6581
- if (chosen) {
6582
- const cmdText = (chosen.name.startsWith('/') ? chosen.name : `/${chosen.name}`) + ' ';
6583
- setInput(cmdText);
6584
- return;
6585
- }
6586
- }
6587
- if (e.key === 'Escape') {
6588
- e.preventDefault();
6589
- setSlashCommandsDismissed(true);
6590
- return;
6591
- }
6107
+ // the typed popover (`/ @ #`) owns the keyboard while it is open — ↑↓ move, → ↵ ⇥ take,
6108
+ // removes the prefix and what was typed after it, Esc closes. It listens in the capture
6109
+ // phase, so nothing here needs to repeat that; only ← is ours, because it edits the box.
6110
+ if (typedToken) {
6111
+ if (e.key === 'ArrowLeft') {
6112
+ e.preventDefault();
6113
+ const next = input.slice(0, typedToken.start) + input.slice(typedToken.end);
6114
+ setInput(next);
6115
+ setTypedToken(null);
6116
+ requestAnimationFrame(() => { textareaRef.current?.focus(); textareaRef.current?.setSelectionRange(typedToken.start, typedToken.start); });
6117
+ return;
6592
6118
  }
6119
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Tab' || e.key === 'Escape' || (e.key === 'Enter' && !e.shiftKey))
6120
+ return;
6593
6121
  }
6594
6122
  if (e.key === 'Enter' && !e.shiftKey) {
6595
6123
  e.preventDefault();
@@ -6650,7 +6178,7 @@ export function AgentView(props) {
6650
6178
  appendUserMessage: false,
6651
6179
  enableThinking: rerunModel.startsWith('local:') && rerunModel.endsWith(':thinking'),
6652
6180
  capabilities,
6653
- permissionProfile: props.permissionProfile || 'standard',
6181
+ permissionProfile: effectivePermissionProfile,
6654
6182
  allowedDirectories: effectiveAllowedDirectories,
6655
6183
  approvalRules: effectiveApprovalRules,
6656
6184
  };
@@ -6684,7 +6212,7 @@ export function AgentView(props) {
6684
6212
  agentId,
6685
6213
  fullSystemPrompt,
6686
6214
  capabilities,
6687
- props.permissionProfile,
6215
+ effectivePermissionProfile,
6688
6216
  effectiveRuntimeSelection,
6689
6217
  acpProviderModel,
6690
6218
  runtimeProvider.kind,
@@ -6777,6 +6305,333 @@ export function AgentView(props) {
6777
6305
  }, 180);
6778
6306
  return () => window.clearTimeout(timer);
6779
6307
  }, [conversationId, patchSessionUi, sessionUi?.terminalCwd, sessionUi?.terminalInput, terminalDraftCwd, terminalDraftInput]);
6308
+ // ── the D10 transcript: what a turn can do, and how its words are drawn ──────────────────────
6309
+ const askActionsFor = useAskActions(props.onAddApprovalRule);
6310
+ const [machineRevoking, setMachineRevoking] = useState(false);
6311
+ const revokeMachineGrant = useCallback(async () => {
6312
+ if (!machineGrant || !conversationId)
6313
+ return;
6314
+ if (machineGrant.pending && machineGrant.permission) {
6315
+ askActionsFor(machineGrant.requestId).deny(machineGrant.permission);
6316
+ return;
6317
+ }
6318
+ const machine = getOptionalAgentPlatformBridge()?.machine;
6319
+ if (!machineGrant.grantId || !machine)
6320
+ return;
6321
+ setMachineRevoking(true);
6322
+ try {
6323
+ const result = await machine.revoke({ conversationId, grantId: machineGrant.grantId });
6324
+ if (result.success)
6325
+ setHostMachineGrants((grants) => grants.filter((grant) => grant.grantId !== machineGrant.grantId));
6326
+ }
6327
+ finally {
6328
+ setMachineRevoking(false);
6329
+ }
6330
+ }, [askActionsFor, conversationId, machineGrant]);
6331
+ const stepsMode = sessionUi?.stepsMode === 'collapsed' ? 'collapsed' : 'expanded';
6332
+ // ── the composer's own state (D9): which chooser is open, the clock, the strip's git facts ──
6333
+ const permPosture = postureOf(effectivePermissionProfile);
6334
+ const [permOpen, setPermOpen] = useState(false);
6335
+ const [laneOpen, setLaneOpen] = useState(false);
6336
+ const [moreOpen, setMoreOpen] = useState(false);
6337
+ const [branchMoreOpen, setBranchMoreOpen] = useState(false);
6338
+ const [backgroundRunsOpen, setBackgroundRunsOpen] = useState(false);
6339
+ const dockRef = useRef(null);
6340
+ const boxRef = useRef(null);
6341
+ const [typedToken, setTypedToken] = useState(null);
6342
+ const [showAllTiles, setShowAllTiles] = useState(false);
6343
+ /**
6344
+ * What `/ @ #` offer — D9's three verbs, each fed from what this surface ACTUALLY has. A section
6345
+ * with no source behind it says so rather than listing an invention; the prototype's fixtures are
6346
+ * not data this product owns.
6347
+ */
6348
+ const typedSections = useMemo(() => {
6349
+ const token = typedToken;
6350
+ if (!token)
6351
+ return [];
6352
+ const q = token.query;
6353
+ const has = (...values) => values.some((value) => (value || '').toLowerCase().includes(q));
6354
+ const cap = (items) => items.slice(0, 40);
6355
+ if (token.prefix === '/') {
6356
+ const commands = [];
6357
+ const skills = [];
6358
+ for (const command of activeCommandsList) {
6359
+ if (!has(command.name, command.description))
6360
+ continue;
6361
+ const item = {
6362
+ glyph: command.origin === 'skill' ? 'box' : 'arrow-right',
6363
+ token: command.name,
6364
+ ...(command.description ? { desc: command.description } : {}),
6365
+ ...(command.origin === 'skill' ? { kind: 'Skill' } : command.origin === 'provider' ? { kind: 'Provider' } : {}),
6366
+ insert: command.name + ' ',
6367
+ };
6368
+ (command.origin === 'skill' ? skills : commands).push(item);
6369
+ }
6370
+ return [
6371
+ { header: 'Commands', items: cap(commands) },
6372
+ { header: 'Skills', items: cap(skills) },
6373
+ ];
6374
+ }
6375
+ if (token.prefix === '@') {
6376
+ // the files this conversation has actually touched, newest first, then the ones open in the
6377
+ // editor — the product has no project index in the renderer, and inventing one would list
6378
+ // paths that may not exist
6379
+ const seen = new Set();
6380
+ const files = [];
6381
+ const add = (path) => {
6382
+ const clean = path.trim();
6383
+ if (!clean || seen.has(clean) || !has(clean))
6384
+ return;
6385
+ seen.add(clean);
6386
+ files.push({ glyph: clean.endsWith('/') ? 'folder' : 'file', token: '@' + clean, insert: '@' + clean + ' ' });
6387
+ };
6388
+ for (let i = runtimeProjectedMessages.length - 1; i >= 0 && files.length < 40; i -= 1) {
6389
+ for (const call of runtimeProjectedMessages[i]?.toolCalls || []) {
6390
+ const path = deriveToolCallFilePath(call);
6391
+ if (path)
6392
+ add(path);
6393
+ }
6394
+ }
6395
+ const sources = activeMcpList
6396
+ .filter((mcp) => has(mcp.server))
6397
+ .map((mcp) => ({ glyph: 'link', token: '@' + mcp.server, desc: mcp.status === 'connected' ? 'Connected' : mcp.status === 'auth_required' ? 'Needs sign-in' : 'Unavailable', kind: 'MCP server', insert: '@' + mcp.server + ' ' }));
6398
+ const others = Object.values(conversations)
6399
+ .filter((entry) => entry.id !== conversationId && entry.agentId === agentId && has(entry.title))
6400
+ .sort((a, b) => (b.lastMessageAt || 0) - (a.lastMessageAt || 0))
6401
+ .slice(0, 8)
6402
+ .map((entry) => ({ glyph: 'message', token: '@' + conversationSlug(entry.title), desc: entry.title, kind: relativeDay(entry.lastMessageAt), insert: '@' + conversationSlug(entry.title) + ' ' }));
6403
+ return [
6404
+ { header: 'Files and folders', items: cap(files), ...(files.length ? {} : { empty: 'No file in this conversation yet — open one, or let the agent read one.' }) },
6405
+ { header: 'Sources', items: sources, ...(sources.length ? {} : { empty: 'No connector or MCP server is connected.' }) },
6406
+ { header: 'Conversations', items: others },
6407
+ ];
6408
+ }
6409
+ // `#` is the one that also sends something OUT, and the loop that carries it back is not built
6410
+ // (`XENO REPORT - SPEC.md` §1). An empty list that said "nothing matches" would imply there is
6411
+ // a source and it is empty.
6412
+ return [{ header: 'Work items', items: [], empty: 'Work items arrive with the Report and Forum loop; nothing to reference yet.' }];
6413
+ }, [activeCommandsList, activeMcpList, agentId, conversationId, conversations, runtimeProjectedMessages, typedToken]);
6414
+ const syncTyped = useCallback((element) => {
6415
+ if (!element) {
6416
+ setTypedToken(null);
6417
+ return;
6418
+ }
6419
+ setTypedToken(tokenAt(element.value, element.selectionStart ?? element.value.length));
6420
+ }, []);
6421
+ const takeTyped = useCallback((item) => {
6422
+ const element = textareaRef.current;
6423
+ if (!element || !typedToken)
6424
+ return;
6425
+ // recorded on the conversation, so the SENT message can draw it as a reference too
6426
+ if (sessionUi?.referenceTokens?.[item.token] !== item.glyph)
6427
+ patchSessionUi({ referenceTokens: { ...(sessionUi?.referenceTokens || {}), [item.token]: item.glyph } });
6428
+ const next = element.value.slice(0, typedToken.start) + item.insert + element.value.slice(typedToken.end);
6429
+ setInput(next);
6430
+ setTypedToken(null);
6431
+ requestAnimationFrame(() => {
6432
+ const caret = typedToken.start + item.insert.length;
6433
+ element.focus();
6434
+ element.setSelectionRange(caret, caret);
6435
+ });
6436
+ }, [patchSessionUi, sessionUi?.referenceTokens, setInput, typedToken]);
6437
+ // the popovers position themselves against the dock, so they need the element, not just the ref
6438
+ const [dockEl, setDockEl] = useState(null);
6439
+ useEffect(() => { setDockEl(dockRef.current); }, []);
6440
+ const [delegatedRunsOpen, setDelegatedRunsOpen] = useState(false);
6441
+ // `Working · Ns` counts from the moment the turn started; the segments' return replays once when it ends
6442
+ const [turnSince, setTurnSince] = useState(null);
6443
+ const [composerSettled, setComposerSettled] = useState(true);
6444
+ useEffect(() => {
6445
+ if (agentWorking) {
6446
+ setTurnSince((prev) => prev ?? Date.now());
6447
+ setComposerSettled(true);
6448
+ setPermOpen(false);
6449
+ setLaneOpen(false);
6450
+ return;
6451
+ }
6452
+ setTurnSince((prev) => {
6453
+ if (prev === null)
6454
+ return prev;
6455
+ setComposerSettled(false);
6456
+ return null;
6457
+ });
6458
+ const id = window.setTimeout(() => setComposerSettled(true), 900);
6459
+ return () => window.clearTimeout(id);
6460
+ }, [agentWorking]);
6461
+ // the context meter: what the runtime reported against the model's window, when both are known
6462
+ const contextUsed = useMemo(() => {
6463
+ const total = sessionUi?.runtimeTokenUsage?.total;
6464
+ const window = cloudRuntimeProvider?.models.find((entry) => entry.id === actualModelId)?.contextWindowTokens;
6465
+ if (!total || !window)
6466
+ return null;
6467
+ return Math.min(1, total / window);
6468
+ }, [actualModelId, cloudRuntimeProvider, sessionUi?.runtimeTokenUsage?.total]);
6469
+ // the strip's branch and diff come from the host's git status, read when the project is chosen and
6470
+ // after every turn — never polled while one runs
6471
+ const [gitStrip, setGitStrip] = useState(null);
6472
+ const gitStripRoot = workspaceBinding.mode === 'agent' ? (workspaceBinding.rootDirectory || workspaceBinding.allowedDirectories[0] || '') : '';
6473
+ // Which project the strip was last read for. Skipping every read while a turn streams meant a
6474
+ // conversation OPENED mid-turn showed no branch and no diff until the turn ended — the bench's agent
6475
+ // frame shows both during its live turn. Now: read once when the project is chosen, even mid-turn,
6476
+ // and again after every turn; a running turn still never triggers a re-read.
6477
+ const gitStripReadRoot = useRef('');
6478
+ useEffect(() => {
6479
+ if (!gitStripRoot) {
6480
+ gitStripReadRoot.current = '';
6481
+ return;
6482
+ }
6483
+ if (isStreaming && gitStripReadRoot.current === gitStripRoot)
6484
+ return;
6485
+ gitStripReadRoot.current = gitStripRoot;
6486
+ let cancelled = false;
6487
+ const read = getOptionalAgentPlatformBridge()?.engineering?.gitStatus;
6488
+ if (!read) {
6489
+ setGitStrip(null);
6490
+ return;
6491
+ }
6492
+ void read({ rootDirectory: gitStripRoot }).then((status) => {
6493
+ if (cancelled)
6494
+ return;
6495
+ if (!status?.success || !status.isGitRepo) {
6496
+ setGitStrip(null);
6497
+ return;
6498
+ }
6499
+ const files = Array.isArray(status.files) ? status.files : [];
6500
+ setGitStrip({
6501
+ branch: status.branch,
6502
+ upstream: status.upstream,
6503
+ added: files.reduce((sum, file) => sum + (file.additions || 0), 0),
6504
+ removed: files.reduce((sum, file) => sum + (file.deletions || 0), 0),
6505
+ });
6506
+ }).catch(() => { if (!cancelled)
6507
+ setGitStrip(null); });
6508
+ return () => { cancelled = true; };
6509
+ }, [gitStripRoot, isStreaming]);
6510
+ // Create PR is a turn: the agent opens the pull request with its own tools, in the project it is bound to
6511
+ const handleCreatePullRequest = useCallback(async () => {
6512
+ const command = activeCommandsList.find((entry) => entry.name === '/pr');
6513
+ if (command) {
6514
+ await handleExecuteCommandOrPrompt('/pr');
6515
+ return;
6516
+ }
6517
+ await handleExecuteCommandOrPrompt('Open a pull request for the current branch: summarise the changes since it diverged from its base in the title and body, and use the repository\'s PR conventions if it has any.');
6518
+ }, [activeCommandsList, handleExecuteCommandOrPrompt]);
6519
+ /*
6520
+ * The reply's markdown, as the bench writes it: real `<p>`, `<blockquote>`, `<ul>`, `<table>`, styled
6521
+ * by `.xa-body` in `transcript.css` — the agent-interface's own vocabulary.
6522
+ *
6523
+ * 🔴 It used to wrap everything in `div.agent-markdown` and render every paragraph as a `<div>` with
6524
+ * utility margins, so `.xa-body p`, `.xa-body blockquote` and the rest of that sheet matched nothing
6525
+ * and the older `.agent-markdown` rules styled the reply instead.
6526
+ */
6527
+ /** `<blockquote><p>x</p></blockquote>` → `<blockquote>x</blockquote>`, and only for a single paragraph. */
6528
+ const unwrapSoleParagraph = (children) => {
6529
+ const nodes = React.Children.toArray(children).filter((node) => typeof node !== 'string' || node.trim());
6530
+ const only = nodes.length === 1 ? nodes[0] : null;
6531
+ return React.isValidElement(only) && only.type === 'p' ? only.props.children : children;
6532
+ };
6533
+ const renderAssistantMarkdown = (text, messageFileRefs) => (_jsx(_Fragment, { children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], urlTransform: agentMarkdownUrlTransform, components: {
6534
+ pre: ({ children }) => _jsx(_Fragment, { children: children }),
6535
+ // a quote of one paragraph is one line of quoted words; the extra `<p>` markdown puts
6536
+ // inside it is grammar, not structure, and it takes the paragraph's spacing with it
6537
+ blockquote: ({ children }) => _jsx("blockquote", { children: unwrapSoleParagraph(children) }),
6538
+ a: ({ href, children }) => (_jsx(AgentMarkdownLink, { href: href, fileReferences: messageFileRefs, onOpenEditorPath: props.onOpenEditorPath, allowedDirectories: effectiveAllowedDirectories, conversationId: conversationId || undefined, children: children })),
6539
+ code: ({ className, children, node, ...codeProps }) => {
6540
+ const isBlock = className || (node?.position && node.position.start?.line !== node.position.end?.line);
6541
+ if (isBlock) {
6542
+ return _jsx(CodeBlock, { className: className, ...codeProps, children: children });
6543
+ }
6544
+ const inlineText = React.Children.toArray(children).join('');
6545
+ const fileRef = props.onOpenEditorPath
6546
+ ? findMessageFileReference(inlineText, messageFileRefs)
6547
+ : null;
6548
+ if (fileRef) {
6549
+ return (_jsx("button", { type: "button", onClick: () => props.onOpenEditorPath?.(fileRef.path), className: "relative -top-px inline-flex items-center align-middle whitespace-nowrap px-1.5 py-0.5 rounded-[3px] bg-black/30 border border-white/[0.04] text-[12px] leading-none font-mono text-white/45 hover:text-white/78 hover:border-white/[0.12] transition-colors cursor-pointer", children: children }));
6550
+ }
6551
+ return _jsx("code", { className: "px-1.5 py-0.5 rounded-[3px] bg-black/30 border border-white/[0.04] text-[12px] font-mono text-white/45", ...codeProps, children: children });
6552
+ },
6553
+ img: ({ src, alt, node: _node, ...imageProps }) => (_jsx("a", { href: src, onClick: (e) => { e.preventDefault(); if (src)
6554
+ getAgentPlatformBridge()?.window?.openExternal(src); }, className: "inline-block max-w-full my-2 rounded-md overflow-hidden border border-white/[0.06] hover:border-white/[0.12] transition-colors cursor-pointer", children: _jsx("img", { src: src, alt: alt || '', className: "max-w-full max-h-[300px] object-contain", referrerPolicy: "no-referrer", loading: "lazy", ...imageProps }) }))
6555
+ }, children: prepareAgentMarkdown(text, messageFileRefs) }) }));
6556
+ const transcriptActionsFor = (msg, msgIdx) => {
6557
+ const previousUserIndex = (() => { for (let i = msgIdx - 1; i >= 0; i -= 1)
6558
+ if (runtimeProjectedMessages[i]?.role === 'user')
6559
+ return i; return -1; })();
6560
+ return {
6561
+ ask: askActionsFor(msg.id),
6562
+ openPath: props.onPeekEditorPath || props.onOpenEditorPath,
6563
+ openDiff: props.onPeekEditorDiff || props.onOpenEditorDiff,
6564
+ openExternal: (url) => { getOptionalAgentPlatformBridge()?.window?.openExternal(url); },
6565
+ copy: (text) => copyTextWithFallback(text),
6566
+ ...(conversationId && getOptionalAgentPlatformBridge()?.agent?.resumeTurn
6567
+ ? { resume: () => { void getOptionalAgentPlatformBridge()?.agent?.resumeTurn?.({ conversationId, requestId: msg.id }); } }
6568
+ : {}),
6569
+ ...(previousUserIndex >= 0 && !isRunningCommand
6570
+ ? {
6571
+ retry: () => { void rerunUserTurn(previousUserIndex); },
6572
+ // a reply cannot be edited — Edit opens the PROMPT that produced it, where the branch is made
6573
+ edit: () => { const prompt = runtimeProjectedMessages[previousUserIndex]; if (prompt)
6574
+ setEditingMsgId(prompt.id); },
6575
+ }
6576
+ : {}),
6577
+ ...(conversationId
6578
+ ? {
6579
+ rate: (rating) => { rateMessage(conversationId, msg.id, rating); },
6580
+ rememberThinkingTime: (ms) => { rememberThinkingTime(conversationId, msg.id, ms); },
6581
+ }
6582
+ : {}),
6583
+ chooseDirectory: (toolCall) => { void handleChooseDirectorySelect(msg.id, toolCall); },
6584
+ ...(workspacePlacementCard ? { workspacePlacement: workspacePlacementCard } : {}),
6585
+ // a backgrounded run's row reads the RUN, not the tool call that detached it
6586
+ background: {
6587
+ find: (taskId) => backgroundRunViews.find((run) => run.taskId === taskId) || null,
6588
+ output: (taskId) => { const task = backgroundTasks.find((entry) => entry.taskId === taskId); if (task)
6589
+ void handleReadBackgroundTask(task); },
6590
+ stop: (taskId) => { const task = backgroundTasks.find((entry) => entry.taskId === taskId); if (task)
6591
+ void handleStopBackgroundTask(task); },
6592
+ },
6593
+ };
6594
+ };
6595
+ /**
6596
+ * The sub-agents this conversation spawned. A delegation is a CHILD CONVERSATION here — the store
6597
+ * marks it with `parentConversationId` — so this reads those rather than a second record that
6598
+ * could disagree with them. A child with a turn in flight is live.
6599
+ */
6600
+ const activeTurns = useAgentChatStore(state => state.activeTurnsByConversation);
6601
+ const delegatedRunViews = useMemo(() => {
6602
+ if (!conversationId)
6603
+ return [];
6604
+ return Object.values(conversations)
6605
+ .filter((child) => child.sessionUi?.parentConversationId === conversationId)
6606
+ .map((child) => {
6607
+ const last = child.messages[child.messages.length - 1];
6608
+ const live = !!activeTurns[child.id] || !!last?.isStreaming;
6609
+ return {
6610
+ conversationId: child.id,
6611
+ title: child.title || 'Sub-agent',
6612
+ live,
6613
+ startedAt: child.messages[0]?.timestamp || child.createdAt || Date.now(),
6614
+ updatedAt: last?.timestamp || child.lastMessageAt || Date.now(),
6615
+ ...(live && last?.toolCalls?.length ? { current: toolCallLabel(last.toolCalls[last.toolCalls.length - 1].toolName, last.toolCalls[last.toolCalls.length - 1].params) } : {}),
6616
+ };
6617
+ })
6618
+ .sort((a, b) => Number(b.live) - Number(a.live) || b.updatedAt - a.updatedAt);
6619
+ }, [activeTurns, conversationId, conversations]);
6620
+ /** The conversation's background runs as the transcript sees them — one shape for the row and the tray. */
6621
+ const backgroundRunViews = useMemo(() => backgroundTasks.map((task) => ({
6622
+ taskId: task.taskId,
6623
+ status: task.status,
6624
+ ...(task.command ? { command: task.command } : {}),
6625
+ startedAt: task.startedAt,
6626
+ updatedAt: task.updatedAt,
6627
+ ...(task.exitCode === undefined ? {} : { exitCode: task.exitCode }),
6628
+ ...(task.lastOutput ? { lastOutput: task.lastOutput } : {}),
6629
+ ...(backgroundTaskBusy[task.taskId] ? { busy: backgroundTaskBusy[task.taskId] } : {}),
6630
+ })), [backgroundTasks, backgroundTaskBusy]);
6631
+ const tasksForMessage = (msg) => {
6632
+ const own = conversationTasks.filter((task) => task.sourceMessageId === msg.id || task.sourceRequestId === msg.id);
6633
+ return own.length ? own : undefined;
6634
+ };
6780
6635
  const traceMessages = useMemo(() => ([...runtimeProjectedMessages]
6781
6636
  .filter((message) => message.role === 'assistant' && (!!message.toolCalls?.length
6782
6637
  || !!message.error
@@ -7471,25 +7326,38 @@ export function AgentView(props) {
7471
7326
  props.onNavigateConversation?.(id);
7472
7327
  } });
7473
7328
  }
7474
- return (_jsxs("div", { className: "xeno-app-shell flex-1 relative min-h-0", children: [sdkConfirmation.confirmation, _jsx("div", { ref: messagesScrollRef, className: "agent-scrollbar absolute inset-0 overflow-auto pr-1.5", style: { scrollbarGutter: 'stable' }, children: conversationHydrating ? (_jsx(ConversationLoadingSkeleton, {})) : (_jsxs("div", { className: "w-full max-w-[820px] mx-auto px-6 pt-6 min-h-full flex flex-col gap-1.5", style: { paddingBottom: messageViewportBottomPadding }, children: [conversation?.sessionUi?.parentConversationId && props.onNavigateConversation && (_jsx("div", { className: "sticky top-2 z-[21] -mx-1 px-1 pb-2", children: _jsxs("button", { type: "button", onClick: () => props.onNavigateConversation?.(conversation.sessionUi?.parentConversationId || ''), className: "xeno-border-subtle w-full rounded-[6px] border bg-[#111111] px-3 py-2 text-left text-[11px] text-white/48 hover:bg-white/[0.045] hover:text-white/72 transition-colors inline-flex items-center gap-2", children: [_jsx(ChevronLeft, { className: "h-3.5 w-3.5" }), _jsx("span", { children: "Back to parent agent" }), _jsx("span", { className: "ml-auto text-[10px] text-white/22 truncate", children: agentName })] }) })), conversation && devSessionOpen && (_jsx("div", { className: "sticky top-2 z-[20] -mx-1 px-1 pb-2", children: _jsxs("div", { className: "xeno-border-subtle rounded-[6px] border bg-[#111111] px-3 py-2 flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-wrap", children: [_jsx("span", { className: "text-[10px] uppercase tracking-[0.14em] text-white/28", children: "Dev session" }), _jsx("span", { className: `text-[10px] uppercase tracking-[0.14em] ${sessionStatusClassName}`, children: sessionStatusLabel }), _jsxs("span", { className: "text-[10px] text-white/24 tabular-nums", children: [pendingTurns.length, " queued"] }), _jsxs("span", { className: "text-[10px] text-white/24 tabular-nums", children: [checkpoints.length, " checkpoints"] }), _jsxs("span", { className: "text-[10px] text-white/18", children: ["Last active ", new Date(conversation.lastMessageAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })] })] }), _jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [_jsxs("button", { type: "button", onClick: () => { void handleCopyDebugSnapshot('standard'); }, disabled: !conversationId || conversationHydrating, className: "h-7 px-2.5 rounded-[4px] bg-white/[0.05] hover:bg-white/[0.08] text-[10px] text-white/55 hover:text-white/78 transition-colors disabled:opacity-35 inline-flex items-center gap-1.5", title: "Copy a redacted support report", children: [_jsx(Copy, { className: "w-3 h-3" }), "Copy report"] }), _jsx("button", { type: "button", onClick: () => { void handleCopyDebugSnapshot('full'); }, disabled: !conversationId || conversationHydrating, className: "h-7 px-2.5 rounded-[4px] bg-white/[0.05] hover:bg-white/[0.08] text-[10px] text-white/45 hover:text-white/72 transition-colors disabled:opacity-35", title: "Copy the full redacted support report", children: "Full report" }), _jsx("button", { type: "button", onClick: toggleDevSession, className: "h-7 px-2.5 rounded-[4px] bg-white/[0.05] hover:bg-white/[0.08] text-[10px] text-white/35 hover:text-white/70 transition-colors", title: "Hide developer session (Ctrl+Shift+D)", children: "Hide" })] })] }) })), runtimeProjectedMessages.length === 0 && (_jsx("div", { className: "flex-1 flex items-center justify-center pt-[20vh]", children: _jsxs("p", { className: "text-[13px] text-white/20", children: ["Message ", agentName, " to get started"] }) })), runtimeProjectedMessages.map((msg, msgIdx) => {
7329
+ return (
7330
+ // The panel's frame, in the Turn Modes Bench's anatomy: `.xa-app` is a column — the thread scrolls
7331
+ // inside `.xa-transcript`, the dock is pinned below it, and the jump-to-latest button floats above the
7332
+ // dock. The transcript used to scroll BEHIND an absolutely positioned dock and pad its end by the
7333
+ // dock's measured height; in a column nothing needs measuring for the thread to end above the box.
7334
+ // (No `xeno-app-shell`: that is the product chrome's surface, declared later at equal specificity, and it
7335
+ // painted the panel's root in the chrome's ink and background instead of the panel's own.)
7336
+ _jsxs("div", { className: "xa-app", children: [sdkConfirmation.confirmation, _jsx("div", { ref: messagesScrollRef, className: `xa-transcript${stepsMode === 'collapsed' ? ' xa-collapsed' : ''}`, "data-transcript": true, style: messageViewportBottomPadding ? { paddingBottom: messageViewportBottomPadding } : undefined, children: conversationHydrating ? (_jsx(ConversationLoadingSkeleton, {})) : (_jsxs("div", { ref: threadRef, className: "xa-thread", "data-stage": true, children: [conversation?.sessionUi?.parentConversationId && props.onNavigateConversation && (_jsx("div", { className: "sticky top-2 z-[21] -mx-1 px-1 pb-2", children: _jsxs("button", { type: "button", onClick: () => props.onNavigateConversation?.(conversation.sessionUi?.parentConversationId || ''), className: "xeno-border-subtle w-full rounded-[6px] border bg-[#111111] px-3 py-2 text-left text-[11px] text-white/48 hover:bg-white/[0.045] hover:text-white/72 transition-colors inline-flex items-center gap-2", children: [_jsx(ChevronLeft, { className: "h-3.5 w-3.5" }), _jsx("span", { children: "Back to parent agent" }), _jsx("span", { className: "ml-auto text-[10px] text-white/22 truncate", children: agentName })] }) })), conversation && devSessionOpen && (_jsx("div", { className: "sticky top-2 z-[20] -mx-1 px-1 pb-2", children: _jsxs("div", { className: "xeno-border-subtle rounded-[6px] border bg-[#111111] px-3 py-2 flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-wrap", children: [_jsx("span", { className: "text-[10px] uppercase tracking-[0.14em] text-white/28", children: "Dev session" }), _jsx("span", { className: `text-[10px] uppercase tracking-[0.14em] ${sessionStatusClassName}`, children: sessionStatusLabel }), _jsxs("span", { className: "text-[10px] text-white/24 tabular-nums", children: [pendingTurns.length, " queued"] }), _jsxs("span", { className: "text-[10px] text-white/24 tabular-nums", children: [checkpoints.length, " checkpoints"] }), _jsxs("span", { className: "text-[10px] text-white/18", children: ["Last active ", new Date(conversation.lastMessageAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })] })] }), _jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [_jsxs("button", { type: "button", onClick: () => { void handleCopyDebugSnapshot('standard'); }, disabled: !conversationId || conversationHydrating, className: "h-7 px-2.5 rounded-[4px] bg-white/[0.05] hover:bg-white/[0.08] text-[10px] text-white/55 hover:text-white/78 transition-colors disabled:opacity-35 inline-flex items-center gap-1.5", title: "Copy a redacted support report", children: [_jsx(Copy, { className: "w-3 h-3" }), "Copy report"] }), _jsx("button", { type: "button", onClick: () => { void handleCopyDebugSnapshot('full'); }, disabled: !conversationId || conversationHydrating, className: "h-7 px-2.5 rounded-[4px] bg-white/[0.05] hover:bg-white/[0.08] text-[10px] text-white/45 hover:text-white/72 transition-colors disabled:opacity-35", title: "Copy the full redacted support report", children: "Full report" }), _jsx("button", { type: "button", onClick: toggleDevSession, className: "h-7 px-2.5 rounded-[4px] bg-white/[0.05] hover:bg-white/[0.08] text-[10px] text-white/35 hover:text-white/70 transition-colors", title: "Hide developer session (Ctrl+Shift+D)", children: "Hide" })] })] }) })), runtimeProjectedMessages.length === 0 && (_jsx("div", { className: "flex-1 flex items-center justify-center pt-[20vh]", children: _jsxs("p", { className: "text-[13px] text-white/20", children: ["Message ", agentName, " to get started"] }) })), runtimeProjectedMessages.map((msg, msgIdx) => {
7475
7337
  const previousMsg = runtimeProjectedMessages[msgIdx - 1];
7476
- const parts = computeMsgParts(msg);
7477
- const addConversationTurnGap = previousMsg !== undefined && msg.role !== previousMsg.role;
7478
- const assistantLaneParts = parts.filter((part) => part.kind !== 'gap');
7479
- // Show hover toolbar for plain text assistant messages (no tool calls, no errors)
7480
- const hasTextOnly = parts.length > 0 && parts.every(p => p.kind === 'text' || p.kind === 'thinking');
7481
- const showToolbar = !msg.hideToolbar && !msg.isStreaming && !msg.isThinking && msg.content && msg.role === 'assistant' && hasTextOnly;
7482
- const deferToolbarToBlockedPrompt = !!(blockedPromptVisible
7483
- && blockedPromptAnchorMessage
7484
- && blockedPromptAnchorMessage.id === msg.id);
7338
+ // a day boundary in the thread: before the first message, and wherever the day changes
7339
+ const dayLabel = relativeDay(msg.timestamp);
7340
+ const startsDay = !previousMsg || relativeDay(previousMsg.timestamp) !== dayLabel;
7341
+ const nextMsg = runtimeProjectedMessages[msgIdx + 1];
7342
+ // The reply's Retry reruns THIS message. It exists only on a reply that has one, so a turn
7343
+ // that failed or never answered would leave no way to run it again the user row offers
7344
+ // Rerun exactly then, and only then. A turn still in flight is neither: it has not failed,
7345
+ // and running it again while it runs is not something to offer.
7346
+ const replyIsUnderway = nextMsg?.role === 'assistant' && (!!nextMsg.isStreaming || !!nextMsg.isThinking);
7347
+ const replyOffersRetry = replyIsUnderway || (nextMsg?.role === 'assistant' && !!nextMsg.content?.trim() && !nextMsg.error && !nextMsg.isStreaming);
7485
7348
  const messageFileRefs = msg.role === 'assistant' ? resolveMessageFileReferences(runtimeProjectedMessages, msgIdx) : [];
7486
7349
  const messageModel = msg.model || selectedModel;
7350
+ /*
7351
+ * The reply's receipt names the model the way the CATALOG does — `Sonnet 5`. The
7352
+ * provider's own display name first, because it is what every other control in this
7353
+ * panel shows; the static table is the fallback for a model the catalog never listed.
7354
+ */
7487
7355
  const displayMessageModel = messageModel
7488
7356
  ? (messageModel.startsWith('local:')
7489
7357
  ? messageModel.slice('local:'.length)
7490
- : cloudModelName(messageModel)).toLowerCase()
7358
+ : cloudModels.find((entry) => entry.id === messageModel)?.name || cloudModelName(messageModel))
7491
7359
  : null;
7492
- return (_jsx(React.Fragment, { children: _jsx("div", { className: `flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} ${addConversationTurnGap ? 'mt-7' : ''}`, children: msg.role === 'user' ? (_jsxs("div", { className: "relative w-fit max-w-[85%] group/msg", children: [editingMsgId === msg.id ? (_jsxs("div", { className: "rounded-lg bg-black/40 border border-white/[0.08] p-2", children: [_jsx("div", { className: "rounded-[4px] border border-white/[0.1] bg-white/[0.04] overflow-hidden", children: _jsx("textarea", { ref: el => { if (el && !el.dataset.focused) {
7360
+ return (_jsxs(React.Fragment, { children: [startsDay && _jsx("div", { className: "xa-sep xa-label", children: dayLabel }), msg.role === 'user' ? (_jsxs("div", { className: "xa-user", children: [editingMsgId === msg.id ? (_jsxs("div", { className: "rounded-lg bg-black/40 border border-white/[0.08] p-2", children: [_jsx("div", { className: "rounded-[4px] border border-white/[0.1] bg-white/[0.04] overflow-hidden", children: _jsx("textarea", { ref: el => { if (el && !el.dataset.focused) {
7493
7361
  el.focus();
7494
7362
  el.dataset.focused = '1';
7495
7363
  el.style.height = 'auto';
@@ -7517,11 +7385,7 @@ export function AgentView(props) {
7517
7385
  setEditingMsgId(null);
7518
7386
  });
7519
7387
  }
7520
- }, className: "px-3 py-1.5 rounded-[3px] bg-white/[0.1] border border-white/[0.12] text-[11px] text-white/70 hover:bg-white/[0.16] hover:text-white transition-colors", children: "Save" })] })] })] })) : (_jsxs("div", { className: `w-fit max-w-full ml-auto px-3.5 py-2.5 rounded-lg text-[13px] leading-relaxed text-white/90 whitespace-pre-wrap break-words ${msg.deliveryState === 'queued'
7521
- ? 'bg-white/[0.05] border border-white/[0.06]'
7522
- : 'bg-white/[0.08]'}`, children: [msg.attachments && msg.attachments.length > 0 && (_jsx("div", { className: "grid grid-cols-2 gap-2 mb-2", children: msg.attachments.map(att => (_jsx("div", { className: "rounded overflow-hidden border border-white/[0.08] bg-black/30", children: _jsx("img", { src: att.dataUrl, alt: att.name, className: "w-full h-24 object-cover" }) }, att.id))) })), msg.content] })), _jsxs("div", { className: `flex items-center justify-end gap-1 whitespace-nowrap transition-opacity ${msg.deliveryState === 'queued'
7523
- ? 'opacity-100'
7524
- : 'absolute right-0 top-full z-20 pt-1 opacity-0 pointer-events-none group-hover/msg:opacity-100 group-hover/msg:pointer-events-auto'} ${msg.deliveryState === 'queued' ? 'mt-1' : ''}`, children: [msg.deliveryState === 'queued' && (_jsx("span", { className: "text-[9px] uppercase tracking-[0.14em] text-white/26 mr-1", children: "Queued" })), msg.deliveryState === 'queued' && conversationId && (
7388
+ }, className: "px-3 py-1.5 rounded-[3px] bg-white/[0.1] border border-white/[0.12] text-[11px] text-white/70 hover:bg-white/[0.16] hover:text-white transition-colors", children: "Save" })] })] })] })) : (_jsxs("div", { className: `xa-bubble${msg.deliveryState === 'queued' ? ' xa-queued' : ''}`, children: [msg.attachments && msg.attachments.length > 0 && (_jsx("div", { className: "grid grid-cols-2 gap-2 mb-2", children: msg.attachments.map(att => (_jsx("div", { className: "rounded overflow-hidden border border-white/[0.08] bg-black/30", children: _jsx("img", { src: att.dataUrl, alt: att.name, className: "w-full h-24 object-cover" }) }, att.id))) })), _jsx(MessageText, { text: msg.content, references: sessionUi?.referenceTokens })] })), _jsxs("div", { className: `xa-acts${msg.deliveryState === 'queued' ? ' xa-on' : ''}`, children: [msg.deliveryState === 'queued' && _jsx("span", { className: "xa-t xa-label", children: "Queued" }), msg.deliveryState === 'queued' && conversationId && (
7525
7389
  /**
7526
7390
  * Send this into the RUNNING turn instead of waiting
7527
7391
  * for it (ADE spec §5.5).
@@ -7535,7 +7399,7 @@ export function AgentView(props) {
7535
7399
  * be steered, so the message simply stays queued —
7536
7400
  * the honest outcome, not a silent no-op.
7537
7401
  */
7538
- _jsx("button", { onClick: () => {
7402
+ _jsxs("button", { onClick: () => {
7539
7403
  if (!activeRequestId)
7540
7404
  return;
7541
7405
  void getAgentPlatformBridge()?.agent?.steerTurn?.({
@@ -7543,74 +7407,14 @@ export function AgentView(props) {
7543
7407
  requestId: activeRequestId,
7544
7408
  instruction: msg.content,
7545
7409
  });
7546
- }, disabled: !isRunningCommand || !activeRequestId, className: "p-1 rounded text-white/20 hover:text-white/50 hover:bg-white/[0.04] active:scale-75 transition-all duration-100 disabled:cursor-not-allowed disabled:opacity-20", title: "Send now \u2014 deliver this into the running turn instead of waiting", children: _jsx(CornerDownLeft, { className: "w-3 h-3" }) })), _jsx("span", { className: "text-[9px] text-white/15 mr-1", children: new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }), _jsx(DiffCopyButton, { getText: () => msg.content, title: "Copy message" }), _jsx("button", { onClick: () => { setEditingMsgId(msg.id); }, disabled: isRunningCommand, className: "p-1 rounded text-white/20 hover:text-white/50 hover:bg-white/[0.04] active:scale-75 transition-all duration-100 disabled:cursor-not-allowed disabled:opacity-20", title: "Edit", children: _jsx(Pencil, { className: "w-3 h-3" }) }), _jsx("button", { onClick: () => {
7547
- void rerunUserTurn(msgIdx);
7548
- }, disabled: isRunningCommand, className: "p-1 rounded text-white/20 hover:text-white/50 hover:bg-white/[0.04] active:scale-75 transition-all duration-100 disabled:opacity-20", title: "Rerun from here", children: _jsx(RefreshCw, { className: "w-3 h-3" }) }), (() => {
7410
+ }, disabled: !isRunningCommand || !activeRequestId, className: "xa-b", title: "Send now \u2014 deliver this into the running turn instead of waiting", children: [_jsx(Glyph, { name: "send", size: 11 }), "Send now"] })), _jsx("span", { className: "xa-t xa-num", children: new Date(msg.timestamp).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) }), _jsxs("button", { type: "button", className: "xa-b", onClick: () => { setEditingMsgId(msg.id); }, disabled: isRunningCommand, title: "Edit this message and run it again", children: [_jsx(Glyph, { name: "edit", size: 11 }), "Edit"] }), _jsx(MessageCopyButton, { text: msg.content, copy: copyTextWithFallback }), !replyOffersRetry && (_jsxs("button", { type: "button", className: "xa-b", onClick: () => { void rerunUserTurn(msgIdx); }, disabled: isRunningCommand, title: "Run this message again", children: [_jsx(Glyph, { name: "refresh", size: 11 }), "Rerun"] })), (() => {
7549
7411
  if (!conversationId)
7550
7412
  return null;
7551
7413
  const branchInfo = getBranchInfo(conversationId, msg.id);
7552
7414
  if (!branchInfo)
7553
7415
  return null;
7554
- return (_jsxs("div", { className: "flex items-center gap-0.5 ml-1", children: [_jsx("button", { onClick: () => switchBranch(conversationId, msg.id, 'prev'), disabled: isBusy, className: "p-0.5 rounded text-white/30 hover:text-white/60 hover:bg-white/[0.06] transition-colors disabled:cursor-not-allowed disabled:opacity-20", children: _jsx(ChevronLeft, { className: "w-3 h-3" }) }), _jsxs("span", { className: "text-[9px] text-white/30 tabular-nums min-w-[20px] text-center", children: [branchInfo.current, "/", branchInfo.total] }), _jsx("button", { onClick: () => switchBranch(conversationId, msg.id, 'next'), disabled: isBusy, className: "p-0.5 rounded text-white/30 hover:text-white/60 hover:bg-white/[0.06] transition-colors disabled:cursor-not-allowed disabled:opacity-20", children: _jsx(ChevronRight, { className: "w-3 h-3" }) })] }));
7555
- })()] })] })) : (_jsxs("div", { className: "w-full group/ast", children: [_jsx("div", { className: "space-y-2.5", children: assistantLaneParts.map((part, laneIdx) => {
7556
- const isLastLanePart = laneIdx === assistantLaneParts.length - 1;
7557
- return (_jsx(AssistantTimelineRow, { markerKind: assistantLaneMarkerKind(part), markerEmbedded: part.kind === 'error'
7558
- || part.kind === 'turn-status'
7559
- || (part.kind === 'tool-calls'
7560
- && part.calls.length > 0
7561
- && isCommandToolCall(part.calls[0])), isLast: isLastLanePart, showConnectorBelow: isLastLanePart && !!showToolbar && !deferToolbarToBlockedPrompt, children: (() => {
7562
- switch (part.kind) {
7563
- case 'command-result':
7564
- return _jsx(CommandResultBlock, { command: part.command, content: part.content, isError: part.isError });
7565
- case 'error':
7566
- return _jsx(AssistantTurnErrorNotice, { text: part.text, isCredits: part.isCredits });
7567
- case 'turn-status':
7568
- return (_jsx(AssistantTurnStatusNotice, { status: part.status, ...(part.status === 'interrupted' && conversationId
7569
- ? {
7570
- onResume: () => {
7571
- void getAgentPlatformBridge()?.agent?.resumeTurn?.({
7572
- conversationId,
7573
- // A message id IS the turn's requestId; the
7574
- // permission projection relies on the same
7575
- // identity to attach prompts to a turn.
7576
- requestId: msg.id,
7577
- });
7578
- },
7579
- }
7580
- : {}) }));
7581
- case 'thinking':
7582
- return _jsx(ThinkingBlock, { thinking: part.text, isThinking: part.active });
7583
- case 'text':
7584
- return (_jsx("div", { className: "min-w-0", children: _jsx("div", { className: "max-w-full", children: _jsx("div", { className: "agent-markdown min-w-0 text-[13px] leading-relaxed text-white/70", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], urlTransform: agentMarkdownUrlTransform, components: {
7585
- p: ({ children }) => _jsx("div", { className: "mb-2 last:mb-0", children: children }),
7586
- pre: ({ children }) => _jsx(_Fragment, { children: children }),
7587
- a: ({ href, children }) => (_jsx(AgentMarkdownLink, { href: href, fileReferences: messageFileRefs, onOpenEditorPath: props.onOpenEditorPath, allowedDirectories: effectiveAllowedDirectories, conversationId: conversationId || undefined, children: children })),
7588
- code: ({ className, children, node, ...codeProps }) => {
7589
- const isBlock = className || (node?.position && node.position.start?.line !== node.position.end?.line);
7590
- if (isBlock) {
7591
- return _jsx(CodeBlock, { className: className, ...codeProps, children: children });
7592
- }
7593
- const inlineText = React.Children.toArray(children).join('');
7594
- const fileRef = props.onOpenEditorPath
7595
- ? findMessageFileReference(inlineText, messageFileRefs)
7596
- : null;
7597
- if (fileRef) {
7598
- return (_jsx("button", { type: "button", onClick: () => props.onOpenEditorPath?.(fileRef.path), className: "relative -top-px inline-flex items-center align-middle whitespace-nowrap px-1.5 py-0.5 rounded-[3px] bg-black/30 border border-white/[0.04] text-[12px] leading-none font-mono text-white/45 hover:text-white/78 hover:border-white/[0.12] transition-colors cursor-pointer", children: children }));
7599
- }
7600
- return _jsx("code", { className: "px-1.5 py-0.5 rounded-[3px] bg-black/30 border border-white/[0.04] text-[12px] font-mono text-white/45", ...codeProps, children: children });
7601
- },
7602
- img: ({ src, alt, node: _node, ...imageProps }) => (_jsx("a", { href: src, onClick: (e) => { e.preventDefault(); if (src)
7603
- getAgentPlatformBridge()?.window?.openExternal(src); }, className: "inline-block max-w-full my-2 rounded-md overflow-hidden border border-white/[0.06] hover:border-white/[0.12] transition-colors cursor-pointer", children: _jsx("img", { src: src, alt: alt || '', className: "max-w-full max-h-[300px] object-contain", referrerPolicy: "no-referrer", loading: "lazy", ...imageProps }) }))
7604
- }, children: prepareAgentMarkdown(part.text, messageFileRefs) }) }) }) }));
7605
- case 'tool-calls':
7606
- return (_jsx(ToolCallsBlock, { calls: part.calls, requestId: msg.id, onAddApprovalRule: props.onAddApprovalRule, onOpenEditorPath: props.onOpenEditorPath, onOpenEditorDiff: props.onOpenEditorDiff, onPeekEditorPath: props.onPeekEditorPath, onPeekEditorDiff: props.onPeekEditorDiff, onChooseDirectorySelect: handleChooseDirectorySelect, onExpandIntoView: keepExpandedToolAboveComposer, workspacePlacementCard: workspacePlacementCard }));
7607
- case 'loading':
7608
- return (_jsx(ThinkingIndicator, { mode: msg.showThinking === false ? 'response' : 'reasoning', showDots: false }));
7609
- default:
7610
- return null;
7611
- }
7612
- })() }, `${msg.id}_${laneIdx}_${part.kind}`));
7613
- }) }), showToolbar && !deferToolbarToBlockedPrompt && (_jsxs("div", { className: "mt-0 grid grid-cols-[12px_minmax(0,1fr)] gap-[6px] opacity-0 group-hover/ast:opacity-100 transition-opacity", children: [_jsxs("div", { className: "relative flex justify-center", children: [_jsx("span", { className: "absolute top-[2px] bottom-[10px] w-px -translate-x-[0.5px] bg-[var(--xeno-border-subtle)]" }), _jsx("span", { className: "relative mt-[4px] inline-flex h-3 w-3 items-center justify-center", children: _jsx("span", { className: "block h-1.5 w-1.5 rounded-[2px] bg-[var(--xeno-marker-solid)]" }) })] }), _jsxs("div", { className: "flex items-center gap-1 flex-wrap min-w-0", children: [_jsx("span", { className: "text-[9px] text-white/15 mr-1", children: new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }), !!displayMessageModel && (_jsx("span", { className: "text-[9px] text-white/20", title: `Model used for this message: ${messageModel}`, children: displayMessageModel })), _jsx(DiffCopyButton, { getText: () => msg.content, title: "Copy message" })] })] }))] })) }) }, msg.id));
7416
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "xa-vr" }), _jsx("button", { type: "button", className: "xa-b", "aria-label": "Previous branch", onClick: () => switchBranch(conversationId, msg.id, 'prev'), disabled: isBusy, children: _jsx("span", { style: { display: 'inline-flex', transform: 'scaleX(-1)' }, children: _jsx(Glyph, { name: "chevron-right", size: 11 }) }) }), _jsx("span", { className: "xa-t xa-num", children: `${branchInfo.current}/${branchInfo.total}` }), _jsx("button", { type: "button", className: "xa-b", "aria-label": "Next branch", onClick: () => switchBranch(conversationId, msg.id, 'next'), disabled: isBusy, children: _jsx(Glyph, { name: "chevron-right", size: 11 }) })] }));
7417
+ })()] })] })) : (_jsx(TranscriptTurn, { msg: msg, stepsMode: stepsMode, actions: transcriptActionsFor(msg, msgIdx), renderMarkdown: (text) => renderAssistantMarkdown(text, messageFileRefs), tasks: tasksForMessage(msg), model: displayMessageModel ?? undefined, latest: msgIdx === runtimeProjectedMessages.length - 1 }))] }, msg.id));
7614
7418
  }), blockedNarrationVisible && (_jsx("div", { className: "flex justify-start", children: _jsx("div", { className: "w-full", children: _jsx(ThinkingIndicator, { mode: "response" }) }) })), blockedPromptVisible && (_jsx("div", { className: "flex justify-start -mt-0.5", children: _jsx("div", { className: "w-full", children: _jsxs("div", { className: "-mx-1.5 rounded-[4px] border border-white/[0.08] bg-white/[0.02] px-3 py-2 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-[12px] text-white/50 whitespace-nowrap overflow-hidden text-ellipsis flex-1 min-w-0", children: blockedFileActionPrompt.reason === 'capability'
7615
7419
  ? 'Enable file writing for this agent before creating this file?'
7616
7420
  : 'Choose a directory for this conversation before continuing?' }), _jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [_jsx("button", { onClick: () => {
@@ -7627,7 +7431,9 @@ export function AgentView(props) {
7627
7431
  ? 'Working...'
7628
7432
  : blockedFileActionPrompt.reason === 'capability'
7629
7433
  ? 'Enable and continue'
7630
- : 'Choose directory and continue' })] })] }) }) })), _jsx("div", { ref: messagesEndRef })] })) }), renderWorkbenchPanel && sessionPanelOpen && (_jsx("div", { className: "absolute inset-x-0 bottom-[96px] z-[3] px-6", children: _jsxs("div", { ref: sessionWorkbenchRef, className: "xeno-panel-shell xeno-border-subtle w-full max-w-[820px] mx-auto rounded-[6px] border overflow-hidden", children: [_jsxs("div", { className: "xeno-border-subtle h-9 border-b px-3 flex items-center justify-between gap-3", children: [_jsx("div", { className: "flex items-center gap-1.5", children: ['trace', 'output', 'problems', 'terminal'].map((tab) => (_jsx("button", { onClick: () => patchSessionUi({ panelOpen: true, panelTab: tab }), className: `h-6 px-2.5 rounded-[4px] text-[10px] uppercase tracking-[0.12em] transition-colors ${sessionPanelTab === tab
7434
+ : 'Choose directory and continue' })] })] }) }) }))] })) }), renderWorkbenchPanel && sessionPanelOpen && (
7435
+ // floats just above the dock, which is now in the frame's flow rather than overlaid on it
7436
+ _jsx("div", { className: "absolute inset-x-0 z-[3] px-6", style: { bottom: Math.max(footerDockHeight, 0) + 8 }, children: _jsxs("div", { ref: sessionWorkbenchRef, className: "xeno-panel-shell xeno-border-subtle w-full max-w-[820px] mx-auto rounded-[6px] border overflow-hidden", children: [_jsxs("div", { className: "xeno-border-subtle h-9 border-b px-3 flex items-center justify-between gap-3", children: [_jsx("div", { className: "flex items-center gap-1.5", children: ['trace', 'output', 'problems', 'terminal'].map((tab) => (_jsx("button", { onClick: () => patchSessionUi({ panelOpen: true, panelTab: tab }), className: `h-6 px-2.5 rounded-[4px] text-[10px] uppercase tracking-[0.12em] transition-colors ${sessionPanelTab === tab
7631
7437
  ? 'bg-[var(--xeno-accent-soft)] text-[color:var(--xeno-ink-primary)]'
7632
7438
  : 'xeno-text-faint hover:text-[color:var(--xeno-ink-secondary)] hover:bg-[var(--xeno-bg-subtle)]'}`, children: tab }, tab))) }), _jsx("button", { onClick: () => patchSessionUi({ panelOpen: false }), className: "text-[10px] xeno-text-faint hover:text-[color:var(--xeno-ink-secondary)] transition-colors", children: "Close" })] }), _jsxs("div", { className: "h-[240px] min-h-0", children: [sessionPanelTab === 'trace' && (_jsxs("div", { className: "h-full grid grid-cols-[240px_minmax(0,1fr)]", children: [_jsx("div", { className: "border-r border-white/[0.06] overflow-y-auto", children: traceMessages.length === 0 ? (_jsx("div", { className: "px-3 py-3 text-[11px] text-white/28", children: "No traced turns yet." })) : (traceMessages.map((message) => {
7633
7439
  const toolCount = message.toolCalls?.length || 0;
@@ -7640,173 +7446,205 @@ export function AgentView(props) {
7640
7446
  event.preventDefault();
7641
7447
  void handleRunTerminalCommand();
7642
7448
  }
7643
- }, placeholder: terminalReady ? 'Run a safe workspace command...' : 'Open a folder to use the terminal', disabled: !terminalCapabilityEnabled || isRunningTerminalCommand, className: "h-8 rounded-[4px] border border-white/[0.06] bg-black/20 px-2.5 text-[11px] text-white/78 placeholder:text-white/18 outline-none focus:border-white/[0.12] disabled:opacity-35" }), _jsx("button", { onClick: () => { void handleRunTerminalCommand(); }, disabled: !terminalReady || isRunningTerminalCommand || !terminalDraftInput.trim(), className: "h-8 px-3 rounded-[4px] bg-white/[0.08] hover:bg-white/[0.12] text-[10px] uppercase tracking-[0.12em] text-white/72 transition-colors disabled:opacity-35", children: isRunningTerminalCommand ? 'Running' : 'Run' })] }) })] }))] })] }) })), _jsxs("div", { ref: footerDockRef, className: "xeno-agent-footer-dock absolute inset-x-0 bottom-0 z-[30] isolate px-6 pb-5 pt-3", children: [_jsx("div", { ref: sessionPanelRef, className: "absolute bottom-full right-6 z-[45] mb-1", children: _jsx(SdkSessionPanel, { open: sessionDrawerOpen, onOpenChange: setSessionDrawerOpen, workingDirectory: sdkSessionWorkingDirectory, attachedId: sessionUi?.sdkSessionId, sessions: sdkSessions, loading: sdkSessionsLoading, busy: sdkConfirmation.busy || sdkSessionBindingBusy || isBusy || isPreparingModel, status: sdkSessionStatus, unavailableReason: !conversationId ? 'Select a conversation before managing SDK sessions.' : undefined, canList: !!getOptionalAgentPlatformBridge()?.agent?.listSdkSessions, canAttach: !!getOptionalAgentPlatformBridge()?.agent?.attachSdkSession, canDetach: !!getOptionalAgentPlatformBridge()?.agent?.detachSdkSession, canDelete: !!getOptionalAgentPlatformBridge()?.agent?.deleteSdkSession, canClean: !!getOptionalAgentPlatformBridge()?.agent?.cleanSdkSessions, onReload: () => { void loadSdkSessions(); }, onAttach: sessionId => { void handleAttachSdkSession(sessionId); }, onDetach: () => { void handleDetachSdkSession(); }, onDelete: sessionId => { void handleDeleteSdkSession(sessionId); }, onClean: () => { void handleCleanSdkSessions(); } }) }), !isModelReady && (_jsxs("div", { className: "w-full max-w-[820px] mx-auto mb-2 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-[11px] text-white/35", children: localRuntimeNeedsStart
7644
- ? `xeno-rt is off. Click xeno-rt in the footer before loading ${localModelName}.`
7645
- : isPreparingModel
7646
- ? (isStartingLocalRuntime ? 'Starting xeno-rt...' : `Loading local model ${localModelName}...`)
7647
- : `Local model failed to load: ${modelWarmupError || 'Unknown error'}` }), !isPreparingModel && !localRuntimeNeedsStart && (_jsx("button", { onClick: () => setWarmupAttempt(v => v + 1), className: "px-2 py-1 rounded bg-white/[0.07] hover:bg-white/[0.12] text-[10px] text-white/70 transition-colors", children: "Retry" }))] })), toolStatus && (_jsx("div", { className: "w-full max-w-[820px] mx-auto mb-2", children: _jsx("p", { className: "text-[11px] text-white/35", children: toolStatus }) })), (cloudModelUnavailable || cloudModelDegraded) && (_jsx("div", { className: "w-full max-w-[820px] mx-auto mb-2 flex items-center justify-between gap-3", children: _jsx("p", { className: `text-[11px] ${cloudModelUnavailable ? 'text-[#ff8f8f]' : 'text-[#ffd38a]'}`, children: cloudModelUnavailable
7648
- ? `${cloudModelName(actualModelId)} is unavailable. ${cloudModelStatusReason || ''}`.trim()
7649
- : `${cloudModelName(actualModelId)} is using the fallback path. ${cloudModelStatusReason || ''}`.trim() }) })), taskCounts.total > 0 && (_jsx("div", { className: "relative z-[2] w-full max-w-[760px] mx-auto mb-0", children: _jsxs("div", { className: "xeno-border-subtle rounded-t-md rounded-b-none border bg-[#111111]", children: [_jsxs("button", { type: "button", onClick: () => patchSessionUi({ tasksCollapsed: !tasksCollapsed }), "aria-expanded": !tasksCollapsed, className: `w-full flex items-center justify-between gap-3 px-3 py-1.5 text-left transition-colors hover:bg-white/[0.03] ${tasksCollapsed ? '' : 'border-b xeno-border-subtle'}`, children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("span", { className: "text-[9px] uppercase tracking-[0.12em] xeno-text-faint whitespace-nowrap", children: "Tasks" }), _jsxs("span", { className: "text-[9px] xeno-text-muted tabular-nums whitespace-nowrap", children: [taskCounts.pending, "/", taskCounts.total, " pending", taskCounts.inProgress > 0 ? ` - ${taskCounts.inProgress} active` : '', taskCounts.blocked > 0 ? ` - ${taskCounts.blocked} blocked` : ''] })] }), _jsx(ChevronDown, { className: `w-3 h-3 xeno-text-faint transition-transform duration-200 ${tasksCollapsed ? '' : 'rotate-180'}` })] }), _jsx("div", { className: `grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-out ${tasksCollapsed ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'}`, children: _jsx("div", { className: "min-h-0", children: _jsx("div", { className: "max-h-28 overflow-auto px-2 py-1.5 space-y-0.5 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/12 [&::-webkit-scrollbar-thumb]:rounded-[1px]", children: conversationTasks.map((task) => (_jsxs("div", { className: "w-full rounded-[4px] px-2 py-1 text-left flex items-start gap-2", children: [_jsx("span", { className: `mt-0.5 inline-flex h-3 w-3 items-center justify-center rounded-[2px] border text-[10px] ${task.status === 'done'
7650
- ? 'border-emerald-400/35'
7651
- : task.status === 'in_progress'
7652
- ? 'border-white/35 bg-white/15'
7653
- : task.status === 'blocked'
7654
- ? 'border-amber-400/35 bg-amber-400/14'
7655
- : 'border-white/18'}`, children: task.status === 'done' ? (_jsx(Check, { className: "w-3 h-3 text-emerald-300" })) : task.status === 'in_progress' ? (_jsx("span", { className: "block h-[2px] w-[6px] rounded-[1px] bg-white/60" })) : task.status === 'blocked' ? (_jsx("span", { className: "block h-1.5 w-1.5 rounded-[2px] bg-amber-300/70" })) : (_jsx(Square, { className: "w-3 h-3 text-white/45" })) }), _jsx("span", { className: `text-[10px] leading-[1.35] ${task.status === 'done'
7656
- ? 'line-through text-white/34'
7657
- : task.status === 'in_progress'
7658
- ? 'text-white/80'
7659
- : task.status === 'blocked'
7660
- ? 'text-amber-100/76'
7661
- : 'text-white/62'}`, children: task.text })] }, task.id))) }) }) })] }) })), _jsxs("div", { className: `w-full max-w-[820px] mx-auto relative ${taskCounts.total > 0 ? '-mt-px z-[3]' : 'z-[2]'}`, children: [showLocalRuntimePrompt && localRuntimeNeedsStart && (_jsx("div", { className: "absolute right-0 bottom-[48px] z-40 flex flex-col items-end", children: _jsxs("div", { className: "xeno-elevated-shell xeno-border-subtle rounded-md border px-3 py-2.5", children: [_jsx("p", { className: "text-[11px] text-white/70", children: "Start xeno-rt before loading this local model?" }), _jsxs("div", { className: "mt-2 flex items-center justify-end gap-2", children: [_jsx("button", { onClick: () => setShowLocalRuntimePrompt(false), className: "h-6 px-2 rounded text-[10px] text-white/35 hover:text-white/60 transition-colors cursor-pointer", children: "Not now" }), _jsx("button", { onClick: () => { void handleStartLocalRuntime(); }, disabled: isStartingLocalRuntime, className: "h-6 px-2 rounded bg-white/[0.08] hover:bg-white/[0.12] text-[10px] text-white/75 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed", children: isStartingLocalRuntime ? 'Starting...' : 'Start xeno-rt' })] })] }) })), runtimeMode === 'acp' && acpCliListOpen && (_jsxs("div", { ref: acpCliListRef, role: "listbox", className: "xeno-elevated-shell xeno-border-subtle absolute left-3 bottom-[44px] z-50 w-[220px] border rounded-md max-h-[280px] overflow-y-auto", children: [_jsx("div", { className: "px-3 pt-2 pb-1 text-[9px] uppercase tracking-[0.14em] text-white/25", children: "ACP CLI" }), selectableAcpAgents.length > 0 ? (selectableAcpAgents.map(agent => {
7662
- const selected = agent.id === selectedAcpAgentConfigId;
7663
- const fixture = agent.certification?.status === 'fixture_only';
7664
- const ready = fixture || (agent.certification?.status === 'certified' && agent.providerReview?.runnable === true);
7665
- return (_jsxs("button", { onClick: () => { selectAcpRuntime(agent.id, selected ? acpProviderModel : undefined); }, role: "option", "aria-selected": selected, className: `w-full flex items-center justify-between gap-2 px-3 py-1.5 text-left transition-colors cursor-pointer ${selected ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`, children: [_jsx("span", { className: `min-w-0 flex-1 text-[11px] truncate ${selected ? 'text-white/70' : 'text-white/40'}`, children: acpAgentDisplayName(agent) }), _jsx("span", { className: `text-[8px] uppercase tracking-wide ${ready ? 'text-[#96e59d]/70' : 'text-[#ffd38a]/70'}`, children: ready ? 'ready' : 'check' })] }, agent.id));
7666
- })) : (_jsx("div", { className: "px-3 py-2.5 text-[10px] text-white/25 text-center", children: "No available ACP CLI providers. Enable or repair one in Settings." }))] })), runtimeMode === 'acp' && acpModelListOpen && (_jsxs("div", { ref: acpModelListRef, className: "xeno-elevated-shell xeno-border-subtle absolute left-[170px] bottom-[44px] z-50 flex max-h-[280px] w-[220px] flex-col overflow-hidden rounded-md border", children: [renderModelSearch(), _jsxs("div", { role: "listbox", className: "min-h-0 overflow-y-auto", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pt-2 pb-1 text-[9px] uppercase tracking-[0.14em] text-white/25", children: [_jsx("span", { children: "ACP model" }), sharedProviderCatalogRefreshing && !acpModelsLoading && _jsx("span", { children: "Refreshing..." })] }), _jsx("button", { onClick: () => { if (selectedAcpAgentConfigId)
7667
- selectAcpRuntime(selectedAcpAgentConfigId, undefined); }, disabled: !selectedAcpAgentConfigId, role: "option", "aria-selected": !acpProviderModel, className: `w-full flex items-center justify-between px-3 py-1.5 text-left transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed ${!acpProviderModel ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`, children: _jsx("span", { className: `text-[11px] truncate ${!acpProviderModel ? 'text-white/70' : 'text-white/40'}`, children: "CLI default" }) }), acpModelsLoading ? (_jsx("div", { className: "px-3 py-2.5 text-[10px] text-white/25 text-center", children: "Loading models..." })) : acpModelOptions.length > 0 ? (filteredAcpModelOptions.length > 0 ? filteredAcpModelOptions.map((entry) => {
7668
- const selected = entry.selectionKey === (selectedAcpEffortFamily?.baseId || acpProviderModel);
7669
- return (_jsxs("button", { onClick: () => { if (selectedAcpAgentConfigId)
7670
- selectAcpRuntime(selectedAcpAgentConfigId, entry.id); }, disabled: !selectedAcpAgentConfigId || !entry.available, role: "option", "aria-selected": selected, className: `w-full flex items-center justify-between px-3 py-1.5 text-left transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed ${selected ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`, title: entry.reason || undefined, children: [_jsx("span", { className: `min-w-0 flex-1 text-[11px] truncate ${selected ? 'text-white/70' : 'text-white/40'}`, children: entry.name }), !entry.available && _jsx("span", { className: "ml-2 flex-shrink-0 text-[8px] uppercase tracking-wide text-[#ff8f8f]", children: "Unavailable" })] }, entry.selectionKey));
7671
- }) : (_jsx("div", { className: "px-3 py-2.5 text-[10px] text-white/25 text-center", children: "No matching models" }))) : (_jsx("div", { className: "px-3 py-2.5 text-[10px] text-white/25 text-center", children: selectedAcpAgentConfigId ? (acpModelMessage || 'No preset models for this ACP CLI') : 'Select a CLI first' }))] })] })), modelListOpen && runtimeMode !== 'acp' && (_jsxs("div", { ref: modelListRef, className: "xeno-elevated-shell xeno-border-subtle absolute left-3 bottom-[44px] z-50 flex max-h-[340px] w-[280px] flex-col overflow-hidden rounded-md border", children: [renderModelSearch(), _jsx("div", { role: "listbox", className: "min-h-0 overflow-y-auto", children: runtimeMode === 'cloud' ? (sharedProviderCatalogRefreshing ? (_jsx("div", { className: "px-3 py-3 text-[10px] text-white/25 text-center", children: "Refreshing available models..." })) : cloudModels.length > 0 ? (filteredCloudModelOptions.length > 0 ? filteredCloudModelOptions.map(entry => ((() => {
7672
- const modelMeta = cloudModels.find(cm => cm.id === entry.id);
7673
- const degraded = modelMeta?.availability === 'degraded';
7674
- const selected = entry.selectionKey === (selectedCloudEffortFamily?.baseId || selectedModel);
7675
- return (_jsxs("button", { onClick: () => { setInferenceMode('cloud'); setSelectedModel(entry.id); setModelListOpen(false); setEffortListOpen(false); }, role: "option", "aria-selected": selected, className: `w-full flex items-center justify-between px-3 py-1.5 text-left transition-colors cursor-pointer ${selected ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`, title: modelMeta?.availabilityReason || undefined, children: [_jsxs("div", { className: "min-w-0 flex-1 flex items-center gap-2", children: [_jsx("span", { className: `text-[11px] truncate ${selected ? 'text-white/70' : 'text-white/40'}`, children: entry.name }), degraded && _jsx("span", { className: "text-[8px] uppercase tracking-wide text-[#ffd38a]", children: "Fallback" })] }), _jsx("span", { className: "text-[9px] text-white/20 flex-shrink-0 ml-2 tabular-nums", children: (() => { const m = cloudModels.find(cm => cm.id === entry.id); return m?.creditsPerKToken != null ? `${m.creditsPerKToken}/1k` : ''; })() })] }, entry.id));
7676
- })())) : (_jsx("div", { className: "px-3 py-3 text-[10px] text-white/25 text-center", children: "No matching models" }))) : (_jsx("div", { className: "px-3 py-3 text-[10px] leading-relaxed text-white/35 text-center", children: cloudCatalogUnavailableReason }))) : (_jsxs(_Fragment, { children: [localModels.length > 0 ? (filteredLocalModels.length > 0 ? filteredLocalModels.map(m => {
7677
- const selected = m.id === (selectedLocalEffortFamily?.baseId || selectedModel);
7678
- return (_jsxs("button", { onClick: () => { setInferenceMode('local'); setSelectedModel(m.id); setModelListOpen(false); setEffortListOpen(false); }, role: "option", "aria-selected": selected, className: `w-full flex items-center gap-2 px-3 py-1.5 text-left transition-colors cursor-pointer ${selected ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`, children: [_jsx("span", { className: `text-[11px] truncate ${selected ? 'text-white/70' : 'text-white/40'}`, children: m.name }), m.parameterSize && _jsx("span", { className: "text-[8px] text-white/20 flex-shrink-0 ml-auto", children: m.parameterSize })] }, m.id));
7679
- }) : (_jsx("div", { className: "px-3 py-2.5 text-[10px] text-white/25 text-center", children: "No matching models" }))) : (_jsx("div", { className: "px-3 py-2.5 text-[10px] text-white/25 text-center", children: "No local models installed" })), _jsx("button", { onClick: () => { setModelListOpen(false); goToModelsMarketplace(); }, className: "w-full flex items-center justify-center gap-1.5 px-3 py-2 border-t border-white/[0.06] text-[10px] text-white/35 hover:text-white/60 hover:bg-white/[0.04] transition-colors cursor-pointer", children: "Browse models" })] })) })] })), _jsxs("div", { className: "space-y-0", children: [pendingTurns.length > 0 && (_jsxs("div", { className: "xeno-border-subtle rounded-t-md border border-b-0 bg-[#111111] overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2 border-b xeno-border-subtle", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("span", { className: "text-[10px] uppercase tracking-[0.14em] xeno-text-faint whitespace-nowrap", children: "Queued Messages" }), _jsx("span", { className: "text-[10px] xeno-text-muted tabular-nums whitespace-nowrap", children: pendingTurns.length })] }), _jsx("button", { onClick: () => patchSessionUi({ panelOpen: true }), className: "text-[10px] xeno-text-faint hover:text-[color:var(--xeno-ink-secondary)] transition-colors whitespace-nowrap cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", children: "Manage queue" })] }), _jsxs("div", { className: "divide-y xeno-border-subtle", children: [pendingTurns.slice(0, 3).map((turn) => (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [_jsx("span", { className: "inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] bg-white/[0.04] px-1.5 text-[10px] xeno-text-faint tabular-nums", children: new Date(turn.queuedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }), _jsx("span", { className: "min-w-0 flex-1 text-[12px] leading-4 xeno-text-secondary truncate", children: turn.content }), _jsx("button", { onClick: () => handleRemoveQueuedConversationTurn(turn.id), className: "text-[10px] xeno-text-faint hover:text-[color:var(--xeno-ink-secondary)] transition-colors whitespace-nowrap cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", title: "Remove queued message", children: "Remove" })] }, turn.id))), pendingTurns.length > 3 && (_jsxs("div", { className: "px-3 py-2 text-[11px] xeno-text-faint", children: [pendingTurns.length - 3, " more queued"] }))] })] })), _jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", "data-agent-workspace-binding": workspaceBinding.mode, children: [_jsxs("div", { className: "inline-flex items-center rounded-md border border-white/[0.08] bg-white/[0.025] p-0.5 text-[11px]", children: [_jsx("button", { type: "button", onClick: () => { void handleSelectWorkspaceMode('chat'); }, disabled: isBusy || !!props.externalInputLockReason || isSelectingConversationWorkspace, className: `rounded px-2.5 py-1 font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${workspaceBinding.mode !== 'agent'
7680
- ? 'bg-white/[0.12] text-white shadow-sm'
7681
- : 'text-white/45 hover:bg-white/[0.04] hover:text-white/70'}`, "aria-pressed": workspaceBinding.mode !== 'agent', children: "Chat" }), _jsx("button", { type: "button", onClick: () => { void handleSelectWorkspaceMode('agent'); }, disabled: isBusy || !!props.externalInputLockReason || isSelectingConversationWorkspace, className: `rounded px-2.5 py-1 font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${workspaceBinding.mode === 'agent'
7682
- ? 'bg-white/[0.12] text-white shadow-sm'
7683
- : 'text-white/45 hover:bg-white/[0.04] hover:text-white/70'}`, "aria-pressed": workspaceBinding.mode === 'agent', children: isSelectingConversationWorkspace && workspaceBinding.mode !== 'agent' ? 'Choosing…' : 'Agent' })] }), workspaceBinding.mode === 'agent' ? (_jsxs("div", { className: "flex min-w-0 items-center gap-2 text-[10px]", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-white/45", title: workspaceBinding.rootDirectory || workspaceBinding.allowedDirectories[0], children: workspaceBinding.rootDirectory || workspaceBinding.allowedDirectories[0] }), _jsx("button", { type: "button", onClick: () => { void chooseConversationWorkspace(); }, disabled: isSelectingConversationWorkspace || isBusy || !!props.externalInputLockReason, className: "shrink-0 text-white/35 transition-colors hover:text-white/70 disabled:cursor-not-allowed disabled:opacity-30", children: "Change folder" })] })) : (_jsx("span", { className: "min-w-0 truncate text-[10px] text-white/28", children: "Chat only \u00B7 no project file access" })), activeMcpList.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5 pl-2 border-l border-white/[0.08]", children: [_jsx("span", { className: "text-[10px] text-white/35 font-mono", children: "MCP:" }), activeMcpList.map((mcp) => {
7684
- const statusDot = mcp.status === 'connected'
7685
- ? 'bg-emerald-400'
7686
- : mcp.status === 'auth_required'
7687
- ? 'bg-amber-400 animate-pulse'
7688
- : 'bg-rose-400';
7689
- const statusTitle = `${mcp.server}: ${mcp.status}${mcp.error ? ` (${mcp.error})` : ''}`;
7690
- return (_jsxs("button", { type: "button", onClick: () => { void handleExecuteCommandOrPrompt(`/mcp ${mcp.server}`); }, className: "inline-flex items-center gap-1 rounded bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-mono text-white/60 hover:bg-white/[0.08] transition-colors cursor-pointer", title: statusTitle, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${statusDot}` }), _jsx("span", { children: mcp.server })] }, mcp.server));
7691
- })] }))] }), props.externalInputLockReason && (_jsx("div", { className: "mb-2 rounded-md border border-amber-400/20 bg-amber-400/[0.06] px-3 py-2 text-[11px] text-amber-100/70", children: props.externalInputLockReason })), activeNotifsList.length > 0 && (_jsx(AgentNotificationBanner, { notifications: activeNotifsList, onDismiss: (id) => {
7692
- if (conversationId)
7693
- dismissNotification(conversationId, id);
7694
- }, onAction: (action, cmd, url) => {
7695
- if (action === 'command' && cmd) {
7449
+ }, placeholder: terminalReady ? 'Run a safe workspace command...' : 'Open a folder to use the terminal', disabled: !terminalCapabilityEnabled || isRunningTerminalCommand, className: "h-8 rounded-[4px] border border-white/[0.06] bg-black/20 px-2.5 text-[11px] text-white/78 placeholder:text-white/18 outline-none focus:border-white/[0.12] disabled:opacity-35" }), _jsx("button", { onClick: () => { void handleRunTerminalCommand(); }, disabled: !terminalReady || isRunningTerminalCommand || !terminalDraftInput.trim(), className: "h-8 px-3 rounded-[4px] bg-white/[0.08] hover:bg-white/[0.12] text-[10px] uppercase tracking-[0.12em] text-white/72 transition-colors disabled:opacity-35", children: isRunningTerminalCommand ? 'Running' : 'Run' })] }) })] }))] })] }) })), _jsxs("div", { ref: (node) => {
7450
+ ;
7451
+ dockRef.current = node;
7452
+ footerDockRef.current = node;
7453
+ }, className: "xa-dock", "data-dock": true, "data-agent-workspace-binding": workspaceBinding.mode, onContextMenu: (event) => {
7454
+ if (event.target.closest('textarea, input, .xa-menu'))
7455
+ return;
7456
+ event.preventDefault();
7457
+ setMoreOpen(true);
7458
+ setPermOpen(false);
7459
+ setLaneOpen(false);
7460
+ }, onKeyDown: (event) => {
7461
+ if (event.shiftKey && event.key === 'F10') {
7462
+ event.preventDefault();
7463
+ setMoreOpen(true);
7464
+ setPermOpen(false);
7465
+ setLaneOpen(false);
7466
+ }
7467
+ }, children: [sessionDrawerOpen && _jsx("div", { ref: sessionPanelRef, className: "absolute bottom-full right-6 z-[45] mb-1", children: _jsx(SdkSessionPanel, { trigger: "none", open: sessionDrawerOpen, onOpenChange: setSessionDrawerOpen, workingDirectory: sdkSessionWorkingDirectory, attachedId: sessionUi?.sdkSessionId, sessions: sdkSessions, loading: sdkSessionsLoading, busy: sdkConfirmation.busy || sdkSessionBindingBusy || isBusy || isPreparingModel, status: sdkSessionStatus, unavailableReason: !conversationId ? 'Select a conversation before managing SDK sessions.' : undefined, canList: !!getOptionalAgentPlatformBridge()?.agent?.listSdkSessions, canAttach: !!getOptionalAgentPlatformBridge()?.agent?.attachSdkSession, canDetach: !!getOptionalAgentPlatformBridge()?.agent?.detachSdkSession, canDelete: !!getOptionalAgentPlatformBridge()?.agent?.deleteSdkSession, canClean: !!getOptionalAgentPlatformBridge()?.agent?.cleanSdkSessions, onReload: () => { void loadSdkSessions(); }, onAttach: sessionId => { void handleAttachSdkSession(sessionId); }, onDetach: () => { void handleDetachSdkSession(); }, onDelete: sessionId => { void handleDeleteSdkSession(sessionId); }, onClean: () => { void handleCleanSdkSessions(); } }) }), _jsxs(ComposerDock.Provider, { value: dockEl, children: [_jsxs("div", { className: "xa-dock-inner", children: [!isModelReady && (_jsx(Notice, { glyph: localRuntimeNeedsStart ? 'monitor' : 'clock', title: localRuntimeNeedsStart
7468
+ ? 'xeno-rt is not running'
7469
+ : isPreparingModel
7470
+ ? (isStartingLocalRuntime ? 'Starting xeno-rt' : `Loading ${localModelName}`)
7471
+ : 'The local model did not load', sub: localRuntimeNeedsStart
7472
+ ? `${localModelName} runs on this computer, and the runtime that serves it is off.`
7473
+ : isPreparingModel
7474
+ ? 'The first turn waits for it; nothing is lost.'
7475
+ : (modelWarmupError || 'No reason was reported.'), ...(localRuntimeNeedsStart
7476
+ ? { action: isStartingLocalRuntime ? 'Starting…' : 'Start xeno-rt', onAction: () => { void handleStartLocalRuntime(); } }
7477
+ : !isPreparingModel
7478
+ ? { action: 'Try again', onAction: () => setWarmupAttempt(v => v + 1) }
7479
+ : {}), center: isPreparingModel })), toolStatus && !cloudOutageStated && _jsx(Notice, { glyph: "zap", title: toolStatus, center: true, onDismiss: () => setToolStatus('') }), (cloudModelUnavailable || cloudModelDegraded) && (_jsx(Notice, { glyph: "cloud", tone: cloudModelUnavailable ? 'bad' : 'warn', title: cloudModelUnavailable
7480
+ ? (cloudNeedsSignIn ? 'Cloud needs you signed in' : cloudCatalogState?.state === 'loading' ? 'Loading models…' : cloudCatalogRecovering ? 'Model catalog is reconnecting' : cloudCatalogState?.failure?.code === 'forbidden' ? 'Model access is restricted' : 'Cloud is unavailable')
7481
+ : cloudCatalogState?.state === 'stale' ? 'Refreshing the model catalog' : `${cloudModelName(actualModelId)} is on the fallback path`,
7482
+ // 🔴 The second line says what FOLLOWS, never what the first line already said.
7483
+ // `cloudModelStatusReason` here is "The live XENO model catalog is unavailable" —
7484
+ // the title in other words so it is deliberately NOT the subtitle for the
7485
+ // unavailable case. The degraded case keeps it, because there it carries the one
7486
+ // thing the title does not: which route the turn actually took.
7487
+ sub: cloudModelUnavailable
7488
+ ? (cloudNeedsSignIn
7489
+ // the catalog's own sentence already says what to do; restating it would be the
7490
+ // duplicate-title defect this notice was just rewritten to avoid
7491
+ ? cloudCatalogUnavailableReason
7492
+ : cloudCatalogRecovering
7493
+ ? 'The model list will update automatically when the service responds.'
7494
+ : canRunLocally
7495
+ ? 'Run this turn on your own machine instead.'
7496
+ : 'No local model is installed either. Choose another model, or try again when the catalog returns.')
7497
+ : (cloudModelStatusReason || 'Answers still arrive; the route is not the usual one.'), action: cloudModelUnavailable ? (canRunLocally ? 'Use Local' : cloudNeedsSignIn || cloudCatalogRecovering ? undefined : 'Choose a model') : undefined, onAction: cloudModelUnavailable
7498
+ ? (canRunLocally
7499
+ ? () => { switchToLocalMode(); setToolStatus(null); }
7500
+ : () => { setModelSearchQuery(''); setModelListOpen(true); })
7501
+ : undefined, onDismiss: cloudModelUnavailable ? () => setToolStatus(null) : undefined })), props.externalInputLockReason && (_jsx(Notice, { glyph: "lock", tone: "warn", title: "This conversation is not taking messages", sub: props.externalInputLockReason })), activeNotifsList.length > 0 && (_jsx(AgentNotificationBanner, { notifications: activeNotifsList, onDismiss: (id) => { if (conversationId)
7502
+ dismissNotification(conversationId, id); }, onAction: (action, cmd, url) => {
7503
+ if (action === 'command' && cmd)
7696
7504
  void handleExecuteCommandOrPrompt(cmd);
7697
- }
7698
- else if (action === 'link' && url) {
7505
+ else if (action === 'link' && url)
7699
7506
  window.open(url, '_blank');
7700
- }
7701
- } })), _jsxs("div", { className: `xeno-agent-composer-shell relative border border-white/[0.08] ${pendingTurns.length > 0 ? 'rounded-t-none rounded-b-md' : 'rounded-md'}`, children: [showSlashCommands && (_jsx(SlashCommandAutocomplete, { commands: activeCommandsList, filter: input, selectedIndex: slashCommandIndex, onSelect: (cmdName) => {
7702
- const cmdText = (cmdName.startsWith('/') ? cmdName : `/${cmdName}`) + ' ';
7703
- setInput(cmdText);
7704
- if (textareaRef.current) {
7705
- textareaRef.current.focus();
7706
- }
7707
- }, onClose: () => setSlashCommandsDismissed(true) })), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, onChange: (e) => { void handleAttachmentPick(e); }, className: "hidden" }), pendingAttachments.length > 0 && (_jsx("div", { className: "px-3 pt-3 pb-1", children: _jsx("div", { className: "grid grid-cols-4 gap-2", children: pendingAttachments.map(att => (_jsxs("div", { className: "xeno-app-shell xeno-border-subtle relative rounded overflow-hidden border", children: [_jsx("img", { src: att.dataUrl, alt: att.name, className: "w-full h-16 object-cover" }), _jsx("button", { onClick: () => removePendingAttachment(att.id), className: "absolute top-1 right-1 w-5 h-5 rounded-[4px] bg-black/60 text-white/70 hover:text-white flex items-center justify-center transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", "aria-label": `Remove attachment ${att.name}`, title: `Remove ${att.name}`, children: _jsx(X, { className: "w-3 h-3" }) })] }, att.id))) }) })), _jsx(GhostTextArea, { ref: textareaRef, rows: 2, value: input, onChange: handleInputChange, onKeyDown: handleKeyDown, disabled: inputLocked, placeholder: effectiveGhostText
7708
- ? ''
7709
- : (isModelReady
7710
- ? `Message ${agentName.replace(/\s*powered by .+$/i, '')}...`
7711
- : (localRuntimeNeedsStart
7712
- ? 'Start xeno-rt to use this local model'
7713
- : (isPreparingModel ? `Loading local model ${localModelName}...` : 'Local model is not ready yet'))), effectiveGhostText: effectiveGhostText, onAcceptGhost: handleAcceptGhost }), _jsxs("div", { className: "flex items-center justify-between px-3 pb-2.5 pt-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [AGENT_MODEL_MODE_SWITCH_ENABLED && _jsxs("div", { ref: modeRef, className: "flex items-center", children: [_jsx("button", { onClick: () => { setModeExpanded(v => !v); setModelListOpen(false); setEffortListOpen(false); setAcpCliListOpen(false); setAcpModelListOpen(false); }, disabled: isBusy, className: `w-7 h-7 flex items-center justify-center rounded transition-all duration-200 cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:opacity-30 disabled:cursor-not-allowed ${modeExpanded
7714
- ? 'bg-white/[0.08] text-white/60'
7715
- : 'bg-white/[0.04] hover:bg-white/[0.08] text-white/40 hover:text-white/60'}`, title: `Mode: ${runtimeMode === 'acp' ? 'ACP' : runtimeMode === 'cloud' ? 'Cloud' : 'Local'}`, "aria-label": `Runtime mode: ${runtimeMode === 'acp' ? 'ACP' : runtimeMode === 'cloud' ? 'Cloud' : 'Local'}`, children: runtimeMode === 'acp'
7716
- ? _jsx(ShieldAlert, { className: "w-3.5 h-3.5" })
7717
- : runtimeMode === 'cloud'
7718
- ? _jsx(Cloud, { className: "w-3.5 h-3.5" })
7719
- : _jsx(Monitor, { className: "w-3.5 h-3.5" }) }), _jsxs("div", { className: `flex items-center gap-0.5 overflow-hidden transition-all duration-200 ${modeExpanded ? 'max-w-[210px] ml-1 opacity-100' : 'max-w-0 ml-0 opacity-0'}`, children: [_jsx("button", { onClick: switchToCloudMode, className: `h-7 px-2.5 rounded text-[10px] font-medium tracking-wide whitespace-nowrap transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 ${runtimeMode === 'cloud'
7720
- ? 'bg-white/[0.10] text-white/80'
7721
- : 'bg-white/[0.03] text-white/35 hover:bg-white/[0.06] hover:text-white/60'}`, children: "Cloud" }), _jsx("button", { onClick: switchToLocalMode, className: `h-7 px-2.5 rounded text-[10px] font-medium tracking-wide whitespace-nowrap transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 ${runtimeMode === 'local'
7722
- ? 'bg-white/[0.10] text-white/80'
7723
- : 'bg-white/[0.03] text-white/35 hover:bg-white/[0.06] hover:text-white/60'}`, children: "Local" }), _jsx("button", { onClick: () => { void switchToAcpMode(); }, className: `h-7 px-2.5 rounded text-[10px] font-medium tracking-wide whitespace-nowrap transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 ${runtimeMode === 'acp'
7724
- ? 'bg-white/[0.10] text-white/80'
7725
- : 'bg-white/[0.03] text-white/35 hover:bg-white/[0.06] hover:text-white/60'}`, children: "ACP" })] })] }), modelSelectorVisible && (_jsxs(_Fragment, { children: [runtimeMode === 'acp' && (_jsxs("button", { ref: acpCliBtnRef, onClick: () => { setAcpCliListOpen(v => !v); setAcpModelListOpen(false); setModelListOpen(false); setEffortListOpen(false); setModeExpanded(false); }, disabled: isBusy || modeExpanded, className: "h-7 flex items-center gap-1 px-2 rounded bg-white/[0.04] hover:bg-white/[0.08] text-white/40 hover:text-white/60 transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:opacity-30 disabled:cursor-not-allowed whitespace-nowrap", title: "Select ACP CLI", "aria-label": "Select ACP CLI", "aria-haspopup": "listbox", "aria-expanded": acpCliListOpen, children: [_jsx("span", { className: "text-[10px] max-w-[105px] truncate", children: selectedAcpAgent ? acpAgentDisplayName(selectedAcpAgent) : 'Select CLI' }), _jsx(ChevronDown, { className: `w-3 h-3 flex-shrink-0 transition-transform duration-150 ${acpCliListOpen ? 'rotate-180' : ''}` })] })), _jsxs("button", { ref: runtimeMode === 'acp' ? acpModelBtnRef : modelBtnRef, onClick: () => {
7726
- setModelSearchQuery('');
7727
- if (runtimeMode === 'acp') {
7728
- setAcpModelListOpen(v => !v);
7729
- setModelListOpen(false);
7730
- }
7731
- else {
7732
- setModelListOpen(v => !v);
7733
- setAcpModelListOpen(false);
7734
- }
7735
- setEffortListOpen(false);
7736
- setAcpCliListOpen(false);
7737
- setModeExpanded(false);
7738
- }, disabled: isBusy || modeExpanded || (runtimeMode === 'acp' && !selectedAcpAgentConfigId), className: `h-7 flex items-center gap-1 px-2 rounded transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:opacity-30 disabled:cursor-not-allowed whitespace-nowrap ${(runtimeMode === 'acp' ? selectedAcpAgentConfigId : currentModelName)
7739
- ? 'bg-white/[0.04] hover:bg-white/[0.08] text-white/40 hover:text-white/60'
7740
- : 'bg-white/[0.06] hover:bg-white/[0.10] text-white/25 hover:text-white/45'}`, title: runtimeMode === 'acp' && !selectedAcpAgentConfigId ? 'Select an ACP CLI first' : 'Select model', "aria-label": "Select model", "aria-haspopup": "listbox", "aria-expanded": runtimeMode === 'acp' ? acpModelListOpen : modelListOpen, children: [_jsx("span", { className: "text-[10px] max-w-[140px] truncate", children: runtimeMode === 'acp'
7741
- ? (selectedAcpAgentConfigId
7742
- ? (acpModelsLoading
7743
- ? 'Loading...'
7744
- : acpProviderModelDisplayName)
7745
- : 'Select model')
7746
- : (currentModelName || 'Select model') }), _jsx(ChevronDown, { className: `w-3 h-3 flex-shrink-0 transition-transform duration-150 ${(runtimeMode === 'acp' ? acpModelListOpen : modelListOpen) ? 'rotate-180' : ''}` })] }), _jsxs("button", { type: "button", "aria-pressed": suggestionsAvailable, disabled: !suggestionModelAvailable, title: runtimeMode === 'acp' ? 'Provider-native suggestions are not available through this bridge.' : `Opt in to additional suggestion requests using ${actualModelId}. Cloud requests may incur charges.`, onClick: () => { clearAllSuggestions(); setSuggestionOptInScope(suggestionsAvailable ? null : suggestionScope); }, className: "h-7 rounded bg-white/[0.04] px-2 text-[10px] text-white/45 disabled:opacity-30", children: ["Suggestions: ", suggestionsAvailable ? 'On' : 'Off'] }), activeReasoningSelector && (_jsxs("div", { ref: effortSelectorRef, className: "relative", onKeyDown: (event) => {
7747
- if (event.key !== 'Escape')
7748
- return;
7749
- setEffortListOpen(false);
7750
- effortBtnRef.current?.focus();
7751
- }, children: [_jsxs("button", { ref: effortBtnRef, onClick: () => { setEffortListOpen((open) => !open); setModelListOpen(false); setAcpModelListOpen(false); setAcpCliListOpen(false); setModeExpanded(false); }, disabled: isBusy || modeExpanded, className: "h-7 flex items-center gap-1 rounded bg-white/[0.04] px-2 text-white/40 transition-colors hover:bg-white/[0.08] hover:text-white/60 focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:cursor-not-allowed disabled:opacity-30", title: "Select reasoning effort", "aria-label": "Select reasoning effort", "aria-haspopup": "listbox", "aria-expanded": effortListOpen, children: [_jsxs("span", { className: "text-[10px] whitespace-nowrap", children: ["Effort: ", activeReasoningSelector.label] }), _jsx(ChevronDown, { className: `h-3 w-3 flex-shrink-0 transition-transform duration-150 ${effortListOpen ? 'rotate-180' : ''}` })] }), effortListOpen && (_jsx("div", { role: "listbox", className: "xeno-elevated-shell xeno-border-subtle absolute bottom-[34px] right-0 z-50 w-[130px] overflow-hidden rounded-md border py-1", children: activeReasoningSelector.options.map((option) => {
7752
- const selected = runtimeMode === 'acp'
7753
- ? selectedAcpEffortFamily?.effort === option.id
7754
- : runtimeMode === 'cloud'
7755
- ? selectedCloudEffortFamily?.effort === option.id
7756
- : selectedModel === option.modelId;
7757
- const acpDiscoveredOption = runtimeMode === 'acp'
7758
- ? acpProviderModelOptionById.get(option.modelId)
7759
- : undefined;
7760
- const unavailable = option.available === false || acpDiscoveredOption?.available === false;
7761
- return (_jsx("button", { type: "button", role: "option", "aria-selected": selected, disabled: unavailable, title: option.reason || acpDiscoveredOption?.reason || undefined, onClick: () => {
7762
- setReasoningChoice({ conversationId: conversationId || null, providerKey: reasoningProviderKey, modelId: option.modelId, optionId: option.id });
7763
- if (runtimeMode === 'acp') {
7764
- if (selectedAcpAgentConfigId)
7765
- selectAcpRuntime(selectedAcpAgentConfigId, option.modelId);
7766
- }
7767
- else {
7768
- setSelectedModel(option.modelId);
7769
- setEffortListOpen(false);
7770
- }
7771
- if (conversationId) {
7772
- const latest = useAgentChatStore.getState().getConversation(conversationId);
7773
- if (latest?.runtime)
7774
- updateConversation(conversationId, {
7775
- runtime: { ...latest.runtime, model: providerRuntimeModelId(option.modelId), reasoning: { ...latest.runtime.reasoning, optionId: option.id } },
7776
- });
7777
- }
7778
- }, className: `w-full px-3 py-1.5 text-left text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-30 ${selected ? 'bg-white/[0.06] text-white/70' : 'text-white/40 hover:bg-white/[0.04] hover:text-white/60'}`, children: option.label }, option.id));
7779
- }) }))] })), _jsxs("button", { onClick: () => { if (isAuthenticatedForCloud)
7780
- setCloudRunEnabled(!cloudRunEnabled); }, disabled: isBusy || !isAuthenticatedForCloud, className: `h-7 flex items-center gap-1 px-2 rounded transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:cursor-not-allowed whitespace-nowrap ${!isAuthenticatedForCloud
7781
- ? 'bg-white/[0.03] text-white/25 opacity-60'
7782
- : cloudRunEnabled
7783
- ? 'bg-sky-500/20 text-sky-200 hover:bg-sky-500/25'
7784
- : 'bg-white/[0.04] hover:bg-white/[0.08] text-white/40 hover:text-white/60'}`, title: isAuthenticatedForCloud
7785
- ? (cloudRunEnabled
7786
- ? 'Run in cloud is ON; this turn runs on the hosted agent (billed via credits). Click to run locally.'
7787
- : 'Run in cloud: dispatch this turn to the hosted agent (billed via credits)')
7788
- : 'Sign in to run in the cloud', "aria-label": "Toggle run in cloud", "aria-pressed": cloudRunEnabled, children: [_jsx(Cloud, { className: "w-3 h-3 flex-shrink-0" }), _jsx("span", { className: "text-[10px]", children: "Cloud" })] }), cloudRunEnabled && isAuthenticatedForCloud && activeCloudRunStatus && (_jsxs("span", { className: "h-7 flex items-center gap-1 px-2 rounded bg-white/[0.03] text-white/45 text-[10px] whitespace-nowrap", title: `Hosted run ${activeCloudRunStatus.runId}`, children: [_jsx("span", { className: "capitalize", children: activeCloudRunStatus.status || 'queued' }), typeof activeCloudRunStatus.creditsSettled === 'number' && activeCloudRunStatus.creditsSettled > 0
7789
- ? _jsxs("span", { className: "text-white/30", children: ["- ", activeCloudRunStatus.creditsSettled, "cr"] })
7790
- : typeof activeCloudRunStatus.creditsHeld === 'number' && activeCloudRunStatus.creditsHeld > 0
7791
- ? _jsxs("span", { className: "text-white/30", children: ["- ", activeCloudRunStatus.creditsHeld, "cr held"] })
7792
- : null] }))] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [conversationId && (_jsx(LiveConversationCollaboration, { localConversationId: conversationId, title: conversation?.title || 'Agent conversation', modelId: actualModelId, ownerMessages: runtimeProjectedMessages, request: collaborationRequest, onAccessChange: setCollaborationAccess, onNavigateConversation: id => {
7793
- useAgentChatStore.getState().createConversation(agentId, id);
7794
- props.onNavigateConversation?.(id);
7795
- } })), props.onOpenRuntimeSettings && (_jsx("button", { onClick: props.onOpenRuntimeSettings, disabled: isBusy, className: "w-7 h-7 flex items-center justify-center rounded bg-white/[0.04] hover:bg-white/[0.08] text-white/35 hover:text-white/60 transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:opacity-30 disabled:cursor-not-allowed", title: "Edit runtime provider", "aria-label": "Edit runtime provider", children: _jsx(Pencil, { className: "w-3 h-3" }) })), props.onOpenConversationSettings && (_jsx("button", { onClick: props.onOpenConversationSettings, className: "h-7 px-2.5 rounded-[4px] text-[10px] bg-white/[0.04] hover:bg-white/[0.08] text-white/35 hover:text-white/60 transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", title: "Open chat and workspace settings", children: "Settings" })), _jsx("button", { onClick: (event) => {
7796
- if (event.shiftKey) {
7797
- event.preventDefault();
7798
- toggleDevSession();
7507
+ } })), taskCounts.total > 0 && !planAwaitingApproval && (_jsx(TasksTray, { tasks: conversationTasks, open: !tasksCollapsed, onToggle: () => patchSessionUi({ tasksCollapsed: !tasksCollapsed }) })), _jsx(AgentsTray, { runs: delegatedRunViews, open: delegatedRunsOpen, onToggle: () => setDelegatedRunsOpen((value) => !value), onOpen: (childId) => { props.onNavigateConversation?.(childId); } }), _jsx(BackgroundTray, { runs: backgroundRunViews, open: backgroundRunsOpen, onToggle: () => setBackgroundRunsOpen((value) => !value), onOutput: (taskId) => { const task = backgroundTasks.find((entry) => entry.taskId === taskId); if (task)
7508
+ void handleReadBackgroundTask(task); }, onStop: (taskId) => { const task = backgroundTasks.find((entry) => entry.taskId === taskId); if (task)
7509
+ void handleStopBackgroundTask(task); } }), _jsxs("div", { className: "xa-wsrow", children: [_jsx(ModeSegment, { mode: composerMode, onMode: (mode) => { void handleSelectWorkspaceMode(mode); }, machine: machineOffer, disabled: controlsLocked || !!props.externalInputLockReason || isSelectingConversationWorkspace, choosing: isSelectingConversationWorkspace }), _jsx(CtxStrip, { children: workspaceBinding.mode === 'agent' ? (_jsxs(_Fragment, { children: [_jsx(CtxSeg, { glyph: "folder", name: true, title: workspaceBinding.rootDirectory || workspaceBinding.allowedDirectories[0], children: projectDisplayName(workspaceBinding.rootDirectory || workspaceBinding.allowedDirectories[0]) }), _jsx(CtxVr, {}), _jsx(CtxSeg, { glyph: cloudRunEnabled && isAuthenticatedForCloud ? 'box' : 'monitor', dim: true, title: !isAuthenticatedForCloud
7510
+ ? 'The agent works on this computer. Sign in to move it into a sandbox on XENO’s servers.'
7511
+ : cloudRunEnabled
7512
+ ? 'The agent works inside a sandbox on XENO’s servers (billed via credits). Click to work on this computer instead.'
7513
+ : 'The agent works on this computer. Click to move it into a sandbox on XENO’s servers.', onClick: () => { if (isAuthenticatedForCloud)
7514
+ setCloudRunEnabled(!cloudRunEnabled); }, disabled: controlsLocked, children: cloudRunEnabled && isAuthenticatedForCloud
7515
+ ? (activeCloudRunStatus ? `Hosted sandbox · ${activeCloudRunStatus.status || 'queued'}` : 'Hosted sandbox')
7516
+ : 'This machine' }), gitStrip?.branch && (_jsxs(_Fragment, { children: [_jsx(CtxVr, { branch: true }), _jsx(CtxSeg, { glyph: "git-branch", dim: true, branch: true, title: gitStrip.upstream ? `${gitStrip.branch} · tracks ${gitStrip.upstream}` : gitStrip.branch, children: gitStrip.branch })] })), activeMcpList.length > 0 && (_jsxs(_Fragment, { children: [_jsx(CtxVr, {}), activeMcpList.map((mcp) => (_jsx(CtxSeg, { dim: true, title: `${mcp.server}: ${mcp.status}${mcp.error ? ` (${mcp.error})` : ''}`, onClick: () => { void handleExecuteCommandOrPrompt(`/mcp ${mcp.server}`); }, children: mcp.status === 'connected' ? mcp.server : `${mcp.server} · ${mcp.status === 'auth_required' ? 'sign in' : 'down'}` }, mcp.server)))] })), _jsx(CtxGrow, {}), gitStrip && (gitStrip.added > 0 || gitStrip.removed > 0) && _jsx(CtxDiff, { added: gitStrip.added, removed: gitStrip.removed }), _jsx(CtxSplit, { label: "Create PR", title: "Ask the agent to open a pull request for this branch", disabled: controlsLocked || inputLocked, onClick: () => { void handleCreatePullRequest(); }, moreMenu: "branch", moreOpen: branchMoreOpen, onMore: () => setBranchMoreOpen((open) => !open) }), _jsxs(XaMenu, { open: branchMoreOpen, onClose: () => setBranchMoreOpen(false), menu: "branch", width: 260, children: [_jsx(XaRow, { label: "Push this branch", sub: "Push it to its remote, setting upstream if it has none", onClick: () => { setBranchMoreOpen(false); void handleExecuteCommandOrPrompt('Push the current branch to its remote, setting the upstream if it has none. Report what was pushed.'); } }), _jsx(XaRow, { label: "Open a draft pull request", sub: "A PR marked draft, otherwise the same as Create PR", onClick: () => { setBranchMoreOpen(false); void handleExecuteCommandOrPrompt('Open a DRAFT pull request for the current branch: summarise the changes since it diverged from its base in the title and body.'); } }), _jsx(XaRow, { label: "Show me what changed", sub: "The diff against the base, summarised", onClick: () => { setBranchMoreOpen(false); void handleExecuteCommandOrPrompt('Summarise what has changed on this branch since it diverged from its base, file by file.'); } })] })] })) : composerMode === 'machine' ? (
7517
+ /* D11 this computer, per app, per session, under a grant: the machine, what it
7518
+ holds, and the one way to take it back */
7519
+ _jsxs(_Fragment, { children: [_jsx(CtxSeg, { glyph: "monitor", name: true, title: machineOffer?.available ? 'The agent acts on this computer, inside the apps you grant it' : (machineOffer?.reason || 'This computer is not answering — the agent cannot act on it yet'), children: "This machine" }), _jsx(CtxVr, {}), machineGrant ? (_jsxs(CtxSeg, { glyph: "app-window", dim: true, title: machineGrant.pending ? `${machineGrant.name} — asked for, not yet granted` : `${machineGrant.name} — granted until this ${machineGrant.scope} ends`, children: [machineGrant.name, " \u00B7 this ", machineGrant.scope] })) : (_jsx(CtxSeg, { dim: true, title: "No app is granted yet \u2014 the agent asks for one when it needs it", children: "No app yet" })), _jsx(CtxGrow, {}), machineGrant
7520
+ ? _jsx(CtxAction, { title: machineGrant.pending ? 'Refuse this grant' : 'Take the grant back the agent stops acting in it now', disabled: machineRevoking, onClick: () => { void revokeMachineGrant(); }, children: machineRevoking ? 'Revoking…' : 'Revoke' })
7521
+ : props.onNavigateConversation && (_jsx(CtxAction, { title: "Start a new conversation", onClick: () => { const id = useAgentChatStore.getState().createConversation(agentId); props.onNavigateConversation?.(id); }, children: "New chat" }))] })) : (_jsxs(_Fragment, { children: [_jsx(CtxSeg, { glyph: "message", name: true, title: conversation?.title || 'New conversation', children: conversation?.title || 'New conversation' }), _jsx(CtxVr, {}), _jsx(CtxSeg, { dim: true, title: "Chat only \u2014 no access to project files", children: "Private workspace" }), _jsx(CtxGrow, {}), contextUsed !== null && _jsx(CtxMeter, { used: contextUsed, title: "How much of the context window this conversation has used" }), props.onNavigateConversation && (_jsx(CtxAction, { title: "Start a new conversation", onClick: () => { const id = useAgentChatStore.getState().createConversation(agentId); props.onNavigateConversation?.(id); }, children: "New chat" }))] })) })] }), pendingTurns.length > 0 && (_jsxs("div", { className: "xeno-border-subtle rounded-t-md border border-b-0 bg-[#111111] overflow-hidden -mb-[9px]", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2 border-b xeno-border-subtle", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("span", { className: "xa-label whitespace-nowrap", children: "Queued" }), _jsx("span", { className: "text-[10px] xeno-text-muted tabular-nums whitespace-nowrap", children: pendingTurns.length })] }), _jsx("button", { onClick: () => patchSessionUi({ panelOpen: true }), className: "text-[10px] xeno-text-faint hover:text-[color:var(--xeno-ink-secondary)] transition-colors whitespace-nowrap cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", children: "Manage queue" })] }), _jsxs("div", { className: "divide-y xeno-border-subtle", children: [pendingTurns.slice(0, 3).map((turn) => (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [_jsx("span", { className: "inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] bg-white/[0.04] px-1.5 text-[10px] xeno-text-faint tabular-nums", children: new Date(turn.queuedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }), _jsx("span", { className: "min-w-0 flex-1 text-[12px] leading-4 xeno-text-secondary truncate", children: turn.content }), _jsx("button", { onClick: () => handleRemoveQueuedConversationTurn(turn.id), className: "text-[10px] xeno-text-faint hover:text-[color:var(--xeno-ink-secondary)] transition-colors whitespace-nowrap cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", title: "Remove queued message", children: "Remove" })] }, turn.id))), pendingTurns.length > 3 && _jsxs("div", { className: "px-3 py-2 text-[11px] xeno-text-faint", children: [pendingTurns.length - 3, " more queued"] })] })] })), _jsxs("div", { ref: boxRef, className: `xa-box${pendingTurns.length > 0 ? ' xa-queued' : ''}${inputLocked ? ' xa-off' : ''}`, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, onChange: (e) => { void handleAttachmentPick(e); }, className: "hidden" }), _jsx(Tiles, { items: pendingAttachments.map((att) => ({ id: att.id, name: att.name, dataUrl: att.dataUrl })), showAll: showAllTiles, onShowAll: () => setShowAllTiles(true), onRemove: (id) => removePendingAttachment(id) }), _jsx("div", { className: `xa-field${effectiveGhostText ? ' xa-ghosting' : ''}`, children: _jsx(GhostTextArea, { ref: textareaRef, rows: 1, value: input, onChange: handleInputChange, onKeyDown: handleKeyDown, disabled: inputLocked, placeholder: effectiveGhostText
7522
+ ? ''
7523
+ : (isModelReady
7524
+ // the three prefixes the box answers to, named the way the bench names them
7525
+ ? `Message ${agentName.replace(/\s*powered by .+$/i, '')} / for commands, @ for files, # for work items`
7526
+ : (localRuntimeNeedsStart
7527
+ ? 'Start xeno-rt to use this local model'
7528
+ : (isPreparingModel ? `Loading local model ${localModelName}…` : 'Local model is not ready yet'))), effectiveGhostText: effectiveGhostText, onAcceptGhost: handleAcceptGhost, chrome: "panel", onCaret: syncTyped }) })] }), _jsxs("div", { className: `xa-controls${agentWorking ? ' xa-running' : composerSettled ? '' : ' xa-idle'}`, children: [_jsx("button", { type: "button", className: "xa-ic", "data-tip": "Attach images from this machine", "data-key": "\u2318U", "aria-label": "Attach", disabled: inputLocked, onClick: () => fileInputRef.current?.click(), children: _jsx(DockIcon, { name: "plus" }) }), _jsx(PermChip
7529
+ // who approves what the agent does — asked wherever it can DO something: in a project, or on this machine
7530
+ , {
7531
+ // who approves what the agent does — asked wherever it can DO something: in a project, or on this machine
7532
+ present: composerMode !== 'chat', value: permPosture, open: permOpen, onToggle: () => { setPermOpen((v) => !v); setLaneOpen(false); setMoreOpen(false); }, onPick: (posture) => { patchSessionUi({ permissionProfile: posture }); setPermOpen(false); }, disabled: controlsLocked || !!props.externalInputLockReason }), _jsx("div", { className: "xa-grow" }), AGENT_MODEL_MODE_SWITCH_ENABLED && (_jsxs(LaneGroup, { lane: runtimeMode, laneOpen: laneOpen, onLaneToggle: () => { setLaneOpen((v) => !v); setPermOpen(false); setModelListOpen(false); setEffortListOpen(false); setAcpCliListOpen(false); setAcpModelListOpen(false); }, onLanePick: (lane) => { setLaneOpen(false); if (lane === 'cloud')
7533
+ switchToCloudMode();
7534
+ else if (lane === 'local')
7535
+ switchToLocalMode();
7536
+ else
7537
+ void switchToAcpMode(); }, laneDown: {
7538
+ ...(sharedProviderCatalogLoaded && cloudModels.length === 0 ? { cloud: cloudCatalogUnavailableReason } : {}),
7539
+ ...(localModels.length === 0 ? { local: 'No local model is installed.' } : {}),
7540
+ ...(selectableAcpAgents.length === 0 ? { acp: 'No ACP agent is available. Enable one in Settings.' } : {}),
7541
+ },
7542
+ // the artifact's sentence for a lane that cannot take the turn — but a lane
7543
+ // waiting on a sign-in already told you what to do, so it does not get the
7544
+ // "pick somewhere else" suffix on top of it
7545
+ currentDown: runtimeMode === 'cloud' && sharedProviderCatalogLoaded && cloudModels.length === 0
7546
+ ? (cloudNeedsSignIn || cloudCatalogRecovering ? cloudCatalogUnavailableReason : `${cloudCatalogUnavailableReason} Pick another place to run it.`)
7547
+ : undefined, ...(cloudNeedsSignIn ? { currentDownLabel: 'Sign in for Cloud' } : cloudCatalogState?.state === 'loading' ? { currentDownLabel: 'Loading models…' } : cloudCatalogRecovering ? { currentDownLabel: 'Retrying models…' } : {}), disabled: controlsLocked, running: agentWorking, harness: runtimeMode === 'acp' ? {
7548
+ label: selectedAcpAgent ? acpAgentDisplayName(selectedAcpAgent) : 'Choose an agent',
7549
+ open: acpCliListOpen,
7550
+ onClick: () => { setAcpCliListOpen(v => !v); setAcpModelListOpen(false); setModelListOpen(false); setEffortListOpen(false); setLaneOpen(false); },
7551
+ } : undefined, model: {
7552
+ label: runtimeMode === 'acp'
7553
+ ? (selectedAcpAgentConfigId ? (acpModelsLoading ? 'Loading…' : acpProviderModelDisplayName) : 'Model')
7554
+ : (currentModelName || 'Choose a model'),
7555
+ open: runtimeMode === 'acp' ? acpModelListOpen : modelListOpen,
7556
+ disabled: runtimeMode === 'acp' && !selectedAcpAgentConfigId,
7557
+ title: runtimeMode === 'acp' && !selectedAcpAgentConfigId ? 'Choose an agent first' : 'Which model answers',
7558
+ onClick: () => {
7559
+ setModelSearchQuery('');
7560
+ if (runtimeMode === 'acp') {
7561
+ setAcpModelListOpen(v => !v);
7562
+ setModelListOpen(false);
7563
+ }
7564
+ else {
7565
+ setModelListOpen(v => !v);
7566
+ setAcpModelListOpen(false);
7567
+ }
7568
+ setEffortListOpen(false);
7569
+ setAcpCliListOpen(false);
7570
+ setLaneOpen(false);
7571
+ },
7572
+ }, effort: activeReasoningSelector ? {
7573
+ label: activeReasoningSelector.label,
7574
+ open: effortListOpen,
7575
+ onClick: () => { setEffortListOpen(o => !o); setModelListOpen(false); setAcpModelListOpen(false); setAcpCliListOpen(false); setLaneOpen(false); },
7576
+ } : undefined, children: [runtimeMode === 'acp' && acpCliListOpen && _jsx("div", { ref: acpCliListRef, children: _jsx(XaMenu, { open: runtimeMode === 'acp' && acpCliListOpen, onClose: () => setAcpCliListOpen(false), clickAway: false, menu: "harness", width: 236, header: "Harness", children: selectableAcpAgents.length > 0 ? selectableAcpAgents.map((agent, i) => {
7577
+ const fixture = agent.certification?.status === 'fixture_only';
7578
+ const ready = fixture || (agent.certification?.status === 'certified' && agent.providerReview?.runnable === true);
7579
+ return _jsx(XaRow, { label: acpAgentDisplayName(agent), selected: agent.id === selectedAcpAgentConfigId, idx: ready ? i + 1 : undefined, na: ready ? undefined : 'check', onClick: () => { selectAcpRuntime(agent.id, agent.id === selectedAcpAgentConfigId ? acpProviderModel : undefined); setAcpCliListOpen(false); } }, agent.id);
7580
+ }) : _jsx("div", { className: "xa-none", children: "No ACP agent is available. Enable or repair one in Settings." }) }) }), runtimeMode === 'acp' && acpModelListOpen && _jsx("div", { ref: acpModelListRef, children: _jsxs(XaMenu, { open: runtimeMode === 'acp' && acpModelListOpen, onClose: () => setAcpModelListOpen(false), clickAway: false, menu: "model", width: 236, header: `${selectedAcpAgent ? acpAgentDisplayName(selectedAcpAgent) : 'Harness'} models`, children: [acpModelOptions.length > 6 && _jsx(XaMenuSearch, { value: modelSearchQuery, onChange: setModelSearchQuery, placeholder: "Search models" }), _jsx(XaRow, { label: "Harness default", selected: !acpProviderModel, disabled: !selectedAcpAgentConfigId, onClick: () => { if (selectedAcpAgentConfigId)
7581
+ selectAcpRuntime(selectedAcpAgentConfigId, undefined); setAcpModelListOpen(false); } }), acpModelsLoading ? _jsx("div", { className: "xa-none", children: "Loading models\u2026" })
7582
+ : acpModelOptions.length > 0
7583
+ ? (filteredAcpModelOptions.length > 0 ? filteredAcpModelOptions.map((entry, i) => (_jsx(XaRow, { label: entry.name, selected: entry.selectionKey === (selectedAcpEffortFamily?.baseId || acpProviderModel), disabled: !selectedAcpAgentConfigId || !entry.available, na: entry.available ? undefined : 'unavailable', idx: entry.available ? i + 1 : undefined, title: entry.reason || undefined, onClick: () => { if (selectedAcpAgentConfigId)
7584
+ selectAcpRuntime(selectedAcpAgentConfigId, entry.id); setAcpModelListOpen(false); } }, entry.selectionKey))) : _jsx("div", { className: "xa-none", children: "No matching models" }))
7585
+ : _jsx("div", { className: "xa-none", children: selectedAcpAgentConfigId ? (acpModelMessage || 'No preset models for this agent') : 'Choose an agent first' })] }) }), runtimeMode !== 'acp' && modelListOpen && _jsx("div", { ref: modelListRef, children: _jsxs(XaMenu, { open: true, onClose: () => setModelListOpen(false), clickAway: false, menu: "model", width: 236, header: "Model", children: [(runtimeMode === 'cloud' ? cloudModels.length : localModels.length) > 6 && _jsx(XaMenuSearch, { value: modelSearchQuery, onChange: setModelSearchQuery, placeholder: "Search models" }), runtimeMode === 'cloud' ? (sharedProviderCatalogRefreshing ? _jsx("div", { className: "xa-none", children: "Refreshing available models\u2026" })
7586
+ : cloudModels.length > 0
7587
+ ? (filteredCloudModelOptions.length > 0 ? filteredCloudModelOptions.map((entry, i) => {
7588
+ const modelMeta = cloudModels.find(cm => cm.id === entry.id);
7589
+ return _jsx(XaRow, { label: entry.name, selected: entry.selectionKey === (selectedCloudEffortFamily?.baseId || selectedModel), na: modelMeta?.availability === 'degraded' ? 'fallback' : undefined, idx: modelMeta?.availability === 'degraded' ? undefined : i + 1, title: modelMeta?.availabilityReason || undefined, onClick: () => { setInferenceMode('cloud'); setSelectedModel(entry.id); setModelListOpen(false); setEffortListOpen(false); } }, entry.id);
7590
+ }) : _jsx("div", { className: "xa-none", children: "No matching models" }))
7591
+ : _jsx("div", { className: "xa-none", children: cloudCatalogUnavailableReason })) : (_jsxs(_Fragment, { children: [localModels.length > 0
7592
+ ? (filteredLocalModels.length > 0 ? filteredLocalModels.map((m, i) => (_jsx(XaRow, { label: m.name, tag: m.parameterSize || undefined, idx: m.parameterSize ? undefined : i + 1, selected: m.id === (selectedLocalEffortFamily?.baseId || selectedModel), onClick: () => { setInferenceMode('local'); setSelectedModel(m.id); setModelListOpen(false); setEffortListOpen(false); } }, m.id))) : _jsx("div", { className: "xa-none", children: "No matching models" }))
7593
+ : _jsx("div", { className: "xa-none", children: "No local models installed" }), _jsx("div", { className: "xa-hr" }), _jsx(XaRow, { label: "Browse models", chev: true, onClick: () => { setModelListOpen(false); goToModelsMarketplace(); } })] }))] }) }), !!activeReasoningSelector && effortListOpen && _jsx("div", { ref: effortSelectorRef, onKeyDown: (event) => { if (event.key === 'Escape')
7594
+ setEffortListOpen(false); }, children: _jsx(XaMenu, { open: !!activeReasoningSelector && effortListOpen, onClose: () => setEffortListOpen(false), clickAway: false, menu: "effort", width: 224, children: activeReasoningSelector && (_jsx(EffortCells, { options: activeReasoningSelector.options.map((option) => {
7595
+ const acpDiscoveredOption = runtimeMode === 'acp' ? acpProviderModelOptionById.get(option.modelId) : undefined;
7596
+ const unavailable = option.available === false || acpDiscoveredOption?.available === false;
7597
+ return { id: option.id, label: option.label, disabled: unavailable, title: option.reason || acpDiscoveredOption?.reason || undefined };
7598
+ }), value: runtimeMode === 'acp' ? selectedAcpEffortFamily?.effort : runtimeMode === 'cloud' ? selectedCloudEffortFamily?.effort : activeReasoningSelector.options.find((o) => o.modelId === selectedModel)?.id, onPick: (id) => {
7599
+ const option = activeReasoningSelector.options.find((o) => o.id === id);
7600
+ if (!option)
7799
7601
  return;
7602
+ setReasoningChoice({ conversationId: conversationId || null, providerKey: reasoningProviderKey, modelId: option.modelId, optionId: option.id });
7603
+ if (runtimeMode === 'acp') {
7604
+ if (selectedAcpAgentConfigId)
7605
+ selectAcpRuntime(selectedAcpAgentConfigId, option.modelId);
7800
7606
  }
7801
- patchSessionUi({ panelOpen: !sessionPanelOpen });
7802
- }, className: `h-7 px-2.5 rounded-[4px] text-[10px] transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 ${sessionPanelOpen
7803
- ? 'bg-white/[0.08] text-white/72'
7804
- : 'bg-white/[0.04] hover:bg-white/[0.08] text-white/35 hover:text-white/60'}`, title: "Toggle workbench panel. Shift+click or Ctrl+Shift+D for the developer session.", children: "Panel" }), _jsx(AIStatusIndicator, { model: actualModelId, className: "justify-end", refreshKey: runtimeStatusRefreshKey, runtimeAvailable: selectedModel.startsWith('local:') ? localRuntimeAvailable : null, onRuntimeClick: selectedModel.startsWith('local:') ? handleRuntimeIndicatorClick : undefined }), isStreaming && !activeCliThreadRun && (_jsx("button", {
7805
- // Names the conversation this Stop belongs to: with concurrent turns the
7806
- // foreground scalar is not necessarily this conversation's turn.
7807
- onClick: () => { abort(conversationId); clearAllSuggestions(); }, disabled: isCancelling, className: "w-7 h-7 flex items-center justify-center rounded bg-white/[0.08] hover:bg-white/[0.14] text-white/50 hover:text-white/90 transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:cursor-wait disabled:opacity-55", title: "Stop current response", "aria-label": "Stop current response", children: isCancelling ? (_jsx(RefreshCw, { className: "w-3 h-3 animate-spin" })) : (_jsx(Square, { className: "w-3 h-3" })) })), activeCliThreadRun && (_jsx("button", { onClick: () => { void cancelCliAppServerMessage(); }, className: "w-7 h-7 flex items-center justify-center rounded bg-white/[0.08] hover:bg-white/[0.14] text-white/50 hover:text-white/90 transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35", title: activeCliThreadRun.kind === 'remote' ? 'Stop CLI remote run start' : 'Stop CLI app-server turn', "aria-label": activeCliThreadRun.kind === 'remote' ? 'Stop CLI remote run start' : 'Stop CLI app-server turn', children: _jsx(Square, { className: "w-3 h-3" }) })), _jsx("button", { onClick: () => { void handleSend(); }, disabled: (!input.trim() && pendingAttachments.length === 0) || inputLocked, className: "w-7 h-7 flex items-center justify-center rounded bg-white/[0.08] hover:bg-white/[0.14] text-white/50 hover:text-white/90 transition-colors cursor-pointer focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/35 disabled:opacity-30 disabled:cursor-not-allowed", title: isStreaming && !inputLocked ? 'Queue message' : 'Send message', "aria-label": isStreaming && !inputLocked ? 'Queue message' : 'Send message', children: _jsx(ArrowUp, { className: "w-3.5 h-3.5" }) })] })] })] })] })] })] })] }));
7607
+ else
7608
+ setSelectedModel(option.modelId);
7609
+ if (conversationId) {
7610
+ const latest = useAgentChatStore.getState().getConversation(conversationId);
7611
+ if (latest?.runtime)
7612
+ updateConversation(conversationId, {
7613
+ runtime: { ...latest.runtime, model: providerRuntimeModelId(option.modelId), reasoning: { ...latest.runtime.reasoning, optionId: option.id } },
7614
+ });
7615
+ }
7616
+ } })) }) })] })), _jsx(TurnStatus, { since: agentWorking ? turnSince : null }), _jsxs(XaMenu, { open: moreOpen, onClose: () => setMoreOpen(false), menu: "more", width: 248, header: "This conversation", children: [_jsx(XaRow, { label: `Steps · ${stepsMode}`, sub: stepsMode === 'expanded' ? 'Every step draws as the agent works' : 'Steps stay folded; the current one rides the clock line', onClick: () => { patchSessionUi({ stepsMode: stepsMode === 'collapsed' ? 'expanded' : 'collapsed' }); } }), _jsx(XaRow, { label: `Suggestions · ${suggestionsAvailable ? 'On' : 'Off'}`, sub: runtimeMode === 'acp' ? 'Not available through this agent' : 'Ghost text while you type; cloud requests may draw credits', disabled: !suggestionModelAvailable, onClick: () => { clearAllSuggestions(); setSuggestionOptInScope(suggestionsAvailable ? null : suggestionScope); } }), renderWorkbenchPanel && _jsx(XaRow, { label: `Workbench · ${sessionPanelOpen ? 'Open' : 'Closed'}`, sub: "Terminal, background runs, checkpoints, the queue \u00B7 Ctrl+Shift+D for the developer session", onClick: () => { patchSessionUi({ panelOpen: !sessionPanelOpen }); setMoreOpen(false); } }), conversationId && (_jsx(LiveConversationCollaboration, { localConversationId: conversationId, title: conversation?.title || 'Agent conversation', modelId: actualModelId, ownerMessages: runtimeProjectedMessages, request: collaborationRequest ?? undefined, onAccessChange: setCollaborationAccess, onNavigateConversation: id => { useAgentChatStore.getState().createConversation(agentId, id); props.onNavigateConversation?.(id); }, renderTrigger: (open) => _jsx(XaRow, { label: "Share", sub: "Live collaboration on this conversation", onClick: () => { setMoreOpen(false); open(); } }) })), props.onOpenConversationSettings && _jsx(XaRow, { label: "Settings", sub: "Chat and workspace settings", onClick: () => { setMoreOpen(false); props.onOpenConversationSettings?.(); } }), props.onOpenRuntimeSettings && _jsx(XaRow, { label: "Runtime provider", sub: "Edit where turns run", disabled: isBusy, onClick: () => { setMoreOpen(false); props.onOpenRuntimeSettings?.(); } }), _jsx(XaRow, { label: "SDK sessions", sub: sessionUi?.sdkSessionId ? `Attached · ${sessionUi.sdkSessionId.slice(0, 8)}` : 'Attach, detach or clean the agent’s sessions', onClick: () => { setMoreOpen(false); setSessionDrawerOpen(true); } }), _jsx("div", { className: "xa-hr" }), _jsx("div", { className: "px-[9px] py-[6px]", children: _jsx(AIStatusIndicator, { model: actualModelId, refreshKey: runtimeStatusRefreshKey, runtimeAvailable: selectedModel.startsWith('local:') ? localRuntimeAvailable : null, onRuntimeClick: selectedModel.startsWith('local:') ? handleRuntimeIndicatorClick : undefined }) })] }), _jsx(SendSquare, { state: (isStreaming && !agentIsAsking) || !!activeCliThreadRun ? 'running' : ((input.trim() || pendingAttachments.length > 0) && !inputLocked ? 'ready' : 'idle'), stopping: isCancelling, sendTitle: isStreaming && !inputLocked ? 'Queue message' : 'Send', onSend: () => { void handleSend(); }, onStop: () => { if (activeCliThreadRun)
7617
+ void cancelCliAppServerMessage();
7618
+ else {
7619
+ abort(conversationId || undefined);
7620
+ clearAllSuggestions();
7621
+ } } })] })] }), _jsx(Tip, { dockRef: dockRef, suspended: !!typedToken || moreOpen || modelListOpen || effortListOpen || acpCliListOpen || acpModelListOpen }), _jsx(TypedMenu, { open: !!typedToken, token: typedToken, sections: typedSections, onTake: takeTyped, onClose: () => setTypedToken(null), boxRef: boxRef, dock: dockEl })] })] }), _jsx("button", { type: "button", className: `xa-tobottom${toBottom.show ? ' xa-show' : ''}`, "data-tobottom": true, "aria-label": "Jump to the latest", tabIndex: toBottom.show ? 0 : -1, style: { bottom: toBottom.bottom }, onClick: jumpToLatest, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "square", "aria-hidden": "true", children: _jsx("path", { d: "M6 9l6 6 6-6" }) }) })] }));
7808
7622
  }
7809
7623
  const ACP_MODEL_ID = /^[A-Za-z0-9._:/+-]{1,120}$/;
7624
+ /** `@` names a conversation the way the prototype does — its title, slugged. */
7625
+ function conversationSlug(title) {
7626
+ return (title || 'conversation').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') || 'conversation';
7627
+ }
7628
+ /** When a conversation last spoke, in the words the picker's KIND column uses. */
7629
+ function relativeDay(at) {
7630
+ if (!at)
7631
+ return '';
7632
+ const days = Math.floor((Date.now() - at) / 86_400_000);
7633
+ if (days <= 0)
7634
+ return 'Today';
7635
+ if (days === 1)
7636
+ return 'Yesterday';
7637
+ if (days < 7)
7638
+ return new Date(at).toLocaleDateString([], { weekday: 'long' });
7639
+ return new Date(at).toLocaleDateString([], { month: 'short', day: 'numeric' });
7640
+ }
7641
+ /** The last segment of a path, on either separator — `path.basename` follows the HOST, not the string. */
7642
+ function projectDisplayName(root) {
7643
+ if (!root)
7644
+ return 'No project';
7645
+ const parts = root.replace(/[\\/]+$/, '').split(/[\\/]/);
7646
+ return parts[parts.length - 1] || root;
7647
+ }
7810
7648
  function projectSharedAcpProviderModels(provider) {
7811
7649
  const options = [];
7812
7650
  const seen = new Set();