@qiaolei81/copilot-session-viewer 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/controllers/sessionController.js +3 -3
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-be-responsive-on-mobile-viewport-1771605454041.json +0 -435
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-display-sessions-if-available-1771605462872.json +0 -435
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-JavaScript-errors-gracefully-1771605463381.json +0 -435
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-session-import-dialog-1771605466264.json +0 -435
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-have-working-infinite-scroll-elements-1771605454038.json +0 -435
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-homepage-with-basic-elements-1771605454001.json +0 -435
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-time-analysis-page-1771605464990.json +0 -1236
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-navigate-to-session-detail-page-1771605472595.json +0 -1177
- package/.nyc_output/coverage-e2e-merged.json +0 -1
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-display-session-list-1771605453565.json +0 -435
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-load-homepage-successfully-1771605453552.json +0 -435
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-navigate-to-session-detail-on-click-1771605469317.json +0 -1134
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-show-session-metadata-1771605460581.json +0 -435
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-display-Load-More-Sessions-button-when-there-are-more-sessions-1771605468486.json +0 -435
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-handle-API-errors-gracefully-during-infinite-scroll-1771605482161.json +0 -471
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-hide-Load-More-button-when-no-more-sessions-available-1771605478370.json +0 -471
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-load-additional-sessions-when-Load-More-button-is-clicked-1771605475059.json +0 -471
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-preserve-session-list-state-during-navigation-1771605494575.json +0 -1633
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-show-loading-state-when-Load-More-button-is-clicked-1771605475401.json +0 -471
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-trigger-infinite-scroll-when-scrolling-near-bottom-1771605476949.json +0 -471
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-clear-search-filter-1771605508542.json +0 -1255
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-event-list-1771605505572.json +0 -1156
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-session-metadata-1771605504552.json +0 -701
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-expand-and-collapse-tool-details-1771605515809.json +0 -1182
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-filter-events-by-search-1771605513421.json +0 -1245
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-load-session-detail-page-1771605494974.json +0 -701
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-toggle-content-visibility-1771605550729.json +0 -1177
- package/.nyc_output/coverage-unit.json +0 -21
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"url": "http://localhost:3838/session/1243bdda-b712-4714-b3c7-1b680c2be232",
|
|
4
|
-
"scriptId": "7",
|
|
5
|
-
"source": "\n window.sessionData = {\n sessionId: '1243bdda-b712-4714-b3c7-1b680c2be232',\n metadata: {\"type\":\"file\",\"source\":\"claude\",\"summary\":\"Configure E2E test coverage collection using Playwright's built-in coverage API and merge it with un\",\"cwd\":\"/Users/qiaolei/workspace/copilot-session-viewer\",\"created\":\"2026-02-20T16:27:55.809Z\",\"updated\":\"2026-02-20T16:32:55.406Z\",\"copilotVersion\":\"2.1.42\",\"sessionStatus\":\"completed\"},\n events: [] // Will be loaded asynchronously\n };\n ",
|
|
6
|
-
"functions": [
|
|
7
|
-
{
|
|
8
|
-
"functionName": "",
|
|
9
|
-
"isBlockCoverage": true,
|
|
10
|
-
"ranges": [
|
|
11
|
-
{
|
|
12
|
-
"startOffset": 0,
|
|
13
|
-
"endOffset": 492,
|
|
14
|
-
"count": 1
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"url": "http://localhost:3838/session/1243bdda-b712-4714-b3c7-1b680c2be232",
|
|
22
|
-
"scriptId": "8",
|
|
23
|
-
"source": "\n const { createApp, ref, computed, onMounted, onBeforeUnmount, reactive, watch } = Vue;\n const { DynamicScroller, DynamicScrollerItem } = window.VueVirtualScroller;\n \n const app = createApp({\n components: {\n DynamicScroller,\n DynamicScrollerItem\n },\n \n setup() {\n const sessionId = ref(window.sessionData.sessionId);\n const metadata = ref(window.sessionData.metadata);\n const exporting = ref(false);\n \n // Load sidebar state from localStorage\n const sidebarCollapsed = ref(\n localStorage.getItem('sidebarCollapsed') === 'true'\n );\n \n // Persist sidebar state to localStorage\n watch(sidebarCollapsed, (newValue) => {\n localStorage.setItem('sidebarCollapsed', newValue.toString());\n });\n \n const expandedTools = ref({});\n const expandedContent = ref({});\n const MAX_EXPANDED_ITEMS = 50; // Memory leak fix: Limit expanded items\n \n // Clean up old expansion state to prevent memory leak\n const cleanupExpansionState = () => {\n const toolKeys = Object.keys(expandedTools.value);\n if (toolKeys.length > MAX_EXPANDED_ITEMS) {\n // Keep only recent 50 expanded items\n const toRemove = toolKeys.slice(0, toolKeys.length - MAX_EXPANDED_ITEMS);\n toRemove.forEach(key => delete expandedTools.value[key]);\n }\n \n const contentKeys = Object.keys(expandedContent.value);\n if (contentKeys.length > MAX_EXPANDED_ITEMS) {\n const toRemove = contentKeys.slice(0, contentKeys.length - MAX_EXPANDED_ITEMS);\n toRemove.forEach(key => delete expandedContent.value[key]);\n }\n };\n \n const currentFilter = ref('all');\n const searchText = ref('');\n const debouncedSearchText = ref('');\n const currentTurnIndex = ref(0); // Current selected turn\n const scrollerRef = ref(null);\n const visibleRange = ref({ start: 0, end: 0 });\n \n // Debounce search input\n let searchTimeout = null;\n watch(searchText, (newValue) => {\n clearTimeout(searchTimeout);\n searchTimeout = setTimeout(() => {\n debouncedSearchText.value = newValue;\n }, 300);\n });\n \n // Memory leak fix: Clean up expansion state when filter/search changes\n watch(currentFilter, () => {\n cleanupExpansionState();\n });\n \n watch(debouncedSearchText, () => {\n cleanupExpansionState();\n });\n \n // Async loading state\n const loadedEvents = ref([]);\n const eventsLoading = ref(true);\n const eventsError = ref(null);\n \n // Flatten and sort events (stable sort using _fileIndex tiebreaker)\n const flatEvents = computed(() => {\n const events = loadedEvents.value\n .filter(e =>\n e.type !== 'assistant.turn_end' &&\n e.type !== 'assistant.turn_complete'\n )\n .sort((a, b) => {\n const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;\n const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;\n if (timeA !== timeB) return timeA - timeB;\n return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);\n })\n .map((e, index) => ({ \n ...e, \n virtualIndex: index,\n stableId: e.id || `${e.timestamp}-${e.type}-${index}` // Stable ID for toggle state\n }));\n return events;\n });\n \n // Helper: check if event matches search\n const matchesSearch = (e) => {\n if (!debouncedSearchText.value.trim()) return true;\n \n const search = debouncedSearchText.value.toLowerCase();\n // Only search in event.data fields, not type\n const content = [\n e.data?.message,\n e.data?.text,\n e.data?.content,\n e.data?.reason,\n e.data?.errorType,\n e.data?.previousModel,\n e.data?.newModel\n ].filter(Boolean).join(' ').toLowerCase();\n \n return content.includes(search);\n };\n \n // Events after search (before type filter) - used for filter counts\n const searchFilteredEvents = computed(() => {\n const excludeToolCalls = (e) => {\n const eventType = e.type || '';\n return eventType !== 'tool.execution_start' && eventType !== 'tool.execution_complete';\n };\n \n let events = flatEvents.value.filter(excludeToolCalls);\n \n // Apply search only (use debouncedSearchText for consistency)\n if (debouncedSearchText.value.trim()) {\n events = events.filter(matchesSearch);\n }\n \n return events;\n });\n \n // Final filtered events (search + type filter)\n const filteredEvents = computed(() => {\n let events = searchFilteredEvents.value;\n\n // Apply type filter\n if (currentFilter.value !== 'all') {\n events = events.filter(e => e.type === currentFilter.value);\n }\n\n // Divider types (no separator before these)\n const dividerTypes = ['assistant.turn_start', 'subagent.started', 'subagent.completed', 'subagent.failed'];\n \n // Mark events that shouldn't have separator\n const totalCount = events.length;\n return events.map((e, index) => {\n const nextItem = events[index + 1];\n const isLast = index === totalCount - 1;\n const nextIsDivider = nextItem && dividerTypes.includes(nextItem.type);\n \n return {\n ...e,\n filteredIndex: index,\n filteredTotal: totalCount,\n isLastEvent: isLast || nextIsDivider // Hide separator if last OR next is divider\n };\n });\n });\n \n // Event type counts (based on search results)\n const eventCounts = computed(() => {\n const counts = {};\n searchFilteredEvents.value.forEach(e => {\n if (e.type) {\n counts[e.type] = (counts[e.type] || 0) + 1;\n }\n });\n return counts;\n });\n \n // Search result count for display\n const searchResultCount = computed(() => {\n if (!debouncedSearchText.value.trim()) return null;\n const count = searchFilteredEvents.value.length;\n return count > 0 ? `${count} result${count !== 1 ? 's' : ''}` : 'No matches';\n });\n \n // Track expansion state changes for size-dependencies\n const expansionCount = computed(() => {\n const toolsExpanded = Object.keys(expandedTools.value).filter(k => expandedTools.value[k]).length;\n const contentExpanded = Object.keys(expandedContent.value).filter(k => expandedContent.value[k]).length;\n return toolsExpanded + contentExpanded;\n });\n \n // Available filters (with counts based on search results)\n const filters = computed(() => {\n const totalEvents = searchFilteredEvents.value.length;\n \n // Start with \"All\" filter\n const result = [{ type: 'all', label: `All (${totalEvents})`, count: totalEvents }];\n \n // Dynamically extract all event types from actual events\n const typeCounts = {};\n searchFilteredEvents.value.forEach(e => {\n if (e.type) {\n typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;\n }\n });\n \n // Convert to array and sort by count (descending)\n const sortedTypes = Object.entries(typeCounts)\n .sort((a, b) => b[1] - a[1]) // Sort by count descending\n .map(([type, count]) => ({\n type,\n label: `${type} (${count})`,\n count,\n disabled: false\n }));\n \n return [...result, ...sortedTypes];\n });\n \n // Turns\n const turns = computed(() => {\n const turnStarts = flatEvents.value.filter(e => e.type === 'assistant.turn_start');\n const allUserMessages = flatEvents.value.filter(e => e.type === 'user.message');\n\n return turnStarts.map((turn, idx) => {\n // Use idx as the display turn number (sequential, no duplicates)\n const turnId = idx;\n const startTime = new Date(turn.timestamp).getTime();\n\n // Find turn end\n let endTime;\n const nextTurnIndex = turnStarts.indexOf(turn) + 1;\n if (nextTurnIndex < turnStarts.length) {\n endTime = new Date(turnStarts[nextTurnIndex].timestamp).getTime();\n } else {\n endTime = Date.now();\n }\n\n // Calculate duration\n const durationMs = endTime - startTime;\n const totalSeconds = Math.floor(durationMs / 1000);\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n const durationText = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;\n\n // Find user message before this turn\n const userMessage = flatEvents.value\n .slice(0, flatEvents.value.indexOf(turn))\n .reverse()\n .find(e => e.type === 'user.message');\n\n // Calculate UserReq number (1-indexed)\n const userReqNumber = userMessage\n ? allUserMessages.indexOf(userMessage) + 1\n : 0;\n\n return {\n id: turnId,\n index: turn.virtualIndex,\n originalTurnId: turn.data?.turnId, // Keep original for reference\n timestamp: turn.timestamp,\n duration: durationText,\n message: userMessage?.data?.content || userMessage?.data?.transformedContent || '',\n userReqNumber: userReqNumber\n };\n });\n });\n\n // Group turns by UserReq for optgroup navigation\n const userReqs = computed(() => {\n const groups = [];\n const reqMap = new Map();\n\n turns.value.forEach(turn => {\n const reqNum = turn.userReqNumber || 0;\n if (!reqMap.has(reqNum)) {\n const group = {\n reqNumber: reqNum,\n message: turn.message,\n turns: []\n };\n reqMap.set(reqNum, group);\n groups.push(group);\n }\n reqMap.get(reqNum).turns.push(turn);\n });\n\n return groups;\n });\n\n // Truncate text helper for optgroup labels\n const truncateText = (text, maxLen) => {\n if (!text) return '';\n if (text.length <= maxLen) return text;\n return text.substring(0, maxLen) + '…';\n };\n \n // Tool call map\n // Subagent ownership: attribute events to their owning subagent\n const subagentOwnership = computed(() => {\n const sorted = flatEvents.value;\n const ownerMap = new Map(); // stableId → toolCallId\n const subagentInfo = new Map(); // toolCallId → { name, colorIndex }\n\n // 1. Collect all subagent.started toolCallIds + assign colorIndex\n let colorIdx = 0;\n for (const ev of sorted) {\n if (ev.type === 'subagent.started') {\n const tcid = ev.data?.toolCallId;\n if (tcid) {\n subagentInfo.set(tcid, {\n name: ev.data?.agentDisplayName || ev.data?.agentName || 'SubAgent',\n colorIndex: colorIdx++\n });\n }\n }\n }\n\n if (subagentInfo.size === 0) return { ownerMap, subagentInfo };\n\n // 2. Build id → event lookup for parentId chain walking\n const idMap = new Map();\n for (const ev of sorted) {\n if (ev.id) idMap.set(ev.id, ev);\n }\n\n // 3. Attribute assistant.message events via data.parentToolCallId\n for (const ev of sorted) {\n if (ev.type === 'assistant.message') {\n const ptcid = ev.data?.parentToolCallId;\n if (ptcid && subagentInfo.has(ptcid)) {\n ownerMap.set(ev.stableId, ptcid);\n }\n }\n }\n\n // 4. Attribute reasoning events by walking parentId → assistant.message\n for (const ev of sorted) {\n if (ev.type !== 'reasoning') continue;\n let current = ev.parentId;\n let depth = 0;\n while (current && depth < 10) {\n const parent = idMap.get(current);\n if (!parent) break;\n if (parent.type === 'assistant.message') {\n const ptcid = parent.data?.parentToolCallId;\n if (ptcid && subagentInfo.has(ptcid)) {\n ownerMap.set(ev.stableId, ptcid);\n }\n break;\n }\n current = parent.parentId;\n depth++;\n }\n }\n\n // 5. Attribute tool.execution_start/complete by walking parentId chain\n const startIdByToolCallId = new Map();\n for (const ev of sorted) {\n if (ev.type !== 'tool.execution_start') continue;\n let current = ev.parentId;\n let depth = 0;\n while (current && depth < 10) {\n const parent = idMap.get(current);\n if (!parent) break;\n if (parent.type === 'assistant.message') {\n const ptcid = parent.data?.parentToolCallId;\n if (ptcid && subagentInfo.has(ptcid)) {\n ownerMap.set(ev.stableId, ptcid);\n const tcid = ev.data?.toolCallId;\n if (tcid) startIdByToolCallId.set(tcid, ptcid);\n }\n break;\n }\n current = parent.parentId;\n depth++;\n }\n }\n\n for (const ev of sorted) {\n if (ev.type !== 'tool.execution_complete') continue;\n const tcid = ev.data?.toolCallId;\n if (tcid && startIdByToolCallId.has(tcid)) {\n ownerMap.set(ev.stableId, startIdByToolCallId.get(tcid));\n }\n }\n\n return { ownerMap, subagentInfo };\n });\n\n // Methods\n const formatTime = (timestamp) => {\n if (!timestamp) return '';\n const date = new Date(timestamp);\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n const seconds = String(date.getSeconds()).padStart(2, '0');\n return `${hours}:${minutes}:${seconds}`;\n };\n \n // Performance fix: Cache markdown rendering results\n const markdownCache = new Map();\n const MAX_CACHE_SIZE = 200;\n \n const renderMarkdown = (text) => {\n if (!text) return '';\n \n // Check cache first\n if (markdownCache.has(text)) {\n return markdownCache.get(text);\n }\n \n try {\n // 处理转义序列:将 \\r\\n、\\n、\\t 等转换为实际字符\n let processedText = text\n .replace(/\\\\r\\\\n/g, '\\n') // \\r\\n → 换行\n .replace(/\\\\n/g, '\\n') // \\n → 换行\n .replace(/\\\\t/g, '\\t') // \\t → 制表符\n .replace(/\\\\\"/g, '\"') // \\\" → 引号\n .replace(/\\\\\\\\/g, '\\\\'); // \\\\ → 反斜杠\n \n // Parse YAML frontmatter\n const frontmatterMatch = processedText.match(/^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/);\n if (frontmatterMatch) {\n const frontmatter = frontmatterMatch[1];\n const content = frontmatterMatch[2];\n \n // Parse frontmatter into key-value pairs\n const pairs = frontmatter.split('\\n').filter(line => line.trim() && line.includes(':')).map(line => {\n const colonIndex = line.indexOf(':');\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n return { key, value };\n });\n \n // Render frontmatter as table\n let tableHTML = '<table style=\"margin-bottom: 16px; border-collapse: collapse;\"><tbody>';\n pairs.forEach(pair => {\n tableHTML += `<tr><td style=\"padding: 4px 12px; border: 1px solid #30363d; font-weight: 600; color: #7d8590;\">${pair.key}</td><td style=\"padding: 4px 12px; border: 1px solid #30363d;\">${pair.value}</td></tr>`;\n });\n tableHTML += '</tbody></table>';\n \n // Render remaining content\n return tableHTML + marked.parse(content);\n }\n \n const result = marked.parse(processedText);\n \n // Cache the result (with size limit to prevent memory leak)\n if (markdownCache.size >= MAX_CACHE_SIZE) {\n const firstKey = markdownCache.keys().next().value;\n markdownCache.delete(firstKey);\n }\n markdownCache.set(text, result);\n \n return result;\n } catch (e) {\n return text;\n }\n };\n \n const toggleTool = (toolId) => {\n const newState = { ...expandedTools.value };\n if (newState[toolId]) {\n delete newState[toolId];\n } else {\n newState[toolId] = true;\n }\n expandedTools.value = newState;\n };\n \n const highlightSearchText = (html, searchTerm) => {\n if (!searchTerm || !searchTerm.trim() || !html) return html;\n \n const term = searchTerm.trim();\n // Escape HTML in search term to prevent XSS\n const escapedTerm = escapeHtml(term)\n .replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'); // Also escape regex special chars\n \n // Create a temporary element to parse HTML\n const temp = document.createElement('div');\n temp.innerHTML = html;\n \n // Function to highlight text in text nodes\n const highlightTextNode = (node) => {\n if (node.nodeType === Node.TEXT_NODE) {\n const text = node.textContent;\n const regex = new RegExp(`(${escapedTerm})`, 'gi');\n if (regex.test(text)) {\n const highlighted = text.replace(regex, '<mark class=\"search-highlight\">$1</mark>');\n const span = document.createElement('span');\n span.innerHTML = highlighted;\n node.parentNode.replaceChild(span, node);\n }\n } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') {\n Array.from(node.childNodes).forEach(highlightTextNode);\n }\n };\n \n Array.from(temp.childNodes).forEach(highlightTextNode);\n return temp.innerHTML;\n };\n \n const toggleContent = (contentId) => {\n // Create new object to trigger Vue reactivity\n const newState = { ...expandedContent.value };\n if (newState[contentId]) {\n delete newState[contentId];\n } else {\n newState[contentId] = true;\n }\n expandedContent.value = newState;\n };\n \n const isContentTooLong = (text) => {\n if (!text) return false;\n const lineCount = text.split('\\n').length;\n return lineCount > 20 || text.length > 2000;\n };\n \n const truncateContent = (text) => {\n const lines = text.split('\\n');\n if (lines.length <= 20) return text;\n return lines.slice(0, 20).join('\\n') + '\\n\\n...';\n };\n \n const getBadgeInfo = (type) => {\n // Special case for specific event types\n if (type === 'session.model_change') {\n return { label: 'MODEL CHANGE', class: 'badge-session' };\n }\n if (type === 'session.truncation') {\n return { label: 'TRUNCATION', class: 'badge-truncation' };\n }\n if (type === 'session.compaction_start' || type === 'session.compaction_complete') {\n return { label: 'COMPACTION', class: 'badge-compaction' };\n }\n \n const parts = (type || '').split('.');\n const category = parts[0] || 'unknown';\n \n const badges = {\n user: { label: 'USER', class: 'badge-user' },\n assistant: { label: 'ASSISTANT', class: 'badge-assistant' },\n reasoning: { label: 'REASONING', class: 'badge-reasoning' },\n turn: { label: 'TURN', class: 'badge-turn' },\n tool: { label: 'TOOL', class: 'badge-tool' },\n subagent: { label: 'SUBAGENT', class: 'badge-subagent' },\n skill: { label: 'SKILL', class: 'badge-skill' },\n session: { label: 'SESSION', class: 'badge-session' },\n error: { label: 'ERROR', class: 'badge-error' },\n abort: { label: 'ABORT', class: 'badge-error' }\n };\n \n return badges[category] || { label: category.toUpperCase(), class: 'badge-info' };\n };\n \n const getToolStatus = (group) => {\n if (!group.complete) {\n return { icon: '⏳', color: 'tool-status-running', text: '' };\n }\n \n const completeData = group.complete.data || {};\n if (completeData.error || completeData.isError) {\n return { icon: '❌', color: 'tool-status-error', text: '' };\n }\n \n return { icon: '✓', color: 'tool-status-success', text: '' };\n };\n \n const getToolErrorMessage = (group) => {\n if (!group.complete?.data?.error) return '';\n \n const error = group.complete.data.error;\n \n // If error is an object with message property\n if (typeof error === 'object' && error.message) {\n return error.message;\n }\n \n // If error is a string, try to parse as JSON\n if (typeof error === 'string') {\n try {\n const parsed = JSON.parse(error);\n if (parsed.message) return parsed.message;\n } catch (e) {\n // Not JSON, return as-is\n }\n return error;\n }\n \n // Fallback to stringified error\n return String(error);\n };\n \n const getToolDuration = (group) => {\n if (!group.complete) return '';\n \n const startTime = new Date(group.start.timestamp).getTime();\n const endTime = new Date(group.complete.timestamp).getTime();\n const durationMs = endTime - startTime;\n \n if (durationMs >= 100) {\n return `${(durationMs / 1000).toFixed(1)}s`;\n }\n return '';\n };\n \n const getToolCommand = (group) => {\n if (!group.start) return '';\n const args = group.start.data?.arguments || {};\n const toolName = group.start.data?.toolName || group.tool || '';\n \n let command = '';\n if (toolName === 'bash' || toolName === 'exec') {\n command = args.command || args.description || '';\n } else if (toolName === 'ask_user') {\n command = args.question || args.message || '';\n } else if (toolName === 'read' || toolName === 'write' || toolName === 'edit') {\n command = args.file_path || args.path || '';\n } else if (toolName === 'view') {\n command = args.path || args.file || '';\n } else if (toolName === 'create') {\n command = args.path || args.name || '';\n } else if (toolName === 'report_intent') {\n command = args.intent || args.message || '';\n } else if (toolName === 'web_search') {\n command = args.query || '';\n } else if (toolName === 'web_fetch') {\n command = args.url || '';\n } else if (toolName === 'browser') {\n const action = args.action || '';\n const url = args.targetUrl || args.url || '';\n command = url ? `${action} ${url}` : action;\n } else {\n command = args.description || args.command || args.message || \n args.path || args.file_path || args.query || '';\n }\n \n if (command && command.length > 100) {\n command = command.substring(0, 100) + '...';\n }\n \n return command;\n };\n \n const hasTools = (event) => {\n // Unified format: check data.tools (works for both Copilot and Claude)\n return event.data?.tools && event.data.tools.length > 0;\n };\n \n const getToolGroups = (event) => {\n // Unified format from server (both Copilot and Claude normalized to data.tools)\n if (event.data?.tools && Array.isArray(event.data.tools)) {\n return event.data.tools.map(tool => {\n if (tool.type === 'tool_use') {\n return {\n tool: tool.name,\n start: {\n data: {\n toolName: tool.name,\n arguments: tool.input\n }\n },\n complete: tool._matched ? {\n data: {\n result: tool.result\n }\n } : null\n };\n }\n return null;\n }).filter(g => g !== null);\n }\n \n return [];\n };\n\n // Subagent color palette for parallel subagent distinction\n const SUBAGENT_COLORS = [\n '#58a6ff', // blue\n '#f0883e', // orange\n '#a371f7', // purple\n '#3fb950', // green\n '#f778ba', // pink\n '#79c0ff', // light blue\n '#d29922', // amber\n '#56d4dd' // teal\n ];\n\n // Hash function for generating consistent color indices\n const hashCode = (str) => {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash; // Convert to 32bit integer\n }\n return hash;\n };\n\n const getSubagentInfo = (event) => {\n const { ownerMap, subagentInfo } = subagentOwnership.value;\n // For subagent dividers, use their own toolCallId\n if (event.type === 'subagent.started' || event.type === 'subagent.completed' || event.type === 'subagent.failed') {\n const tcid = event.data?.toolCallId;\n if (tcid && subagentInfo.has(tcid)) {\n const info = subagentInfo.get(tcid);\n return { name: info.name, toolCallId: tcid, colorIndex: info.colorIndex };\n }\n return null;\n }\n // For regular events, first check _subagent metadata (Claude format)\n if (event._subagent) {\n const subagentId = event._subagent.id;\n const subagentName = event._subagent.name;\n // Use subagentId as toolCallId for consistency\n if (subagentInfo.has(subagentId)) {\n const info = subagentInfo.get(subagentId);\n return { name: info.name, toolCallId: subagentId, colorIndex: info.colorIndex };\n }\n // If not in subagentInfo, create a default entry\n return { name: subagentName, toolCallId: subagentId, colorIndex: Math.abs(hashCode(subagentId)) };\n }\n // For regular events, look up ownership (Copilot format)\n const tcid = ownerMap.get(event.stableId);\n if (!tcid) return null;\n const info = subagentInfo.get(tcid);\n if (!info) return null;\n return { name: info.name, toolCallId: tcid, colorIndex: info.colorIndex };\n };\n\n const getSubagentColor = (event) => {\n const info = getSubagentInfo(event);\n if (!info) return null;\n return SUBAGENT_COLORS[info.colorIndex % SUBAGENT_COLORS.length];\n };\n\n const setFilter = (type) => {\n currentFilter.value = type;\n };\n \n const scrollToTurn = (turn) => {\n // Clear search and filter when jumping to a turn\n searchText.value = '';\n currentFilter.value = 'all';\n\n currentTurnIndex.value = turn.id;\n\n // Wait for DOM to update and virtual scroller to re-calculate\n Vue.nextTick(() => {\n if (scrollerRef.value) {\n // Use turn.index (virtualIndex) to find the exact turn_start event\n const targetIndex = filteredEvents.value.findIndex(e =>\n e.virtualIndex === turn.index\n );\n\n if (targetIndex >= 0) {\n // DynamicScroller with variable heights needs multiple scroll passes\n // to converge on the correct position as it measures real item sizes\n const doScroll = (attempts) => {\n if (attempts <= 0 || !scrollerRef.value) return;\n scrollerRef.value.scrollToItem(targetIndex);\n setTimeout(() => doScroll(attempts - 1), 100);\n };\n setTimeout(() => doScroll(3), 50);\n }\n }\n });\n };\n \n const jumpToTurn = (turnId) => {\n const turn = turns.value.find(t => t.id === turnId);\n if (turn) {\n // Update URL with eventType + eventName\n const eventName = `UserReq${turn.userReqNumber}_Turn${turn.id}`;\n const newUrl = `${window.location.pathname}?eventType=assistant.turn_start&eventName=${eventName}`;\n window.history.pushState({}, '', newUrl);\n \n // Scroll to turn\n scrollToTurn(turn);\n }\n };\n \n const getTurnNumber = (virtualIndex) => {\n // Find the turn with matching virtualIndex\n const turn = turns.value.find(t => t.index === virtualIndex);\n if (!turn) return '?';\n\n const turnLabel = turn.originalTurnId != null ? turn.originalTurnId : turn.id;\n // Format: \"UserReq N - Turn M\" or just \"Turn M\" if no UserReq\n if (turn.userReqNumber > 0) {\n return `${turn.userReqNumber} - Turn ${turnLabel}`;\n }\n return `Turn ${turnLabel}`;\n };\n const escapeHtml = (text) => {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n };\n \n const formatDateTime = (timestamp) => {\n if (!timestamp) return 'N/A';\n return new Date(timestamp).toLocaleString();\n };\n \n const exportSession = async () => {\n exporting.value = true;\n try {\n const response = await fetch(`/session/${sessionId.value}/export`);\n if (!response.ok) {\n throw new Error('Share failed');\n }\n \n // Download the file\n const blob = await response.blob();\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `session-${sessionId.value}.zip`;\n document.body.appendChild(a);\n a.click();\n window.URL.revokeObjectURL(url);\n document.body.removeChild(a);\n } catch (err) {\n console.error('Share session error:', err);\n alert('Failed to share session: ' + err.message);\n } finally {\n exporting.value = false;\n }\n };\n \n\n // Lifecycle\n onMounted(async () => {\n // Load events asynchronously\n try {\n console.log('[Navigation] Starting event loading...');\n const response = await fetch(`/api/sessions/${sessionId.value}/events`);\n if (!response.ok) {\n throw new Error(`Failed to load events: ${response.statusText}`);\n }\n loadedEvents.value = await response.json();\n console.log('[Navigation] Events loaded:', loadedEvents.value.length);\n \n // Check for URL query parameters and jump to event AFTER events are loaded\n const urlParams = new URLSearchParams(window.location.search);\n const eventTypeParam = urlParams.get('eventType');\n const eventNameParam = urlParams.get('eventName');\n const eventTimestampParam = urlParams.get('eventTimestamp');\n console.log('[Navigation] URL params:', eventTypeParam, eventNameParam, eventTimestampParam);\n \n if (eventTypeParam && eventNameParam) {\n console.log('[Navigation] Waiting for Vue to render...');\n // Wait for Vue to process the events and render\n Vue.nextTick(() => {\n console.log('[Navigation] nextTick - flatEvents count:', flatEvents.value?.length);\n let targetEvent = null;\n \n if (eventTypeParam === 'assistant.turn_start') {\n // Parse \"UserReq1_Turn0\" format\n const match = eventNameParam.match(/UserReq(\\d+)_Turn(\\d+)/);\n if (match) {\n const turnId = parseInt(match[2], 10);\n if (!isNaN(turnId)) {\n console.log('[Navigation] Jumping to turn:', turnId);\n jumpToTurn(turnId);\n return;\n }\n }\n } else if (eventTypeParam === 'subagent.started') {\n console.log('[Navigation] Searching for subagent:', eventNameParam, 'timestamp:', eventTimestampParam);\n // Find subagent by name + timestamp (handles duplicate subagent names)\n if (eventTimestampParam) {\n targetEvent = flatEvents.value.find(event =>\n event.type === 'subagent.started' &&\n event.timestamp === eventTimestampParam\n );\n }\n // Fallback: match by name only (for links without timestamp)\n if (!targetEvent) {\n targetEvent = flatEvents.value.find(event =>\n event.type === 'subagent.started' &&\n (event.data?.agentDisplayName === eventNameParam ||\n event.data?.agentName === eventNameParam ||\n event.data?.label === eventNameParam)\n );\n }\n console.log('[Navigation] Target event found:', targetEvent ? 'YES' : 'NO', 'virtualIndex:', targetEvent?.virtualIndex);\n } else {\n // Generic: match by type only\n targetEvent = flatEvents.value.find(event => event.type === eventTypeParam);\n }\n \n if (targetEvent) {\n // Find target in filteredEvents (which may be different from flatEvents due to filters)\n const targetIndex = filteredEvents.value.findIndex(e => \n e.virtualIndex === targetEvent.virtualIndex\n );\n console.log('[Navigation] Target in filteredEvents at index:', targetIndex);\n \n if (targetIndex >= 0 && scrollerRef.value) {\n console.log('[Navigation] Scrolling to index:', targetIndex);\n // Use retry mechanism like scrollToTurn\n const doScroll = (attempts) => {\n if (attempts <= 0 || !scrollerRef.value) return;\n scrollerRef.value.scrollToItem(targetIndex);\n setTimeout(() => doScroll(attempts - 1), 100);\n };\n setTimeout(() => doScroll(3), 50);\n } else {\n console.log('[Navigation] Failed - targetIndex:', targetIndex, 'scrollerRef:', !!scrollerRef.value);\n }\n } else {\n console.log('[Navigation] Target event not found');\n }\n });\n }\n } catch (error) {\n console.error('Error loading events:', error);\n eventsError.value = error.message;\n } finally {\n eventsLoading.value = false;\n }\n \n window.addEventListener('keydown', (e) => {\n if (e.ctrlKey && e.key === 'b') {\n e.preventDefault();\n sidebarCollapsed.value = !sidebarCollapsed.value;\n }\n });\n \n if (window.marked) {\n marked.setOptions({\n breaks: true,\n gfm: true\n });\n }\n \n // 监听滚动事件来更新 visibleRange\n const updateVisibleRange = () => {\n if (!scrollerRef.value) return;\n \n // 尝试多种方式访问 scroller 元素\n let scroller = null;\n if (scrollerRef.value.$el && typeof scrollerRef.value.$el.querySelector === 'function') {\n scroller = scrollerRef.value.$el.querySelector('.vue-recycle-scroller');\n } else if (scrollerRef.value.querySelector && typeof scrollerRef.value.querySelector === 'function') {\n scroller = scrollerRef.value.querySelector('.vue-recycle-scroller');\n }\n \n if (!scroller) {\n // 如果还找不到,直接查询 DOM\n scroller = document.querySelector('.vue-recycle-scroller');\n }\n \n if (scroller) {\n const scrollTop = scroller.scrollTop;\n const clientHeight = scroller.clientHeight;\n \n // 估算可见范围\n const avgItemHeight = 80;\n const startIndex = Math.floor(scrollTop / avgItemHeight);\n const visibleCount = Math.ceil(clientHeight / avgItemHeight);\n const endIndex = Math.min(startIndex + visibleCount, filteredEvents.value.length);\n \n const startPos = Math.max(1, startIndex + 1);\n const endPos = Math.max(1, endIndex);\n \n visibleRange.value = {\n start: Math.min(startPos, endPos), // Ensure start <= end\n end: endPos\n };\n }\n };\n \n // 初始更新和添加滚动监听\n let scrollCleanup = null;\n setTimeout(() => {\n updateVisibleRange();\n \n const scroller = document.querySelector('.vue-recycle-scroller');\n if (scroller) {\n scroller.addEventListener('scroll', updateVisibleRange);\n // Store cleanup function\n scrollCleanup = () => {\n scroller.removeEventListener('scroll', updateVisibleRange);\n };\n }\n }, 500);\n \n // Cleanup on unmount\n onBeforeUnmount(() => {\n // Clear search timeout (memory leak fix)\n if (searchTimeout) {\n clearTimeout(searchTimeout);\n searchTimeout = null;\n }\n \n // Clean scroll listeners\n if (scrollCleanup) {\n scrollCleanup();\n }\n \n // Clear expansion state (memory leak fix)\n expandedTools.value = {};\n expandedContent.value = {};\n \n // Clear markdown cache (memory leak fix)\n markdownCache.clear();\n });\n });\n \n return {\n sessionId,\n metadata,\n exporting,\n sidebarCollapsed,\n expandedTools,\n expandedContent,\n expansionCount,\n currentFilter,\n searchText,\n currentTurnIndex,\n scrollerRef,\n visibleRange,\n loadedEvents,\n eventsLoading,\n eventsError,\n flatEvents,\n filteredEvents,\n eventCounts,\n filters,\n turns,\n userReqs,\n truncateText,\n formatTime,\n formatDateTime,\n renderMarkdown,\n highlightSearchText,\n toggleTool,\n toggleContent,\n isContentTooLong,\n truncateContent,\n getBadgeInfo,\n getToolStatus,\n getToolErrorMessage,\n getToolDuration,\n getToolCommand,\n hasTools,\n getToolGroups,\n getSubagentInfo,\n getSubagentColor,\n setFilter,\n scrollToTurn,\n jumpToTurn,\n getTurnNumber,\n escapeHtml,\n exportSession\n };\n },\n \n template: `\n <div class=\"container\">\n <div class=\"header\">\n <a href=\"/\" class=\"home-btn\">← Back to Home</a>\n <h1>📋 Session: {{ sessionId }}\n <span v-if=\"metadata.sessionStatus === 'wip'\" style=\"font-size: 12px; padding: 2px 8px; border-radius: 3px; background: rgba(210, 153, 34, 0.2); color: #d29922; border: 1px solid rgba(210, 153, 34, 0.4); vertical-align: middle; margin-left: 8px;\">🔄 WIP</span>\n </h1>\n <div style=\"display: flex; gap: 10px;\">\n <a :href=\"'/session/' + sessionId + '/time-analyze'\" class=\"time-analyze-btn\">⏱ Analysis</a>\n <button @click=\"exportSession\" class=\"export-btn\" :disabled=\"exporting\">\n {{ exporting ? '⏳ Sharing...' : '📤 Share Session' }}\n </button>\n </div>\n </div>\n \n <div class=\"main-layout\">\n <div :class=\"['sidebar', { collapsed: sidebarCollapsed }]\">\n <div class=\"sidebar-section\">\n <div class=\"sidebar-section-title\">Session Info</div>\n <div class=\"session-info\">\n <div v-if=\"metadata.summary\" class=\"session-summary-block\">{{ metadata.summary }}</div>\n <table class=\"session-info-table\">\n <tbody>\n <tr v-if=\"metadata.source\">\n <td>Source</td>\n <td>\n <span :class=\"['source-badge', metadata.source === 'claude' ? 'source-claude' : 'source-copilot']\">\n {{ metadata.source === 'claude' ? 'Claude Code' : 'GitHub Copilot' }}\n </span>\n </td>\n </tr>\n <tr v-if=\"metadata.copilotVersion\">\n <td>CLI Version</td>\n <td>{{ metadata.copilotVersion }}</td>\n </tr>\n <tr v-if=\"metadata.model\">\n <td>Model</td>\n <td>{{ metadata.model }}</td>\n </tr>\n <tr v-if=\"metadata.repo\">\n <td>Repo</td>\n <td>{{ metadata.repo }}</td>\n </tr>\n <tr v-if=\"metadata.branch\">\n <td>Branch</td>\n <td>{{ metadata.branch }}</td>\n </tr>\n <tr v-if=\"metadata.cwd\">\n <td>CWD</td>\n <td>{{ metadata.cwd }}</td>\n </tr>\n <tr v-if=\"metadata.created\">\n <td>Created</td>\n <td>{{ formatDateTime(metadata.created) }}</td>\n </tr>\n <tr v-if=\"metadata.updated\">\n <td>Updated</td>\n <td>{{ formatDateTime(metadata.updated) }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n \n <div class=\"sidebar-section\">\n <div class=\"sidebar-section-title\">Event Filters</div>\n <div class=\"event-filters\">\n <button\n v-for=\"filter in filters\"\n :key=\"filter.type\"\n :class=\"['filter-btn', { active: currentFilter === filter.type }]\"\n :disabled=\"filter.disabled\"\n @click=\"setFilter(filter.type)\"\n >\n {{ filter.label }}\n </button>\n </div>\n </div>\n </div>\n \n <div class=\"content\">\n <div class=\"scroll-indicator\">\n <div class=\"content-toolbar-left\">\n <button \n class=\"sidebar-toggle\"\n @click=\"sidebarCollapsed = !sidebarCollapsed\"\n :title=\"sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'\"\n >\n ☰\n </button>\n \n <!-- Turn dropdown with optgroup -->\n <select\n v-if=\"turns.length > 0\"\n v-model=\"currentTurnIndex\"\n @change=\"jumpToTurn(currentTurnIndex)\"\n class=\"turn-dropdown\"\n >\n <optgroup\n v-for=\"req in userReqs\"\n :key=\"req.reqNumber\"\n :label=\"req.reqNumber > 0 ? 'UserReq ' + req.reqNumber + ': ' + truncateText(req.message, 40) : 'Setup'\"\n >\n <option v-for=\"turn in req.turns\" :key=\"turn.id\" :value=\"turn.id\">\n Turn {{ turn.originalTurnId != null ? turn.originalTurnId : turn.id }} ({{ turn.duration }})\n </option>\n </optgroup>\n </select>\n </div>\n <div class=\"content-toolbar-center\">\n </div>\n <div class=\"content-toolbar-right\">\n <input \n v-model=\"searchText\" \n type=\"text\" \n placeholder=\"🔍 Search events...\" \n class=\"search-input\"\n />\n <span v-if=\"searchResultCount\" class=\"search-result-count\">\n {{ searchResultCount }}\n </span>\n </div>\n </div>\n \n <!-- Loading state -->\n <div v-if=\"eventsLoading\" class=\"loading-message\">\n <div style=\"text-align: center; padding: 40px; color: #c9d1d9;\">\n ⏳ Loading events...\n </div>\n </div>\n \n <!-- Error state -->\n <div v-else-if=\"eventsError\" class=\"error-message\">\n <div style=\"text-align: center; padding: 40px; color: #f85149;\">\n ❌ Error loading events: {{ eventsError }}\n </div>\n </div>\n \n <!-- Events list -->\n <DynamicScroller\n v-else\n ref=\"scrollerRef\"\n :items=\"filteredEvents\"\n :min-item-size=\"80\"\n key-field=\"stableId\"\n class=\"scroller\"\n >\n <template #default=\"{ item, index, active }\">\n <DynamicScrollerItem\n :item=\"item\"\n :active=\"active\"\n :size-dependencies=\"[expansionCount]\"\n :data-index=\"index\"\n >\n <!-- Turn Start Divider -->\n <div \n v-if=\"item.type === 'assistant.turn_start'\"\n :data-type=\"item.type\"\n :data-index=\"item.virtualIndex\"\n class=\"turn-divider\"\n >\n <div class=\"turn-divider-line-left\"></div>\n <span class=\"turn-divider-text\">UserReq {{ getTurnNumber(item.virtualIndex) }} Start</span>\n <div class=\"turn-divider-line-right\"></div>\n <div class=\"divider-separator\"></div>\n </div>\n \n <!-- Subagent Divider -->\n <div\n v-else-if=\"item.type === 'subagent.started' || item.type === 'subagent.completed' || item.type === 'subagent.failed'\"\n :data-type=\"item.type\"\n :data-index=\"item.virtualIndex\"\n :class=\"['subagent-divider', item.type.split('.')[1]]\"\n :style=\"{\n '--sa-color': getSubagentColor(item) || '#58a6ff'\n }\"\n >\n <div class=\"subagent-divider-line-left\" :style=\"{ background: getSubagentColor(item) || '#58a6ff' }\"></div>\n <span class=\"subagent-divider-text\" :style=\"{ color: getSubagentColor(item) || '#58a6ff', borderColor: getSubagentColor(item) || '#58a6ff', background: (getSubagentColor(item) || '#58a6ff') + '1a' }\">\n 🤖 {{ item.data?.agentDisplayName || item.data?.agentName || 'SubAgent' }}\n {{ item.type === 'subagent.started' ? 'Start ▶' : item.type === 'subagent.completed' ? 'Complete ✓' : 'Failed ✗' }}\n </span>\n <div class=\"subagent-divider-line-right\" :style=\"{ background: getSubagentColor(item) || '#58a6ff' }\"></div>\n <div class=\"divider-separator\"></div>\n </div>\n \n <!-- Regular Event -->\n <div\n v-else\n :class=\"['event', getSubagentInfo(item) ? 'event-in-subagent' : '']\"\n :data-type=\"item.type\"\n :data-index=\"item.virtualIndex\"\n :style=\"getSubagentColor(item) ? { '--subagent-border-color': getSubagentColor(item) } : {}\"\n >\n <div class=\"event-header\">\n <span :class=\"['event-badge', getBadgeInfo(item.type).class]\">\n {{ getBadgeInfo(item.type).label }}\n </span>\n <span\n v-if=\"getSubagentInfo(item)\"\n class=\"subagent-owner-tag\"\n :style=\"{ color: getSubagentColor(item), borderColor: getSubagentColor(item) }\"\n >🤖 {{ getSubagentInfo(item).name }}</span>\n <span class=\"event-timestamp\">{{ formatTime(item.timestamp) }}</span>\n </div>\n \n <!-- Abort event: show reason -->\n <div v-if=\"item.type === 'abort' && item.data?.reason\" class=\"event-content\">\n <strong>Reason:</strong> {{ item.data.reason }}\n </div>\n \n <!-- Session start: show type and selectedModel -->\n <div v-else-if=\"item.type === 'session.start'\" class=\"event-content\">\n <div v-if=\"item.data?.type\"><strong>Type:</strong> {{ item.data.type }}</div>\n <div v-if=\"item.data?.selectedModel\"><strong>Model:</strong> {{ item.data.selectedModel }}</div>\n <div v-if=\"item.data?.producer\"><strong>Producer:</strong> {{ item.data.producer }}</div>\n </div>\n \n <!-- Session resume: show resumeTime, eventCount, context -->\n <div v-else-if=\"item.type === 'session.resume'\" class=\"event-content\">\n <div v-if=\"item.data?.resumeTime\"><strong>Resume Time:</strong> {{ formatDateTime(item.data.resumeTime) }}</div>\n <div v-if=\"item.data?.eventCount\"><strong>Event Count:</strong> {{ item.data.eventCount }}</div>\n <div v-if=\"item.data?.context?.branch\"><strong>Branch:</strong> {{ item.data.context.branch }}</div>\n <div v-if=\"item.data?.context?.repository\"><strong>Repository:</strong> {{ item.data.context.repository }}</div>\n <div v-if=\"item.data?.context?.cwd\"><strong>Working Directory:</strong> {{ item.data.context.cwd }}</div>\n </div>\n \n <!-- Session error: show errorType + message -->\n <div v-else-if=\"item.type === 'session.error' && (item.data?.errorType || item.data?.message)\" class=\"event-content\">\n <div v-if=\"item.data?.errorType\"><strong>Error Type:</strong> {{ item.data.errorType }}</div>\n <div v-if=\"item.data?.message\"><strong>Message:</strong> {{ item.data.message }}</div>\n </div>\n \n <!-- Model change: show previousModel → newModel -->\n <div v-else-if=\"item.type === 'session.model_change'\" class=\"event-content model-change-content\">\n <div v-if=\"item.data?.previousModel && item.data?.newModel\" class=\"model-change-text\">\n <span class=\"model-name\">{{ item.data.previousModel }}</span>\n <span class=\"model-arrow\">→</span>\n <span class=\"model-name\">{{ item.data.newModel }}</span>\n </div>\n <div v-else-if=\"item.data?.newModel\" class=\"model-change-text\">\n Switched to <span class=\"model-name\">{{ item.data.newModel }}</span>\n </div>\n <div v-else-if=\"item.data?.model\" class=\"model-change-text\">\n Switched to <span class=\"model-name\">{{ item.data.model }}</span>\n </div>\n <div v-else class=\"model-change-text\">\n Model changed\n </div>\n </div>\n\n <!-- Session truncation: show token/message removal info -->\n <div v-else-if=\"item.type === 'session.truncation'\" class=\"event-content\">\n <div v-if=\"item.data?.messagesRemovedDuringTruncation\"><strong>Messages removed:</strong> {{ item.data.messagesRemovedDuringTruncation }}</div>\n <div v-if=\"item.data?.tokensRemovedDuringTruncation\"><strong>Tokens removed:</strong> {{ item.data.tokensRemovedDuringTruncation.toLocaleString() }}</div>\n <div v-if=\"item.data?.preTruncationTokensInMessages\"><strong>Pre-truncation tokens:</strong> {{ item.data.preTruncationTokensInMessages.toLocaleString() }}</div>\n <div v-if=\"item.data?.postTruncationMessagesLength\"><strong>Post-truncation messages:</strong> {{ item.data.postTruncationMessagesLength }}</div>\n <div v-if=\"item.data?.performedBy\"><strong>Performed by:</strong> {{ item.data.performedBy }}</div>\n </div>\n\n <!-- Session compaction start -->\n <div v-else-if=\"item.type === 'session.compaction_start'\" class=\"event-content\">\n Context compaction started\n </div>\n\n <!-- Session compaction complete: show results -->\n <div v-else-if=\"item.type === 'session.compaction_complete'\" class=\"event-content\">\n <div v-if=\"item.data?.success != null\"><strong>Success:</strong> {{ item.data.success ? '✓' : '✗' }}</div>\n <div v-if=\"item.data?.compactionTokensUsed\">\n <strong>Tokens used:</strong>\n input {{ item.data.compactionTokensUsed.input?.toLocaleString() || 0 }},\n output {{ item.data.compactionTokensUsed.output?.toLocaleString() || 0 }}\n <span v-if=\"item.data.compactionTokensUsed.cachedInput\">, cached {{ item.data.compactionTokensUsed.cachedInput.toLocaleString() }}</span>\n </div>\n <div v-if=\"item.data?.preCompactionMessagesLength\"><strong>Pre-compaction messages:</strong> {{ item.data.preCompactionMessagesLength }}</div>\n <div v-if=\"item.data?.preCompactionTokens\"><strong>Pre-compaction tokens:</strong> {{ item.data.preCompactionTokens.toLocaleString() }}</div>\n <div v-if=\"item.data?.summaryContent\" style=\"margin-top: 8px;\">\n <button\n @click=\"toggleContent('compaction-' + item.stableId)\"\n style=\"background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;\"\n >\n {{ expandedContent['compaction-' + item.stableId] ? 'Hide summary ▲' : 'Show summary ▼' }}\n </button>\n <div v-if=\"expandedContent['compaction-' + item.stableId]\" class=\"event-content\" style=\"margin-top: 8px;\" v-html=\"renderMarkdown(item.data.summaryContent)\"></div>\n </div>\n </div>\n\n <!-- Regular content (unified format from server) -->\n <div v-else-if=\"item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent\">\n <div\n class=\"event-content\"\n v-html=\"highlightSearchText(\n renderMarkdown(\n (expandedContent[item.stableId] || !isContentTooLong(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent))\n ? (item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)\n : truncateContent(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)\n ),\n searchText\n )\"\n ></div>\n <div\n v-if=\"isContentTooLong(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)\"\n style=\"margin-top: 8px;\"\n >\n <button\n @click=\"toggleContent(item.stableId)\"\n :data-content-id=\"item.stableId\"\n style=\"background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;\"\n >\n {{ expandedContent[item.stableId] ? 'Show less ▲' : 'Show more ▼' }}\n </button>\n </div>\n </div>\n \n <!-- No content at all (no message and no tools) -->\n <div v-else-if=\"!hasTools(item)\" class=\"event-content\" style=\"color: #7d8590; font-style: italic;\">\n No available message\n </div>\n \n <!-- Tool calls section (independent of message content) -->\n <div v-if=\"hasTools(item)\" class=\"tool-list\">\n <div\n v-for=\"(group, idx) in getToolGroups(item)\"\n :key=\"idx\"\n class=\"tool-item\"\n >\n <div \n class=\"tool-header-line\"\n @click=\"toggleTool(item.stableId + '-' + idx)\"\n >\n <span class=\"tool-connector\">{{ idx === getToolGroups(item).length - 1 ? '└─' : '├─' }}</span>\n <span class=\"tool-expand-icon\">{{ expandedTools[item.stableId + '-' + idx] ? '▼' : '▶' }}</span>\n <span class=\"tool-name\">🔧 {{ group.start?.data?.toolName || group.tool || 'Tool' }}</span>\n <span :class=\"getToolStatus(group).color\" style=\"margin-left: 4px;\">({{ getToolStatus(group).icon }}{{ getToolDuration(group) ? ' ' + getToolDuration(group) : '' }})</span>\n <span v-if=\"getToolCommand(group)\" style=\"color: #7d8590; margin-left: 8px;\">{{ getToolCommand(group) }}</span>\n <span v-if=\"getToolErrorMessage(group)\" style=\"color: #ff7b72; margin-left: 8px;\">{{ getToolErrorMessage(group).length > 80 ? getToolErrorMessage(group).substring(0, 80) + '...' : getToolErrorMessage(group) }}</span>\n </div>\n \n <div v-if=\"expandedTools[item.stableId + '-' + idx]\" class=\"tool-detail\">\n <div v-if=\"group.start?.data?.arguments\" class=\"tool-detail-section\">\n <div class=\"tool-detail-title\">Arguments:</div>\n <div class=\"tool-detail-content\">\n <pre>{{ JSON.stringify(group.start.data.arguments, null, 2) }}</pre>\n </div>\n </div>\n <div v-if=\"group.complete?.data?.result\" class=\"tool-detail-section\">\n <div class=\"tool-detail-title\">Result:</div>\n <div class=\"tool-detail-content\">\n <pre>{{ JSON.stringify(group.complete.data.result, null, 2) }}</pre>\n </div>\n </div>\n <div v-if=\"getToolErrorMessage(group)\" class=\"tool-detail-section\">\n <div class=\"tool-detail-title\">Error:</div>\n <div class=\"tool-detail-content\" style=\"color: #ff7b72;\">\n {{ getToolErrorMessage(group) }}\n </div>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Separator (inside event for proper height calculation) -->\n <div v-if=\"!item.isLastEvent\" class=\"event-separator\"></div>\n </div>\n </DynamicScrollerItem>\n </template>\n </DynamicScroller>\n </div>\n </div>\n \n </div>\n `\n });\n \n app.mount('#app');\n ",
|
|
24
|
-
"functions": [
|
|
25
|
-
{
|
|
26
|
-
"functionName": "",
|
|
27
|
-
"isBlockCoverage": true,
|
|
28
|
-
"ranges": [
|
|
29
|
-
{
|
|
30
|
-
"startOffset": 0,
|
|
31
|
-
"endOffset": 63342,
|
|
32
|
-
"count": 1
|
|
33
|
-
}
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"functionName": "setup",
|
|
38
|
-
"isBlockCoverage": true,
|
|
39
|
-
"ranges": [
|
|
40
|
-
{
|
|
41
|
-
"startOffset": 300,
|
|
42
|
-
"endOffset": 41296,
|
|
43
|
-
"count": 1
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"functionName": "",
|
|
49
|
-
"isBlockCoverage": false,
|
|
50
|
-
"ranges": [
|
|
51
|
-
{
|
|
52
|
-
"startOffset": 726,
|
|
53
|
-
"endOffset": 824,
|
|
54
|
-
"count": 0
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"functionName": "cleanupExpansionState",
|
|
60
|
-
"isBlockCoverage": false,
|
|
61
|
-
"ranges": [
|
|
62
|
-
{
|
|
63
|
-
"startOffset": 1106,
|
|
64
|
-
"endOffset": 1766,
|
|
65
|
-
"count": 0
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"functionName": "",
|
|
71
|
-
"isBlockCoverage": false,
|
|
72
|
-
"ranges": [
|
|
73
|
-
{
|
|
74
|
-
"startOffset": 2164,
|
|
75
|
-
"endOffset": 2342,
|
|
76
|
-
"count": 0
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"functionName": "",
|
|
82
|
-
"isBlockCoverage": false,
|
|
83
|
-
"ranges": [
|
|
84
|
-
{
|
|
85
|
-
"startOffset": 2463,
|
|
86
|
-
"endOffset": 2515,
|
|
87
|
-
"count": 0
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"functionName": "",
|
|
93
|
-
"isBlockCoverage": false,
|
|
94
|
-
"ranges": [
|
|
95
|
-
{
|
|
96
|
-
"startOffset": 2562,
|
|
97
|
-
"endOffset": 2614,
|
|
98
|
-
"count": 0
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
"functionName": "",
|
|
104
|
-
"isBlockCoverage": true,
|
|
105
|
-
"ranges": [
|
|
106
|
-
{
|
|
107
|
-
"startOffset": 2897,
|
|
108
|
-
"endOffset": 3653,
|
|
109
|
-
"count": 1
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
"functionName": "",
|
|
115
|
-
"isBlockCoverage": false,
|
|
116
|
-
"ranges": [
|
|
117
|
-
{
|
|
118
|
-
"startOffset": 2969,
|
|
119
|
-
"endOffset": 3073,
|
|
120
|
-
"count": 0
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"functionName": "",
|
|
126
|
-
"isBlockCoverage": false,
|
|
127
|
-
"ranges": [
|
|
128
|
-
{
|
|
129
|
-
"startOffset": 3106,
|
|
130
|
-
"endOffset": 3410,
|
|
131
|
-
"count": 0
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"functionName": "",
|
|
137
|
-
"isBlockCoverage": false,
|
|
138
|
-
"ranges": [
|
|
139
|
-
{
|
|
140
|
-
"startOffset": 3429,
|
|
141
|
-
"endOffset": 3616,
|
|
142
|
-
"count": 0
|
|
143
|
-
}
|
|
144
|
-
]
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
"functionName": "matchesSearch",
|
|
148
|
-
"isBlockCoverage": false,
|
|
149
|
-
"ranges": [
|
|
150
|
-
{
|
|
151
|
-
"startOffset": 3744,
|
|
152
|
-
"endOffset": 4299,
|
|
153
|
-
"count": 0
|
|
154
|
-
}
|
|
155
|
-
]
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
"functionName": "",
|
|
159
|
-
"isBlockCoverage": true,
|
|
160
|
-
"ranges": [
|
|
161
|
-
{
|
|
162
|
-
"startOffset": 4433,
|
|
163
|
-
"endOffset": 4961,
|
|
164
|
-
"count": 1
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
"startOffset": 4851,
|
|
168
|
-
"endOffset": 4915,
|
|
169
|
-
"count": 0
|
|
170
|
-
}
|
|
171
|
-
]
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
"functionName": "excludeToolCalls",
|
|
175
|
-
"isBlockCoverage": false,
|
|
176
|
-
"ranges": [
|
|
177
|
-
{
|
|
178
|
-
"startOffset": 4476,
|
|
179
|
-
"endOffset": 4640,
|
|
180
|
-
"count": 0
|
|
181
|
-
}
|
|
182
|
-
]
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
"functionName": "",
|
|
186
|
-
"isBlockCoverage": false,
|
|
187
|
-
"ranges": [
|
|
188
|
-
{
|
|
189
|
-
"startOffset": 5069,
|
|
190
|
-
"endOffset": 6071,
|
|
191
|
-
"count": 0
|
|
192
|
-
}
|
|
193
|
-
]
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
"functionName": "",
|
|
197
|
-
"isBlockCoverage": false,
|
|
198
|
-
"ranges": [
|
|
199
|
-
{
|
|
200
|
-
"startOffset": 6175,
|
|
201
|
-
"endOffset": 6410,
|
|
202
|
-
"count": 0
|
|
203
|
-
}
|
|
204
|
-
]
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"functionName": "",
|
|
208
|
-
"isBlockCoverage": false,
|
|
209
|
-
"ranges": [
|
|
210
|
-
{
|
|
211
|
-
"startOffset": 6508,
|
|
212
|
-
"endOffset": 6734,
|
|
213
|
-
"count": 0
|
|
214
|
-
}
|
|
215
|
-
]
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
"functionName": "",
|
|
219
|
-
"isBlockCoverage": false,
|
|
220
|
-
"ranges": [
|
|
221
|
-
{
|
|
222
|
-
"startOffset": 6849,
|
|
223
|
-
"endOffset": 7140,
|
|
224
|
-
"count": 0
|
|
225
|
-
}
|
|
226
|
-
]
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
"functionName": "",
|
|
230
|
-
"isBlockCoverage": true,
|
|
231
|
-
"ranges": [
|
|
232
|
-
{
|
|
233
|
-
"startOffset": 7252,
|
|
234
|
-
"endOffset": 8187,
|
|
235
|
-
"count": 1
|
|
236
|
-
}
|
|
237
|
-
]
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
"functionName": "",
|
|
241
|
-
"isBlockCoverage": false,
|
|
242
|
-
"ranges": [
|
|
243
|
-
{
|
|
244
|
-
"startOffset": 7625,
|
|
245
|
-
"endOffset": 7749,
|
|
246
|
-
"count": 0
|
|
247
|
-
}
|
|
248
|
-
]
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
"functionName": "",
|
|
252
|
-
"isBlockCoverage": false,
|
|
253
|
-
"ranges": [
|
|
254
|
-
{
|
|
255
|
-
"startOffset": 7899,
|
|
256
|
-
"endOffset": 7920,
|
|
257
|
-
"count": 0
|
|
258
|
-
}
|
|
259
|
-
]
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
"functionName": "",
|
|
263
|
-
"isBlockCoverage": false,
|
|
264
|
-
"ranges": [
|
|
265
|
-
{
|
|
266
|
-
"startOffset": 7968,
|
|
267
|
-
"endOffset": 8118,
|
|
268
|
-
"count": 0
|
|
269
|
-
}
|
|
270
|
-
]
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
"functionName": "",
|
|
274
|
-
"isBlockCoverage": true,
|
|
275
|
-
"ranges": [
|
|
276
|
-
{
|
|
277
|
-
"startOffset": 8247,
|
|
278
|
-
"endOffset": 10173,
|
|
279
|
-
"count": 1
|
|
280
|
-
}
|
|
281
|
-
]
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
"functionName": "",
|
|
285
|
-
"isBlockCoverage": false,
|
|
286
|
-
"ranges": [
|
|
287
|
-
{
|
|
288
|
-
"startOffset": 8308,
|
|
289
|
-
"endOffset": 8346,
|
|
290
|
-
"count": 0
|
|
291
|
-
}
|
|
292
|
-
]
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
"functionName": "",
|
|
296
|
-
"isBlockCoverage": false,
|
|
297
|
-
"ranges": [
|
|
298
|
-
{
|
|
299
|
-
"startOffset": 8407,
|
|
300
|
-
"endOffset": 8437,
|
|
301
|
-
"count": 0
|
|
302
|
-
}
|
|
303
|
-
]
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
"functionName": "",
|
|
307
|
-
"isBlockCoverage": false,
|
|
308
|
-
"ranges": [
|
|
309
|
-
{
|
|
310
|
-
"startOffset": 8473,
|
|
311
|
-
"endOffset": 10161,
|
|
312
|
-
"count": 0
|
|
313
|
-
}
|
|
314
|
-
]
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
"functionName": "",
|
|
318
|
-
"isBlockCoverage": false,
|
|
319
|
-
"ranges": [
|
|
320
|
-
{
|
|
321
|
-
"startOffset": 10269,
|
|
322
|
-
"endOffset": 10808,
|
|
323
|
-
"count": 0
|
|
324
|
-
}
|
|
325
|
-
]
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"functionName": "truncateText",
|
|
329
|
-
"isBlockCoverage": false,
|
|
330
|
-
"ranges": [
|
|
331
|
-
{
|
|
332
|
-
"startOffset": 10893,
|
|
333
|
-
"endOffset": 11054,
|
|
334
|
-
"count": 0
|
|
335
|
-
}
|
|
336
|
-
]
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
"functionName": "",
|
|
340
|
-
"isBlockCoverage": false,
|
|
341
|
-
"ranges": [
|
|
342
|
-
{
|
|
343
|
-
"startOffset": 11206,
|
|
344
|
-
"endOffset": 14556,
|
|
345
|
-
"count": 0
|
|
346
|
-
}
|
|
347
|
-
]
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
"functionName": "formatTime",
|
|
351
|
-
"isBlockCoverage": false,
|
|
352
|
-
"ranges": [
|
|
353
|
-
{
|
|
354
|
-
"startOffset": 14606,
|
|
355
|
-
"endOffset": 14970,
|
|
356
|
-
"count": 0
|
|
357
|
-
}
|
|
358
|
-
]
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
"functionName": "renderMarkdown",
|
|
362
|
-
"isBlockCoverage": false,
|
|
363
|
-
"ranges": [
|
|
364
|
-
{
|
|
365
|
-
"startOffset": 15159,
|
|
366
|
-
"endOffset": 17551,
|
|
367
|
-
"count": 0
|
|
368
|
-
}
|
|
369
|
-
]
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
"functionName": "toggleTool",
|
|
373
|
-
"isBlockCoverage": false,
|
|
374
|
-
"ranges": [
|
|
375
|
-
{
|
|
376
|
-
"startOffset": 17589,
|
|
377
|
-
"endOffset": 17848,
|
|
378
|
-
"count": 0
|
|
379
|
-
}
|
|
380
|
-
]
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
"functionName": "highlightSearchText",
|
|
384
|
-
"isBlockCoverage": false,
|
|
385
|
-
"ranges": [
|
|
386
|
-
{
|
|
387
|
-
"startOffset": 17895,
|
|
388
|
-
"endOffset": 19316,
|
|
389
|
-
"count": 0
|
|
390
|
-
}
|
|
391
|
-
]
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
"functionName": "toggleContent",
|
|
395
|
-
"isBlockCoverage": false,
|
|
396
|
-
"ranges": [
|
|
397
|
-
{
|
|
398
|
-
"startOffset": 19357,
|
|
399
|
-
"endOffset": 19689,
|
|
400
|
-
"count": 0
|
|
401
|
-
}
|
|
402
|
-
]
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
"functionName": "isContentTooLong",
|
|
406
|
-
"isBlockCoverage": false,
|
|
407
|
-
"ranges": [
|
|
408
|
-
{
|
|
409
|
-
"startOffset": 19733,
|
|
410
|
-
"endOffset": 19897,
|
|
411
|
-
"count": 0
|
|
412
|
-
}
|
|
413
|
-
]
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
"functionName": "truncateContent",
|
|
417
|
-
"isBlockCoverage": false,
|
|
418
|
-
"ranges": [
|
|
419
|
-
{
|
|
420
|
-
"startOffset": 19940,
|
|
421
|
-
"endOffset": 20110,
|
|
422
|
-
"count": 0
|
|
423
|
-
}
|
|
424
|
-
]
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
"functionName": "getBadgeInfo",
|
|
428
|
-
"isBlockCoverage": false,
|
|
429
|
-
"ranges": [
|
|
430
|
-
{
|
|
431
|
-
"startOffset": 20150,
|
|
432
|
-
"endOffset": 21565,
|
|
433
|
-
"count": 0
|
|
434
|
-
}
|
|
435
|
-
]
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
"functionName": "getToolStatus",
|
|
439
|
-
"isBlockCoverage": false,
|
|
440
|
-
"ranges": [
|
|
441
|
-
{
|
|
442
|
-
"startOffset": 21606,
|
|
443
|
-
"endOffset": 22043,
|
|
444
|
-
"count": 0
|
|
445
|
-
}
|
|
446
|
-
]
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
"functionName": "getToolErrorMessage",
|
|
450
|
-
"isBlockCoverage": false,
|
|
451
|
-
"ranges": [
|
|
452
|
-
{
|
|
453
|
-
"startOffset": 22090,
|
|
454
|
-
"endOffset": 22840,
|
|
455
|
-
"count": 0
|
|
456
|
-
}
|
|
457
|
-
]
|
|
458
|
-
},
|
|
459
|
-
{
|
|
460
|
-
"functionName": "getToolDuration",
|
|
461
|
-
"isBlockCoverage": false,
|
|
462
|
-
"ranges": [
|
|
463
|
-
{
|
|
464
|
-
"startOffset": 22883,
|
|
465
|
-
"endOffset": 23287,
|
|
466
|
-
"count": 0
|
|
467
|
-
}
|
|
468
|
-
]
|
|
469
|
-
},
|
|
470
|
-
{
|
|
471
|
-
"functionName": "getToolCommand",
|
|
472
|
-
"isBlockCoverage": false,
|
|
473
|
-
"ranges": [
|
|
474
|
-
{
|
|
475
|
-
"startOffset": 23329,
|
|
476
|
-
"endOffset": 24971,
|
|
477
|
-
"count": 0
|
|
478
|
-
}
|
|
479
|
-
]
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
"functionName": "hasTools",
|
|
483
|
-
"isBlockCoverage": false,
|
|
484
|
-
"ranges": [
|
|
485
|
-
{
|
|
486
|
-
"startOffset": 25007,
|
|
487
|
-
"endOffset": 25178,
|
|
488
|
-
"count": 0
|
|
489
|
-
}
|
|
490
|
-
]
|
|
491
|
-
},
|
|
492
|
-
{
|
|
493
|
-
"functionName": "getToolGroups",
|
|
494
|
-
"isBlockCoverage": false,
|
|
495
|
-
"ranges": [
|
|
496
|
-
{
|
|
497
|
-
"startOffset": 25219,
|
|
498
|
-
"endOffset": 26054,
|
|
499
|
-
"count": 0
|
|
500
|
-
}
|
|
501
|
-
]
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
"functionName": "hashCode",
|
|
505
|
-
"isBlockCoverage": false,
|
|
506
|
-
"ranges": [
|
|
507
|
-
{
|
|
508
|
-
"startOffset": 26505,
|
|
509
|
-
"endOffset": 26785,
|
|
510
|
-
"count": 0
|
|
511
|
-
}
|
|
512
|
-
]
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
"functionName": "getSubagentInfo",
|
|
516
|
-
"isBlockCoverage": false,
|
|
517
|
-
"ranges": [
|
|
518
|
-
{
|
|
519
|
-
"startOffset": 26820,
|
|
520
|
-
"endOffset": 28388,
|
|
521
|
-
"count": 0
|
|
522
|
-
}
|
|
523
|
-
]
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
"functionName": "getSubagentColor",
|
|
527
|
-
"isBlockCoverage": false,
|
|
528
|
-
"ranges": [
|
|
529
|
-
{
|
|
530
|
-
"startOffset": 28424,
|
|
531
|
-
"endOffset": 28603,
|
|
532
|
-
"count": 0
|
|
533
|
-
}
|
|
534
|
-
]
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
"functionName": "setFilter",
|
|
538
|
-
"isBlockCoverage": false,
|
|
539
|
-
"ranges": [
|
|
540
|
-
{
|
|
541
|
-
"startOffset": 28632,
|
|
542
|
-
"endOffset": 28691,
|
|
543
|
-
"count": 0
|
|
544
|
-
}
|
|
545
|
-
]
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
"functionName": "scrollToTurn",
|
|
549
|
-
"isBlockCoverage": false,
|
|
550
|
-
"ranges": [
|
|
551
|
-
{
|
|
552
|
-
"startOffset": 28731,
|
|
553
|
-
"endOffset": 29855,
|
|
554
|
-
"count": 0
|
|
555
|
-
}
|
|
556
|
-
]
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
"functionName": "jumpToTurn",
|
|
560
|
-
"isBlockCoverage": false,
|
|
561
|
-
"ranges": [
|
|
562
|
-
{
|
|
563
|
-
"startOffset": 29893,
|
|
564
|
-
"endOffset": 30384,
|
|
565
|
-
"count": 0
|
|
566
|
-
}
|
|
567
|
-
]
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
"functionName": "getTurnNumber",
|
|
571
|
-
"isBlockCoverage": false,
|
|
572
|
-
"ranges": [
|
|
573
|
-
{
|
|
574
|
-
"startOffset": 30425,
|
|
575
|
-
"endOffset": 30930,
|
|
576
|
-
"count": 0
|
|
577
|
-
}
|
|
578
|
-
]
|
|
579
|
-
},
|
|
580
|
-
{
|
|
581
|
-
"functionName": "escapeHtml",
|
|
582
|
-
"isBlockCoverage": false,
|
|
583
|
-
"ranges": [
|
|
584
|
-
{
|
|
585
|
-
"startOffset": 30959,
|
|
586
|
-
"endOffset": 31099,
|
|
587
|
-
"count": 0
|
|
588
|
-
}
|
|
589
|
-
]
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
"functionName": "formatDateTime",
|
|
593
|
-
"isBlockCoverage": true,
|
|
594
|
-
"ranges": [
|
|
595
|
-
{
|
|
596
|
-
"startOffset": 31141,
|
|
597
|
-
"endOffset": 31262,
|
|
598
|
-
"count": 2
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
"startOffset": 31184,
|
|
602
|
-
"endOffset": 31197,
|
|
603
|
-
"count": 0
|
|
604
|
-
}
|
|
605
|
-
]
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
"functionName": "exportSession",
|
|
609
|
-
"isBlockCoverage": false,
|
|
610
|
-
"ranges": [
|
|
611
|
-
{
|
|
612
|
-
"startOffset": 31303,
|
|
613
|
-
"endOffset": 32204,
|
|
614
|
-
"count": 0
|
|
615
|
-
}
|
|
616
|
-
]
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
"functionName": "",
|
|
620
|
-
"isBlockCoverage": true,
|
|
621
|
-
"ranges": [
|
|
622
|
-
{
|
|
623
|
-
"startOffset": 32255,
|
|
624
|
-
"endOffset": 40158,
|
|
625
|
-
"count": 1
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
"startOffset": 32476,
|
|
629
|
-
"endOffset": 33284,
|
|
630
|
-
"count": 0
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
"startOffset": 33286,
|
|
634
|
-
"endOffset": 36789,
|
|
635
|
-
"count": 0
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
"startOffset": 36802,
|
|
639
|
-
"endOffset": 40157,
|
|
640
|
-
"count": 0
|
|
641
|
-
}
|
|
642
|
-
]
|
|
643
|
-
},
|
|
644
|
-
{
|
|
645
|
-
"functionName": "",
|
|
646
|
-
"isBlockCoverage": false,
|
|
647
|
-
"ranges": [
|
|
648
|
-
{
|
|
649
|
-
"startOffset": 33450,
|
|
650
|
-
"endOffset": 36773,
|
|
651
|
-
"count": 0
|
|
652
|
-
}
|
|
653
|
-
]
|
|
654
|
-
},
|
|
655
|
-
{
|
|
656
|
-
"functionName": "",
|
|
657
|
-
"isBlockCoverage": false,
|
|
658
|
-
"ranges": [
|
|
659
|
-
{
|
|
660
|
-
"startOffset": 37055,
|
|
661
|
-
"endOffset": 37233,
|
|
662
|
-
"count": 0
|
|
663
|
-
}
|
|
664
|
-
]
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
"functionName": "updateVisibleRange",
|
|
668
|
-
"isBlockCoverage": false,
|
|
669
|
-
"ranges": [
|
|
670
|
-
{
|
|
671
|
-
"startOffset": 37474,
|
|
672
|
-
"endOffset": 38970,
|
|
673
|
-
"count": 0
|
|
674
|
-
}
|
|
675
|
-
]
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
"functionName": "",
|
|
679
|
-
"isBlockCoverage": false,
|
|
680
|
-
"ranges": [
|
|
681
|
-
{
|
|
682
|
-
"startOffset": 39065,
|
|
683
|
-
"endOffset": 39493,
|
|
684
|
-
"count": 0
|
|
685
|
-
}
|
|
686
|
-
]
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
"functionName": "",
|
|
690
|
-
"isBlockCoverage": false,
|
|
691
|
-
"ranges": [
|
|
692
|
-
{
|
|
693
|
-
"startOffset": 39570,
|
|
694
|
-
"endOffset": 40146,
|
|
695
|
-
"count": 0
|
|
696
|
-
}
|
|
697
|
-
]
|
|
698
|
-
}
|
|
699
|
-
]
|
|
700
|
-
}
|
|
701
|
-
]
|