@kitlangton/motel 0.1.3 → 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 +11 -1
- package/package.json +5 -3
- package/src/App.tsx +239 -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 +62 -4
- package/src/httpApi.ts +4 -1
- package/src/localServer.ts +112 -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 +285 -27
- package/src/services/TraceQueryService.ts +4 -2
- 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 +61 -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 +35 -3
- package/src/ui/Waterfall.tsx +94 -167
- 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 +295 -120
- package/src/ui/app/useAppLayout.ts +14 -11
- package/src/ui/app/useTraceScreenData.ts +191 -35
- package/src/ui/atoms.ts +131 -0
- package/src/ui/filterParser.test.ts +56 -0
- package/src/ui/filterParser.ts +45 -0
- package/src/ui/loaders.ts +120 -0
- package/src/ui/persistence.ts +41 -0
- package/src/ui/primitives.tsx +47 -21
- package/src/ui/state.ts +4 -169
- package/src/ui/useAttrFilterPicker.ts +63 -23
- package/src/ui/useKeyboardNav.ts +576 -300
- package/src/ui/waterfallFilter.test.ts +84 -0
- package/src/ui/waterfallFilter.ts +59 -0
- package/src/ui/waterfallModel.ts +130 -0
- package/src/ui/waterfallNav.test.ts +17 -1
- package/src/ui/waterfallNav.ts +1 -1
- package/web/dist/assets/{index-DKinj-OE.js → index-DnyVo03x.js} +1 -1
- package/web/dist/index.html +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,11 +16,198 @@ 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
|
|
19
207
|
readonly filterMode: boolean
|
|
20
208
|
readonly filterText: string
|
|
209
|
+
readonly waterfallFilterMode: boolean
|
|
210
|
+
readonly waterfallFilterText: string
|
|
21
211
|
readonly traceListProps: TraceListProps
|
|
22
212
|
readonly selectedTraceService: string | null
|
|
23
213
|
readonly serviceLogState: ServiceLogState
|
|
@@ -32,6 +222,16 @@ interface TraceWorkspaceProps {
|
|
|
32
222
|
readonly viewLevel: 0 | 1 | 2
|
|
33
223
|
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
34
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
|
|
35
235
|
readonly selectSpan: (index: number) => void
|
|
36
236
|
}
|
|
37
237
|
|
|
@@ -40,6 +240,8 @@ export const TraceWorkspace = ({
|
|
|
40
240
|
detailView,
|
|
41
241
|
filterMode,
|
|
42
242
|
filterText,
|
|
243
|
+
waterfallFilterMode,
|
|
244
|
+
waterfallFilterText,
|
|
43
245
|
traceListProps,
|
|
44
246
|
selectedTraceService,
|
|
45
247
|
serviceLogState,
|
|
@@ -54,8 +256,19 @@ export const TraceWorkspace = ({
|
|
|
54
256
|
viewLevel,
|
|
55
257
|
selectedSpan,
|
|
56
258
|
selectedSpanLogs,
|
|
259
|
+
selectedAttrIndex,
|
|
260
|
+
aiCallDetailState,
|
|
261
|
+
aiChatChunks,
|
|
262
|
+
selectedChatChunkId,
|
|
263
|
+
onSelectChatChunk,
|
|
264
|
+
chatDetailChunkId,
|
|
265
|
+
onOpenChatChunkDetail,
|
|
266
|
+
onCloseChatChunkDetail,
|
|
267
|
+
chatDetailScrollOffset,
|
|
268
|
+
onSetChatDetailScrollOffset,
|
|
57
269
|
selectSpan,
|
|
58
270
|
}: TraceWorkspaceProps) => {
|
|
271
|
+
const aiDrillIn = selectedSpan !== null && isAiSpan(selectedSpan.tags)
|
|
59
272
|
const {
|
|
60
273
|
contentWidth,
|
|
61
274
|
headerFooterWidth,
|
|
@@ -74,36 +287,34 @@ export const TraceWorkspace = ({
|
|
|
74
287
|
narrowTraceListBodyHeight,
|
|
75
288
|
availableContentHeight,
|
|
76
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)
|
|
77
303
|
|
|
78
304
|
if (detailView === "service-logs") {
|
|
79
305
|
return (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<span fg={colors.defaultService}>{selectedTraceService ?? "unknown"}</span>
|
|
89
|
-
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
90
|
-
<span fg={colors.count}>recent logs</span>
|
|
91
|
-
</TextLine>
|
|
92
|
-
<BlankRow />
|
|
93
|
-
<ServiceLogsView
|
|
94
|
-
serviceName={selectedTraceService}
|
|
95
|
-
logsState={serviceLogState}
|
|
96
|
-
selectedIndex={selectedServiceLogIndex}
|
|
97
|
-
onSelectLog={setSelectedServiceLogIndex}
|
|
98
|
-
contentWidth={headerFooterWidth}
|
|
99
|
-
bodyLines={Math.max(8, availableContentHeight - 3)}
|
|
100
|
-
/>
|
|
101
|
-
</box>
|
|
306
|
+
<ServiceLogsScene
|
|
307
|
+
selectedTraceService={selectedTraceService}
|
|
308
|
+
serviceLogState={serviceLogState}
|
|
309
|
+
selectedServiceLogIndex={selectedServiceLogIndex}
|
|
310
|
+
setSelectedServiceLogIndex={setSelectedServiceLogIndex}
|
|
311
|
+
headerFooterWidth={headerFooterWidth}
|
|
312
|
+
availableContentHeight={availableContentHeight}
|
|
313
|
+
/>
|
|
102
314
|
)
|
|
103
315
|
}
|
|
104
316
|
|
|
105
317
|
if (isWideLayout) {
|
|
106
|
-
// L0: list (left) + trace preview (right). The two-pane zoom.
|
|
107
318
|
if (viewLevel === 0) {
|
|
108
319
|
return (
|
|
109
320
|
<box flexGrow={1} flexDirection="row">
|
|
@@ -120,83 +331,65 @@ export const TraceWorkspace = ({
|
|
|
120
331
|
</box>
|
|
121
332
|
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorJunctionChars} />
|
|
122
333
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
traceSummary={selectedTraceSummary}
|
|
126
|
-
traceStatus={traceDetailState.status}
|
|
127
|
-
traceError={traceDetailState.error}
|
|
128
|
-
traceLogsState={logState}
|
|
334
|
+
<TraceDetailsScene
|
|
335
|
+
{...traceDetailsProps}
|
|
129
336
|
contentWidth={rightContentWidth}
|
|
130
337
|
bodyLines={wideBodyLines}
|
|
131
338
|
paneWidth={rightPaneWidth}
|
|
132
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
133
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
134
339
|
focused={false}
|
|
135
|
-
onSelectSpan={selectSpan}
|
|
136
340
|
/>
|
|
137
341
|
</box>
|
|
138
342
|
</box>
|
|
139
343
|
)
|
|
140
344
|
}
|
|
141
345
|
|
|
142
|
-
// L1: the user pressed enter on a trace — hide the list entirely and
|
|
143
|
-
// let the waterfall take the full width. `leftPaneWidth` is already
|
|
144
|
-
// `contentWidth` in this case (see useAppLayout), so one pane fills
|
|
145
|
-
// the row.
|
|
146
346
|
if (viewLevel === 1) {
|
|
147
347
|
return (
|
|
148
348
|
<box flexGrow={1} flexDirection="row">
|
|
149
349
|
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
traceStatus={traceDetailState.status}
|
|
154
|
-
traceError={traceDetailState.error}
|
|
155
|
-
traceLogsState={logState}
|
|
156
|
-
contentWidth={Math.max(24, leftPaneWidth - sectionPadding * 2)}
|
|
350
|
+
<TraceDetailsScene
|
|
351
|
+
{...traceDetailsProps}
|
|
352
|
+
contentWidth={leftContentWidth}
|
|
157
353
|
bodyLines={wideBodyLines}
|
|
158
354
|
paneWidth={leftPaneWidth}
|
|
159
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
160
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
161
355
|
focused={true}
|
|
162
|
-
|
|
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}
|
|
163
368
|
/>
|
|
164
369
|
</box>
|
|
165
370
|
</box>
|
|
166
371
|
)
|
|
167
372
|
}
|
|
168
373
|
|
|
169
|
-
// L2: waterfall-left + span detail-right. Still no list.
|
|
170
374
|
return (
|
|
171
|
-
<box flexGrow={1} flexDirection="
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
190
|
-
<SpanDetailPane
|
|
191
|
-
span={selectedSpan}
|
|
192
|
-
trace={selectedTrace}
|
|
193
|
-
logs={selectedSpanLogs}
|
|
194
|
-
contentWidth={rightContentWidth}
|
|
195
|
-
bodyLines={wideBodyLines}
|
|
196
|
-
paneWidth={rightPaneWidth}
|
|
197
|
-
focused={true}
|
|
198
|
-
/>
|
|
199
|
-
</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
|
+
/>
|
|
200
393
|
</box>
|
|
201
394
|
)
|
|
202
395
|
}
|
|
@@ -214,19 +407,12 @@ export const TraceWorkspace = ({
|
|
|
214
407
|
padding={sectionPadding}
|
|
215
408
|
/>
|
|
216
409
|
<Divider width={contentWidth} />
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
traceSummary={selectedTraceSummary}
|
|
220
|
-
traceStatus={traceDetailState.status}
|
|
221
|
-
traceError={traceDetailState.error}
|
|
222
|
-
traceLogsState={logState}
|
|
410
|
+
<TraceDetailsScene
|
|
411
|
+
{...traceDetailsProps}
|
|
223
412
|
contentWidth={rightContentWidth}
|
|
224
413
|
bodyLines={narrowBodyLines}
|
|
225
414
|
paneWidth={contentWidth}
|
|
226
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
227
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
228
415
|
focused={false}
|
|
229
|
-
onSelectSpan={selectSpan}
|
|
230
416
|
/>
|
|
231
417
|
</>
|
|
232
418
|
)
|
|
@@ -234,48 +420,37 @@ export const TraceWorkspace = ({
|
|
|
234
420
|
|
|
235
421
|
return (
|
|
236
422
|
<>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<span fg={viewLevel === 1 ? colors.accent : colors.muted}>{selectedTraceSummary.rootOperationName}</span>
|
|
244
|
-
</>
|
|
245
|
-
) : null}
|
|
246
|
-
{viewLevel === 2 && selectedSpan ? (
|
|
247
|
-
<>
|
|
248
|
-
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
249
|
-
<span fg={colors.accent}>{selectedSpan.operationName}</span>
|
|
250
|
-
</>
|
|
251
|
-
) : null}
|
|
252
|
-
</TextLine>
|
|
253
|
-
</box>
|
|
254
|
-
<Divider width={contentWidth} />
|
|
423
|
+
<NarrowDrillInHeader
|
|
424
|
+
contentWidth={contentWidth}
|
|
425
|
+
viewLevel={viewLevel}
|
|
426
|
+
selectedTraceSummary={selectedTraceSummary}
|
|
427
|
+
selectedSpan={selectedSpan}
|
|
428
|
+
/>
|
|
255
429
|
{viewLevel === 1 ? (
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
traceSummary={selectedTraceSummary}
|
|
259
|
-
traceStatus={traceDetailState.status}
|
|
260
|
-
traceError={traceDetailState.error}
|
|
261
|
-
traceLogsState={logState}
|
|
430
|
+
<TraceDetailsScene
|
|
431
|
+
{...traceDetailsProps}
|
|
262
432
|
contentWidth={rightContentWidth}
|
|
263
433
|
bodyLines={narrowFullBodyLines}
|
|
264
434
|
paneWidth={contentWidth}
|
|
265
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
266
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
267
435
|
focused={true}
|
|
268
|
-
onSelectSpan={selectSpan}
|
|
269
436
|
/>
|
|
270
437
|
) : (
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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}
|
|
276
451
|
bodyLines={narrowFullBodyLines}
|
|
277
452
|
paneWidth={contentWidth}
|
|
278
|
-
|
|
453
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
279
454
|
/>
|
|
280
455
|
)}
|
|
281
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)
|