@kitlangton/motel 0.2.0 → 0.2.4
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 +7 -5
- package/src/App.tsx +233 -59
- package/src/daemon.test.ts +213 -6
- package/src/daemon.ts +174 -38
- package/src/domain.test.ts +62 -0
- package/src/domain.ts +16 -0
- package/src/localServer.ts +114 -128
- 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 +68 -0
- package/src/services/TelemetryStore.ts +262 -119
- 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 +244 -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 +308 -0
- package/src/ui/SpanContentView.tsx +181 -0
- package/src/ui/SpanDetail.tsx +98 -17
- package/src/ui/TraceDetailsPane.tsx +11 -28
- package/src/ui/Waterfall.tsx +43 -148
- package/src/ui/aiChatModel.test.ts +391 -0
- package/src/ui/aiChatModel.ts +773 -0
- package/src/ui/aiState.ts +71 -0
- package/src/ui/app/TraceWorkspace.tsx +288 -124
- package/src/ui/app/useAppLayout.ts +14 -11
- package/src/ui/app/useTraceScreenData.ts +174 -40
- 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,188 @@ 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 traceLogCount: number
|
|
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
|
+
}
|
|
37
|
+
|
|
38
|
+
const TraceDetailsScene = ({
|
|
39
|
+
trace,
|
|
40
|
+
traceSummary,
|
|
41
|
+
traceStatus,
|
|
42
|
+
traceError,
|
|
43
|
+
traceLogCount,
|
|
44
|
+
contentWidth,
|
|
45
|
+
bodyLines,
|
|
46
|
+
paneWidth,
|
|
47
|
+
selectedSpanIndex,
|
|
48
|
+
collapsedSpanIds,
|
|
49
|
+
waterfallFilterMode,
|
|
50
|
+
waterfallFilterText,
|
|
51
|
+
onSelectSpan,
|
|
52
|
+
}: TraceDetailsSceneProps) => (
|
|
53
|
+
<TraceDetailsPane
|
|
54
|
+
trace={trace}
|
|
55
|
+
traceSummary={traceSummary}
|
|
56
|
+
traceStatus={traceStatus}
|
|
57
|
+
traceError={traceError}
|
|
58
|
+
traceLogCount={traceLogCount}
|
|
59
|
+
contentWidth={contentWidth}
|
|
60
|
+
bodyLines={bodyLines}
|
|
61
|
+
paneWidth={paneWidth}
|
|
62
|
+
selectedSpanIndex={selectedSpanIndex}
|
|
63
|
+
collapsedSpanIds={collapsedSpanIds}
|
|
64
|
+
waterfallFilterMode={waterfallFilterMode}
|
|
65
|
+
waterfallFilterText={waterfallFilterText}
|
|
66
|
+
onSelectSpan={onSelectSpan}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
interface SpanDrillInSceneProps {
|
|
71
|
+
readonly aiDrillIn: boolean
|
|
72
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
73
|
+
readonly aiCallDetailState: AiCallDetailState
|
|
74
|
+
readonly aiChatChunks: readonly Chunk[]
|
|
75
|
+
readonly selectedChatChunkId: string | null
|
|
76
|
+
readonly onSelectChatChunk: (chunkId: string) => void
|
|
77
|
+
readonly chatDetailChunkId: string | null
|
|
78
|
+
readonly onOpenChatChunkDetail: (chunkId: string) => void
|
|
79
|
+
readonly onCloseChatChunkDetail: () => void
|
|
80
|
+
readonly chatDetailScrollOffset: number
|
|
81
|
+
readonly onSetChatDetailScrollOffset: (updater: (current: number) => number) => void
|
|
82
|
+
readonly contentWidth: number
|
|
83
|
+
readonly bodyLines: number
|
|
84
|
+
readonly paneWidth: number
|
|
85
|
+
readonly selectedAttrIndex: number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SpanDrillInScene = ({
|
|
89
|
+
aiDrillIn,
|
|
90
|
+
selectedSpan,
|
|
91
|
+
aiCallDetailState,
|
|
92
|
+
aiChatChunks,
|
|
93
|
+
selectedChatChunkId,
|
|
94
|
+
onSelectChatChunk,
|
|
95
|
+
chatDetailChunkId,
|
|
96
|
+
onOpenChatChunkDetail,
|
|
97
|
+
onCloseChatChunkDetail,
|
|
98
|
+
chatDetailScrollOffset,
|
|
99
|
+
onSetChatDetailScrollOffset,
|
|
100
|
+
contentWidth,
|
|
101
|
+
bodyLines,
|
|
102
|
+
paneWidth,
|
|
103
|
+
selectedAttrIndex,
|
|
104
|
+
}: SpanDrillInSceneProps) => aiDrillIn ? (
|
|
105
|
+
<AiChatView
|
|
106
|
+
span={selectedSpan}
|
|
107
|
+
detailState={aiCallDetailState}
|
|
108
|
+
chunks={aiChatChunks}
|
|
109
|
+
selectedChunkId={selectedChatChunkId}
|
|
110
|
+
onSelectChunk={onSelectChatChunk}
|
|
111
|
+
detailChunkId={chatDetailChunkId}
|
|
112
|
+
onOpenDetail={onOpenChatChunkDetail}
|
|
113
|
+
onCloseDetail={onCloseChatChunkDetail}
|
|
114
|
+
detailScrollOffset={chatDetailScrollOffset}
|
|
115
|
+
onSetDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
116
|
+
contentWidth={contentWidth}
|
|
117
|
+
bodyLines={bodyLines}
|
|
118
|
+
paneWidth={paneWidth}
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<SpanContentView
|
|
122
|
+
span={selectedSpan}
|
|
123
|
+
contentWidth={contentWidth}
|
|
124
|
+
bodyLines={bodyLines}
|
|
125
|
+
paneWidth={paneWidth}
|
|
126
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
interface ServiceLogsSceneProps {
|
|
131
|
+
readonly selectedTraceService: string | null
|
|
132
|
+
readonly serviceLogState: ServiceLogState
|
|
133
|
+
readonly selectedServiceLogIndex: number
|
|
134
|
+
readonly setSelectedServiceLogIndex: (value: number | ((current: number) => number)) => void
|
|
135
|
+
readonly headerFooterWidth: number
|
|
136
|
+
readonly availableContentHeight: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ServiceLogsScene = ({
|
|
140
|
+
selectedTraceService,
|
|
141
|
+
serviceLogState,
|
|
142
|
+
selectedServiceLogIndex,
|
|
143
|
+
setSelectedServiceLogIndex,
|
|
144
|
+
headerFooterWidth,
|
|
145
|
+
availableContentHeight,
|
|
146
|
+
}: ServiceLogsSceneProps) => (
|
|
147
|
+
<box flexGrow={1} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
148
|
+
<AlignedHeaderLine
|
|
149
|
+
left="SERVICE LOGS"
|
|
150
|
+
right={`${serviceLogState.data.length} logs${serviceLogState.fetchedAt ? `${SEPARATOR}${formatShortDate(serviceLogState.fetchedAt)} ${formatTimestamp(serviceLogState.fetchedAt)}` : ""}`}
|
|
151
|
+
width={headerFooterWidth}
|
|
152
|
+
rightFg={colors.count}
|
|
153
|
+
/>
|
|
154
|
+
<TextLine>
|
|
155
|
+
<span fg={colors.defaultService}>{selectedTraceService ?? "unknown"}</span>
|
|
156
|
+
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
157
|
+
<span fg={colors.count}>recent logs</span>
|
|
158
|
+
</TextLine>
|
|
159
|
+
<BlankRow />
|
|
160
|
+
<ServiceLogsView
|
|
161
|
+
serviceName={selectedTraceService}
|
|
162
|
+
logsState={serviceLogState}
|
|
163
|
+
selectedIndex={selectedServiceLogIndex}
|
|
164
|
+
onSelectLog={setSelectedServiceLogIndex}
|
|
165
|
+
contentWidth={headerFooterWidth}
|
|
166
|
+
bodyLines={Math.max(8, availableContentHeight - 3)}
|
|
167
|
+
/>
|
|
168
|
+
</box>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
interface NarrowDrillInHeaderProps {
|
|
172
|
+
readonly contentWidth: number
|
|
173
|
+
readonly viewLevel: 0 | 1 | 2
|
|
174
|
+
readonly selectedTraceSummary: TraceSummaryItem | null
|
|
175
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const NarrowDrillInHeader = ({ contentWidth, viewLevel, selectedTraceSummary, selectedSpan }: NarrowDrillInHeaderProps) => (
|
|
179
|
+
<>
|
|
180
|
+
<box paddingLeft={1} paddingRight={1} height={1} flexDirection="column">
|
|
181
|
+
<TextLine>
|
|
182
|
+
<span fg={colors.muted}>TRACES</span>
|
|
183
|
+
{selectedTraceSummary ? (
|
|
184
|
+
<>
|
|
185
|
+
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
186
|
+
<span fg={viewLevel === 1 ? colors.accent : colors.muted}>{selectedTraceSummary.rootOperationName}</span>
|
|
187
|
+
</>
|
|
188
|
+
) : null}
|
|
189
|
+
{viewLevel === 2 && selectedSpan ? (
|
|
190
|
+
<>
|
|
191
|
+
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
192
|
+
<span fg={colors.accent}>{selectedSpan.operationName}</span>
|
|
193
|
+
</>
|
|
194
|
+
) : null}
|
|
195
|
+
</TextLine>
|
|
196
|
+
</box>
|
|
197
|
+
<Divider width={contentWidth} />
|
|
198
|
+
</>
|
|
199
|
+
)
|
|
200
|
+
|
|
16
201
|
interface TraceWorkspaceProps {
|
|
17
202
|
readonly layout: AppLayout
|
|
18
203
|
readonly detailView: DetailView
|
|
@@ -34,6 +219,16 @@ interface TraceWorkspaceProps {
|
|
|
34
219
|
readonly viewLevel: 0 | 1 | 2
|
|
35
220
|
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
36
221
|
readonly selectedSpanLogs: readonly LogItem[]
|
|
222
|
+
readonly selectedAttrIndex: number
|
|
223
|
+
readonly aiCallDetailState: AiCallDetailState
|
|
224
|
+
readonly aiChatChunks: readonly Chunk[]
|
|
225
|
+
readonly selectedChatChunkId: string | null
|
|
226
|
+
readonly onSelectChatChunk: (chunkId: string) => void
|
|
227
|
+
readonly chatDetailChunkId: string | null
|
|
228
|
+
readonly onOpenChatChunkDetail: (chunkId: string) => void
|
|
229
|
+
readonly onCloseChatChunkDetail: () => void
|
|
230
|
+
readonly chatDetailScrollOffset: number
|
|
231
|
+
readonly onSetChatDetailScrollOffset: (updater: (current: number) => number) => void
|
|
37
232
|
readonly selectSpan: (index: number) => void
|
|
38
233
|
}
|
|
39
234
|
|
|
@@ -58,8 +253,19 @@ export const TraceWorkspace = ({
|
|
|
58
253
|
viewLevel,
|
|
59
254
|
selectedSpan,
|
|
60
255
|
selectedSpanLogs,
|
|
256
|
+
selectedAttrIndex,
|
|
257
|
+
aiCallDetailState,
|
|
258
|
+
aiChatChunks,
|
|
259
|
+
selectedChatChunkId,
|
|
260
|
+
onSelectChatChunk,
|
|
261
|
+
chatDetailChunkId,
|
|
262
|
+
onOpenChatChunkDetail,
|
|
263
|
+
onCloseChatChunkDetail,
|
|
264
|
+
chatDetailScrollOffset,
|
|
265
|
+
onSetChatDetailScrollOffset,
|
|
61
266
|
selectSpan,
|
|
62
267
|
}: TraceWorkspaceProps) => {
|
|
268
|
+
const aiDrillIn = selectedSpan !== null && isAiSpan(selectedSpan.tags)
|
|
63
269
|
const {
|
|
64
270
|
contentWidth,
|
|
65
271
|
headerFooterWidth,
|
|
@@ -78,36 +284,34 @@ export const TraceWorkspace = ({
|
|
|
78
284
|
narrowTraceListBodyHeight,
|
|
79
285
|
availableContentHeight,
|
|
80
286
|
} = layout
|
|
287
|
+
const traceDetailsProps: SharedTraceDetailsProps = {
|
|
288
|
+
trace: selectedTrace,
|
|
289
|
+
traceSummary: selectedTraceSummary,
|
|
290
|
+
traceStatus: traceDetailState.status,
|
|
291
|
+
traceError: traceDetailState.error,
|
|
292
|
+
traceLogCount: logState.data.length,
|
|
293
|
+
selectedSpanIndex,
|
|
294
|
+
collapsedSpanIds,
|
|
295
|
+
waterfallFilterMode,
|
|
296
|
+
waterfallFilterText,
|
|
297
|
+
onSelectSpan: selectSpan,
|
|
298
|
+
}
|
|
299
|
+
const drillInContentWidth = Math.max(24, contentWidth - 2)
|
|
81
300
|
|
|
82
301
|
if (detailView === "service-logs") {
|
|
83
302
|
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>
|
|
303
|
+
<ServiceLogsScene
|
|
304
|
+
selectedTraceService={selectedTraceService}
|
|
305
|
+
serviceLogState={serviceLogState}
|
|
306
|
+
selectedServiceLogIndex={selectedServiceLogIndex}
|
|
307
|
+
setSelectedServiceLogIndex={setSelectedServiceLogIndex}
|
|
308
|
+
headerFooterWidth={headerFooterWidth}
|
|
309
|
+
availableContentHeight={availableContentHeight}
|
|
310
|
+
/>
|
|
106
311
|
)
|
|
107
312
|
}
|
|
108
313
|
|
|
109
314
|
if (isWideLayout) {
|
|
110
|
-
// L0: list (left) + trace preview (right). The two-pane zoom.
|
|
111
315
|
if (viewLevel === 0) {
|
|
112
316
|
return (
|
|
113
317
|
<box flexGrow={1} flexDirection="row">
|
|
@@ -124,83 +328,63 @@ export const TraceWorkspace = ({
|
|
|
124
328
|
</box>
|
|
125
329
|
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorJunctionChars} />
|
|
126
330
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
traceSummary={selectedTraceSummary}
|
|
130
|
-
traceStatus={traceDetailState.status}
|
|
131
|
-
traceError={traceDetailState.error}
|
|
132
|
-
traceLogsState={logState}
|
|
331
|
+
<TraceDetailsScene
|
|
332
|
+
{...traceDetailsProps}
|
|
133
333
|
contentWidth={rightContentWidth}
|
|
134
334
|
bodyLines={wideBodyLines}
|
|
135
335
|
paneWidth={rightPaneWidth}
|
|
136
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
137
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
138
|
-
focused={false}
|
|
139
|
-
waterfallFilterMode={waterfallFilterMode} waterfallFilterText={waterfallFilterText} onSelectSpan={selectSpan}
|
|
140
336
|
/>
|
|
141
337
|
</box>
|
|
142
338
|
</box>
|
|
143
339
|
)
|
|
144
340
|
}
|
|
145
341
|
|
|
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
342
|
if (viewLevel === 1) {
|
|
151
343
|
return (
|
|
152
344
|
<box flexGrow={1} flexDirection="row">
|
|
153
345
|
<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)}
|
|
346
|
+
<TraceDetailsScene
|
|
347
|
+
{...traceDetailsProps}
|
|
348
|
+
contentWidth={leftContentWidth}
|
|
161
349
|
bodyLines={wideBodyLines}
|
|
162
350
|
paneWidth={leftPaneWidth}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
351
|
+
/>
|
|
352
|
+
</box>
|
|
353
|
+
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorCrossChars} />
|
|
354
|
+
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
355
|
+
<SpanDetailPane
|
|
356
|
+
span={selectedSpan}
|
|
357
|
+
trace={selectedTrace}
|
|
358
|
+
logs={selectedSpanLogs}
|
|
359
|
+
contentWidth={rightContentWidth}
|
|
360
|
+
bodyLines={wideBodyLines}
|
|
361
|
+
paneWidth={rightPaneWidth}
|
|
362
|
+
focused={false}
|
|
167
363
|
/>
|
|
168
364
|
</box>
|
|
169
365
|
</box>
|
|
170
366
|
)
|
|
171
367
|
}
|
|
172
368
|
|
|
173
|
-
// L2: waterfall-left + span detail-right. Still no list.
|
|
174
369
|
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>
|
|
370
|
+
<box flexGrow={1} flexDirection="column">
|
|
371
|
+
<SpanDrillInScene
|
|
372
|
+
aiDrillIn={aiDrillIn}
|
|
373
|
+
selectedSpan={selectedSpan}
|
|
374
|
+
aiCallDetailState={aiCallDetailState}
|
|
375
|
+
aiChatChunks={aiChatChunks}
|
|
376
|
+
selectedChatChunkId={selectedChatChunkId}
|
|
377
|
+
onSelectChatChunk={onSelectChatChunk}
|
|
378
|
+
chatDetailChunkId={chatDetailChunkId}
|
|
379
|
+
onOpenChatChunkDetail={onOpenChatChunkDetail}
|
|
380
|
+
onCloseChatChunkDetail={onCloseChatChunkDetail}
|
|
381
|
+
chatDetailScrollOffset={chatDetailScrollOffset}
|
|
382
|
+
onSetChatDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
383
|
+
contentWidth={drillInContentWidth}
|
|
384
|
+
bodyLines={wideBodyLines}
|
|
385
|
+
paneWidth={contentWidth}
|
|
386
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
387
|
+
/>
|
|
204
388
|
</box>
|
|
205
389
|
)
|
|
206
390
|
}
|
|
@@ -218,19 +402,11 @@ export const TraceWorkspace = ({
|
|
|
218
402
|
padding={sectionPadding}
|
|
219
403
|
/>
|
|
220
404
|
<Divider width={contentWidth} />
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
traceSummary={selectedTraceSummary}
|
|
224
|
-
traceStatus={traceDetailState.status}
|
|
225
|
-
traceError={traceDetailState.error}
|
|
226
|
-
traceLogsState={logState}
|
|
405
|
+
<TraceDetailsScene
|
|
406
|
+
{...traceDetailsProps}
|
|
227
407
|
contentWidth={rightContentWidth}
|
|
228
408
|
bodyLines={narrowBodyLines}
|
|
229
409
|
paneWidth={contentWidth}
|
|
230
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
231
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
232
|
-
focused={false}
|
|
233
|
-
waterfallFilterMode={waterfallFilterMode} waterfallFilterText={waterfallFilterText} onSelectSpan={selectSpan}
|
|
234
410
|
/>
|
|
235
411
|
</>
|
|
236
412
|
)
|
|
@@ -238,48 +414,36 @@ export const TraceWorkspace = ({
|
|
|
238
414
|
|
|
239
415
|
return (
|
|
240
416
|
<>
|
|
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} />
|
|
417
|
+
<NarrowDrillInHeader
|
|
418
|
+
contentWidth={contentWidth}
|
|
419
|
+
viewLevel={viewLevel}
|
|
420
|
+
selectedTraceSummary={selectedTraceSummary}
|
|
421
|
+
selectedSpan={selectedSpan}
|
|
422
|
+
/>
|
|
259
423
|
{viewLevel === 1 ? (
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
traceSummary={selectedTraceSummary}
|
|
263
|
-
traceStatus={traceDetailState.status}
|
|
264
|
-
traceError={traceDetailState.error}
|
|
265
|
-
traceLogsState={logState}
|
|
424
|
+
<TraceDetailsScene
|
|
425
|
+
{...traceDetailsProps}
|
|
266
426
|
contentWidth={rightContentWidth}
|
|
267
427
|
bodyLines={narrowFullBodyLines}
|
|
268
428
|
paneWidth={contentWidth}
|
|
269
|
-
selectedSpanIndex={selectedSpanIndex}
|
|
270
|
-
collapsedSpanIds={collapsedSpanIds}
|
|
271
|
-
focused={true}
|
|
272
|
-
waterfallFilterMode={waterfallFilterMode} waterfallFilterText={waterfallFilterText} onSelectSpan={selectSpan}
|
|
273
429
|
/>
|
|
274
430
|
) : (
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
431
|
+
<SpanDrillInScene
|
|
432
|
+
aiDrillIn={aiDrillIn}
|
|
433
|
+
selectedSpan={selectedSpan}
|
|
434
|
+
aiCallDetailState={aiCallDetailState}
|
|
435
|
+
aiChatChunks={aiChatChunks}
|
|
436
|
+
selectedChatChunkId={selectedChatChunkId}
|
|
437
|
+
onSelectChatChunk={onSelectChatChunk}
|
|
438
|
+
chatDetailChunkId={chatDetailChunkId}
|
|
439
|
+
onOpenChatChunkDetail={onOpenChatChunkDetail}
|
|
440
|
+
onCloseChatChunkDetail={onCloseChatChunkDetail}
|
|
441
|
+
chatDetailScrollOffset={chatDetailScrollOffset}
|
|
442
|
+
onSetChatDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
443
|
+
contentWidth={drillInContentWidth}
|
|
280
444
|
bodyLines={narrowFullBodyLines}
|
|
281
445
|
paneWidth={contentWidth}
|
|
282
|
-
|
|
446
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
283
447
|
/>
|
|
284
448
|
)}
|
|
285
449
|
</>
|
|
@@ -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)
|