@kitlangton/motel 0.2.0 → 0.2.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/AGENTS.md +5 -0
- package/package.json +5 -3
- package/src/App.tsx +233 -59
- package/src/daemon.test.ts +144 -7
- package/src/daemon.ts +113 -8
- package/src/domain.test.ts +62 -0
- package/src/domain.ts +16 -0
- package/src/localServer.ts +111 -121
- package/src/mcp.ts +172 -0
- package/src/motelClient.ts +166 -14
- package/src/registry.ts +26 -23
- package/src/runtime.ts +8 -2
- package/src/server.ts +10 -9
- package/src/services/AsyncIngest.ts +52 -0
- package/src/services/TelemetryStore.ts +151 -26
- package/src/services/TraceQueryService.ts +3 -1
- package/src/services/ingestRpc.ts +41 -0
- package/src/services/telemetryWorker.ts +62 -0
- package/src/storybook/aiChatStory.tsx +243 -0
- package/src/storybook/fixtures/errorState.ts +44 -0
- package/src/storybook/fixtures/imagePaste.ts +34 -0
- package/src/storybook/fixtures/index.ts +62 -0
- package/src/storybook/fixtures/kitchenSink.ts +148 -0
- package/src/storybook/fixtures/rawPrompt.ts +15 -0
- package/src/storybook/fixtures/short.ts +27 -0
- package/src/storybook/fixtures/toolHeavy.ts +65 -0
- package/src/telemetry.test.ts +28 -0
- package/src/ui/AiChatView.tsx +292 -0
- package/src/ui/SpanContentView.tsx +181 -0
- package/src/ui/SpanDetail.tsx +98 -17
- package/src/ui/TraceDetailsPane.tsx +2 -1
- package/src/ui/Waterfall.tsx +38 -138
- package/src/ui/aiChatModel.test.ts +347 -0
- package/src/ui/aiChatModel.ts +736 -0
- package/src/ui/aiState.ts +71 -0
- package/src/ui/app/TraceWorkspace.tsx +291 -120
- package/src/ui/app/useAppLayout.ts +14 -11
- package/src/ui/app/useTraceScreenData.ts +173 -39
- package/src/ui/atoms.ts +131 -0
- package/src/ui/loaders.ts +120 -0
- package/src/ui/persistence.ts +41 -0
- package/src/ui/primitives.tsx +27 -13
- package/src/ui/state.ts +4 -199
- package/src/ui/useAttrFilterPicker.ts +63 -23
- package/src/ui/useKeyboardNav.ts +552 -364
- package/src/ui/waterfallModel.ts +130 -0
- package/src/ui/waterfallNav.test.ts +17 -1
- package/src/ui/waterfallNav.ts +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
3
|
+
import type { AiCallDetail } from "../domain.ts"
|
|
4
|
+
import { queryRuntime } from "../runtime.ts"
|
|
5
|
+
import { TraceQueryService } from "../services/TraceQueryService.ts"
|
|
6
|
+
import type { LoadStatus } from "./atoms.ts"
|
|
7
|
+
|
|
8
|
+
// AI chat view (full-screen when drilled into an `isAiSpan` span).
|
|
9
|
+
// ---------------------------------------------------------------------
|
|
10
|
+
// The main pane is a normal selectable list of semantic chunks (one row
|
|
11
|
+
// per chunk, with stable list scrolling). Opening a chunk shows its full
|
|
12
|
+
// content in a modal overlay that owns its own line scroll offset. This
|
|
13
|
+
// feels much closer to the rest of motel than the previous in-line
|
|
14
|
+
// expansion experiment.
|
|
15
|
+
// ---------------------------------------------------------------------
|
|
16
|
+
/** Chunk id currently selected in the list (null = first chunk). */
|
|
17
|
+
export const selectedChatChunkIdAtom = Atom.make<string | null>(null).pipe(Atom.keepAlive)
|
|
18
|
+
/** Chunk id whose detail modal is currently open. */
|
|
19
|
+
export const chatDetailChunkIdAtom = Atom.make<string | null>(null).pipe(Atom.keepAlive)
|
|
20
|
+
/** Line scroll offset inside the open detail modal. */
|
|
21
|
+
export const chatDetailScrollOffsetAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
22
|
+
|
|
23
|
+
export interface AiCallDetailState {
|
|
24
|
+
readonly status: LoadStatus
|
|
25
|
+
readonly spanId: string | null
|
|
26
|
+
readonly data: AiCallDetail | null
|
|
27
|
+
readonly error: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const initialAiCallDetailState: AiCallDetailState = {
|
|
31
|
+
status: "ready",
|
|
32
|
+
spanId: null,
|
|
33
|
+
data: null,
|
|
34
|
+
error: null,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const aiCallDetailStateAtom = Atom.make(initialAiCallDetailState).pipe(Atom.keepAlive)
|
|
38
|
+
|
|
39
|
+
export const loadAiCallDetail = (spanId: string) =>
|
|
40
|
+
queryRuntime.runPromise(Effect.flatMap(TraceQueryService.asEffect(), (service) => service.getAiCall(spanId)))
|
|
41
|
+
|
|
42
|
+
// AI call detail cache: the `ai.prompt` payload can easily be 50KB+ and
|
|
43
|
+
// we don't want to re-hit SQLite every time j/k moves the selection
|
|
44
|
+
// between adjacent AI spans. Cleared alongside the other per-refresh
|
|
45
|
+
// caches in `useTraceScreenData`.
|
|
46
|
+
const aiCallDetailCache = new Map<string, AiCallDetail | null>()
|
|
47
|
+
const aiCallDetailInflight = new Map<string, Promise<AiCallDetail | null>>()
|
|
48
|
+
|
|
49
|
+
export const getCachedAiCallDetail = (spanId: string): AiCallDetail | null | undefined =>
|
|
50
|
+
aiCallDetailCache.has(spanId) ? aiCallDetailCache.get(spanId) ?? null : undefined
|
|
51
|
+
|
|
52
|
+
export const ensureAiCallDetail = (spanId: string): Promise<AiCallDetail | null> => {
|
|
53
|
+
if (aiCallDetailCache.has(spanId)) return Promise.resolve(aiCallDetailCache.get(spanId) ?? null)
|
|
54
|
+
const existing = aiCallDetailInflight.get(spanId)
|
|
55
|
+
if (existing) return existing
|
|
56
|
+
const request = loadAiCallDetail(spanId)
|
|
57
|
+
.then((data) => {
|
|
58
|
+
aiCallDetailCache.set(spanId, data)
|
|
59
|
+
return data
|
|
60
|
+
})
|
|
61
|
+
.finally(() => {
|
|
62
|
+
aiCallDetailInflight.delete(spanId)
|
|
63
|
+
})
|
|
64
|
+
aiCallDetailInflight.set(spanId, request)
|
|
65
|
+
return request
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const invalidateAiCallDetailCache = () => {
|
|
69
|
+
aiCallDetailCache.clear()
|
|
70
|
+
aiCallDetailInflight.clear()
|
|
71
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isAiSpan, type LogItem, type TraceItem, type TraceSummaryItem } from "../../domain.ts"
|
|
2
|
+
import type { Chunk } from "../aiChatModel.ts"
|
|
3
|
+
import { AiChatView } from "../AiChatView.tsx"
|
|
2
4
|
import { formatShortDate, formatTimestamp } from "../format.ts"
|
|
3
5
|
import { AlignedHeaderLine, BlankRow, Divider, SeparatorColumn, TextLine } from "../primitives.tsx"
|
|
4
6
|
import { ServiceLogsView } from "../ServiceLogs.tsx"
|
|
7
|
+
import { SpanContentView } from "../SpanContentView.tsx"
|
|
5
8
|
import { SpanDetailPane } from "../SpanDetailPane.tsx"
|
|
6
|
-
import type { DetailView, LogState, ServiceLogState, TraceDetailState } from "../state.ts"
|
|
9
|
+
import type { AiCallDetailState, DetailView, LogState, ServiceLogState, TraceDetailState } from "../state.ts"
|
|
7
10
|
import { colors, SEPARATOR } from "../theme.ts"
|
|
8
11
|
import { TraceDetailsPane } from "../TraceDetailsPane.tsx"
|
|
9
12
|
import type { TraceListProps } from "../TraceList.tsx"
|
|
@@ -13,6 +16,191 @@ import type { AppLayout } from "./useAppLayout.ts"
|
|
|
13
16
|
const separatorJunctionChars = new Map<number, string>([[3, "├"]])
|
|
14
17
|
const separatorCrossChars = new Map<number, string>([[3, "┼"]])
|
|
15
18
|
|
|
19
|
+
interface SharedTraceDetailsProps {
|
|
20
|
+
readonly trace: TraceItem | null
|
|
21
|
+
readonly traceSummary: TraceSummaryItem | null
|
|
22
|
+
readonly traceStatus: TraceDetailState["status"]
|
|
23
|
+
readonly traceError: string | null
|
|
24
|
+
readonly traceLogsState: LogState
|
|
25
|
+
readonly selectedSpanIndex: number | null
|
|
26
|
+
readonly collapsedSpanIds: ReadonlySet<string>
|
|
27
|
+
readonly waterfallFilterMode: boolean
|
|
28
|
+
readonly waterfallFilterText: string
|
|
29
|
+
readonly onSelectSpan: (index: number) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TraceDetailsSceneProps extends SharedTraceDetailsProps {
|
|
33
|
+
readonly contentWidth: number
|
|
34
|
+
readonly bodyLines: number
|
|
35
|
+
readonly paneWidth: number
|
|
36
|
+
readonly focused: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TraceDetailsScene = ({
|
|
40
|
+
trace,
|
|
41
|
+
traceSummary,
|
|
42
|
+
traceStatus,
|
|
43
|
+
traceError,
|
|
44
|
+
traceLogsState,
|
|
45
|
+
contentWidth,
|
|
46
|
+
bodyLines,
|
|
47
|
+
paneWidth,
|
|
48
|
+
selectedSpanIndex,
|
|
49
|
+
collapsedSpanIds,
|
|
50
|
+
focused,
|
|
51
|
+
waterfallFilterMode,
|
|
52
|
+
waterfallFilterText,
|
|
53
|
+
onSelectSpan,
|
|
54
|
+
}: TraceDetailsSceneProps) => (
|
|
55
|
+
<TraceDetailsPane
|
|
56
|
+
trace={trace}
|
|
57
|
+
traceSummary={traceSummary}
|
|
58
|
+
traceStatus={traceStatus}
|
|
59
|
+
traceError={traceError}
|
|
60
|
+
traceLogsState={traceLogsState}
|
|
61
|
+
contentWidth={contentWidth}
|
|
62
|
+
bodyLines={bodyLines}
|
|
63
|
+
paneWidth={paneWidth}
|
|
64
|
+
selectedSpanIndex={selectedSpanIndex}
|
|
65
|
+
collapsedSpanIds={collapsedSpanIds}
|
|
66
|
+
focused={focused}
|
|
67
|
+
waterfallFilterMode={waterfallFilterMode}
|
|
68
|
+
waterfallFilterText={waterfallFilterText}
|
|
69
|
+
onSelectSpan={onSelectSpan}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
interface SpanDrillInSceneProps {
|
|
74
|
+
readonly aiDrillIn: boolean
|
|
75
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
76
|
+
readonly aiCallDetailState: AiCallDetailState
|
|
77
|
+
readonly aiChatChunks: readonly Chunk[]
|
|
78
|
+
readonly selectedChatChunkId: string | null
|
|
79
|
+
readonly onSelectChatChunk: (chunkId: string) => void
|
|
80
|
+
readonly chatDetailChunkId: string | null
|
|
81
|
+
readonly onOpenChatChunkDetail: (chunkId: string) => void
|
|
82
|
+
readonly onCloseChatChunkDetail: () => void
|
|
83
|
+
readonly chatDetailScrollOffset: number
|
|
84
|
+
readonly onSetChatDetailScrollOffset: (updater: (current: number) => number) => void
|
|
85
|
+
readonly contentWidth: number
|
|
86
|
+
readonly bodyLines: number
|
|
87
|
+
readonly paneWidth: number
|
|
88
|
+
readonly selectedAttrIndex: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SpanDrillInScene = ({
|
|
92
|
+
aiDrillIn,
|
|
93
|
+
selectedSpan,
|
|
94
|
+
aiCallDetailState,
|
|
95
|
+
aiChatChunks,
|
|
96
|
+
selectedChatChunkId,
|
|
97
|
+
onSelectChatChunk,
|
|
98
|
+
chatDetailChunkId,
|
|
99
|
+
onOpenChatChunkDetail,
|
|
100
|
+
onCloseChatChunkDetail,
|
|
101
|
+
chatDetailScrollOffset,
|
|
102
|
+
onSetChatDetailScrollOffset,
|
|
103
|
+
contentWidth,
|
|
104
|
+
bodyLines,
|
|
105
|
+
paneWidth,
|
|
106
|
+
selectedAttrIndex,
|
|
107
|
+
}: SpanDrillInSceneProps) => aiDrillIn ? (
|
|
108
|
+
<AiChatView
|
|
109
|
+
span={selectedSpan}
|
|
110
|
+
detailState={aiCallDetailState}
|
|
111
|
+
chunks={aiChatChunks}
|
|
112
|
+
selectedChunkId={selectedChatChunkId}
|
|
113
|
+
onSelectChunk={onSelectChatChunk}
|
|
114
|
+
detailChunkId={chatDetailChunkId}
|
|
115
|
+
onOpenDetail={onOpenChatChunkDetail}
|
|
116
|
+
onCloseDetail={onCloseChatChunkDetail}
|
|
117
|
+
detailScrollOffset={chatDetailScrollOffset}
|
|
118
|
+
onSetDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
119
|
+
contentWidth={contentWidth}
|
|
120
|
+
bodyLines={bodyLines}
|
|
121
|
+
paneWidth={paneWidth}
|
|
122
|
+
/>
|
|
123
|
+
) : (
|
|
124
|
+
<SpanContentView
|
|
125
|
+
span={selectedSpan}
|
|
126
|
+
contentWidth={contentWidth}
|
|
127
|
+
bodyLines={bodyLines}
|
|
128
|
+
paneWidth={paneWidth}
|
|
129
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
interface ServiceLogsSceneProps {
|
|
134
|
+
readonly selectedTraceService: string | null
|
|
135
|
+
readonly serviceLogState: ServiceLogState
|
|
136
|
+
readonly selectedServiceLogIndex: number
|
|
137
|
+
readonly setSelectedServiceLogIndex: (value: number | ((current: number) => number)) => void
|
|
138
|
+
readonly headerFooterWidth: number
|
|
139
|
+
readonly availableContentHeight: number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ServiceLogsScene = ({
|
|
143
|
+
selectedTraceService,
|
|
144
|
+
serviceLogState,
|
|
145
|
+
selectedServiceLogIndex,
|
|
146
|
+
setSelectedServiceLogIndex,
|
|
147
|
+
headerFooterWidth,
|
|
148
|
+
availableContentHeight,
|
|
149
|
+
}: ServiceLogsSceneProps) => (
|
|
150
|
+
<box flexGrow={1} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
151
|
+
<AlignedHeaderLine
|
|
152
|
+
left="SERVICE LOGS"
|
|
153
|
+
right={`${serviceLogState.data.length} logs${serviceLogState.fetchedAt ? `${SEPARATOR}${formatShortDate(serviceLogState.fetchedAt)} ${formatTimestamp(serviceLogState.fetchedAt)}` : ""}`}
|
|
154
|
+
width={headerFooterWidth}
|
|
155
|
+
rightFg={colors.count}
|
|
156
|
+
/>
|
|
157
|
+
<TextLine>
|
|
158
|
+
<span fg={colors.defaultService}>{selectedTraceService ?? "unknown"}</span>
|
|
159
|
+
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
160
|
+
<span fg={colors.count}>recent logs</span>
|
|
161
|
+
</TextLine>
|
|
162
|
+
<BlankRow />
|
|
163
|
+
<ServiceLogsView
|
|
164
|
+
serviceName={selectedTraceService}
|
|
165
|
+
logsState={serviceLogState}
|
|
166
|
+
selectedIndex={selectedServiceLogIndex}
|
|
167
|
+
onSelectLog={setSelectedServiceLogIndex}
|
|
168
|
+
contentWidth={headerFooterWidth}
|
|
169
|
+
bodyLines={Math.max(8, availableContentHeight - 3)}
|
|
170
|
+
/>
|
|
171
|
+
</box>
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
interface NarrowDrillInHeaderProps {
|
|
175
|
+
readonly contentWidth: number
|
|
176
|
+
readonly viewLevel: 0 | 1 | 2
|
|
177
|
+
readonly selectedTraceSummary: TraceSummaryItem | null
|
|
178
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const NarrowDrillInHeader = ({ contentWidth, viewLevel, selectedTraceSummary, selectedSpan }: NarrowDrillInHeaderProps) => (
|
|
182
|
+
<>
|
|
183
|
+
<box paddingLeft={1} paddingRight={1} height={1} flexDirection="column">
|
|
184
|
+
<TextLine>
|
|
185
|
+
<span fg={colors.muted}>TRACES</span>
|
|
186
|
+
{selectedTraceSummary ? (
|
|
187
|
+
<>
|
|
188
|
+
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
189
|
+
<span fg={viewLevel === 1 ? colors.accent : colors.muted}>{selectedTraceSummary.rootOperationName}</span>
|
|
190
|
+
</>
|
|
191
|
+
) : null}
|
|
192
|
+
{viewLevel === 2 && selectedSpan ? (
|
|
193
|
+
<>
|
|
194
|
+
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
195
|
+
<span fg={colors.accent}>{selectedSpan.operationName}</span>
|
|
196
|
+
</>
|
|
197
|
+
) : null}
|
|
198
|
+
</TextLine>
|
|
199
|
+
</box>
|
|
200
|
+
<Divider width={contentWidth} />
|
|
201
|
+
</>
|
|
202
|
+
)
|
|
203
|
+
|
|
16
204
|
interface TraceWorkspaceProps {
|
|
17
205
|
readonly layout: AppLayout
|
|
18
206
|
readonly detailView: DetailView
|
|
@@ -34,6 +222,16 @@ interface TraceWorkspaceProps {
|
|
|
34
222
|
readonly viewLevel: 0 | 1 | 2
|
|
35
223
|
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
36
224
|
readonly selectedSpanLogs: readonly LogItem[]
|
|
225
|
+
readonly selectedAttrIndex: number
|
|
226
|
+
readonly aiCallDetailState: AiCallDetailState
|
|
227
|
+
readonly aiChatChunks: readonly Chunk[]
|
|
228
|
+
readonly selectedChatChunkId: string | null
|
|
229
|
+
readonly onSelectChatChunk: (chunkId: string) => void
|
|
230
|
+
readonly chatDetailChunkId: string | null
|
|
231
|
+
readonly onOpenChatChunkDetail: (chunkId: string) => void
|
|
232
|
+
readonly onCloseChatChunkDetail: () => void
|
|
233
|
+
readonly chatDetailScrollOffset: number
|
|
234
|
+
readonly onSetChatDetailScrollOffset: (updater: (current: number) => number) => void
|
|
37
235
|
readonly selectSpan: (index: number) => void
|
|
38
236
|
}
|
|
39
237
|
|
|
@@ -58,8 +256,19 @@ export const TraceWorkspace = ({
|
|
|
58
256
|
viewLevel,
|
|
59
257
|
selectedSpan,
|
|
60
258
|
selectedSpanLogs,
|
|
259
|
+
selectedAttrIndex,
|
|
260
|
+
aiCallDetailState,
|
|
261
|
+
aiChatChunks,
|
|
262
|
+
selectedChatChunkId,
|
|
263
|
+
onSelectChatChunk,
|
|
264
|
+
chatDetailChunkId,
|
|
265
|
+
onOpenChatChunkDetail,
|
|
266
|
+
onCloseChatChunkDetail,
|
|
267
|
+
chatDetailScrollOffset,
|
|
268
|
+
onSetChatDetailScrollOffset,
|
|
61
269
|
selectSpan,
|
|
62
270
|
}: TraceWorkspaceProps) => {
|
|
271
|
+
const aiDrillIn = selectedSpan !== null && isAiSpan(selectedSpan.tags)
|
|
63
272
|
const {
|
|
64
273
|
contentWidth,
|
|
65
274
|
headerFooterWidth,
|
|
@@ -78,36 +287,34 @@ export const TraceWorkspace = ({
|
|
|
78
287
|
narrowTraceListBodyHeight,
|
|
79
288
|
availableContentHeight,
|
|
80
289
|
} = layout
|
|
290
|
+
const traceDetailsProps: SharedTraceDetailsProps = {
|
|
291
|
+
trace: selectedTrace,
|
|
292
|
+
traceSummary: selectedTraceSummary,
|
|
293
|
+
traceStatus: traceDetailState.status,
|
|
294
|
+
traceError: traceDetailState.error,
|
|
295
|
+
traceLogsState: logState,
|
|
296
|
+
selectedSpanIndex,
|
|
297
|
+
collapsedSpanIds,
|
|
298
|
+
waterfallFilterMode,
|
|
299
|
+
waterfallFilterText,
|
|
300
|
+
onSelectSpan: selectSpan,
|
|
301
|
+
}
|
|
302
|
+
const drillInContentWidth = Math.max(24, contentWidth - 2)
|
|
81
303
|
|
|
82
304
|
if (detailView === "service-logs") {
|
|
83
305
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<span fg={colors.defaultService}>{selectedTraceService ?? "unknown"}</span>
|
|
93
|
-
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
94
|
-
<span fg={colors.count}>recent logs</span>
|
|
95
|
-
</TextLine>
|
|
96
|
-
<BlankRow />
|
|
97
|
-
<ServiceLogsView
|
|
98
|
-
serviceName={selectedTraceService}
|
|
99
|
-
logsState={serviceLogState}
|
|
100
|
-
selectedIndex={selectedServiceLogIndex}
|
|
101
|
-
onSelectLog={setSelectedServiceLogIndex}
|
|
102
|
-
contentWidth={headerFooterWidth}
|
|
103
|
-
bodyLines={Math.max(8, availableContentHeight - 3)}
|
|
104
|
-
/>
|
|
105
|
-
</box>
|
|
306
|
+
<ServiceLogsScene
|
|
307
|
+
selectedTraceService={selectedTraceService}
|
|
308
|
+
serviceLogState={serviceLogState}
|
|
309
|
+
selectedServiceLogIndex={selectedServiceLogIndex}
|
|
310
|
+
setSelectedServiceLogIndex={setSelectedServiceLogIndex}
|
|
311
|
+
headerFooterWidth={headerFooterWidth}
|
|
312
|
+
availableContentHeight={availableContentHeight}
|
|
313
|
+
/>
|
|
106
314
|
)
|
|
107
315
|
}
|
|
108
316
|
|
|
109
317
|
if (isWideLayout) {
|
|
110
|
-
// L0: list (left) + trace preview (right). The two-pane zoom.
|
|
111
318
|
if (viewLevel === 0) {
|
|
112
319
|
return (
|
|
113
320
|
<box flexGrow={1} flexDirection="row">
|
|
@@ -124,83 +331,65 @@ export const TraceWorkspace = ({
|
|
|
124
331
|
</box>
|
|
125
332
|
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorJunctionChars} />
|
|
126
333
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
traceSummary={selectedTraceSummary}
|
|
130
|
-
traceStatus={traceDetailState.status}
|
|
131
|
-
traceError={traceDetailState.error}
|
|
132
|
-
traceLogsState={logState}
|
|
334
|
+
<TraceDetailsScene
|
|
335
|
+
{...traceDetailsProps}
|
|
133
336
|
contentWidth={rightContentWidth}
|
|
134
337
|
bodyLines={wideBodyLines}
|
|
135
338
|
paneWidth={rightPaneWidth}
|
|
136
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
137
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
138
339
|
focused={false}
|
|
139
|
-
waterfallFilterMode={waterfallFilterMode} waterfallFilterText={waterfallFilterText} onSelectSpan={selectSpan}
|
|
140
340
|
/>
|
|
141
341
|
</box>
|
|
142
342
|
</box>
|
|
143
343
|
)
|
|
144
344
|
}
|
|
145
345
|
|
|
146
|
-
// L1: the user pressed enter on a trace — hide the list entirely and
|
|
147
|
-
// let the waterfall take the full width. `leftPaneWidth` is already
|
|
148
|
-
// `contentWidth` in this case (see useAppLayout), so one pane fills
|
|
149
|
-
// the row.
|
|
150
346
|
if (viewLevel === 1) {
|
|
151
347
|
return (
|
|
152
348
|
<box flexGrow={1} flexDirection="row">
|
|
153
349
|
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
traceStatus={traceDetailState.status}
|
|
158
|
-
traceError={traceDetailState.error}
|
|
159
|
-
traceLogsState={logState}
|
|
160
|
-
contentWidth={Math.max(24, leftPaneWidth - sectionPadding * 2)}
|
|
350
|
+
<TraceDetailsScene
|
|
351
|
+
{...traceDetailsProps}
|
|
352
|
+
contentWidth={leftContentWidth}
|
|
161
353
|
bodyLines={wideBodyLines}
|
|
162
354
|
paneWidth={leftPaneWidth}
|
|
163
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
164
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
165
355
|
focused={true}
|
|
166
|
-
|
|
356
|
+
/>
|
|
357
|
+
</box>
|
|
358
|
+
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorCrossChars} />
|
|
359
|
+
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
360
|
+
<SpanDetailPane
|
|
361
|
+
span={selectedSpan}
|
|
362
|
+
trace={selectedTrace}
|
|
363
|
+
logs={selectedSpanLogs}
|
|
364
|
+
contentWidth={rightContentWidth}
|
|
365
|
+
bodyLines={wideBodyLines}
|
|
366
|
+
paneWidth={rightPaneWidth}
|
|
367
|
+
focused={false}
|
|
167
368
|
/>
|
|
168
369
|
</box>
|
|
169
370
|
</box>
|
|
170
371
|
)
|
|
171
372
|
}
|
|
172
373
|
|
|
173
|
-
// L2: waterfall-left + span detail-right. Still no list.
|
|
174
374
|
return (
|
|
175
|
-
<box flexGrow={1} flexDirection="
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
194
|
-
<SpanDetailPane
|
|
195
|
-
span={selectedSpan}
|
|
196
|
-
trace={selectedTrace}
|
|
197
|
-
logs={selectedSpanLogs}
|
|
198
|
-
contentWidth={rightContentWidth}
|
|
199
|
-
bodyLines={wideBodyLines}
|
|
200
|
-
paneWidth={rightPaneWidth}
|
|
201
|
-
focused={true}
|
|
202
|
-
/>
|
|
203
|
-
</box>
|
|
375
|
+
<box flexGrow={1} flexDirection="column">
|
|
376
|
+
<SpanDrillInScene
|
|
377
|
+
aiDrillIn={aiDrillIn}
|
|
378
|
+
selectedSpan={selectedSpan}
|
|
379
|
+
aiCallDetailState={aiCallDetailState}
|
|
380
|
+
aiChatChunks={aiChatChunks}
|
|
381
|
+
selectedChatChunkId={selectedChatChunkId}
|
|
382
|
+
onSelectChatChunk={onSelectChatChunk}
|
|
383
|
+
chatDetailChunkId={chatDetailChunkId}
|
|
384
|
+
onOpenChatChunkDetail={onOpenChatChunkDetail}
|
|
385
|
+
onCloseChatChunkDetail={onCloseChatChunkDetail}
|
|
386
|
+
chatDetailScrollOffset={chatDetailScrollOffset}
|
|
387
|
+
onSetChatDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
388
|
+
contentWidth={drillInContentWidth}
|
|
389
|
+
bodyLines={wideBodyLines}
|
|
390
|
+
paneWidth={contentWidth}
|
|
391
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
392
|
+
/>
|
|
204
393
|
</box>
|
|
205
394
|
)
|
|
206
395
|
}
|
|
@@ -218,19 +407,12 @@ export const TraceWorkspace = ({
|
|
|
218
407
|
padding={sectionPadding}
|
|
219
408
|
/>
|
|
220
409
|
<Divider width={contentWidth} />
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
traceSummary={selectedTraceSummary}
|
|
224
|
-
traceStatus={traceDetailState.status}
|
|
225
|
-
traceError={traceDetailState.error}
|
|
226
|
-
traceLogsState={logState}
|
|
410
|
+
<TraceDetailsScene
|
|
411
|
+
{...traceDetailsProps}
|
|
227
412
|
contentWidth={rightContentWidth}
|
|
228
413
|
bodyLines={narrowBodyLines}
|
|
229
414
|
paneWidth={contentWidth}
|
|
230
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
231
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
232
415
|
focused={false}
|
|
233
|
-
waterfallFilterMode={waterfallFilterMode} waterfallFilterText={waterfallFilterText} onSelectSpan={selectSpan}
|
|
234
416
|
/>
|
|
235
417
|
</>
|
|
236
418
|
)
|
|
@@ -238,48 +420,37 @@ export const TraceWorkspace = ({
|
|
|
238
420
|
|
|
239
421
|
return (
|
|
240
422
|
<>
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<span fg={viewLevel === 1 ? colors.accent : colors.muted}>{selectedTraceSummary.rootOperationName}</span>
|
|
248
|
-
</>
|
|
249
|
-
) : null}
|
|
250
|
-
{viewLevel === 2 && selectedSpan ? (
|
|
251
|
-
<>
|
|
252
|
-
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
253
|
-
<span fg={colors.accent}>{selectedSpan.operationName}</span>
|
|
254
|
-
</>
|
|
255
|
-
) : null}
|
|
256
|
-
</TextLine>
|
|
257
|
-
</box>
|
|
258
|
-
<Divider width={contentWidth} />
|
|
423
|
+
<NarrowDrillInHeader
|
|
424
|
+
contentWidth={contentWidth}
|
|
425
|
+
viewLevel={viewLevel}
|
|
426
|
+
selectedTraceSummary={selectedTraceSummary}
|
|
427
|
+
selectedSpan={selectedSpan}
|
|
428
|
+
/>
|
|
259
429
|
{viewLevel === 1 ? (
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
traceSummary={selectedTraceSummary}
|
|
263
|
-
traceStatus={traceDetailState.status}
|
|
264
|
-
traceError={traceDetailState.error}
|
|
265
|
-
traceLogsState={logState}
|
|
430
|
+
<TraceDetailsScene
|
|
431
|
+
{...traceDetailsProps}
|
|
266
432
|
contentWidth={rightContentWidth}
|
|
267
433
|
bodyLines={narrowFullBodyLines}
|
|
268
434
|
paneWidth={contentWidth}
|
|
269
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
270
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
271
435
|
focused={true}
|
|
272
|
-
waterfallFilterMode={waterfallFilterMode} waterfallFilterText={waterfallFilterText} onSelectSpan={selectSpan}
|
|
273
436
|
/>
|
|
274
437
|
) : (
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
438
|
+
<SpanDrillInScene
|
|
439
|
+
aiDrillIn={aiDrillIn}
|
|
440
|
+
selectedSpan={selectedSpan}
|
|
441
|
+
aiCallDetailState={aiCallDetailState}
|
|
442
|
+
aiChatChunks={aiChatChunks}
|
|
443
|
+
selectedChatChunkId={selectedChatChunkId}
|
|
444
|
+
onSelectChatChunk={onSelectChatChunk}
|
|
445
|
+
chatDetailChunkId={chatDetailChunkId}
|
|
446
|
+
onOpenChatChunkDetail={onOpenChatChunkDetail}
|
|
447
|
+
onCloseChatChunkDetail={onCloseChatChunkDetail}
|
|
448
|
+
chatDetailScrollOffset={chatDetailScrollOffset}
|
|
449
|
+
onSetChatDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
450
|
+
contentWidth={drillInContentWidth}
|
|
280
451
|
bodyLines={narrowFullBodyLines}
|
|
281
452
|
paneWidth={contentWidth}
|
|
282
|
-
|
|
453
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
283
454
|
/>
|
|
284
455
|
)}
|
|
285
456
|
</>
|
|
@@ -25,22 +25,25 @@ export const useAppLayout = ({ width, height, notice, detailView, selectedSpanIn
|
|
|
25
25
|
detailView === "span-detail" ? 2 :
|
|
26
26
|
selectedSpanIndex !== null ? 1 :
|
|
27
27
|
0
|
|
28
|
-
//
|
|
29
|
-
// trace list
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
28
|
+
// Split ratios for the two-pane body:
|
|
29
|
+
// L0 (trace list + trace preview): 40% / 60% — list narrow, preview wide
|
|
30
|
+
// L1 (waterfall + span preview): 60% / 40% — always-on preview,
|
|
31
|
+
// read-only (enter drills
|
|
32
|
+
// one level deeper)
|
|
33
|
+
// L2 (full-screen span content): single pane — the waterfall is
|
|
34
|
+
// hidden entirely; the
|
|
35
|
+
// workspace reads
|
|
36
|
+
// contentWidth directly
|
|
37
|
+
// and the split ratio
|
|
38
|
+
// is irrelevant.
|
|
39
|
+
const splitRatio = viewLevelForLayout === 1 ? 0.6 : 0.4
|
|
33
40
|
const listHidden = viewLevelForLayout >= 1
|
|
34
41
|
const leftPaneWidth = !isWideLayout
|
|
35
42
|
? contentWidth
|
|
36
|
-
:
|
|
37
|
-
? (viewLevelForLayout === 2 ? Math.max(40, Math.floor((contentWidth - splitGap) * splitRatio)) : contentWidth)
|
|
38
|
-
: Math.max(40, Math.floor((contentWidth - splitGap) * splitRatio))
|
|
43
|
+
: Math.max(40, Math.floor((contentWidth - splitGap) * splitRatio))
|
|
39
44
|
const rightPaneWidth = !isWideLayout
|
|
40
45
|
? contentWidth
|
|
41
|
-
:
|
|
42
|
-
? 0
|
|
43
|
-
: Math.max(28, contentWidth - leftPaneWidth - splitGap)
|
|
46
|
+
: Math.max(28, contentWidth - leftPaneWidth - splitGap)
|
|
44
47
|
// Left pane: paddingLeft (1) + scrollbar column (1). No right padding —
|
|
45
48
|
// the vertical pane divider handles visual separation from the right pane.
|
|
46
49
|
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
|