@alia.onl/sdk 15.7.0 → 19.0.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/CHANGELOG.md +255 -12
- package/README.md +241 -155
- package/package.json +17 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +3 -3
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +2 -6
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/origins.ts +15 -0
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/voice-worklets.ts +19 -0
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/worklets/voice-processors.js +152 -0
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/api-url.ts +0 -2
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuditEntry,
|
|
3
|
+
type Message,
|
|
4
|
+
type Source,
|
|
5
|
+
type ThoughtStep,
|
|
6
|
+
type TurnLifecycle,
|
|
7
|
+
auditText,
|
|
8
|
+
buildAuditTimeline,
|
|
9
|
+
buildSteps,
|
|
10
|
+
extractOutputs,
|
|
11
|
+
extractSources,
|
|
12
|
+
isLiveLifecycle,
|
|
13
|
+
mergeSources,
|
|
14
|
+
researchSourcesToSources,
|
|
15
|
+
toolCallStatus,
|
|
16
|
+
toolCallText,
|
|
17
|
+
toolLabel,
|
|
18
|
+
turnLifecycle,
|
|
19
|
+
} from '../../runtime/model';
|
|
20
|
+
import { restoreOpenerFocus } from '../../lib/focus-return';
|
|
21
|
+
import { useLabels } from '../../lib/labels';
|
|
22
|
+
import { getToolIcon } from '../../lib/tool-icons';
|
|
23
|
+
import { type ThoughtTab, useThoughtStore } from '../../runtime/thought-store';
|
|
24
|
+
import { FilesAndSources, type OutputActions } from './FilesAndSources';
|
|
25
|
+
import { ToolStep } from './ToolStep';
|
|
26
|
+
import {
|
|
27
|
+
AgentLogRow,
|
|
28
|
+
AgentLogShimmerText,
|
|
29
|
+
AgentLogWorkingRow,
|
|
30
|
+
useAgentLogMotion,
|
|
31
|
+
} from '@oxy.so/bloom/agent-log';
|
|
32
|
+
import { useAiChatShell } from '@oxy.so/bloom/ai-chat';
|
|
33
|
+
import { Button } from '@oxy.so/bloom/button';
|
|
34
|
+
import { EmptyState } from '@oxy.so/bloom/empty-state';
|
|
35
|
+
import { RiCheckboxCircleLine } from '@oxy.so/bloom/icons/RiCheckboxCircleLine';
|
|
36
|
+
import { RiCloseCircleLine } from '@oxy.so/bloom/icons/RiCloseCircleLine';
|
|
37
|
+
import { RiCloseLine } from '@oxy.so/bloom/icons/RiCloseLine';
|
|
38
|
+
import { RiForbidLine } from '@oxy.so/bloom/icons/RiForbidLine';
|
|
39
|
+
import { Loading } from '@oxy.so/bloom/loading';
|
|
40
|
+
import { Tabs, TabsTrigger } from '@oxy.so/bloom/tabs';
|
|
41
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
42
|
+
import { Muted, Text } from '@oxy.so/bloom/typography';
|
|
43
|
+
import * as WebBrowser from 'expo-web-browser';
|
|
44
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
45
|
+
import { Platform, ScrollView, View } from 'react-native';
|
|
46
|
+
|
|
47
|
+
/** One array for "nothing selected", so the memos below hold across renders. */
|
|
48
|
+
const NO_MESSAGES: Message[] = [];
|
|
49
|
+
|
|
50
|
+
function TabToggle({ value, onChange }: { value: ThoughtTab; onChange: (t: ThoughtTab) => void }) {
|
|
51
|
+
const { t } = useLabels();
|
|
52
|
+
const tabs: { key: ThoughtTab; label: string }[] = [
|
|
53
|
+
{ key: 'steps', label: t('thought.steps') },
|
|
54
|
+
{ key: 'sources', label: t('thought.sources') },
|
|
55
|
+
{ key: 'activity', label: t('thought.activity') },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Tabs variant="pill" value={value} onValueChange={(next) => onChange(next as ThoughtTab)}>
|
|
60
|
+
{tabs.map((tab) => (
|
|
61
|
+
<TabsTrigger key={tab.key} value={tab.key} label={tab.label} />
|
|
62
|
+
))}
|
|
63
|
+
</Tabs>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The panel's own wording for every step that is not a tool. Tool steps keep
|
|
69
|
+
* the label the registry gave them; these are lifecycle words, and they are
|
|
70
|
+
* translated here rather than in `buildSteps` so the pure function stays free
|
|
71
|
+
* of the locale.
|
|
72
|
+
*/
|
|
73
|
+
const STEP_LABEL_KEYS: Record<Exclude<ThoughtStep['type'], 'tool'>, string> = {
|
|
74
|
+
thinking: 'thought.thinking',
|
|
75
|
+
writing: 'thought.writing',
|
|
76
|
+
done: 'thought.done',
|
|
77
|
+
failed: 'thought.failed',
|
|
78
|
+
cancelled: 'thought.cancelled',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** A step that is a phase or an ending rather than a tool call. */
|
|
82
|
+
type PhaseStep = Omit<ThoughtStep, 'type'> & {
|
|
83
|
+
type: Exclude<ThoughtStep['type'], 'tool'>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** The glyph of an ending; a phase that is working has none — the log's own shimmer says so. */
|
|
87
|
+
function PhaseIcon({ type }: { type: PhaseStep['type'] }) {
|
|
88
|
+
const { colors } = useTheme();
|
|
89
|
+
if (type === 'done') return <RiCheckboxCircleLine size="sm" fill={colors.success} />;
|
|
90
|
+
if (type === 'failed') return <RiCloseCircleLine size="sm" fill={colors.error} />;
|
|
91
|
+
if (type === 'cancelled') return <RiForbidLine size="sm" fill={colors.textSecondary} />;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A phase or ending as one line of the log. Not a control: there is nothing to expand. */
|
|
96
|
+
function PhaseRow({ step, isActive }: { step: PhaseStep; isActive: boolean }) {
|
|
97
|
+
const { t } = useLabels();
|
|
98
|
+
const label = t(STEP_LABEL_KEYS[step.type]);
|
|
99
|
+
const ending = step.type === 'done' || step.type === 'failed';
|
|
100
|
+
return (
|
|
101
|
+
<View className="flex-row items-center gap-1.5 py-1" accessible accessibilityLabel={label}>
|
|
102
|
+
<PhaseIcon type={step.type} />
|
|
103
|
+
<Text variant={ending ? 'body-medium' : 'body-regular'}>
|
|
104
|
+
{isActive ? <AgentLogShimmerText>{label}</AgentLogShimmerText> : label}
|
|
105
|
+
</Text>
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The turn's steps as Bloom's agent log: each tool call a `ToolStep` whose
|
|
112
|
+
* input and output open in place, the phases and the ending as plain lines,
|
|
113
|
+
* and — while the turn is thinking or writing — the log's working row at the
|
|
114
|
+
* tail. Rows land as the runtime reports them, so the log is driven by real
|
|
115
|
+
* events rather than a ticker.
|
|
116
|
+
*
|
|
117
|
+
* A tool step spins on ITS OWN state while the turn runs — not on being last
|
|
118
|
+
* — so a finished tool after it cannot hide that it is still going, and a
|
|
119
|
+
* call that never returned in a turn that is over sits still and says so.
|
|
120
|
+
*/
|
|
121
|
+
function StepsTab({ steps, lifecycle }: { steps: ThoughtStep[]; lifecycle: TurnLifecycle }) {
|
|
122
|
+
const { t } = useLabels();
|
|
123
|
+
const { colors } = useTheme();
|
|
124
|
+
const reduce = useAgentLogMotion();
|
|
125
|
+
const live = isLiveLifecycle(lifecycle);
|
|
126
|
+
const [openRows, setOpenRows] = useState<Record<string, boolean>>({});
|
|
127
|
+
|
|
128
|
+
if (steps.length === 0) {
|
|
129
|
+
return <EmptyState variant="compact" description={t('thought.noSteps')} />;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const last = steps[steps.length - 1];
|
|
133
|
+
const tail = live && (last.type === 'thinking' || last.type === 'writing') ? last : null;
|
|
134
|
+
const rows = tail === null ? steps : steps.slice(0, -1);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<View>
|
|
138
|
+
<View role="list">
|
|
139
|
+
{rows.map((step, index) => {
|
|
140
|
+
const first = index === 0;
|
|
141
|
+
const isLastRow = index === rows.length - 1;
|
|
142
|
+
if (step.type !== 'tool') {
|
|
143
|
+
return (
|
|
144
|
+
<AgentLogRow
|
|
145
|
+
key={`${step.type}-${index}`}
|
|
146
|
+
first={first}
|
|
147
|
+
last={isLastRow}
|
|
148
|
+
reduce={reduce}
|
|
149
|
+
>
|
|
150
|
+
<PhaseRow
|
|
151
|
+
step={{ ...step, type: step.type }}
|
|
152
|
+
isActive={live && isLastRow && tail === null}
|
|
153
|
+
/>
|
|
154
|
+
</AgentLogRow>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const inv = step.invocation;
|
|
158
|
+
const key = inv?.toolCallId || `tool-${index}`;
|
|
159
|
+
const ToolIcon = getToolIcon(step.toolName || '');
|
|
160
|
+
const status =
|
|
161
|
+
inv === undefined ? (live ? 'running' : 'done') : toolCallStatus(inv, live);
|
|
162
|
+
return (
|
|
163
|
+
<AgentLogRow key={key} first={first} last={isLastRow} reduce={reduce}>
|
|
164
|
+
<ToolStep
|
|
165
|
+
title={step.toolName === undefined ? step.label : toolLabel(step.toolName, t)}
|
|
166
|
+
status={status}
|
|
167
|
+
icon={
|
|
168
|
+
<ToolIcon
|
|
169
|
+
width={14}
|
|
170
|
+
height={14}
|
|
171
|
+
fill={status === 'error' ? colors.error : colors.text}
|
|
172
|
+
/>
|
|
173
|
+
}
|
|
174
|
+
input={inv === undefined ? '' : toolCallText(inv.args)}
|
|
175
|
+
output={inv === undefined ? '' : toolCallText(inv.result)}
|
|
176
|
+
sources={step.sources}
|
|
177
|
+
expanded={openRows[key] === true}
|
|
178
|
+
onToggle={() => setOpenRows((open) => ({ ...open, [key]: open[key] !== true }))}
|
|
179
|
+
/>
|
|
180
|
+
</AgentLogRow>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</View>
|
|
184
|
+
{tail === null ? null : (
|
|
185
|
+
<AgentLogWorkingRow
|
|
186
|
+
label={t(STEP_LABEL_KEYS[tail.type as PhaseStep['type']])}
|
|
187
|
+
reduce={reduce}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The conversation's audit trail as one agent log, oldest first. */
|
|
195
|
+
function ActivityTab({ entries }: { entries: AuditEntry[] }) {
|
|
196
|
+
const { t } = useLabels();
|
|
197
|
+
const { colors } = useTheme();
|
|
198
|
+
const reduce = useAgentLogMotion();
|
|
199
|
+
if (entries.length === 0) {
|
|
200
|
+
return <EmptyState variant="compact" description={t('thought.noActivity')} />;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<View role="list">
|
|
205
|
+
{entries.map((entry, index) => (
|
|
206
|
+
<AgentLogRow
|
|
207
|
+
key={entry.id}
|
|
208
|
+
first={index === 0}
|
|
209
|
+
last={index === entries.length - 1}
|
|
210
|
+
reduce={reduce}
|
|
211
|
+
>
|
|
212
|
+
<View className="py-1">
|
|
213
|
+
<View className="flex-row items-center gap-1.5">
|
|
214
|
+
{entry.status === 'interrupted' ? (
|
|
215
|
+
<RiForbidLine size="xs" fill={colors.textSecondary} />
|
|
216
|
+
) : null}
|
|
217
|
+
<Text variant="body-regular" numberOfLines={1}>
|
|
218
|
+
{entry.status === 'in_progress' ? (
|
|
219
|
+
<AgentLogShimmerText>{auditText(entry.label, t)}</AgentLogShimmerText>
|
|
220
|
+
) : (
|
|
221
|
+
auditText(entry.label, t)
|
|
222
|
+
)}
|
|
223
|
+
</Text>
|
|
224
|
+
</View>
|
|
225
|
+
{auditText(entry.description, t) ? (
|
|
226
|
+
<Muted numberOfLines={1}>{auditText(entry.description, t)}</Muted>
|
|
227
|
+
) : null}
|
|
228
|
+
</View>
|
|
229
|
+
</AgentLogRow>
|
|
230
|
+
))}
|
|
231
|
+
</View>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The panel has nothing to show for the selection, and the reason. Rendered
|
|
237
|
+
* in place of the tabs' content so the three tabs never each say "none" about
|
|
238
|
+
* a message whose data has simply not arrived.
|
|
239
|
+
*/
|
|
240
|
+
function SelectionState({ status }: { status: 'loading' | 'failed' | 'gone' }) {
|
|
241
|
+
const { t } = useLabels();
|
|
242
|
+
const key =
|
|
243
|
+
status === 'loading'
|
|
244
|
+
? 'thought.loading'
|
|
245
|
+
: status === 'failed'
|
|
246
|
+
? 'thought.loadFailed'
|
|
247
|
+
: 'thought.messageGone';
|
|
248
|
+
return (
|
|
249
|
+
<EmptyState
|
|
250
|
+
variant="compact"
|
|
251
|
+
illustration={status === 'loading' ? <Loading size="sm" iconSize={24} /> : undefined}
|
|
252
|
+
description={t(key)}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Open a source where the platform reads the web. */
|
|
258
|
+
function openSource(source: Source): void {
|
|
259
|
+
if (Platform.OS === 'web') {
|
|
260
|
+
window.open(source.url, '_blank', 'noopener,noreferrer');
|
|
261
|
+
} else {
|
|
262
|
+
void WebBrowser.openBrowserAsync(source.url);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface ThoughtPanelProps {
|
|
267
|
+
/**
|
|
268
|
+
* The panel was closed (its button): the selection is already cleared and focus is back where
|
|
269
|
+
* it came from; the host hides the place it put the panel in.
|
|
270
|
+
*/
|
|
271
|
+
onClose: () => void;
|
|
272
|
+
/** Where a generated file opens (the host's canvas). Omitted, outputs are listed and never pressable. */
|
|
273
|
+
outputActions?: OutputActions;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The execution panel: what the selected turn did, produced and read.
|
|
278
|
+
*
|
|
279
|
+
* The thread selects the turn (`useThoughtStore`) and the host decides where this panel goes
|
|
280
|
+
* (`AliaRuntimeHost.showThought`); closing hands focus back to whatever opened it.
|
|
281
|
+
*
|
|
282
|
+
* Everything shown comes from the selection's own scope and the lifecycle the
|
|
283
|
+
* runtime stamps (#542, #543): a message not yet loaded is "loading", a load
|
|
284
|
+
* that failed says so, a message cut out since is "gone", and none of them is
|
|
285
|
+
* ever "no steps".
|
|
286
|
+
*/
|
|
287
|
+
export function ThoughtPanel({ onClose, outputActions }: ThoughtPanelProps) {
|
|
288
|
+
const { colors } = useTheme();
|
|
289
|
+
const shell = useAiChatShell();
|
|
290
|
+
const { t } = useLabels();
|
|
291
|
+
const selection = useThoughtStore((s) => s.selection);
|
|
292
|
+
const setActiveTab = useThoughtStore((s) => s.setTab);
|
|
293
|
+
const closeSelection = useThoughtStore((s) => s.close);
|
|
294
|
+
const activeTab = selection?.tab ?? 'steps';
|
|
295
|
+
const thoughtMessageId = selection?.messageId;
|
|
296
|
+
const scope = selection?.scope ?? null;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The scope's messages are the conversation the selection was made in and
|
|
300
|
+
* nothing else — a screen showing another conversation never gets to write
|
|
301
|
+
* here (see `syncThoughtScope`), so a miss below is about THIS conversation:
|
|
302
|
+
* still loading, failed to load, or a message that has since been cut out
|
|
303
|
+
* of it by an edit or a regenerate.
|
|
304
|
+
*/
|
|
305
|
+
const messages = scope?.messages ?? NO_MESSAGES;
|
|
306
|
+
const message = useMemo(
|
|
307
|
+
() => messages.find((m) => m.id === thoughtMessageId),
|
|
308
|
+
[messages, thoughtMessageId],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const lifecycle = useMemo<TurnLifecycle>(() => {
|
|
312
|
+
if (!message) return 'completed';
|
|
313
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant');
|
|
314
|
+
return turnLifecycle(message, {
|
|
315
|
+
isLoading: scope?.isLoading ?? false,
|
|
316
|
+
isLastAssistant: lastAssistant?.id === message.id,
|
|
317
|
+
failedTurn: scope?.failedTurn ?? null,
|
|
318
|
+
});
|
|
319
|
+
}, [message, messages, scope?.isLoading, scope?.failedTurn]);
|
|
320
|
+
|
|
321
|
+
const steps = useMemo(
|
|
322
|
+
() => (message ? buildSteps(message, lifecycle) : []),
|
|
323
|
+
[message, lifecycle],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// A research answer's sources come from its persisted `deepResearch`
|
|
327
|
+
// invocation after a reload and from the live progress event before one;
|
|
328
|
+
// both are read so the tab is the same either way.
|
|
329
|
+
const sources = useMemo(
|
|
330
|
+
() =>
|
|
331
|
+
message
|
|
332
|
+
? mergeSources(
|
|
333
|
+
extractSources(message.toolInvocations),
|
|
334
|
+
researchSourcesToSources(message.researchProgress?.sources),
|
|
335
|
+
)
|
|
336
|
+
: [],
|
|
337
|
+
[message],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const outputs = useMemo(
|
|
341
|
+
() => (message ? extractOutputs(message.toolInvocations) : []),
|
|
342
|
+
[message],
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const auditEntries = useMemo(
|
|
346
|
+
() =>
|
|
347
|
+
buildAuditTimeline(messages, {
|
|
348
|
+
isLoading: scope?.isLoading ?? false,
|
|
349
|
+
failedTurn: scope?.failedTurn ?? null,
|
|
350
|
+
}),
|
|
351
|
+
[messages, scope?.isLoading, scope?.failedTurn],
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const close = useCallback(() => {
|
|
355
|
+
closeSelection();
|
|
356
|
+
restoreOpenerFocus();
|
|
357
|
+
onClose();
|
|
358
|
+
}, [closeSelection, onClose]);
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Which of the three things a missing message means. A message that IS
|
|
362
|
+
* here but whose conversation is still loading is shown as it is — the
|
|
363
|
+
* live turn is exactly that — so only a miss consults the status.
|
|
364
|
+
*/
|
|
365
|
+
const emptyState: 'loading' | 'failed' | 'gone' | null =
|
|
366
|
+
message !== undefined
|
|
367
|
+
? null
|
|
368
|
+
: scope === null || scope.status === 'loading'
|
|
369
|
+
? 'loading'
|
|
370
|
+
: scope.status === 'failed'
|
|
371
|
+
? 'failed'
|
|
372
|
+
: 'gone';
|
|
373
|
+
|
|
374
|
+
const content =
|
|
375
|
+
emptyState !== null ? (
|
|
376
|
+
<SelectionState status={emptyState} />
|
|
377
|
+
) : activeTab === 'steps' ? (
|
|
378
|
+
<StepsTab steps={steps} lifecycle={lifecycle} />
|
|
379
|
+
) : activeTab === 'sources' ? (
|
|
380
|
+
<FilesAndSources
|
|
381
|
+
outputs={outputs}
|
|
382
|
+
sources={sources}
|
|
383
|
+
outputActions={outputActions}
|
|
384
|
+
onOpenSource={openSource}
|
|
385
|
+
/>
|
|
386
|
+
) : (
|
|
387
|
+
<ActivityTab entries={auditEntries} />
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<View className="min-h-0 flex-1 gap-2.5 pt-2">
|
|
392
|
+
<View className="h-[30px] flex-row items-center justify-between">
|
|
393
|
+
<View className="min-w-0 shrink">
|
|
394
|
+
<TabToggle value={activeTab} onChange={setActiveTab} />
|
|
395
|
+
</View>
|
|
396
|
+
{!shell?.compact && (
|
|
397
|
+
<Button
|
|
398
|
+
appearance="plain"
|
|
399
|
+
tone="neutral"
|
|
400
|
+
size="xs"
|
|
401
|
+
accessibilityLabel={t('common.close')}
|
|
402
|
+
onPress={close}
|
|
403
|
+
icon={<RiCloseLine size="sm" fill={colors.textSecondary} />}
|
|
404
|
+
/>
|
|
405
|
+
)}
|
|
406
|
+
</View>
|
|
407
|
+
|
|
408
|
+
<ScrollView
|
|
409
|
+
className="flex-1 px-4"
|
|
410
|
+
contentContainerClassName="pb-4"
|
|
411
|
+
showsVerticalScrollIndicator={false}
|
|
412
|
+
>
|
|
413
|
+
{content}
|
|
414
|
+
</ScrollView>
|
|
415
|
+
</View>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useLabels } from '../../lib/labels';
|
|
2
|
+
import type { Source, ToolCallStatus } from '../../runtime/model';
|
|
3
|
+
import { Chip } from '@oxy.so/bloom/chip';
|
|
4
|
+
import { CodeBlock } from '@oxy.so/bloom/code';
|
|
5
|
+
import { RiArrowDownSLine } from '@oxy.so/bloom/icons/RiArrowDownSLine';
|
|
6
|
+
import { RiArrowRightSLine } from '@oxy.so/bloom/icons/RiArrowRightSLine';
|
|
7
|
+
import { RiGlobalLine } from '@oxy.so/bloom/icons/RiGlobalLine';
|
|
8
|
+
import { Item } from '@oxy.so/bloom/item';
|
|
9
|
+
import { Loading } from '@oxy.so/bloom/loading';
|
|
10
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
11
|
+
import { Muted } from '@oxy.so/bloom/typography';
|
|
12
|
+
import * as Clipboard from 'expo-clipboard';
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
import { ScrollView, View } from 'react-native';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One execution row: a tool call, its state, and — expanded — what went in
|
|
18
|
+
* and what came out.
|
|
19
|
+
*
|
|
20
|
+
* The row is Bloom's `Item` as a disclosure (`expanded`), the two blocks are
|
|
21
|
+
* Bloom's `CodeBlock`, and a search step's domains are `Chip`s. The expanded
|
|
22
|
+
* block is MOUNTED only while expanded, so nothing inside a collapsed row can
|
|
23
|
+
* hold focus.
|
|
24
|
+
*
|
|
25
|
+
* Status is read from the lifecycle, never from the row being last: a call
|
|
26
|
+
* that never returned in a turn that is over reads as interrupted, and a call
|
|
27
|
+
* whose result carries an error reads as an error, so an execution list never
|
|
28
|
+
* says "done" about work that did not finish.
|
|
29
|
+
*/
|
|
30
|
+
export interface ToolStepProps {
|
|
31
|
+
title: string;
|
|
32
|
+
/** The call's argument summary, printed muted under the title. */
|
|
33
|
+
description?: string;
|
|
34
|
+
status: ToolCallStatus;
|
|
35
|
+
/** The tool's icon, drawn in the leading slot while the call is not running. */
|
|
36
|
+
icon: ReactNode;
|
|
37
|
+
/** The call's arguments as text; empty hides the block. */
|
|
38
|
+
input: string;
|
|
39
|
+
/** The call's result as text; empty hides the block. */
|
|
40
|
+
output: string;
|
|
41
|
+
/** The web sources a search step returned, drawn as domain chips under the block. */
|
|
42
|
+
sources?: Source[];
|
|
43
|
+
expanded: boolean;
|
|
44
|
+
onToggle: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** How many domain chips a search step shows before it counts the rest. */
|
|
48
|
+
const CHIP_LIMIT = 3;
|
|
49
|
+
|
|
50
|
+
async function copy(code: string) {
|
|
51
|
+
await Clipboard.setStringAsync(code);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ToolStep({
|
|
55
|
+
title,
|
|
56
|
+
description,
|
|
57
|
+
status,
|
|
58
|
+
icon,
|
|
59
|
+
input,
|
|
60
|
+
output,
|
|
61
|
+
sources,
|
|
62
|
+
expanded,
|
|
63
|
+
onToggle,
|
|
64
|
+
}: ToolStepProps) {
|
|
65
|
+
const { t } = useLabels();
|
|
66
|
+
const { colors } = useTheme();
|
|
67
|
+
const chips = sources ? sources.slice(0, CHIP_LIMIT) : [];
|
|
68
|
+
const extra = sources ? Math.max(0, sources.length - CHIP_LIMIT) : 0;
|
|
69
|
+
const statusWord =
|
|
70
|
+
status === 'error'
|
|
71
|
+
? t('thought.failed')
|
|
72
|
+
: status === 'interrupted'
|
|
73
|
+
? t('thought.cancelled')
|
|
74
|
+
: null;
|
|
75
|
+
const Chevron = expanded ? RiArrowDownSLine : RiArrowRightSLine;
|
|
76
|
+
const labels = { copy: t('panels.code.copy'), copied: t('panels.code.copied') };
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View>
|
|
80
|
+
<Item
|
|
81
|
+
density="compact"
|
|
82
|
+
leading={status === 'running' ? <Loading size="sm" iconSize={14} /> : icon}
|
|
83
|
+
title={title}
|
|
84
|
+
subtitle={description}
|
|
85
|
+
destructive={status === 'error'}
|
|
86
|
+
trailing={
|
|
87
|
+
<View className="flex-row items-center gap-1.5">
|
|
88
|
+
{statusWord === null ? null : <Muted>{statusWord}</Muted>}
|
|
89
|
+
<Chevron size="sm" fill={colors.textSecondary} />
|
|
90
|
+
</View>
|
|
91
|
+
}
|
|
92
|
+
expanded={expanded}
|
|
93
|
+
onPress={onToggle}
|
|
94
|
+
accessibilityLabel={statusWord === null ? title : `${title}, ${statusWord}`}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{expanded ? (
|
|
98
|
+
<View className="gap-2 pb-2">
|
|
99
|
+
{input.length > 0 || output.length > 0 ? (
|
|
100
|
+
<ScrollView
|
|
101
|
+
className="max-h-[200px]"
|
|
102
|
+
contentContainerClassName="gap-2"
|
|
103
|
+
nestedScrollEnabled
|
|
104
|
+
>
|
|
105
|
+
{input.length > 0 ? (
|
|
106
|
+
<CodeBlock
|
|
107
|
+
code={input}
|
|
108
|
+
filename={t('thought.input')}
|
|
109
|
+
lineNumbers={false}
|
|
110
|
+
wrap
|
|
111
|
+
onCopy={copy}
|
|
112
|
+
labels={labels}
|
|
113
|
+
/>
|
|
114
|
+
) : null}
|
|
115
|
+
{output.length > 0 ? (
|
|
116
|
+
<CodeBlock
|
|
117
|
+
code={output}
|
|
118
|
+
filename={t('thought.output')}
|
|
119
|
+
lineNumbers={false}
|
|
120
|
+
wrap
|
|
121
|
+
onCopy={copy}
|
|
122
|
+
labels={labels}
|
|
123
|
+
/>
|
|
124
|
+
) : null}
|
|
125
|
+
</ScrollView>
|
|
126
|
+
) : null}
|
|
127
|
+
{chips.length > 0 ? (
|
|
128
|
+
<View className="flex-row flex-wrap gap-1.5">
|
|
129
|
+
{chips.map((source) => (
|
|
130
|
+
<Chip key={source.url} size="sm" leadingIcon={RiGlobalLine}>
|
|
131
|
+
{source.domain}
|
|
132
|
+
</Chip>
|
|
133
|
+
))}
|
|
134
|
+
{extra > 0 ? <Chip size="sm">{`+ ${extra}`}</Chip> : null}
|
|
135
|
+
</View>
|
|
136
|
+
) : null}
|
|
137
|
+
</View>
|
|
138
|
+
) : null}
|
|
139
|
+
</View>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { AgentActivityState, PlanItem } from '../../runtime/thread/agent-activity';
|
|
2
|
+
import { useLabels } from '../../lib/labels';
|
|
3
|
+
import {
|
|
4
|
+
Accordion,
|
|
5
|
+
AccordionContent,
|
|
6
|
+
AccordionItem,
|
|
7
|
+
AccordionTrigger,
|
|
8
|
+
} from '@oxy.so/bloom/accordion';
|
|
9
|
+
import { Admonition } from '@oxy.so/bloom/admonition';
|
|
10
|
+
import { Badge } from '@oxy.so/bloom/badge';
|
|
11
|
+
import { Card, CardBody, CardHeader } from '@oxy.so/bloom/card';
|
|
12
|
+
import { RiAlertLine } from '@oxy.so/bloom/icons/RiAlertLine';
|
|
13
|
+
import { RiCheckboxCircleLine } from '@oxy.so/bloom/icons/RiCheckboxCircleLine';
|
|
14
|
+
import { RiCloseCircleLine } from '@oxy.so/bloom/icons/RiCloseCircleLine';
|
|
15
|
+
import { Item } from '@oxy.so/bloom/item';
|
|
16
|
+
import { Muted, Text } from '@oxy.so/bloom/typography';
|
|
17
|
+
import React, { useState } from 'react';
|
|
18
|
+
import { View } from 'react-native';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* AgentResultCard — the summary an agent run leaves in the chat when it ends.
|
|
22
|
+
*
|
|
23
|
+
* A Bloom `Card`: the outcome as a `Badge` beside the heading, the run's
|
|
24
|
+
* numbers in one secondary line, an error as an `Admonition`, and the plan as
|
|
25
|
+
* an `Accordion` section.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface AgentResultCardProps {
|
|
29
|
+
activity: AgentActivityState;
|
|
30
|
+
agentName?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `42s`, `3m 5s`, `1h 2m` — how long the run took. */
|
|
34
|
+
export function formatRunDuration(startedAt: number | null, now: number): string {
|
|
35
|
+
if (!startedAt) return '--';
|
|
36
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
37
|
+
if (seconds < 60) return `${seconds}s`;
|
|
38
|
+
const minutes = Math.floor(seconds / 60);
|
|
39
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
40
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PLAN_ICON = {
|
|
44
|
+
completed: RiCheckboxCircleLine,
|
|
45
|
+
blocked: RiAlertLine,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
function PlanItemRow({ item }: { item: PlanItem }) {
|
|
49
|
+
const Icon = PLAN_ICON[item.status as keyof typeof PLAN_ICON] ?? RiCloseCircleLine;
|
|
50
|
+
return <Item density="compact" role="listitem" leading={<Icon size="sm" />} title={item.text} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const AgentResultCard = React.memo(function AgentResultCard({
|
|
54
|
+
activity,
|
|
55
|
+
agentName,
|
|
56
|
+
}: AgentResultCardProps) {
|
|
57
|
+
const { t } = useLabels();
|
|
58
|
+
const [open, setOpen] = useState<string[]>([]);
|
|
59
|
+
// Measured once, when the card first draws the finished run.
|
|
60
|
+
const [now] = useState(() => Date.now());
|
|
61
|
+
|
|
62
|
+
const { plan, isComplete, hasError, lastError, eventCount, startedAt } = activity;
|
|
63
|
+
const isSuccess = isComplete && !hasError;
|
|
64
|
+
|
|
65
|
+
const stats = [
|
|
66
|
+
formatRunDuration(startedAt, now),
|
|
67
|
+
t('chat.agentRun.steps', { count: String(eventCount) }),
|
|
68
|
+
plan
|
|
69
|
+
? t('chat.agentRun.planItems', { done: String(plan.completed), total: String(plan.total) })
|
|
70
|
+
: null,
|
|
71
|
+
activity.creditsCharged != null
|
|
72
|
+
? t('chat.agentRun.credits', { count: String(activity.creditsCharged) })
|
|
73
|
+
: null,
|
|
74
|
+
].filter((part): part is string => part !== null);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<View className="flex-row items-center gap-2">
|
|
80
|
+
<Badge
|
|
81
|
+
variant="subtle"
|
|
82
|
+
color={isSuccess ? 'success' : hasError ? 'error' : 'default'}
|
|
83
|
+
content={t(
|
|
84
|
+
isSuccess
|
|
85
|
+
? 'chat.agentRun.completed'
|
|
86
|
+
: hasError
|
|
87
|
+
? 'chat.agentRun.failed'
|
|
88
|
+
: 'chat.agentRun.unknown',
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
<View className="min-w-0 flex-1">
|
|
92
|
+
<Text variant="headline-semibold">
|
|
93
|
+
{t(isSuccess ? 'chat.agentRun.taskComplete' : 'chat.agentRun.taskFailed')}
|
|
94
|
+
</Text>
|
|
95
|
+
{agentName ? <Muted>{t('chat.agentRun.by', { name: agentName })}</Muted> : null}
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
</CardHeader>
|
|
99
|
+
<CardBody>
|
|
100
|
+
<View className="gap-3">
|
|
101
|
+
<Muted>{stats.join(' · ')}</Muted>
|
|
102
|
+
|
|
103
|
+
{hasError && lastError ? <Admonition type="error">{lastError}</Admonition> : null}
|
|
104
|
+
|
|
105
|
+
{plan && plan.items.length > 0 ? (
|
|
106
|
+
<Accordion
|
|
107
|
+
type="multiple"
|
|
108
|
+
value={open}
|
|
109
|
+
onValueChange={(next) => setOpen(Array.isArray(next) ? next : next ? [next] : [])}
|
|
110
|
+
>
|
|
111
|
+
<AccordionItem value="plan">
|
|
112
|
+
<AccordionTrigger>{t('chat.agentRun.planSummary')}</AccordionTrigger>
|
|
113
|
+
<AccordionContent>
|
|
114
|
+
{plan.items.map((item) => (
|
|
115
|
+
<PlanItemRow key={item.id} item={item} />
|
|
116
|
+
))}
|
|
117
|
+
</AccordionContent>
|
|
118
|
+
</AccordionItem>
|
|
119
|
+
</Accordion>
|
|
120
|
+
) : null}
|
|
121
|
+
</View>
|
|
122
|
+
</CardBody>
|
|
123
|
+
</Card>
|
|
124
|
+
);
|
|
125
|
+
});
|