@parhelia/core 0.1.12744 → 0.1.12752

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/agents-view/AgentsView.js +1 -1
  2. package/dist/agents-view/AgentsView.js.map +1 -1
  3. package/dist/agents-view/AgentsWorkspaceView.js +41 -21
  4. package/dist/agents-view/AgentsWorkspaceView.js.map +1 -1
  5. package/dist/components/ui/PlaceholderInput.js +3 -3
  6. package/dist/components/ui/PlaceholderInput.js.map +1 -1
  7. package/dist/components/ui/PlaceholderInputTypes.js +2 -2
  8. package/dist/components/ui/PlaceholderInputTypes.js.map +1 -1
  9. package/dist/components/ui/PlaceholderItemSelector.js +2 -2
  10. package/dist/components/ui/PlaceholderItemSelector.js.map +1 -1
  11. package/dist/editor/LinkEditorDialog.js +2 -2
  12. package/dist/editor/LinkEditorDialog.js.map +1 -1
  13. package/dist/editor/ai/AgentBanners.d.ts +11 -0
  14. package/dist/editor/ai/AgentBanners.js +16 -0
  15. package/dist/editor/ai/AgentBanners.js.map +1 -0
  16. package/dist/editor/ai/AgentCostDisplay.d.ts +12 -0
  17. package/dist/editor/ai/AgentCostDisplay.js +24 -1
  18. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  19. package/dist/editor/ai/AgentDocumentList.js +5 -4
  20. package/dist/editor/ai/AgentDocumentList.js.map +1 -1
  21. package/dist/editor/ai/AgentInlineDialogContent.d.ts +17 -0
  22. package/dist/editor/ai/AgentInlineDialogContent.js +32 -0
  23. package/dist/editor/ai/AgentInlineDialogContent.js.map +1 -0
  24. package/dist/editor/ai/AgentSharingSection.d.ts +6 -0
  25. package/dist/editor/ai/AgentSharingSection.js +149 -0
  26. package/dist/editor/ai/AgentSharingSection.js.map +1 -0
  27. package/dist/editor/ai/AgentTerminal.d.ts +2 -1
  28. package/dist/editor/ai/AgentTerminal.js +298 -969
  29. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  30. package/dist/editor/ai/AgentTerminalStatusBar.d.ts +18 -0
  31. package/dist/editor/ai/AgentTerminalStatusBar.js +431 -126
  32. package/dist/editor/ai/AgentTerminalStatusBar.js.map +1 -1
  33. package/dist/editor/ai/Agents.js +1 -1
  34. package/dist/editor/ai/Agents.js.map +1 -1
  35. package/dist/editor/ai/HeartbeatMessage.d.ts +4 -0
  36. package/dist/editor/ai/HeartbeatMessage.js +7 -0
  37. package/dist/editor/ai/HeartbeatMessage.js.map +1 -0
  38. package/dist/editor/ai/InitialThinkingSplash.d.ts +3 -0
  39. package/dist/editor/ai/InitialThinkingSplash.js +9 -0
  40. package/dist/editor/ai/InitialThinkingSplash.js.map +1 -0
  41. package/dist/editor/ai/InlineAiDialog.js +50 -2
  42. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  43. package/dist/editor/ai/QueuedPromptsPanel.d.ts +4 -0
  44. package/dist/editor/ai/QueuedPromptsPanel.js +36 -0
  45. package/dist/editor/ai/QueuedPromptsPanel.js.map +1 -0
  46. package/dist/editor/ai/TodoListPanel.d.ts +5 -0
  47. package/dist/editor/ai/TodoListPanel.js +113 -0
  48. package/dist/editor/ai/TodoListPanel.js.map +1 -0
  49. package/dist/editor/ai/ToolCallDisplay.js +136 -136
  50. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  51. package/dist/editor/ai/UserMessage.d.ts +4 -0
  52. package/dist/editor/ai/UserMessage.js +42 -0
  53. package/dist/editor/ai/UserMessage.js.map +1 -0
  54. package/dist/editor/ai/agentMessageConversion.d.ts +11 -0
  55. package/dist/editor/ai/agentMessageConversion.js +134 -0
  56. package/dist/editor/ai/agentMessageConversion.js.map +1 -0
  57. package/dist/editor/ai/agentMessageGrouping.d.ts +22 -0
  58. package/dist/editor/ai/agentMessageGrouping.js +103 -0
  59. package/dist/editor/ai/agentMessageGrouping.js.map +1 -0
  60. package/dist/editor/ai/agentMessageHelpers.d.ts +23 -0
  61. package/dist/editor/ai/agentMessageHelpers.js +124 -0
  62. package/dist/editor/ai/agentMessageHelpers.js.map +1 -0
  63. package/dist/editor/ai/agentMessageMarkdown.d.ts +2 -0
  64. package/dist/editor/ai/agentMessageMarkdown.js +14 -0
  65. package/dist/editor/ai/agentMessageMarkdown.js.map +1 -0
  66. package/dist/editor/ai/agentTodoExtraction.d.ts +12 -0
  67. package/dist/editor/ai/agentTodoExtraction.js +205 -0
  68. package/dist/editor/ai/agentTodoExtraction.js.map +1 -0
  69. package/dist/editor/client/EditorShell.js +38 -51
  70. package/dist/editor/client/EditorShell.js.map +1 -1
  71. package/dist/editor/client/editContext.d.ts +1 -0
  72. package/dist/editor/client/editContext.js.map +1 -1
  73. package/dist/editor/client/operations.d.ts +3 -1
  74. package/dist/editor/client/operations.js +20 -2
  75. package/dist/editor/client/operations.js.map +1 -1
  76. package/dist/editor/menubar/VersionPreviewCard.js +34 -3
  77. package/dist/editor/menubar/VersionPreviewCard.js.map +1 -1
  78. package/dist/editor/menubar/toolbar-sections/ViewportControls.js +2 -2
  79. package/dist/editor/menubar/toolbar-sections/ViewportControls.js.map +1 -1
  80. package/dist/editor/reviews/SuggestedEdit.js +65 -48
  81. package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
  82. package/dist/editor/reviews/SuggestionDisplayPopover.js +5 -1
  83. package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -1
  84. package/dist/editor/services/agentService.d.ts +24 -0
  85. package/dist/editor/services/agentService.js +40 -0
  86. package/dist/editor/services/agentService.js.map +1 -1
  87. package/dist/editor/services/aiService.d.ts +10 -0
  88. package/dist/editor/services/aiService.js.map +1 -1
  89. package/dist/editor/settings/panels/AgentProfileConfigPanel.js +1 -1
  90. package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -1
  91. package/dist/revision.d.ts +2 -2
  92. package/dist/revision.js +2 -2
  93. package/dist/task-board/TaskBoardWorkspace.js +57 -26
  94. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  95. package/dist/task-board/components/ProjectDashboard.js +2 -3
  96. package/dist/task-board/components/ProjectDashboard.js.map +1 -1
  97. package/dist/task-board/components/TaskBoardTitlebar.js +2 -1
  98. package/dist/task-board/components/TaskBoardTitlebar.js.map +1 -1
  99. package/dist/task-board/components/TaskDetailPanel.d.ts +8 -0
  100. package/dist/task-board/components/TaskDetailPanel.js +5 -3
  101. package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
  102. package/package.json +1 -1
  103. package/styles.css +11 -10
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState, useRef, useCallback, useLayoutEffect, useMemo, } from "react";
3
3
  import { flushSync } from "react-dom";
4
- import { Send, AlertCircle, Loader2, User, Wand2, Square, Mic, MicOff, ChevronDown, ChevronUp, ListTodo, ArrowLeft, DollarSign, ExternalLink, Settings2, Target, X, Plus, } from "lucide-react";
5
- import { getAgent, startAgent, claimAgentBrowser, cancelAgentDialog, assignAgentSkill, persistDraftAgent, updateAgentSettings, updateAgentCostLimit, updateAgentContext, getAgentSkillCatalog, getAgentAvailableTools, getAgentOperationAllowances, getAgentTriggerSubscriptions, cancelAgent, canonicalizeAgentMetadata, getPendingPrompts, getAgentDiagnostics, releaseAgentBrowser, revokeAgentSkill, } from "../services/agentService";
4
+ import { Send, AlertCircle, Loader2, Wand2, Square, Mic, MicOff, ChevronDown, ChevronUp, ArrowLeft, DollarSign, ExternalLink, Settings2, Target, X, Plus, } from "lucide-react";
5
+ import { getAgent, startAgent, claimAgentBrowser, cancelAgentDialog, assignAgentSkill, persistDraftAgent, updateAgentSettings, updateAgentCostLimit, updateAgentContext, getAgentSkillCatalog, getAgentAvailableTools, getAgentOperationAllowances, getAgentTriggerSubscriptions, cancelAgent, canonicalizeAgentMetadata, getPendingPrompts, getAgentDiagnostics, releaseAgentBrowser, rejectToolCall, revokeAgentSkill, } from "../services/agentService";
6
6
  import { parseAgentStatus } from "../services/agentStatus";
7
7
  import { useEditContext, useFieldsEditContext } from "../client/editContext";
8
8
  import { localStorageService } from "../services/localStorageService";
@@ -11,19 +11,31 @@ import { Button } from "../../components/ui/button";
11
11
  import { PlaceholderInput, } from "../../components/ui/PlaceholderInput";
12
12
  import { AiResponseMessage } from "./AiResponseMessage";
13
13
  import { ContextInfoBar } from "./ContextInfoBar";
14
+ import { UserMessage } from "./UserMessage";
15
+ import { HeartbeatMessage } from "./HeartbeatMessage";
16
+ import { calculateTotalTokens, getOperationsForMessageGroup, groupConsecutiveMessages, mergeMessagesById, } from "./agentMessageGrouping";
17
+ import { convertAgentMessagesToAiFormat, extractToolCallFields, stringifyToolField, } from "./agentMessageConversion";
18
+ import { TodoListPanel } from "./TodoListPanel";
19
+ import { extractTodosFromMessages } from "./agentTodoExtraction";
20
+ import { QueuedPromptsPanel } from "./QueuedPromptsPanel";
21
+ import { AgentCapacityBanner, AgentCostLimitBanner, AgentErrorBanner, } from "./AgentBanners";
22
+ import { InitialThinkingSplash } from "./InitialThinkingSplash";
23
+ import { AgentInlineDialogContent } from "./AgentInlineDialogContent";
24
+ import { AGENT_HISTORY_LIMIT, MACHINE_CAPACITY_REASON, buildPlaceholderAgentDetails, formatAllowanceLabel, formatAllowanceSource, getAgentRunMessageAgentId, getAgentRunMessageDetail, getAgentRunMessageSeq, getVisibleDialogRegistry, isAgentErrorStatusValue, isFinishedServerExecutionStatus, isHeartbeatRunEventMessage, mergeAgentOperationHistory, normalizeDialogAgentId, normalizeProfileAllowanceOperations, normalizeServerExecutionStatus, } from "./agentMessageHelpers";
14
25
  import { AgentDocumentList, } from "./AgentDocumentList";
15
26
  import { AgentEditOperationsPanel } from "./EditOperationsPanel";
16
27
  import { SpawnedAgentsPanel } from "./SpawnedAgentsPanel";
28
+ import { AgentSharingSection } from "./AgentSharingSection";
17
29
  import { getComponentById } from "../componentTreeHelper";
18
30
  import { toUserFacingAgentErrorMessage } from "../services/agentErrorMessage";
19
31
  import { AgentGreeting } from "./AgentGreeting";
20
32
  import { getAgentHistory } from "../services/editService";
21
- import { QuestionnaireInline } from "./dialogs/QuestionnaireInline";
22
33
  import { getBrowserCaptureClaim, setBrowserCaptureClaim, } from "./dialogs/browserBoundCapture";
34
+ import { invalidateAiProfilesCache } from "../services/aiService";
23
35
  import { DIALOG_TYPES, } from "./dialogs/agentDialogTypes";
24
36
  import { Popover, PopoverContent, PopoverTrigger, } from "../../components/ui/popover";
25
37
  import { SecretAgentIcon } from "../ui/Icons";
26
- import { formatTime, formatDateTime } from "../utils";
38
+ import { formatTime } from "../utils";
27
39
  import { cn } from "../../lib/utils";
28
40
  import { sanitizeSvg } from "../../lib/sanitize";
29
41
  import { Select } from "../../components/ui/select";
@@ -31,802 +43,22 @@ import { AgentTerminalStatusBar } from "./AgentTerminalStatusBar";
31
43
  import { SimpleTabs } from "../ui/SimpleTabs";
32
44
  import { Splitter } from "../ui/Splitter";
33
45
  import { ScrollingContentTree } from "../ScrollingContentTree";
34
- import { MarkdownDisplay, } from "../../components/MarkdownDisplay";
35
- const AGENT_HISTORY_LIMIT = 1000;
36
46
  const RECENT_RUN_EVENTS_LIMIT = 50;
37
- function mergeAgentOperationHistory(existing, incoming, limit = AGENT_HISTORY_LIMIT) {
38
- const merged = new Map(existing.map((operation) => [operation.id, operation]));
39
- for (const operation of incoming) {
40
- merged.set(operation.id, operation);
41
- }
42
- return Array.from(merged.values())
43
- .sort((left, right) => new Date(right.date).getTime() - new Date(left.date).getTime())
44
- .slice(0, limit);
45
- }
46
- const userMessageMarkdownComponents = {
47
- h1: (props) => (_jsx("h1", { ...props, className: "mb-2 text-sm leading-5 font-semibold text-gray-900" })),
48
- h2: (props) => (_jsx("h2", { ...props, className: "mb-1.5 text-[13px] leading-5 font-semibold text-gray-900" })),
49
- h3: (props) => (_jsx("h3", { ...props, className: "mb-1 text-[12px] leading-5 font-semibold text-gray-900" })),
50
- h4: (props) => (_jsx("h4", { ...props, className: "mb-1 text-[12px] leading-5 font-medium text-gray-800" })),
51
- p: (props) => (_jsx("p", { ...props, className: "my-1 text-[12px] leading-5 text-gray-700" })),
52
- ul: (props) => (_jsx("ul", { ...props, className: "my-2 ml-5 list-disc space-y-1 text-[12px] leading-5 text-gray-700" })),
53
- ol: (props) => (_jsx("ol", { ...props, className: "my-2 ml-5 list-decimal space-y-1 text-[12px] leading-5 text-gray-700" })),
54
- li: (props) => (_jsx("li", { ...props, className: "text-[12px] leading-5 text-gray-700" })),
55
- pre: (props) => (_jsx("pre", { ...props, className: "my-2 overflow-auto rounded-md bg-slate-100 px-3 py-2 text-[11px] leading-4 text-slate-700" })),
56
- code: ({ inline, className, ...props }) => inline ? (_jsx("code", { ...props, className: "rounded bg-slate-100 px-1 py-0.5 text-[11px] text-slate-700" })) : (_jsx("code", { ...props, className: className })),
57
- };
58
- function buildPlaceholderAgentDetails(agentStub) {
59
- const now = new Date().toISOString();
60
- const updated = agentStub.updatedDate || now;
61
- // AgentDetails has required fields, but some workspaces only pass an Agent stub initially.
62
- // This placeholder keeps streaming/tool-call UI working until `getAgent()` returns full details.
63
- return {
64
- ...agentStub,
65
- name: agentStub.name || "Agent",
66
- userId: agentStub.userId || "",
67
- updatedDate: updated,
68
- profileName: agentStub.profileName || "",
69
- model: agentStub.model || "",
70
- createdDate: agentStub.createdDate || updated,
71
- totalTokensUsed: 0,
72
- totalInputTokens: 0,
73
- totalOutputTokens: 0,
74
- totalCachedInputTokens: 0,
75
- totalInputTokenCost: 0,
76
- totalOutputTokenCost: 0,
77
- totalCachedInputTokenCost: 0,
78
- totalImageCost: 0,
79
- totalCost: 0,
80
- currency: agentStub.currency || "USD",
81
- messageCount: agentStub.messageCount || 0,
82
- };
83
- }
84
- function normalizeDialogAgentId(value) {
85
- return value?.trim().toLowerCase() || "";
86
- }
87
- function normalizeServerExecutionStatus(value) {
88
- return (value || "").replace(/\s+/g, "").toLowerCase();
89
- }
90
- function isFinishedServerExecutionStatus(value) {
91
- const normalized = normalizeServerExecutionStatus(value);
92
- return normalized === "completed" || normalized === "cancelled";
93
- }
94
- const MACHINE_CAPACITY_REASON = "machineCapacity";
95
- const MACHINE_CAPACITY_DETAIL = "waitingForCapacity";
96
- function formatAllowanceSource(source) {
97
- const normalized = source?.trim();
98
- if (!normalized)
99
- return null;
100
- if (normalized === "user")
101
- return "User granted";
102
- if (normalized.startsWith("preconfigured:profile:"))
103
- return "Profile";
104
- if (normalized.startsWith("preconfigured:skill:"))
105
- return "Skill";
106
- if (normalized.startsWith("system:")) {
107
- return normalized.slice("system:".length).replace(/[-_]+/g, " ").trim();
108
- }
109
- return normalized;
110
- }
111
- function formatAllowanceLabel(allowance) {
112
- return `${allowance.operationType || "*"}${"itemPath" in allowance
113
- ? ` ${allowance.itemPath}`
114
- : ` ${allowance.normalizedPath}`}`;
115
- }
116
- function getAgentRunMessageAgentId(payload) {
117
- const agentId = payload?.agentId;
118
- return typeof agentId === "string" ? normalizeDialogAgentId(agentId) : null;
119
- }
120
- function getAgentRunMessageSeq(payload) {
121
- const seq = payload?.seq;
122
- return typeof seq === "number" ? seq : null;
123
- }
124
- function getAgentRunMessageDetail(type, payload) {
125
- if (type === "agent:run:delta") {
126
- return payload?.type || null;
127
- }
128
- if (type === "agent:run:status") {
129
- if (payload?.data?.reason === MACHINE_CAPACITY_REASON) {
130
- return MACHINE_CAPACITY_DETAIL;
131
- }
132
- return payload?.data?.state || payload?.data?.status || null;
133
- }
134
- if (type === "agent:run:error") {
135
- return payload?.error || null;
136
- }
137
- if (type === "agent:run:complete") {
138
- return payload?.finalStatus || null;
139
- }
140
- if (type === "agent:run:start") {
141
- return payload?.agentName || null;
142
- }
143
- return null;
144
- }
145
- function isHeartbeatRunEventMessage(message) {
146
- if (!message)
147
- return false;
148
- if (message.type === "agent:run:delta") {
149
- const deltaType = message.payload?.type;
150
- return String(deltaType || "").toLowerCase() === "heartbeat";
151
- }
152
- if (message.type === "agent:run:status") {
153
- const state = message.payload?.data?.state || message.payload?.data?.status;
154
- return String(state || "").toLowerCase() === "heartbeat";
155
- }
156
- return false;
157
- }
158
- function getVisibleDialogRegistry() {
159
- const registry = globalThis.__agentDialogVisibleCallbacks;
160
- return registry && typeof registry === "object" ? registry : {};
161
- }
162
- function isAgentErrorStatusValue(status) {
163
- return status === "error";
164
- }
165
- // Simple user message component
166
- const UserMessage = ({ message }) => {
167
- const content = message.content || "";
168
- const [isTriggerExpanded, setIsTriggerExpanded] = useState(false);
169
- // Trigger-sourced prompts are prefixed by backend as "[Trigger: {name}]: {content}"
170
- const triggerPattern = /^\[Trigger: ([^\]]+)\]:\s*(.*)$/s;
171
- const triggerMatch = content.match(triggerPattern);
172
- const triggerName = triggerMatch?.[1]?.trim() || "";
173
- const triggerContent = triggerMatch?.[2] || "";
174
- const isTriggerMessage = triggerName.length > 0;
175
- if (isTriggerMessage) {
176
- return (_jsx("div", { className: "px-4 py-2", children: _jsxs("div", { className: "text-[11px]", children: [_jsxs("button", { type: "button", onClick: () => setIsTriggerExpanded((expanded) => !expanded), className: "text-theme-secondary hover:bg-theme-hover flex w-full items-center gap-2 rounded-md border-l-2 border-cyan-200 px-2 py-1 text-left transition-colors", "data-testid": "trigger-message-toggle", "data-expanded": isTriggerExpanded ? "true" : "false", children: [_jsx(Target, { className: "h-3.5 w-3.5 shrink-0", strokeWidth: 1.5 }), _jsxs("span", { className: "truncate font-medium", children: ["Trigger: ", triggerName] }), message.createdDate && (_jsx("span", { className: "ml-1 shrink-0 text-[10px] text-gray-400", children: formatTime(new Date(message.createdDate)) })), _jsx("span", { className: "ml-auto shrink-0", children: isTriggerExpanded ? (_jsx(ChevronUp, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })) })] }), isTriggerExpanded && (_jsx("div", { className: "mt-1 border-l-2 border-cyan-100 pl-[1.35rem] text-[11px] text-gray-600", children: _jsx(MarkdownDisplay, { source: triggerContent, components: userMessageMarkdownComponents }) }))] }) }));
177
- }
178
- // Parse source agent name from content if it starts with "[From ...]:"
179
- // Backend formats messages from other agents as "[From {sourceAgentName}]: {content}"
180
- const fromPattern = /^\[From ([^\]]+)\]:\s*(.*)$/s;
181
- const match = content.match(fromPattern);
182
- let sourceAgentName;
183
- let displayContent = content;
184
- if (match && match[1]) {
185
- const extractedName = match[1];
186
- // If it says "From User", treat it as a regular user message
187
- if (extractedName.toLowerCase() === "user") {
188
- sourceAgentName = undefined;
189
- displayContent = content; // Keep original content
190
- }
191
- else {
192
- sourceAgentName = extractedName;
193
- displayContent = match[2] || ""; // Remove the "[From ...]:" prefix from content
194
- }
195
- }
196
- else {
197
- // Fallback: check for source agent name in message metadata or properties
198
- sourceAgentName =
199
- message.sourceAgentName ||
200
- message.metadata?.sourceAgentName ||
201
- message.sourceAgent?.name;
202
- }
203
- const displayName = sourceAgentName ? `[From ${sourceAgentName}]` : "You";
204
- return (_jsxs("div", { className: "flex gap-3 p-4", children: [_jsx("div", { className: "shrink-0", children: _jsx(User, { className: "text-theme-secondary h-5 w-5", strokeWidth: 1 }) }), _jsxs("div", { className: "min-w-0 flex-1 select-text", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2", children: [_jsx("span", { className: "text-[12px] font-medium text-gray-900", children: displayName }), message.createdDate && (_jsx("span", { className: "text-[12px] text-gray-400", "data-testid": "user-message-timestamp", "data-timestamp": message.createdDate, children: formatTime(new Date(message.createdDate)) }))] }), _jsx("div", { className: "prose prose max-w-none text-[12px] text-gray-700 select-text", children: _jsx(MarkdownDisplay, { source: displayContent, components: userMessageMarkdownComponents }) })] })] }));
205
- };
206
- const HeartbeatMessage = ({ message }) => {
207
- return (_jsx("div", { className: "px-4 py-2", "data-testid": "agent-heartbeat-message", children: _jsxs("div", { className: "flex items-center gap-2 rounded-md border border-sky-100 bg-sky-50/80 px-3 py-2 text-[11px] text-sky-700", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", strokeWidth: 1.5 }), _jsx("span", { className: "min-w-0 flex-1", children: message.content }), message.createdDate && (_jsx("span", { className: "shrink-0 text-[10px] text-sky-500", children: formatTime(new Date(message.createdDate)) }))] }) }));
208
- };
209
- // Helper to extract todos from potentially incomplete JSON during streaming
210
- const extractPartialTodos = (jsonText) => {
211
- // First try to parse complete JSON
212
- try {
213
- const parsed = JSON.parse(jsonText);
214
- const result = Array.isArray(parsed) ? parsed : parsed?.items || [];
215
- return result;
216
- }
217
- catch (e) {
218
- // If JSON is incomplete, try to extract whatever todo items we can find
219
- const items = [];
220
- // Look for individual todo objects in the partial JSON
221
- // Match patterns like: { "text": "...", "done": false, "note": "..." }
222
- // Handle various field orderings (text can be anywhere in the object)
223
- const textPattern = /"text"\s*:\s*"([^"]+)"/g;
224
- const textMatches = [];
225
- let textMatch;
226
- while ((textMatch = textPattern.exec(jsonText)) !== null) {
227
- if (textMatch[1]) {
228
- textMatches.push({
229
- text: textMatch[1],
230
- startIdx: textMatch.index,
231
- });
232
- }
233
- }
234
- // For each text field found, try to find the enclosing object
235
- for (const { text, startIdx } of textMatches) {
236
- // Find the opening brace before this text field
237
- let openBrace = -1;
238
- for (let i = startIdx - 1; i >= 0; i--) {
239
- if (jsonText[i] === "{") {
240
- openBrace = i;
241
- break;
242
- }
243
- if (jsonText[i] === "}")
244
- break; // Hit another object's end
245
- }
246
- if (openBrace === -1)
247
- continue;
248
- // Find the closing brace after this text field
249
- let closeBrace = -1;
250
- let depth = 0;
251
- for (let i = openBrace; i < jsonText.length; i++) {
252
- if (jsonText[i] === "{")
253
- depth++;
254
- if (jsonText[i] === "}") {
255
- depth--;
256
- if (depth === 0) {
257
- closeBrace = i;
258
- break;
259
- }
260
- }
261
- }
262
- // Extract the object and try to parse it
263
- const objStr = closeBrace !== -1
264
- ? jsonText.substring(openBrace, closeBrace + 1)
265
- : jsonText.substring(openBrace) + "}"; // Try to close incomplete object
266
- try {
267
- const obj = JSON.parse(objStr);
268
- if (obj.title || obj.text) {
269
- items.push({
270
- title: obj.title || obj.text,
271
- status: obj.status || "pending",
272
- instructions: obj.instructions || obj.note || undefined,
273
- agentProfileId: obj.agentProfileId || undefined,
274
- });
275
- }
276
- }
277
- catch (e) {
278
- // Skip malformed objects
279
- }
280
- }
281
- // Also try to extract from partial objects at the end
282
- // Look for the last opening brace and try to parse up to where we have valid content
283
- const lines = jsonText.split("\n");
284
- for (let i = lines.length - 1; i >= 0; i--) {
285
- const partialJson = lines.slice(0, i + 1).join("\n");
286
- // Try to close any open braces/brackets
287
- let testJson = partialJson;
288
- const openBraces = (testJson.match(/\{/g) || []).length;
289
- const closeBraces = (testJson.match(/\}/g) || []).length;
290
- const openBrackets = (testJson.match(/\[/g) || []).length;
291
- const closeBrackets = (testJson.match(/\]/g) || []).length;
292
- // Add missing closing characters
293
- testJson += "]".repeat(openBrackets - closeBrackets);
294
- testJson += "}".repeat(openBraces - closeBraces);
295
- try {
296
- const parsed = JSON.parse(testJson);
297
- const partialItems = Array.isArray(parsed)
298
- ? parsed
299
- : parsed?.items || [];
300
- if (partialItems.length > items.length) {
301
- return partialItems;
302
- }
303
- }
304
- catch (e) {
305
- continue;
306
- }
307
- }
308
- return items;
309
- }
310
- };
311
- const extractTodosFromMessages = (messages) => {
312
- const todos = [];
313
- const fencedTodoToken = "```todo_list";
314
- const plainTodoToken = "todo_list";
315
- for (const message of messages) {
316
- if (message.role !== "assistant" || !message.content)
317
- continue;
318
- const content = message.content;
319
- let cursor = 0;
320
- while (cursor < content.length) {
321
- const nextFenced = content.indexOf(fencedTodoToken, cursor);
322
- const nextPlain = content.indexOf(plainTodoToken, cursor);
323
- let todoStart = -1;
324
- let isFenced = false;
325
- if (nextFenced !== -1 && (nextPlain === -1 || nextFenced < nextPlain)) {
326
- todoStart = nextFenced;
327
- isFenced = true;
328
- }
329
- else if (nextPlain !== -1) {
330
- // Check if it's at line start
331
- const before = nextPlain > 0 ? content[nextPlain - 1] : "\n";
332
- if (before === "\n" || before === "\r" || nextPlain === 0) {
333
- todoStart = nextPlain;
334
- isFenced = false;
335
- }
336
- }
337
- if (todoStart === -1)
338
- break;
339
- try {
340
- let jsonText = "";
341
- let isComplete = true;
342
- if (isFenced) {
343
- const afterToken = todoStart + fencedTodoToken.length;
344
- const closePos = content.indexOf("```", afterToken);
345
- if (closePos === -1) {
346
- // Incomplete fenced block - extract what we have so far
347
- jsonText = content.slice(afterToken).trim();
348
- isComplete = false;
349
- cursor = content.length; // Process till end
350
- }
351
- else {
352
- jsonText = content.slice(afterToken, closePos).trim();
353
- cursor = closePos + 3;
354
- }
355
- }
356
- else {
357
- const afterToken = todoStart + plainTodoToken.length;
358
- const braceStart = content.indexOf("{", afterToken);
359
- if (braceStart === -1)
360
- break;
361
- let depth = 0;
362
- let braceEnd = -1;
363
- for (let i = braceStart; i < content.length; i++) {
364
- if (content[i] === "{")
365
- depth++;
366
- if (content[i] === "}") {
367
- depth--;
368
- if (depth === 0) {
369
- braceEnd = i;
370
- break;
371
- }
372
- }
373
- }
374
- if (braceEnd === -1) {
375
- // Incomplete JSON - extract what we have
376
- jsonText = content.slice(braceStart).trim();
377
- isComplete = false;
378
- cursor = content.length;
379
- }
380
- else {
381
- jsonText = content.slice(braceStart, braceEnd + 1).trim();
382
- cursor = braceEnd + 1;
383
- }
384
- }
385
- // Use the partial extraction helper for incomplete JSON
386
- const todoItems = isComplete
387
- ? (() => {
388
- try {
389
- const parsed = JSON.parse(jsonText);
390
- return Array.isArray(parsed) ? parsed : parsed?.items || [];
391
- }
392
- catch (e) {
393
- return [];
394
- }
395
- })()
396
- : extractPartialTodos(jsonText);
397
- const title = (() => {
398
- try {
399
- const parsed = JSON.parse(jsonText);
400
- return Array.isArray(parsed) ? undefined : parsed?.title;
401
- }
402
- catch (e) {
403
- return undefined;
404
- }
405
- })();
406
- todoItems.forEach((item) => {
407
- if (!item)
408
- return;
409
- const title = item.title ||
410
- item.text ||
411
- item.content ||
412
- item.label ||
413
- String(item.task || "");
414
- if (!title)
415
- return;
416
- todos.push({
417
- id: item.id,
418
- title,
419
- status: item.status || "pending",
420
- instructions: item.instructions || item.note || item.description,
421
- agentProfileId: item.agentProfileId,
422
- messageId: message.id,
423
- sourceTitle: item.sourceTitle,
424
- });
425
- });
426
- }
427
- catch (e) {
428
- cursor++;
429
- continue;
430
- }
431
- }
432
- }
433
- // Deduplicate todos by ID (or title if no ID), keeping the latest version
434
- // Later entries (from more recent messages) overwrite earlier ones
435
- const todoMap = new Map();
436
- for (const todo of todos) {
437
- const key = todo.id || todo.title;
438
- todoMap.set(key, todo);
439
- }
440
- const result = Array.from(todoMap.values());
441
- return result;
442
- };
443
- // TodoListPanel component
444
- const TodoListPanel = ({ messages, agentMetadata, }) => {
445
- const [isExpanded, setIsExpanded] = useState(true);
446
- // First try to get todos from agent metadata (real-time updates)
447
- // Server sends additionalData.todoList directly via contextChanged status
448
- const metadataTodos = (() => {
449
- try {
450
- const todoList = agentMetadata?.additionalData?.todoList;
451
- if (todoList?.items && Array.isArray(todoList.items)) {
452
- const rawItems = todoList.items
453
- .map((item, idx) => ({
454
- id: item.id || `metadata-${idx}`,
455
- title: item.title || item.text || item.label || String(item.task || ""),
456
- status: item.status || "pending",
457
- instructions: item.instructions || item.note || item.description,
458
- agentProfileId: item.agentProfileId,
459
- messageId: undefined,
460
- sourceTitle: todoList.title,
461
- }))
462
- .filter((item) => item.title);
463
- // Apply deduplication to metadata todos as well
464
- const todoMap = new Map();
465
- for (const todo of rawItems) {
466
- const key = todo.id || todo.title;
467
- todoMap.set(key, todo);
468
- }
469
- return Array.from(todoMap.values());
470
- }
471
- }
472
- catch (e) {
473
- console.error("📋 Error extracting todos from metadata:", e);
474
- }
475
- return null;
476
- })();
477
- // If we have metadata todos, use them; otherwise extract from messages
478
- const usingMetadata = metadataTodos && metadataTodos.length > 0;
479
- const todos = usingMetadata
480
- ? metadataTodos
481
- : extractTodosFromMessages(messages);
482
- // Check if there's an active streaming message with incomplete todo content
483
- const isUpdating = messages.some((msg) => {
484
- if (msg.role !== "assistant" || msg.isCompleted)
485
- return false;
486
- const content = msg.content || "";
487
- // Check for incomplete fenced todo blocks
488
- const fencedStart = content.indexOf("```todo_list");
489
- if (fencedStart !== -1) {
490
- const afterStart = fencedStart + "```todo_list".length;
491
- const closePos = content.indexOf("```", afterStart);
492
- if (closePos === -1) {
493
- // Incomplete fenced block
494
- return true;
495
- }
496
- }
497
- // Check for incomplete plain todo blocks
498
- const plainStart = content.indexOf("todo_list");
499
- if (plainStart !== -1 && plainStart !== fencedStart) {
500
- const before = plainStart > 0 ? content[plainStart - 1] : "\n";
501
- if (before === "\n" || before === "\r" || plainStart === 0) {
502
- const braceStart = content.indexOf("{", plainStart);
503
- if (braceStart !== -1) {
504
- let depth = 0;
505
- let braceEnd = -1;
506
- for (let i = braceStart; i < content.length; i++) {
507
- if (content[i] === "{")
508
- depth++;
509
- if (content[i] === "}") {
510
- depth--;
511
- if (depth === 0) {
512
- braceEnd = i;
513
- break;
514
- }
515
- }
516
- }
517
- if (braceEnd === -1) {
518
- // Incomplete plain block
519
- return true;
520
- }
521
- }
522
- }
523
- }
524
- return false;
525
- });
526
- if (todos.length === 0 && !isUpdating)
527
- return null;
528
- const completedCount = todos.filter((t) => t.status === "completed").length;
529
- const totalCount = todos.length;
530
- const getStatusIcon = (status) => {
531
- switch (status) {
532
- case "completed":
533
- return (_jsx("div", { className: "flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500", children: _jsx("svg", { className: "h-3 w-3 text-white", fill: "none", strokeWidth: 2, stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) }));
534
- case "in_progress":
535
- return (_jsx("div", { className: "flex h-4 w-4 items-center justify-center rounded border-2 border-blue-500 bg-blue-500", children: _jsx(Loader2, { className: "h-3 w-3 animate-spin text-white", strokeWidth: 2 }) }));
536
- case "cancelled":
537
- return (_jsx("div", { className: "flex h-4 w-4 items-center justify-center rounded border-2 border-gray-400 bg-gray-400", children: _jsx("svg", { className: "h-3 w-3 text-white", fill: "none", strokeWidth: 2, stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }));
538
- default: // pending
539
- return _jsx("div", { className: "h-4 w-4 rounded border-2 border-gray-300" });
540
- }
541
- };
542
- const getTextClass = (status) => {
543
- switch (status) {
544
- case "completed":
545
- return "text-gray-500 line-through";
546
- case "cancelled":
547
- return "text-gray-400 line-through";
548
- case "in_progress":
549
- return "text-blue-700 font-medium";
550
- default:
551
- return "text-gray-900";
552
- }
553
- };
554
- return (_jsxs("div", { className: "border-t border-gray-200 bg-gray-50", children: [_jsxs("button", { onClick: () => setIsExpanded(!isExpanded), className: "flex w-full cursor-pointer items-center justify-between px-4 py-2 text-left transition-colors hover:bg-gray-100", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ListTodo, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 }), _jsx("span", { className: "text-[11px] font-medium text-gray-700", children: "Todo List" }), isUpdating ? (_jsxs("span", { className: "text-theme-secondary flex items-center gap-1 text-[11px]", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin", strokeWidth: 1 }), "Updating..."] })) : (_jsxs("span", { className: "text-[11px] text-gray-500", children: [completedCount, "/", totalCount, " completed"] }))] }), isExpanded ? (_jsx(ChevronUp, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 })) : (_jsx(ChevronDown, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 }))] }), isExpanded && (_jsxs("div", { className: "max-h-64 overflow-y-auto px-4 pb-3", children: [todos.length > 0 && (_jsx("div", { className: "space-y-1.5", children: todos.map((todo, idx) => (_jsxs("div", { className: "flex items-start gap-2 rounded bg-white p-2 text-[11px]", children: [_jsx("div", { className: "shrink-0 pt-0.5", children: getStatusIcon(todo.status) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: getTextClass(todo.status), children: todo.title }), todo.instructions && (_jsx("div", { className: "mt-0.5 text-[11px] text-gray-500", children: todo.instructions }))] })] }, todo.id || `${todo.messageId}-${idx}`))) })), isUpdating && (_jsxs("div", { className: `flex items-center gap-2 rounded px-3 py-2 text-[11px] ${todos.length > 0
555
- ? "bg-theme-secondary-light text-theme-secondary mt-2"
556
- : "justify-center bg-white text-gray-500"}`, children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin", strokeWidth: 1 }), _jsx("span", { children: todos.length > 0
557
- ? "Updating todo list..."
558
- : "Loading todo list..." })] }))] }))] }));
559
- };
560
- const groupConsecutiveMessages = (agentMessages) => {
561
- // Work directly with the messages array - streaming messages are identified by their properties
562
- const allMessages = agentMessages;
563
- const groups = [];
564
- let currentAssistantGroup = [];
565
- for (const message of allMessages) {
566
- if (message.role === "user") {
567
- // Finish any current assistant group
568
- if (currentAssistantGroup.length > 0) {
569
- groups.push({
570
- type: "assistant-group",
571
- messages: currentAssistantGroup,
572
- });
573
- currentAssistantGroup = [];
574
- }
575
- // Add user message
576
- groups.push({ type: "user", messages: [message] });
577
- }
578
- else if (message.messageType === "heartbeat" ||
579
- message.role === "system") {
580
- if (currentAssistantGroup.length > 0) {
581
- groups.push({
582
- type: "assistant-group",
583
- messages: currentAssistantGroup,
584
- });
585
- currentAssistantGroup = [];
586
- }
587
- groups.push({ type: "heartbeat", messages: [message] });
588
- }
589
- else if (message.role === "assistant") {
590
- // Add to current assistant group
591
- currentAssistantGroup.push(message);
592
- }
593
- // Skip tool messages as they're handled within assistant messages
594
- }
595
- // Add any remaining assistant group
596
- if (currentAssistantGroup.length > 0) {
597
- groups.push({ type: "assistant-group", messages: currentAssistantGroup });
598
- }
599
- return groups;
600
- };
601
- // Merge messages from DB and local state with ID-based deduplication
602
- const mergeMessagesById = (dbMessages, localMessages) => {
603
- const messageMap = new Map();
604
- // Normalize ID key (lowercase) to avoid duplicates caused by casing differences
605
- const keyOf = (id) => (id ? id.toLowerCase() : "");
606
- // First, add all DB messages (source of truth for completed messages)
607
- dbMessages.forEach((msg) => {
608
- if (msg.id)
609
- messageMap.set(keyOf(msg.id), msg);
610
- });
611
- // Then merge local messages (preserve streaming state, prefer longer content)
612
- localMessages.forEach((localMsg) => {
613
- if (!localMsg.id)
614
- return;
615
- const key = keyOf(localMsg.id);
616
- const existingMsg = messageMap.get(key);
617
- if (!existingMsg) {
618
- // New message only in local state (e.g., streaming)
619
- messageMap.set(key, localMsg);
620
- }
621
- else if (!localMsg.isCompleted && localMsg.messageType === "streaming") {
622
- // Keep streaming version if more recent/longer (considering both content and reasoning)
623
- const localLen = (localMsg.content?.length || 0) + (localMsg.reasoning?.length || 0);
624
- const existingLen = (existingMsg.content?.length || 0) +
625
- (existingMsg.reasoning?.length || 0);
626
- if (localLen > existingLen) {
627
- messageMap.set(key, localMsg);
628
- }
629
- }
630
- // Otherwise, keep the DB version (completed messages from DB are authoritative)
631
- });
632
- // Sort by messageIndex or createdDate to maintain order
633
- // Ignore messageIndex if it's -1 (unassigned/streaming)
634
- return Array.from(messageMap.values()).sort((a, b) => {
635
- const aIndex = a.messageIndex === -1 ? Infinity : a.messageIndex;
636
- const bIndex = b.messageIndex === -1 ? Infinity : b.messageIndex;
637
- if (aIndex !== bIndex) {
638
- return aIndex - bIndex;
639
- }
640
- return (new Date(a.createdDate || 0).getTime() -
641
- new Date(b.createdDate || 0).getTime());
642
- });
643
- };
644
- // Calculate total token usage and cost data from agent messages
645
- const calculateTotalTokens = (messages) => {
646
- const totals = messages.reduce((acc, message) => {
647
- return {
648
- input: acc.input + (message.inputTokens || 0),
649
- output: acc.output + (message.outputTokens || 0),
650
- cached: acc.cached + (message.cachedInputTokens || 0),
651
- cacheWrite: acc.cacheWrite,
652
- inputCost: acc.inputCost + (message.inputTokenCost || 0),
653
- outputCost: acc.outputCost + (message.outputTokenCost || 0),
654
- cachedCost: acc.cachedCost + (message.cachedInputTokenCost || 0),
655
- cacheWriteCost: acc.cacheWriteCost,
656
- imageCost: acc.imageCost,
657
- totalCost: acc.totalCost + (message.totalCost || 0),
658
- };
659
- }, {
660
- input: 0,
661
- output: 0,
662
- cached: 0,
663
- cacheWrite: 0,
664
- inputCost: 0,
665
- outputCost: 0,
666
- cachedCost: 0,
667
- cacheWriteCost: 0,
668
- imageCost: 0,
669
- totalCost: 0,
670
- });
671
- return totals;
672
- };
673
- // Get edit operations for a message group by matching toolCallIds
674
- const getOperationsForMessageGroup = (messages, agentOperations) => {
675
- const toolCallIds = new Set(messages.flatMap((m) => m.tool_calls?.map((tc) => tc.id) ?? []));
676
- const matched = agentOperations.filter((op) => op.toolCallId && toolCallIds.has(op.toolCallId));
677
- return matched;
678
- };
679
- const stringifyToolField = (value) => {
680
- if (value === undefined || value === null)
681
- return undefined;
682
- if (typeof value === "string") {
683
- return value.trim().length > 0 ? value : undefined;
684
- }
685
- try {
686
- return JSON.stringify(value);
687
- }
688
- catch {
689
- return String(value);
690
- }
691
- };
692
- const parseToolResultValue = (value) => {
693
- if (value === undefined || value === null) {
694
- return undefined;
695
- }
696
- if (typeof value === "object") {
697
- return value;
698
- }
699
- if (typeof value !== "string") {
700
- return String(value);
701
- }
702
- const trimmed = value.trim();
703
- if (!trimmed) {
704
- return undefined;
705
- }
706
- try {
707
- let parsed = JSON.parse(trimmed);
708
- if (typeof parsed === "string" &&
709
- (parsed.startsWith("{") || parsed.startsWith("["))) {
710
- parsed = JSON.parse(parsed);
711
- }
712
- if (parsed && typeof parsed === "object") {
713
- return parsed;
714
- }
715
- }
716
- catch {
717
- // Fall back to the original string when the payload is plain text.
718
- }
719
- return value;
720
- };
721
- const getFirstToolCallEnvelope = (data) => {
722
- if (!data || typeof data !== "object")
723
- return undefined;
724
- const direct = data.toolCall || data.tool_call;
725
- if (direct)
726
- return direct;
727
- const arrayCandidates = [data.tool_calls, data.toolCalls];
728
- for (const candidate of arrayCandidates) {
729
- if (Array.isArray(candidate) && candidate.length > 0) {
730
- return candidate[0];
731
- }
732
- }
733
- return undefined;
734
- };
735
- const extractToolCallFields = (data) => {
736
- const envelope = getFirstToolCallEnvelope(data);
737
- const functionPayload = data?.function || envelope?.function;
738
- const functionName = data?.functionName ||
739
- data?.name ||
740
- functionPayload?.name ||
741
- envelope?.functionName ||
742
- envelope?.name ||
743
- "unknown";
744
- const toolCallId = data?.toolCallId || data?.id || envelope?.id;
745
- const functionArguments = stringifyToolField(data?.functionArguments) ||
746
- stringifyToolField(data?.arguments) ||
747
- stringifyToolField(functionPayload?.arguments) ||
748
- stringifyToolField(envelope?.functionArguments) ||
749
- stringifyToolField(envelope?.arguments) ||
750
- "{}";
751
- return {
752
- toolCallId,
753
- functionName,
754
- functionArguments,
755
- };
756
- };
757
- // Convert agent messages to AI terminal format for a response group
758
- const convertAgentMessagesToAiFormat = (agentMessages) => {
759
- return agentMessages.map((agentMessage) => {
760
- const message = {
761
- id: agentMessage.id,
762
- content: agentMessage.content,
763
- reasoning: agentMessage.reasoning,
764
- formattedContent: agentMessage.content
765
- ?.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>")
766
- ?.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>")
767
- ?.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>")
768
- ?.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>")
769
- ?.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>")
770
- ?.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>")
771
- ?.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
772
- ?.replace(/\n/g, "<br/>"),
773
- name: agentMessage.name,
774
- role: agentMessage.role,
775
- createdDate: agentMessage.createdDate,
776
- tool_calls: agentMessage.toolCalls
777
- ? agentMessage.toolCalls.map((toolCall) => {
778
- const isPruned = !!toolCall.isPruned ||
779
- /^PRUNED$/i.test(toolCall.functionError || "");
780
- const displayResult = parseToolResultValue(toolCall.functionResultRichContent) ?? toolCall.functionResult;
781
- return {
782
- id: toolCall.toolCallId,
783
- displayName: toolCall.functionName,
784
- function: {
785
- name: toolCall.functionName,
786
- arguments: toolCall.functionArguments,
787
- result: displayResult,
788
- error: toolCall.functionError,
789
- },
790
- // Pass through approval info if present on the tool call
791
- requiresApproval: toolCall.requiresApproval,
792
- // Pass through prune metadata so the terminal can render a neutral state
793
- isPruned,
794
- prunedAt: toolCall.prunedAt,
795
- // Pass through isCompleted so ToolCallDisplay knows when to hide spinner
796
- isCompleted: toolCall.isCompleted,
797
- // Tool call is streaming if message is not completed and tool call has no result yet
798
- isStreaming: !agentMessage.isCompleted &&
799
- !toolCall.isCompleted &&
800
- !displayResult &&
801
- !toolCall.functionError &&
802
- !isPruned,
803
- // Pass through message IDs for approval/rejection events
804
- messageId: toolCall.messageId,
805
- dbMessageId: toolCall.dbMessageId,
806
- responseTimeMs: toolCall.responseTimeMs,
807
- createdDate: toolCall.createdDate,
808
- };
809
- })
810
- : [],
811
- };
812
- if (agentMessage.toolCallId) {
813
- message.tool_call_id = agentMessage.toolCallId;
814
- }
815
- return message;
816
- });
817
- };
818
47
  // interface AgentTerminalProps {
819
48
  // agentStub: Agent;
820
49
  // }
821
- export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive = true, compact = false, displayMode = "full", showSummaryInput = false, hideContext = false, hideBottomControls = false, hideGreeting = false, defaultCollapseJson = false, simpleMode = false, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages = false, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder = false, }) {
50
+ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive = true, compact = false, displayMode = "full", showSummaryInput = false, hideContext = false, hideBottomControls = false, hideGreeting = false, defaultCollapseJson = false, simpleMode = false, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages = false, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder = false, }) {
822
51
  const editContext = useEditContext();
823
52
  const fieldsContext = useFieldsEditContext();
824
53
  const [agent, setAgent] = useState(() => buildPlaceholderAgentDetails(agentStub));
825
54
  const [messages, setMessages] = useState([]);
826
55
  const [agentOperations, setAgentOperations] = useState([]);
827
- const [prompt, setPrompt] = useState("");
56
+ const [prompt, setPrompt] = useState(() => localStorageService.getItem(`editor.agent.draftPrompt.${agentStub.id}`) || "");
828
57
  const [inputPlaceholder, setInputPlaceholder] = useState("Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)");
829
- const [isLoading, setIsLoading] = useState(false);
58
+ // Existing (persisted) agents start in loading state so the loading
59
+ // screen renders on first paint — avoids an empty-terminal flash between
60
+ // mount and the loadAgent() effect.
61
+ const [isLoading, setIsLoading] = useState(() => agentStub.status !== "new");
830
62
  const [isConnecting, setIsConnecting] = useState(false);
831
63
  const [isSubmitting, setIsSubmitting] = useState(false);
832
64
  const [activePlaceholderInput, setActivePlaceholderInput] = useState(null);
@@ -1154,7 +386,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
1154
386
  };
1155
387
  const [mode, setMode] = useState("supervised");
1156
388
  const [queuedPrompts, setQueuedPrompts] = useState([]);
1157
- const [expandedQueuedTriggerIds, setExpandedQueuedTriggerIds] = useState({});
1158
389
  const [contextPanelsActiveTab, setContextPanelsActiveTab] = useState(0);
1159
390
  const [hiddenContextPanelTabIds, setHiddenContextPanelTabIds] = useState(new Set());
1160
391
  const [showCostAndAgent, setShowCostAndAgent] = useState(false);
@@ -1364,6 +595,11 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
1364
595
  active = false;
1365
596
  };
1366
597
  }
598
+ // Always refresh profiles (and therefore available models) when the
599
+ // Agent settings popover opens, so changes made in Sitecore are picked
600
+ // up even if the websocket invalidation was missed.
601
+ invalidateAiProfilesCache();
602
+ void onReloadProfiles?.();
1367
603
  if (!agent?.id || isLocalOnlyDraftAgent) {
1368
604
  setTriggerSubscriptions([]);
1369
605
  setTriggerSubscriptionsLoading(false);
@@ -1477,24 +713,71 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
1477
713
  agent?.userId,
1478
714
  agent?.profileId,
1479
715
  mode,
716
+ onReloadProfiles,
1480
717
  ]);
1481
718
  const activeTriggerSubscriptions = useMemo(() => triggerSubscriptions.filter((sub) => sub.isActive), [triggerSubscriptions]);
719
+ const profileOperationAllowances = useMemo(() => {
720
+ const profileSource = activeProfile?.id
721
+ ? `preconfigured:profile:${activeProfile.id}`
722
+ : "preconfigured:profile";
723
+ return {
724
+ sitecore: (activeProfile?.sitecoreAllowances ?? []).flatMap((allowance) => {
725
+ const itemIdentifier = String(allowance?.itemIdentifier || "").trim();
726
+ if (!itemIdentifier)
727
+ return [];
728
+ return normalizeProfileAllowanceOperations(allowance.operationTypes).map((operationType) => ({
729
+ itemId: itemIdentifier,
730
+ itemPath: itemIdentifier,
731
+ operationType,
732
+ scopeType: "itemSubtree",
733
+ source: profileSource,
734
+ grantedBy: null,
735
+ grantedAt: null,
736
+ }));
737
+ }),
738
+ filesystem: (activeProfile?.filesystemAllowances ?? []).flatMap((allowance) => {
739
+ const normalizedPath = String(allowance?.path || "").trim();
740
+ if (!normalizedPath)
741
+ return [];
742
+ return normalizeProfileAllowanceOperations(allowance.operationTypes).map((operationType) => ({
743
+ normalizedPath,
744
+ operationType,
745
+ source: profileSource,
746
+ grantedBy: null,
747
+ grantedAt: null,
748
+ }));
749
+ }),
750
+ };
751
+ }, [
752
+ activeProfile?.filesystemAllowances,
753
+ activeProfile?.id,
754
+ activeProfile?.sitecoreAllowances,
755
+ ]);
756
+ const displayedOperationAllowances = isLocalOnlyDraftAgent
757
+ ? profileOperationAllowances
758
+ : operationAllowances;
759
+ const displayedOperationAllowancesLoading = isLocalOnlyDraftAgent
760
+ ? false
761
+ : operationAllowancesLoading;
762
+ const displayedOperationAllowancesError = isLocalOnlyDraftAgent
763
+ ? null
764
+ : operationAllowancesError;
1482
765
  const allowanceGroups = useMemo(() => [
1483
766
  {
1484
767
  key: "sitecore",
1485
768
  label: "Sitecore",
1486
- rows: operationAllowances.sitecore,
769
+ rows: displayedOperationAllowances.sitecore,
1487
770
  },
1488
771
  {
1489
772
  key: "filesystem",
1490
773
  label: "Filesystem",
1491
- rows: operationAllowances.filesystem,
774
+ rows: displayedOperationAllowances.filesystem,
1492
775
  },
1493
- ], [operationAllowances]);
1494
- const hasAnyAllowances = useMemo(() => operationAllowances.sitecore.length > 0 ||
1495
- operationAllowances.filesystem.length > 0, [operationAllowances]);
1496
- const allowancesTotalCount = useMemo(() => operationAllowances.sitecore.length +
1497
- operationAllowances.filesystem.length, [operationAllowances]);
776
+ ], [displayedOperationAllowances]);
777
+ const hasAnyAllowances = useMemo(() => displayedOperationAllowances.sitecore.length > 0 ||
778
+ displayedOperationAllowances.filesystem.length > 0, [displayedOperationAllowances]);
779
+ const allowancesTotalCount = useMemo(() => displayedOperationAllowances.sitecore.length +
780
+ displayedOperationAllowances.filesystem.length, [displayedOperationAllowances]);
1498
781
  const listedProfileSkillIdSet = useMemo(() => {
1499
782
  const ids = [
1500
783
  ...(activeProfile?.availableSkills ?? []),
@@ -1813,6 +1096,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
1813
1096
  useEffect(() => {
1814
1097
  localStorageService.setItem("editor.agent.promptHistory", promptHistory);
1815
1098
  }, [promptHistory]);
1099
+ // Rehydrate draft when switching to a different agent so each agent keeps its own draft.
1100
+ useEffect(() => {
1101
+ setPrompt(localStorageService.getItem(`editor.agent.draftPrompt.${agentStub.id}`) || "");
1102
+ }, [agentStub.id]);
1103
+ // Persist the in-progress prompt per agent so it survives workspace switches/unmounts.
1104
+ useEffect(() => {
1105
+ const key = `editor.agent.draftPrompt.${agentStub.id}`;
1106
+ if (prompt) {
1107
+ localStorageService.setItem(key, prompt);
1108
+ }
1109
+ else {
1110
+ localStorageService.removeItem(key);
1111
+ }
1112
+ }, [prompt, agentStub.id]);
1816
1113
  useEffect(() => {
1817
1114
  // Keep messagesRef synchronized with messages state
1818
1115
  messagesRef.current = messages;
@@ -1858,6 +1155,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
1858
1155
  const documentListRef = useRef(null);
1859
1156
  const placeholderInputRef = useRef(null);
1860
1157
  const promptPlaceholderInputRef = useRef(null);
1158
+ const shouldSubmitFilledPlaceholderInputRef = useRef(false);
1861
1159
  const messagesContainerRef = useRef(null);
1862
1160
  const inlineDialogContainerRef = useRef(null);
1863
1161
  const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
@@ -1905,16 +1203,21 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
1905
1203
  setIsVoiceSupported(false);
1906
1204
  }
1907
1205
  }, []);
1908
- // Auto-focus terminal input on mount
1206
+ // Auto-focus terminal input on mount, and place cursor at the end of any
1207
+ // restored draft prompt (e.g. when switching workspaces with the same agent).
1909
1208
  useEffect(() => {
1910
- if (textareaRef.current) {
1911
- try {
1912
- textareaRef.current.focus({ preventScroll: true });
1913
- }
1914
- catch {
1915
- textareaRef.current.focus();
1916
- }
1209
+ const textarea = textareaRef.current;
1210
+ if (!textarea)
1211
+ return;
1212
+ try {
1213
+ textarea.focus({ preventScroll: true });
1917
1214
  }
1215
+ catch {
1216
+ textarea.focus();
1217
+ }
1218
+ const end = textarea.value.length;
1219
+ textarea.selectionStart = end;
1220
+ textarea.selectionEnd = end;
1918
1221
  }, []);
1919
1222
  // Start voice recognition
1920
1223
  const startVoice = () => {
@@ -2311,12 +1614,21 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
2311
1614
  output: Number(cost.tokens?.output) || 0,
2312
1615
  cached: Number(cost.tokens?.cached) || 0,
2313
1616
  cacheWrite: Number(cost.tokens?.cacheWrite) || 0,
1617
+ inputAudio: Number(cost.tokens?.inputAudio) || 0,
1618
+ outputAudio: Number(cost.tokens?.outputAudio) || 0,
1619
+ outputImage: Number(cost.tokens?.outputImage) || 0,
1620
+ reasoning: Number(cost.tokens?.reasoning) || 0,
2314
1621
  inputCost: Number(cost.input) || 0,
2315
1622
  outputCost: Number(cost.output) || 0,
2316
1623
  cachedCost: Number(cost.cached) || 0,
2317
1624
  cacheWriteCost: Number(cost.cacheWrite) || 0,
1625
+ inputAudioCost: Number(cost.inputAudio) || 0,
1626
+ outputAudioCost: Number(cost.outputAudio) || 0,
1627
+ outputImageCost: Number(cost.outputImage) || 0,
2318
1628
  imageCost: Number(cost.imageCost ?? cost.totalImageCost) ||
2319
1629
  0,
1630
+ markupCost: Number(cost.markup) || 0,
1631
+ reportedTotalCost: Number(cost.reportedTotal) || 0,
2320
1632
  totalCost: Number(cost.total) || 0,
2321
1633
  currency: "USD",
2322
1634
  };
@@ -2650,11 +1962,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
2650
1962
  output: Number(cost.tokens?.output) || 0,
2651
1963
  cached: Number(cost.tokens?.cached) || 0,
2652
1964
  cacheWrite: Number(cost.tokens?.cacheWrite) || 0,
1965
+ inputAudio: Number(cost.tokens?.inputAudio) || 0,
1966
+ outputAudio: Number(cost.tokens?.outputAudio) || 0,
1967
+ outputImage: Number(cost.tokens?.outputImage) || 0,
1968
+ reasoning: Number(cost.tokens?.reasoning) || 0,
2653
1969
  inputCost: Number(cost.input) || 0,
2654
1970
  outputCost: Number(cost.output) || 0,
2655
1971
  cachedCost: Number(cost.cached) || 0,
2656
1972
  cacheWriteCost: Number(cost.cacheWrite) || 0,
1973
+ inputAudioCost: Number(cost.inputAudio) || 0,
1974
+ outputAudioCost: Number(cost.outputAudio) || 0,
1975
+ outputImageCost: Number(cost.outputImage) || 0,
2657
1976
  imageCost: Number(cost.imageCost) || 0,
1977
+ markupCost: Number(cost.markup) || 0,
1978
+ reportedTotalCost: Number(cost.reportedTotal) || 0,
2658
1979
  totalCost: Number(cost.total) || 0,
2659
1980
  currency: "USD",
2660
1981
  };
@@ -2699,11 +2020,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
2699
2020
  output: Number(data.totalOutputTokens) || 0,
2700
2021
  cached: Number(data.totalCachedTokens) || 0,
2701
2022
  cacheWrite: Number(data.totalCacheWriteTokens) || 0,
2023
+ inputAudio: Number(data.totalInputAudioTokens) || 0,
2024
+ outputAudio: Number(data.totalOutputAudioTokens) || 0,
2025
+ outputImage: Number(data.totalOutputImageTokens) || 0,
2026
+ reasoning: Number(data.totalReasoningTokens) || 0,
2702
2027
  inputCost: Number(data.totalInputTokenCost) || 0,
2703
2028
  outputCost: Number(data.totalOutputTokenCost) || 0,
2704
2029
  cachedCost: Number(data.totalCachedTokenCost) || 0,
2705
2030
  cacheWriteCost: Number(data.totalCacheWriteTokenCost) || 0,
2031
+ inputAudioCost: Number(data.totalInputAudioCost) || 0,
2032
+ outputAudioCost: Number(data.totalOutputAudioCost) || 0,
2033
+ outputImageCost: Number(data.totalOutputImageCost) || 0,
2706
2034
  imageCost: Number(data.totalImageCost) || 0,
2035
+ markupCost: Number(data.totalMarkupCost) || 0,
2036
+ reportedTotalCost: Number(data.totalReportedCost) || 0,
2707
2037
  totalCost: Number(data.totalCost) || 0,
2708
2038
  currency: data.currency || "USD",
2709
2039
  };
@@ -3697,11 +3027,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
3697
3027
  output: Number(totals.totalOutputTokens) || 0,
3698
3028
  cached: Number(totals.totalCachedInputTokens) || 0,
3699
3029
  cacheWrite: Number(totals.totalCacheWriteTokens) || 0,
3030
+ inputAudio: Number(totals.totalInputAudioTokens) || 0,
3031
+ outputAudio: Number(totals.totalOutputAudioTokens) || 0,
3032
+ outputImage: Number(totals.totalOutputImageTokens) || 0,
3033
+ reasoning: Number(totals.totalReasoningTokens) || 0,
3700
3034
  inputCost: Number(totals.totalInputTokenCost) || 0,
3701
3035
  outputCost: Number(totals.totalOutputTokenCost) || 0,
3702
3036
  cachedCost: Number(totals.totalCachedInputTokenCost) || 0,
3703
3037
  cacheWriteCost: Number(totals.totalCacheWriteTokenCost) || 0,
3038
+ inputAudioCost: Number(totals.totalInputAudioCost) || 0,
3039
+ outputAudioCost: Number(totals.totalOutputAudioCost) || 0,
3040
+ outputImageCost: Number(totals.totalOutputImageCost) || 0,
3704
3041
  imageCost: Number(totals.totalImageCost) || 0,
3042
+ markupCost: Number(totals.totalMarkupCost) || 0,
3043
+ reportedTotalCost: Number(totals.totalReportedCost) || 0,
3705
3044
  totalCost: totalCost,
3706
3045
  currency: totals.currency,
3707
3046
  };
@@ -4468,15 +3807,44 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
4468
3807
  }
4469
3808
  }, []);
4470
3809
  const handleSubmit = async (promptOverride) => {
3810
+ const hasPromptOverride = promptOverride !== undefined;
4471
3811
  // Guard against double-submit and missing context
4472
3812
  if (isSubmitting) {
4473
3813
  console.warn("[AgentTerminal] handleSubmit blocked: already submitting");
4474
3814
  return;
4475
3815
  }
4476
- // Block submission while there is a pending tool call approval decision
4477
- if (hasPendingApprovals()) {
4478
- console.warn("[AgentTerminal] handleSubmit blocked: pending tool call approval");
4479
- return;
3816
+ // If there are pending tool call approvals, treat this submission as a
3817
+ // supersede: reject all pending tool calls and continue with the new prompt.
3818
+ const pendingToReject = allPendingApprovals;
3819
+ if (pendingToReject.length > 0) {
3820
+ const agentIdForReject = agent?.id || agentStub.id;
3821
+ if (!agentIdForReject) {
3822
+ console.error("[AgentTerminal] handleSubmit blocked: cannot supersede pending approvals — no agentId");
3823
+ setError("Agent not ready. Please try again.");
3824
+ return;
3825
+ }
3826
+ try {
3827
+ await Promise.all(pendingToReject.map(async (pending) => {
3828
+ await rejectToolCall({
3829
+ agentId: agentIdForReject,
3830
+ messageId: pending.dbMessageId || pending.messageId,
3831
+ toolCallId: pending.toolCallId,
3832
+ note: "Superseded by new user instructions",
3833
+ });
3834
+ window.dispatchEvent(new CustomEvent("agent:toolApprovalResolved", {
3835
+ detail: {
3836
+ messageId: pending.dbMessageId || pending.messageId,
3837
+ toolCallId: pending.toolCallId,
3838
+ approved: false,
3839
+ },
3840
+ }));
3841
+ }));
3842
+ }
3843
+ catch (rejectErr) {
3844
+ console.error("[AgentTerminal] Failed to supersede pending tool calls:", rejectErr);
3845
+ setError(`Failed to cancel pending tool call: ${rejectErr?.message || String(rejectErr)}`);
3846
+ return;
3847
+ }
4480
3848
  }
4481
3849
  if (!editContext) {
4482
3850
  console.error("[AgentTerminal] handleSubmit blocked: editContext is undefined");
@@ -4484,12 +3852,17 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
4484
3852
  return;
4485
3853
  }
4486
3854
  // If placeholder input is active, delegate to its submit method
4487
- if (activePlaceholderInput && allPlaceholdersFilled) {
4488
- placeholderInputRef.current?.submit();
3855
+ if (!hasPromptOverride && activePlaceholderInput && allPlaceholdersFilled) {
3856
+ const placeholderInput = placeholderInputRef.current;
3857
+ if (!placeholderInput)
3858
+ return;
3859
+ shouldSubmitFilledPlaceholderInputRef.current = true;
3860
+ placeholderInput.submit();
4489
3861
  return;
4490
3862
  }
4491
3863
  // If prompt contains placeholders, delegate to the prompt placeholder input's submit method
4492
- if (prompt &&
3864
+ if (!hasPromptOverride &&
3865
+ prompt &&
4493
3866
  /\{\{[^{}]+\}\}|<<[^<>]+>>/.test(prompt) &&
4494
3867
  allPlaceholdersFilled) {
4495
3868
  promptPlaceholderInputRef.current?.submit();
@@ -5554,6 +4927,25 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5554
4927
  : "completed");
5555
4928
  return;
5556
4929
  }
4930
+ // Backstop: if the backend reports an error but we never received the
4931
+ // agent:run:error lifecycle event (e.g. it was missed or the broadcast
4932
+ // path failed to fire), surface the error locally instead of waiting
4933
+ // for the user to reload.
4934
+ if (normalizeServerExecutionStatus(serverStatus) === "error") {
4935
+ const rawError = diagnostics.execution?.error ?? null;
4936
+ const errorMsg = toUserFacingAgentErrorMessage(rawError) ||
4937
+ rawError ||
4938
+ "AI could not complete this request.";
4939
+ clearHeartbeatMessages();
4940
+ setLastRunStatusReason(null);
4941
+ setError(errorMsg);
4942
+ setAgent((prev) => prev ? { ...prev, status: "error", statusMessage: errorMsg } : prev);
4943
+ setIsWaitingForResponse(false);
4944
+ isWaitingRef.current = false;
4945
+ setIsConnecting(false);
4946
+ setIsAgentThinking(false);
4947
+ return;
4948
+ }
5557
4949
  const serverLastSeq = diagnostics.currentSession?.lastDelivery?.lastSeq ??
5558
4950
  diagnostics.transport?.lastSeq ??
5559
4951
  null;
@@ -5621,6 +5013,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5621
5013
  isActive,
5622
5014
  isExecuting,
5623
5015
  appendToolUiEvent,
5016
+ clearHeartbeatMessages,
5624
5017
  loadAgent,
5625
5018
  settleCompletedRun,
5626
5019
  ]);
@@ -5712,56 +5105,47 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5712
5105
  detail: { agentId: parentAgentId },
5713
5106
  }));
5714
5107
  }, [parentAgentId]);
5715
- const loadingContent = isLoading && !activeInlineDialog ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-[11px] text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin", strokeWidth: 1 }), "Loading agent..."] }) })) : null;
5108
+ const shouldShowLoadingContent = isLoading && !activeInlineDialog && !agent && messages.length === 0;
5109
+ const loadingContent = shouldShowLoadingContent ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-[11px] text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin", strokeWidth: 1 }), "Loading agent..."] }) })) : null;
5716
5110
  const renderContextInfoBar = () => (_jsx(ContextInfoBar, { agent: agent, agentMetadata: agentMetadata, setAgentMetadata: setAgentMetadata, setAgent: setAgent, resolvedPageName: resolvedPageName, resolvedComponentName: resolvedComponentName, resolvedFieldName: resolvedFieldName, isLiveEditorContextMode: isLiveEditorContextMode, omitEditorContext: omitsEditorContext, onRefreshContext: handleRefreshContext }));
5111
+ const handleExtendCostLimit = useCallback(async () => {
5112
+ if (!agent?.id)
5113
+ return;
5114
+ try {
5115
+ const result = await updateAgentCostLimit(agent.id, "extend");
5116
+ if (result.success && result.costLimit !== undefined) {
5117
+ setAgent((prev) => prev
5118
+ ? {
5119
+ ...prev,
5120
+ costLimit: result.costLimit,
5121
+ status: prev.status === "costLimitReached"
5122
+ ? "running"
5123
+ : prev.status,
5124
+ }
5125
+ : prev);
5126
+ }
5127
+ setCostLimitExceeded(null);
5128
+ setIsWaitingForResponse(true);
5129
+ isWaitingRef.current = true;
5130
+ setIsAgentThinking(true);
5131
+ setShouldAutoScroll(true);
5132
+ }
5133
+ catch (e) {
5134
+ console.error("Failed to extend cost limit:", e);
5135
+ setError(e instanceof Error ? e.message : "Failed to extend cost limit");
5136
+ }
5137
+ }, [agent?.id]);
5717
5138
  const renderCostLimitBanner = () => {
5718
5139
  if (!costLimitExceeded)
5719
5140
  return null;
5720
- const { totalCost, costLimit, initialCostLimit } = costLimitExceeded;
5721
- return (_jsxs("div", { className: "m-3 rounded border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 text-amber-500", strokeWidth: 1 }), _jsxs("span", { children: ["Cost limit exceeded. Spent $", totalCost.toFixed(2), " / $", costLimit.toFixed(2), "."] })] }), _jsx("div", { className: "flex gap-2", children: _jsx("button", { className: "rounded border border-amber-300 bg-white px-2 py-1 hover:bg-amber-100", onClick: async () => {
5722
- if (!agent?.id)
5723
- return;
5724
- try {
5725
- // Extend cost limit - backend will automatically resume the agent
5726
- const result = await updateAgentCostLimit(agent.id, "extend");
5727
- // Update the agent's cost limit and clear the costLimitReached
5728
- // status in local state so the useEffect watcher doesn't
5729
- // immediately re-show the banner before the backend status
5730
- // update arrives.
5731
- if (result.success && result.costLimit !== undefined) {
5732
- setAgent((prev) => prev
5733
- ? {
5734
- ...prev,
5735
- costLimit: result.costLimit,
5736
- status: prev.status === "costLimitReached"
5737
- ? "running"
5738
- : prev.status,
5739
- }
5740
- : prev);
5741
- }
5742
- // Clear the banner and set waiting state
5743
- // Agent will resume automatically via backend's ResumeAgentAsync
5744
- setCostLimitExceeded(null);
5745
- setIsWaitingForResponse(true);
5746
- isWaitingRef.current = true;
5747
- setIsAgentThinking(true);
5748
- setShouldAutoScroll(true);
5749
- }
5750
- catch (e) {
5751
- console.error("Failed to extend cost limit:", e);
5752
- setError(e instanceof Error
5753
- ? e.message
5754
- : "Failed to extend cost limit");
5755
- }
5756
- }, children: "Extend limit and continue" }) })] }));
5141
+ const { totalCost, costLimit } = costLimitExceeded;
5142
+ return (_jsx(AgentCostLimitBanner, { totalCost: totalCost, costLimit: costLimit, onExtend: handleExtendCostLimit }));
5757
5143
  };
5758
5144
  const renderErrorBanner = () => {
5759
5145
  const currentAgent = agent || agentStub;
5760
5146
  const isErrorStatus = currentAgent?.status === "error";
5761
5147
  const isWaitingForInputStatus = currentAgent?.status === "waitingForInput";
5762
5148
  const isWaitingForApprovalStatus = currentAgent?.status === "waitingForApproval";
5763
- // Show error banner for error status, or for any terminal status that still
5764
- // carries a statusMessage (e.g. agent closed after an error).
5765
5149
  const isTerminalWithError = !isErrorStatus &&
5766
5150
  !!currentAgent?.statusMessage &&
5767
5151
  currentAgent?.status !== "running" &&
@@ -5773,9 +5157,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5773
5157
  : null) || error;
5774
5158
  if (!rawErrorMessage)
5775
5159
  return null;
5776
- // Clean the error message (statusMessage from DB may contain raw JSON)
5777
5160
  const errorMessage = toUserFacingAgentErrorMessage(rawErrorMessage) || rawErrorMessage;
5778
- return (_jsx("div", { className: "m-3 rounded border border-red-300 bg-red-50 p-3 text-[11px] text-red-900", "data-testid": "agent-error-banner", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0 text-red-500", strokeWidth: 1 }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "mb-1 font-semibold", children: "Agent Error" }), _jsx("div", { className: "text-red-800", children: errorMessage })] })] }) }));
5161
+ return _jsx(AgentErrorBanner, { message: errorMessage });
5779
5162
  };
5780
5163
  const renderCapacityBanner = () => {
5781
5164
  if (lastRunStatusReason !== MACHINE_CAPACITY_REASON) {
@@ -5784,7 +5167,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5784
5167
  const currentAgent = agent || agentStub;
5785
5168
  const message = currentAgent?.statusMessage ||
5786
5169
  "Waiting for capacity. The agent will start automatically when a slot becomes available.";
5787
- return (_jsx("div", { className: "m-3 rounded border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900", "data-testid": "agent-capacity-banner", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-500", strokeWidth: 1 }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "mb-1 font-semibold", children: "Waiting for capacity" }), _jsx("div", { className: "text-amber-800", children: message })] })] }) }));
5170
+ return _jsx(AgentCapacityBanner, { message: message });
5788
5171
  };
5789
5172
  const renderBrowserClaimBanner = (variant = "inline") => {
5790
5173
  if (!agent?.id || !editContext?.sessionId)
@@ -5875,31 +5258,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5875
5258
  const renderInlineDialogContent = () => {
5876
5259
  if (!activeInlineDialog)
5877
5260
  return null;
5878
- if (activeInlineDialog.request.dialogType === "questionnaire") {
5879
- return (_jsx("div", { ref: inlineDialogContainerRef, className: cn("agent-inline-dialog min-h-0 overflow-hidden", displayMode === "full" && "h-full"), children: _jsx(QuestionnaireInline, { requestId: activeInlineDialog.request.callbackId, agentId: activeInlineDialog.request.agentId, title: activeInlineDialog.request.title, description: activeInlineDialog.request.description, parameters: activeInlineDialog.request.parameters, footerActions: questionnaireFooterActions, onClose: (result) => {
5880
- activeInlineDialog.onComplete(result);
5881
- setActiveInlineDialog(null);
5882
- void onInteractionSubmitted?.();
5883
- }, onCancel: () => {
5884
- activeInlineDialog.onCancel();
5885
- setActiveInlineDialog(null);
5886
- } }) }));
5887
- }
5888
5261
  const dialogRegistration = editContext?.configuration?.editor?.agentDialogs?.find((d) => d.dialogType === activeInlineDialog.request.dialogType);
5889
- if (dialogRegistration) {
5890
- const DialogComponent = dialogRegistration.component;
5891
- return (_jsx("div", { className: "agent-inline-dialog", children: _jsx(DialogComponent, { title: activeInlineDialog.request.title, description: activeInlineDialog.request.description, parameters: activeInlineDialog.request.parameters, onClose: (result) => {
5892
- activeInlineDialog.onComplete(result);
5893
- setActiveInlineDialog(null);
5894
- if (activeInlineDialog.request.dialogType === "questionnaire") {
5895
- void onInteractionSubmitted?.();
5896
- }
5897
- }, onCancel: () => {
5898
- activeInlineDialog.onCancel();
5899
- setActiveInlineDialog(null);
5900
- } }) }));
5901
- }
5902
- return (_jsx("div", { className: "agent-inline-dialog", children: _jsxs("div", { className: "p-4 text-sm text-red-500", children: ["Unknown dialog type: ", activeInlineDialog.request.dialogType] }) }));
5262
+ return (_jsx(AgentInlineDialogContent, { activeInlineDialog: activeInlineDialog, setActiveInlineDialog: setActiveInlineDialog, containerRef: inlineDialogContainerRef, displayMode: displayMode, questionnaireFooterActions: questionnaireFooterActions, dialogRegistration: dialogRegistration, onInteractionSubmitted: onInteractionSubmitted }));
5903
5263
  };
5904
5264
  const latestSummaryAssistantGroup = useMemo(() => {
5905
5265
  if (hideSummaryMessages)
@@ -5931,10 +5291,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5931
5291
  const summaryOperations = latestSummaryAssistantGroup
5932
5292
  ? getOperationsForMessageGroup(summaryMessages, agentOperations)
5933
5293
  : [];
5934
- return (_jsxs("div", { className: `flex h-full min-h-0 flex-col ${className || ""}`, children: [fixedBrowserClaimBanner, _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [error &&
5935
- !isAgentErrorStatusValue((agent || agentStub)?.status) && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3 select-text", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-[11px] font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-[11px] text-red-700", children: error })] })] }) })), showInitialThinkingSplash && (_jsx("div", { className: "flex h-full items-center justify-center p-8", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [activeProfile?.svgIcon ? (_jsx("div", { className: "flex h-16 w-16 items-center justify-center text-gray-400 [&>svg]:h-full [&>svg]:w-full", dangerouslySetInnerHTML: {
5936
- __html: sanitizeSvg(activeProfile.svgIcon),
5937
- } })) : (_jsx(SecretAgentIcon, { size: 64, strokeWidth: 1, className: "text-gray-400" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]" }), _jsx("span", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.15s]" }), _jsx("span", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400" })] })] }) })), inlineBrowserClaimBanner, renderCapacityBanner(), renderErrorBanner(), inlineDialog ? (inlineDialog) : latestSummaryAssistantGroup ? (_jsx("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: _jsx(AiResponseMessage, { messages: summaryMessages, finished: !latestSummaryAssistantGroup.isLastGroup || !isExecuting, editOperations: summaryOperations, defaultCollapseJson: defaultCollapseJson, profileSvgIcon: activeProfile?.svgIcon, agentId: agent?.id || agentStub.id, agentName: activeProfile?.agentName ||
5294
+ return (_jsxs("div", { className: `flex h-full min-h-0 flex-col ${className || ""}`, children: [fixedBrowserClaimBanner, error &&
5295
+ !isAgentErrorStatusValue((agent || agentStub)?.status) && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3 select-text", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-[11px] font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-[11px] text-red-700", children: error })] })] }) })), renderCapacityBanner(), renderErrorBanner(), _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [showInitialThinkingSplash && (_jsx(InitialThinkingSplash, { svgIcon: activeProfile?.svgIcon })), inlineBrowserClaimBanner, inlineDialog ? (inlineDialog) : latestSummaryAssistantGroup ? (_jsx("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: _jsx(AiResponseMessage, { messages: summaryMessages, finished: !latestSummaryAssistantGroup.isLastGroup || !isExecuting, editOperations: summaryOperations, defaultCollapseJson: defaultCollapseJson, profileSvgIcon: activeProfile?.svgIcon, agentId: agent?.id || agentStub.id, agentName: activeProfile?.agentName ||
5938
5296
  activeProfile?.displayTitle ||
5939
5297
  activeProfile?.name, allPendingApprovals: allPendingApprovals, onSwitchToAutonomous: handleSwitchToAutonomous, browserCaptureInlinePrompt: browserCaptureInlinePrompt, onQuickAction: (action) => {
5940
5298
  const text = (action.prompt ||
@@ -5963,10 +5321,13 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5963
5321
  activeProfile?.displayTitle ||
5964
5322
  activeProfile?.name ||
5965
5323
  "Agent" }), _jsx("span", { className: "text-xs text-gray-400", children: formatTime(new Date()) })] }), _jsxs("div", { className: "flex items-center gap-1 pt-2", children: [_jsx("div", { className: "h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]" }), _jsx("div", { className: "h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.15s]" }), _jsx("div", { className: "h-1 w-1 animate-bounce rounded-full bg-gray-400" })] })] })] })), _jsx("div", { ref: messagesEndRef })] }), showSummaryInput && !activeInlineDialog ? (_jsxs("div", { className: cn("border-t border-gray-200 p-4", simpleMode && "pb-10"), children: [activePlaceholderInput ? (_jsx(PlaceholderInput, { ref: placeholderInputRef, text: activePlaceholderInput.text, showButtons: hideBottomControls, buttonsClassName: hideBottomControls ? "justify-end" : "", onFilledChange: setAllPlaceholdersFilled, onComplete: (filledText) => {
5324
+ const shouldSubmitFilledText = shouldSubmitFilledPlaceholderInputRef.current ||
5325
+ activePlaceholderInput.behavior !== "compose" ||
5326
+ hideBottomControls;
5327
+ shouldSubmitFilledPlaceholderInputRef.current = false;
5966
5328
  setActivePlaceholderInput(null);
5967
5329
  setAllPlaceholdersFilled(false);
5968
- if (activePlaceholderInput.behavior === "compose" &&
5969
- !hideBottomControls) {
5330
+ if (!shouldSubmitFilledText) {
5970
5331
  setPrompt(filledText);
5971
5332
  setInputPlaceholder("Review and edit, then press Enter to send");
5972
5333
  if (textareaRef.current) {
@@ -5978,32 +5339,30 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
5978
5339
  }
5979
5340
  catch { }
5980
5341
  }
5342
+ return;
5981
5343
  }
5982
- else {
5983
- if (isExecuting) {
5984
- try {
5985
- handleStop();
5986
- }
5987
- catch { }
5344
+ if (isExecuting) {
5345
+ try {
5346
+ handleStop();
5988
5347
  }
5989
- sendQuickMessage(filledText);
5348
+ catch { }
5990
5349
  }
5350
+ void handleSubmit(filledText);
5991
5351
  }, onCancel: () => {
5352
+ shouldSubmitFilledPlaceholderInputRef.current = false;
5992
5353
  setActivePlaceholderInput(null);
5993
5354
  setAllPlaceholdersFilled(false);
5994
5355
  } })) : prompt && /\{\{[^{}]+\}\}|<<[^<>]+>>/.test(prompt) ? (_jsx(PlaceholderInput, { ref: promptPlaceholderInputRef, text: prompt, showButtons: hideBottomControls, buttonsClassName: hideBottomControls ? "justify-end" : "", onFilledChange: setAllPlaceholdersFilled, onComplete: (filledText) => {
5995
- setPrompt(filledText);
5356
+ shouldSubmitFilledPlaceholderInputRef.current = false;
5996
5357
  setAllPlaceholdersFilled(false);
5997
5358
  if (filledText.trim()) {
5998
- if (isExecuting) {
5999
- try {
6000
- handleStop();
6001
- }
6002
- catch { }
6003
- }
6004
- sendQuickMessage(filledText);
5359
+ void handleSubmit(filledText);
5360
+ }
5361
+ else {
5362
+ setPrompt("");
6005
5363
  }
6006
5364
  }, onCancel: () => {
5365
+ shouldSubmitFilledPlaceholderInputRef.current = false;
6007
5366
  setPrompt("");
6008
5367
  setAllPlaceholdersFilled(false);
6009
5368
  setInputPlaceholder("Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)");
@@ -6045,7 +5404,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
6045
5404
  })()
6046
5405
  : null;
6047
5406
  const fullModeInlineDialog = displayMode === "full" ? renderInlineDialogContent() : null;
6048
- const fullModeUpperContent = (_jsxs("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: [fixedBrowserClaimBanner, _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [messages.length === 0 && !error && !hideGreeting && (_jsx("div", { className: "flex h-full items-center justify-center", children: !activeProfile ? (_jsx(Loader2, { className: "mx-auto h-8 w-8 animate-spin text-gray-400" })) : (_jsx(AgentGreeting, { profile: activeProfile, onPromptClick: (p) => {
5407
+ const fullModeUpperContent = (_jsxs("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: [fixedBrowserClaimBanner, renderCapacityBanner(), renderErrorBanner(), _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [messages.length === 0 && !error && !hideGreeting && !isLoading && (_jsx("div", { className: "flex h-full items-center justify-center", children: !activeProfile ? (_jsx(Loader2, { className: "mx-auto h-8 w-8 animate-spin text-gray-400" })) : (_jsx(AgentGreeting, { profile: activeProfile, onPromptClick: (p) => {
6049
5408
  setPrompt(p);
6050
5409
  // Use setTimeout to ensure state is updated before submission
6051
5410
  setTimeout(() => {
@@ -6058,9 +5417,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
6058
5417
  handleSubmit();
6059
5418
  }
6060
5419
  }, 0);
6061
- } })) })), showInitialThinkingSplash && (_jsx("div", { className: "flex h-full items-center justify-center p-8", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [activeProfile?.svgIcon ? (_jsx("div", { className: "flex h-16 w-16 items-center justify-center text-gray-400 [&>svg]:h-full [&>svg]:w-full", dangerouslySetInnerHTML: {
6062
- __html: sanitizeSvg(activeProfile.svgIcon),
6063
- } })) : (_jsx(SecretAgentIcon, { size: 64, strokeWidth: 1, className: "text-gray-400" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]" }), _jsx("span", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.15s]" }), _jsx("span", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400" })] })] }) })), inlineBrowserClaimBanner, renderCapacityBanner(), renderErrorBanner(), _jsxs("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: [(() => {
5420
+ } })) })), showInitialThinkingSplash && (_jsx(InitialThinkingSplash, { svgIcon: activeProfile?.svgIcon })), inlineBrowserClaimBanner, _jsxs("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: [(() => {
6064
5421
  const groups = groupConsecutiveMessages(messages);
6065
5422
  return groups.map((group, groupIndex) => {
6066
5423
  const isLastGroup = groupIndex === groups.length - 1;
@@ -6187,33 +5544,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
6187
5544
  hasTodoContent,
6188
5545
  hasSpawnedAgents,
6189
5546
  agent?.id && hasHistoryContent,
6190
- ].filter(Boolean).length - 1)), setActiveTab: setContextPanelsActiveTab, className: "justify-start px-4" }) })) : (_jsxs(_Fragment, { children: [renderContextInfoBar(), agent?.id && activeProfile && (_jsx(AgentDocumentList, { ref: documentListRef, agentId: agent.id, maxFileSizeMB: activeProfile.maxDocumentSizeMB ?? 10, enabled: activeProfile.enableDocumentUpload ?? false, profileId: activeProfile.id }, `${agent.id}-${agent.updatedDate || ""}-${activeProfile.id}`)), _jsx(TodoListPanel, { messages: messages, agentMetadata: agentMetadata }), _jsx(SpawnedAgentsPanel, { agentMetadata: agentMetadata }), agent?.id && (_jsx(AgentEditOperationsPanel, { operations: agentOperations }))] }))), queuedPrompts.length > 0 && !simpleMode && (_jsx("div", { className: "border-t border-gray-200 bg-amber-50/50", "data-testid": "queued-prompts-section", children: _jsxs("div", { className: "px-4 pt-2.5 pb-2", children: [_jsxs("div", { className: "mb-1.5 flex items-center gap-1.5", children: [_jsx("div", { className: "h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" }), _jsxs("span", { className: "text-[10px] font-medium tracking-wide text-amber-600 uppercase", "data-testid": "queued-prompts-count", children: ["Queued (", queuedPrompts.length, ")"] })] }), _jsx("div", { className: "max-h-64 space-y-0.5 overflow-y-auto", children: queuedPrompts.map((qp) => {
6191
- let triggerName = "";
6192
- if (qp.data) {
6193
- try {
6194
- const parsed = JSON.parse(qp.data);
6195
- triggerName = parsed?.triggerName?.trim() || "";
6196
- }
6197
- catch {
6198
- // Ignore invalid JSON metadata and render as regular queued prompt.
6199
- }
6200
- }
6201
- const isTriggerQueuedPrompt = !qp.sourceAgentName && triggerName.length > 0;
6202
- const isTriggerExpanded = !!expandedQueuedTriggerIds[qp.id];
6203
- if (isTriggerQueuedPrompt) {
6204
- return (_jsxs("div", { className: "text-[11px]", "data-testid": "queued-prompt-item", children: [_jsxs("button", { type: "button", onClick: () => setExpandedQueuedTriggerIds((prev) => ({
6205
- ...prev,
6206
- [qp.id]: !prev[qp.id],
6207
- })), className: "flex w-full items-center gap-1.5 rounded px-1 py-0.5 text-left text-amber-800 transition-colors hover:bg-amber-100/60", "data-testid": "queued-trigger-toggle", "data-expanded": isTriggerExpanded ? "true" : "false", children: [_jsx(Target, { className: "h-3 w-3 shrink-0 text-amber-500", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: triggerName }), qp.createdDate && (_jsx("span", { className: "ml-1 shrink-0 text-[10px] text-amber-500", children: formatTime(new Date(qp.createdDate)) })), _jsx("span", { className: "ml-auto shrink-0 text-amber-400", children: isTriggerExpanded ? (_jsx(ChevronUp, { className: "h-3 w-3" })) : (_jsx(ChevronDown, { className: "h-3 w-3" })) })] }), isTriggerExpanded && (_jsx("div", { className: "mt-0.5 border-l-2 border-amber-200 pl-5 text-[11px] text-amber-700/80", children: qp.prompt }))] }, qp.id));
6208
- }
6209
- return (_jsx("div", { className: "rounded-md border border-amber-200 bg-white p-2.5 text-[11px]", "data-testid": "queued-prompt-item", children: _jsx("div", { className: "flex items-start justify-between gap-2", children: _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "mb-1 flex items-center gap-1.5", children: qp.sourceAgentName ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-medium text-gray-700", children: ["From ", qp.sourceAgentName] }), qp.priority > 5 && (_jsx("span", { className: "rounded bg-red-100 px-1.5 py-0.5 text-[11px] font-medium text-red-700", children: "High Priority" }))] })) : (_jsx("span", { className: "font-medium text-gray-700", children: "From User" })) }), _jsx("div", { className: "wrap-break-word whitespace-pre-wrap text-gray-600", "data-testid": "queued-prompt-text", children: qp.prompt }), _jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-gray-400", children: [qp.createdDate && (_jsxs("span", { children: ["Queued ", formatTime(new Date(qp.createdDate))] })), qp.scheduledFor &&
6210
- new Date(qp.scheduledFor).getTime() >
6211
- new Date(qp.createdDate || 0).getTime() +
6212
- 1000 && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-gray-300", children: "\u2022" }), _jsxs("span", { className: "font-medium text-amber-600", children: ["Scheduled for", " ", new Date(qp.scheduledFor).toDateString() ===
6213
- new Date().toDateString()
6214
- ? formatTime(new Date(qp.scheduledFor))
6215
- : formatDateTime(new Date(qp.scheduledFor))] })] }))] })] }) }) }, qp.id));
6216
- }) })] }) }))] }));
5547
+ ].filter(Boolean).length - 1)), setActiveTab: setContextPanelsActiveTab, className: "justify-start px-4" }) })) : (_jsxs(_Fragment, { children: [renderContextInfoBar(), agent?.id && activeProfile && (_jsx(AgentDocumentList, { ref: documentListRef, agentId: agent.id, maxFileSizeMB: activeProfile.maxDocumentSizeMB ?? 10, enabled: activeProfile.enableDocumentUpload ?? false, profileId: activeProfile.id }, `${agent.id}-${agent.updatedDate || ""}-${activeProfile.id}`)), _jsx(TodoListPanel, { messages: messages, agentMetadata: agentMetadata }), _jsx(SpawnedAgentsPanel, { agentMetadata: agentMetadata }), agent?.id && (_jsx(AgentEditOperationsPanel, { operations: agentOperations }))] }))), !simpleMode && (_jsx(QueuedPromptsPanel, { queuedPrompts: queuedPrompts }))] }));
6217
5548
  const showQuestionnaireSplitter = isQuestionnaireDialogOpen && !!fullModeInlineDialog;
6218
5549
  const fullModeContent = showQuestionnaireSplitter ? (_jsx(Splitter, { panels: [
6219
5550
  {
@@ -6233,12 +5564,13 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
6233
5564
  // Placeholder Input (from quick actions)
6234
5565
  // Show internal buttons only in splash mode (hideBottomControls) since external buttons won't be visible there
6235
5566
  _jsx(PlaceholderInput, { ref: placeholderInputRef, text: activePlaceholderInput.text, showButtons: hideBottomControls, buttonsClassName: hideBottomControls ? "justify-end" : "", onFilledChange: setAllPlaceholdersFilled, onComplete: (filledText) => {
5567
+ const shouldSubmitFilledText = shouldSubmitFilledPlaceholderInputRef.current ||
5568
+ activePlaceholderInput.behavior !== "compose" ||
5569
+ hideBottomControls;
5570
+ shouldSubmitFilledPlaceholderInputRef.current = false;
6236
5571
  setActivePlaceholderInput(null);
6237
5572
  setAllPlaceholdersFilled(false);
6238
- // When hideBottomControls is true (e.g., splash screen), always auto-submit
6239
- // because the regular textarea is hidden and user can't manually send
6240
- if (activePlaceholderInput.behavior === "compose" &&
6241
- !hideBottomControls) {
5573
+ if (!shouldSubmitFilledText) {
6242
5574
  setPrompt(filledText);
6243
5575
  setInputPlaceholder("Review and edit, then press Enter to send");
6244
5576
  if (textareaRef.current) {
@@ -6250,37 +5582,34 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
6250
5582
  }
6251
5583
  catch { }
6252
5584
  }
5585
+ return;
6253
5586
  }
6254
- else {
6255
- // Submit behavior or default (or compose with hideBottomControls)
6256
- if (isExecuting) {
6257
- try {
6258
- handleStop();
6259
- }
6260
- catch { }
5587
+ if (isExecuting) {
5588
+ try {
5589
+ handleStop();
6261
5590
  }
6262
- sendQuickMessage(filledText);
5591
+ catch { }
6263
5592
  }
5593
+ void handleSubmit(filledText);
6264
5594
  }, onCancel: () => {
5595
+ shouldSubmitFilledPlaceholderInputRef.current = false;
6265
5596
  setActivePlaceholderInput(null);
6266
5597
  setAllPlaceholdersFilled(false);
6267
5598
  } })) : prompt && /\{\{[^{}]+\}\}|<<[^<>]+>>/.test(prompt) ? (
6268
5599
  // Template mode: show PlaceholderInput when prompt contains placeholders
6269
5600
  // Show internal buttons only in splash mode (hideBottomControls) since external buttons won't be visible there
6270
5601
  _jsx(PlaceholderInput, { ref: promptPlaceholderInputRef, text: prompt, showButtons: hideBottomControls, buttonsClassName: hideBottomControls ? "justify-end" : "", onFilledChange: setAllPlaceholdersFilled, onComplete: (filledText) => {
6271
- setPrompt(filledText);
5602
+ shouldSubmitFilledPlaceholderInputRef.current = false;
6272
5603
  setAllPlaceholdersFilled(false);
6273
5604
  // Auto-submit after filling placeholders
6274
5605
  if (filledText.trim()) {
6275
- if (isExecuting) {
6276
- try {
6277
- handleStop();
6278
- }
6279
- catch { }
6280
- }
6281
- sendQuickMessage(filledText);
5606
+ void handleSubmit(filledText);
5607
+ }
5608
+ else {
5609
+ setPrompt("");
6282
5610
  }
6283
5611
  }, onCancel: () => {
5612
+ shouldSubmitFilledPlaceholderInputRef.current = false;
6284
5613
  setPrompt("");
6285
5614
  setAllPlaceholdersFilled(false);
6286
5615
  setInputPlaceholder("Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)");
@@ -6488,27 +5817,27 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
6488
5817
  }, title: "Remove skill", "aria-label": `Remove ${skill?.name || skillId}`, children: _jsx(X, { className: "h-2.5 w-2.5", strokeWidth: 1 }) }))] }, skillId));
6489
5818
  }) }))] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setToolsSectionExpanded((open) => !open), className: "mb-0.5 flex w-full items-center justify-between rounded px-0.5 py-0.5 text-left text-[11px] font-medium text-gray-700 hover:bg-gray-100/80", "aria-expanded": toolsSectionExpanded, "data-testid": "agent-tools-section-toggle", children: [_jsxs("span", { children: ["Available tools (", displayedAvailableTools.length, ")"] }), toolsSectionExpanded ? (_jsx(ChevronUp, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 })) : (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 }))] }), toolsSectionExpanded && (_jsx("div", { className: "max-h-36 overflow-y-auto rounded border border-gray-100 bg-gray-50/50 p-1", "data-testid": "agent-tools-section", children: displayedAvailableToolsLoading ? (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Loading available tools..." })) : displayedAvailableTools.length > 0 ? (_jsxs("div", { className: "space-y-0.5", children: [isLocalOnlyDraftAgent && (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Preview based on the current profile, mode, and selected skills." })), displayedAvailableTools.map((toolName) => (_jsx("div", { className: "rounded px-1 py-0.5 transition-colors hover:bg-white/60", "data-testid": "agent-tool-row", title: toolName, children: _jsx("div", { className: "truncate text-[10px] text-gray-700", children: toolName }) }, toolName)))] })) : (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: isLocalOnlyDraftAgent
6490
5819
  ? "No available tools for this profile and mode"
6491
- : "No available tools" })) })), displayedAvailableToolsError && (_jsx("div", { className: "mt-1 text-[10px] text-red-600", children: displayedAvailableToolsError }))] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setAllowancesSectionExpanded((open) => !open), className: "mb-0.5 flex w-full items-center justify-between rounded px-0.5 py-0.5 text-left text-[11px] font-medium text-gray-700 hover:bg-gray-100/80", "aria-expanded": allowancesSectionExpanded, "data-testid": "agent-allowances-section-toggle", children: [_jsxs("span", { children: ["Allowances (", allowancesTotalCount, ")"] }), allowancesSectionExpanded ? (_jsx(ChevronUp, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 })) : (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 }))] }), allowancesSectionExpanded && (_jsx("div", { className: "max-h-36 overflow-y-auto rounded border border-gray-100 bg-gray-50/50 p-1", "data-testid": "agent-allowances-section", children: operationAllowancesLoading ? (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Loading allowances..." })) : hasAnyAllowances ? (_jsx("div", { className: "space-y-1", children: allowanceGroups.map((group) => group.rows.length > 0 ? (_jsxs("div", { className: "space-y-0.5", children: [_jsx("div", { className: "px-1 text-[9px] font-medium tracking-wide text-gray-400 uppercase", children: group.label }), group.rows.map((allowance, index) => {
6492
- const sourceLabel = formatAllowanceSource(allowance.source);
6493
- const pathLabel = "itemPath" in allowance
6494
- ? allowance.itemPath
6495
- : allowance.normalizedPath;
6496
- return (_jsxs("div", { className: "rounded px-1 py-0.5 transition-colors hover:bg-white/60", "data-testid": `agent-allowance-row-${group.key}`, children: [_jsxs("div", { className: "flex items-baseline gap-1.5", children: [_jsx("div", { className: "shrink-0 text-[10px] font-medium text-gray-700", children: allowance.operationType ||
6497
- "*" }), _jsx("div", { className: "truncate text-[9px] text-gray-500", title: formatAllowanceLabel(allowance), children: pathLabel })] }), (sourceLabel ||
6498
- allowance.grantedBy) && (_jsx("div", { className: "truncate pl-6 text-[9px] text-gray-400", children: [
6499
- sourceLabel,
6500
- allowance.grantedBy,
6501
- ]
6502
- .filter(Boolean)
6503
- .join(" · ") }))] }, `${group.key}-${allowance.operationType}-${pathLabel}-${index}`));
6504
- })] }, group.key)) : null) })) : (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: isLocalOnlyDraftAgent
6505
- ? "Allowances are shown after the agent is created"
6506
- : "No active allowances" })) })), operationAllowancesError && (_jsx("div", { className: "mt-1 text-[10px] text-red-600", children: operationAllowancesError }))] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setSubscribedTriggersSectionExpanded((open) => !open), className: "mb-0.5 flex w-full items-center justify-between rounded px-0.5 py-0.5 text-left text-[11px] font-medium text-gray-700 hover:bg-gray-100/80", "aria-expanded": subscribedTriggersSectionExpanded, "data-testid": "agent-subscribed-triggers-section-toggle", children: [_jsx("span", { children: `Subscribed triggers (${activeTriggerSubscriptions.length})` }), subscribedTriggersSectionExpanded ? (_jsx(ChevronUp, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 })) : (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 }))] }), subscribedTriggersSectionExpanded && (_jsx("div", { className: "max-h-28 overflow-y-auto rounded border border-gray-100 bg-gray-50/50 p-1", "data-testid": "agent-subscribed-triggers-section", children: triggerSubscriptionsLoading ? (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Loading subscribed triggers..." })) : activeTriggerSubscriptions.length > 0 ? (_jsx("div", { className: "space-y-0.5", children: activeTriggerSubscriptions.map((sub) => {
5820
+ : "No available tools" })) })), displayedAvailableToolsError && (_jsx("div", { className: "mt-1 text-[10px] text-red-600", children: displayedAvailableToolsError }))] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setAllowancesSectionExpanded((open) => !open), className: "mb-0.5 flex w-full items-center justify-between rounded px-0.5 py-0.5 text-left text-[11px] font-medium text-gray-700 hover:bg-gray-100/80", "aria-expanded": allowancesSectionExpanded, "data-testid": "agent-allowances-section-toggle", children: [_jsxs("span", { children: ["Allowances (", allowancesTotalCount, ")"] }), allowancesSectionExpanded ? (_jsx(ChevronUp, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 })) : (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 }))] }), allowancesSectionExpanded && (_jsx("div", { className: "max-h-36 overflow-y-auto rounded border border-gray-100 bg-gray-50/50 p-1", "data-testid": "agent-allowances-section", children: displayedOperationAllowancesLoading ? (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Loading allowances..." })) : hasAnyAllowances ? (_jsxs("div", { className: "space-y-1", children: [isLocalOnlyDraftAgent && (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Preview based on the current profile." })), allowanceGroups.map((group) => group.rows.length > 0 ? (_jsxs("div", { className: "space-y-0.5", children: [_jsx("div", { className: "px-1 text-[9px] font-medium tracking-wide text-gray-400 uppercase", children: group.label }), group.rows.map((allowance, index) => {
5821
+ const sourceLabel = formatAllowanceSource(allowance.source);
5822
+ const pathLabel = "itemPath" in allowance
5823
+ ? allowance.itemPath
5824
+ : allowance.normalizedPath;
5825
+ return (_jsxs("div", { className: "rounded px-1 py-0.5 transition-colors hover:bg-white/60", "data-testid": `agent-allowance-row-${group.key}`, children: [_jsxs("div", { className: "flex items-baseline gap-1.5", children: [_jsx("div", { className: "shrink-0 text-[10px] font-medium text-gray-700", children: allowance.operationType ||
5826
+ "*" }), _jsx("div", { className: "truncate text-[9px] text-gray-500", title: formatAllowanceLabel(allowance), children: pathLabel })] }), (sourceLabel ||
5827
+ allowance.grantedBy) && (_jsx("div", { className: "truncate pl-6 text-[9px] text-gray-400", children: [
5828
+ sourceLabel,
5829
+ allowance.grantedBy,
5830
+ ]
5831
+ .filter(Boolean)
5832
+ .join(" · ") }))] }, `${group.key}-${allowance.operationType}-${pathLabel}-${index}`));
5833
+ })] }, group.key)) : null)] })) : (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: isLocalOnlyDraftAgent
5834
+ ? "No configured allowances for this profile"
5835
+ : "No active allowances" })) })), displayedOperationAllowancesError && (_jsx("div", { className: "mt-1 text-[10px] text-red-600", children: displayedOperationAllowancesError }))] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setSubscribedTriggersSectionExpanded((open) => !open), className: "mb-0.5 flex w-full items-center justify-between rounded px-0.5 py-0.5 text-left text-[11px] font-medium text-gray-700 hover:bg-gray-100/80", "aria-expanded": subscribedTriggersSectionExpanded, "data-testid": "agent-subscribed-triggers-section-toggle", children: [_jsx("span", { children: `Subscribed triggers (${activeTriggerSubscriptions.length})` }), subscribedTriggersSectionExpanded ? (_jsx(ChevronUp, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 })) : (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-gray-400", strokeWidth: 1.5 }))] }), subscribedTriggersSectionExpanded && (_jsx("div", { className: "max-h-28 overflow-y-auto rounded border border-gray-100 bg-gray-50/50 p-1", "data-testid": "agent-subscribed-triggers-section", children: triggerSubscriptionsLoading ? (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: "Loading subscribed triggers..." })) : activeTriggerSubscriptions.length > 0 ? (_jsx("div", { className: "space-y-0.5", children: activeTriggerSubscriptions.map((sub) => {
6507
5836
  const filterText = (sub.filter || "").trim();
6508
5837
  return (_jsxs("div", { className: "flex items-baseline gap-1.5 rounded px-1 py-0.5 transition-colors hover:bg-white/60", children: [_jsx("div", { className: "shrink-0 text-[10px] font-medium text-gray-700", children: sub.triggerName }), filterText.length > 0 && (_jsx("div", { className: "truncate text-[9px] text-gray-400", title: filterText, children: filterText }))] }, sub.id));
6509
5838
  }) })) : (_jsx("div", { className: "px-1 text-[10px] text-gray-500", children: isLocalOnlyDraftAgent
6510
5839
  ? "Subscribed triggers are shown after the agent is created"
6511
- : "No active trigger subscriptions" })) })), triggerSubscriptionsError && (_jsx("div", { className: "mt-1 text-[10px] text-red-600", children: triggerSubscriptionsError }))] })] }) })] }), activeProfile?.prompts?.length ? (_jsxs(Popover, { open: showPredefined, onOpenChange: setShowPredefined, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { className: "rounded p-1 hover:bg-gray-100", onClick: () => { }, title: "Predefined prompts", "aria-label": "Predefined prompts", type: "button", children: _jsx(Wand2, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _jsx(PopoverContent, { className: "w-64 p-0", align: "start", children: _jsx("div", { className: "max-h-56 overflow-y-auto p-2", children: activeProfile.prompts.map((p, index) => (_jsx("div", { className: "cursor-pointer rounded p-1.5 text-[10px] text-gray-700 hover:bg-gray-100", onClick: () => {
5840
+ : "No active trigger subscriptions" })) })), triggerSubscriptionsError && (_jsx("div", { className: "mt-1 text-[10px] text-red-600", children: triggerSubscriptionsError }))] }), agent?.id && !isLocalOnlyDraftAgent ? (_jsx("div", { className: "border-t border-gray-200 pt-2", children: _jsx(AgentSharingSection, { agentId: agent.id, canManage: agent?.canManage ?? true }) })) : null] }) })] }), activeProfile?.prompts?.length ? (_jsxs(Popover, { open: showPredefined, onOpenChange: setShowPredefined, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { className: "rounded p-1 hover:bg-gray-100", onClick: () => { }, title: "Predefined prompts", "aria-label": "Predefined prompts", type: "button", children: _jsx(Wand2, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _jsx(PopoverContent, { className: "w-64 p-0", align: "start", children: _jsx("div", { className: "max-h-56 overflow-y-auto p-2", children: activeProfile.prompts.map((p, index) => (_jsx("div", { className: "cursor-pointer rounded p-1.5 text-[10px] text-gray-700 hover:bg-gray-100", onClick: () => {
6512
5841
  setPrompt(p.prompt);
6513
5842
  setShowPredefined(false);
6514
5843
  if (textareaRef.current)