@exoscient/control-panel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/AppControlPanel.d.ts +77 -0
- package/dist/AppControlPanel.d.ts.map +1 -0
- package/dist/AppControlPanel.js +1625 -0
- package/dist/AppControlPanel.js.map +1 -0
- package/dist/ControlPanelShell.d.ts +39 -0
- package/dist/ControlPanelShell.d.ts.map +1 -0
- package/dist/ControlPanelShell.js +152 -0
- package/dist/ControlPanelShell.js.map +1 -0
- package/dist/ExoLauncherSimulator.d.ts +36 -0
- package/dist/ExoLauncherSimulator.d.ts.map +1 -0
- package/dist/ExoLauncherSimulator.js +253 -0
- package/dist/ExoLauncherSimulator.js.map +1 -0
- package/dist/TaskDetail.d.ts +180 -0
- package/dist/TaskDetail.d.ts.map +1 -0
- package/dist/TaskDetail.js +889 -0
- package/dist/TaskDetail.js.map +1 -0
- package/dist/TaskListPage.d.ts +28 -0
- package/dist/TaskListPage.d.ts.map +1 -0
- package/dist/TaskListPage.js +16 -0
- package/dist/TaskListPage.js.map +1 -0
- package/dist/TaskWorkspace.d.ts +62 -0
- package/dist/TaskWorkspace.d.ts.map +1 -0
- package/dist/TaskWorkspace.js +592 -0
- package/dist/TaskWorkspace.js.map +1 -0
- package/dist/ai-plane.d.ts +75 -0
- package/dist/ai-plane.d.ts.map +1 -0
- package/dist/ai-plane.js +124 -0
- package/dist/ai-plane.js.map +1 -0
- package/dist/browser-icons.d.ts +25 -0
- package/dist/browser-icons.d.ts.map +1 -0
- package/dist/browser-icons.js +125 -0
- package/dist/browser-icons.js.map +1 -0
- package/dist/client-actions.d.ts +45 -0
- package/dist/client-actions.d.ts.map +1 -0
- package/dist/client-actions.js +48 -0
- package/dist/client-actions.js.map +1 -0
- package/dist/control-panel-shared.d.ts +58 -0
- package/dist/control-panel-shared.d.ts.map +1 -0
- package/dist/control-panel-shared.js +79 -0
- package/dist/control-panel-shared.js.map +1 -0
- package/dist/control-panel.css +4156 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/repository-workflow.d.ts +27 -0
- package/dist/repository-workflow.d.ts.map +1 -0
- package/dist/repository-workflow.js +24 -0
- package/dist/repository-workflow.js.map +1 -0
- package/dist/result.d.ts +6 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +77 -0
- package/dist/result.js.map +1 -0
- package/dist/task-consistency.d.ts +28 -0
- package/dist/task-consistency.d.ts.map +1 -0
- package/dist/task-consistency.js +25 -0
- package/dist/task-consistency.js.map +1 -0
- package/dist/task-detail.browser.js +2709 -0
- package/dist/task-detail.css +1601 -0
- package/dist/task-state.d.ts +11 -0
- package/dist/task-state.d.ts.map +1 -0
- package/dist/task-state.js +103 -0
- package/dist/task-state.js.map +1 -0
- package/dist/telemetry.d.ts +39 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +106 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace.d.ts +80 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +694 -0
- package/dist/trace.js.map +1 -0
- package/dist/updates.d.ts +72 -0
- package/dist/updates.d.ts.map +1 -0
- package/dist/updates.js +269 -0
- package/dist/updates.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Check, Hourglass, Maximize2, Minimize2, PanelBottomClose, PanelBottomOpen, Paperclip, RefreshCw, Send, Share2, Square, Trash2, Waypoints, X } from 'lucide-react';
|
|
4
|
+
import { externalContinuation, isReloadClientAppAction, isUserDecisionAction, userDecisionAllowsOther, userDecisionOtherLabel, userDecisionOtherPlaceholder, userDecisionSelectionMode, } from './client-actions';
|
|
5
|
+
import { splitAgentResult } from './result';
|
|
6
|
+
import { buildTraceItems, latestContextPressure, traceItemMatchesFilter } from './trace';
|
|
7
|
+
export function TaskDetailPage({ restoreMaximized, setMaximized, setMinimized, setRestoreMaximized, setOpenTraceTurnId, onCloseTask, onCloseReset, ...taskDetailProps }) {
|
|
8
|
+
return (_jsx(TaskDetail, { ...taskDetailProps, onClose: () => {
|
|
9
|
+
onCloseTask();
|
|
10
|
+
setMinimized(false);
|
|
11
|
+
setRestoreMaximized(false);
|
|
12
|
+
setOpenTraceTurnId(null);
|
|
13
|
+
onCloseReset?.();
|
|
14
|
+
}, onMinimize: () => {
|
|
15
|
+
setRestoreMaximized(taskDetailProps.maximized);
|
|
16
|
+
setMinimized(true);
|
|
17
|
+
setMaximized(false);
|
|
18
|
+
setOpenTraceTurnId(null);
|
|
19
|
+
}, onRestore: () => {
|
|
20
|
+
setMinimized(false);
|
|
21
|
+
setMaximized(restoreMaximized);
|
|
22
|
+
}, onToggleMaximized: () => setMaximized((maximized) => !maximized), onCloseTrace: () => setOpenTraceTurnId(null) }));
|
|
23
|
+
}
|
|
24
|
+
export function TaskDetail({ task, maximized, minimized, platformStatus, elapsedNow, traceLoadingTurnId, selectedTraceTurn, selectedTraceTurnIndex, turnRefs, followUp, followUpAttachments, followUpSubmitting, followUpError = null, interruptMessage, interruptSubmitting, steerSubmitting, taskCommandStatus = null, refreshSubmitting = false, onClose, onMinimize, onRestore, onToggleMaximized, onRefresh, onOpenTrace, onCloseTrace, onSubmitFollowUp, onSubmitSteer, onFollowUpChange, onFollowUpAttachmentsChange, uploadAttachment, onSubmitInterrupt, onInterruptMessageChange, onSubmitClientAction, currentUserId, shareTask, unshareTask, }) {
|
|
25
|
+
const [followUpAttachmentsProcessing, setFollowUpAttachmentsProcessing] = useState(false);
|
|
26
|
+
const [accessDetailsOpen, setAccessDetailsOpen] = useState(false);
|
|
27
|
+
const [shareUser, setShareUser] = useState('');
|
|
28
|
+
const [shareLevel, setShareLevel] = useState('read');
|
|
29
|
+
const [shareSubmitting, setShareSubmitting] = useState(false);
|
|
30
|
+
const [shareError, setShareError] = useState(null);
|
|
31
|
+
const [clientActionSubmitting, setClientActionSubmitting] = useState(false);
|
|
32
|
+
const [clientActionError, setClientActionError] = useState(null);
|
|
33
|
+
const previousTaskStateRef = useRef(null);
|
|
34
|
+
const taskInputPlaceholder = 'Send instructions to the AI agent';
|
|
35
|
+
const taskQueued = task ? isTaskQueued(task) : false;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setAccessDetailsOpen(false);
|
|
38
|
+
setShareUser('');
|
|
39
|
+
setShareLevel('read');
|
|
40
|
+
setShareError(null);
|
|
41
|
+
setClientActionError(null);
|
|
42
|
+
}, [task?.id]);
|
|
43
|
+
const canManageSharing = Boolean(task && shareTask && unshareTask && (!currentUserId || task.createdByUserId === currentUserId));
|
|
44
|
+
async function submitShare(event) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
if (!task || !shareTask || !shareUser.trim() || shareSubmitting)
|
|
47
|
+
return;
|
|
48
|
+
setShareSubmitting(true);
|
|
49
|
+
setShareError(null);
|
|
50
|
+
try {
|
|
51
|
+
const updated = await shareTask(task.id, {
|
|
52
|
+
userId: shareUser.trim(),
|
|
53
|
+
email: shareUser.includes('@') ? shareUser.trim() : undefined,
|
|
54
|
+
accessLevel: shareLevel,
|
|
55
|
+
});
|
|
56
|
+
if (updated)
|
|
57
|
+
setShareUser('');
|
|
58
|
+
else
|
|
59
|
+
setShareError('Task sharing failed.');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
setShareError(error instanceof Error ? error.message : 'Task sharing failed.');
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setShareSubmitting(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function removeShare(userId) {
|
|
69
|
+
if (!task || !unshareTask || shareSubmitting)
|
|
70
|
+
return;
|
|
71
|
+
setShareSubmitting(true);
|
|
72
|
+
setShareError(null);
|
|
73
|
+
try {
|
|
74
|
+
const updated = await unshareTask(task.id, userId);
|
|
75
|
+
if (!updated)
|
|
76
|
+
setShareError('Task sharing failed.');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
setShareError(error instanceof Error ? error.message : 'Task sharing failed.');
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setShareSubmitting(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const previous = previousTaskStateRef.current;
|
|
87
|
+
if (!task) {
|
|
88
|
+
previousTaskStateRef.current = null;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (previous?.id === task.id && previous.busy && !isTaskBusy(task) && isTaskFinishedStatus(task.status)) {
|
|
92
|
+
notifyTaskFinished(task);
|
|
93
|
+
}
|
|
94
|
+
previousTaskStateRef.current = { id: task.id, busy: isTaskBusy(task), status: task.status };
|
|
95
|
+
}, [task]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!task || !isReloadClientAppAction(task.clientAction))
|
|
98
|
+
return;
|
|
99
|
+
if (typeof window === 'undefined')
|
|
100
|
+
return;
|
|
101
|
+
const action = task.clientAction;
|
|
102
|
+
const key = `exo-client-action:${task.id}:${action.id}`;
|
|
103
|
+
try {
|
|
104
|
+
if (window.sessionStorage.getItem(key))
|
|
105
|
+
return;
|
|
106
|
+
window.sessionStorage.setItem(key, new Date().toISOString());
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Storage is only for deduping reload loops; continue if unavailable.
|
|
110
|
+
}
|
|
111
|
+
const delay = typeof action.delayMs === 'number' && Number.isFinite(action.delayMs)
|
|
112
|
+
? Math.max(0, Math.min(30000, action.delayMs))
|
|
113
|
+
: 0;
|
|
114
|
+
const timer = window.setTimeout(() => window.location.reload(), delay);
|
|
115
|
+
return () => window.clearTimeout(timer);
|
|
116
|
+
}, [task?.id, task?.clientAction]);
|
|
117
|
+
async function submitClientAction(action, message) {
|
|
118
|
+
const continuation = externalContinuation(action);
|
|
119
|
+
if (!task || !onSubmitClientAction || !continuation?.token || clientActionSubmitting)
|
|
120
|
+
return;
|
|
121
|
+
setClientActionSubmitting(true);
|
|
122
|
+
setClientActionError(null);
|
|
123
|
+
try {
|
|
124
|
+
const updated = await onSubmitClientAction(task.id, { actionId: action.id, token: continuation.token, message });
|
|
125
|
+
if (!updated)
|
|
126
|
+
setClientActionError('Decision could not be submitted.');
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
setClientActionError(error instanceof Error ? error.message : 'Decision could not be submitted.');
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
setClientActionSubmitting(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return (_jsxs(_Fragment, { children: [task && minimized && (_jsxs("div", { className: "task-detail-dock", role: "region", "aria-label": "Minimized task detail", children: [_jsxs("div", { className: "task-detail-dock-main", children: [_jsx("strong", { title: formatTaskFullTitle(task), children: formatTaskDetailTitle(task) }), _jsx("span", { children: taskStatusContent(task, platformStatus) })] }), _jsxs("div", { className: "task-detail-dock-actions", children: [onRefresh && (_jsx("button", { className: "icon-button", onClick: onRefresh, disabled: refreshSubmitting, "aria-label": "Refresh task detail", title: "Refresh", children: refreshSubmitting ? _jsx(InlineSpinner, {}) : _jsx(RefreshCw, { "aria-hidden": "true" }) })), _jsx("button", { className: "icon-button", onClick: onRestore, "aria-label": "Restore task detail", title: "Restore", children: _jsx(PanelBottomOpen, { "aria-hidden": "true" }) }), _jsx("button", { className: "icon-button", onClick: onClose, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] })] })), _jsx("aside", { className: `drawer${task && !minimized ? ' open' : ''}${maximized ? ' maximized' : ''}`, "aria-hidden": !task || minimized, children: task && (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsxs("div", { className: "drawer-heading", children: [_jsxs("div", { className: "drawer-status-row", children: [_jsx("p", { children: taskStatusContent(task, platformStatus) }), _jsx(TaskAccessSummary, { task: task, detailsOpen: accessDetailsOpen, onToggle: () => setAccessDetailsOpen((open) => !open) })] }), _jsxs("div", { className: "drawer-tools", children: [onRefresh && (_jsx("button", { className: "icon-button", onClick: onRefresh, disabled: refreshSubmitting, "aria-label": "Refresh task detail", title: "Refresh", children: refreshSubmitting ? _jsx(InlineSpinner, {}) : _jsx(RefreshCw, { "aria-hidden": "true" }) })), _jsx("button", { className: "icon-button", onClick: onMinimize, "aria-label": "Minimize task detail", title: "Minimize", children: _jsx(PanelBottomClose, { "aria-hidden": "true" }) }), _jsx("button", { className: "icon-button", onClick: onToggleMaximized, "aria-label": maximized ? 'Restore task detail' : 'Maximize task detail', title: maximized ? 'Restore' : 'Maximize', children: maximized ? _jsx(Minimize2, { "aria-hidden": "true" }) : _jsx(Maximize2, { "aria-hidden": "true" }) }), _jsx("button", { className: "icon-button", onClick: onClose, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] })] }), _jsx("h2", { title: formatTaskFullTitle(task), children: formatTaskDetailTitle(task) }), accessDetailsOpen && _jsx(TaskDetailDetails, { task: task }), canManageSharing && (_jsx(TaskSharingPanel, { task: task, shareUser: shareUser, shareLevel: shareLevel, submitting: shareSubmitting, error: shareError, onShareUserChange: setShareUser, onShareLevelChange: setShareLevel, onSubmit: submitShare, onRemove: removeShare }))] }), _jsxs("div", { className: "drawer-content", children: [_jsxs("div", { className: "drawer-body", children: [task.updatePublicationFailures?.length ? (_jsx(TaskUpdatePublicationWarning, { failures: task.updatePublicationFailures })) : null, task.turns.map((turn, index) => {
|
|
136
|
+
const result = taskTurnResult(task, turn, index);
|
|
137
|
+
return (_jsxs("article", { className: "turn", ref: (element) => {
|
|
138
|
+
if (element) {
|
|
139
|
+
turnRefs.current.set(turn.id, element);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
turnRefs.current.delete(turn.id);
|
|
143
|
+
}
|
|
144
|
+
}, children: [_jsxs("section", { className: "turn-panel request-panel", "aria-label": `Request ${index + 1}`, children: [_jsx(TurnTitle, { index: index, timestamp: turn.createdAt }), _jsx("p", { children: turn.content })] }), _jsx(SteeringMessages, { messages: taskSteeringMessages(task, index) }), _jsxs("section", { className: "turn-panel output-panel", "aria-label": `AI agent output for request ${index + 1}`, children: [_jsxs("div", { className: "turn-title output-title", children: [_jsx("h3", { children: formatAgentTitle(turn, elapsedNow) }), _jsx("time", { children: formatCompletionTime(turn) })] }), _jsx("div", { className: "result", children: isTurnQueued(turn) ? (_jsx("div", { className: "working-result-body", children: _jsx("span", { className: `status-text ${statusToneClass(turn.status)}`, children: "Queued" }) })) : isTurnWorking(turn) ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "working-result-body", children: _jsx(WorkingIndicator, { label: platformStatus.available ? 'Working' : 'The platform is down. Wait until it is back up', animate: platformStatus.available }) }), _jsx("div", { className: "output-panel-actions", children: _jsx(TraceButton, { loading: traceLoadingTurnId === turn.id, onClick: () => onOpenTrace(turn.id) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(FormattedText, { text: result.owner || 'No output recorded.' }), result.technical && (_jsxs("details", { className: "technical-summary", children: [_jsxs("summary", { children: [_jsx("span", { className: "technical-summary-title", children: "Technical summary" }), _jsx(TraceButton, { loading: traceLoadingTurnId === turn.id, onClick: () => onOpenTrace(turn.id), stopPropagation: true })] }), _jsx("div", { className: "technical-summary-body", children: _jsx(FormattedText, { text: result.technical }) })] })), !result.technical && (_jsx("div", { className: "output-panel-actions", children: _jsx(TraceButton, { loading: traceLoadingTurnId === turn.id, onClick: () => onOpenTrace(turn.id) }) })), index === task.turns.length - 1 && isUserDecisionAction(task.clientAction) && (_jsx(ClientActionPanel, { action: task.clientAction, submitting: clientActionSubmitting, error: clientActionError, canSubmit: Boolean(onSubmitClientAction), onSubmit: (message) => void submitClientAction(task.clientAction, message) }))] })) })] })] }, turn.id));
|
|
145
|
+
})] }), _jsx("form", { className: "drawer-form", onSubmit: followUpAttachmentsProcessing ? (event) => event.preventDefault() : onSubmitFollowUp, children: task.busy ? (_jsxs("div", { className: "interrupt-panel", children: [_jsxs("div", { className: "drawer-form-toolbar", children: [_jsx(CommandStatus, { status: taskCommandStatus }), _jsxs("div", { className: "drawer-form-buttons", children: [_jsx("button", { className: "primary icon-action-button", type: "button", disabled: taskQueued || interruptSubmitting || steerSubmitting || !interruptMessage.trim(), onClick: onSubmitSteer, "aria-label": "Send steering message", title: "Steer", children: steerSubmitting ? _jsx(InlineSpinner, {}) : (_jsx(Send, { "aria-hidden": "true" })) }), _jsx("button", { className: "danger icon-action-button", type: "button", disabled: interruptSubmitting || steerSubmitting, onClick: onSubmitInterrupt, "aria-label": "Interrupt task", title: "Interrupt", children: interruptSubmitting ? _jsx(InlineSpinner, {}) : (_jsx(Square, { "aria-hidden": "true" })) })] })] }), _jsx("textarea", { disabled: taskQueued || interruptSubmitting || steerSubmitting, value: interruptMessage, onChange: (event) => onInterruptMessageChange(event.target.value), placeholder: taskQueued ? 'Task is queued' : taskInputPlaceholder })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "drawer-form-toolbar", children: [_jsx(AttachmentPicker, { attachments: followUpAttachments, disabled: followUpSubmitting, onChange: onFollowUpAttachmentsChange, onProcessingChange: setFollowUpAttachmentsProcessing, uploadAttachment: uploadAttachment, compact: true }), followUpError && _jsx("span", { className: "command-status error", role: "alert", children: followUpError }), _jsx("div", { className: "drawer-form-buttons", children: _jsx("button", { className: "primary icon-action-button", disabled: followUpAttachmentsProcessing || followUpSubmitting || !followUp.trim(), "aria-label": "Continue task", title: "Continue", children: followUpAttachmentsProcessing || followUpSubmitting ? _jsx(InlineSpinner, {}) : (_jsx(Send, { "aria-hidden": "true" })) }) })] }), _jsx("textarea", { disabled: followUpSubmitting, value: followUp, onChange: (event) => onFollowUpChange(event.target.value), placeholder: taskInputPlaceholder })] })) }), selectedTraceTurn && (_jsx("div", { className: "task-trace-overlay", role: "dialog", "aria-modal": "false", "aria-label": `Trace for request ${selectedTraceTurnIndex + 1}`, children: _jsx(TracePanel, { turn: selectedTraceTurn, turnIndex: selectedTraceTurnIndex, loading: traceLoadingTurnId === selectedTraceTurn.id && (selectedTraceTurn.trace?.events?.length ?? 0) === 0, onCollapse: onCloseTrace }) }))] })] })) })] }));
|
|
146
|
+
}
|
|
147
|
+
function TaskUpdatePublicationWarning({ failures }) {
|
|
148
|
+
const latest = failures[failures.length - 1];
|
|
149
|
+
const previousCount = failures.length - 1;
|
|
150
|
+
return (_jsxs("section", { className: "task-publication-warning", "aria-label": "Task update publication warning", children: [_jsx("strong", { children: "Update publication degraded" }), _jsxs("span", { children: [latest.updateType, " could not be published. Retry status: ", formatRetryStatus(latest.retryStatus, latest.retryAttempts), ".", previousCount > 0 ? ` ${previousCount} earlier update${previousCount === 1 ? '' : 's'} also failed.` : ''] })] }));
|
|
151
|
+
}
|
|
152
|
+
function ClientActionPanel({ action, submitting, error, canSubmit, onSubmit, }) {
|
|
153
|
+
const mode = userDecisionSelectionMode(action);
|
|
154
|
+
const options = action.options.filter((option) => option && typeof option.id === 'string' && typeof option.label === 'string');
|
|
155
|
+
const [selectedIds, setSelectedIds] = useState(() => options[0]?.id ? [options[0].id] : []);
|
|
156
|
+
const [otherText, setOtherText] = useState('');
|
|
157
|
+
const allowsOther = userDecisionAllowsOther(action);
|
|
158
|
+
const continuation = externalContinuation(action);
|
|
159
|
+
const canContinue = canSubmit && Boolean(continuation?.token) && (selectedIds.length > 0 || (allowsOther && Boolean(otherText.trim())));
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
setSelectedIds(options[0]?.id ? [options[0].id] : []);
|
|
162
|
+
setOtherText('');
|
|
163
|
+
}, [action.id]);
|
|
164
|
+
function toggleOption(optionId) {
|
|
165
|
+
if (mode === 'single') {
|
|
166
|
+
setSelectedIds([optionId]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setSelectedIds((current) => current.includes(optionId)
|
|
170
|
+
? current.filter((id) => id !== optionId)
|
|
171
|
+
: [...current, optionId]);
|
|
172
|
+
}
|
|
173
|
+
function submit(event) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
if (!canContinue || submitting)
|
|
176
|
+
return;
|
|
177
|
+
onSubmit(`User decision result:\n${JSON.stringify({
|
|
178
|
+
clientActionResult: {
|
|
179
|
+
type: 'userDecision',
|
|
180
|
+
actionId: action.id,
|
|
181
|
+
selectedOptionIds: selectedIds,
|
|
182
|
+
otherText: allowsOther && otherText.trim() ? otherText.trim() : null,
|
|
183
|
+
submittedAt: new Date().toISOString(),
|
|
184
|
+
},
|
|
185
|
+
}, null, 2)}`);
|
|
186
|
+
}
|
|
187
|
+
return (_jsxs("form", { className: "client-action-panel", onSubmit: submit, children: [_jsx("div", { className: "client-action-heading", children: _jsx("strong", { children: action.prompt }) }), _jsx("div", { className: "client-action-options", children: options.map((option) => {
|
|
188
|
+
const checked = selectedIds.includes(option.id);
|
|
189
|
+
return (_jsxs("label", { className: "client-action-option", children: [_jsx("input", { type: mode === 'multiple' ? 'checkbox' : 'radio', name: `client-action-${action.id}`, checked: checked, onChange: () => toggleOption(option.id) }), _jsxs("span", { children: [_jsx("strong", { children: option.label }), option.description && _jsx("small", { children: option.description })] })] }, option.id));
|
|
190
|
+
}) }), allowsOther && (_jsxs("label", { className: "client-action-other", children: [_jsx("span", { children: userDecisionOtherLabel(action) }), _jsx("textarea", { value: otherText, onChange: (event) => setOtherText(event.target.value), placeholder: userDecisionOtherPlaceholder(action), rows: 2 })] })), _jsxs("div", { className: "client-action-footer", children: [error && _jsx("span", { className: "command-status error", role: "alert", children: error }), !continuation?.token && _jsx("span", { className: "command-status muted", children: "Waiting for continuation access." }), _jsx("button", { className: "primary icon-action-button", type: "submit", disabled: !canContinue || submitting, "aria-label": "Submit decision", title: "Submit decision", children: submitting ? _jsx(InlineSpinner, {}) : _jsx(Check, { "aria-hidden": "true" }) })] })] }));
|
|
191
|
+
}
|
|
192
|
+
function formatRetryStatus(status, attempts) {
|
|
193
|
+
const label = status.trim().replace(/[-_]+/g, ' ') || 'unknown';
|
|
194
|
+
return attempts > 0 ? `${label}, ${attempts} attempt${attempts === 1 ? '' : 's'}` : label;
|
|
195
|
+
}
|
|
196
|
+
function TaskSharingPanel({ task, shareUser, shareLevel, submitting, error, onShareUserChange, onShareLevelChange, onSubmit, onRemove, }) {
|
|
197
|
+
return (_jsxs("section", { className: "task-sharing-panel", "aria-label": "Task sharing", children: [_jsxs("form", { className: "task-sharing-form", onSubmit: onSubmit, children: [_jsx(Share2, { "aria-hidden": "true" }), _jsx("input", { value: shareUser, onChange: (event) => onShareUserChange(event.target.value), placeholder: "User ID or email", disabled: submitting }), _jsxs("select", { value: shareLevel, onChange: (event) => onShareLevelChange(event.target.value), disabled: submitting, children: [_jsx("option", { value: "read", children: "Read only" }), _jsx("option", { value: "full", children: "Full" })] }), _jsx("button", { type: "submit", disabled: submitting || !shareUser.trim(), children: submitting ? _jsx(InlineSpinner, {}) : 'Share' })] }), error && _jsx("div", { className: "task-sharing-error", children: error }), (task.shares?.length ?? 0) > 0 && (_jsx("div", { className: "task-share-list", children: task.shares.map((share) => (_jsxs("div", { className: "task-share-row", children: [_jsx("span", { children: share.displayName || share.email || share.userId }), _jsx("strong", { children: share.accessLevel === 'full' ? 'Full' : 'Read only' }), _jsx("button", { type: "button", onClick: () => onRemove(share.userId), disabled: submitting, "aria-label": `Remove ${share.displayName || share.email || share.userId}`, children: _jsx(Trash2, { "aria-hidden": "true" }) })] }, share.userId))) }))] }));
|
|
198
|
+
}
|
|
199
|
+
function TaskDetailDetails({ task }) {
|
|
200
|
+
return (_jsxs("div", { className: "task-detail-details", children: [_jsx(TaskDetailMeta, { task: task }), _jsx(TaskAccessChips, { task: task })] }));
|
|
201
|
+
}
|
|
202
|
+
function TaskDetailMeta({ task }) {
|
|
203
|
+
const startedAt = task.startedAt ?? task.createdAt;
|
|
204
|
+
const completedAt = task.completedAt ?? null;
|
|
205
|
+
const updatedAt = task.updatedAt ?? completedAt ?? startedAt;
|
|
206
|
+
const turnsCount = task.turns.length;
|
|
207
|
+
const codexModel = normalizeMetadataValue(task.codexModel);
|
|
208
|
+
const codexReasoningEffort = normalizeMetadataValue(task.codexReasoningEffort);
|
|
209
|
+
const creator = formatCreatorName(task);
|
|
210
|
+
return (_jsxs("div", { className: "drawer-meta", "aria-label": "Task metadata", children: [_jsxs("span", { children: [_jsx("span", { className: "muted", children: "ID" }), "\u00A0", _jsx("code", { children: task.id })] }), _jsxs("span", { children: [_jsx("span", { className: "muted", children: "Creator" }), "\u00A0", creator] }), codexModel && _jsxs("span", { children: [_jsx("span", { className: "muted", children: "Model" }), "\u00A0", _jsx("code", { children: codexModel })] }), codexReasoningEffort && _jsxs("span", { children: [_jsx("span", { className: "muted", children: "Effort" }), "\u00A0", _jsx("code", { children: codexReasoningEffort })] }), _jsxs("span", { children: [_jsx("span", { className: "muted", children: "Started" }), "\u00A0", _jsx("time", { dateTime: startedAt, children: formatDateTime(startedAt) })] }), _jsxs("span", { children: [_jsx("span", { className: "muted", children: "Updated" }), "\u00A0", _jsx("time", { dateTime: updatedAt, children: formatDateTime(updatedAt) })] }), _jsxs("span", { children: [_jsx("span", { className: "muted", children: "Turns" }), "\u00A0", turnsCount] })] }));
|
|
211
|
+
}
|
|
212
|
+
function formatCreatorName(task) {
|
|
213
|
+
return normalizeMetadataValue(task.createdByUserDisplayName)
|
|
214
|
+
?? normalizeMetadataValue(task.createdByUserEmail)
|
|
215
|
+
?? normalizeMetadataValue(task.createdByUserId)
|
|
216
|
+
?? 'Unknown';
|
|
217
|
+
}
|
|
218
|
+
function normalizeMetadataValue(value) {
|
|
219
|
+
const normalized = value?.trim();
|
|
220
|
+
return normalized ? normalized : null;
|
|
221
|
+
}
|
|
222
|
+
function TraceButton({ loading, onClick, stopPropagation = false, }) {
|
|
223
|
+
return (_jsxs("button", { className: `trace-open-button${loading ? ' loading' : ''}`, type: "button", onClick: (event) => {
|
|
224
|
+
if (stopPropagation)
|
|
225
|
+
event.stopPropagation();
|
|
226
|
+
onClick();
|
|
227
|
+
}, disabled: loading, "aria-busy": loading, children: [loading ? _jsx(InlineSpinner, {}) : _jsx(Waypoints, { "aria-hidden": "true" }), _jsx("span", { children: loading ? 'Loading trace' : 'Trace' })] }));
|
|
228
|
+
}
|
|
229
|
+
function TaskAccessSummary({ task, detailsOpen, onToggle, }) {
|
|
230
|
+
const counts = accessRequestCounts(groupedAccessRequests(task.accessRequests ?? []));
|
|
231
|
+
const total = counts.granted + counts.released + counts.waiting + counts.denied;
|
|
232
|
+
return (_jsx("button", { className: `task-access-summary${total === 0 ? ' task-details-summary' : ''}`, type: "button", onClick: onToggle, "aria-expanded": detailsOpen, "aria-label": accessSummaryLabel(counts, detailsOpen), title: detailsOpen ? 'Hide access details' : 'Show access details', children: total === 0 ? (_jsx("span", { className: "task-details-summary-label", children: "Details" })) : (_jsxs(_Fragment, { children: [counts.granted > 0 && _jsx(TaskAccessSummaryItem, { state: "granted", count: counts.granted }), counts.released > 0 && _jsx(TaskAccessSummaryItem, { state: "released", count: counts.released }), counts.waiting > 0 && _jsx(TaskAccessSummaryItem, { state: "waiting", count: counts.waiting }), counts.denied > 0 && _jsx(TaskAccessSummaryItem, { state: "denied", count: counts.denied })] })) }));
|
|
233
|
+
}
|
|
234
|
+
function TaskAccessSummaryItem({ state, count }) {
|
|
235
|
+
const Icon = accessRequestIcon(state);
|
|
236
|
+
return (_jsxs("span", { className: `task-access-summary-item ${state}`, children: [_jsx(Icon, { className: "task-access-summary-icon", "aria-hidden": "true" }), _jsx("span", { children: count })] }));
|
|
237
|
+
}
|
|
238
|
+
function TaskAccessChips({ task }) {
|
|
239
|
+
const requests = groupedAccessRequests(task.accessRequests ?? []);
|
|
240
|
+
if (requests.length === 0) {
|
|
241
|
+
return (_jsx("div", { className: "task-access-chips", "aria-label": "Agent access", children: _jsx("span", { className: "task-access-chip muted", children: "No access requests" }) }));
|
|
242
|
+
}
|
|
243
|
+
return (_jsx("div", { className: "task-access-chips", "aria-label": "Agent access", children: requests.map((request, index) => (_jsx(TaskAccessChip, { request: request, index: index }, accessRequestKey(request, index)))) }));
|
|
244
|
+
}
|
|
245
|
+
function TaskAccessChip({ request, index }) {
|
|
246
|
+
const state = accessRequestState(request);
|
|
247
|
+
const label = accessRequestLabel(request);
|
|
248
|
+
const Icon = accessRequestIcon(state);
|
|
249
|
+
const occurrences = accessRequestOccurrences(request);
|
|
250
|
+
return (_jsxs("span", { className: `task-access-chip ${state}`, title: `${label} ${request.level} access to ${request.target} at ${formatDateTime(accessRequestTime(request))}${occurrences > 1 ? ` (${occurrences} requests)` : ''}`, "aria-label": `${label} ${request.target} ${request.level}`, "data-access-index": index, children: [_jsx(Icon, { className: "task-access-icon", "aria-hidden": "true" }), _jsxs("span", { className: "task-access-text", children: [_jsx("strong", { children: request.target }), " ", request.level, occurrences > 1 ? ` x${occurrences}` : ''] })] }));
|
|
251
|
+
}
|
|
252
|
+
function groupedAccessRequests(requests) {
|
|
253
|
+
const groups = new Map();
|
|
254
|
+
for (const request of requests) {
|
|
255
|
+
const key = `${request.target.trim().toLowerCase()}\u0000${request.level.trim().toLowerCase()}`;
|
|
256
|
+
groups.set(key, [...(groups.get(key) ?? []), request]);
|
|
257
|
+
}
|
|
258
|
+
return Array.from(groups.values())
|
|
259
|
+
.map((group) => {
|
|
260
|
+
const ordered = [...group].sort((left, right) => Date.parse(left.requestedAt) - Date.parse(right.requestedAt));
|
|
261
|
+
const active = ordered.filter((request) => accessRequestState(request) !== 'released');
|
|
262
|
+
const selected = latestAccessRequest(active.length > 0 ? active : ordered);
|
|
263
|
+
return {
|
|
264
|
+
...selected,
|
|
265
|
+
occurrences: ordered.length,
|
|
266
|
+
firstRequestedAt: ordered[0]?.requestedAt,
|
|
267
|
+
lastRequestedAt: ordered[ordered.length - 1]?.requestedAt,
|
|
268
|
+
};
|
|
269
|
+
})
|
|
270
|
+
.sort((left, right) => Date.parse(accessRequestTime(right)) - Date.parse(accessRequestTime(left)));
|
|
271
|
+
}
|
|
272
|
+
function latestAccessRequest(requests) {
|
|
273
|
+
return requests.reduce((latest, request) => (Date.parse(accessRequestTime(request)) >= Date.parse(accessRequestTime(latest)) ? request : latest), requests[0]);
|
|
274
|
+
}
|
|
275
|
+
function accessRequestState(request) {
|
|
276
|
+
if (request.status?.toLowerCase() === 'released' || request.releasedAt)
|
|
277
|
+
return 'released';
|
|
278
|
+
if (request.granted)
|
|
279
|
+
return 'granted';
|
|
280
|
+
const status = `${request.status ?? ''} ${request.message ?? ''}`.toLowerCase();
|
|
281
|
+
if (status.includes('wait') || status.includes('queue'))
|
|
282
|
+
return 'waiting';
|
|
283
|
+
return 'denied';
|
|
284
|
+
}
|
|
285
|
+
function accessRequestIcon(state) {
|
|
286
|
+
return state === 'granted' || state === 'released' ? Check : state === 'waiting' ? Hourglass : Square;
|
|
287
|
+
}
|
|
288
|
+
function accessRequestCounts(requests) {
|
|
289
|
+
return requests.reduce((counts, request) => {
|
|
290
|
+
counts[accessRequestState(request)] += 1;
|
|
291
|
+
return counts;
|
|
292
|
+
}, { granted: 0, released: 0, waiting: 0, denied: 0 });
|
|
293
|
+
}
|
|
294
|
+
function accessSummaryLabel(counts, detailsOpen) {
|
|
295
|
+
const parts = [
|
|
296
|
+
counts.granted > 0 ? `${counts.granted} granted` : null,
|
|
297
|
+
counts.released > 0 ? `${counts.released} released` : null,
|
|
298
|
+
counts.waiting > 0 ? `${counts.waiting} waiting` : null,
|
|
299
|
+
counts.denied > 0 ? `${counts.denied} denied` : null,
|
|
300
|
+
].filter(Boolean);
|
|
301
|
+
if (parts.length === 0)
|
|
302
|
+
return `${detailsOpen ? 'Hide' : 'Show'} task details`;
|
|
303
|
+
return `${detailsOpen ? 'Hide' : 'Show'} access details: ${parts.join(', ')}`;
|
|
304
|
+
}
|
|
305
|
+
function accessRequestLabel(request) {
|
|
306
|
+
const state = accessRequestState(request);
|
|
307
|
+
return state === 'granted' ? 'Granted' : state === 'released' ? 'Released' : state === 'waiting' ? 'Waiting' : 'Denied';
|
|
308
|
+
}
|
|
309
|
+
function accessRequestTime(request) {
|
|
310
|
+
return request.releasedAt ?? request.requestedAt;
|
|
311
|
+
}
|
|
312
|
+
function accessRequestOccurrences(request) {
|
|
313
|
+
return request.occurrences ?? 1;
|
|
314
|
+
}
|
|
315
|
+
function accessRequestKey(request, index) {
|
|
316
|
+
return request.requestId || `${request.requestedAt}-${request.target}-${request.level}-${index}`;
|
|
317
|
+
}
|
|
318
|
+
export function AttachmentPicker({ attachments, disabled, compact, onChange, onProcessingChange, uploadAttachment, }) {
|
|
319
|
+
const inputRef = useRef(null);
|
|
320
|
+
const [processingFiles, setProcessingFiles] = useState([]);
|
|
321
|
+
const hasUploadingAttachments = attachments.some((attachment) => attachment.status === 'uploading');
|
|
322
|
+
const hasFailedAttachments = attachments.some((attachment) => attachment.status === 'failed');
|
|
323
|
+
const hasBlockingAttachments = hasUploadingAttachments || hasFailedAttachments;
|
|
324
|
+
const isProcessing = processingFiles.length > 0 || hasBlockingAttachments;
|
|
325
|
+
const preparingCount = Math.max(processingFiles.length, attachments.filter((attachment) => attachment.status === 'uploading').length);
|
|
326
|
+
const statusLabel = hasFailedAttachments && !hasUploadingAttachments && processingFiles.length === 0
|
|
327
|
+
? 'Remove failed attachment before sending'
|
|
328
|
+
: compact ? 'Preparing attachments' : `Preparing ${preparingCount} attachment${preparingCount === 1 ? '' : 's'}`;
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
onProcessingChange?.(isProcessing);
|
|
331
|
+
}, [isProcessing, onProcessingChange]);
|
|
332
|
+
async function addFiles(files) {
|
|
333
|
+
if (!files || disabled || isProcessing)
|
|
334
|
+
return;
|
|
335
|
+
const selectedFiles = Array.from(files).slice(0, 8);
|
|
336
|
+
const batchId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
337
|
+
const pendingAttachments = selectedFiles.map((file) => ({
|
|
338
|
+
name: file.name,
|
|
339
|
+
contentType: file.type || 'text/plain',
|
|
340
|
+
content: '',
|
|
341
|
+
uploadId: `pending-${batchId}-${file.name}-${file.size}-${file.lastModified}`,
|
|
342
|
+
status: 'uploading',
|
|
343
|
+
progress: 0,
|
|
344
|
+
sizeBytes: file.size,
|
|
345
|
+
}));
|
|
346
|
+
let nextAttachments = [...attachments, ...pendingAttachments];
|
|
347
|
+
onChange(nextAttachments);
|
|
348
|
+
setProcessingFiles(selectedFiles.map((file) => file.name));
|
|
349
|
+
try {
|
|
350
|
+
for (let index = 0; index < selectedFiles.length; index += 1) {
|
|
351
|
+
const file = selectedFiles[index];
|
|
352
|
+
const pending = pendingAttachments[index];
|
|
353
|
+
const pendingId = pending.uploadId;
|
|
354
|
+
const setProgress = (progress) => {
|
|
355
|
+
nextAttachments = nextAttachments.map((attachment) => attachment.uploadId === pendingId ? { ...attachment, progress } : attachment);
|
|
356
|
+
onChange(nextAttachments);
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
const attachment = uploadAttachment
|
|
360
|
+
? await uploadAttachment(file, setProgress)
|
|
361
|
+
: await readAttachment(file);
|
|
362
|
+
nextAttachments = nextAttachments.map((item) => item.uploadId === pendingId ? {
|
|
363
|
+
...attachment,
|
|
364
|
+
name: file.name,
|
|
365
|
+
contentType: file.type || 'text/plain',
|
|
366
|
+
sizeBytes: file.size,
|
|
367
|
+
status: 'uploaded',
|
|
368
|
+
progress: 100,
|
|
369
|
+
} : item);
|
|
370
|
+
onChange(nextAttachments);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
nextAttachments = nextAttachments.map((item) => item.uploadId === pendingId ? {
|
|
374
|
+
...item,
|
|
375
|
+
status: 'failed',
|
|
376
|
+
progress: 0,
|
|
377
|
+
error: error instanceof Error ? error.message : 'Upload failed.',
|
|
378
|
+
} : item);
|
|
379
|
+
onChange(nextAttachments);
|
|
380
|
+
}
|
|
381
|
+
setProcessingFiles(selectedFiles.slice(index + 1).map((item) => item.name));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
setProcessingFiles([]);
|
|
386
|
+
if (inputRef.current)
|
|
387
|
+
inputRef.current.value = '';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return (_jsxs("div", { className: `attachment-picker${compact ? ' compact' : ''}`, children: [_jsx("input", { ref: inputRef, type: "file", multiple: true, onChange: (event) => void addFiles(event.target.files), disabled: disabled || isProcessing }), _jsxs("button", { type: "button", className: "attachment-button", disabled: disabled || isProcessing, onClick: () => inputRef.current?.click(), "aria-label": compact ? 'Attach files' : undefined, title: compact ? 'Attach files' : undefined, children: [_jsx(Paperclip, { "aria-hidden": "true" }), !compact && _jsx("span", { children: "Attach files" })] }), isProcessing && (_jsxs("div", { className: "attachment-status", role: "status", "aria-live": "polite", children: [!hasFailedAttachments && _jsx(InlineSpinner, {}), _jsx("span", { children: statusLabel })] })), attachments.length > 0 && (_jsx("div", { className: "attachment-list", children: attachments.map((attachment, index) => (_jsxs("span", { className: "attachment-chip", children: [_jsx("span", { children: attachment.name }), attachment.status === 'uploading' && _jsxs("small", { children: [Math.max(0, Math.min(100, Math.round(attachment.progress ?? 0))), "%"] }), attachment.status === 'failed' && _jsx("small", { children: attachment.error ?? 'Upload failed' }), _jsx("button", { type: "button", onClick: () => onChange(attachments.filter((_, itemIndex) => itemIndex !== index)), "aria-label": `Remove ${attachment.name}`, children: _jsx(X, { "aria-hidden": "true" }) })] }, `${attachment.name}-${index}`))) }))] }));
|
|
391
|
+
}
|
|
392
|
+
function CommandStatus({ status }) {
|
|
393
|
+
if (!status) {
|
|
394
|
+
return _jsx("span", { "aria-hidden": "true" });
|
|
395
|
+
}
|
|
396
|
+
const label = {
|
|
397
|
+
interrupting: 'Requesting interrupt',
|
|
398
|
+
'interrupt-accepted': 'Interrupt accepted',
|
|
399
|
+
steering: 'Sending steering',
|
|
400
|
+
'steer-accepted': 'Steering accepted',
|
|
401
|
+
failed: 'Command was not accepted',
|
|
402
|
+
}[status];
|
|
403
|
+
return (_jsx("span", { className: `command-status ${status === 'failed' ? 'error' : ''}`, role: "status", children: label }));
|
|
404
|
+
}
|
|
405
|
+
async function readAttachment(file) {
|
|
406
|
+
const maxAttachmentBytes = 10 * 1024 * 1024;
|
|
407
|
+
if (file.size > maxAttachmentBytes) {
|
|
408
|
+
return { content: `[Attachment omitted: ${file.name} is larger than 10 MB.]` };
|
|
409
|
+
}
|
|
410
|
+
const dataUrl = await readFileAsDataUrl(file);
|
|
411
|
+
return { content: dataUrl.split(',', 2)[1] ?? '', contentEncoding: 'base64' };
|
|
412
|
+
}
|
|
413
|
+
function readFileAsDataUrl(file) {
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
const reader = new FileReader();
|
|
416
|
+
reader.onload = () => resolve(String(reader.result));
|
|
417
|
+
reader.onerror = () => reject(reader.error ?? Error('Unable to read attachment.'));
|
|
418
|
+
reader.readAsDataURL(file);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function formatTaskDetailTitle(task) {
|
|
422
|
+
return truncateWords(taskTitleText(task), 15) || formatTaskLabel(task);
|
|
423
|
+
}
|
|
424
|
+
function formatTaskFullTitle(task) {
|
|
425
|
+
return taskTitleText(task) || formatTaskLabel(task);
|
|
426
|
+
}
|
|
427
|
+
export function formatTaskLabel(task) {
|
|
428
|
+
return `Task: created: ${formatDateTime(task.createdAt)}, modified: ${formatDateTime(task.updatedAt ?? task.completedAt ?? task.startedAt ?? task.createdAt)}`;
|
|
429
|
+
}
|
|
430
|
+
export function taskTitleText(task) {
|
|
431
|
+
return collapseWhitespace(task.turns[0]?.content || task.message);
|
|
432
|
+
}
|
|
433
|
+
export function truncateWords(value, maxWords) {
|
|
434
|
+
const words = collapseWhitespace(value).split(' ').filter(Boolean);
|
|
435
|
+
if (words.length <= maxWords)
|
|
436
|
+
return words.join(' ');
|
|
437
|
+
return `${words.slice(0, maxWords).join(' ')}...`;
|
|
438
|
+
}
|
|
439
|
+
function isTaskFinishedStatus(status) {
|
|
440
|
+
const normalized = status.toLocaleLowerCase();
|
|
441
|
+
return normalized === 'closed'
|
|
442
|
+
|| normalized === 'failed'
|
|
443
|
+
|| normalized === 'error'
|
|
444
|
+
|| normalized === 'cancelled'
|
|
445
|
+
|| normalized === 'canceled';
|
|
446
|
+
}
|
|
447
|
+
function notifyTaskFinished(task) {
|
|
448
|
+
if (typeof window === 'undefined' || !('Notification' in window))
|
|
449
|
+
return;
|
|
450
|
+
const title = taskTitleText(task) || 'Task finished';
|
|
451
|
+
const body = `${capitalize(task.status)}.`;
|
|
452
|
+
const showNotification = () => {
|
|
453
|
+
try {
|
|
454
|
+
new Notification(title, { body, tag: `exoscient-task-${task.id}` });
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// Some browsers expose Notification but still reject construction.
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
if (Notification.permission === 'granted') {
|
|
461
|
+
showNotification();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (Notification.permission === 'default') {
|
|
465
|
+
void Notification.requestPermission().then((permission) => {
|
|
466
|
+
if (permission === 'granted')
|
|
467
|
+
showNotification();
|
|
468
|
+
}).catch(() => { });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function taskTurnResult(task, turn, index) {
|
|
472
|
+
const split = splitAgentResult(taskTurnResultText(task, turn, index));
|
|
473
|
+
const turnTechnical = turn.technicalMessage?.trim();
|
|
474
|
+
if (turnTechnical) {
|
|
475
|
+
return {
|
|
476
|
+
owner: split.owner,
|
|
477
|
+
technical: turnTechnical,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (index === task.turns.length - 1 && task.technicalMessage) {
|
|
481
|
+
return {
|
|
482
|
+
owner: split.owner || task.ownerMessage || '',
|
|
483
|
+
technical: task.technicalMessage,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return split;
|
|
487
|
+
}
|
|
488
|
+
function taskTurnResultText(task, turn, index) {
|
|
489
|
+
if (turn.output || turn.error)
|
|
490
|
+
return turn.output || turn.error || '';
|
|
491
|
+
if (index === task.turns.length - 1 && (task.ownerMessage || task.technicalMessage))
|
|
492
|
+
return task.ownerMessage || task.technicalMessage || '';
|
|
493
|
+
return index === task.turns.length - 1 ? task.latestResult || '' : '';
|
|
494
|
+
}
|
|
495
|
+
function taskSteeringMessages(task, index) {
|
|
496
|
+
if (index !== task.turns.length - 1)
|
|
497
|
+
return [];
|
|
498
|
+
return (task.steeringMessages ?? []).filter((item) => item.message.trim().length > 0);
|
|
499
|
+
}
|
|
500
|
+
function collapseWhitespace(value) {
|
|
501
|
+
return value.trim().replace(/\s+/g, ' ');
|
|
502
|
+
}
|
|
503
|
+
function SteeringMessages({ messages }) {
|
|
504
|
+
if (messages.length === 0)
|
|
505
|
+
return null;
|
|
506
|
+
return (_jsx("section", { className: "turn-panel steering-panel", "aria-label": "Steering messages", children: messages.map((item, index) => (_jsxs("div", { className: "steering-message", children: [_jsx(Send, { "aria-hidden": "true" }), _jsxs("div", { children: [_jsxs("div", { className: "steering-message-title", children: [_jsx("span", { children: "Steering" }), item.at && _jsx("time", { children: formatDateTime(item.at) })] }), _jsx("p", { children: item.message })] })] }, `${item.at ?? 'steering'}-${index}`))) }));
|
|
507
|
+
}
|
|
508
|
+
function TurnTitle({ index, timestamp }) {
|
|
509
|
+
return (_jsxs("div", { className: "turn-title", children: [_jsxs("h3", { children: ["#", index + 1] }), _jsx("time", { children: formatDateTime(timestamp) })] }));
|
|
510
|
+
}
|
|
511
|
+
function formatAgentTitle(turn, now) {
|
|
512
|
+
const duration = formatTurnDuration(turn, now);
|
|
513
|
+
return duration ? `AI agent, ${duration}` : 'AI agent';
|
|
514
|
+
}
|
|
515
|
+
function taskStatusContent(task, platformStatus) {
|
|
516
|
+
if (isTaskQueued(task)) {
|
|
517
|
+
return _jsx("span", { className: `status-text ${statusToneClass(task.status)}`, children: "Queued" });
|
|
518
|
+
}
|
|
519
|
+
if (task.busy) {
|
|
520
|
+
return _jsx(WorkingIndicator, { label: platformStatus.available ? 'Working' : 'The platform is down. Wait until it is back up', animate: platformStatus.available });
|
|
521
|
+
}
|
|
522
|
+
return _jsx("span", { className: `status-text ${statusToneClass(task.status)}`, children: taskStatusLabel(task.status) });
|
|
523
|
+
}
|
|
524
|
+
function formatCompletionTime(turn) {
|
|
525
|
+
const completedAt = turn.completedAt ?? turn.trace?.completedAt;
|
|
526
|
+
return completedAt ? formatDateTime(completedAt) : 'In progress';
|
|
527
|
+
}
|
|
528
|
+
function formatTurnDuration(turn, now = Date.now()) {
|
|
529
|
+
const completedAt = turn.completedAt ?? turn.trace?.completedAt;
|
|
530
|
+
if (!completedAt && !isTurnWorking(turn))
|
|
531
|
+
return '';
|
|
532
|
+
const startedAt = turn.startedAt ?? turn.trace?.startedAt ?? turn.createdAt;
|
|
533
|
+
const elapsedMs = (completedAt ? new Date(completedAt).getTime() : now) - new Date(startedAt).getTime();
|
|
534
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs < 0)
|
|
535
|
+
return '';
|
|
536
|
+
const totalSeconds = Math.max(1, Math.round(elapsedMs / 1000));
|
|
537
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
538
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
539
|
+
const seconds = totalSeconds % 60;
|
|
540
|
+
if (hours > 0)
|
|
541
|
+
return `${hours}h ${minutes}m`;
|
|
542
|
+
if (minutes > 0)
|
|
543
|
+
return `${minutes}m ${seconds}s`;
|
|
544
|
+
return `${seconds}s`;
|
|
545
|
+
}
|
|
546
|
+
export function formatDateTime(value) {
|
|
547
|
+
if (!value)
|
|
548
|
+
return '';
|
|
549
|
+
const date = new Date(value);
|
|
550
|
+
return Number.isFinite(date.getTime()) ? date.toLocaleString() : '';
|
|
551
|
+
}
|
|
552
|
+
function isTurnQueued(turn) {
|
|
553
|
+
return turn.status.trim().toLocaleLowerCase() === 'queued';
|
|
554
|
+
}
|
|
555
|
+
function isTurnWorking(turn) {
|
|
556
|
+
const normalized = turn.status.trim().toLocaleLowerCase();
|
|
557
|
+
return normalized === 'running' || normalized === 'working' || isTurnQueued(turn);
|
|
558
|
+
}
|
|
559
|
+
export function isTaskBusy(task) {
|
|
560
|
+
const normalized = task.status.trim().toLocaleLowerCase();
|
|
561
|
+
return task.busy || normalized === 'running' || normalized === 'working' || isTaskQueued(task);
|
|
562
|
+
}
|
|
563
|
+
export function isTaskQueued(task) {
|
|
564
|
+
return task.status.trim().toLocaleLowerCase() === 'queued';
|
|
565
|
+
}
|
|
566
|
+
export function useElapsedNow(active) {
|
|
567
|
+
const [now, setNow] = useState(() => Date.now());
|
|
568
|
+
useEffect(() => {
|
|
569
|
+
if (!active)
|
|
570
|
+
return;
|
|
571
|
+
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
|
572
|
+
return () => window.clearInterval(timer);
|
|
573
|
+
}, [active]);
|
|
574
|
+
return now;
|
|
575
|
+
}
|
|
576
|
+
export function capitalize(value) {
|
|
577
|
+
return value ? `${value.charAt(0).toLocaleUpperCase()}${value.slice(1)}` : value;
|
|
578
|
+
}
|
|
579
|
+
export function taskStatusLabel(status) {
|
|
580
|
+
const normalized = status.trim().toLocaleLowerCase();
|
|
581
|
+
if (normalized === 'waiting_for_user_continuation')
|
|
582
|
+
return 'Waiting for user continuation';
|
|
583
|
+
if (normalized === 'waiting_for_external_continuation')
|
|
584
|
+
return 'Waiting for external continuation';
|
|
585
|
+
if (normalized === 'closed')
|
|
586
|
+
return 'Closed';
|
|
587
|
+
if (normalized === 'working' || normalized === 'running' || normalized === 'in_progress')
|
|
588
|
+
return 'Working';
|
|
589
|
+
return capitalize(status.replace(/[_-]+/g, ' '));
|
|
590
|
+
}
|
|
591
|
+
export function statusToneClass(status) {
|
|
592
|
+
const normalized = status.trim().toLocaleLowerCase();
|
|
593
|
+
if (normalized === 'closed')
|
|
594
|
+
return 'status-success';
|
|
595
|
+
if (normalized === 'running' || normalized === 'working' || normalized === 'queued' || normalized === 'in_progress')
|
|
596
|
+
return 'status-active';
|
|
597
|
+
if (normalized === 'failed' || normalized === 'error' || normalized === 'cancelled' || normalized === 'canceled')
|
|
598
|
+
return 'status-danger';
|
|
599
|
+
return 'status-muted';
|
|
600
|
+
}
|
|
601
|
+
export function WorkingIndicator({ label = 'Working', animate = true }) {
|
|
602
|
+
return (_jsxs("span", { className: "working-indicator", role: "status", "aria-label": label, children: [_jsx("span", { children: label }), animate && (_jsxs("span", { className: "working-dots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }))] }));
|
|
603
|
+
}
|
|
604
|
+
export function InlineSpinner() {
|
|
605
|
+
return _jsx("span", { className: "inline-spinner", "aria-hidden": "true" });
|
|
606
|
+
}
|
|
607
|
+
export function LoadingState({ label }) {
|
|
608
|
+
return (_jsxs("div", { className: "loading-state", role: "status", "aria-live": "polite", children: [_jsx(InlineSpinner, {}), _jsx("span", { children: label })] }));
|
|
609
|
+
}
|
|
610
|
+
function TracePanel({ turn, turnIndex, loading, onCollapse, }) {
|
|
611
|
+
const isWorking = isTurnWorking(turn);
|
|
612
|
+
const [follow, setFollow] = useState(isWorking);
|
|
613
|
+
const [filters, setFilters] = useState(['narration']);
|
|
614
|
+
const scrollRef = useRef(null);
|
|
615
|
+
const events = turn.trace?.events ?? [];
|
|
616
|
+
const items = useMemo(() => buildTraceItems(events), [events]);
|
|
617
|
+
const contextPressure = useMemo(() => latestContextPressure(events), [events]);
|
|
618
|
+
const visibleItems = useMemo(() => items.filter((item) => traceItemMatchesFilter(item, filters)), [filters, items]);
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
if (follow && scrollRef.current) {
|
|
621
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
622
|
+
}
|
|
623
|
+
}, [follow, visibleItems.length, events.length]);
|
|
624
|
+
return (_jsxs("div", { className: "trace-shell", children: [_jsxs("div", { className: "trace-toolbar", children: [_jsxs("div", { className: "trace-toolbar-summary", children: [_jsxs("span", { children: ["Request #", turnIndex + 1, " trace - ", events.length, " events"] }), contextPressure && (_jsxs("div", { className: `context-pressure context-pressure-${contextPressure.tone}`, title: contextPressure.label, children: [_jsxs("span", { children: ["Context ", contextPressure.percent, "%"] }), _jsx("div", { className: "context-pressure-meter", "aria-hidden": "true", children: _jsx("div", { className: "context-pressure-fill", style: { width: `${Math.min(contextPressure.percent, 100)}%` } }) })] }))] }), _jsxs("div", { className: "trace-toolbar-actions", children: [isWorking && (_jsx("button", { className: `trace-toggle-button${follow ? ' active' : ''}`, type: "button", onClick: () => setFollow((current) => !current), "aria-pressed": follow, children: "Follow" })), _jsx("button", { className: "trace-collapse-button", type: "button", onClick: onCollapse, "aria-label": "Close trace", title: "Close trace", children: _jsx(X, { "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "trace-filter-bar", "aria-label": "Trace filters", children: traceFilterOptions.map((option) => (_jsx("button", { className: `trace-filter-button${filters.includes(option.value) ? ' active' : ''}`, type: "button", onClick: () => setFilters((current) => current.includes(option.value) ? current.filter((item) => item !== option.value) : [...current, option.value]), "aria-pressed": filters.includes(option.value), children: option.label }, option.value))) }), _jsxs("div", { className: "trace-list", ref: scrollRef, children: [loading && _jsx(LoadingState, { label: "Loading trace" }), !loading && items.length === 0 && _jsx("p", { className: "muted", children: "No trace events yet" }), !loading && items.length > 0 && visibleItems.length === 0 && _jsx("p", { className: "muted", children: "No events match this filter" }), visibleItems.map((item, index) => (_jsx(TraceItemView, { item: item }, `${item.kind}-${item.at}-${index}`)))] })] }));
|
|
625
|
+
}
|
|
626
|
+
const traceFilterOptions = [
|
|
627
|
+
{ value: 'narration', label: 'Assistant Remarks' },
|
|
628
|
+
{ value: 'coding', label: 'Coding' },
|
|
629
|
+
{ value: 'shell', label: 'Shell Commands' },
|
|
630
|
+
{ value: 'tokens', label: 'Token Usage' },
|
|
631
|
+
{ value: 'status', label: 'Status' },
|
|
632
|
+
{ value: 'errors', label: 'Errors' },
|
|
633
|
+
{ value: 'raw', label: 'Raw' },
|
|
634
|
+
];
|
|
635
|
+
function TraceItemView({ item }) {
|
|
636
|
+
const label = item.kind === 'output'
|
|
637
|
+
? item.stream ?? item.kind
|
|
638
|
+
: item.kind === 'raw'
|
|
639
|
+
? item.label
|
|
640
|
+
: 'title' in item
|
|
641
|
+
? item.title
|
|
642
|
+
: item.kind;
|
|
643
|
+
return (_jsxs("div", { className: `trace-event ${item.kind}`, children: [_jsxs("div", { className: "trace-event-header", children: [_jsx("span", { children: formatDateTime(item.at) || 'Unknown time' }), _jsx("strong", { children: label })] }), _jsx(TraceItemBody, { item: item })] }));
|
|
644
|
+
}
|
|
645
|
+
function TraceItemBody({ item }) {
|
|
646
|
+
if (item.kind === 'assistant' || item.kind === 'result') {
|
|
647
|
+
return _jsx(FormattedText, { text: item.text });
|
|
648
|
+
}
|
|
649
|
+
if (item.kind === 'command') {
|
|
650
|
+
return (_jsxs("div", { className: "trace-body", children: [item.status && _jsx("p", { className: "trace-meta", children: item.status }), item.command && _jsx("pre", { className: "trace-command", children: item.command }), item.output && _jsx("pre", { children: item.output }), item.raw && _jsx(RawDetails, { text: item.raw })] }));
|
|
651
|
+
}
|
|
652
|
+
if (item.kind === 'tool') {
|
|
653
|
+
return (_jsxs("div", { className: "trace-body", children: [item.status && _jsx("p", { className: "trace-meta", children: item.status }), item.input && _jsx("pre", { children: item.input }), item.output && _jsx("pre", { children: item.output }), item.raw && _jsx(RawDetails, { text: item.raw })] }));
|
|
654
|
+
}
|
|
655
|
+
if (item.kind === 'file') {
|
|
656
|
+
return (_jsxs("div", { className: "trace-body", children: [item.path && _jsx("p", { className: "trace-meta", children: item.path }), item.text && _jsx("pre", { children: item.text }), item.raw && _jsx(RawDetails, { text: item.raw })] }));
|
|
657
|
+
}
|
|
658
|
+
if (item.kind === 'status' || item.kind === 'error' || item.kind === 'tokens') {
|
|
659
|
+
return (_jsxs("div", { className: "trace-body", children: [_jsx(TraceTextOrJson, { text: item.text }), item.raw && _jsx(RawDetails, { text: item.raw })] }));
|
|
660
|
+
}
|
|
661
|
+
return _jsx(TraceTextOrJson, { text: item.text });
|
|
662
|
+
}
|
|
663
|
+
function RawDetails({ text }) {
|
|
664
|
+
return (_jsxs("details", { className: "raw-details", children: [_jsx("summary", { children: "Raw event" }), _jsx(TraceTextOrJson, { text: text })] }));
|
|
665
|
+
}
|
|
666
|
+
function TraceTextOrJson({ text }) {
|
|
667
|
+
const json = parseTraceJson(text);
|
|
668
|
+
if (json !== undefined) {
|
|
669
|
+
return _jsx(JsonTree, { value: json });
|
|
670
|
+
}
|
|
671
|
+
return _jsx("pre", { children: text });
|
|
672
|
+
}
|
|
673
|
+
function JsonTree({ value }) {
|
|
674
|
+
if (Array.isArray(value)) {
|
|
675
|
+
return (_jsx("ol", { className: "json-tree json-array", children: value.map((entry, index) => (_jsxs("li", { children: [_jsx("span", { className: "json-key", children: index }), _jsx(JsonTree, { value: entry })] }, index))) }));
|
|
676
|
+
}
|
|
677
|
+
if (isJsonObject(value)) {
|
|
678
|
+
const entries = Object.entries(value);
|
|
679
|
+
return (_jsx("ul", { className: "json-tree", children: entries.map(([key, entry]) => (_jsxs("li", { children: [_jsx("span", { className: "json-key", children: key }), _jsx(JsonTree, { value: entry })] }, key))) }));
|
|
680
|
+
}
|
|
681
|
+
return _jsx("span", { className: `json-value ${value === null ? 'null' : typeof value}`, children: formatJsonScalar(value) });
|
|
682
|
+
}
|
|
683
|
+
function parseTraceJson(text) {
|
|
684
|
+
const trimmed = text.trim();
|
|
685
|
+
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('[')))
|
|
686
|
+
return undefined;
|
|
687
|
+
try {
|
|
688
|
+
return normalizeTraceJson(JSON.parse(trimmed));
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function normalizeTraceJson(value, depth = 0) {
|
|
695
|
+
if (depth > 6)
|
|
696
|
+
return value;
|
|
697
|
+
if (typeof value === 'string') {
|
|
698
|
+
const trimmed = value.trim();
|
|
699
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
700
|
+
try {
|
|
701
|
+
return normalizeTraceJson(JSON.parse(trimmed), depth + 1);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return value;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
if (Array.isArray(value)) {
|
|
710
|
+
return value.map((entry) => normalizeTraceJson(entry, depth + 1));
|
|
711
|
+
}
|
|
712
|
+
if (isJsonObject(value)) {
|
|
713
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeTraceJson(entry, depth + 1)]));
|
|
714
|
+
}
|
|
715
|
+
return value;
|
|
716
|
+
}
|
|
717
|
+
function formatJsonScalar(value) {
|
|
718
|
+
if (value === null)
|
|
719
|
+
return 'null';
|
|
720
|
+
if (typeof value === 'string')
|
|
721
|
+
return value;
|
|
722
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
723
|
+
return String(value);
|
|
724
|
+
return '';
|
|
725
|
+
}
|
|
726
|
+
function isJsonObject(value) {
|
|
727
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
728
|
+
}
|
|
729
|
+
function FormattedText({ text }) {
|
|
730
|
+
const blocks = parseMarkdownBlocks(text);
|
|
731
|
+
return (_jsx("div", { className: "formatted-text", children: blocks.map((block, index) => renderMarkdownBlock(block, index)) }));
|
|
732
|
+
}
|
|
733
|
+
function parseMarkdownBlocks(text) {
|
|
734
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
735
|
+
const blocks = [];
|
|
736
|
+
let index = 0;
|
|
737
|
+
while (index < lines.length) {
|
|
738
|
+
const line = lines[index];
|
|
739
|
+
if (line.trim() === '') {
|
|
740
|
+
index += 1;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const fence = line.match(/^```([a-zA-Z0-9_-]+)?\s*$/);
|
|
744
|
+
if (fence) {
|
|
745
|
+
const codeLines = [];
|
|
746
|
+
index += 1;
|
|
747
|
+
while (index < lines.length && !/^```\s*$/.test(lines[index])) {
|
|
748
|
+
codeLines.push(lines[index]);
|
|
749
|
+
index += 1;
|
|
750
|
+
}
|
|
751
|
+
if (index < lines.length)
|
|
752
|
+
index += 1;
|
|
753
|
+
blocks.push({ type: 'code', text: codeLines.join('\n'), language: fence[1] });
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
757
|
+
if (heading) {
|
|
758
|
+
blocks.push({ type: 'heading', depth: heading[1].length, text: heading[2].trim() });
|
|
759
|
+
index += 1;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const unordered = line.match(/^\s*[-*]\s+(.+)$/);
|
|
763
|
+
if (unordered) {
|
|
764
|
+
const items = [];
|
|
765
|
+
while (index < lines.length) {
|
|
766
|
+
const item = lines[index].match(/^\s*[-*]\s+(.+)$/);
|
|
767
|
+
if (!item)
|
|
768
|
+
break;
|
|
769
|
+
items.push(item[1].trim());
|
|
770
|
+
index += 1;
|
|
771
|
+
}
|
|
772
|
+
blocks.push({ type: 'unordered-list', items });
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
|
776
|
+
if (ordered) {
|
|
777
|
+
const items = [];
|
|
778
|
+
while (index < lines.length) {
|
|
779
|
+
const item = lines[index].match(/^\s*\d+[.)]\s+(.+)$/);
|
|
780
|
+
if (!item)
|
|
781
|
+
break;
|
|
782
|
+
items.push(item[1].trim());
|
|
783
|
+
index += 1;
|
|
784
|
+
}
|
|
785
|
+
blocks.push({ type: 'ordered-list', items });
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const paragraphLines = [];
|
|
789
|
+
while (index < lines.length) {
|
|
790
|
+
const current = lines[index];
|
|
791
|
+
if (current.trim() === ''
|
|
792
|
+
|| /^```([a-zA-Z0-9_-]+)?\s*$/.test(current)
|
|
793
|
+
|| /^(#{1,6})\s+(.+)$/.test(current)
|
|
794
|
+
|| /^\s*[-*]\s+(.+)$/.test(current)
|
|
795
|
+
|| /^\s*\d+[.)]\s+(.+)$/.test(current)) {
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
paragraphLines.push(current);
|
|
799
|
+
index += 1;
|
|
800
|
+
}
|
|
801
|
+
blocks.push({ type: 'paragraph', text: paragraphLines.join('\n').trimEnd() });
|
|
802
|
+
}
|
|
803
|
+
return blocks.length === 0 ? [{ type: 'paragraph', text }] : blocks;
|
|
804
|
+
}
|
|
805
|
+
function renderMarkdownBlock(block, index) {
|
|
806
|
+
switch (block.type) {
|
|
807
|
+
case 'code':
|
|
808
|
+
return _jsx("pre", { className: "code-block", "data-language": block.language, children: block.text }, index);
|
|
809
|
+
case 'heading':
|
|
810
|
+
return _jsx(MarkdownHeading, { depth: block.depth, text: block.text }, index);
|
|
811
|
+
case 'unordered-list':
|
|
812
|
+
return _jsx("ul", { children: block.items.map((item, itemIndex) => _jsx("li", { children: renderInlineMarkdown(item, `u-${index}-${itemIndex}`) }, itemIndex)) }, index);
|
|
813
|
+
case 'ordered-list':
|
|
814
|
+
return _jsx("ol", { children: block.items.map((item, itemIndex) => _jsx("li", { children: renderInlineMarkdown(item, `o-${index}-${itemIndex}`) }, itemIndex)) }, index);
|
|
815
|
+
case 'paragraph':
|
|
816
|
+
return _jsx("p", { children: renderInlineMarkdown(block.text, `p-${index}`) }, index);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function MarkdownHeading({ depth, text }) {
|
|
820
|
+
const children = renderInlineMarkdown(text, `h-${depth}-${text}`);
|
|
821
|
+
if (depth <= 2)
|
|
822
|
+
return _jsx("h4", { children: children });
|
|
823
|
+
if (depth === 3)
|
|
824
|
+
return _jsx("h5", { children: children });
|
|
825
|
+
return _jsx("h6", { children: children });
|
|
826
|
+
}
|
|
827
|
+
function renderInlineMarkdown(text, keyPrefix) {
|
|
828
|
+
const nodes = [];
|
|
829
|
+
let index = 0;
|
|
830
|
+
let nodeIndex = 0;
|
|
831
|
+
const pushText = (value) => {
|
|
832
|
+
if (value)
|
|
833
|
+
nodes.push(value);
|
|
834
|
+
};
|
|
835
|
+
while (index < text.length) {
|
|
836
|
+
if (text[index] === '`') {
|
|
837
|
+
const end = text.indexOf('`', index + 1);
|
|
838
|
+
if (end > index + 1) {
|
|
839
|
+
nodes.push(_jsx("code", { children: text.slice(index + 1, end) }, `${keyPrefix}-code-${nodeIndex++}`));
|
|
840
|
+
index = end + 1;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (text.startsWith('**', index)) {
|
|
845
|
+
const end = text.indexOf('**', index + 2);
|
|
846
|
+
if (end > index + 2) {
|
|
847
|
+
nodes.push(_jsx("strong", { children: renderInlineMarkdown(text.slice(index + 2, end), `${keyPrefix}-strong-${nodeIndex}`) }, `${keyPrefix}-strong-${nodeIndex++}`));
|
|
848
|
+
index = end + 2;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (text[index] === '*') {
|
|
853
|
+
const end = text.indexOf('*', index + 1);
|
|
854
|
+
if (end > index + 1) {
|
|
855
|
+
nodes.push(_jsx("em", { children: renderInlineMarkdown(text.slice(index + 1, end), `${keyPrefix}-em-${nodeIndex}`) }, `${keyPrefix}-em-${nodeIndex++}`));
|
|
856
|
+
index = end + 1;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (text[index] === '[') {
|
|
861
|
+
const textEnd = text.indexOf(']', index + 1);
|
|
862
|
+
const hrefStart = textEnd >= 0 ? text.indexOf('(', textEnd) : -1;
|
|
863
|
+
const hrefEnd = hrefStart === textEnd + 1 ? text.indexOf(')', hrefStart + 1) : -1;
|
|
864
|
+
if (textEnd > index + 1 && hrefEnd > hrefStart + 1) {
|
|
865
|
+
const label = text.slice(index + 1, textEnd);
|
|
866
|
+
const href = text.slice(hrefStart + 1, hrefEnd).trim();
|
|
867
|
+
nodes.push(isSafeMarkdownHref(href)
|
|
868
|
+
? _jsx("a", { href: href, rel: "noreferrer", target: "_blank", children: renderInlineMarkdown(label, `${keyPrefix}-link-${nodeIndex}`) }, `${keyPrefix}-link-${nodeIndex++}`)
|
|
869
|
+
: label);
|
|
870
|
+
index = hrefEnd + 1;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const nextSpecial = nextMarkdownSpecialIndex(text, index + 1);
|
|
875
|
+
pushText(text.slice(index, nextSpecial));
|
|
876
|
+
index = nextSpecial;
|
|
877
|
+
}
|
|
878
|
+
return nodes;
|
|
879
|
+
}
|
|
880
|
+
function nextMarkdownSpecialIndex(text, start) {
|
|
881
|
+
const indexes = ['`', '*', '[']
|
|
882
|
+
.map((character) => text.indexOf(character, start))
|
|
883
|
+
.filter((value) => value >= 0);
|
|
884
|
+
return indexes.length > 0 ? Math.min(...indexes) : text.length;
|
|
885
|
+
}
|
|
886
|
+
function isSafeMarkdownHref(href) {
|
|
887
|
+
return /^(https?:|mailto:|tel:|\/|#)/i.test(href);
|
|
888
|
+
}
|
|
889
|
+
//# sourceMappingURL=TaskDetail.js.map
|