@aryasaatvik/motel 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +180 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/package.json +119 -0
- package/skills/motel-debug/SKILL.md +203 -0
- package/skills/motel-debug/references/effect.md +38 -0
- package/src/App.tsx +414 -0
- package/src/StartupGate.test.ts +80 -0
- package/src/StartupGate.tsx +263 -0
- package/src/cli.ts +205 -0
- package/src/config.ts +45 -0
- package/src/daemon.test.ts +547 -0
- package/src/daemon.ts +486 -0
- package/src/domain.test.ts +62 -0
- package/src/domain.ts +291 -0
- package/src/httpApi.ts +391 -0
- package/src/httpListPolicy.test.ts +76 -0
- package/src/httpListPolicy.ts +129 -0
- package/src/index.tsx +25 -0
- package/src/instructions.ts +72 -0
- package/src/launchAgent.test.ts +430 -0
- package/src/launchAgent.ts +462 -0
- package/src/localServer.ts +568 -0
- package/src/locator.ts +138 -0
- package/src/mcp.ts +433 -0
- package/src/motel.test.ts +176 -0
- package/src/motel.ts +88 -0
- package/src/motelClient.ts +353 -0
- package/src/opentui-jsx.d.ts +11 -0
- package/src/otlp.test.ts +65 -0
- package/src/otlp.ts +162 -0
- package/src/otlpProtobuf.ts +35 -0
- package/src/queryFilters.ts +39 -0
- package/src/registry.ts +115 -0
- package/src/runtime.ts +40 -0
- package/src/server.ts +11 -0
- package/src/services/AsyncIngest.ts +82 -0
- package/src/services/LogQueryService.ts +29 -0
- package/src/services/TelemetryQuery.ts +62 -0
- package/src/services/TelemetryStore.ts +2554 -0
- package/src/services/TraceQueryService.ts +43 -0
- package/src/services/ingestRpc.ts +39 -0
- package/src/services/queryRpc.ts +15 -0
- package/src/services/telemetryQueryWorker.ts +32 -0
- package/src/services/telemetryWorker.ts +59 -0
- package/src/startupBench.ts +19 -0
- package/src/startupLifecycle.ts +57 -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 +1053 -0
- package/src/ui/AiChatView.tsx +308 -0
- package/src/ui/AttrFilterModal.tsx +120 -0
- package/src/ui/ServiceLogs.tsx +115 -0
- package/src/ui/SpanContentView.tsx +184 -0
- package/src/ui/SpanDetail.tsx +215 -0
- package/src/ui/SpanDetailFull.tsx +224 -0
- package/src/ui/SpanDetailPane.tsx +90 -0
- package/src/ui/TraceDetailsPane.tsx +175 -0
- package/src/ui/TraceList.tsx +254 -0
- package/src/ui/Waterfall.tsx +389 -0
- package/src/ui/aiChatModel.test.ts +391 -0
- package/src/ui/aiChatModel.ts +773 -0
- package/src/ui/aiState.ts +59 -0
- package/src/ui/app/TraceListPane.tsx +39 -0
- package/src/ui/app/TraceWorkspace.tsx +452 -0
- package/src/ui/app/useAppLayout.ts +101 -0
- package/src/ui/app/useTraceScreenData.ts +590 -0
- package/src/ui/atoms.ts +131 -0
- package/src/ui/cachedLoader.test.ts +23 -0
- package/src/ui/cachedLoader.ts +60 -0
- package/src/ui/filterParser.test.ts +56 -0
- package/src/ui/filterParser.ts +45 -0
- package/src/ui/format.ts +128 -0
- package/src/ui/loaders.ts +101 -0
- package/src/ui/persistence.ts +41 -0
- package/src/ui/primitives.tsx +198 -0
- package/src/ui/state.ts +6 -0
- package/src/ui/theme.ts +160 -0
- package/src/ui/traceDetailsWidth.repro.test.ts +126 -0
- package/src/ui/traceSortNav.repro.seed.ts +62 -0
- package/src/ui/traceSortNav.repro.test.ts +231 -0
- package/src/ui/useAttrFilterPicker.ts +89 -0
- package/src/ui/useKeyboardNav.ts +1023 -0
- 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.repro.seed.ts +86 -0
- package/src/ui/waterfallNav.repro.test.ts +271 -0
- package/src/ui/waterfallNav.test.ts +453 -0
- package/src/ui/waterfallNav.ts +75 -0
- package/web/dist/assets/index-B01z9BaO.css +2 -0
- package/web/dist/assets/index-CYlBaY9O.js +22 -0
- package/web/dist/index.html +13 -0
|
@@ -0,0 +1,59 @@
|
|
|
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 { TelemetryStoreReadonly } from "../services/TelemetryStore.ts"
|
|
6
|
+
import type { LoadStatus } from "./atoms.ts"
|
|
7
|
+
import { makeCachedLoader } from "./cachedLoader.ts"
|
|
8
|
+
|
|
9
|
+
// AI chat view (full-screen when drilled into an `isAiSpan` span).
|
|
10
|
+
// ---------------------------------------------------------------------
|
|
11
|
+
// The main pane is a normal selectable list of semantic chunks (one row
|
|
12
|
+
// per chunk, with stable list scrolling). Opening a chunk shows its full
|
|
13
|
+
// content in a modal overlay that owns its own line scroll offset. This
|
|
14
|
+
// feels much closer to the rest of motel than the previous in-line
|
|
15
|
+
// expansion experiment.
|
|
16
|
+
// ---------------------------------------------------------------------
|
|
17
|
+
/** Chunk id currently selected in the list (null = first chunk). */
|
|
18
|
+
export const selectedChatChunkIdAtom = Atom.make<string | null>(null).pipe(Atom.keepAlive)
|
|
19
|
+
/** Chunk id whose detail modal is currently open. */
|
|
20
|
+
export const chatDetailChunkIdAtom = Atom.make<string | null>(null).pipe(Atom.keepAlive)
|
|
21
|
+
/** Line scroll offset inside the open detail modal. */
|
|
22
|
+
export const chatDetailScrollOffsetAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
23
|
+
|
|
24
|
+
export interface AiCallDetailState {
|
|
25
|
+
readonly status: LoadStatus
|
|
26
|
+
readonly spanId: string | null
|
|
27
|
+
readonly data: AiCallDetail | null
|
|
28
|
+
readonly error: string | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const initialAiCallDetailState: AiCallDetailState = {
|
|
32
|
+
status: "ready",
|
|
33
|
+
spanId: null,
|
|
34
|
+
data: null,
|
|
35
|
+
error: null,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const aiCallDetailStateAtom = Atom.make(initialAiCallDetailState).pipe(Atom.keepAlive)
|
|
39
|
+
|
|
40
|
+
export const loadAiCallDetail = (spanId: string) =>
|
|
41
|
+
queryRuntime.runPromise(Effect.flatMap(TelemetryStoreReadonly, (service) => service.getAiCall(spanId)))
|
|
42
|
+
|
|
43
|
+
// AI call detail cache: the `ai.prompt` payload can easily be 50KB+ and
|
|
44
|
+
// we don't want to re-hit SQLite every time j/k moves the selection
|
|
45
|
+
// between adjacent AI spans. Cleared alongside the other per-refresh
|
|
46
|
+
// caches in `useTraceScreenData`.
|
|
47
|
+
const aiCallDetailLoader = makeCachedLoader<string, AiCallDetail | null>({
|
|
48
|
+
load: loadAiCallDetail,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const getCachedAiCallDetail = (spanId: string): AiCallDetail | null | undefined =>
|
|
52
|
+
aiCallDetailLoader.get(spanId)
|
|
53
|
+
|
|
54
|
+
export const ensureAiCallDetail = (spanId: string): Promise<AiCallDetail | null> =>
|
|
55
|
+
aiCallDetailLoader.ensure(spanId)
|
|
56
|
+
|
|
57
|
+
export const invalidateAiCallDetailCache = () => {
|
|
58
|
+
aiCallDetailLoader.invalidate()
|
|
59
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { FilterBar } from "../primitives.tsx"
|
|
2
|
+
import { TraceListBody, TraceListHeader, type TraceListProps } from "../TraceList.tsx"
|
|
3
|
+
|
|
4
|
+
interface TraceListPaneProps {
|
|
5
|
+
readonly traceListProps: TraceListProps
|
|
6
|
+
readonly filterMode: boolean
|
|
7
|
+
readonly filterText: string
|
|
8
|
+
readonly filterWidth: number
|
|
9
|
+
readonly containerHeight: number
|
|
10
|
+
readonly bodyHeight: number
|
|
11
|
+
readonly padding: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Replaced the opentui <scrollbox> with a direct virtual-windowed body.
|
|
16
|
+
* Rationale: the scrollbox's scrollSize is updated during opentui's render
|
|
17
|
+
* pass, not during React commit, so the useLayoutEffect that adjusted
|
|
18
|
+
* scrollTop on refresh was reading a stale max and clamping our intended
|
|
19
|
+
* scroll position. Rendering only the visible rows ourselves keeps the
|
|
20
|
+
* viewport math entirely in React state and eliminates the race.
|
|
21
|
+
*/
|
|
22
|
+
export const TraceListPane = ({
|
|
23
|
+
traceListProps,
|
|
24
|
+
filterMode,
|
|
25
|
+
filterText,
|
|
26
|
+
filterWidth,
|
|
27
|
+
containerHeight,
|
|
28
|
+
bodyHeight,
|
|
29
|
+
padding,
|
|
30
|
+
}: TraceListPaneProps) => {
|
|
31
|
+
const bodyRows = Math.max(1, filterMode ? bodyHeight - 1 : bodyHeight)
|
|
32
|
+
return (
|
|
33
|
+
<box height={containerHeight} flexDirection="column" paddingLeft={padding} paddingRight={0}>
|
|
34
|
+
<TraceListHeader {...traceListProps} />
|
|
35
|
+
{filterMode ? <FilterBar text={filterText} width={filterWidth} /> : null}
|
|
36
|
+
<TraceListBody {...traceListProps} viewportRows={bodyRows} />
|
|
37
|
+
</box>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,452 @@
|
|
|
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"
|
|
4
|
+
import { formatShortDate, formatTimestamp } from "../format.ts"
|
|
5
|
+
import { AlignedHeaderLine, BlankRow, Divider, SeparatorColumn, TextLine } from "../primitives.tsx"
|
|
6
|
+
import { ServiceLogsView } from "../ServiceLogs.tsx"
|
|
7
|
+
import { SpanContentView } from "../SpanContentView.tsx"
|
|
8
|
+
import { SpanDetailPane } from "../SpanDetailPane.tsx"
|
|
9
|
+
import type { DetailView, LogState, ServiceLogState, TraceDetailState } from "../atoms.ts"
|
|
10
|
+
import type { AiCallDetailState } from "../aiState.ts"
|
|
11
|
+
import { colors, SEPARATOR } from "../theme.ts"
|
|
12
|
+
import { TraceDetailsPane } from "../TraceDetailsPane.tsx"
|
|
13
|
+
import type { TraceListProps } from "../TraceList.tsx"
|
|
14
|
+
import { TraceListPane } from "./TraceListPane.tsx"
|
|
15
|
+
import type { AppLayout } from "./useAppLayout.ts"
|
|
16
|
+
|
|
17
|
+
const separatorJunctionChars = new Map<number, string>([[3, "├"]])
|
|
18
|
+
const separatorCrossChars = new Map<number, string>([[3, "┼"]])
|
|
19
|
+
|
|
20
|
+
interface SharedTraceDetailsProps {
|
|
21
|
+
readonly trace: TraceItem | null
|
|
22
|
+
readonly traceSummary: TraceSummaryItem | null
|
|
23
|
+
readonly traceStatus: TraceDetailState["status"]
|
|
24
|
+
readonly traceError: string | null
|
|
25
|
+
readonly traceLogCount: number
|
|
26
|
+
readonly selectedSpanIndex: number | null
|
|
27
|
+
readonly collapsedSpanIds: ReadonlySet<string>
|
|
28
|
+
readonly waterfallFilterMode: boolean
|
|
29
|
+
readonly waterfallFilterText: string
|
|
30
|
+
readonly onSelectSpan: (index: number) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface TraceDetailsSceneProps extends SharedTraceDetailsProps {
|
|
34
|
+
readonly contentWidth: number
|
|
35
|
+
readonly bodyLines: number
|
|
36
|
+
readonly paneWidth: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TraceDetailsScene = ({
|
|
40
|
+
trace,
|
|
41
|
+
traceSummary,
|
|
42
|
+
traceStatus,
|
|
43
|
+
traceError,
|
|
44
|
+
traceLogCount,
|
|
45
|
+
contentWidth,
|
|
46
|
+
bodyLines,
|
|
47
|
+
paneWidth,
|
|
48
|
+
selectedSpanIndex,
|
|
49
|
+
collapsedSpanIds,
|
|
50
|
+
waterfallFilterMode,
|
|
51
|
+
waterfallFilterText,
|
|
52
|
+
onSelectSpan,
|
|
53
|
+
}: TraceDetailsSceneProps) => (
|
|
54
|
+
<TraceDetailsPane
|
|
55
|
+
trace={trace}
|
|
56
|
+
traceSummary={traceSummary}
|
|
57
|
+
traceStatus={traceStatus}
|
|
58
|
+
traceError={traceError}
|
|
59
|
+
traceLogCount={traceLogCount}
|
|
60
|
+
contentWidth={contentWidth}
|
|
61
|
+
bodyLines={bodyLines}
|
|
62
|
+
paneWidth={paneWidth}
|
|
63
|
+
selectedSpanIndex={selectedSpanIndex}
|
|
64
|
+
collapsedSpanIds={collapsedSpanIds}
|
|
65
|
+
waterfallFilterMode={waterfallFilterMode}
|
|
66
|
+
waterfallFilterText={waterfallFilterText}
|
|
67
|
+
onSelectSpan={onSelectSpan}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
interface SpanDrillInSceneProps {
|
|
72
|
+
readonly aiDrillIn: boolean
|
|
73
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
74
|
+
readonly aiCallDetailState: AiCallDetailState
|
|
75
|
+
readonly aiChatChunks: readonly Chunk[]
|
|
76
|
+
readonly selectedChatChunkId: string | null
|
|
77
|
+
readonly onSelectChatChunk: (chunkId: string) => void
|
|
78
|
+
readonly chatDetailChunkId: string | null
|
|
79
|
+
readonly onOpenChatChunkDetail: (chunkId: string) => void
|
|
80
|
+
readonly onCloseChatChunkDetail: () => void
|
|
81
|
+
readonly chatDetailScrollOffset: number
|
|
82
|
+
readonly onSetChatDetailScrollOffset: (updater: (current: number) => number) => void
|
|
83
|
+
readonly contentWidth: number
|
|
84
|
+
readonly bodyLines: number
|
|
85
|
+
readonly paneWidth: number
|
|
86
|
+
readonly selectedAttrIndex: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const SpanDrillInScene = ({
|
|
90
|
+
aiDrillIn,
|
|
91
|
+
selectedSpan,
|
|
92
|
+
aiCallDetailState,
|
|
93
|
+
aiChatChunks,
|
|
94
|
+
selectedChatChunkId,
|
|
95
|
+
onSelectChatChunk,
|
|
96
|
+
chatDetailChunkId,
|
|
97
|
+
onOpenChatChunkDetail,
|
|
98
|
+
onCloseChatChunkDetail,
|
|
99
|
+
chatDetailScrollOffset,
|
|
100
|
+
onSetChatDetailScrollOffset,
|
|
101
|
+
contentWidth,
|
|
102
|
+
bodyLines,
|
|
103
|
+
paneWidth,
|
|
104
|
+
selectedAttrIndex,
|
|
105
|
+
}: SpanDrillInSceneProps) => aiDrillIn ? (
|
|
106
|
+
<AiChatView
|
|
107
|
+
span={selectedSpan}
|
|
108
|
+
detailState={aiCallDetailState}
|
|
109
|
+
chunks={aiChatChunks}
|
|
110
|
+
selectedChunkId={selectedChatChunkId}
|
|
111
|
+
onSelectChunk={onSelectChatChunk}
|
|
112
|
+
detailChunkId={chatDetailChunkId}
|
|
113
|
+
onOpenDetail={onOpenChatChunkDetail}
|
|
114
|
+
onCloseDetail={onCloseChatChunkDetail}
|
|
115
|
+
detailScrollOffset={chatDetailScrollOffset}
|
|
116
|
+
onSetDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
117
|
+
contentWidth={contentWidth}
|
|
118
|
+
bodyLines={bodyLines}
|
|
119
|
+
paneWidth={paneWidth}
|
|
120
|
+
/>
|
|
121
|
+
) : (
|
|
122
|
+
<SpanContentView
|
|
123
|
+
span={selectedSpan}
|
|
124
|
+
contentWidth={contentWidth}
|
|
125
|
+
bodyLines={bodyLines}
|
|
126
|
+
paneWidth={paneWidth}
|
|
127
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
interface ServiceLogsSceneProps {
|
|
132
|
+
readonly selectedTraceService: string | null
|
|
133
|
+
readonly serviceLogState: ServiceLogState
|
|
134
|
+
readonly selectedServiceLogIndex: number
|
|
135
|
+
readonly setSelectedServiceLogIndex: (value: number | ((current: number) => number)) => void
|
|
136
|
+
readonly headerFooterWidth: number
|
|
137
|
+
readonly availableContentHeight: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ServiceLogsScene = ({
|
|
141
|
+
selectedTraceService,
|
|
142
|
+
serviceLogState,
|
|
143
|
+
selectedServiceLogIndex,
|
|
144
|
+
setSelectedServiceLogIndex,
|
|
145
|
+
headerFooterWidth,
|
|
146
|
+
availableContentHeight,
|
|
147
|
+
}: ServiceLogsSceneProps) => (
|
|
148
|
+
<box flexGrow={1} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
149
|
+
<AlignedHeaderLine
|
|
150
|
+
left="SERVICE LOGS"
|
|
151
|
+
right={`${serviceLogState.data.length} logs${serviceLogState.fetchedAt ? `${SEPARATOR}${formatShortDate(serviceLogState.fetchedAt)} ${formatTimestamp(serviceLogState.fetchedAt)}` : ""}`}
|
|
152
|
+
width={headerFooterWidth}
|
|
153
|
+
rightFg={colors.count}
|
|
154
|
+
/>
|
|
155
|
+
<TextLine>
|
|
156
|
+
<span fg={colors.defaultService}>{selectedTraceService ?? "unknown"}</span>
|
|
157
|
+
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
158
|
+
<span fg={colors.count}>recent logs</span>
|
|
159
|
+
</TextLine>
|
|
160
|
+
<BlankRow />
|
|
161
|
+
<ServiceLogsView
|
|
162
|
+
serviceName={selectedTraceService}
|
|
163
|
+
logsState={serviceLogState}
|
|
164
|
+
selectedIndex={selectedServiceLogIndex}
|
|
165
|
+
onSelectLog={setSelectedServiceLogIndex}
|
|
166
|
+
contentWidth={headerFooterWidth}
|
|
167
|
+
bodyLines={Math.max(8, availableContentHeight - 3)}
|
|
168
|
+
/>
|
|
169
|
+
</box>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
interface NarrowDrillInHeaderProps {
|
|
173
|
+
readonly contentWidth: number
|
|
174
|
+
readonly viewLevel: 0 | 1 | 2
|
|
175
|
+
readonly selectedTraceSummary: TraceSummaryItem | null
|
|
176
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const NarrowDrillInHeader = ({ contentWidth, viewLevel, selectedTraceSummary, selectedSpan }: NarrowDrillInHeaderProps) => (
|
|
180
|
+
<>
|
|
181
|
+
<box paddingLeft={1} paddingRight={1} height={1} flexDirection="column">
|
|
182
|
+
<TextLine>
|
|
183
|
+
<span fg={colors.muted}>TRACES</span>
|
|
184
|
+
{selectedTraceSummary ? (
|
|
185
|
+
<>
|
|
186
|
+
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
187
|
+
<span fg={viewLevel === 1 ? colors.accent : colors.muted}>{selectedTraceSummary.rootOperationName}</span>
|
|
188
|
+
</>
|
|
189
|
+
) : null}
|
|
190
|
+
{viewLevel === 2 && selectedSpan ? (
|
|
191
|
+
<>
|
|
192
|
+
<span fg={colors.separator}>{" "}{SEPARATOR}{" "}</span>
|
|
193
|
+
<span fg={colors.accent}>{selectedSpan.operationName}</span>
|
|
194
|
+
</>
|
|
195
|
+
) : null}
|
|
196
|
+
</TextLine>
|
|
197
|
+
</box>
|
|
198
|
+
<Divider width={contentWidth} />
|
|
199
|
+
</>
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
interface TraceWorkspaceProps {
|
|
203
|
+
readonly layout: AppLayout
|
|
204
|
+
readonly detailView: DetailView
|
|
205
|
+
readonly filterMode: boolean
|
|
206
|
+
readonly filterText: string
|
|
207
|
+
readonly waterfallFilterMode: boolean
|
|
208
|
+
readonly waterfallFilterText: string
|
|
209
|
+
readonly traceListProps: TraceListProps
|
|
210
|
+
readonly selectedTraceService: string | null
|
|
211
|
+
readonly serviceLogState: ServiceLogState
|
|
212
|
+
readonly selectedServiceLogIndex: number
|
|
213
|
+
readonly setSelectedServiceLogIndex: (value: number | ((current: number) => number)) => void
|
|
214
|
+
readonly traceDetailState: TraceDetailState
|
|
215
|
+
readonly selectedTrace: TraceItem | null
|
|
216
|
+
readonly selectedTraceSummary: TraceSummaryItem | null
|
|
217
|
+
readonly logState: LogState
|
|
218
|
+
readonly selectedSpanIndex: number | null
|
|
219
|
+
readonly collapsedSpanIds: ReadonlySet<string>
|
|
220
|
+
readonly viewLevel: 0 | 1 | 2
|
|
221
|
+
readonly selectedSpan: TraceItem["spans"][number] | null
|
|
222
|
+
readonly selectedSpanLogs: readonly LogItem[]
|
|
223
|
+
readonly selectedAttrIndex: number
|
|
224
|
+
readonly aiCallDetailState: AiCallDetailState
|
|
225
|
+
readonly aiChatChunks: readonly Chunk[]
|
|
226
|
+
readonly selectedChatChunkId: string | null
|
|
227
|
+
readonly onSelectChatChunk: (chunkId: string) => void
|
|
228
|
+
readonly chatDetailChunkId: string | null
|
|
229
|
+
readonly onOpenChatChunkDetail: (chunkId: string) => void
|
|
230
|
+
readonly onCloseChatChunkDetail: () => void
|
|
231
|
+
readonly chatDetailScrollOffset: number
|
|
232
|
+
readonly onSetChatDetailScrollOffset: (updater: (current: number) => number) => void
|
|
233
|
+
readonly selectSpan: (index: number) => void
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const TraceWorkspace = ({
|
|
237
|
+
layout,
|
|
238
|
+
detailView,
|
|
239
|
+
filterMode,
|
|
240
|
+
filterText,
|
|
241
|
+
waterfallFilterMode,
|
|
242
|
+
waterfallFilterText,
|
|
243
|
+
traceListProps,
|
|
244
|
+
selectedTraceService,
|
|
245
|
+
serviceLogState,
|
|
246
|
+
selectedServiceLogIndex,
|
|
247
|
+
setSelectedServiceLogIndex,
|
|
248
|
+
traceDetailState,
|
|
249
|
+
selectedTrace,
|
|
250
|
+
selectedTraceSummary,
|
|
251
|
+
logState,
|
|
252
|
+
selectedSpanIndex,
|
|
253
|
+
collapsedSpanIds,
|
|
254
|
+
viewLevel,
|
|
255
|
+
selectedSpan,
|
|
256
|
+
selectedSpanLogs,
|
|
257
|
+
selectedAttrIndex,
|
|
258
|
+
aiCallDetailState,
|
|
259
|
+
aiChatChunks,
|
|
260
|
+
selectedChatChunkId,
|
|
261
|
+
onSelectChatChunk,
|
|
262
|
+
chatDetailChunkId,
|
|
263
|
+
onOpenChatChunkDetail,
|
|
264
|
+
onCloseChatChunkDetail,
|
|
265
|
+
chatDetailScrollOffset,
|
|
266
|
+
onSetChatDetailScrollOffset,
|
|
267
|
+
selectSpan,
|
|
268
|
+
}: TraceWorkspaceProps) => {
|
|
269
|
+
const aiDrillIn = selectedSpan !== null && isAiSpan(selectedSpan.tags)
|
|
270
|
+
const {
|
|
271
|
+
contentWidth,
|
|
272
|
+
headerFooterWidth,
|
|
273
|
+
isWideLayout,
|
|
274
|
+
leftPaneWidth,
|
|
275
|
+
rightPaneWidth,
|
|
276
|
+
leftContentWidth,
|
|
277
|
+
rightContentWidth,
|
|
278
|
+
sectionPadding,
|
|
279
|
+
wideBodyHeight,
|
|
280
|
+
wideBodyLines,
|
|
281
|
+
narrowListHeight,
|
|
282
|
+
narrowBodyLines,
|
|
283
|
+
narrowFullBodyLines,
|
|
284
|
+
wideTraceListBodyHeight,
|
|
285
|
+
narrowTraceListBodyHeight,
|
|
286
|
+
availableContentHeight,
|
|
287
|
+
} = layout
|
|
288
|
+
const traceDetailsProps: SharedTraceDetailsProps = {
|
|
289
|
+
trace: selectedTrace,
|
|
290
|
+
traceSummary: selectedTraceSummary,
|
|
291
|
+
traceStatus: traceDetailState.status,
|
|
292
|
+
traceError: traceDetailState.error,
|
|
293
|
+
traceLogCount: logState.data.length,
|
|
294
|
+
selectedSpanIndex,
|
|
295
|
+
collapsedSpanIds,
|
|
296
|
+
waterfallFilterMode,
|
|
297
|
+
waterfallFilterText,
|
|
298
|
+
onSelectSpan: selectSpan,
|
|
299
|
+
}
|
|
300
|
+
const drillInContentWidth = Math.max(24, contentWidth - 2)
|
|
301
|
+
|
|
302
|
+
if (detailView === "service-logs") {
|
|
303
|
+
return (
|
|
304
|
+
<ServiceLogsScene
|
|
305
|
+
selectedTraceService={selectedTraceService}
|
|
306
|
+
serviceLogState={serviceLogState}
|
|
307
|
+
selectedServiceLogIndex={selectedServiceLogIndex}
|
|
308
|
+
setSelectedServiceLogIndex={setSelectedServiceLogIndex}
|
|
309
|
+
headerFooterWidth={headerFooterWidth}
|
|
310
|
+
availableContentHeight={availableContentHeight}
|
|
311
|
+
/>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (isWideLayout) {
|
|
316
|
+
if (viewLevel === 0) {
|
|
317
|
+
return (
|
|
318
|
+
<box flexGrow={1} flexDirection="row">
|
|
319
|
+
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
320
|
+
<TraceListPane
|
|
321
|
+
traceListProps={traceListProps}
|
|
322
|
+
filterMode={filterMode}
|
|
323
|
+
filterText={filterText}
|
|
324
|
+
filterWidth={leftContentWidth}
|
|
325
|
+
containerHeight={wideBodyHeight}
|
|
326
|
+
bodyHeight={wideTraceListBodyHeight}
|
|
327
|
+
padding={sectionPadding}
|
|
328
|
+
/>
|
|
329
|
+
</box>
|
|
330
|
+
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorJunctionChars} />
|
|
331
|
+
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
332
|
+
<TraceDetailsScene
|
|
333
|
+
{...traceDetailsProps}
|
|
334
|
+
contentWidth={rightContentWidth}
|
|
335
|
+
bodyLines={wideBodyLines}
|
|
336
|
+
paneWidth={rightPaneWidth}
|
|
337
|
+
/>
|
|
338
|
+
</box>
|
|
339
|
+
</box>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (viewLevel === 1) {
|
|
344
|
+
return (
|
|
345
|
+
<box flexGrow={1} flexDirection="row">
|
|
346
|
+
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
347
|
+
<TraceDetailsScene
|
|
348
|
+
{...traceDetailsProps}
|
|
349
|
+
contentWidth={leftContentWidth}
|
|
350
|
+
bodyLines={wideBodyLines}
|
|
351
|
+
paneWidth={leftPaneWidth}
|
|
352
|
+
/>
|
|
353
|
+
</box>
|
|
354
|
+
<SeparatorColumn height={wideBodyHeight} junctionChars={separatorCrossChars} />
|
|
355
|
+
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
356
|
+
<SpanDetailPane
|
|
357
|
+
span={selectedSpan}
|
|
358
|
+
trace={selectedTrace}
|
|
359
|
+
logs={selectedSpanLogs}
|
|
360
|
+
contentWidth={rightContentWidth}
|
|
361
|
+
bodyLines={wideBodyLines}
|
|
362
|
+
paneWidth={rightPaneWidth}
|
|
363
|
+
focused={false}
|
|
364
|
+
/>
|
|
365
|
+
</box>
|
|
366
|
+
</box>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<box flexGrow={1} flexDirection="column">
|
|
372
|
+
<SpanDrillInScene
|
|
373
|
+
aiDrillIn={aiDrillIn}
|
|
374
|
+
selectedSpan={selectedSpan}
|
|
375
|
+
aiCallDetailState={aiCallDetailState}
|
|
376
|
+
aiChatChunks={aiChatChunks}
|
|
377
|
+
selectedChatChunkId={selectedChatChunkId}
|
|
378
|
+
onSelectChatChunk={onSelectChatChunk}
|
|
379
|
+
chatDetailChunkId={chatDetailChunkId}
|
|
380
|
+
onOpenChatChunkDetail={onOpenChatChunkDetail}
|
|
381
|
+
onCloseChatChunkDetail={onCloseChatChunkDetail}
|
|
382
|
+
chatDetailScrollOffset={chatDetailScrollOffset}
|
|
383
|
+
onSetChatDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
384
|
+
contentWidth={drillInContentWidth}
|
|
385
|
+
bodyLines={wideBodyLines}
|
|
386
|
+
paneWidth={contentWidth}
|
|
387
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
388
|
+
/>
|
|
389
|
+
</box>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (viewLevel === 0) {
|
|
394
|
+
return (
|
|
395
|
+
<>
|
|
396
|
+
<TraceListPane
|
|
397
|
+
traceListProps={traceListProps}
|
|
398
|
+
filterMode={filterMode}
|
|
399
|
+
filterText={filterText}
|
|
400
|
+
filterWidth={leftContentWidth}
|
|
401
|
+
containerHeight={narrowListHeight}
|
|
402
|
+
bodyHeight={narrowTraceListBodyHeight}
|
|
403
|
+
padding={sectionPadding}
|
|
404
|
+
/>
|
|
405
|
+
<Divider width={contentWidth} />
|
|
406
|
+
<TraceDetailsScene
|
|
407
|
+
{...traceDetailsProps}
|
|
408
|
+
contentWidth={rightContentWidth}
|
|
409
|
+
bodyLines={narrowBodyLines}
|
|
410
|
+
paneWidth={contentWidth}
|
|
411
|
+
/>
|
|
412
|
+
</>
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<>
|
|
418
|
+
<NarrowDrillInHeader
|
|
419
|
+
contentWidth={contentWidth}
|
|
420
|
+
viewLevel={viewLevel}
|
|
421
|
+
selectedTraceSummary={selectedTraceSummary}
|
|
422
|
+
selectedSpan={selectedSpan}
|
|
423
|
+
/>
|
|
424
|
+
{viewLevel === 1 ? (
|
|
425
|
+
<TraceDetailsScene
|
|
426
|
+
{...traceDetailsProps}
|
|
427
|
+
contentWidth={rightContentWidth}
|
|
428
|
+
bodyLines={narrowFullBodyLines}
|
|
429
|
+
paneWidth={contentWidth}
|
|
430
|
+
/>
|
|
431
|
+
) : (
|
|
432
|
+
<SpanDrillInScene
|
|
433
|
+
aiDrillIn={aiDrillIn}
|
|
434
|
+
selectedSpan={selectedSpan}
|
|
435
|
+
aiCallDetailState={aiCallDetailState}
|
|
436
|
+
aiChatChunks={aiChatChunks}
|
|
437
|
+
selectedChatChunkId={selectedChatChunkId}
|
|
438
|
+
onSelectChatChunk={onSelectChatChunk}
|
|
439
|
+
chatDetailChunkId={chatDetailChunkId}
|
|
440
|
+
onOpenChatChunkDetail={onOpenChatChunkDetail}
|
|
441
|
+
onCloseChatChunkDetail={onCloseChatChunkDetail}
|
|
442
|
+
chatDetailScrollOffset={chatDetailScrollOffset}
|
|
443
|
+
onSetChatDetailScrollOffset={onSetChatDetailScrollOffset}
|
|
444
|
+
contentWidth={drillInContentWidth}
|
|
445
|
+
bodyLines={narrowFullBodyLines}
|
|
446
|
+
paneWidth={contentWidth}
|
|
447
|
+
selectedAttrIndex={selectedAttrIndex}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
</>
|
|
451
|
+
)
|
|
452
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { fitCell } from "../format.ts"
|
|
3
|
+
import type { DetailView } from "../atoms.ts"
|
|
4
|
+
|
|
5
|
+
interface UseAppLayoutInput {
|
|
6
|
+
readonly width: number | undefined
|
|
7
|
+
readonly height: number | undefined
|
|
8
|
+
readonly notice: string | null
|
|
9
|
+
readonly detailView: DetailView
|
|
10
|
+
readonly selectedSpanIndex: number | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useAppLayout = ({ width, height, notice, detailView, selectedSpanIndex }: UseAppLayoutInput) => useMemo(() => {
|
|
14
|
+
const contentWidth = Math.max(60, width ?? 100)
|
|
15
|
+
const isWideLayout = (width ?? 100) >= 100
|
|
16
|
+
const splitGap = 1
|
|
17
|
+
const sectionPadding = 1
|
|
18
|
+
const traceListHeaderHeight = 1
|
|
19
|
+
const footerNotice = notice ? fitCell(notice, Math.max(24, contentWidth - 2)) : null
|
|
20
|
+
const footerHeight = 1
|
|
21
|
+
const footerFrameHeight = footerHeight > 0 ? 1 + footerHeight : 0
|
|
22
|
+
const frameHeight = 1 + 1 + footerFrameHeight
|
|
23
|
+
const availableContentHeight = Math.max(10, (height ?? 24) - frameHeight)
|
|
24
|
+
const viewLevelForLayout: 0 | 1 | 2 =
|
|
25
|
+
detailView === "span-detail" ? 2 :
|
|
26
|
+
selectedSpanIndex !== null ? 1 :
|
|
27
|
+
0
|
|
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
|
|
40
|
+
const listHidden = viewLevelForLayout >= 1
|
|
41
|
+
const leftPaneWidth = !isWideLayout
|
|
42
|
+
? contentWidth
|
|
43
|
+
: Math.max(40, Math.floor((contentWidth - splitGap) * splitRatio))
|
|
44
|
+
const rightPaneWidth = !isWideLayout
|
|
45
|
+
? contentWidth
|
|
46
|
+
: Math.max(28, contentWidth - leftPaneWidth - splitGap)
|
|
47
|
+
// Left pane: paddingLeft (1) + scrollbar column (1). No right padding —
|
|
48
|
+
// the vertical pane divider handles visual separation from the right pane.
|
|
49
|
+
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
50
|
+
// Right pane: both left and right padding. Trace details and span detail
|
|
51
|
+
// content needs a little breathing room on the right so long op names
|
|
52
|
+
// and the duration column don't butt against the pane border.
|
|
53
|
+
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
54
|
+
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
55
|
+
const wideBodyHeight = availableContentHeight
|
|
56
|
+
// TraceDetailsPane + SpanDetailPane both reserve 4 rows for their header
|
|
57
|
+
// (title, op line, meta line, divider), so `bodyLines = paneHeight - 4`
|
|
58
|
+
// makes the pane fill its parent exactly. Using `-5` here left a visible
|
|
59
|
+
// blank row between the last waterfall span and the bottom divider.
|
|
60
|
+
const wideBodyLines = Math.max(8, wideBodyHeight - 4)
|
|
61
|
+
const narrowSplitHeight = Math.max(10, availableContentHeight - 1)
|
|
62
|
+
const narrowListHeight = Math.max(4, Math.min(10, Math.floor(narrowSplitHeight * 0.4), narrowSplitHeight - 9))
|
|
63
|
+
const narrowDetailHeight = narrowSplitHeight - narrowListHeight
|
|
64
|
+
const narrowBodyLines = Math.max(2, narrowDetailHeight - 4)
|
|
65
|
+
const narrowFullBodyLines = Math.max(8, availableContentHeight - 6)
|
|
66
|
+
const wideTraceListBodyHeight = Math.max(1, wideBodyHeight - traceListHeaderHeight)
|
|
67
|
+
const narrowTraceListBodyHeight = Math.max(1, narrowListHeight - traceListHeaderHeight)
|
|
68
|
+
const traceViewportRows = isWideLayout ? wideTraceListBodyHeight : narrowTraceListBodyHeight
|
|
69
|
+
const tracePageSize = Math.max(1, traceViewportRows - 1)
|
|
70
|
+
const spanViewportRows = Math.max(1, (isWideLayout ? wideBodyLines : narrowBodyLines) - 1)
|
|
71
|
+
const spanPageSize = Math.max(1, spanViewportRows - 1)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
contentWidth,
|
|
75
|
+
isWideLayout,
|
|
76
|
+
splitGap,
|
|
77
|
+
sectionPadding,
|
|
78
|
+
availableContentHeight,
|
|
79
|
+
viewLevel: viewLevelForLayout,
|
|
80
|
+
listHidden,
|
|
81
|
+
footerNotice,
|
|
82
|
+
footerHeight,
|
|
83
|
+
leftPaneWidth,
|
|
84
|
+
rightPaneWidth,
|
|
85
|
+
leftContentWidth,
|
|
86
|
+
rightContentWidth,
|
|
87
|
+
headerFooterWidth,
|
|
88
|
+
wideBodyHeight,
|
|
89
|
+
wideBodyLines,
|
|
90
|
+
narrowListHeight,
|
|
91
|
+
narrowBodyLines,
|
|
92
|
+
narrowFullBodyLines,
|
|
93
|
+
wideTraceListBodyHeight,
|
|
94
|
+
narrowTraceListBodyHeight,
|
|
95
|
+
traceViewportRows,
|
|
96
|
+
tracePageSize,
|
|
97
|
+
spanPageSize,
|
|
98
|
+
} as const
|
|
99
|
+
}, [detailView, height, notice, selectedSpanIndex, width])
|
|
100
|
+
|
|
101
|
+
export type AppLayout = ReturnType<typeof useAppLayout>
|