@lvce-editor/chat-debug-view 3.4.0 → 4.1.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
  };
@@ -1030,39 +1071,485 @@ const diff2 = uid => {
1030
1071
  return diff(oldState, newState);
1031
1072
  };
1032
1073
 
1074
+ const parseSelectedEventIndex$1 = value => {
1075
+ const parsed = Number.parseInt(value, 10);
1076
+ if (Number.isNaN(parsed) || parsed < 0) {
1077
+ return null;
1078
+ }
1079
+ return parsed;
1080
+ };
1081
+ const handleEventRowClick = (state, value) => {
1082
+ const selectedEventIndex = parseSelectedEventIndex$1(value);
1083
+ if (selectedEventIndex === null) {
1084
+ return state;
1085
+ }
1086
+ return {
1087
+ ...state,
1088
+ selectedEventIndex
1089
+ };
1090
+ };
1091
+
1092
+ const startedEventType = 'tool-execution-started';
1093
+ const finishedEventType = 'tool-execution-finished';
1094
+ const mergedEventType = 'tool-execution';
1095
+ const eventStableIds = new WeakMap();
1096
+ let nextStableEventId = 1;
1097
+ const getOrCreateStableEventId = event => {
1098
+ const existingStableEventId = eventStableIds.get(event);
1099
+ if (existingStableEventId) {
1100
+ return existingStableEventId;
1101
+ }
1102
+ const stableEventId = `event-${nextStableEventId++}`;
1103
+ eventStableIds.set(event, stableEventId);
1104
+ return stableEventId;
1105
+ };
1106
+ const setStableEventId = (event, stableEventId) => {
1107
+ eventStableIds.set(event, stableEventId);
1108
+ };
1109
+ const getStartedTimestamp = event => {
1110
+ return event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp;
1111
+ };
1112
+ const getEndedTimestamp = event => {
1113
+ return event.ended ?? event.endTime ?? event.endTimestamp ?? event.timestamp;
1114
+ };
1115
+ const isToolExecutionStartedEvent = event => {
1116
+ return event.type === startedEventType;
1117
+ };
1118
+ const isToolExecutionFinishedEvent = event => {
1119
+ return event.type === finishedEventType;
1120
+ };
1121
+ const hasMatchingToolName = (startedEvent, finishedEvent) => {
1122
+ if (typeof startedEvent.toolName === 'string' && typeof finishedEvent.toolName === 'string') {
1123
+ return startedEvent.toolName === finishedEvent.toolName;
1124
+ }
1125
+ return true;
1126
+ };
1127
+ const isMatchingToolExecutionPair = (startedEvent, finishedEvent) => {
1128
+ return startedEvent.sessionId === finishedEvent.sessionId && hasMatchingToolName(startedEvent, finishedEvent);
1129
+ };
1130
+ const mergeToolExecutionEvents = (startedEvent, finishedEvent) => {
1131
+ const mergedEvent = {
1132
+ ...startedEvent,
1133
+ ...finishedEvent,
1134
+ ended: getEndedTimestamp(finishedEvent),
1135
+ started: getStartedTimestamp(startedEvent),
1136
+ type: mergedEventType
1137
+ };
1138
+ const stableEventId = `${getOrCreateStableEventId(startedEvent)}:${getOrCreateStableEventId(finishedEvent)}`;
1139
+ setStableEventId(mergedEvent, stableEventId);
1140
+ return mergedEvent;
1141
+ };
1142
+ const getStableEventId = event => {
1143
+ return getOrCreateStableEventId(event);
1144
+ };
1145
+ const collapseToolExecutionEvents = events => {
1146
+ const collapsedEvents = [];
1147
+ for (let i = 0; i < events.length; i++) {
1148
+ const event = events[i];
1149
+ if (isToolExecutionStartedEvent(event)) {
1150
+ const nextEvent = events[i + 1];
1151
+ if (nextEvent && isToolExecutionFinishedEvent(nextEvent) && isMatchingToolExecutionPair(event, nextEvent)) {
1152
+ collapsedEvents.push(mergeToolExecutionEvents(event, nextEvent));
1153
+ i++;
1154
+ continue;
1155
+ }
1156
+ }
1157
+ collapsedEvents.push(event);
1158
+ }
1159
+ return collapsedEvents;
1160
+ };
1161
+
1033
1162
  const getBoolean = value => {
1034
1163
  return value === true || value === 'true' || value === 'on' || value === '1';
1035
1164
  };
1036
1165
 
1166
+ const RE_SPACE = /\s+/;
1167
+ const tokenToEventCategoryFilter = new Map([['@tools', Tools], ['@network', Network], ['@ui', Ui], ['@stream', Stream]]);
1168
+ const parseFilterValue = filterValue => {
1169
+ const normalizedFilter = filterValue.trim().toLowerCase();
1170
+ if (!normalizedFilter) {
1171
+ return {
1172
+ eventCategoryFilter: All,
1173
+ filterText: ''
1174
+ };
1175
+ }
1176
+ const parts = normalizedFilter.split(RE_SPACE);
1177
+ const eventCategoryFilter = parts.map(part => tokenToEventCategoryFilter.get(part)).find(Boolean) || All;
1178
+ const filterText = parts.filter(part => !tokenToEventCategoryFilter.has(part)).join(' ');
1179
+ return {
1180
+ eventCategoryFilter,
1181
+ filterText
1182
+ };
1183
+ };
1184
+
1185
+ const toolEventTypePrefix = 'tool-execution';
1186
+ const isToolEvent = event => {
1187
+ return event.type.startsWith(toolEventTypePrefix);
1188
+ };
1189
+ const isNetworkEvent = event => {
1190
+ const normalizedType = event.type.toLowerCase();
1191
+ return normalizedType === 'request' || normalizedType === 'response' || normalizedType === 'handle-response' || normalizedType.includes('fetch') || normalizedType.includes('xhr');
1192
+ };
1193
+ const isUiEvent = event => {
1194
+ return event.type.startsWith('handle-') && event.type !== 'handle-response';
1195
+ };
1196
+ const isStreamEvent = event => {
1197
+ return event.type === 'sse-response-part' || event.type === 'event-stream-finished';
1198
+ };
1199
+ const matchesEventCategoryFilter = (event, eventCategoryFilter) => {
1200
+ switch (eventCategoryFilter) {
1201
+ case Network:
1202
+ return isNetworkEvent(event);
1203
+ case Stream:
1204
+ return isStreamEvent(event);
1205
+ case Tools:
1206
+ return isToolEvent(event);
1207
+ case Ui:
1208
+ return isUiEvent(event);
1209
+ default:
1210
+ return true;
1211
+ }
1212
+ };
1213
+ const getVisibleEvents = (events, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents) => {
1214
+ return events.filter(event => {
1215
+ if (!showInputEvents && event.type === 'handle-input') {
1216
+ return false;
1217
+ }
1218
+ if (!showResponsePartEvents && event.type === 'sse-response-part') {
1219
+ return false;
1220
+ }
1221
+ if (!showEventStreamFinishedEvents && event.type === 'event-stream-finished') {
1222
+ return false;
1223
+ }
1224
+ // hide session creation events by default — not useful in the debug view
1225
+ if (event.type === 'chat-session-created') {
1226
+ return false;
1227
+ }
1228
+ return true;
1229
+ });
1230
+ };
1231
+ const getFilteredEvents = (events, filterValue, eventCategoryFilter, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents) => {
1232
+ const visibleEvents = getVisibleEvents(events, showInputEvents, showResponsePartEvents, showEventStreamFinishedEvents);
1233
+ const collapsedEvents = collapseToolExecutionEvents(visibleEvents);
1234
+ const parsedFilter = parseFilterValue(filterValue);
1235
+ const activeEventCategoryFilter = parsedFilter.eventCategoryFilter === All ? eventCategoryFilter : parsedFilter.eventCategoryFilter;
1236
+ const filteredByCategory = collapsedEvents.filter(event => matchesEventCategoryFilter(event, activeEventCategoryFilter));
1237
+ const {
1238
+ filterText
1239
+ } = parsedFilter;
1240
+ if (!filterText) {
1241
+ return filteredByCategory;
1242
+ }
1243
+ return filteredByCategory.filter(event => JSON.stringify(event).toLowerCase().includes(filterText));
1244
+ };
1245
+
1246
+ const toTimeNumber = value => {
1247
+ if (typeof value === 'number' && Number.isFinite(value)) {
1248
+ return value;
1249
+ }
1250
+ if (typeof value === 'string') {
1251
+ const timestamp = Date.parse(value);
1252
+ if (!Number.isNaN(timestamp)) {
1253
+ return timestamp;
1254
+ }
1255
+ }
1256
+ return undefined;
1257
+ };
1258
+
1259
+ const getEventTime = event => {
1260
+ return toTimeNumber(event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp);
1261
+ };
1262
+
1263
+ const maxBarUnits = 8;
1264
+ const parseTimelineSeconds = value => {
1265
+ const trimmed = value.trim();
1266
+ if (!trimmed) {
1267
+ return undefined;
1268
+ }
1269
+ const parsed = Number.parseFloat(trimmed);
1270
+ if (!Number.isFinite(parsed) || parsed < 0) {
1271
+ return undefined;
1272
+ }
1273
+ return parsed;
1274
+ };
1275
+ const roundSeconds = value => {
1276
+ return Number(value.toFixed(3));
1277
+ };
1278
+ const getEventsWithTime = events => {
1279
+ return events.flatMap(event => {
1280
+ const time = getEventTime(event);
1281
+ if (time === undefined) {
1282
+ return [];
1283
+ }
1284
+ return [{
1285
+ event,
1286
+ time
1287
+ }];
1288
+ });
1289
+ };
1290
+ const getNormalizedRange = (durationSeconds, startValue, endValue) => {
1291
+ const parsedStart = parseTimelineSeconds(startValue);
1292
+ const parsedEnd = parseTimelineSeconds(endValue);
1293
+ if (parsedStart === undefined && parsedEnd === undefined) {
1294
+ return {
1295
+ endSeconds: null,
1296
+ hasSelection: false,
1297
+ startSeconds: null
1298
+ };
1299
+ }
1300
+ const rawStart = parsedStart ?? 0;
1301
+ const rawEnd = parsedEnd ?? durationSeconds;
1302
+ const normalizedStart = Math.max(0, Math.min(durationSeconds, Math.min(rawStart, rawEnd)));
1303
+ const normalizedEnd = Math.max(0, Math.min(durationSeconds, Math.max(rawStart, rawEnd)));
1304
+ return {
1305
+ endSeconds: roundSeconds(normalizedEnd),
1306
+ hasSelection: true,
1307
+ startSeconds: roundSeconds(normalizedStart)
1308
+ };
1309
+ };
1310
+ const filterEventsByTimelineRange = (events, startValue, endValue) => {
1311
+ const eventsWithTime = getEventsWithTime(events);
1312
+ if (eventsWithTime.length === 0) {
1313
+ return events;
1314
+ }
1315
+ const baseTime = eventsWithTime[0].time;
1316
+ const lastTime = eventsWithTime.at(-1)?.time ?? baseTime;
1317
+ const durationSeconds = roundSeconds(Math.max(0, lastTime - baseTime) / 1000);
1318
+ const range = getNormalizedRange(durationSeconds, startValue, endValue);
1319
+ if (!range.hasSelection || range.startSeconds === null || range.endSeconds === null) {
1320
+ return events;
1321
+ }
1322
+ const startTime = baseTime + range.startSeconds * 1000;
1323
+ const endTime = baseTime + range.endSeconds * 1000;
1324
+ return eventsWithTime.filter(item => item.time >= startTime && item.time <= endTime).map(item => item.event);
1325
+ };
1326
+ const getTimelineInfo = (events, startValue, endValue) => {
1327
+ const eventsWithTime = getEventsWithTime(events);
1328
+ if (eventsWithTime.length === 0) {
1329
+ return {
1330
+ buckets: [],
1331
+ durationSeconds: 0,
1332
+ endSeconds: null,
1333
+ hasSelection: false,
1334
+ startSeconds: null
1335
+ };
1336
+ }
1337
+ const baseTime = eventsWithTime[0].time;
1338
+ const lastTime = eventsWithTime.at(-1)?.time ?? baseTime;
1339
+ const durationMs = Math.max(0, lastTime - baseTime);
1340
+ const durationSeconds = roundSeconds(durationMs / 1000);
1341
+ const range = getNormalizedRange(durationSeconds, startValue, endValue);
1342
+ const bucketCount = durationSeconds === 0 ? 1 : Math.max(12, Math.min(48, Math.ceil(durationSeconds)));
1343
+ const bucketDurationMs = durationMs === 0 ? 1000 : durationMs / bucketCount;
1344
+ const counts = Array.from({
1345
+ length: bucketCount
1346
+ }).fill(0);
1347
+ for (const item of eventsWithTime) {
1348
+ const offsetMs = item.time - baseTime;
1349
+ const index = durationMs === 0 ? 0 : Math.min(bucketCount - 1, Math.floor(offsetMs / durationMs * bucketCount));
1350
+ counts[index] += 1;
1351
+ }
1352
+ const maxCount = Math.max(...counts);
1353
+ const buckets = counts.map((count, index) => {
1354
+ const bucketStartMs = index * bucketDurationMs;
1355
+ const bucketEndMs = index === bucketCount - 1 ? durationMs : (index + 1) * bucketDurationMs;
1356
+ const hasSelection = range.hasSelection && range.startSeconds !== null && range.endSeconds !== null;
1357
+ const selectionStartMs = hasSelection ? range.startSeconds * 1000 : 0;
1358
+ const selectionEndMs = hasSelection ? range.endSeconds * 1000 : 0;
1359
+ return {
1360
+ count,
1361
+ endSeconds: roundSeconds(bucketEndMs / 1000),
1362
+ isSelected: hasSelection && bucketEndMs >= selectionStartMs && bucketStartMs <= selectionEndMs,
1363
+ startSeconds: roundSeconds(bucketStartMs / 1000),
1364
+ unitCount: count === 0 ? 0 : Math.max(1, Math.round(count / maxCount * maxBarUnits))
1365
+ };
1366
+ });
1367
+ return {
1368
+ buckets,
1369
+ durationSeconds,
1370
+ endSeconds: range.endSeconds,
1371
+ hasSelection: range.hasSelection,
1372
+ startSeconds: range.startSeconds
1373
+ };
1374
+ };
1375
+
1037
1376
  const Filter = 'filter';
1377
+ const EventCategoryFilter = 'eventCategoryFilter';
1038
1378
  const ShowEventStreamFinishedEvents = 'showEventStreamFinishedEvents';
1039
1379
  const ShowInputEvents = 'showInputEvents';
1040
1380
  const ShowResponsePartEvents = 'showResponsePartEvents';
1041
-
1381
+ const UseDevtoolsLayout = 'useDevtoolsLayout';
1382
+ const SelectedEventIndex = 'selectedEventIndex';
1383
+ const CloseDetails = 'closeDetails';
1384
+ const TimelineStartSeconds = 'timelineStartSeconds';
1385
+ const TimelineEndSeconds = 'timelineEndSeconds';
1386
+ const TimelineRangePreset = 'timelineRangePreset';
1387
+
1388
+ const getCurrentEvents = state => {
1389
+ const filteredEvents = getFilteredEvents(state.events, state.filterValue, state.eventCategoryFilter, state.showInputEvents, state.showResponsePartEvents, state.showEventStreamFinishedEvents);
1390
+ return filterEventsByTimelineRange(filteredEvents, state.timelineStartSeconds, state.timelineEndSeconds);
1391
+ };
1392
+ const parseTimelineRangePreset = value => {
1393
+ if (!value) {
1394
+ return {
1395
+ timelineEndSeconds: '',
1396
+ timelineStartSeconds: ''
1397
+ };
1398
+ }
1399
+ const [timelineStartSeconds = '', timelineEndSeconds = ''] = value.split(':', 2);
1400
+ return {
1401
+ timelineEndSeconds,
1402
+ timelineStartSeconds
1403
+ };
1404
+ };
1405
+ const getEventIndexByStableId = (events, event) => {
1406
+ const stableEventId = getStableEventId(event);
1407
+ return events.findIndex(candidate => getStableEventId(candidate) === stableEventId);
1408
+ };
1409
+ const getSelectedEventIndex = state => {
1410
+ const {
1411
+ selectedEventIndex
1412
+ } = state;
1413
+ if (selectedEventIndex === null) {
1414
+ return null;
1415
+ }
1416
+ const filteredEvents = getCurrentEvents(state);
1417
+ const selectedEvent = filteredEvents[selectedEventIndex];
1418
+ if (!selectedEvent) {
1419
+ return null;
1420
+ }
1421
+ const newIndex = getEventIndexByStableId(filteredEvents, selectedEvent);
1422
+ if (newIndex === -1) {
1423
+ return null;
1424
+ }
1425
+ return newIndex;
1426
+ };
1427
+ const getPreservedSelectedEventIndex = (oldState, newState) => {
1428
+ const {
1429
+ selectedEventIndex
1430
+ } = oldState;
1431
+ if (selectedEventIndex === null) {
1432
+ return null;
1433
+ }
1434
+ const oldFilteredEvents = getCurrentEvents(oldState);
1435
+ const selectedEvent = oldFilteredEvents[selectedEventIndex];
1436
+ if (!selectedEvent) {
1437
+ return null;
1438
+ }
1439
+ const newFilteredEvents = getCurrentEvents(newState);
1440
+ const newIndex = getEventIndexByStableId(newFilteredEvents, selectedEvent);
1441
+ if (newIndex === -1) {
1442
+ return null;
1443
+ }
1444
+ return newIndex;
1445
+ };
1446
+ const parseSelectedEventIndex = value => {
1447
+ const parsed = Number.parseInt(value, 10);
1448
+ if (Number.isNaN(parsed) || parsed < 0) {
1449
+ return null;
1450
+ }
1451
+ return parsed;
1452
+ };
1042
1453
  const handleInput = (state, name, value, checked) => {
1043
1454
  if (name === Filter) {
1044
- return {
1455
+ const nextState = {
1045
1456
  ...state,
1046
1457
  filterValue: value
1047
1458
  };
1459
+ return {
1460
+ ...nextState,
1461
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1462
+ };
1048
1463
  }
1049
- if (name === ShowEventStreamFinishedEvents) {
1464
+ if (name === EventCategoryFilter) {
1465
+ const nextState = {
1466
+ ...state,
1467
+ eventCategoryFilter: value || All
1468
+ };
1050
1469
  return {
1470
+ ...nextState,
1471
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1472
+ };
1473
+ }
1474
+ if (name === ShowEventStreamFinishedEvents) {
1475
+ const nextState = {
1051
1476
  ...state,
1052
1477
  showEventStreamFinishedEvents: getBoolean(checked)
1053
1478
  };
1479
+ return {
1480
+ ...nextState,
1481
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1482
+ };
1054
1483
  }
1055
1484
  if (name === ShowInputEvents) {
1056
- return {
1485
+ const nextState = {
1057
1486
  ...state,
1058
1487
  showInputEvents: getBoolean(checked)
1059
1488
  };
1489
+ return {
1490
+ ...nextState,
1491
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1492
+ };
1060
1493
  }
1061
1494
  if (name === ShowResponsePartEvents) {
1062
- return {
1495
+ const nextState = {
1063
1496
  ...state,
1064
1497
  showResponsePartEvents: getBoolean(checked)
1065
1498
  };
1499
+ return {
1500
+ ...nextState,
1501
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1502
+ };
1503
+ }
1504
+ if (name === UseDevtoolsLayout) {
1505
+ const useDevtoolsLayout = getBoolean(checked);
1506
+ return {
1507
+ ...state,
1508
+ selectedEventIndex: useDevtoolsLayout ? getSelectedEventIndex(state) : null,
1509
+ useDevtoolsLayout
1510
+ };
1511
+ }
1512
+ if (name === SelectedEventIndex) {
1513
+ return {
1514
+ ...state,
1515
+ selectedEventIndex: parseSelectedEventIndex(value)
1516
+ };
1517
+ }
1518
+ if (name === TimelineStartSeconds) {
1519
+ const nextState = {
1520
+ ...state,
1521
+ timelineStartSeconds: value
1522
+ };
1523
+ return {
1524
+ ...nextState,
1525
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1526
+ };
1527
+ }
1528
+ if (name === TimelineEndSeconds) {
1529
+ const nextState = {
1530
+ ...state,
1531
+ timelineEndSeconds: value
1532
+ };
1533
+ return {
1534
+ ...nextState,
1535
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1536
+ };
1537
+ }
1538
+ if (name === TimelineRangePreset) {
1539
+ const nextState = {
1540
+ ...state,
1541
+ ...parseTimelineRangePreset(value)
1542
+ };
1543
+ return {
1544
+ ...nextState,
1545
+ selectedEventIndex: getPreservedSelectedEventIndex(state, nextState)
1546
+ };
1547
+ }
1548
+ if (name === CloseDetails) {
1549
+ return {
1550
+ ...state,
1551
+ selectedEventIndex: null
1552
+ };
1066
1553
  }
1067
1554
  return state;
1068
1555
  };
@@ -1448,6 +1935,7 @@ const loadContent = async state => {
1448
1935
  errorMessage: getInvalidUriMessage(uri, parsed.code),
1449
1936
  events: [],
1450
1937
  initial: false,
1938
+ selectedEventIndex: null,
1451
1939
  sessionId: ''
1452
1940
  };
1453
1941
  }
@@ -1462,6 +1950,7 @@ const loadContent = async state => {
1462
1950
  errorMessage: getSessionNotFoundMessage(sessionId),
1463
1951
  events: [],
1464
1952
  initial: false,
1953
+ selectedEventIndex: null,
1465
1954
  sessionId
1466
1955
  };
1467
1956
  }
@@ -1470,6 +1959,7 @@ const loadContent = async state => {
1470
1959
  errorMessage: '',
1471
1960
  events,
1472
1961
  initial: false,
1962
+ selectedEventIndex: null,
1473
1963
  sessionId
1474
1964
  };
1475
1965
  } catch {
@@ -1478,6 +1968,7 @@ const loadContent = async state => {
1478
1968
  errorMessage: getFailedToLoadMessage(sessionId),
1479
1969
  events: [],
1480
1970
  initial: false,
1971
+ selectedEventIndex: null,
1481
1972
  sessionId
1482
1973
  };
1483
1974
  }
@@ -1489,7 +1980,8 @@ const refresh = async state => {
1489
1980
  ...state,
1490
1981
  errorMessage: '',
1491
1982
  events,
1492
- initial: false
1983
+ initial: false,
1984
+ selectedEventIndex: null
1493
1985
  };
1494
1986
  };
1495
1987
 
@@ -1499,22 +1991,28 @@ const TargetValue = 'event.target.value';
1499
1991
 
1500
1992
  const SetCss = 'Viewlet.setCss';
1501
1993
  const SetDom2 = 'Viewlet.setDom2';
1994
+ const SetPatches = 'Viewlet.setPatches';
1502
1995
 
1503
1996
  const getCss = () => {
1504
1997
  return `
1505
1998
  .ChatDebugView {
1506
1999
  padding: 8px;
1507
- display: grid;
1508
- grid-template-rows: auto auto 1fr;
2000
+ display: flex;
2001
+ flex-direction: column;
1509
2002
  height: 100%;
1510
2003
  box-sizing: border-box;
1511
2004
  gap: 8px;
1512
2005
  }
1513
2006
 
2007
+ .ChatDebugView--devtools {
2008
+ gap: 4px;
2009
+ }
2010
+
1514
2011
  .ChatDebugViewTop {
1515
2012
  display: flex;
1516
2013
  align-items: center;
1517
2014
  gap: 12px;
2015
+ flex-wrap: wrap;
1518
2016
  }
1519
2017
 
1520
2018
  .ChatDebugViewTop .InputBox {
@@ -1534,97 +2032,418 @@ const getCss = () => {
1534
2032
  gap: 4px;
1535
2033
  }
1536
2034
 
1537
- .ChatDebugViewEventCount {
1538
- font-size: 12px;
1539
- opacity: 0.8;
2035
+ .ChatDebugViewQuickFilterPill {
2036
+ display: inline-flex;
2037
+ align-items: center;
2038
+ justify-content: center;
1540
2039
  }
1541
2040
 
1542
- .ChatDebugViewEvents {
1543
- overflow: auto;
1544
- scrollbar-width: thin;
1545
- scrollbar-color: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, 0.4)) transparent;
1546
- }
1547
2041
 
1548
- .ChatDebugViewEvents::-webkit-scrollbar {
1549
- width: 10px;
1550
- height: 10px;
2042
+ .ChatDebugViewQuickFilters {
2043
+ display: flex;
2044
+ gap: 8px;
2045
+ justify-content: center;
2046
+ min-height: 28px;
2047
+ padding: 0 12px;
2048
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2049
+ border-radius: 999px;
2050
+ background: var(--vscode-editorWidget-background, transparent);
2051
+ cursor: pointer;
2052
+ font-size: 12px;
2053
+ line-height: 1;
1551
2054
  }
1552
2055
 
1553
- .ChatDebugViewEvents::-webkit-scrollbar-track {
1554
- background: transparent;
2056
+ .ChatDebugViewQuickFilterPillSelected {
2057
+ border-color: var(--vscode-focusBorder, #007fd4);
2058
+ background: var(--vscode-list-activeSelectionBackground, rgba(14, 99, 156, 0.35));
2059
+ color: var(--vscode-list-activeSelectionForeground, inherit);
1555
2060
  }
1556
2061
 
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;
2062
+ .ChatDebugViewQuickFilterInput {
2063
+ position: absolute;
2064
+ opacity: 0;
2065
+ pointer-events: none;
1562
2066
  }
1563
2067
 
1564
- .ChatDebugViewEvents::-webkit-scrollbar-thumb:hover {
1565
- background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, 0.7));
2068
+ .ChatDebugViewEvents {
2069
+ display: flex;
2070
+ flex-direction: column;
2071
+ overflow: auto;
2072
+ min-width: 0;
2073
+ min-height: 0;
2074
+ scrollbar-width: thin;
2075
+ scrollbar-color: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, 0.4)) transparent;
1566
2076
  }
1567
2077
 
1568
- .ChatDebugViewEvents::-webkit-scrollbar-thumb:active {
1569
- background: var(--vscode-scrollbarSlider-activeBackground, rgba(191, 191, 191, 0.4));
2078
+ .ChatDebugViewEvents--timeline {
2079
+ gap: 0;
1570
2080
  }
1571
2081
 
1572
- .ChatDebugViewEvent {
1573
- margin: 0;
1574
- padding: 8px;
2082
+ .ChatDebugView--devtools .ChatDebugViewEvents {
1575
2083
  border: 1px solid var(--vscode-editorWidget-border, #454545);
1576
2084
  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);
1581
- font-size: 12px;
1582
- user-select: text;
2085
+ margin-bottom: 0;
2086
+ overflow: hidden;
1583
2087
  }
1584
2088
 
1585
- .ChatDebugViewEmpty {
1586
- opacity: 0.8;
2089
+ .ChatDebugViewEventsFullWidth {
2090
+ flex: 1 1 100%;
1587
2091
  }
1588
2092
 
1589
- .ChatDebugViewError {
1590
- color: var(--vscode-errorForeground, #f14c4c);
1591
- white-space: normal;
2093
+ .ChatDebugViewDevtoolsMain {
2094
+ display: flex;
2095
+ flex-wrap: wrap;
2096
+ align-items: stretch;
2097
+ gap: 8px;
2098
+ min-width: 0;
2099
+ min-height: 0;
2100
+ overflow: hidden;
1592
2101
  }
1593
2102
 
1594
- .TokenText {
1595
- color: var(--vscode-editor-foreground, inherit);
2103
+ .ChatDebugViewDevtoolsMain > .ChatDebugViewEvents {
2104
+ flex: 1 1 480px;
2105
+ min-width: 0;
1596
2106
  }
1597
2107
 
1598
- .TokenKey {
1599
- color: var(--vscode-symbolIcon-propertyForeground, var(--vscode-editor-foreground, inherit));
2108
+ .ChatDebugViewDevtoolsMain > .ChatDebugViewDetails {
2109
+ flex: 0 0 clamp(320px, 32vw, 420px);
1600
2110
  }
1601
2111
 
1602
- .TokenString {
1603
- color: var(--vscode-debugTokenExpression-string, var(--vscode-charts-green, #89d185));
2112
+ .ChatDebugViewTable {
2113
+ display: flex;
2114
+ flex-direction: column;
2115
+ min-height: 0;
2116
+ flex: 1 1 auto;
1604
2117
  }
1605
2118
 
1606
- .TokenNumeric {
1607
- color: var(--vscode-debugTokenExpression-number, var(--vscode-charts-blue, #75beff));
2119
+ .ChatDebugViewTimeline {
2120
+ display: flex;
2121
+ flex-direction: column;
2122
+ gap: 8px;
2123
+ padding: 10px;
2124
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2125
+ background: color-mix(in srgb, var(--vscode-editorWidget-background, transparent) 82%, var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.12)) 18%);
1608
2126
  }
1609
2127
 
1610
- .TokenBoolean {
1611
- color: var(--vscode-debugTokenExpression-boolean, var(--vscode-charts-yellow, #dcdcaa));
2128
+ .ChatDebugViewTimelineTop {
2129
+ display: flex;
2130
+ align-items: baseline;
2131
+ justify-content: space-between;
2132
+ gap: 8px;
2133
+ flex-wrap: wrap;
1612
2134
  }
1613
2135
 
1614
- .ChatOrderedList{
1615
- margin:0;
1616
- padding:0;
1617
- padding-left:10px;
2136
+ .ChatDebugViewTimelineTitle {
2137
+ font-size: 12px;
2138
+ font-weight: 600;
1618
2139
  }
1619
2140
 
1620
- .ChatOrderedListItem{
1621
- margin:0;
1622
- padding:0;
2141
+ .ChatDebugViewTimelineSummary {
2142
+ font-size: 12px;
2143
+ opacity: 0.8;
1623
2144
  }
1624
2145
 
1625
- .ChatToolCalls{
1626
- margin:0;
1627
- padding:0;
2146
+ .ChatDebugViewTimelineControls {
2147
+ display: flex;
2148
+ align-items: center;
2149
+ gap: 8px;
2150
+ flex-wrap: wrap;
2151
+ }
2152
+
2153
+ .ChatDebugViewTimelineReset {
2154
+ display: inline-flex;
2155
+ align-items: center;
2156
+ justify-content: center;
2157
+ min-height: 28px;
2158
+ padding: 0 12px;
2159
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2160
+ border-radius: 999px;
2161
+ cursor: pointer;
2162
+ font-size: 12px;
2163
+ }
2164
+
2165
+ .ChatDebugViewTimelineResetSelected {
2166
+ border-color: var(--vscode-focusBorder, #007fd4);
2167
+ background: var(--vscode-list-activeSelectionBackground, rgba(14, 99, 156, 0.35));
2168
+ color: var(--vscode-list-activeSelectionForeground, inherit);
2169
+ }
2170
+
2171
+ .ChatDebugViewTimelineBuckets {
2172
+ display: flex;
2173
+ align-items: end;
2174
+ gap: 4px;
2175
+ min-height: 60px;
2176
+ }
2177
+
2178
+ .ChatDebugViewTimelineBucket {
2179
+ display: flex;
2180
+ align-items: stretch;
2181
+ flex: 1 1 10px;
2182
+ min-width: 10px;
2183
+ min-height: 60px;
2184
+ cursor: pointer;
2185
+ }
2186
+
2187
+ .ChatDebugViewTimelinePresetInput {
2188
+ position: absolute;
2189
+ opacity: 0;
2190
+ pointer-events: none;
2191
+ }
2192
+
2193
+ .ChatDebugViewTimelineBucketBar {
2194
+ width: 100%;
2195
+ display: flex;
2196
+ flex-direction: column;
2197
+ justify-content: flex-end;
2198
+ gap: 2px;
2199
+ padding: 4px 2px;
2200
+ border: 1px solid transparent;
2201
+ border-radius: 4px;
2202
+ background: color-mix(in srgb, var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.18)) 68%, transparent 32%);
2203
+ }
2204
+
2205
+ .ChatDebugViewTimelineBucketSelected .ChatDebugViewTimelineBucketBar,
2206
+ .ChatDebugViewTimelineBucketBarSelected {
2207
+ background: color-mix(in srgb, var(--vscode-charts-blue, #75beff) 72%, transparent 28%);
2208
+ border-color: var(--vscode-focusBorder, #007fd4);
2209
+ }
2210
+
2211
+ .ChatDebugViewTimelineBucketUnit {
2212
+ width: 100%;
2213
+ height: 4px;
2214
+ border-radius: 999px;
2215
+ background: var(--vscode-charts-blue, #75beff);
2216
+ }
2217
+
2218
+ .ChatDebugViewTimelineBucketUnitEmpty {
2219
+ opacity: 0.35;
2220
+ background: var(--vscode-editorWidget-border, #454545);
2221
+ }
2222
+
2223
+ .ChatDebugViewTableHeader,
2224
+ .ChatDebugViewEventRow {
2225
+ display: flex;
2226
+ align-items: center;
2227
+ gap: 8px;
2228
+ }
2229
+
2230
+ .ChatDebugViewTableHeader {
2231
+ padding: 8px;
2232
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2233
+ background: var(--vscode-editorWidget-background, transparent);
2234
+ position: sticky;
2235
+ top: 0;
2236
+ z-index: 1;
2237
+ }
2238
+
2239
+ .ChatDebugViewHeaderCell {
2240
+ font-size: 11px;
2241
+ text-transform: uppercase;
2242
+ letter-spacing: 0.04em;
2243
+ opacity: 0.8;
2244
+ }
2245
+
2246
+ .ChatDebugViewTableBody {
2247
+ overflow: auto;
2248
+ min-height: 0;
2249
+ flex: 1 1 auto;
2250
+ contain: strict;
2251
+ }
2252
+
2253
+ .ChatDebugViewEventRowLabel {
2254
+ display: block;
2255
+ }
2256
+
2257
+ .ChatDebugViewEventRowLabelSelected .ChatDebugViewEventRow,
2258
+ .ChatDebugViewEventRowSelected {
2259
+ background: var(--vscode-list-activeSelectionBackground, rgba(14, 99, 156, 0.35));
2260
+ color: var(--vscode-list-activeSelectionForeground, inherit);
2261
+ }
2262
+
2263
+ .ChatDebugViewEventRowInput {
2264
+ position: absolute;
2265
+ opacity: 0;
2266
+ pointer-events: none;
2267
+ }
2268
+
2269
+ .ChatDebugViewEventRow {
2270
+ padding: 8px;
2271
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2272
+ cursor: pointer;
2273
+ }
2274
+
2275
+ .ChatDebugViewEventRow:hover {
2276
+ background: var(--vscode-list-hoverBackground, rgba(90, 93, 94, 0.31));
2277
+ }
2278
+
2279
+ .ChatDebugViewCell {
2280
+ overflow: hidden;
2281
+ text-overflow: ellipsis;
2282
+ white-space: nowrap;
2283
+ min-width: 0;
2284
+ }
2285
+
2286
+ .ChatDebugViewCellType {
2287
+ flex: 1 1 140px;
2288
+ min-width: 0;
2289
+ }
2290
+
2291
+ .ChatDebugViewCellTime {
2292
+ flex: 1 1 180px;
2293
+ min-width: 0;
2294
+ }
2295
+
2296
+ .ChatDebugViewCellDuration {
2297
+ flex: 0 0 90px;
2298
+ text-align: right;
2299
+ }
2300
+
2301
+ .ChatDebugViewCellStatus {
2302
+ flex: 0 0 64px;
2303
+ text-align: right;
2304
+ }
2305
+
2306
+ .ChatDebugViewDetails {
2307
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2308
+ border-radius: 6px;
2309
+ overflow: hidden;
2310
+ min-width: 0;
2311
+ min-height: 0;
2312
+ display: flex;
2313
+ flex-direction: column;
2314
+ contain: strict;
2315
+ }
2316
+
2317
+ .ChatDebugViewDetailsTop {
2318
+ display: flex;
2319
+ align-items: center;
2320
+ justify-content: space-between;
2321
+ padding: 8px;
2322
+ border-bottom: 1px solid var(--vscode-editorWidget-border, #454545);
2323
+ }
2324
+
2325
+ .ChatDebugViewDetailsTitle {
2326
+ font-size: 12px;
2327
+ font-weight: 600;
2328
+ }
2329
+
2330
+ .ChatDebugViewDetailsClose {
2331
+ width: 18px;
2332
+ height: 18px;
2333
+ appearance: none;
2334
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2335
+ border-radius: 4px;
2336
+ cursor: pointer;
2337
+ position: relative;
2338
+ }
2339
+
2340
+ .ChatDebugViewDetailsClose::before,
2341
+ .ChatDebugViewDetailsClose::after {
2342
+ content: '';
2343
+ position: absolute;
2344
+ left: 50%;
2345
+ top: 50%;
2346
+ width: 10px;
2347
+ height: 1px;
2348
+ background: currentColor;
2349
+ }
2350
+
2351
+ .ChatDebugViewDetailsClose::before {
2352
+ transform: translate(-50%, -50%) rotate(45deg);
2353
+ }
2354
+
2355
+ .ChatDebugViewDetailsClose::after {
2356
+ transform: translate(-50%, -50%) rotate(-45deg);
2357
+ }
2358
+
2359
+ .ChatDebugViewDetailsBody {
2360
+ overflow: auto;
2361
+ padding: 8px;
2362
+ flex: 1 1 auto;
2363
+ min-height: 0;
2364
+ contain: strict;
2365
+ }
2366
+
2367
+ .ChatDebugViewEvents::-webkit-scrollbar {
2368
+ width: 10px;
2369
+ height: 10px;
2370
+ }
2371
+
2372
+ .ChatDebugViewEvents::-webkit-scrollbar-track {
2373
+ background: transparent;
2374
+ }
2375
+
2376
+ .ChatDebugViewEvents::-webkit-scrollbar-thumb {
2377
+ background: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, 0.4));
2378
+ border-radius: 999px;
2379
+ border: 2px solid transparent;
2380
+ background-clip: content-box;
2381
+ }
2382
+
2383
+ .ChatDebugViewEvents::-webkit-scrollbar-thumb:hover {
2384
+ background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, 0.7));
2385
+ }
2386
+
2387
+ .ChatDebugViewEvents::-webkit-scrollbar-thumb:active {
2388
+ background: var(--vscode-scrollbarSlider-activeBackground, rgba(191, 191, 191, 0.4));
2389
+ }
2390
+
2391
+ .ChatDebugViewEvent {
2392
+ margin: 0;
2393
+ padding: 8px;
2394
+ border: 1px solid var(--vscode-editorWidget-border, #454545);
2395
+ border-radius: 6px;
2396
+ margin-bottom: 8px;
2397
+ white-space: pre-wrap;
2398
+ word-break: break-word;
2399
+ font-family: var(--vscode-editor-font-family, monospace);
2400
+ font-size: 12px;
2401
+ user-select: text;
2402
+ }
2403
+
2404
+ .ChatDebugViewEmpty {
2405
+ opacity: 0.8;
2406
+ }
2407
+
2408
+ .ChatDebugViewError {
2409
+ color: var(--vscode-errorForeground, #f14c4c);
2410
+ white-space: normal;
2411
+ }
2412
+
2413
+ .TokenText {
2414
+ color: var(--vscode-editor-foreground, inherit);
2415
+ }
2416
+
2417
+ .TokenKey {
2418
+ color: var(--vscode-symbolIcon-propertyForeground, var(--vscode-editor-foreground, inherit));
2419
+ }
2420
+
2421
+ .TokenString {
2422
+ color: var(--vscode-debugTokenExpression-string, var(--vscode-charts-green, #89d185));
2423
+ }
2424
+
2425
+ .TokenNumeric {
2426
+ color: var(--vscode-debugTokenExpression-number, var(--vscode-charts-blue, #75beff));
2427
+ }
2428
+
2429
+ .TokenBoolean {
2430
+ color: var(--vscode-debugTokenExpression-boolean, var(--vscode-charts-yellow, #dcdcaa));
2431
+ }
2432
+
2433
+ .ChatOrderedList{
2434
+ margin:0;
2435
+ padding:0;
2436
+ padding-left:10px;
2437
+ }
2438
+
2439
+ .ChatOrderedListItem{
2440
+ margin:0;
2441
+ padding:0;
2442
+ }
2443
+
2444
+ .ChatToolCalls{
2445
+ margin:0;
2446
+ padding:0;
1628
2447
  }
1629
2448
  `;
1630
2449
  };
@@ -1640,6 +2459,7 @@ const Span = 8;
1640
2459
  const Text = 12;
1641
2460
  const Pre = 51;
1642
2461
  const Label = 66;
2462
+ const Reference = 100;
1643
2463
 
1644
2464
  const text = data => {
1645
2465
  return {
@@ -1649,8 +2469,422 @@ const text = data => {
1649
2469
  };
1650
2470
  };
1651
2471
 
2472
+ const SetText = 1;
2473
+ const Replace = 2;
2474
+ const SetAttribute = 3;
2475
+ const RemoveAttribute = 4;
2476
+ const Add = 6;
2477
+ const NavigateChild = 7;
2478
+ const NavigateParent = 8;
2479
+ const RemoveChild = 9;
2480
+ const NavigateSibling = 10;
2481
+ const SetReferenceNodeUid = 11;
2482
+
2483
+ const isKey = key => {
2484
+ return key !== 'type' && key !== 'childCount';
2485
+ };
2486
+
2487
+ const getKeys = node => {
2488
+ const keys = Object.keys(node).filter(isKey);
2489
+ return keys;
2490
+ };
2491
+
2492
+ const arrayToTree = nodes => {
2493
+ const result = [];
2494
+ let i = 0;
2495
+ while (i < nodes.length) {
2496
+ const node = nodes[i];
2497
+ const {
2498
+ children,
2499
+ nodesConsumed
2500
+ } = getChildrenWithCount(nodes, i + 1, node.childCount || 0);
2501
+ result.push({
2502
+ node,
2503
+ children
2504
+ });
2505
+ i += 1 + nodesConsumed;
2506
+ }
2507
+ return result;
2508
+ };
2509
+ const getChildrenWithCount = (nodes, startIndex, childCount) => {
2510
+ if (childCount === 0) {
2511
+ return {
2512
+ children: [],
2513
+ nodesConsumed: 0
2514
+ };
2515
+ }
2516
+ const children = [];
2517
+ let i = startIndex;
2518
+ let remaining = childCount;
2519
+ let totalConsumed = 0;
2520
+ while (remaining > 0 && i < nodes.length) {
2521
+ const node = nodes[i];
2522
+ const nodeChildCount = node.childCount || 0;
2523
+ const {
2524
+ children: nodeChildren,
2525
+ nodesConsumed
2526
+ } = getChildrenWithCount(nodes, i + 1, nodeChildCount);
2527
+ children.push({
2528
+ node,
2529
+ children: nodeChildren
2530
+ });
2531
+ const nodeSize = 1 + nodesConsumed;
2532
+ i += nodeSize;
2533
+ totalConsumed += nodeSize;
2534
+ remaining--;
2535
+ }
2536
+ return {
2537
+ children,
2538
+ nodesConsumed: totalConsumed
2539
+ };
2540
+ };
2541
+
2542
+ const compareNodes = (oldNode, newNode) => {
2543
+ const patches = [];
2544
+ // Check if node type changed - return null to signal incompatible nodes
2545
+ // (caller should handle this with a Replace operation)
2546
+ if (oldNode.type !== newNode.type) {
2547
+ return null;
2548
+ }
2549
+ // Handle reference nodes - special handling for uid changes
2550
+ if (oldNode.type === Reference) {
2551
+ if (oldNode.uid !== newNode.uid) {
2552
+ patches.push({
2553
+ type: SetReferenceNodeUid,
2554
+ uid: newNode.uid
2555
+ });
2556
+ }
2557
+ return patches;
2558
+ }
2559
+ // Handle text nodes
2560
+ if (oldNode.type === Text && newNode.type === Text) {
2561
+ if (oldNode.text !== newNode.text) {
2562
+ patches.push({
2563
+ type: SetText,
2564
+ value: newNode.text
2565
+ });
2566
+ }
2567
+ return patches;
2568
+ }
2569
+ // Compare attributes
2570
+ const oldKeys = getKeys(oldNode);
2571
+ const newKeys = getKeys(newNode);
2572
+ // Check for attribute changes
2573
+ for (const key of newKeys) {
2574
+ if (oldNode[key] !== newNode[key]) {
2575
+ patches.push({
2576
+ type: SetAttribute,
2577
+ key,
2578
+ value: newNode[key]
2579
+ });
2580
+ }
2581
+ }
2582
+ // Check for removed attributes
2583
+ for (const key of oldKeys) {
2584
+ if (!(key in newNode)) {
2585
+ patches.push({
2586
+ type: RemoveAttribute,
2587
+ key
2588
+ });
2589
+ }
2590
+ }
2591
+ return patches;
2592
+ };
2593
+
2594
+ const treeToArray = node => {
2595
+ const result = [node.node];
2596
+ for (const child of node.children) {
2597
+ result.push(...treeToArray(child));
2598
+ }
2599
+ return result;
2600
+ };
2601
+
2602
+ const diffChildren = (oldChildren, newChildren, patches) => {
2603
+ const maxLength = Math.max(oldChildren.length, newChildren.length);
2604
+ // Track where we are: -1 means at parent, >= 0 means at child index
2605
+ let currentChildIndex = -1;
2606
+ // Collect indices of children to remove (we'll add these patches at the end in reverse order)
2607
+ const indicesToRemove = [];
2608
+ for (let i = 0; i < maxLength; i++) {
2609
+ const oldNode = oldChildren[i];
2610
+ const newNode = newChildren[i];
2611
+ if (!oldNode && !newNode) {
2612
+ continue;
2613
+ }
2614
+ if (!oldNode) {
2615
+ // Add new node - we should be at the parent
2616
+ if (currentChildIndex >= 0) {
2617
+ // Navigate back to parent
2618
+ patches.push({
2619
+ type: NavigateParent
2620
+ });
2621
+ currentChildIndex = -1;
2622
+ }
2623
+ // Flatten the entire subtree so renderInternal can handle it
2624
+ const flatNodes = treeToArray(newNode);
2625
+ patches.push({
2626
+ type: Add,
2627
+ nodes: flatNodes
2628
+ });
2629
+ } else if (newNode) {
2630
+ // Compare nodes to see if we need any patches
2631
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2632
+ // If nodePatches is null, the node types are incompatible - need to replace
2633
+ if (nodePatches === null) {
2634
+ // Navigate to this child
2635
+ if (currentChildIndex === -1) {
2636
+ patches.push({
2637
+ type: NavigateChild,
2638
+ index: i
2639
+ });
2640
+ currentChildIndex = i;
2641
+ } else if (currentChildIndex !== i) {
2642
+ patches.push({
2643
+ type: NavigateSibling,
2644
+ index: i
2645
+ });
2646
+ currentChildIndex = i;
2647
+ }
2648
+ // Replace the entire subtree
2649
+ const flatNodes = treeToArray(newNode);
2650
+ patches.push({
2651
+ type: Replace,
2652
+ nodes: flatNodes
2653
+ });
2654
+ // After replace, we're at the new element (same position)
2655
+ continue;
2656
+ }
2657
+ // Check if we need to recurse into children
2658
+ const hasChildrenToCompare = oldNode.children.length > 0 || newNode.children.length > 0;
2659
+ // Only navigate to this element if we need to do something
2660
+ if (nodePatches.length > 0 || hasChildrenToCompare) {
2661
+ // Navigate to this child if not already there
2662
+ if (currentChildIndex === -1) {
2663
+ patches.push({
2664
+ type: NavigateChild,
2665
+ index: i
2666
+ });
2667
+ currentChildIndex = i;
2668
+ } else if (currentChildIndex !== i) {
2669
+ patches.push({
2670
+ type: NavigateSibling,
2671
+ index: i
2672
+ });
2673
+ currentChildIndex = i;
2674
+ }
2675
+ // Apply node patches (these apply to the current element, not children)
2676
+ if (nodePatches.length > 0) {
2677
+ patches.push(...nodePatches);
2678
+ }
2679
+ // Compare children recursively
2680
+ if (hasChildrenToCompare) {
2681
+ diffChildren(oldNode.children, newNode.children, patches);
2682
+ }
2683
+ }
2684
+ } else {
2685
+ // Remove old node - collect the index for later removal
2686
+ indicesToRemove.push(i);
2687
+ }
2688
+ }
2689
+ // Navigate back to parent if we ended at a child
2690
+ if (currentChildIndex >= 0) {
2691
+ patches.push({
2692
+ type: NavigateParent
2693
+ });
2694
+ currentChildIndex = -1;
2695
+ }
2696
+ // Add remove patches in reverse order (highest index first)
2697
+ // This ensures indices remain valid as we remove
2698
+ for (let j = indicesToRemove.length - 1; j >= 0; j--) {
2699
+ patches.push({
2700
+ type: RemoveChild,
2701
+ index: indicesToRemove[j]
2702
+ });
2703
+ }
2704
+ };
2705
+ const diffTrees = (oldTree, newTree, patches, path) => {
2706
+ // At the root level (path.length === 0), we're already AT the element
2707
+ // So we compare the root node directly, then compare its children
2708
+ if (path.length === 0 && oldTree.length === 1 && newTree.length === 1) {
2709
+ const oldNode = oldTree[0];
2710
+ const newNode = newTree[0];
2711
+ // Compare root nodes
2712
+ const nodePatches = compareNodes(oldNode.node, newNode.node);
2713
+ // If nodePatches is null, the root node types are incompatible - need to replace
2714
+ if (nodePatches === null) {
2715
+ const flatNodes = treeToArray(newNode);
2716
+ patches.push({
2717
+ type: Replace,
2718
+ nodes: flatNodes
2719
+ });
2720
+ return;
2721
+ }
2722
+ if (nodePatches.length > 0) {
2723
+ patches.push(...nodePatches);
2724
+ }
2725
+ // Compare children
2726
+ if (oldNode.children.length > 0 || newNode.children.length > 0) {
2727
+ diffChildren(oldNode.children, newNode.children, patches);
2728
+ }
2729
+ } else {
2730
+ // Non-root level or multiple root elements - use the regular comparison
2731
+ diffChildren(oldTree, newTree, patches);
2732
+ }
2733
+ };
2734
+
2735
+ const removeTrailingNavigationPatches = patches => {
2736
+ // Find the last non-navigation patch
2737
+ let lastNonNavigationIndex = -1;
2738
+ for (let i = patches.length - 1; i >= 0; i--) {
2739
+ const patch = patches[i];
2740
+ if (patch.type !== NavigateChild && patch.type !== NavigateParent && patch.type !== NavigateSibling) {
2741
+ lastNonNavigationIndex = i;
2742
+ break;
2743
+ }
2744
+ }
2745
+ // Return patches up to and including the last non-navigation patch
2746
+ return lastNonNavigationIndex === -1 ? [] : patches.slice(0, lastNonNavigationIndex + 1);
2747
+ };
2748
+
2749
+ const diffTree = (oldNodes, newNodes) => {
2750
+ // Step 1: Convert flat arrays to tree structures
2751
+ const oldTree = arrayToTree(oldNodes);
2752
+ const newTree = arrayToTree(newNodes);
2753
+ // Step 3: Compare the trees
2754
+ const patches = [];
2755
+ diffTrees(oldTree, newTree, patches, []);
2756
+ // Remove trailing navigation patches since they serve no purpose
2757
+ return removeTrailingNavigationPatches(patches);
2758
+ };
2759
+
1652
2760
  const HandleInput = 4;
1653
2761
  const HandleFilterInput = 5;
2762
+ const HandleSimpleInput = 6;
2763
+ const HandleEventRowClick = 7;
2764
+
2765
+ const getDurationText = event => {
2766
+ const explicitDuration = event.durationMs ?? event.duration;
2767
+ if (typeof explicitDuration === 'number' && Number.isFinite(explicitDuration)) {
2768
+ return `${explicitDuration}ms`;
2769
+ }
2770
+ const start = toTimeNumber(event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp);
2771
+ const end = toTimeNumber(event.ended ?? event.endTime ?? event.endTimestamp ?? event.timestamp);
2772
+ if (start === undefined || end === undefined || end < start) {
2773
+ return '-';
2774
+ }
2775
+ return `${end - start}ms`;
2776
+ };
2777
+
2778
+ const timestampFormatter = new Intl.DateTimeFormat('en-US', {
2779
+ day: '2-digit',
2780
+ fractionalSecondDigits: 3,
2781
+ hour: '2-digit',
2782
+ hourCycle: 'h23',
2783
+ minute: '2-digit',
2784
+ month: 'short',
2785
+ second: '2-digit',
2786
+ timeZone: 'UTC',
2787
+ year: 'numeric'
2788
+ });
2789
+ const formatTimestamp = date => {
2790
+ return `${timestampFormatter.format(date)} UTC`;
2791
+ };
2792
+
2793
+ const getTimestampText = value => {
2794
+ if (typeof value === 'string') {
2795
+ const timestamp = Date.parse(value);
2796
+ if (!Number.isNaN(timestamp)) {
2797
+ return formatTimestamp(new Date(timestamp));
2798
+ }
2799
+ return value;
2800
+ }
2801
+ if (typeof value === 'number' && Number.isFinite(value)) {
2802
+ return formatTimestamp(new Date(value));
2803
+ }
2804
+ return '-';
2805
+ };
2806
+
2807
+ const getEndText = event => {
2808
+ return getTimestampText(event.ended ?? event.endTime ?? event.endTimestamp ?? event.timestamp);
2809
+ };
2810
+
2811
+ const getStartText = event => {
2812
+ return getTimestampText(event.started ?? event.startTime ?? event.startTimestamp ?? event.timestamp);
2813
+ };
2814
+
2815
+ const hasErrorStatus = event => {
2816
+ if (event.type === 'error') {
2817
+ return true;
2818
+ }
2819
+ if (event.success === false || event.ok === false) {
2820
+ return true;
2821
+ }
2822
+ const {
2823
+ status
2824
+ } = event;
2825
+ if (typeof status === 'number' && status >= 400) {
2826
+ return true;
2827
+ }
2828
+ if (typeof status === 'string') {
2829
+ const parsedStatus = Number(status);
2830
+ if (Number.isFinite(parsedStatus) && parsedStatus >= 400) {
2831
+ return true;
2832
+ }
2833
+ }
2834
+ return typeof event.error === 'string' || typeof event.errorMessage === 'string' || typeof event.exception === 'string';
2835
+ };
2836
+
2837
+ const getStatusText = event => {
2838
+ return hasErrorStatus(event) ? '400' : '200';
2839
+ };
2840
+
2841
+ const getDevtoolsRows = (events, selectedEventIndex) => {
2842
+ if (events.length === 0) {
2843
+ return [{
2844
+ childCount: 1,
2845
+ className: 'ChatDebugViewEmpty',
2846
+ type: Div
2847
+ }, text('No events')];
2848
+ }
2849
+ const rows = [];
2850
+ for (let i = 0; i < events.length; i++) {
2851
+ const event = events[i];
2852
+ const isSelected = selectedEventIndex === i;
2853
+ const rowIndex = String(i);
2854
+ rows.push({
2855
+ childCount: 5,
2856
+ className: `ChatDebugViewEventRow${isSelected ? ' ChatDebugViewEventRowSelected' : ''}`,
2857
+ 'data-index': rowIndex,
2858
+ type: Div
2859
+ }, {
2860
+ childCount: 1,
2861
+ className: 'ChatDebugViewCell ChatDebugViewCellType',
2862
+ 'data-index': rowIndex,
2863
+ type: Div
2864
+ }, text(event.type), {
2865
+ childCount: 1,
2866
+ className: 'ChatDebugViewCell ChatDebugViewCellTime',
2867
+ 'data-index': rowIndex,
2868
+ type: Div
2869
+ }, text(getStartText(event)), {
2870
+ childCount: 1,
2871
+ className: 'ChatDebugViewCell ChatDebugViewCellTime',
2872
+ 'data-index': rowIndex,
2873
+ type: Div
2874
+ }, text(getEndText(event)), {
2875
+ childCount: 1,
2876
+ className: 'ChatDebugViewCell ChatDebugViewCellDuration',
2877
+ 'data-index': rowIndex,
2878
+ type: Div
2879
+ }, text(getDurationText(event)), {
2880
+ childCount: 1,
2881
+ className: 'ChatDebugViewCell ChatDebugViewCellStatus',
2882
+ 'data-index': rowIndex,
2883
+ type: Div
2884
+ }, text(getStatusText(event)));
2885
+ }
2886
+ return rows;
2887
+ };
1654
2888
 
1655
2889
  const numberRegex = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
1656
2890
  const whitespaceRegex = /\s/u;
@@ -1755,7 +2989,227 @@ const getEventNode = event => {
1755
2989
  }, ...tokenNodes];
1756
2990
  };
1757
2991
 
1758
- const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedEvents, showInputEvents, showResponsePartEvents, events) => {
2992
+ const trailingZeroFractionRegex = /\.0+$/;
2993
+ const trailingFractionZeroRegex = /(\.\d*?)0+$/;
2994
+ const formatTimelinePresetValue = value => {
2995
+ return value.toFixed(3).replace(trailingZeroFractionRegex, '').replace(trailingFractionZeroRegex, '$1');
2996
+ };
2997
+
2998
+ const formatTimelineSeconds = value => {
2999
+ if (Number.isInteger(value)) {
3000
+ return `${value}s`;
3001
+ }
3002
+ return `${Number(value.toFixed(1))}s`;
3003
+ };
3004
+ const getTimelineSummary = (timelineEvents, timelineStartSeconds, timelineEndSeconds) => {
3005
+ const timelineInfo = getTimelineInfo(timelineEvents, timelineStartSeconds, timelineEndSeconds);
3006
+ if (timelineInfo.hasSelection && timelineInfo.startSeconds !== null && timelineInfo.endSeconds !== null) {
3007
+ return `Window ${formatTimelineSeconds(timelineInfo.startSeconds)}-${formatTimelineSeconds(timelineInfo.endSeconds)} of ${formatTimelineSeconds(timelineInfo.durationSeconds)}`;
3008
+ }
3009
+ return `Window 0s-${formatTimelineSeconds(timelineInfo.durationSeconds)} of ${formatTimelineSeconds(timelineInfo.durationSeconds)}`;
3010
+ };
3011
+
3012
+ const getTimelineNodes = (timelineEvents, timelineStartSeconds, timelineEndSeconds) => {
3013
+ const timelineInfo = getTimelineInfo(timelineEvents, timelineStartSeconds, timelineEndSeconds);
3014
+ if (timelineInfo.buckets.length === 0) {
3015
+ return [];
3016
+ }
3017
+ return [{
3018
+ childCount: 3,
3019
+ className: 'ChatDebugViewTimeline',
3020
+ type: Div
3021
+ }, {
3022
+ childCount: 2,
3023
+ className: 'ChatDebugViewTimelineTop',
3024
+ type: Div
3025
+ }, {
3026
+ childCount: 1,
3027
+ className: 'ChatDebugViewTimelineTitle',
3028
+ type: Div
3029
+ }, text('Timeline'), {
3030
+ childCount: 1,
3031
+ className: 'ChatDebugViewTimelineSummary',
3032
+ type: Div
3033
+ }, text(getTimelineSummary(timelineEvents, timelineStartSeconds, timelineEndSeconds)), {
3034
+ childCount: 1,
3035
+ className: 'ChatDebugViewTimelineControls',
3036
+ type: Div
3037
+ }, {
3038
+ childCount: 2,
3039
+ className: `ChatDebugViewTimelineReset${timelineInfo.hasSelection ? '' : ' ChatDebugViewTimelineResetSelected'}`,
3040
+ type: Label
3041
+ }, {
3042
+ checked: !timelineInfo.hasSelection,
3043
+ childCount: 0,
3044
+ className: 'ChatDebugViewTimelinePresetInput',
3045
+ inputType: 'radio',
3046
+ name: TimelineRangePreset,
3047
+ onChange: HandleSimpleInput,
3048
+ type: Input,
3049
+ value: ''
3050
+ }, text('All'), {
3051
+ childCount: timelineInfo.buckets.length,
3052
+ className: 'ChatDebugViewTimelineBuckets',
3053
+ type: Div
3054
+ }, ...timelineInfo.buckets.flatMap(bucket => {
3055
+ const presetValue = `${formatTimelinePresetValue(bucket.startSeconds)}:${formatTimelinePresetValue(bucket.endSeconds)}`;
3056
+ return [{
3057
+ childCount: 2,
3058
+ className: `ChatDebugViewTimelineBucket${bucket.isSelected ? ' ChatDebugViewTimelineBucketSelected' : ''}`,
3059
+ type: Label
3060
+ }, {
3061
+ checked: false,
3062
+ childCount: 0,
3063
+ className: 'ChatDebugViewTimelinePresetInput',
3064
+ inputType: 'radio',
3065
+ name: TimelineRangePreset,
3066
+ onChange: HandleSimpleInput,
3067
+ type: Input,
3068
+ value: presetValue
3069
+ }, {
3070
+ childCount: bucket.unitCount === 0 ? 1 : bucket.unitCount,
3071
+ className: `ChatDebugViewTimelineBucketBar${bucket.isSelected ? ' ChatDebugViewTimelineBucketBarSelected' : ''}`,
3072
+ type: Div
3073
+ }, ...(bucket.unitCount === 0 ? [{
3074
+ childCount: 0,
3075
+ className: 'ChatDebugViewTimelineBucketUnit ChatDebugViewTimelineBucketUnitEmpty',
3076
+ type: Div
3077
+ }] : Array.from({
3078
+ length: bucket.unitCount
3079
+ }).fill({
3080
+ childCount: 0,
3081
+ className: 'ChatDebugViewTimelineBucketUnit',
3082
+ type: Div
3083
+ }))];
3084
+ })];
3085
+ };
3086
+
3087
+ const getDevtoolsDom = (events, selectedEventIndex, timelineEvents, timelineStartSeconds, timelineEndSeconds) => {
3088
+ const rowNodes = getDevtoolsRows(events, selectedEventIndex);
3089
+ const timelineNodes = getTimelineNodes(timelineEvents, timelineStartSeconds, timelineEndSeconds);
3090
+ const selectedEvent = selectedEventIndex === null ? undefined : events[selectedEventIndex];
3091
+ const selectedEventNodes = selectedEvent ? getEventNode(selectedEvent) : [];
3092
+ const hasSelectedEvent = selectedEventNodes.length > 0;
3093
+ const eventsClassName = `${hasSelectedEvent ? 'ChatDebugViewEvents' : 'ChatDebugViewEvents ChatDebugViewEventsFullWidth'}${timelineNodes.length > 0 ? ' ChatDebugViewEvents--timeline' : ''}`;
3094
+ const eventsChildCount = timelineNodes.length > 0 ? 2 : 1;
3095
+ const detailsNodes = hasSelectedEvent ? [{
3096
+ childCount: 2,
3097
+ className: 'ChatDebugViewDetails',
3098
+ type: Div
3099
+ }, {
3100
+ childCount: 2,
3101
+ className: 'ChatDebugViewDetailsTop',
3102
+ type: Div
3103
+ }, {
3104
+ childCount: 1,
3105
+ className: 'ChatDebugViewDetailsTitle',
3106
+ type: Div
3107
+ }, text('Details'), {
3108
+ childCount: 0,
3109
+ className: 'ChatDebugViewDetailsClose',
3110
+ inputType: 'checkbox',
3111
+ name: CloseDetails,
3112
+ onChange: HandleSimpleInput,
3113
+ type: Input,
3114
+ value: 'close'
3115
+ }, {
3116
+ childCount: selectedEventNodes.length,
3117
+ className: 'ChatDebugViewDetailsBody',
3118
+ type: Div
3119
+ }, ...selectedEventNodes] : [];
3120
+ return [{
3121
+ childCount: hasSelectedEvent ? 2 : 1,
3122
+ className: 'ChatDebugViewDevtoolsMain',
3123
+ type: Div
3124
+ }, {
3125
+ childCount: eventsChildCount,
3126
+ className: eventsClassName,
3127
+ type: Div
3128
+ }, ...timelineNodes, {
3129
+ childCount: 2,
3130
+ className: 'ChatDebugViewTable',
3131
+ type: Div
3132
+ }, {
3133
+ childCount: 5,
3134
+ className: 'ChatDebugViewTableHeader',
3135
+ type: Div
3136
+ }, {
3137
+ childCount: 1,
3138
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellType',
3139
+ type: Div
3140
+ }, text('Type'), {
3141
+ childCount: 1,
3142
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellTime',
3143
+ type: Div
3144
+ }, text('Started'), {
3145
+ childCount: 1,
3146
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellTime',
3147
+ type: Div
3148
+ }, text('Ended'), {
3149
+ childCount: 1,
3150
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellDuration',
3151
+ type: Div
3152
+ }, text('Duration'), {
3153
+ childCount: 1,
3154
+ className: 'ChatDebugViewHeaderCell ChatDebugViewCellStatus',
3155
+ type: Div
3156
+ }, text('Status'), {
3157
+ childCount: rowNodes.length === 0 ? 1 : rowNodes.length,
3158
+ className: 'ChatDebugViewTableBody',
3159
+ onClick: HandleEventRowClick,
3160
+ type: Div
3161
+ }, ...rowNodes, ...detailsNodes];
3162
+ };
3163
+
3164
+ const getLegacyEventsDom = (errorMessage, emptyMessage, eventNodes) => {
3165
+ return [{
3166
+ childCount: eventNodes.length === 0 ? 1 : eventNodes.length,
3167
+ className: 'ChatDebugViewEvents',
3168
+ type: Div
3169
+ }, ...(eventNodes.length === 0 ? [{
3170
+ childCount: 1,
3171
+ className: errorMessage ? 'ChatDebugViewError' : 'ChatDebugViewEmpty',
3172
+ type: Div
3173
+ }, text(errorMessage || emptyMessage)] : eventNodes)];
3174
+ };
3175
+ const getQuickFilterNodes = eventCategoryFilter => {
3176
+ return [{
3177
+ childCount: options.length,
3178
+ className: 'ChatDebugViewQuickFilters',
3179
+ type: Div
3180
+ }, ...options.flatMap(option => {
3181
+ const isSelected = option.value === eventCategoryFilter;
3182
+ return [{
3183
+ childCount: 2,
3184
+ className: `ChatDebugViewQuickFilterPill${isSelected ? ' ChatDebugViewQuickFilterPillSelected' : ''}`,
3185
+ type: Label
3186
+ }, {
3187
+ checked: isSelected,
3188
+ childCount: 0,
3189
+ className: 'ChatDebugViewQuickFilterInput',
3190
+ inputType: 'radio',
3191
+ name: EventCategoryFilter,
3192
+ onChange: HandleInput,
3193
+ type: Input,
3194
+ value: option.value
3195
+ }, text(option.label)];
3196
+ })];
3197
+ };
3198
+ const getTimelineFilterDescription = (timelineStartSeconds, timelineEndSeconds) => {
3199
+ const trimmedStart = timelineStartSeconds.trim();
3200
+ const trimmedEnd = timelineEndSeconds.trim();
3201
+ if (trimmedStart && trimmedEnd) {
3202
+ return `${trimmedStart}s-${trimmedEnd}s`;
3203
+ }
3204
+ if (trimmedStart) {
3205
+ return `from ${trimmedStart}s`;
3206
+ }
3207
+ if (trimmedEnd) {
3208
+ return `to ${trimmedEnd}s`;
3209
+ }
3210
+ return '';
3211
+ };
3212
+ const getChatDebugViewDom = (errorMessage, filterValue, eventCategoryFilter, showEventStreamFinishedEvents, showInputEvents, showResponsePartEvents, useDevtoolsLayout, selectedEventIndex, timelineStartSeconds, timelineEndSeconds, timelineEvents, events) => {
1759
3213
  if (errorMessage) {
1760
3214
  return [{
1761
3215
  childCount: 1,
@@ -1769,13 +3223,29 @@ const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedE
1769
3223
  }
1770
3224
  const eventNodes = events.flatMap(getEventNode);
1771
3225
  const trimmedFilterValue = filterValue.trim();
1772
- const hasFilterValue = trimmedFilterValue.length > 0;
1773
- const noFilteredEventsMessage = `no events found matching ${trimmedFilterValue}`;
3226
+ const filterDescriptionParts = [];
3227
+ if (eventCategoryFilter !== All) {
3228
+ filterDescriptionParts.push(getEventCategoryFilterLabel(eventCategoryFilter).toLowerCase());
3229
+ }
3230
+ if (trimmedFilterValue) {
3231
+ filterDescriptionParts.push(trimmedFilterValue);
3232
+ }
3233
+ const timelineFilterDescription = getTimelineFilterDescription(timelineStartSeconds, timelineEndSeconds);
3234
+ if (timelineFilterDescription) {
3235
+ filterDescriptionParts.push(timelineFilterDescription);
3236
+ }
3237
+ const hasFilterValue = filterDescriptionParts.length > 0;
3238
+ const filterDescription = filterDescriptionParts.join(' ');
3239
+ const noFilteredEventsMessage = `no events found matching ${filterDescription}`;
1774
3240
  const eventCountText = events.length === 0 && hasFilterValue ? noFilteredEventsMessage : `${events.length} event${events.length === 1 ? '' : 's'}`;
1775
3241
  const emptyMessage = events.length === 0 && hasFilterValue ? noFilteredEventsMessage : 'No events';
3242
+ const safeSelectedEventIndex = selectedEventIndex === null || selectedEventIndex < 0 || selectedEventIndex >= events.length ? null : selectedEventIndex;
3243
+ const contentNodes = useDevtoolsLayout ? getDevtoolsDom(events, safeSelectedEventIndex, timelineEvents, timelineStartSeconds, timelineEndSeconds) : getLegacyEventsDom(errorMessage, emptyMessage, eventNodes);
3244
+ const quickFilterNodes = useDevtoolsLayout ? getQuickFilterNodes(eventCategoryFilter) : [];
3245
+ const rootChildCount = useDevtoolsLayout ? 4 : 3;
1776
3246
  return [{
1777
- childCount: 3,
1778
- className: 'ChatDebugView',
3247
+ childCount: rootChildCount,
3248
+ className: useDevtoolsLayout ? 'ChatDebugView ChatDebugView--devtools' : 'ChatDebugView',
1779
3249
  type: Div
1780
3250
  }, {
1781
3251
  childCount: 2,
@@ -1792,7 +3262,7 @@ const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedE
1792
3262
  type: Input,
1793
3263
  value: filterValue
1794
3264
  }, {
1795
- childCount: 3,
3265
+ childCount: 4,
1796
3266
  className: 'ChatDebugViewToggle',
1797
3267
  type: Div
1798
3268
  }, {
@@ -1829,71 +3299,21 @@ const getChatDebugViewDom = (errorMessage, filterValue, showEventStreamFinishedE
1829
3299
  onChange: HandleInput,
1830
3300
  type: Input
1831
3301
  }, text('Show response part events'), {
3302
+ childCount: 2,
3303
+ className: 'ChatDebugViewToggleLabel',
3304
+ type: Label
3305
+ }, {
3306
+ checked: useDevtoolsLayout,
3307
+ childCount: 0,
3308
+ inputType: 'checkbox',
3309
+ name: UseDevtoolsLayout,
3310
+ onChange: HandleInput,
3311
+ type: Input
3312
+ }, text('Use devtools layout'), ...quickFilterNodes, {
1832
3313
  childCount: 1,
1833
3314
  className: 'ChatDebugViewEventCount',
1834
3315
  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));
3316
+ }, text(eventCountText), ...contentNodes];
1897
3317
  };
1898
3318
 
1899
3319
  const withSessionEventIds = events => {
@@ -1905,16 +3325,28 @@ const withSessionEventIds = events => {
1905
3325
  });
1906
3326
  };
1907
3327
  const renderItems = (oldState, newState) => {
1908
- 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);
3328
+ if (newState.initial) {
3329
+ return [SetDom2, newState.uid, []];
3330
+ }
3331
+ const timelineEvents = getFilteredEvents(newState.events, newState.filterValue, newState.eventCategoryFilter, newState.showInputEvents, newState.showResponsePartEvents, newState.showEventStreamFinishedEvents);
3332
+ const filteredEvents = filterEventsByTimelineRange(timelineEvents, newState.timelineStartSeconds, newState.timelineEndSeconds);
3333
+ const dom = getChatDebugViewDom(newState.errorMessage, newState.filterValue, newState.eventCategoryFilter, newState.showEventStreamFinishedEvents, newState.showInputEvents, newState.showResponsePartEvents, newState.useDevtoolsLayout, newState.selectedEventIndex, newState.timelineStartSeconds, newState.timelineEndSeconds, withSessionEventIds(timelineEvents), withSessionEventIds(filteredEvents));
1911
3334
  return [SetDom2, newState.uid, dom];
1912
3335
  };
1913
3336
 
3337
+ const renderIncremental = (oldState, newState) => {
3338
+ const oldDom = renderItems(oldState, oldState)[2];
3339
+ const newDom = renderItems(newState, newState)[2];
3340
+ const patches = diffTree(oldDom, newDom);
3341
+ return [SetPatches, newState.uid, patches];
3342
+ };
3343
+
1914
3344
  const getRenderer = diffType => {
1915
3345
  switch (diffType) {
1916
3346
  case RenderCss:
1917
3347
  return renderCss;
3348
+ case RenderIncremental:
3349
+ return renderIncremental;
1918
3350
  case RenderItems:
1919
3351
  return renderItems;
1920
3352
  default:
@@ -1945,11 +3377,17 @@ const render2 = (uid, diffResult) => {
1945
3377
 
1946
3378
  const renderEventListeners = () => {
1947
3379
  return [{
3380
+ name: HandleEventRowClick,
3381
+ params: ['handleEventRowClick', 'event.target.dataset.index']
3382
+ }, {
1948
3383
  name: HandleFilterInput,
1949
3384
  params: ['handleInput', TargetName, TargetValue]
1950
3385
  }, {
1951
3386
  name: HandleInput,
1952
3387
  params: ['handleInput', TargetName, TargetValue, TargetChecked]
3388
+ }, {
3389
+ name: HandleSimpleInput,
3390
+ params: ['handleInput', TargetName, TargetValue]
1953
3391
  }];
1954
3392
  };
1955
3393
 
@@ -1966,23 +3404,31 @@ const resize = (state, dimensions) => {
1966
3404
 
1967
3405
  const saveState = state => {
1968
3406
  const {
3407
+ eventCategoryFilter,
1969
3408
  filterValue,
1970
3409
  height,
1971
3410
  sessionId,
1972
3411
  showEventStreamFinishedEvents,
1973
3412
  showInputEvents,
1974
3413
  showResponsePartEvents,
3414
+ timelineEndSeconds,
3415
+ timelineStartSeconds,
3416
+ useDevtoolsLayout,
1975
3417
  width,
1976
3418
  x,
1977
3419
  y
1978
3420
  } = state;
1979
3421
  return {
3422
+ eventCategoryFilter,
1980
3423
  filterValue,
1981
3424
  height,
1982
3425
  sessionId,
1983
3426
  showEventStreamFinishedEvents,
1984
3427
  showInputEvents,
1985
3428
  showResponsePartEvents,
3429
+ timelineEndSeconds,
3430
+ timelineStartSeconds,
3431
+ useDevtoolsLayout,
1986
3432
  width,
1987
3433
  x,
1988
3434
  y
@@ -1994,7 +3440,8 @@ const setEvents = (state, events) => {
1994
3440
  ...state,
1995
3441
  errorMessage: '',
1996
3442
  events,
1997
- initial: false
3443
+ initial: false,
3444
+ selectedEventIndex: null
1998
3445
  };
1999
3446
  };
2000
3447
 
@@ -2013,6 +3460,7 @@ const commandMap = {
2013
3460
  'ChatDebug.create': create,
2014
3461
  'ChatDebug.diff2': diff2,
2015
3462
  'ChatDebug.getCommandIds': getCommandIds,
3463
+ 'ChatDebug.handleEventRowClick': wrapCommand(handleEventRowClick),
2016
3464
  'ChatDebug.handleInput': wrapCommand(handleInput),
2017
3465
  'ChatDebug.loadContent': wrapCommand(loadContent),
2018
3466
  'ChatDebug.loadContent2': wrapCommand(loadContent),