@xenosystem/agent-conversation 0.1.36 → 0.1.38
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.
- package/dist/adapters/xenoAgentHostCatalog.d.ts.map +1 -1
- package/dist/adapters/xenoAgentHostCatalog.js +20 -5
- package/dist/adapters/xenoAgentHostCatalog.js.map +1 -1
- package/dist/components/agent/AgentView.d.ts +9 -0
- package/dist/components/agent/AgentView.d.ts.map +1 -1
- package/dist/components/agent/AgentView.js +883 -1045
- package/dist/components/agent/AgentView.js.map +1 -1
- package/dist/components/agent/GhostTextArea.d.ts +6 -0
- package/dist/components/agent/GhostTextArea.d.ts.map +1 -1
- package/dist/components/agent/GhostTextArea.js +15 -5
- package/dist/components/agent/GhostTextArea.js.map +1 -1
- package/dist/components/agent/LiveConversationCollaboration.d.ts +3 -1
- package/dist/components/agent/LiveConversationCollaboration.d.ts.map +1 -1
- package/dist/components/agent/LiveConversationCollaboration.js +3 -1
- package/dist/components/agent/LiveConversationCollaboration.js.map +1 -1
- package/dist/components/agent/MessageText.d.ts +36 -0
- package/dist/components/agent/MessageText.d.ts.map +1 -0
- package/dist/components/agent/MessageText.js +76 -0
- package/dist/components/agent/MessageText.js.map +1 -0
- package/dist/components/agent/SdkSessionPanel.d.ts +2 -0
- package/dist/components/agent/SdkSessionPanel.d.ts.map +1 -1
- package/dist/components/agent/SdkSessionPanel.js +1 -1
- package/dist/components/agent/SdkSessionPanel.js.map +1 -1
- package/dist/components/agent/composer/Composer.d.ts +282 -0
- package/dist/components/agent/composer/Composer.d.ts.map +1 -0
- package/dist/components/agent/composer/Composer.js +393 -0
- package/dist/components/agent/composer/Composer.js.map +1 -0
- package/dist/components/agent/composer/EffortCells.d.ts +12 -0
- package/dist/components/agent/composer/EffortCells.d.ts.map +1 -0
- package/dist/components/agent/composer/EffortCells.js +146 -0
- package/dist/components/agent/composer/EffortCells.js.map +1 -0
- package/dist/components/agent/composer/Tip.d.ts +19 -0
- package/dist/components/agent/composer/Tip.d.ts.map +1 -0
- package/dist/components/agent/composer/Tip.js +86 -0
- package/dist/components/agent/composer/Tip.js.map +1 -0
- package/dist/components/agent/composer/TypedMenu.d.ts +56 -0
- package/dist/components/agent/composer/TypedMenu.d.ts.map +1 -0
- package/dist/components/agent/composer/TypedMenu.js +156 -0
- package/dist/components/agent/composer/TypedMenu.js.map +1 -0
- package/dist/components/agent/mainAssistant.d.ts +9 -2
- package/dist/components/agent/mainAssistant.d.ts.map +1 -1
- package/dist/components/agent/mainAssistant.js +9 -2
- package/dist/components/agent/mainAssistant.js.map +1 -1
- package/dist/components/agent/toolCallDerive.d.ts +82 -0
- package/dist/components/agent/toolCallDerive.d.ts.map +1 -0
- package/dist/components/agent/toolCallDerive.js +741 -0
- package/dist/components/agent/toolCallDerive.js.map +1 -0
- package/dist/components/agent/transcript/Transcript.d.ts +127 -0
- package/dist/components/agent/transcript/Transcript.d.ts.map +1 -0
- package/dist/components/agent/transcript/Transcript.js +544 -0
- package/dist/components/agent/transcript/Transcript.js.map +1 -0
- package/dist/components/agent/transcript/glyphs.d.ts +83 -0
- package/dist/components/agent/transcript/glyphs.d.ts.map +1 -0
- package/dist/components/agent/transcript/glyphs.js +124 -0
- package/dist/components/agent/transcript/glyphs.js.map +1 -0
- package/dist/components/agent/transcript/stepModel.d.ts +163 -0
- package/dist/components/agent/transcript/stepModel.d.ts.map +1 -0
- package/dist/components/agent/transcript/stepModel.js +701 -0
- package/dist/components/agent/transcript/stepModel.js.map +1 -0
- package/dist/components/agent/transcript/turnModel.d.ts +124 -0
- package/dist/components/agent/transcript/turnModel.d.ts.map +1 -0
- package/dist/components/agent/transcript/turnModel.js +166 -0
- package/dist/components/agent/transcript/turnModel.js.map +1 -0
- package/dist/components/agent/transcript/useAskActions.d.ts +23 -0
- package/dist/components/agent/transcript/useAskActions.d.ts.map +1 -0
- package/dist/components/agent/transcript/useAskActions.js +92 -0
- package/dist/components/agent/transcript/useAskActions.js.map +1 -0
- package/dist/parityFixture.d.ts +54 -0
- package/dist/parityFixture.d.ts.map +1 -0
- package/dist/parityFixture.js +195 -0
- package/dist/parityFixture.js.map +1 -0
- package/dist/platformApiTypes.d.ts +10 -1
- package/dist/platformApiTypes.d.ts.map +1 -1
- package/dist/stores/agentChatStore.d.ts +61 -1
- package/dist/stores/agentChatStore.d.ts.map +1 -1
- package/dist/stores/agentChatStore.js +89 -25
- package/dist/stores/agentChatStore.js.map +1 -1
- package/dist/types/agentPermissions.d.ts +3 -1
- package/dist/types/agentPermissions.d.ts.map +1 -1
- package/dist/utils/acpExecutionAuthority.d.ts.map +1 -1
- package/dist/utils/acpExecutionAuthority.js +2 -0
- package/dist/utils/acpExecutionAuthority.js.map +1 -1
- package/package.json +6 -5
- package/src/composer.css +337 -0
- package/src/styles.css +7 -0
- 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:
|
|
2648
|
+
permissionProfile: effectivePermissionProfile,
|
|
3366
2649
|
acpAgents: acpAgentsForRuntime,
|
|
3367
|
-
}), [acpAgentsForRuntime, effectiveRuntimeSelection,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4442
|
-
|
|
4443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4557
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4739
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
7477
|
-
const
|
|
7478
|
-
const
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
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))
|
|
7358
|
+
: cloudModels.find((entry) => entry.id === messageModel)?.name || cloudModelName(messageModel))
|
|
7491
7359
|
: null;
|
|
7492
|
-
return (
|
|
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: `
|
|
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
|
-
|
|
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: "
|
|
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("
|
|
7555
|
-
})()] })] })) : (
|
|
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' })] })] }) }) }))
|
|
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:
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
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
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
:
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
? (
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
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
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
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();
|