@open-mercato/ui 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -0
- package/dist/ai/AiAssistantLauncher.js +8 -8
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +92 -93
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiIcon.js +17 -0
- package/dist/ai/AiIcon.js.map +7 -0
- package/dist/ai/LoopDisabledBanner.js +28 -0
- package/dist/ai/LoopDisabledBanner.js.map +7 -0
- package/dist/ai/LoopTracePanel.js +123 -0
- package/dist/ai/LoopTracePanel.js.map +7 -0
- package/dist/ai/ModelPicker.js.map +1 -1
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +1 -5
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +8 -15
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +1 -4
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +0 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/ai/useAiChat.js +18 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/CrudForm.js +24 -2
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +2 -2
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +102 -96
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/detail/CustomDataSection.js +22 -3
- package/dist/backend/detail/CustomDataSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/injection/ComponentOverrideProvider.js +1 -1
- package/dist/backend/injection/ComponentOverrideProvider.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +25 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/progress/useProgressPoll.js +58 -3
- package/dist/backend/progress/useProgressPoll.js.map +2 -2
- package/dist/backend/progress/useProgressSse.js +15 -2
- package/dist/backend/progress/useProgressSse.js.map +2 -2
- package/dist/backend/utils/bulkDelete.js +76 -0
- package/dist/backend/utils/bulkDelete.js.map +2 -2
- package/package.json +11 -6
- package/src/ai/AiAssistantLauncher.tsx +8 -8
- package/src/ai/AiChat.tsx +44 -38
- package/src/ai/AiIcon.tsx +12 -0
- package/src/ai/LoopDisabledBanner.tsx +38 -0
- package/src/ai/LoopTracePanel.tsx +184 -0
- package/src/ai/ModelPicker.tsx +1 -1
- package/src/ai/__tests__/AiChat.test.tsx +174 -5
- package/src/ai/index.ts +8 -0
- package/src/ai/parts/ConfirmationCard.tsx +1 -5
- package/src/ai/parts/FieldDiffCard.tsx +0 -3
- package/src/ai/parts/MutationResultCard.tsx +1 -4
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +5 -0
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +0 -2
- package/src/ai/useAiChat.ts +35 -5
- package/src/backend/CrudForm.tsx +30 -3
- package/src/backend/DataTable.tsx +2 -2
- package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +72 -0
- package/src/backend/__tests__/CustomDataSection.test.tsx +76 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +36 -0
- package/src/backend/__tests__/component-replacement.test.tsx +44 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +101 -91
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +19 -0
- package/src/backend/detail/CustomDataSection.tsx +26 -3
- package/src/backend/filters/AdvancedFilterBuilder.tsx +6 -6
- package/src/backend/injection/ComponentOverrideProvider.tsx +2 -1
- package/src/backend/inputs/ComboboxInput.tsx +29 -7
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +33 -2
- package/src/backend/progress/useProgressPoll.ts +68 -3
- package/src/backend/progress/useProgressSse.ts +16 -2
- package/src/backend/utils/__tests__/bulkDelete.test.ts +42 -0
- package/src/backend/utils/bulkDelete.ts +93 -0
package/src/ai/AiChat.tsx
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from './useAiChat'
|
|
50
50
|
import { useAiChatUpload } from './useAiChatUpload'
|
|
51
51
|
import { useAiShortcuts } from './useAiShortcuts'
|
|
52
|
+
import { LoopTracePanel, type LoopTracePanelTrace } from './LoopTracePanel'
|
|
52
53
|
|
|
53
54
|
// Cap inline previews so we do not blow past localStorage quota (~5MB on most
|
|
54
55
|
// browsers). Images larger than this still upload + send to the LLM as inline
|
|
@@ -95,7 +96,8 @@ function writeModelPickerValue(agentId: string, value: ModelPickerValue | null):
|
|
|
95
96
|
|
|
96
97
|
interface ModelsApiResponse {
|
|
97
98
|
agentId: string
|
|
98
|
-
|
|
99
|
+
allowRuntimeOverride: boolean
|
|
100
|
+
allowRuntimeModelOverride?: boolean
|
|
99
101
|
defaultProviderId: string | null
|
|
100
102
|
defaultModelId: string | null
|
|
101
103
|
defaultProviderName?: string | null
|
|
@@ -105,12 +107,13 @@ interface ModelsApiResponse {
|
|
|
105
107
|
|
|
106
108
|
function useAgentModels(agent: string): {
|
|
107
109
|
providers: ModelPickerProvider[]
|
|
110
|
+
allowRuntimeOverride: boolean
|
|
108
111
|
allowRuntimeModelOverride: boolean
|
|
109
112
|
defaultLabel: string | null
|
|
110
113
|
loaded: boolean
|
|
111
114
|
} {
|
|
112
115
|
const [providers, setProviders] = React.useState<ModelPickerProvider[]>([])
|
|
113
|
-
const [
|
|
116
|
+
const [allowRuntimeOverride, setAllowRuntimeOverride] = React.useState(false)
|
|
114
117
|
const [defaultLabel, setDefaultLabel] = React.useState<string | null>(null)
|
|
115
118
|
const [loaded, setLoaded] = React.useState(false)
|
|
116
119
|
|
|
@@ -123,7 +126,9 @@ function useAgentModels(agent: string): {
|
|
|
123
126
|
setLoaded(true)
|
|
124
127
|
return
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
const effectiveAllowRuntimeOverride =
|
|
130
|
+
result.result.allowRuntimeOverride ?? result.result.allowRuntimeModelOverride ?? false
|
|
131
|
+
setAllowRuntimeOverride(effectiveAllowRuntimeOverride)
|
|
127
132
|
setProviders(result.result.providers)
|
|
128
133
|
setDefaultLabel(
|
|
129
134
|
result.result.defaultProviderName && result.result.defaultModelName
|
|
@@ -136,19 +141,13 @@ function useAgentModels(agent: string): {
|
|
|
136
141
|
})
|
|
137
142
|
}, [agent])
|
|
138
143
|
|
|
139
|
-
return {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (const provider of providers) {
|
|
146
|
-
const model = provider.models[0]
|
|
147
|
-
if (model) {
|
|
148
|
-
return { providerId: provider.id, modelId: model.id }
|
|
149
|
-
}
|
|
144
|
+
return {
|
|
145
|
+
providers,
|
|
146
|
+
allowRuntimeOverride,
|
|
147
|
+
allowRuntimeModelOverride: allowRuntimeOverride,
|
|
148
|
+
defaultLabel,
|
|
149
|
+
loaded,
|
|
150
150
|
}
|
|
151
|
-
return null
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
function isModelPickerValueAvailable(
|
|
@@ -263,6 +262,13 @@ export interface AiChatProps {
|
|
|
263
262
|
* `debug` is falsy.
|
|
264
263
|
*/
|
|
265
264
|
debugPromptSections?: AiChatDebugPromptSection[]
|
|
265
|
+
/**
|
|
266
|
+
* Optional loop trace from the last completed turn. When present and
|
|
267
|
+
* `debug` is truthy, renders a `LoopTracePanel` inside the debug panel.
|
|
268
|
+
* Populated by the host from the dispatcher SSE `loop-finish` event
|
|
269
|
+
* (wired in l4.4). Ignored when `debug` is falsy.
|
|
270
|
+
*/
|
|
271
|
+
loopTrace?: LoopTracePanelTrace
|
|
266
272
|
/** Suggested prompts shown in the empty / welcome state. */
|
|
267
273
|
suggestions?: AiChatSuggestion[]
|
|
268
274
|
/** Context items shown as pills above the transcript (e.g. selected products). */
|
|
@@ -381,6 +387,10 @@ function ToolCallList({ toolCalls }: { toolCalls: AiChatToolCallSnapshot[] }) {
|
|
|
381
387
|
if (!toolCalls || toolCalls.length === 0) return null
|
|
382
388
|
return (
|
|
383
389
|
<div className="space-y-1" data-ai-chat-tool-calls="">
|
|
390
|
+
<div className="flex items-center justify-between px-0.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
391
|
+
<span>{t('ai_assistant.chat.agentTasksTitle', 'Agent tasks')}</span>
|
|
392
|
+
<span>{toolCalls.length}</span>
|
|
393
|
+
</div>
|
|
384
394
|
{toolCalls.map((call) => {
|
|
385
395
|
const isOpen = openId === call.id
|
|
386
396
|
const isError = call.state === 'error'
|
|
@@ -828,6 +838,7 @@ export function AiChat({
|
|
|
828
838
|
uiParts: uiPartsProp,
|
|
829
839
|
debugTools,
|
|
830
840
|
debugPromptSections,
|
|
841
|
+
loopTrace,
|
|
831
842
|
conversationId,
|
|
832
843
|
suggestions,
|
|
833
844
|
contextItems,
|
|
@@ -892,6 +903,7 @@ export function AiChat({
|
|
|
892
903
|
|
|
893
904
|
const {
|
|
894
905
|
providers: modelProviders,
|
|
906
|
+
allowRuntimeOverride,
|
|
895
907
|
allowRuntimeModelOverride,
|
|
896
908
|
defaultLabel: modelDefaultLabel,
|
|
897
909
|
loaded: modelProvidersLoaded,
|
|
@@ -902,15 +914,15 @@ export function AiChat({
|
|
|
902
914
|
)
|
|
903
915
|
|
|
904
916
|
const effectiveModelPickerValue = React.useMemo(() => {
|
|
905
|
-
if (!modelProvidersLoaded || !
|
|
917
|
+
if (!modelProvidersLoaded || !allowRuntimeOverride || modelProviders.length === 0) {
|
|
906
918
|
return null
|
|
907
919
|
}
|
|
908
920
|
if (modelPickerValue && isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
|
|
909
921
|
return modelPickerValue
|
|
910
922
|
}
|
|
911
|
-
return
|
|
923
|
+
return null
|
|
912
924
|
}, [
|
|
913
|
-
|
|
925
|
+
allowRuntimeOverride,
|
|
914
926
|
modelPickerValue,
|
|
915
927
|
modelProviders,
|
|
916
928
|
modelProvidersLoaded,
|
|
@@ -930,9 +942,8 @@ export function AiChat({
|
|
|
930
942
|
return
|
|
931
943
|
}
|
|
932
944
|
if (modelPickerValue && !isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
writeModelPickerValue(agent, fallback)
|
|
945
|
+
setModelPickerValue(null)
|
|
946
|
+
writeModelPickerValue(agent, null)
|
|
936
947
|
}
|
|
937
948
|
}, [
|
|
938
949
|
agent,
|
|
@@ -979,7 +990,9 @@ export function AiChat({
|
|
|
979
990
|
// (d) streaming, and the last visible event was a finished tool call
|
|
980
991
|
// — the model is reasoning about the result before emitting more
|
|
981
992
|
// text or kicking off the next tool
|
|
982
|
-
//
|
|
993
|
+
// Once visible text/reasoning exists and no tool is pending, hide the
|
|
994
|
+
// placeholder even if the network stream stays open for a tail event. That
|
|
995
|
+
// avoids showing "Thinking..." under an already usable answer.
|
|
983
996
|
const lastAssistant = React.useMemo(() => {
|
|
984
997
|
for (let index = chat.messages.length - 1; index >= 0; index -= 1) {
|
|
985
998
|
const candidate = chat.messages[index]
|
|
@@ -1014,29 +1027,16 @@ export function AiChat({
|
|
|
1014
1027
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1015
1028
|
}, [lastAssistant])
|
|
1016
1029
|
|
|
1017
|
-
const lastStreamUpdateRef = React.useRef<number>(Date.now())
|
|
1018
1030
|
const lastSnapshotRef = React.useRef<string>('')
|
|
1019
1031
|
const [, setStreamTick] = React.useState(0)
|
|
1020
1032
|
|
|
1021
1033
|
React.useEffect(() => {
|
|
1022
1034
|
if (assistantStreamSnapshot !== lastSnapshotRef.current) {
|
|
1023
1035
|
lastSnapshotRef.current = assistantStreamSnapshot
|
|
1024
|
-
lastStreamUpdateRef.current = Date.now()
|
|
1025
1036
|
setStreamTick((value) => value + 1)
|
|
1026
1037
|
}
|
|
1027
1038
|
}, [assistantStreamSnapshot])
|
|
1028
1039
|
|
|
1029
|
-
React.useEffect(() => {
|
|
1030
|
-
if (!isStreaming && !isSubmitting) return
|
|
1031
|
-
const interval = window.setInterval(() => {
|
|
1032
|
-
setStreamTick((value) => value + 1)
|
|
1033
|
-
}, 200)
|
|
1034
|
-
return () => window.clearInterval(interval)
|
|
1035
|
-
}, [isStreaming, isSubmitting])
|
|
1036
|
-
|
|
1037
|
-
const idleDuringStream =
|
|
1038
|
-
isStreaming && Date.now() - lastStreamUpdateRef.current >= 300
|
|
1039
|
-
|
|
1040
1040
|
const showThinkingIndicator =
|
|
1041
1041
|
isSubmitting ||
|
|
1042
1042
|
(isStreaming &&
|
|
@@ -1044,8 +1044,7 @@ export function AiChat({
|
|
|
1044
1044
|
!hasAnyVisibleSignal ||
|
|
1045
1045
|
hasPendingToolCall ||
|
|
1046
1046
|
// Tool just returned and the model hasn't started speaking yet.
|
|
1047
|
-
(hasCompletedToolCall && !trimmedContent)
|
|
1048
|
-
idleDuringStream
|
|
1047
|
+
(hasCompletedToolCall && !trimmedContent)
|
|
1049
1048
|
))
|
|
1050
1049
|
|
|
1051
1050
|
const activeRegistry = registry ?? defaultAiUiPartRegistry
|
|
@@ -1429,7 +1428,7 @@ export function AiChat({
|
|
|
1429
1428
|
>
|
|
1430
1429
|
<Paperclip className="size-4" aria-hidden />
|
|
1431
1430
|
</IconButton>
|
|
1432
|
-
{
|
|
1431
|
+
{allowRuntimeOverride && modelProviders.length > 0 ? (
|
|
1433
1432
|
<ModelPicker
|
|
1434
1433
|
agentId={agent}
|
|
1435
1434
|
value={effectiveModelPickerValue}
|
|
@@ -1499,6 +1498,7 @@ export function AiChat({
|
|
|
1499
1498
|
<AiChatDebugPanel
|
|
1500
1499
|
tools={debugTools}
|
|
1501
1500
|
promptSections={debugPromptSections}
|
|
1501
|
+
loopTrace={loopTrace ?? chat.lastLoopTrace ?? undefined}
|
|
1502
1502
|
lastRequestDebug={chat.lastRequestDebug}
|
|
1503
1503
|
lastResponseDebug={chat.lastResponseDebug}
|
|
1504
1504
|
status={chat.status}
|
|
@@ -1512,6 +1512,7 @@ export function AiChat({
|
|
|
1512
1512
|
interface DebugPanelProps {
|
|
1513
1513
|
tools?: AiChatDebugTool[]
|
|
1514
1514
|
promptSections?: AiChatDebugPromptSection[]
|
|
1515
|
+
loopTrace?: LoopTracePanelTrace
|
|
1515
1516
|
lastRequestDebug: { url: string; body: unknown } | null
|
|
1516
1517
|
lastResponseDebug: { status: number; text: string } | null
|
|
1517
1518
|
status: 'idle' | 'submitting' | 'streaming'
|
|
@@ -1521,6 +1522,7 @@ interface DebugPanelProps {
|
|
|
1521
1522
|
function AiChatDebugPanel({
|
|
1522
1523
|
tools,
|
|
1523
1524
|
promptSections,
|
|
1525
|
+
loopTrace,
|
|
1524
1526
|
lastRequestDebug,
|
|
1525
1527
|
lastResponseDebug,
|
|
1526
1528
|
status,
|
|
@@ -1536,6 +1538,10 @@ function AiChatDebugPanel({
|
|
|
1536
1538
|
{t('ai_assistant.chat.debug.panelTitle', 'Debug panel')}
|
|
1537
1539
|
</div>
|
|
1538
1540
|
|
|
1541
|
+
{loopTrace ? (
|
|
1542
|
+
<LoopTracePanel trace={loopTrace} />
|
|
1543
|
+
) : null}
|
|
1544
|
+
|
|
1539
1545
|
<details className="rounded border border-border bg-background" data-ai-chat-debug-section="tools" open>
|
|
1540
1546
|
<summary className="cursor-pointer px-2 py-1 font-semibold">
|
|
1541
1547
|
{t('ai_assistant.chat.debug.toolsSection', 'Resolved tools')}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Sparkles, type LucideProps } from 'lucide-react'
|
|
2
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
3
|
+
|
|
4
|
+
export function AiIcon({ className, ...props }: LucideProps) {
|
|
5
|
+
return (
|
|
6
|
+
<Sparkles
|
|
7
|
+
aria-hidden
|
|
8
|
+
{...props}
|
|
9
|
+
className={cn(className, 'text-brand-violet')}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { ShieldOff } from 'lucide-react'
|
|
5
|
+
import { Alert, AlertDescription, AlertTitle } from '../primitives/alert'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Banner displayed when the tenant kill switch (`loop_disabled = true`) is
|
|
10
|
+
* active for the current agent. Rendered on the playground page and on
|
|
11
|
+
* `<AiChat>` headers so operators are not blindsided (R11 mitigation, Phase 3
|
|
12
|
+
* of spec `2026-04-28-ai-agents-agentic-loop-controls`).
|
|
13
|
+
*
|
|
14
|
+
* The agent still completes turns but as single-step runs. The banner is
|
|
15
|
+
* informational only — operators must visit the Loop policy section of the
|
|
16
|
+
* AI assistant settings to re-enable the loop.
|
|
17
|
+
*/
|
|
18
|
+
export function LoopDisabledBanner({ agentId }: { agentId?: string }) {
|
|
19
|
+
const t = useT()
|
|
20
|
+
return (
|
|
21
|
+
<Alert
|
|
22
|
+
variant="warning"
|
|
23
|
+
icon={<ShieldOff aria-hidden="true" />}
|
|
24
|
+
data-ai-loop-disabled-banner
|
|
25
|
+
data-ai-loop-disabled-agent-id={agentId}
|
|
26
|
+
>
|
|
27
|
+
<AlertTitle>
|
|
28
|
+
{t('ai_assistant.loop.disabledBanner.title', 'Agent loop disabled by tenant policy')}
|
|
29
|
+
</AlertTitle>
|
|
30
|
+
<AlertDescription>
|
|
31
|
+
{t(
|
|
32
|
+
'ai_assistant.loop.disabledBanner.description',
|
|
33
|
+
'The agentic loop has been disabled for this agent by a tenant administrator. Each turn runs as a single model call. To re-enable the loop, update the Loop policy in AI assistant settings.',
|
|
34
|
+
)}
|
|
35
|
+
</AlertDescription>
|
|
36
|
+
</Alert>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { ChevronDown, ChevronRight, Wrench } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export interface LoopTracePanelStepRecord {
|
|
7
|
+
stepIndex: number
|
|
8
|
+
modelId: string
|
|
9
|
+
toolCalls: Array<{
|
|
10
|
+
toolName: string
|
|
11
|
+
args: unknown
|
|
12
|
+
result?: unknown
|
|
13
|
+
error?: { code: string; message: string }
|
|
14
|
+
repairAttempted: boolean
|
|
15
|
+
durationMs: number
|
|
16
|
+
}>
|
|
17
|
+
textDelta: string
|
|
18
|
+
usage: { inputTokens: number; outputTokens: number }
|
|
19
|
+
finishReason: 'stop' | 'tool-calls' | 'length' | 'content-filter' | 'error'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LoopTracePanelTrace {
|
|
23
|
+
agentId: string
|
|
24
|
+
turnId: string
|
|
25
|
+
steps: LoopTracePanelStepRecord[]
|
|
26
|
+
stopReason:
|
|
27
|
+
| 'step-count'
|
|
28
|
+
| 'has-tool-call'
|
|
29
|
+
| 'custom-stop'
|
|
30
|
+
| 'budget-tokens'
|
|
31
|
+
| 'budget-tool-calls'
|
|
32
|
+
| 'budget-wall-clock'
|
|
33
|
+
| 'tenant-disabled'
|
|
34
|
+
| 'finish-reason'
|
|
35
|
+
| 'abort'
|
|
36
|
+
totalDurationMs: number
|
|
37
|
+
totalUsage: { inputTokens: number; outputTokens: number }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type LoopStepRecord = LoopTracePanelStepRecord
|
|
41
|
+
type LoopTrace = LoopTracePanelTrace
|
|
42
|
+
|
|
43
|
+
function formatMs(ms: number): string {
|
|
44
|
+
if (ms < 1000) return `${ms}ms`
|
|
45
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function LoopStepRow({ step }: { step: LoopStepRecord }) {
|
|
49
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
50
|
+
const hasTools = step.toolCalls.length > 0
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<li className="border-b border-border last:border-0">
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-xs hover:bg-muted/40"
|
|
57
|
+
onClick={() => setExpanded((prev) => !prev)}
|
|
58
|
+
aria-expanded={expanded}
|
|
59
|
+
aria-label={`Step ${step.stepIndex}`}
|
|
60
|
+
>
|
|
61
|
+
{expanded ? (
|
|
62
|
+
<ChevronDown className="size-3 shrink-0 text-muted-foreground" aria-hidden />
|
|
63
|
+
) : (
|
|
64
|
+
<ChevronRight className="size-3 shrink-0 text-muted-foreground" aria-hidden />
|
|
65
|
+
)}
|
|
66
|
+
<span className="font-medium text-muted-foreground">Step {step.stepIndex}</span>
|
|
67
|
+
<span className="font-mono text-muted-foreground/60">{step.modelId}</span>
|
|
68
|
+
{hasTools ? (
|
|
69
|
+
<span className="ml-auto flex items-center gap-1 text-muted-foreground">
|
|
70
|
+
<Wrench className="size-3" aria-hidden />
|
|
71
|
+
{step.toolCalls.length}
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
74
|
+
<span className="ml-auto font-mono text-muted-foreground/60">{step.finishReason}</span>
|
|
75
|
+
<span className="font-mono text-muted-foreground/60">
|
|
76
|
+
{step.usage.inputTokens}↑ {step.usage.outputTokens}↓
|
|
77
|
+
</span>
|
|
78
|
+
</button>
|
|
79
|
+
|
|
80
|
+
{expanded ? (
|
|
81
|
+
<div className="bg-muted/20 px-4 pb-2 pt-1 text-xs">
|
|
82
|
+
{step.textDelta ? (
|
|
83
|
+
<div className="mb-2">
|
|
84
|
+
<div className="mb-0.5 font-semibold text-muted-foreground">Text</div>
|
|
85
|
+
<pre className="max-h-32 overflow-auto whitespace-pre-wrap rounded bg-background p-1.5 font-mono">
|
|
86
|
+
{step.textDelta}
|
|
87
|
+
</pre>
|
|
88
|
+
</div>
|
|
89
|
+
) : null}
|
|
90
|
+
{hasTools ? (
|
|
91
|
+
<div>
|
|
92
|
+
<div className="mb-0.5 font-semibold text-muted-foreground">Tool calls</div>
|
|
93
|
+
<ul className="flex flex-col gap-1">
|
|
94
|
+
{step.toolCalls.map((tc, index) => (
|
|
95
|
+
<li
|
|
96
|
+
key={index}
|
|
97
|
+
className="rounded border border-border bg-background p-1.5"
|
|
98
|
+
data-loop-trace-tool-call={tc.toolName}
|
|
99
|
+
>
|
|
100
|
+
<div className="flex items-center justify-between gap-2">
|
|
101
|
+
<span className="font-mono font-semibold">{tc.toolName}</span>
|
|
102
|
+
{tc.durationMs ? (
|
|
103
|
+
<span className="text-muted-foreground/60">{formatMs(tc.durationMs)}</span>
|
|
104
|
+
) : null}
|
|
105
|
+
{tc.error ? (
|
|
106
|
+
<span className="text-status-error-fg">error: {tc.error.code}</span>
|
|
107
|
+
) : null}
|
|
108
|
+
</div>
|
|
109
|
+
{tc.args !== undefined ? (
|
|
110
|
+
<pre className="mt-1 max-h-24 overflow-auto whitespace-pre-wrap font-mono text-xs text-muted-foreground">
|
|
111
|
+
{JSON.stringify(tc.args, null, 2)}
|
|
112
|
+
</pre>
|
|
113
|
+
) : null}
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
) : null}
|
|
121
|
+
</li>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface LoopTracePanelProps {
|
|
126
|
+
trace: LoopTracePanelTrace
|
|
127
|
+
className?: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Collapsible playground / debug panel that renders a `LoopTrace` per turn.
|
|
132
|
+
* Collapsed by default; each step row is independently expandable.
|
|
133
|
+
*
|
|
134
|
+
* Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
135
|
+
*/
|
|
136
|
+
export function LoopTracePanel({ trace, className }: LoopTracePanelProps) {
|
|
137
|
+
const [open, setOpen] = React.useState(false)
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<section
|
|
141
|
+
className={`rounded-md border border-border bg-background text-xs${className ? ` ${className}` : ''}`}
|
|
142
|
+
data-ai-loop-trace-panel={trace.agentId}
|
|
143
|
+
>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className="flex w-full items-center gap-2 px-3 py-2 hover:bg-muted/40"
|
|
147
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
148
|
+
aria-expanded={open}
|
|
149
|
+
aria-controls="loop-trace-steps"
|
|
150
|
+
>
|
|
151
|
+
{open ? (
|
|
152
|
+
<ChevronDown className="size-3 shrink-0 text-muted-foreground" aria-hidden />
|
|
153
|
+
) : (
|
|
154
|
+
<ChevronRight className="size-3 shrink-0 text-muted-foreground" aria-hidden />
|
|
155
|
+
)}
|
|
156
|
+
<span className="font-semibold text-muted-foreground">Loop trace</span>
|
|
157
|
+
<span className="ml-1 font-mono text-muted-foreground/60">
|
|
158
|
+
{trace.steps.length} step{trace.steps.length !== 1 ? 's' : ''}
|
|
159
|
+
</span>
|
|
160
|
+
<span
|
|
161
|
+
className="ml-auto font-mono text-muted-foreground/60"
|
|
162
|
+
data-loop-trace-stop-reason={trace.stopReason}
|
|
163
|
+
>
|
|
164
|
+
{trace.stopReason}
|
|
165
|
+
</span>
|
|
166
|
+
<span className="font-mono text-muted-foreground/60">{formatMs(trace.totalDurationMs)}</span>
|
|
167
|
+
<span className="font-mono text-muted-foreground/60">
|
|
168
|
+
{trace.totalUsage.inputTokens + trace.totalUsage.outputTokens} tok
|
|
169
|
+
</span>
|
|
170
|
+
</button>
|
|
171
|
+
|
|
172
|
+
{open ? (
|
|
173
|
+
<ul id="loop-trace-steps" className="border-t border-border" role="list">
|
|
174
|
+
{trace.steps.map((step) => (
|
|
175
|
+
<LoopStepRow key={step.stepIndex} step={step} />
|
|
176
|
+
))}
|
|
177
|
+
{trace.steps.length === 0 ? (
|
|
178
|
+
<li className="px-3 py-2 text-muted-foreground">No steps recorded.</li>
|
|
179
|
+
) : null}
|
|
180
|
+
</ul>
|
|
181
|
+
) : null}
|
|
182
|
+
</section>
|
|
183
|
+
)
|
|
184
|
+
}
|
package/src/ai/ModelPicker.tsx
CHANGED
|
@@ -47,7 +47,7 @@ export interface ModelPickerProps {
|
|
|
47
47
|
* - Does NOT expose a baseURL input (R6 mitigation: only provider+model selection from the
|
|
48
48
|
* curated catalog is exposed to end users).
|
|
49
49
|
* - localStorage persistence keyed by agentId is handled by the consumer (AiChat / 4b.2).
|
|
50
|
-
* - When
|
|
50
|
+
* - When allowRuntimeOverride is false the consumer should not render this component
|
|
51
51
|
* at all; this component itself does no gating.
|
|
52
52
|
*/
|
|
53
53
|
export function ModelPicker({
|