@lvce-editor/chat-debug-view 3.4.0 → 4.0.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.
@@ -964,39 +964,82 @@ const {
964
964
  wrapGetter
965
965
  } = create$1();
966
966
 
967
+ const All = 'all';
968
+ const Tools = 'tools';
969
+ const Network = 'network';
970
+ const Ui = 'ui';
971
+ const Stream = 'stream';
972
+ const options = [{
973
+ label: 'All',
974
+ value: All
975
+ }, {
976
+ label: 'Tools',
977
+ value: Tools
978
+ }, {
979
+ label: 'Network',
980
+ value: Network
981
+ }, {
982
+ label: 'UI',
983
+ value: Ui
984
+ }, {
985
+ label: 'Stream',
986
+ value: Stream
987
+ }];
988
+ const getEventCategoryFilterLabel = eventCategoryFilter => {
989
+ switch (eventCategoryFilter) {
990
+ case Network:
991
+ return 'Network';
992
+ case Stream:
993
+ return 'Stream';
994
+ case Tools:
995
+ return 'Tools';
996
+ case Ui:
997
+ return 'UI';
998
+ default:
999
+ return 'All';
1000
+ }
1001
+ };
1002
+
967
1003
  const createDefaultState = () => {
968
1004
  return {
969
1005
  assetDir: '',
970
1006
  databaseName: 'lvce-chat-view-sessions',
971
1007
  dataBaseVersion: 2,
972
1008
  errorMessage: '',
1009
+ eventCategoryFilter: All,
973
1010
  events: [],
974
1011
  eventStoreName: 'chat-view-events',
975
1012
  filterValue: '',
976
1013
  height: 0,
977
- initial: true,
1014
+ initial: false,
978
1015
  platform: 0,
1016
+ selectedEventIndex: null,
979
1017
  sessionId: '',
980
1018
  sessionIdIndexName: 'sessionId',
981
1019
  showEventStreamFinishedEvents: false,
982
1020
  showInputEvents: false,
983
1021
  showResponsePartEvents: false,
1022
+ timelineEndSeconds: '',
1023
+ timelineStartSeconds: '',
984
1024
  uid: 0,
985
1025
  uri: '',
1026
+ useDevtoolsLayout: false,
986
1027
  width: 0,
987
1028
  x: 0,
988
1029
  y: 0
989
1030
  };
990
1031
  };
991
1032
 
992
- const create = (uid, uri, x, y, width, height, platform, assetDir, sessionId = '', databaseName = 'lvce-chat-view-sessions', dataBaseVersion = 2, eventStoreName = 'chat-view-events', sessionIdIndexName = 'sessionId') => {
1033
+ const create = (uid, uri, x, y, width, height, platform, assetDir, sessionId = '', databaseName = 'lvce-chat-view-sessions', dataBaseVersion = 2, eventStoreName = 'chat-view-events', sessionIdIndexName = 'sessionId', savedState = {}) => {
993
1034
  const state = {
994
1035
  ...createDefaultState(),
1036
+ ...savedState,
995
1037
  assetDir,
996
1038
  databaseName,
997
1039
  dataBaseVersion,
998
1040
  eventStoreName,
999
1041
  height,
1042
+ initial: true,
1000
1043
  platform,
1001
1044
  sessionId,
1002
1045
  sessionIdIndexName,
@@ -1011,13 +1054,11 @@ const create = (uid, uri, x, y, width, height, platform, assetDir, sessionId = '
1011
1054
 
1012
1055
  const RenderItems = 1;
1013
1056
  const RenderCss = 2;
1057
+ const RenderIncremental = 3;
1014
1058
 
1015
1059
  const diff = (oldState, newState) => {
1016
- if (oldState.initial !== newState.initial) {
1017
- return [RenderCss, RenderItems];
1018
- }
1019
- if (oldState.errorMessage !== newState.errorMessage || oldState.events !== newState.events || oldState.filterValue !== newState.filterValue || oldState.sessionId !== newState.sessionId || oldState.showEventStreamFinishedEvents !== newState.showEventStreamFinishedEvents || oldState.showInputEvents !== newState.showInputEvents || oldState.showResponsePartEvents !== newState.showResponsePartEvents || oldState.uid !== newState.uid) {
1020
- return [RenderItems];
1060
+ if (oldState.errorMessage !== newState.errorMessage || oldState.eventCategoryFilter !== newState.eventCategoryFilter || oldState.events !== newState.events || oldState.filterValue !== newState.filterValue || oldState.sessionId !== newState.sessionId || oldState.showEventStreamFinishedEvents !== newState.showEventStreamFinishedEvents || oldState.showInputEvents !== newState.showInputEvents || oldState.showResponsePartEvents !== newState.showResponsePartEvents || oldState.timelineEndSeconds !== newState.timelineEndSeconds || oldState.timelineStartSeconds !== newState.timelineStartSeconds || oldState.useDevtoolsLayout !== newState.useDevtoolsLayout || oldState.selectedEventIndex !== newState.selectedEventIndex || oldState.uid !== newState.uid) {
1061
+ return [RenderIncremental, RenderCss];
1021
1062
  }
1022
1063
  return [];
1023
1064
  };
@@ -1034,35 +1075,388 @@ const getBoolean = value => {
1034
1075
  return value === true || value === 'true' || value === 'on' || value === '1';
1035
1076
  };
1036
1077
 
1078
+ const RE_SPACE = /\s+/;
1079
+ const tokenToEventCategoryFilter = new Map([['@tools', Tools], ['@network', Network], ['@ui', Ui], ['@stream', Stream]]);
1080
+ const parseFilterValue = filterValue => {
1081
+ const normalizedFilter = filterValue.trim().toLowerCase();
1082
+ if (!normalizedFilter) {
1083
+ return {
1084
+ eventCategoryFilter: All,
1085
+ filterText: ''
1086
+ };
1087
+ }
1088
+ const parts = normalizedFilter.split(RE_SPACE);
1089
+ const eventCategoryFilter = parts.map(part => tokenToEventCategoryFilter.get(part)).find(Boolean) || All;
1090
+ const filterText = parts.filter(part => !tokenToEventCategoryFilter.has(part)).join(' ');
1091
+ return {
1092
+ eventCategoryFilter,
1093
+ filterText
1094
+ };
1095
+ };
1096
+
1097
+ const toolEventTypePrefix = 'tool-execution-';
1098
+ const isToolEvent = event => {
1099
+ return event.type.startsWith(toolEventTypePrefix);
1100
+ };
1101
+ const isNetworkEvent = event => {
1102
+ const normalizedType = event.type.toLowerCase();
1103
+ return normalizedType === 'request' || normalizedType === 'response' || normalizedType === 'handle-response' || normalizedType.includes('fetch') || normalizedType.includes('xhr');
1104
+ };
1105
+ const isUiEvent = event => {
1106
+ return event.type.startsWith('handle-') && event.type !== 'handle-response';
1107
+ };
1108
+ const isStreamEvent = event => {
1109
+ return event.type === 'sse-response-part' || event.type === 'event-stream-finished';
1110
+ };
1111
+ const matchesEventCategoryFilter = (event, eventCategoryFilter) => {
1112
+ switch (eventCategoryFilter) {
1113
+ case Network:
1114
+ return isNetworkEvent(event);
1115
+ case Stream:
1116
+ return isStreamEvent(event);
1117
+ case Tools:
1118
+ return isToolEvent(event);
1119
+ case Ui:
1120
+ return isUiEvent(event);
1121
+ default:
1122
+ return true;
1123
+ }
1124
+ };
1125
+ const getVisibleEvents = (events, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents) => {
1126
+ return events.filter(event => {
1127
+ if (!showInputEvents && (event.type === 'handle-input' || event.type === 'handle-submit')) {
1128
+ return false;
1129
+ }
1130
+ if (!showResponsePartEvents && event.type === 'sse-response-part') {
1131
+ return false;
1132
+ }
1133
+ if (!showEventStreamFinishedEvents && event.type === 'event-stream-finished') {
1134
+ return false;
1135
+ }
1136
+ // hide session creation events by default — not useful in the debug view
1137
+ if (event.type === 'chat-session-created') {
1138
+ return false;
1139
+ }
1140
+ return true;
1141
+ });
1142
+ };
1143
+ const getFilteredEvents = (events, filterValue, eventCategoryFilter, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents) => {
1144
+ const visibleEvents = getVisibleEvents(events, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents);
1145
+ const parsedFilter = parseFilterValue(filterValue);
1146
+ const activeEventCategoryFilter = parsedFilter.eventCategoryFilter === All ? eventCategoryFilter : parsedFilter.eventCategoryFilter;
1147
+ const filteredByCategory = visibleEvents.filter(event => matchesEventCategoryFilter(event, activeEventCategoryFilter));
1148
+ const {
1149
+ filterText
1150
+ } = parsedFilter;
1151
+ if (!filterText) {
1152
+ return filteredByCategory;
1153
+ }
1154
+ return filteredByCategory.filter(event => JSON.stringify(event).toLowerCase().includes(filterText));
1155
+ };
1156
+
1157
+ const toTimeNumber = value => {
1158
+ if (typeof value === 'number' && Number.isFinite(value)) {
1159
+ return value;
1160
+ }
1161
+ if (typeof value === 'string') {
1162
+ const timestamp = Date.parse(value);
1163
+ if (!Number.isNaN(timestamp)) {
1164
+ return timestamp;
1165
+ }
1166
+ }
1167
+ return undefined;
1168
+ };
1169
+
1170
+ const getEventTime = event => {
1171
+ return toTimeNumber(event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp);
1172
+ };
1173
+
1174
+ const maxBarUnits = 8;
1175
+ const parseTimelineSeconds = value => {
1176
+ const trimmed = value.trim();
1177
+ if (!trimmed) {
1178
+ return undefined;
1179
+ }
1180
+ const parsed = Number.parseFloat(trimmed);
1181
+ if (!Number.isFinite(parsed) || parsed < 0) {
1182
+ return undefined;
1183
+ }
1184
+ return parsed;
1185
+ };
1186
+ const roundSeconds = value => {
1187
+ return Number(value.toFixed(3));
1188
+ };
1189
+ const getEventsWithTime = events => {
1190
+ return events.flatMap(event => {
1191
+ const time = getEventTime(event);
1192
+ if (time === undefined) {
1193
+ return [];
1194
+ }
1195
+ return [{
1196
+ event,
1197
+ time
1198
+ }];
1199
+ });
1200
+ };
1201
+ const getNormalizedRange = (durationSeconds, startValue, endValue) => {
1202
+ const parsedStart = parseTimelineSeconds(startValue);
1203
+ const parsedEnd = parseTimelineSeconds(endValue);
1204
+ if (parsedStart === undefined && parsedEnd === undefined) {
1205
+ return {
1206
+ endSeconds: null,
1207
+ hasSelection: false,
1208
+ startSeconds: null
1209
+ };
1210
+ }
1211
+ const rawStart = parsedStart ?? 0;
1212
+ const rawEnd = parsedEnd ?? durationSeconds;
1213
+ const normalizedStart = Math.max(0, Math.min(durationSeconds, Math.min(rawStart, rawEnd)));
1214
+ const normalizedEnd = Math.max(0, Math.min(durationSeconds, Math.max(rawStart, rawEnd)));
1215
+ return {
1216
+ endSeconds: roundSeconds(normalizedEnd),
1217
+ hasSelection: true,
1218
+ startSeconds: roundSeconds(normalizedStart)
1219
+ };
1220
+ };
1221
+ const filterEventsByTimelineRange = (events, startValue, endValue) => {
1222
+ const eventsWithTime = getEventsWithTime(events);
1223
+ if (eventsWithTime.length === 0) {
1224
+ return events;
1225
+ }
1226
+ const baseTime = eventsWithTime[0].time;
1227
+ const lastTime = eventsWithTime.at(-1)?.time ?? baseTime;
1228
+ const durationSeconds = roundSeconds(Math.max(0, lastTime - baseTime) / 1000);
1229
+ const range = getNormalizedRange(durationSeconds, startValue, endValue);
1230
+ if (!range.hasSelection || range.startSeconds === null || range.endSeconds === null) {
1231
+ return events;
1232
+ }
1233
+ const startTime = baseTime + range.startSeconds * 1000;
1234
+ const endTime = baseTime + range.endSeconds * 1000;
1235
+ return eventsWithTime.filter(item => item.time >= startTime && item.time <= endTime).map(item => item.event);
1236
+ };
1237
+ const getTimelineInfo = (events, startValue, endValue) => {
1238
+ const eventsWithTime = getEventsWithTime(events);
1239
+ if (eventsWithTime.length === 0) {
1240
+ return {
1241
+ buckets: [],
1242
+ durationSeconds: 0,
1243
+ endSeconds: null,
1244
+ hasSelection: false,
1245
+ startSeconds: null
1246
+ };
1247
+ }
1248
+ const baseTime = eventsWithTime[0].time;
1249
+ const lastTime = eventsWithTime.at(-1)?.time ?? baseTime;
1250
+ const durationMs = Math.max(0, lastTime - baseTime);
1251
+ const durationSeconds = roundSeconds(durationMs / 1000);
1252
+ const range = getNormalizedRange(durationSeconds, startValue, endValue);
1253
+ const bucketCount = durationSeconds === 0 ? 1 : Math.max(12, Math.min(48, Math.ceil(durationSeconds)));
1254
+ const bucketDurationMs = durationMs === 0 ? 1000 : durationMs / bucketCount;
1255
+ const counts = Array.from({
1256
+ length: bucketCount
1257
+ }).fill(0);
1258
+ for (const item of eventsWithTime) {
1259
+ const offsetMs = item.time - baseTime;
1260
+ const index = durationMs === 0 ? 0 : Math.min(bucketCount - 1, Math.floor(offsetMs / durationMs * bucketCount));
1261
+ counts[index] += 1;
1262
+ }
1263
+ const maxCount = Math.max(...counts);
1264
+ const buckets = counts.map((count, index) => {
1265
+ const bucketStartMs = index * bucketDurationMs;
1266
+ const bucketEndMs = index === bucketCount - 1 ? durationMs : (index + 1) * bucketDurationMs;
1267
+ const hasSelection = range.hasSelection && range.startSeconds !== null && range.endSeconds !== null;
1268
+ const selectionStartMs = hasSelection ? range.startSeconds * 1000 : 0;
1269
+ const selectionEndMs = hasSelection ? range.endSeconds * 1000 : 0;
1270
+ return {
1271
+ count,
1272
+ endSeconds: roundSeconds(bucketEndMs / 1000),
1273
+ isSelected: hasSelection && bucketEndMs >= selectionStartMs && bucketStartMs <= selectionEndMs,
1274
+ startSeconds: roundSeconds(bucketStartMs / 1000),
1275
+ unitCount: count === 0 ? 0 : Math.max(1, Math.round(count / maxCount * maxBarUnits))
1276
+ };
1277
+ });
1278
+ return {
1279
+ buckets,
1280
+ durationSeconds,
1281
+ endSeconds: range.endSeconds,
1282
+ hasSelection: range.hasSelection,
1283
+ startSeconds: range.startSeconds
1284
+ };
1285
+ };
1286
+
1037
1287
  const Filter = 'filter';
1288
+ const EventCategoryFilter = 'eventCategoryFilter';
1038
1289
  const ShowEventStreamFinishedEvents = 'showEventStreamFinishedEvents';
1039
1290
  const ShowInputEvents = 'showInputEvents';
1040
1291
  const ShowResponsePartEvents = 'showResponsePartEvents';
1041
-
1292
+ const UseDevtoolsLayout = 'useDevtoolsLayout';
1293
+ const SelectedEventIndex = 'selectedEventIndex';
1294
+ const CloseDetails = 'closeDetails';
1295
+ const TimelineStartSeconds = 'timelineStartSeconds';
1296
+ const TimelineEndSeconds = 'timelineEndSeconds';
1297
+ const TimelineRangePreset = 'timelineRangePreset';
1298
+
1299
+ const getCurrentEvents = state => {
1300
+ const filteredEvents = getFilteredEvents(state.events, state.filterValue, state.eventCategoryFilter, state.showInputEvents, state.showResponsePartEvents, state.showEventStreamFinishedEvents);
1301
+ return filterEventsByTimelineRange(filteredEvents, state.timelineStartSeconds, state.timelineEndSeconds);
1302
+ };
1303
+ const parseTimelineRangePreset = value => {
1304
+ if (!value) {
1305
+ return {
1306
+ timelineEndSeconds: '',
1307
+ timelineStartSeconds: ''
1308
+ };
1309
+ }
1310
+ const [timelineStartSeconds = '', timelineEndSeconds = ''] = value.split(':', 2);
1311
+ return {
1312
+ timelineEndSeconds,
1313
+ timelineStartSeconds
1314
+ };
1315
+ };
1316
+ const getSelectedEventIndex = state => {
1317
+ const {
1318
+ selectedEventIndex
1319
+ } = state;
1320
+ if (selectedEventIndex === null) {
1321
+ return null;
1322
+ }
1323
+ const filteredEvents = getCurrentEvents(state);
1324
+ const selectedEvent = filteredEvents[selectedEventIndex];
1325
+ if (!selectedEvent) {
1326
+ return null;
1327
+ }
1328
+ const newIndex = filteredEvents.indexOf(selectedEvent);
1329
+ if (newIndex === -1) {
1330
+ return null;
1331
+ }
1332
+ return newIndex;
1333
+ };
1334
+ const getPreservedSelectedEventIndex = (oldState, newState) => {
1335
+ const {
1336
+ selectedEventIndex
1337
+ } = oldState;
1338
+ if (selectedEventIndex === null) {
1339
+ return null;
1340
+ }
1341
+ const oldFilteredEvents = getCurrentEvents(oldState);
1342
+ const selectedEvent = oldFilteredEvents[selectedEventIndex];
1343
+ if (!selectedEvent) {
1344
+ return null;
1345
+ }
1346
+ const newFilteredEvents = getCurrentEvents(newState);
1347
+ const newIndex = newFilteredEvents.indexOf(selectedEvent);
1348
+ if (newIndex === -1) {
1349
+ return null;
1350
+ }
1351
+ return newIndex;
1352
+ };
1353
+ const parseSelectedEventIndex = value => {
1354
+ const parsed = Number.parseInt(value, 10);
1355
+ if (Number.isNaN(parsed) || parsed < 0) {
1356
+ return null;
1357
+ }
1358
+ return parsed;
1359
+ };
1042
1360
  const handleInput = (state, name, value, checked) => {
1043
1361
  if (name === Filter) {
1044
- return {
1362
+ const nextState = {
1045
1363
  ...state,
1046
1364
  filterValue: value
1047
1365
  };
1366
+ return {
1367
+ ...nextState,
1368
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1369
+ };
1048
1370
  }
1049
- if (name === ShowEventStreamFinishedEvents) {
1371
+ if (name === EventCategoryFilter) {
1372
+ const nextState = {
1373
+ ...state,
1374
+ eventCategoryFilter: value || All
1375
+ };
1050
1376
  return {
1377
+ ...nextState,
1378
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1379
+ };
1380
+ }
1381
+ if (name === ShowEventStreamFinishedEvents) {
1382
+ const nextState = {
1051
1383
  ...state,
1052
1384
  showEventStreamFinishedEvents: getBoolean(checked)
1053
1385
  };
1386
+ return {
1387
+ ...nextState,
1388
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1389
+ };
1054
1390
  }
1055
1391
  if (name === ShowInputEvents) {
1056
- return {
1392
+ const nextState = {
1057
1393
  ...state,
1058
1394
  showInputEvents: getBoolean(checked)
1059
1395
  };
1396
+ return {
1397
+ ...nextState,
1398
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1399
+ };
1060
1400
  }
1061
1401
  if (name === ShowResponsePartEvents) {
1062
- return {
1402
+ const nextState = {
1063
1403
  ...state,
1064
1404
  showResponsePartEvents: getBoolean(checked)
1065
1405
  };
1406
+ return {
1407
+ ...nextState,
1408
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1409
+ };
1410
+ }
1411
+ if (name === UseDevtoolsLayout) {
1412
+ const useDevtoolsLayout = getBoolean(checked);
1413
+ return {
1414
+ ...state,
1415
+ selectedEventIndex: useDevtoolsLayout ? getSelectedEventIndex(state) : null,
1416
+ useDevtoolsLayout
1417
+ };
1418
+ }
1419
+ if (name === SelectedEventIndex) {
1420
+ return {
1421
+ ...state,
1422
+ selectedEventIndex: parseSelectedEventIndex(value)
1423
+ };
1424
+ }
1425
+ if (name === TimelineStartSeconds) {
1426
+ const nextState = {
1427
+ ...state,
1428
+ timelineStartSeconds: value
1429
+ };
1430
+ return {
1431
+ ...nextState,
1432
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1433
+ };
1434
+ }
1435
+ if (name === TimelineEndSeconds) {
1436
+ const nextState = {
1437
+ ...state,
1438
+ timelineEndSeconds: value
1439
+ };
1440
+ return {
1441
+ ...nextState,
1442
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1443
+ };
1444
+ }
1445
+ if (name === TimelineRangePreset) {
1446
+ const nextState = {
1447
+ ...state,
1448
+ ...parseTimelineRangePreset(value)
1449
+ };
1450
+ return {
1451
+ ...nextState,
1452
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1453
+ };
1454
+ }
1455
+ if (name === CloseDetails) {
1456
+ return {
1457
+ ...state,
1458
+ selectedEventIndex: null
1459
+ };
1066
1460
  }
1067
1461
  return state;
1068
1462
  };
@@ -1448,6 +1842,7 @@ const loadContent = async state => {
1448
1842
  errorMessage: getInvalidUriMessage(uri, parsed.code),
1449
1843
  events: [],
1450
1844
  initial: false,
1845
+ selectedEventIndex: null,
1451
1846
  sessionId: ''
1452
1847
  };
1453
1848
  }
@@ -1462,6 +1857,7 @@ const loadContent = async state => {
1462
1857
  errorMessage: getSessionNotFoundMessage(sessionId),
1463
1858
  events: [],
1464
1859
  initial: false,
1860
+ selectedEventIndex: null,
1465
1861
  sessionId
1466
1862
  };
1467
1863
  }
@@ -1470,6 +1866,7 @@ const loadContent = async state => {
1470
1866
  errorMessage: '',
1471
1867
  events,
1472
1868
  initial: false,
1869
+ selectedEventIndex: null,
1473
1870
  sessionId
1474
1871
  };
1475
1872
  } catch {
@@ -1478,6 +1875,7 @@ const loadContent = async state => {
1478
1875
  errorMessage: getFailedToLoadMessage(sessionId),
1479
1876
  events: [],
1480
1877
  initial: false,
1878
+ selectedEventIndex: null,
1481
1879
  sessionId
1482
1880
  };
1483
1881
  }
@@ -1489,7 +1887,8 @@ const refresh = async state => {
1489
1887
  ...state,
1490
1888
  errorMessage: '',
1491
1889
  events,
1492
- initial: false
1890
+ initial: false,
1891
+ selectedEventIndex: null
1493
1892
  };
1494
1893
  };
1495
1894
 
@@ -1499,6 +1898,7 @@ const TargetValue = 'event.target.value';
1499
1898
 
1500
1899
  const SetCss = 'Viewlet.setCss';
1501
1900
  const SetDom2 = 'Viewlet.setDom2';
1901
+ const SetPatches = 'Viewlet.setPatches';
1502
1902
 
1503
1903
  const getCss = () => {
1504
1904
  return `
@@ -1511,10 +1911,15 @@ const getCss = () => {
1511
1911
  gap: 8px;
1512
1912
  }
1513
1913
 
1914
+ .ChatDebugView--devtools {
1915
+ gap: 4px;
1916
+ }
1917
+
1514
1918
  .ChatDebugViewTop {
1515
1919
  display: flex;
1516
1920
  align-items: center;
1517
1921
  gap: 12px;
1922
+ flex-wrap: wrap;
1518
1923
  }
1519
1924
 
1520
1925
  .ChatDebugViewTop .InputBox {
@@ -1539,107 +1944,406 @@ const getCss = () => {
1539
1944
  opacity: 0.8;
1540
1945
  }
1541
1946
 
1947
+ .ChatDebugViewQuickFilters {
1948
+ display: flex;
1949
+ gap: 8px;
1950
+ flex-wrap: wrap;
1951
+ }
1952
+
1953
+ .ChatDebugViewQuickFilterPill {
1954
+ display: inline-flex;
1955
+ align-items: center;
1956
+ justify-content: center;
1957
+ min-height: 28px;
1958
+ padding: 0 12px;
1959
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
1960
+ border-radius: 999px;
1961
+ background: var(--vscode-editorWidget-background, transparent);
1962
+ cursor: pointer;
1963
+ font-size: 12px;
1964
+ line-height: 1;
1965
+ }
1966
+
1967
+ .ChatDebugViewQuickFilterPillSelected {
1968
+ border-color: var(--vscode-focusBorder, #007fd4);
1969
+ background: var(--vscode-list-activeSelectionBackground, rgba(14, 99, 156, 0.35));
1970
+ color: var(--vscode-list-activeSelectionForeground, inherit);
1971
+ }
1972
+
1973
+ .ChatDebugViewQuickFilterInput {
1974
+ position: absolute;
1975
+ opacity: 0;
1976
+ pointer-events: none;
1977
+ }
1978
+
1542
1979
  .ChatDebugViewEvents {
1980
+ display: grid;
1981
+ grid-template-rows: auto minmax(0, 1fr);
1543
1982
  overflow: auto;
1983
+ min-width: 0;
1984
+ min-height: 0;
1544
1985
  scrollbar-width: thin;
1545
1986
  scrollbar-color: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, 0.4)) transparent;
1546
1987
  }
1547
1988
 
1548
- .ChatDebugViewEvents::-webkit-scrollbar {
1549
- width: 10px;
1550
- height: 10px;
1989
+ .ChatDebugViewEvents--timeline {
1990
+ grid-template-rows: auto auto minmax(0, 1fr);
1551
1991
  }
1552
1992
 
1553
- .ChatDebugViewEvents::-webkit-scrollbar-track {
1554
- background: transparent;
1993
+ .ChatDebugView--devtools .ChatDebugViewEvents {
1994
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
1995
+ border-radius: 6px;
1996
+ margin-bottom: 0;
1555
1997
  }
1556
1998
 
1557
- .ChatDebugViewEvents::-webkit-scrollbar-thumb {
1558
- background: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, 0.4));
1559
- border-radius: 999px;
1560
- border: 2px solid transparent;
1561
- background-clip: content-box;
1999
+ .ChatDebugViewEventsFullWidth {
2000
+ grid-column: 1 / -1;
1562
2001
  }
1563
2002
 
1564
- .ChatDebugViewEvents::-webkit-scrollbar-thumb:hover {
1565
- background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, 0.7));
2003
+ .ChatDebugViewDevtoolsMain {
2004
+ display: grid;
2005
+ grid-template-columns: minmax(0, 1fr) minmax(320px, 420px);
2006
+ gap: 8px;
2007
+ min-width: 0;
2008
+ min-height: 0;
2009
+ overflow: hidden;
1566
2010
  }
1567
2011
 
1568
- .ChatDebugViewEvents::-webkit-scrollbar-thumb:active {
1569
- background: var(--vscode-scrollbarSlider-activeBackground, rgba(191, 191, 191, 0.4));
2012
+ .ChatDebugViewTimeline {
2013
+ display: grid;
2014
+ gap: 8px;
2015
+ padding: 10px;
2016
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2017
+ background: color-mix(in srgb, var(--vscode-editorWidget-background, transparent) 82%, var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.12)) 18%);
1570
2018
  }
1571
2019
 
1572
- .ChatDebugViewEvent {
1573
- margin: 0;
1574
- padding: 8px;
1575
- border: 1px solid var(--vscode-editorWidget-border, #454545);
1576
- border-radius: 6px;
1577
- margin-bottom: 8px;
1578
- white-space: pre-wrap;
1579
- word-break: break-word;
1580
- font-family: var(--vscode-editor-font-family, monospace);
2020
+ .ChatDebugViewTimelineTop {
2021
+ display: flex;
2022
+ align-items: baseline;
2023
+ justify-content: space-between;
2024
+ gap: 8px;
2025
+ flex-wrap: wrap;
2026
+ }
2027
+
2028
+ .ChatDebugViewTimelineTitle {
1581
2029
  font-size: 12px;
1582
- user-select: text;
2030
+ font-weight: 600;
1583
2031
  }
1584
2032
 
1585
- .ChatDebugViewEmpty {
2033
+ .ChatDebugViewTimelineSummary {
2034
+ font-size: 12px;
1586
2035
  opacity: 0.8;
1587
2036
  }
1588
2037
 
1589
- .ChatDebugViewError {
1590
- color: var(--vscode-errorForeground, #f14c4c);
1591
- white-space: normal;
2038
+ .ChatDebugViewTimelineControls {
2039
+ display: flex;
2040
+ align-items: center;
2041
+ gap: 8px;
2042
+ flex-wrap: wrap;
1592
2043
  }
1593
2044
 
1594
- .TokenText {
1595
- color: var(--vscode-editor-foreground, inherit);
2045
+ .ChatDebugViewTimelineField {
2046
+ display: inline-flex;
2047
+ align-items: center;
2048
+ gap: 6px;
2049
+ font-size: 12px;
1596
2050
  }
1597
2051
 
1598
- .TokenKey {
1599
- color: var(--vscode-symbolIcon-propertyForeground, var(--vscode-editor-foreground, inherit));
2052
+ .ChatDebugViewTimelineInput {
2053
+ width: 84px;
1600
2054
  }
1601
2055
 
1602
- .TokenString {
1603
- color: var(--vscode-debugTokenExpression-string, var(--vscode-charts-green, #89d185));
2056
+ .ChatDebugViewTimelineReset {
2057
+ display: inline-flex;
2058
+ align-items: center;
2059
+ justify-content: center;
2060
+ min-height: 28px;
2061
+ padding: 0 12px;
2062
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2063
+ border-radius: 999px;
2064
+ cursor: pointer;
2065
+ font-size: 12px;
1604
2066
  }
1605
2067
 
1606
- .TokenNumeric {
1607
- color: var(--vscode-debugTokenExpression-number, var(--vscode-charts-blue, #75beff));
2068
+ .ChatDebugViewTimelineResetSelected {
2069
+ border-color: var(--vscode-focusBorder, #007fd4);
2070
+ background: var(--vscode-list-activeSelectionBackground, rgba(14, 99, 156, 0.35));
2071
+ color: var(--vscode-list-activeSelectionForeground, inherit);
1608
2072
  }
1609
2073
 
1610
- .TokenBoolean {
1611
- color: var(--vscode-debugTokenExpression-boolean, var(--vscode-charts-yellow, #dcdcaa));
2074
+ .ChatDebugViewTimelineBuckets {
2075
+ display: grid;
2076
+ grid-template-columns: repeat(auto-fit, minmax(10px, 1fr));
2077
+ align-items: end;
2078
+ gap: 4px;
2079
+ min-height: 60px;
1612
2080
  }
1613
2081
 
1614
- .ChatOrderedList{
1615
- margin:0;
1616
- padding:0;
1617
- padding-left:10px;
2082
+ .ChatDebugViewTimelineBucket {
2083
+ display: flex;
2084
+ align-items: stretch;
2085
+ min-height: 60px;
2086
+ cursor: pointer;
1618
2087
  }
1619
2088
 
1620
- .ChatOrderedListItem{
1621
- margin:0;
1622
- padding:0;
2089
+ .ChatDebugViewTimelinePresetInput {
2090
+ position: absolute;
2091
+ opacity: 0;
2092
+ pointer-events: none;
1623
2093
  }
1624
2094
 
1625
- .ChatToolCalls{
1626
- margin:0;
1627
- padding:0;
2095
+ .ChatDebugViewTimelineBucketBar {
2096
+ width: 100%;
2097
+ display: flex;
2098
+ flex-direction: column;
2099
+ justify-content: flex-end;
2100
+ gap: 2px;
2101
+ padding: 4px 2px;
2102
+ border: 1px solid transparent;
2103
+ border-radius: 4px;
2104
+ background: color-mix(in srgb, var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.18)) 68%, transparent 32%);
1628
2105
  }
1629
- `;
1630
- };
1631
2106
 
1632
- const renderCss = (oldState, newState) => {
1633
- const css = getCss();
1634
- return [SetCss, newState.uid, css];
1635
- };
2107
+ .ChatDebugViewTimelineBucketSelected .ChatDebugViewTimelineBucketBar,
2108
+ .ChatDebugViewTimelineBucketBarSelected {
2109
+ background: color-mix(in srgb, var(--vscode-charts-blue, #75beff) 72%, transparent 28%);
2110
+ border-color: var(--vscode-focusBorder, #007fd4);
2111
+ }
1636
2112
 
1637
- const Div = 4;
1638
- const Input = 6;
1639
- const Span = 8;
1640
- const Text = 12;
2113
+ .ChatDebugViewTimelineBucketUnit {
2114
+ width: 100%;
2115
+ height: 4px;
2116
+ border-radius: 999px;
2117
+ background: var(--vscode-charts-blue, #75beff);
2118
+ }
2119
+
2120
+ .ChatDebugViewTimelineBucketUnitEmpty {
2121
+ opacity: 0.35;
2122
+ background: var(--vscode-editorWidget-border, #454545);
2123
+ }
2124
+
2125
+ .ChatDebugViewTableHeader,
2126
+ .ChatDebugViewEventRow {
2127
+ display: grid;
2128
+ grid-template-columns: minmax(140px, 1fr) minmax(180px, 1fr) minmax(180px, 1fr) 90px 64px;
2129
+ align-items: center;
2130
+ gap: 8px;
2131
+ }
2132
+
2133
+ .ChatDebugViewTableHeader {
2134
+ padding: 8px;
2135
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2136
+ background: var(--vscode-editorWidget-background, transparent);
2137
+ position: sticky;
2138
+ top: 0;
2139
+ z-index: 1;
2140
+ }
2141
+
2142
+ .ChatDebugViewHeaderCell {
2143
+ font-size: 11px;
2144
+ text-transform: uppercase;
2145
+ letter-spacing: 0.04em;
2146
+ opacity: 0.8;
2147
+ }
2148
+
2149
+ .ChatDebugViewTableBody {
2150
+ overflow: auto;
2151
+ min-height: 0;
2152
+ }
2153
+
2154
+ .ChatDebugViewEventRowLabel {
2155
+ display: block;
2156
+ }
2157
+
2158
+ .ChatDebugViewEventRowLabelSelected .ChatDebugViewEventRow,
2159
+ .ChatDebugViewEventRowSelected {
2160
+ background: var(--vscode-list-activeSelectionBackground, rgba(14, 99, 156, 0.35));
2161
+ color: var(--vscode-list-activeSelectionForeground, inherit);
2162
+ }
2163
+
2164
+ .ChatDebugViewEventRowInput {
2165
+ position: absolute;
2166
+ opacity: 0;
2167
+ pointer-events: none;
2168
+ }
2169
+
2170
+ .ChatDebugViewEventRow {
2171
+ padding: 8px;
2172
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2173
+ cursor: pointer;
2174
+ }
2175
+
2176
+ .ChatDebugViewEventRow:hover {
2177
+ background: var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.31));
2178
+ }
2179
+
2180
+ .ChatDebugViewCell {
2181
+ overflow: hidden;
2182
+ text-overflow: ellipsis;
2183
+ white-space: nowrap;
2184
+ }
2185
+
2186
+ .ChatDebugViewCellDuration {
2187
+ text-align: right;
2188
+ }
2189
+
2190
+ .ChatDebugViewCellStatus {
2191
+ text-align: right;
2192
+ }
2193
+
2194
+ .ChatDebugViewDetails {
2195
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2196
+ border-radius: 6px;
2197
+ overflow: hidden;
2198
+ min-width: 0;
2199
+ min-height: 0;
2200
+ display: grid;
2201
+ grid-template-rows: auto 1fr;
2202
+ }
2203
+
2204
+ .ChatDebugViewDetailsTop {
2205
+ display: flex;
2206
+ align-items: center;
2207
+ justify-content: space-between;
2208
+ padding: 8px;
2209
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2210
+ }
2211
+
2212
+ .ChatDebugViewDetailsTitle {
2213
+ font-size: 12px;
2214
+ font-weight: 600;
2215
+ }
2216
+
2217
+ .ChatDebugViewDetailsClose {
2218
+ width: 18px;
2219
+ height: 18px;
2220
+ appearance: none;
2221
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2222
+ border-radius: 4px;
2223
+ cursor: pointer;
2224
+ position: relative;
2225
+ }
2226
+
2227
+ .ChatDebugViewDetailsClose::before,
2228
+ .ChatDebugViewDetailsClose::after {
2229
+ content: '';
2230
+ position: absolute;
2231
+ left: 50%;
2232
+ top: 50%;
2233
+ width: 10px;
2234
+ height: 1px;
2235
+ background: currentColor;
2236
+ }
2237
+
2238
+ .ChatDebugViewDetailsClose::before {
2239
+ transform: translate(-50%, -50%) rotate(45deg);
2240
+ }
2241
+
2242
+ .ChatDebugViewDetailsClose::after {
2243
+ transform: translate(-50%, -50%) rotate(-45deg);
2244
+ }
2245
+
2246
+ .ChatDebugViewDetailsBody {
2247
+ overflow: auto;
2248
+ padding: 8px;
2249
+ }
2250
+
2251
+ .ChatDebugViewEvents::-webkit-scrollbar {
2252
+ width: 10px;
2253
+ height: 10px;
2254
+ }
2255
+
2256
+ .ChatDebugViewEvents::-webkit-scrollbar-track {
2257
+ background: transparent;
2258
+ }
2259
+
2260
+ .ChatDebugViewEvents::-webkit-scrollbar-thumb {
2261
+ background: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, 0.4));
2262
+ border-radius: 999px;
2263
+ border: 2px solid transparent;
2264
+ background-clip: content-box;
2265
+ }
2266
+
2267
+ .ChatDebugViewEvents::-webkit-scrollbar-thumb:hover {
2268
+ background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, 0.7));
2269
+ }
2270
+
2271
+ .ChatDebugViewEvents::-webkit-scrollbar-thumb:active {
2272
+ background: var(--vscode-scrollbarSlider-activeBackground, rgba(191, 191, 191, 0.4));
2273
+ }
2274
+
2275
+ .ChatDebugViewEvent {
2276
+ margin: 0;
2277
+ padding: 8px;
2278
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2279
+ border-radius: 6px;
2280
+ margin-bottom: 8px;
2281
+ white-space: pre-wrap;
2282
+ word-break: break-word;
2283
+ font-family: var(--vscode-editor-font-family, monospace);
2284
+ font-size: 12px;
2285
+ user-select: text;
2286
+ }
2287
+
2288
+ .ChatDebugViewEmpty {
2289
+ opacity: 0.8;
2290
+ }
2291
+
2292
+ .ChatDebugViewError {
2293
+ color: var(--vscode-errorForeground, #f14c4c);
2294
+ white-space: normal;
2295
+ }
2296
+
2297
+ .TokenText {
2298
+ color: var(--vscode-editor-foreground, inherit);
2299
+ }
2300
+
2301
+ .TokenKey {
2302
+ color: var(--vscode-symbolIcon-propertyForeground, var(--vscode-editor-foreground, inherit));
2303
+ }
2304
+
2305
+ .TokenString {
2306
+ color: var(--vscode-debugTokenExpression-string, var(--vscode-charts-green, #89d185));
2307
+ }
2308
+
2309
+ .TokenNumeric {
2310
+ color: var(--vscode-debugTokenExpression-number, var(--vscode-charts-blue, #75beff));
2311
+ }
2312
+
2313
+ .TokenBoolean {
2314
+ color: var(--vscode-debugTokenExpression-boolean, var(--vscode-charts-yellow, #dcdcaa));
2315
+ }
2316
+
2317
+ .ChatOrderedList{
2318
+ margin:0;
2319
+ padding:0;
2320
+ padding-left:10px;
2321
+ }
2322
+
2323
+ .ChatOrderedListItem{
2324
+ margin:0;
2325
+ padding:0;
2326
+ }
2327
+
2328
+ .ChatToolCalls{
2329
+ margin:0;
2330
+ padding:0;
2331
+ }
2332
+ `;
2333
+ };
2334
+
2335
+ const renderCss = (oldState, newState) => {
2336
+ const css = getCss();
2337
+ return [SetCss, newState.uid, css];
2338
+ };
2339
+
2340
+ const Div = 4;
2341
+ const Input = 6;
2342
+ const Span = 8;
2343
+ const Text = 12;
1641
2344
  const Pre = 51;
1642
2345
  const Label = 66;
2346
+ const Reference = 100;
1643
2347
 
1644
2348
  const text = data => {
1645
2349
  return {
@@ -1649,8 +2353,427 @@ const text = data => {
1649
2353
  };
1650
2354
  };
1651
2355
 
2356
+ const SetText = 1;
2357
+ const Replace = 2;
2358
+ const SetAttribute = 3;
2359
+ const RemoveAttribute = 4;
2360
+ const Add = 6;
2361
+ const NavigateChild = 7;
2362
+ const NavigateParent = 8;
2363
+ const RemoveChild = 9;
2364
+ const NavigateSibling = 10;
2365
+ const SetReferenceNodeUid = 11;
2366
+
2367
+ const isKey = key => {
2368
+ return key !== 'type' && key !== 'childCount';
2369
+ };
2370
+
2371
+ const getKeys = node => {
2372
+ const keys = Object.keys(node).filter(isKey);
2373
+ return keys;
2374
+ };
2375
+
2376
+ const arrayToTree = nodes => {
2377
+ const result = [];
2378
+ let i = 0;
2379
+ while (i < nodes.length) {
2380
+ const node = nodes[i];
2381
+ const {
2382
+ children,
2383
+ nodesConsumed
2384
+ } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
2385
+ result.push({
2386
+ node,
2387
+ children
2388
+ });
2389
+ i += 1 + nodesConsumed;
2390
+ }
2391
+ return result;
2392
+ };
2393
+ const getChildrenWithCount = (nodes, startIndex, childCount) => {
2394
+ if (childCount === 0) {
2395
+ return {
2396
+ children: [],
2397
+ nodesConsumed: 0
2398
+ };
2399
+ }
2400
+ const children = [];
2401
+ let i = startIndex;
2402
+ let remaining = childCount;
2403
+ let totalConsumed = 0;
2404
+ while (remaining > 0 && i < nodes.length) {
2405
+ const node = nodes[i];
2406
+ const nodeChildCount = node.childCount || 0;
2407
+ const {
2408
+ children: nodeChildren,
2409
+ nodesConsumed
2410
+ } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
2411
+ children.push({
2412
+ node,
2413
+ children: nodeChildren
2414
+ });
2415
+ const nodeSize = 1 + nodesConsumed;
2416
+ i += nodeSize;
2417
+ totalConsumed += nodeSize;
2418
+ remaining--;
2419
+ }
2420
+ return {
2421
+ children,
2422
+ nodesConsumed: totalConsumed
2423
+ };
2424
+ };
2425
+
2426
+ const compareNodes = (oldNode, newNode) => {
2427
+ const patches = [];
2428
+ // Check if node type changed - return null to signal incompatible nodes
2429
+ // (caller should handle this with a Replace operation)
2430
+ if (oldNode.type !== newNode.type) {
2431
+ return null;
2432
+ }
2433
+ // Handle reference nodes - special handling for uid changes
2434
+ if (oldNode.type === Reference) {
2435
+ if (oldNode.uid !== newNode.uid) {
2436
+ patches.push({
2437
+ type: SetReferenceNodeUid,
2438
+ uid: newNode.uid
2439
+ });
2440
+ }
2441
+ return patches;
2442
+ }
2443
+ // Handle text nodes
2444
+ if (oldNode.type === Text && newNode.type === Text) {
2445
+ if (oldNode.text !== newNode.text) {
2446
+ patches.push({
2447
+ type: SetText,
2448
+ value: newNode.text
2449
+ });
2450
+ }
2451
+ return patches;
2452
+ }
2453
+ // Compare attributes
2454
+ const oldKeys = getKeys(oldNode);
2455
+ const newKeys = getKeys(newNode);
2456
+ // Check for attribute changes
2457
+ for (const key of newKeys) {
2458
+ if (oldNode[key] !== newNode[key]) {
2459
+ patches.push({
2460
+ type: SetAttribute,
2461
+ key,
2462
+ value: newNode[key]
2463
+ });
2464
+ }
2465
+ }
2466
+ // Check for removed attributes
2467
+ for (const key of oldKeys) {
2468
+ if (!(key in newNode)) {
2469
+ patches.push({
2470
+ type: RemoveAttribute,
2471
+ key
2472
+ });
2473
+ }
2474
+ }
2475
+ return patches;
2476
+ };
2477
+
2478
+ const treeToArray = node => {
2479
+ const result = [node.node];
2480
+ for (const child of node.children) {
2481
+ result.push(...treeToArray(child));
2482
+ }
2483
+ return result;
2484
+ };
2485
+
2486
+ const diffChildren = (oldChildren, newChildren, patches) => {
2487
+ const maxLength = Math.max(oldChildren.length, newChildren.length);
2488
+ // Track where we are: -1 means at parent, >= 0 means at child index
2489
+ let currentChildIndex = -1;
2490
+ // Collect indices of children to remove (we'll add these patches at the end in reverse order)
2491
+ const indicesToRemove = [];
2492
+ for (let i = 0; i < maxLength; i++) {
2493
+ const oldNode = oldChildren[i];
2494
+ const newNode = newChildren[i];
2495
+ if (!oldNode && !newNode) {
2496
+ continue;
2497
+ }
2498
+ if (!oldNode) {
2499
+ // Add new node - we should be at the parent
2500
+ if (currentChildIndex >= 0) {
2501
+ // Navigate back to parent
2502
+ patches.push({
2503
+ type: NavigateParent
2504
+ });
2505
+ currentChildIndex = -1;
2506
+ }
2507
+ // Flatten the entire subtree so renderInternal can handle it
2508
+ const flatNodes = treeToArray(newNode);
2509
+ patches.push({
2510
+ type: Add,
2511
+ nodes: flatNodes
2512
+ });
2513
+ } else if (newNode) {
2514
+ // Compare nodes to see if we need any patches
2515
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2516
+ // If nodePatches is null, the node types are incompatible - need to replace
2517
+ if (nodePatches === null) {
2518
+ // Navigate to this child
2519
+ if (currentChildIndex === -1) {
2520
+ patches.push({
2521
+ type: NavigateChild,
2522
+ index: i
2523
+ });
2524
+ currentChildIndex = i;
2525
+ } else if (currentChildIndex !== i) {
2526
+ patches.push({
2527
+ type: NavigateSibling,
2528
+ index: i
2529
+ });
2530
+ currentChildIndex = i;
2531
+ }
2532
+ // Replace the entire subtree
2533
+ const flatNodes = treeToArray(newNode);
2534
+ patches.push({
2535
+ type: Replace,
2536
+ nodes: flatNodes
2537
+ });
2538
+ // After replace, we're at the new element (same position)
2539
+ continue;
2540
+ }
2541
+ // Check if we need to recurse into children
2542
+ const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
2543
+ // Only navigate to this element if we need to do something
2544
+ if (nodePatches.length > 0 || hasChildrenToCompare) {
2545
+ // Navigate to this child if not already there
2546
+ if (currentChildIndex === -1) {
2547
+ patches.push({
2548
+ type: NavigateChild,
2549
+ index: i
2550
+ });
2551
+ currentChildIndex = i;
2552
+ } else if (currentChildIndex !== i) {
2553
+ patches.push({
2554
+ type: NavigateSibling,
2555
+ index: i
2556
+ });
2557
+ currentChildIndex = i;
2558
+ }
2559
+ // Apply node patches (these apply to the current element, not children)
2560
+ if (nodePatches.length > 0) {
2561
+ patches.push(...nodePatches);
2562
+ }
2563
+ // Compare children recursively
2564
+ if (hasChildrenToCompare) {
2565
+ diffChildren(oldNode.children, newNode.children, patches);
2566
+ }
2567
+ }
2568
+ } else {
2569
+ // Remove old node - collect the index for later removal
2570
+ indicesToRemove.push(i);
2571
+ }
2572
+ }
2573
+ // Navigate back to parent if we ended at a child
2574
+ if (currentChildIndex >= 0) {
2575
+ patches.push({
2576
+ type: NavigateParent
2577
+ });
2578
+ currentChildIndex = -1;
2579
+ }
2580
+ // Add remove patches in reverse order (highest index first)
2581
+ // This ensures indices remain valid as we remove
2582
+ for (let j = indicesToRemove.length - 1; j >= 0; j--) {
2583
+ patches.push({
2584
+ type: RemoveChild,
2585
+ index: indicesToRemove[j]
2586
+ });
2587
+ }
2588
+ };
2589
+ const diffTrees = (oldTree, newTree, patches, path) => {
2590
+ // At the root level (path.length === 0), we're already AT the element
2591
+ // So we compare the root node directly, then compare its children
2592
+ if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
2593
+ const oldNode = oldTree[0];
2594
+ const newNode = newTree[0];
2595
+ // Compare root nodes
2596
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2597
+ // If nodePatches is null, the root node types are incompatible - need to replace
2598
+ if (nodePatches === null) {
2599
+ const flatNodes = treeToArray(newNode);
2600
+ patches.push({
2601
+ type: Replace,
2602
+ nodes: flatNodes
2603
+ });
2604
+ return;
2605
+ }
2606
+ if (nodePatches.length > 0) {
2607
+ patches.push(...nodePatches);
2608
+ }
2609
+ // Compare children
2610
+ if (oldNode.children.length > 0 || newNode.children.length > 0) {
2611
+ diffChildren(oldNode.children, newNode.children, patches);
2612
+ }
2613
+ } else {
2614
+ // Non-root level or multiple root elements - use the regular comparison
2615
+ diffChildren(oldTree, newTree, patches);
2616
+ }
2617
+ };
2618
+
2619
+ const removeTrailingNavigationPatches = patches => {
2620
+ // Find the last non-navigation patch
2621
+ let lastNonNavigationIndex = -1;
2622
+ for (let i = patches.length - 1; i >= 0; i--) {
2623
+ const patch = patches[i];
2624
+ if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
2625
+ lastNonNavigationIndex = i;
2626
+ break;
2627
+ }
2628
+ }
2629
+ // Return patches up to and including the last non-navigation patch
2630
+ return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2631
+ };
2632
+
2633
+ const diffTree = (oldNodes, newNodes) => {
2634
+ // Step 1: Convert flat arrays to tree structures
2635
+ const oldTree = arrayToTree(oldNodes);
2636
+ const newTree = arrayToTree(newNodes);
2637
+ // Step 3: Compare the trees
2638
+ const patches = [];
2639
+ diffTrees(oldTree, newTree, patches, []);
2640
+ // Remove trailing navigation patches since they serve no purpose
2641
+ return removeTrailingNavigationPatches(patches);
2642
+ };
2643
+
1652
2644
  const HandleInput = 4;
1653
2645
  const HandleFilterInput = 5;
2646
+ const HandleSimpleInput = 6;
2647
+
2648
+ const getDurationText = event => {
2649
+ const explicitDuration = event.durationMs ?? event.duration;
2650
+ if (typeof explicitDuration === 'number' && Number.isFinite(explicitDuration)) {
2651
+ return `${explicitDuration}ms`;
2652
+ }
2653
+ const start = toTimeNumber(event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp);
2654
+ const end = toTimeNumber(event.ended ?? event.endTime ?? event.endTimestamp ?? event.timestamp);
2655
+ if (start === undefined || end === undefined || end < start) {
2656
+ return '-';
2657
+ }
2658
+ return `${end - start}ms`;
2659
+ };
2660
+
2661
+ const timestampFormatter = new Intl.DateTimeFormat('en-US', {
2662
+ day: '2-digit',
2663
+ fractionalSecondDigits: 3,
2664
+ hour: '2-digit',
2665
+ hourCycle: 'h23',
2666
+ minute: '2-digit',
2667
+ month: 'short',
2668
+ second: '2-digit',
2669
+ timeZone: 'UTC',
2670
+ year: 'numeric'
2671
+ });
2672
+ const formatTimestamp = date => {
2673
+ return `${timestampFormatter.format(date)} UTC`;
2674
+ };
2675
+
2676
+ const getTimestampText = value => {
2677
+ if (typeof value === 'string') {
2678
+ const timestamp = Date.parse(value);
2679
+ if (!Number.isNaN(timestamp)) {
2680
+ return formatTimestamp(new Date(timestamp));
2681
+ }
2682
+ return value;
2683
+ }
2684
+ if (typeof value === 'number' && Number.isFinite(value)) {
2685
+ return formatTimestamp(new Date(value));
2686
+ }
2687
+ return '-';
2688
+ };
2689
+
2690
+ const getEndText = event => {
2691
+ return getTimestampText(event.ended ?? event.endTime ?? event.endTimestamp ?? event.timestamp);
2692
+ };
2693
+
2694
+ const getStartText = event => {
2695
+ return getTimestampText(event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp);
2696
+ };
2697
+
2698
+ const hasErrorStatus = event => {
2699
+ if (event.type === 'error') {
2700
+ return true;
2701
+ }
2702
+ if (event.success === false || event.ok === false) {
2703
+ return true;
2704
+ }
2705
+ const {
2706
+ status
2707
+ } = event;
2708
+ if (typeof status === 'number' && status >= 400) {
2709
+ return true;
2710
+ }
2711
+ if (typeof status === 'string') {
2712
+ const parsedStatus = Number(status);
2713
+ if (Number.isFinite(parsedStatus) && parsedStatus >= 400) {
2714
+ return true;
2715
+ }
2716
+ }
2717
+ return typeof event.error === 'string' || typeof event.errorMessage === 'string' || typeof event.exception === 'string';
2718
+ };
2719
+
2720
+ const getStatusText = event => {
2721
+ return hasErrorStatus(event) ? '400' : '200';
2722
+ };
2723
+
2724
+ const getDevtoolsRows = (events, selectedEventIndex) => {
2725
+ if (events.length === 0) {
2726
+ return [{
2727
+ childCount: 1,
2728
+ className: 'ChatDebugViewEmpty',
2729
+ type: Div
2730
+ }, text('No events')];
2731
+ }
2732
+ const rows = [];
2733
+ for (let i = 0; i < events.length; i++) {
2734
+ const event = events[i];
2735
+ const isSelected = selectedEventIndex === i;
2736
+ rows.push({
2737
+ childCount: 2,
2738
+ className: `ChatDebugViewEventRowLabel${isSelected ? ' ChatDebugViewEventRowLabelSelected' : ''}`,
2739
+ type: Label
2740
+ }, {
2741
+ checked: isSelected,
2742
+ childCount: 0,
2743
+ className: 'ChatDebugViewEventRowInput',
2744
+ inputType: 'radio',
2745
+ name: SelectedEventIndex,
2746
+ onChange: HandleSimpleInput,
2747
+ type: Input,
2748
+ value: String(i)
2749
+ }, {
2750
+ childCount: 5,
2751
+ className: `ChatDebugViewEventRow${isSelected ? ' ChatDebugViewEventRowSelected' : ''}`,
2752
+ type: Div
2753
+ }, {
2754
+ childCount: 1,
2755
+ className: 'ChatDebugViewCell ChatDebugViewCellType',
2756
+ type: Div
2757
+ }, text(event.type), {
2758
+ childCount: 1,
2759
+ className: 'ChatDebugViewCell',
2760
+ type: Div
2761
+ }, text(getStartText(event)), {
2762
+ childCount: 1,
2763
+ className: 'ChatDebugViewCell',
2764
+ type: Div
2765
+ }, text(getEndText(event)), {
2766
+ childCount: 1,
2767
+ className: 'ChatDebugViewCell ChatDebugViewCellDuration',
2768
+ type: Div
2769
+ }, text(getDurationText(event)), {
2770
+ childCount: 1,
2771
+ className: 'ChatDebugViewCell ChatDebugViewCellStatus',
2772
+ type: Div
2773
+ }, text(getStatusText(event)));
2774
+ }
2775
+ return rows;
2776
+ };
1654
2777
 
1655
2778
  const numberRegex = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
1656
2779
  const whitespaceRegex = /\s/u;
@@ -1755,7 +2878,247 @@ const getEventNode = event => {
1755
2878
  }, ...tokenNodes];
1756
2879
  };
1757
2880
 
1758
- const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedEvents, showInputEvents, showResponsePartEvents, events) => {
2881
+ const trailingZeroFractionRegex = /\.0+$/;
2882
+ const trailingFractionZeroRegex = /(\.\d*?)0+$/;
2883
+ const formatTimelinePresetValue = value => {
2884
+ return value.toFixed(3).replace(trailingZeroFractionRegex, '').replace(trailingFractionZeroRegex, '$1');
2885
+ };
2886
+
2887
+ const formatTimelineSeconds = value => {
2888
+ if (Number.isInteger(value)) {
2889
+ return `${value}s`;
2890
+ }
2891
+ return `${Number(value.toFixed(1))}s`;
2892
+ };
2893
+ const getTimelineSummary = (timelineEvents, timelineStartSeconds, timelineEndSeconds) => {
2894
+ const timelineInfo = getTimelineInfo(timelineEvents, timelineStartSeconds, timelineEndSeconds);
2895
+ if (timelineInfo.hasSelection && timelineInfo.startSeconds !== null && timelineInfo.endSeconds !== null) {
2896
+ return `Window ${formatTimelineSeconds(timelineInfo.startSeconds)}-${formatTimelineSeconds(timelineInfo.endSeconds)} of ${formatTimelineSeconds(timelineInfo.durationSeconds)}`;
2897
+ }
2898
+ return `Window 0s-${formatTimelineSeconds(timelineInfo.durationSeconds)} of ${formatTimelineSeconds(timelineInfo.durationSeconds)}`;
2899
+ };
2900
+
2901
+ const getTimelineNodes = (timelineEvents, timelineStartSeconds, timelineEndSeconds) => {
2902
+ const timelineInfo = getTimelineInfo(timelineEvents, timelineStartSeconds, timelineEndSeconds);
2903
+ if (timelineInfo.buckets.length === 0) {
2904
+ return [];
2905
+ }
2906
+ return [{
2907
+ childCount: 3,
2908
+ className: 'ChatDebugViewTimeline',
2909
+ type: Div
2910
+ }, {
2911
+ childCount: 2,
2912
+ className: 'ChatDebugViewTimelineTop',
2913
+ type: Div
2914
+ }, {
2915
+ childCount: 1,
2916
+ className: 'ChatDebugViewTimelineTitle',
2917
+ type: Div
2918
+ }, text('Timeline'), {
2919
+ childCount: 1,
2920
+ className: 'ChatDebugViewTimelineSummary',
2921
+ type: Div
2922
+ }, text(getTimelineSummary(timelineEvents, timelineStartSeconds, timelineEndSeconds)), {
2923
+ childCount: 3,
2924
+ className: 'ChatDebugViewTimelineControls',
2925
+ type: Div
2926
+ }, {
2927
+ childCount: 2,
2928
+ className: 'ChatDebugViewTimelineField',
2929
+ type: Label
2930
+ }, text('From'), {
2931
+ childCount: 0,
2932
+ className: 'InputBox ChatDebugViewTimelineInput',
2933
+ inputType: 'number',
2934
+ name: TimelineStartSeconds,
2935
+ onInput: HandleFilterInput,
2936
+ placeholder: '0',
2937
+ type: Input,
2938
+ value: timelineStartSeconds
2939
+ }, {
2940
+ childCount: 2,
2941
+ className: 'ChatDebugViewTimelineField',
2942
+ type: Label
2943
+ }, text('To'), {
2944
+ childCount: 0,
2945
+ className: 'InputBox ChatDebugViewTimelineInput',
2946
+ inputType: 'number',
2947
+ name: TimelineEndSeconds,
2948
+ onInput: HandleFilterInput,
2949
+ placeholder: formatTimelinePresetValue(timelineInfo.durationSeconds),
2950
+ type: Input,
2951
+ value: timelineEndSeconds
2952
+ }, {
2953
+ childCount: 2,
2954
+ className: `ChatDebugViewTimelineReset${timelineInfo.hasSelection ? '' : ' ChatDebugViewTimelineResetSelected'}`,
2955
+ type: Label
2956
+ }, {
2957
+ checked: !timelineInfo.hasSelection,
2958
+ childCount: 0,
2959
+ className: 'ChatDebugViewTimelinePresetInput',
2960
+ inputType: 'radio',
2961
+ name: TimelineRangePreset,
2962
+ onChange: HandleSimpleInput,
2963
+ type: Input,
2964
+ value: ''
2965
+ }, text('All'), {
2966
+ childCount: timelineInfo.buckets.length,
2967
+ className: 'ChatDebugViewTimelineBuckets',
2968
+ type: Div
2969
+ }, ...timelineInfo.buckets.flatMap(bucket => {
2970
+ const presetValue = `${formatTimelinePresetValue(bucket.startSeconds)}:${formatTimelinePresetValue(bucket.endSeconds)}`;
2971
+ return [{
2972
+ childCount: 2,
2973
+ className: `ChatDebugViewTimelineBucket${bucket.isSelected ? ' ChatDebugViewTimelineBucketSelected' : ''}`,
2974
+ type: Label
2975
+ }, {
2976
+ checked: false,
2977
+ childCount: 0,
2978
+ className: 'ChatDebugViewTimelinePresetInput',
2979
+ inputType: 'radio',
2980
+ name: TimelineRangePreset,
2981
+ onChange: HandleSimpleInput,
2982
+ type: Input,
2983
+ value: presetValue
2984
+ }, {
2985
+ childCount: bucket.unitCount === 0 ? 1 : bucket.unitCount,
2986
+ className: `ChatDebugViewTimelineBucketBar${bucket.isSelected ? ' ChatDebugViewTimelineBucketBarSelected' : ''}`,
2987
+ type: Div
2988
+ }, ...(bucket.unitCount === 0 ? [{
2989
+ childCount: 0,
2990
+ className: 'ChatDebugViewTimelineBucketUnit ChatDebugViewTimelineBucketUnitEmpty',
2991
+ type: Div
2992
+ }] : Array.from({
2993
+ length: bucket.unitCount
2994
+ }).fill({
2995
+ childCount: 0,
2996
+ className: 'ChatDebugViewTimelineBucketUnit',
2997
+ type: Div
2998
+ }))];
2999
+ })];
3000
+ };
3001
+
3002
+ const getDevtoolsDom = (events, selectedEventIndex, timelineEvents, timelineStartSeconds, timelineEndSeconds) => {
3003
+ const rowNodes = getDevtoolsRows(events, selectedEventIndex);
3004
+ const timelineNodes = getTimelineNodes(timelineEvents, timelineStartSeconds, timelineEndSeconds);
3005
+ const selectedEvent = selectedEventIndex === null ? undefined : events[selectedEventIndex];
3006
+ const selectedEventNodes = selectedEvent ? getEventNode(selectedEvent) : [];
3007
+ const hasSelectedEvent = selectedEventNodes.length > 0;
3008
+ const eventsClassName = `${hasSelectedEvent ? 'ChatDebugViewEvents' : 'ChatDebugViewEvents ChatDebugViewEventsFullWidth'}${timelineNodes.length > 0 ? ' ChatDebugViewEvents--timeline' : ''}`;
3009
+ const detailsNodes = hasSelectedEvent ? [{
3010
+ childCount: 2,
3011
+ className: 'ChatDebugViewDetails',
3012
+ type: Div
3013
+ }, {
3014
+ childCount: 2,
3015
+ className: 'ChatDebugViewDetailsTop',
3016
+ type: Div
3017
+ }, {
3018
+ childCount: 1,
3019
+ className: 'ChatDebugViewDetailsTitle',
3020
+ type: Div
3021
+ }, text('Details'), {
3022
+ childCount: 0,
3023
+ className: 'ChatDebugViewDetailsClose',
3024
+ inputType: 'checkbox',
3025
+ name: CloseDetails,
3026
+ onChange: HandleSimpleInput,
3027
+ type: Input,
3028
+ value: 'close'
3029
+ }, {
3030
+ childCount: selectedEventNodes.length,
3031
+ className: 'ChatDebugViewDetailsBody',
3032
+ type: Div
3033
+ }, ...selectedEventNodes] : [];
3034
+ return [{
3035
+ childCount: hasSelectedEvent ? 2 : 1,
3036
+ className: 'ChatDebugViewDevtoolsMain',
3037
+ type: Div
3038
+ }, {
3039
+ childCount: timelineNodes.length > 0 ? 3 : 2,
3040
+ className: eventsClassName,
3041
+ type: Div
3042
+ }, ...timelineNodes, {
3043
+ childCount: 5,
3044
+ className: 'ChatDebugViewTableHeader',
3045
+ type: Div
3046
+ }, {
3047
+ childCount: 1,
3048
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellType',
3049
+ type: Div
3050
+ }, text('Type'), {
3051
+ childCount: 1,
3052
+ className: 'ChatDebugViewHeaderCell',
3053
+ type: Div
3054
+ }, text('Started'), {
3055
+ childCount: 1,
3056
+ className: 'ChatDebugViewHeaderCell',
3057
+ type: Div
3058
+ }, text('Ended'), {
3059
+ childCount: 1,
3060
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellDuration',
3061
+ type: Div
3062
+ }, text('Duration'), {
3063
+ childCount: 1,
3064
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellStatus',
3065
+ type: Div
3066
+ }, text('Status'), {
3067
+ childCount: rowNodes.length === 0 ? 1 : rowNodes.length,
3068
+ className: 'ChatDebugViewTableBody',
3069
+ type: Div
3070
+ }, ...rowNodes, ...detailsNodes];
3071
+ };
3072
+
3073
+ const getLegacyEventsDom = (errorMessage, emptyMessage, eventNodes) => {
3074
+ return [{
3075
+ childCount: eventNodes.length === 0 ? 1 : eventNodes.length,
3076
+ className: 'ChatDebugViewEvents',
3077
+ type: Div
3078
+ }, ...(eventNodes.length === 0 ? [{
3079
+ childCount: 1,
3080
+ className: errorMessage ? 'ChatDebugViewError' : 'ChatDebugViewEmpty',
3081
+ type: Div
3082
+ }, text(errorMessage || emptyMessage)] : eventNodes)];
3083
+ };
3084
+ const getQuickFilterNodes = eventCategoryFilter => {
3085
+ return [{
3086
+ childCount: options.length,
3087
+ className: 'ChatDebugViewQuickFilters',
3088
+ type: Div
3089
+ }, ...options.flatMap(option => {
3090
+ const isSelected = option.value === eventCategoryFilter;
3091
+ return [{
3092
+ childCount: 2,
3093
+ className: `ChatDebugViewQuickFilterPill${isSelected ? ' ChatDebugViewQuickFilterPillSelected' : ''}`,
3094
+ type: Label
3095
+ }, {
3096
+ checked: isSelected,
3097
+ childCount: 0,
3098
+ className: 'ChatDebugViewQuickFilterInput',
3099
+ inputType: 'radio',
3100
+ name: EventCategoryFilter,
3101
+ onChange: HandleInput,
3102
+ type: Input,
3103
+ value: option.value
3104
+ }, text(option.label)];
3105
+ })];
3106
+ };
3107
+ const getTimelineFilterDescription = (timelineStartSeconds, timelineEndSeconds) => {
3108
+ const trimmedStart = timelineStartSeconds.trim();
3109
+ const trimmedEnd = timelineEndSeconds.trim();
3110
+ if (trimmedStart && trimmedEnd) {
3111
+ return `${trimmedStart}s-${trimmedEnd}s`;
3112
+ }
3113
+ if (trimmedStart) {
3114
+ return `from ${trimmedStart}s`;
3115
+ }
3116
+ if (trimmedEnd) {
3117
+ return `to ${trimmedEnd}s`;
3118
+ }
3119
+ return '';
3120
+ };
3121
+ const getChatDebugViewDom = (errorMessage, filterValue, eventCategoryFilter, showEventStreamFinishedEvents, showInputEvents, showResponsePartEvents, useDevtoolsLayout, selectedEventIndex, timelineStartSeconds, timelineEndSeconds, timelineEvents, events) => {
1759
3122
  if (errorMessage) {
1760
3123
  return [{
1761
3124
  childCount: 1,
@@ -1769,13 +3132,29 @@ const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedE
1769
3132
  }
1770
3133
  const eventNodes = events.flatMap(getEventNode);
1771
3134
  const trimmedFilterValue = filterValue.trim();
1772
- const hasFilterValue = trimmedFilterValue.length > 0;
1773
- const noFilteredEventsMessage = `no events found matching ${trimmedFilterValue}`;
3135
+ const filterDescriptionParts = [];
3136
+ if (eventCategoryFilter !== All) {
3137
+ filterDescriptionParts.push(getEventCategoryFilterLabel(eventCategoryFilter).toLowerCase());
3138
+ }
3139
+ if (trimmedFilterValue) {
3140
+ filterDescriptionParts.push(trimmedFilterValue);
3141
+ }
3142
+ const timelineFilterDescription = getTimelineFilterDescription(timelineStartSeconds, timelineEndSeconds);
3143
+ if (timelineFilterDescription) {
3144
+ filterDescriptionParts.push(timelineFilterDescription);
3145
+ }
3146
+ const hasFilterValue = filterDescriptionParts.length > 0;
3147
+ const filterDescription = filterDescriptionParts.join(' ');
3148
+ const noFilteredEventsMessage = `no events found matching ${filterDescription}`;
1774
3149
  const eventCountText = events.length === 0 && hasFilterValue ? noFilteredEventsMessage : `${events.length} event${events.length === 1 ? '' : 's'}`;
1775
3150
  const emptyMessage = events.length === 0 && hasFilterValue ? noFilteredEventsMessage : 'No events';
3151
+ const safeSelectedEventIndex = selectedEventIndex === null || selectedEventIndex < 0 || selectedEventIndex >= events.length ? null : selectedEventIndex;
3152
+ const contentNodes = useDevtoolsLayout ? getDevtoolsDom(events, safeSelectedEventIndex, timelineEvents, timelineStartSeconds, timelineEndSeconds) : getLegacyEventsDom(errorMessage, emptyMessage, eventNodes);
3153
+ const quickFilterNodes = useDevtoolsLayout ? getQuickFilterNodes(eventCategoryFilter) : [];
3154
+ const rootChildCount = useDevtoolsLayout ? 4 : 3;
1776
3155
  return [{
1777
- childCount: 3,
1778
- className: 'ChatDebugView',
3156
+ childCount: rootChildCount,
3157
+ className: useDevtoolsLayout ? 'ChatDebugView ChatDebugView--devtools' : 'ChatDebugView',
1779
3158
  type: Div
1780
3159
  }, {
1781
3160
  childCount: 2,
@@ -1792,7 +3171,7 @@ const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedE
1792
3171
  type: Input,
1793
3172
  value: filterValue
1794
3173
  }, {
1795
- childCount: 3,
3174
+ childCount: 4,
1796
3175
  className: 'ChatDebugViewToggle',
1797
3176
  type: Div
1798
3177
  }, {
@@ -1829,71 +3208,21 @@ const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedE
1829
3208
  onChange: HandleInput,
1830
3209
  type: Input
1831
3210
  }, text('Show response part events'), {
3211
+ childCount: 2,
3212
+ className: 'ChatDebugViewToggleLabel',
3213
+ type: Label
3214
+ }, {
3215
+ checked: useDevtoolsLayout,
3216
+ childCount: 0,
3217
+ inputType: 'checkbox',
3218
+ name: UseDevtoolsLayout,
3219
+ onChange: HandleInput,
3220
+ type: Input
3221
+ }, text('Use devtools layout'), ...quickFilterNodes, {
1832
3222
  childCount: 1,
1833
3223
  className: 'ChatDebugViewEventCount',
1834
3224
  type: Div
1835
- }, text(eventCountText), {
1836
- childCount: eventNodes.length === 0 ? 1 : eventNodes.length,
1837
- className: 'ChatDebugViewEvents',
1838
- type: Div
1839
- }, ...(eventNodes.length === 0 ? [{
1840
- childCount: 1,
1841
- className: errorMessage ? 'ChatDebugViewError' : 'ChatDebugViewEmpty',
1842
- type: Div
1843
- }, text(errorMessage || emptyMessage)] : eventNodes)];
1844
- };
1845
-
1846
- const RE_SPACE = /\s+/;
1847
- const parseFilterValue = filterValue => {
1848
- const normalizedFilter = filterValue.trim().toLowerCase();
1849
- if (!normalizedFilter) {
1850
- return {
1851
- filterText: '',
1852
- toolsOnly: false
1853
- };
1854
- }
1855
- const parts = normalizedFilter.split(RE_SPACE);
1856
- const toolsOnly = parts.includes('@tools');
1857
- const filterText = parts.filter(part => part !== '@tools').join(' ');
1858
- return {
1859
- filterText,
1860
- toolsOnly
1861
- };
1862
- };
1863
-
1864
- const toolEventTypePrefix = 'tool-execution-';
1865
- const isToolEvent = event => {
1866
- return event.type.startsWith(toolEventTypePrefix);
1867
- };
1868
- const getVisibleEvents = (events, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents) => {
1869
- return events.filter(event => {
1870
- if (!showInputEvents && (event.type === 'handle-input' || event.type === 'handle-submit')) {
1871
- return false;
1872
- }
1873
- if (!showResponsePartEvents && event.type === 'sse-response-part') {
1874
- return false;
1875
- }
1876
- if (!showEventStreamFinishedEvents && event.type === 'event-stream-finished') {
1877
- return false;
1878
- }
1879
- // hide session creation events by default — not useful in the debug view
1880
- if (event.type === 'chat-session-created') {
1881
- return false;
1882
- }
1883
- return true;
1884
- });
1885
- };
1886
- const getFilteredEvents = (events, filterValue, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents) => {
1887
- const visibleEvents = getVisibleEvents(events, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents);
1888
- const {
1889
- filterText,
1890
- toolsOnly
1891
- } = parseFilterValue(filterValue);
1892
- const filteredBySyntax = toolsOnly ? visibleEvents.filter(isToolEvent) : visibleEvents;
1893
- if (!filterText) {
1894
- return filteredBySyntax;
1895
- }
1896
- return filteredBySyntax.filter(event => JSON.stringify(event).toLowerCase().includes(filterText));
3225
+ }, text(eventCountText), ...contentNodes];
1897
3226
  };
1898
3227
 
1899
3228
  const withSessionEventIds = events => {
@@ -1905,16 +3234,29 @@ const withSessionEventIds = events => {
1905
3234
  });
1906
3235
  };
1907
3236
  const renderItems = (oldState, newState) => {
3237
+ if (newState.initial) {
3238
+ return [SetDom2, newState.uid, []];
3239
+ }
1908
3240
  const eventsWithIds = withSessionEventIds(newState.events);
1909
- const filteredEvents = getFilteredEvents(eventsWithIds, newState.filterValue, newState.showInputEvents, newState.showResponsePartEvents, newState.showEventStreamFinishedEvents);
1910
- const dom = getChatDebugViewDom(newState.errorMessage, newState.filterValue, newState.showEventStreamFinishedEvents, newState.showInputEvents, newState.showResponsePartEvents, filteredEvents);
3241
+ const timelineEvents = getFilteredEvents(eventsWithIds, newState.filterValue, newState.eventCategoryFilter, newState.showInputEvents, newState.showResponsePartEvents, newState.showEventStreamFinishedEvents);
3242
+ const filteredEvents = filterEventsByTimelineRange(timelineEvents, newState.timelineStartSeconds, newState.timelineEndSeconds);
3243
+ const dom = getChatDebugViewDom(newState.errorMessage, newState.filterValue, newState.eventCategoryFilter, newState.showEventStreamFinishedEvents, newState.showInputEvents, newState.showResponsePartEvents, newState.useDevtoolsLayout, newState.selectedEventIndex, newState.timelineStartSeconds, newState.timelineEndSeconds, timelineEvents, filteredEvents);
1911
3244
  return [SetDom2, newState.uid, dom];
1912
3245
  };
1913
3246
 
3247
+ const renderIncremental = (oldState, newState) => {
3248
+ const oldDom = renderItems(oldState, oldState)[2];
3249
+ const newDom = renderItems(newState, newState)[2];
3250
+ const patches = diffTree(oldDom, newDom);
3251
+ return [SetPatches, newState.uid, patches];
3252
+ };
3253
+
1914
3254
  const getRenderer = diffType => {
1915
3255
  switch (diffType) {
1916
3256
  case RenderCss:
1917
3257
  return renderCss;
3258
+ case RenderIncremental:
3259
+ return renderIncremental;
1918
3260
  case RenderItems:
1919
3261
  return renderItems;
1920
3262
  default:
@@ -1950,6 +3292,9 @@ const renderEventListeners = () => {
1950
3292
  }, {
1951
3293
  name: HandleInput,
1952
3294
  params: ['handleInput', TargetName, TargetValue, TargetChecked]
3295
+ }, {
3296
+ name: HandleSimpleInput,
3297
+ params: ['handleInput', TargetName, TargetValue]
1953
3298
  }];
1954
3299
  };
1955
3300
 
@@ -1966,23 +3311,31 @@ const resize = (state, dimensions) => {
1966
3311
 
1967
3312
  const saveState = state => {
1968
3313
  const {
3314
+ eventCategoryFilter,
1969
3315
  filterValue,
1970
3316
  height,
1971
3317
  sessionId,
1972
3318
  showEventStreamFinishedEvents,
1973
3319
  showInputEvents,
1974
3320
  showResponsePartEvents,
3321
+ timelineEndSeconds,
3322
+ timelineStartSeconds,
3323
+ useDevtoolsLayout,
1975
3324
  width,
1976
3325
  x,
1977
3326
  y
1978
3327
  } = state;
1979
3328
  return {
3329
+ eventCategoryFilter,
1980
3330
  filterValue,
1981
3331
  height,
1982
3332
  sessionId,
1983
3333
  showEventStreamFinishedEvents,
1984
3334
  showInputEvents,
1985
3335
  showResponsePartEvents,
3336
+ timelineEndSeconds,
3337
+ timelineStartSeconds,
3338
+ useDevtoolsLayout,
1986
3339
  width,
1987
3340
  x,
1988
3341
  y
@@ -1994,7 +3347,8 @@ const setEvents = (state, events) => {
1994
3347
  ...state,
1995
3348
  errorMessage: '',
1996
3349
  events,
1997
- initial: false
3350
+ initial: false,
3351
+ selectedEventIndex: null
1998
3352
  };
1999
3353
  };
2000
3354