pi-tau-web-server 1.0.8
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/README.md +303 -0
- package/bin/auth.js +96 -0
- package/bin/config.js +99 -0
- package/bin/model-utils.js +134 -0
- package/bin/server-main.js +1189 -0
- package/bin/sessions.js +492 -0
- package/bin/tau.js +8 -0
- package/bin/tree.js +248 -0
- package/bin/types.js +2 -0
- package/package.json +74 -0
- package/public/app-main.js +2025 -0
- package/public/app-types.js +1 -0
- package/public/app.js +1 -0
- package/public/command-palette.js +42 -0
- package/public/dialogs.js +199 -0
- package/public/file-browser.js +196 -0
- package/public/icons/apple-touch-icon.png +0 -0
- package/public/icons/favicon-16.png +0 -0
- package/public/icons/favicon-32.png +0 -0
- package/public/icons/tau-192.png +0 -0
- package/public/icons/tau-512.png +0 -0
- package/public/icons/tau-logo.png +0 -0
- package/public/icons/tau-logo.svg +13 -0
- package/public/icons/tau-maskable-512.png +0 -0
- package/public/icons/tau-new.png +0 -0
- package/public/index.html +264 -0
- package/public/launcher-panel.js +54 -0
- package/public/launcher.js +84 -0
- package/public/manifest.json +28 -0
- package/public/markdown.js +336 -0
- package/public/message-renderer.js +268 -0
- package/public/model-picker.js +478 -0
- package/public/session-sidebar.js +460 -0
- package/public/session-stats-card.js +123 -0
- package/public/state.js +81 -0
- package/public/style.css +3864 -0
- package/public/sw.js +66 -0
- package/public/themes.js +72 -0
- package/public/tool-card.js +317 -0
- package/public/tree-view.js +474 -0
- package/public/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/public/vendor/katex/katex.min.css +1 -0
- package/public/vendor/katex/katex.min.js +1 -0
- package/public/voice-input.js +74 -0
- package/public/websocket-client.js +156 -0
- package/scripts/copy-katex.mjs +32 -0
- package/src/pi-extension/tau-tree.ts +71 -0
- package/src/public/app-main.ts +2190 -0
- package/src/public/app-types.ts +96 -0
- package/src/public/app.ts +1 -0
- package/src/public/command-palette.ts +53 -0
- package/src/public/dialogs.ts +251 -0
- package/src/public/file-browser.ts +224 -0
- package/src/public/launcher-panel.ts +68 -0
- package/src/public/launcher.ts +101 -0
- package/src/public/legacy-dom.d.ts +29 -0
- package/src/public/markdown.ts +372 -0
- package/src/public/message-renderer.ts +311 -0
- package/src/public/model-picker.ts +500 -0
- package/src/public/session-sidebar.ts +522 -0
- package/src/public/session-stats-card.ts +176 -0
- package/src/public/state.ts +96 -0
- package/src/public/sw.ts +79 -0
- package/src/public/themes.ts +73 -0
- package/src/public/tool-card.ts +375 -0
- package/src/public/tree-view.ts +527 -0
- package/src/public/voice-input.ts +98 -0
- package/src/public/websocket-client.ts +165 -0
- package/src/server/auth.ts +88 -0
- package/src/server/config.ts +88 -0
- package/src/server/model-utils.ts +122 -0
- package/src/server/server-main.ts +1004 -0
- package/src/server/sessions.ts +481 -0
- package/src/server/tau.ts +9 -0
- package/src/server/tree.ts +288 -0
- package/src/server/types.ts +68 -0
- package/tsconfig.json +3 -0
- package/tsconfig.public.json +15 -0
- package/tsconfig.server.json +16 -0
package/public/sw.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tau Service Worker — minimal, just enables PWA install
|
|
3
|
+
// No aggressive caching since Tau connects to a live local server
|
|
4
|
+
/// <reference lib="webworker" />
|
|
5
|
+
const CACHE_NAME = 'tau-v4';
|
|
6
|
+
const serviceWorker = self;
|
|
7
|
+
// Cache only the app shell on install
|
|
8
|
+
serviceWorker.addEventListener('install', (event) => {
|
|
9
|
+
event.waitUntil(caches.open(CACHE_NAME).then((cache) => {
|
|
10
|
+
return cache.addAll([
|
|
11
|
+
'/',
|
|
12
|
+
'/style.css',
|
|
13
|
+
'/app.js',
|
|
14
|
+
'/app-main.js',
|
|
15
|
+
'/state.js',
|
|
16
|
+
'/themes.js',
|
|
17
|
+
'/markdown.js',
|
|
18
|
+
'/message-renderer.js',
|
|
19
|
+
'/tool-card.js',
|
|
20
|
+
'/tree-view.js',
|
|
21
|
+
'/dialogs.js',
|
|
22
|
+
'/session-sidebar.js',
|
|
23
|
+
'/session-stats-card.js',
|
|
24
|
+
'/websocket-client.js',
|
|
25
|
+
'/manifest.json',
|
|
26
|
+
// KaTeX shell assets; fonts are picked up on demand by the
|
|
27
|
+
// network-first runtime cache so a missing font can't fail install.
|
|
28
|
+
'/vendor/katex/katex.min.js',
|
|
29
|
+
'/vendor/katex/katex.min.css',
|
|
30
|
+
]);
|
|
31
|
+
}));
|
|
32
|
+
serviceWorker.skipWaiting();
|
|
33
|
+
});
|
|
34
|
+
// Clean old caches
|
|
35
|
+
serviceWorker.addEventListener('activate', (event) => {
|
|
36
|
+
event.waitUntil(caches.keys().then((names) => {
|
|
37
|
+
return Promise.all(names.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name)));
|
|
38
|
+
}));
|
|
39
|
+
serviceWorker.clients.claim();
|
|
40
|
+
});
|
|
41
|
+
// Network-first strategy — always try live server, fall back to cache
|
|
42
|
+
serviceWorker.addEventListener('fetch', (event) => {
|
|
43
|
+
const url = new URL(event.request.url);
|
|
44
|
+
// Don't cache API/WebSocket requests
|
|
45
|
+
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws')) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
event.respondWith(fetch(event.request)
|
|
49
|
+
.then((response) => {
|
|
50
|
+
// Update cache with fresh response
|
|
51
|
+
if (response.ok && event.request.method === 'GET') {
|
|
52
|
+
const clone = response.clone();
|
|
53
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
54
|
+
}
|
|
55
|
+
return response;
|
|
56
|
+
})
|
|
57
|
+
.catch(() => {
|
|
58
|
+
// Offline — serve from cache
|
|
59
|
+
return caches.match(event.request).then((cached) => {
|
|
60
|
+
return cached || new Response('Tau is offline — start your pi session to connect.', {
|
|
61
|
+
status: 503,
|
|
62
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}));
|
|
66
|
+
});
|
package/public/themes.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme system — four themes: two light, two dark
|
|
3
|
+
*/
|
|
4
|
+
export const themes = {
|
|
5
|
+
night: {
|
|
6
|
+
name: 'Dusk',
|
|
7
|
+
dark: true,
|
|
8
|
+
colors: ['#212121', '#a0a0a0', '#777777', '#666666'],
|
|
9
|
+
vars: {},
|
|
10
|
+
},
|
|
11
|
+
dawn: {
|
|
12
|
+
name: 'Dawn',
|
|
13
|
+
dark: true,
|
|
14
|
+
colors: ['#1a1d26', '#7a8ab0', '#6a5a80', '#5a7a9a'],
|
|
15
|
+
vars: {},
|
|
16
|
+
},
|
|
17
|
+
midnight: {
|
|
18
|
+
name: 'Midnight',
|
|
19
|
+
dark: true,
|
|
20
|
+
colors: ['#000000', '#5a7a9a', '#4a5565', '#4a5a72'],
|
|
21
|
+
vars: {},
|
|
22
|
+
},
|
|
23
|
+
clean: {
|
|
24
|
+
name: 'Clean',
|
|
25
|
+
dark: false,
|
|
26
|
+
colors: ['#ffffff', '#0580c4', '#007aff', '#5ac8fa'],
|
|
27
|
+
vars: {},
|
|
28
|
+
},
|
|
29
|
+
terracotta: {
|
|
30
|
+
name: 'Terracotta',
|
|
31
|
+
dark: false,
|
|
32
|
+
colors: ['#f4f1ec', '#b06a48', '#5c2860', '#3a6a9b'],
|
|
33
|
+
vars: {},
|
|
34
|
+
},
|
|
35
|
+
sage: {
|
|
36
|
+
name: 'Sage',
|
|
37
|
+
dark: false,
|
|
38
|
+
colors: ['#f0f2ec', '#6a7d5a', '#4a3860', '#3a6a7a'],
|
|
39
|
+
vars: {},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export function applyTheme(themeId) {
|
|
43
|
+
const root = document.documentElement;
|
|
44
|
+
// Validate
|
|
45
|
+
if (!(themeId in themes))
|
|
46
|
+
themeId = 'night';
|
|
47
|
+
root.setAttribute('data-theme', themeId);
|
|
48
|
+
localStorage.setItem('tau-theme', themeId);
|
|
49
|
+
}
|
|
50
|
+
export function getCurrentTheme() {
|
|
51
|
+
const saved = localStorage.getItem('tau-theme');
|
|
52
|
+
// Migrate old values
|
|
53
|
+
if (saved === 'dark')
|
|
54
|
+
return 'night';
|
|
55
|
+
if (saved === 'light')
|
|
56
|
+
return 'terracotta';
|
|
57
|
+
if (saved && saved in themes)
|
|
58
|
+
return saved;
|
|
59
|
+
// Auto-detect from OS
|
|
60
|
+
if (window.matchMedia?.('(prefers-color-scheme: light)').matches)
|
|
61
|
+
return 'terracotta';
|
|
62
|
+
return 'night';
|
|
63
|
+
}
|
|
64
|
+
// Listen for OS theme changes if no explicit preference saved
|
|
65
|
+
if (!localStorage.getItem('tau-theme')) {
|
|
66
|
+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
|
67
|
+
if (!localStorage.getItem('tau-theme')) {
|
|
68
|
+
const root = document.documentElement;
|
|
69
|
+
root.setAttribute('data-theme', e.matches ? 'terracotta' : 'night');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Card - Renders and updates tool execution cards (collapsible)
|
|
3
|
+
*/
|
|
4
|
+
export class ToolCardRenderer {
|
|
5
|
+
container;
|
|
6
|
+
toolCards;
|
|
7
|
+
constructor(container) {
|
|
8
|
+
this.container = container;
|
|
9
|
+
this.toolCards = new Map(); // toolCallId -> element
|
|
10
|
+
}
|
|
11
|
+
createToolCard(toolExecution) {
|
|
12
|
+
const { toolCallId, toolName, args, status } = toolExecution;
|
|
13
|
+
const card = document.createElement('div');
|
|
14
|
+
card.className = 'tool-card';
|
|
15
|
+
card.dataset.toolCallId = String(toolCallId || '');
|
|
16
|
+
const argsPreview = this.getArgsPreview(String(toolName || ''), args);
|
|
17
|
+
const argsJson = this.formatJson(args);
|
|
18
|
+
const isExpanded = (status === 'streaming' || status === 'pending');
|
|
19
|
+
const isEdit = (toolName === 'edit' || toolName === 'Edit') && args && (args.oldText || args.old_text) && (args.newText || args.new_text);
|
|
20
|
+
card.innerHTML = `
|
|
21
|
+
<div class="tool-card-header" onclick="this.parentElement.querySelector('.tool-card-body').classList.toggle('expanded'); this.querySelector('.tool-card-chevron').classList.toggle('expanded')">
|
|
22
|
+
<div class="tool-header-left">
|
|
23
|
+
<span class="tool-card-chevron${isExpanded ? ' expanded' : ''}"><svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor"><path d="M2 1l4 3-4 3z"/></svg></span>
|
|
24
|
+
<span class="tool-name">${this.escapeHtml(toolName || '')}</span>
|
|
25
|
+
${argsPreview ? `<span class="tool-args-preview">${this.escapeHtml(argsPreview)}</span>` : ''}
|
|
26
|
+
</div>
|
|
27
|
+
<div class="tool-header-right">
|
|
28
|
+
<button class="tool-action-btn copy-output-btn" title="Copy output" onclick="event.stopPropagation(); var t=this.closest('.tool-card').querySelector('.tool-output'); if(!t||!t.textContent.trim())return; var s=t.textContent,b=this; (navigator.clipboard?navigator.clipboard.writeText(s):new Promise(function(r){var a=document.createElement('textarea');a.value=s;a.style.cssText='position:fixed;left:-9999px';document.body.appendChild(a);a.select();document.execCommand('copy');document.body.removeChild(a);r()})).then(function(){b.classList.add('copied');setTimeout(function(){b.classList.remove('copied')},1500)})"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
|
|
29
|
+
<div class="tool-status ${status}">${status}</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="tool-card-body${isExpanded ? ' expanded' : ''}">
|
|
33
|
+
${!isEdit && argsJson ? `<div class="tool-args">${this.escapeHtml(argsJson)}</div>` : ''}
|
|
34
|
+
<div class="tool-output-wrapper">
|
|
35
|
+
<div class="tool-output"></div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
`;
|
|
39
|
+
// Insert diff view for Edit tools
|
|
40
|
+
if (isEdit) {
|
|
41
|
+
const diffEl = this.renderDiff(String(args.oldText || args.old_text || ''), String(args.newText || args.new_text || ''));
|
|
42
|
+
const body = card.querySelector('.tool-card-body');
|
|
43
|
+
if (body)
|
|
44
|
+
body.insertBefore(diffEl, body.firstChild);
|
|
45
|
+
}
|
|
46
|
+
this.container.appendChild(card);
|
|
47
|
+
this.toolCards.set(String(toolCallId || ''), card);
|
|
48
|
+
this.scrollToBottom();
|
|
49
|
+
return card;
|
|
50
|
+
}
|
|
51
|
+
updateToolCard(toolExecution) {
|
|
52
|
+
let card = this.toolCards.get(String(toolExecution.toolCallId || ''));
|
|
53
|
+
if (!card) {
|
|
54
|
+
card = this.createToolCard(toolExecution);
|
|
55
|
+
}
|
|
56
|
+
// Update status
|
|
57
|
+
const statusElement = card.querySelector('.tool-status');
|
|
58
|
+
if (statusElement) {
|
|
59
|
+
statusElement.className = `tool-status ${toolExecution.status}`;
|
|
60
|
+
statusElement.textContent = toolExecution.status ?? null;
|
|
61
|
+
}
|
|
62
|
+
// Auto-expand when streaming
|
|
63
|
+
if (toolExecution.status === 'streaming') {
|
|
64
|
+
const body = card.querySelector('.tool-card-body');
|
|
65
|
+
const chevron = card.querySelector('.tool-card-chevron');
|
|
66
|
+
if (body)
|
|
67
|
+
body.classList.add('expanded');
|
|
68
|
+
if (chevron)
|
|
69
|
+
chevron.classList.add('expanded');
|
|
70
|
+
}
|
|
71
|
+
// Update output
|
|
72
|
+
const outputElement = card.querySelector('.tool-output');
|
|
73
|
+
if (outputElement && toolExecution.output) {
|
|
74
|
+
outputElement.textContent = toolExecution.output;
|
|
75
|
+
this.scrollToBottom();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finalizeToolCard(toolCallId, result, isError) {
|
|
79
|
+
const card = this.toolCards.get(toolCallId);
|
|
80
|
+
if (!card)
|
|
81
|
+
return;
|
|
82
|
+
// Update status
|
|
83
|
+
const statusElement = card.querySelector('.tool-status');
|
|
84
|
+
if (statusElement) {
|
|
85
|
+
const status = isError ? 'error' : 'complete';
|
|
86
|
+
statusElement.className = `tool-status ${status}`;
|
|
87
|
+
statusElement.textContent = status;
|
|
88
|
+
}
|
|
89
|
+
// Update output with final result
|
|
90
|
+
const outputElement = card.querySelector('.tool-output');
|
|
91
|
+
if (outputElement && result) {
|
|
92
|
+
const output = this.formatResult(result);
|
|
93
|
+
outputElement.textContent = output;
|
|
94
|
+
}
|
|
95
|
+
// Collapse completed cards (less noise)
|
|
96
|
+
if (!isError) {
|
|
97
|
+
const body = card.querySelector('.tool-card-body');
|
|
98
|
+
const chevron = card.querySelector('.tool-card-chevron');
|
|
99
|
+
if (body)
|
|
100
|
+
body.classList.remove('expanded');
|
|
101
|
+
if (chevron)
|
|
102
|
+
chevron.classList.remove('expanded');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create a pre-collapsed card for session history using DOM methods (no innerHTML)
|
|
107
|
+
*/
|
|
108
|
+
createHistoryCard(toolExecution) {
|
|
109
|
+
const { toolCallId, toolName, args } = toolExecution;
|
|
110
|
+
const card = document.createElement('div');
|
|
111
|
+
card.className = 'tool-card';
|
|
112
|
+
card.dataset.toolCallId = String(toolCallId || '');
|
|
113
|
+
// Header
|
|
114
|
+
const header = document.createElement('div');
|
|
115
|
+
header.className = 'tool-card-header';
|
|
116
|
+
const headerLeft = document.createElement('div');
|
|
117
|
+
headerLeft.className = 'tool-header-left';
|
|
118
|
+
const chevron = document.createElement('span');
|
|
119
|
+
chevron.className = 'tool-card-chevron';
|
|
120
|
+
chevron.innerHTML = '<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor"><path d="M2 1l4 3-4 3z"/></svg>';
|
|
121
|
+
headerLeft.appendChild(chevron);
|
|
122
|
+
const name = document.createElement('span');
|
|
123
|
+
name.className = 'tool-name';
|
|
124
|
+
name.textContent = String(toolName || '');
|
|
125
|
+
headerLeft.appendChild(name);
|
|
126
|
+
const preview = this.getArgsPreview(String(toolName || ''), args);
|
|
127
|
+
if (preview) {
|
|
128
|
+
const previewEl = document.createElement('span');
|
|
129
|
+
previewEl.className = 'tool-args-preview';
|
|
130
|
+
previewEl.textContent = preview;
|
|
131
|
+
headerLeft.appendChild(previewEl);
|
|
132
|
+
}
|
|
133
|
+
header.appendChild(headerLeft);
|
|
134
|
+
// Right side: copy button + status
|
|
135
|
+
const headerRight = document.createElement('div');
|
|
136
|
+
headerRight.className = 'tool-header-right';
|
|
137
|
+
const copyBtn = document.createElement('button');
|
|
138
|
+
copyBtn.className = 'tool-action-btn copy-output-btn';
|
|
139
|
+
copyBtn.title = 'Copy output';
|
|
140
|
+
copyBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
|
|
141
|
+
copyBtn.addEventListener('click', (e) => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
const output = card.querySelector('.tool-output');
|
|
144
|
+
if (!output || !output.textContent.trim())
|
|
145
|
+
return;
|
|
146
|
+
const text = output.textContent;
|
|
147
|
+
(navigator.clipboard ? navigator.clipboard.writeText(text) : new Promise((r) => {
|
|
148
|
+
const ta = document.createElement('textarea');
|
|
149
|
+
ta.value = text;
|
|
150
|
+
ta.style.cssText = 'position:fixed;left:-9999px';
|
|
151
|
+
document.body.appendChild(ta);
|
|
152
|
+
ta.select();
|
|
153
|
+
document.execCommand('copy');
|
|
154
|
+
document.body.removeChild(ta);
|
|
155
|
+
r();
|
|
156
|
+
})).then(() => {
|
|
157
|
+
copyBtn.classList.add('copied');
|
|
158
|
+
setTimeout(() => copyBtn.classList.remove('copied'), 1500);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
headerRight.appendChild(copyBtn);
|
|
162
|
+
const status = document.createElement('div');
|
|
163
|
+
status.className = 'tool-status complete';
|
|
164
|
+
status.textContent = 'complete';
|
|
165
|
+
headerRight.appendChild(status);
|
|
166
|
+
header.appendChild(headerRight);
|
|
167
|
+
// Toggle expand on click
|
|
168
|
+
header.addEventListener('click', () => {
|
|
169
|
+
body.classList.toggle('expanded');
|
|
170
|
+
chevron.classList.toggle('expanded');
|
|
171
|
+
});
|
|
172
|
+
card.appendChild(header);
|
|
173
|
+
// Body (collapsed by default)
|
|
174
|
+
const body = document.createElement('div');
|
|
175
|
+
body.className = 'tool-card-body';
|
|
176
|
+
const isEdit = (toolName === 'edit' || toolName === 'Edit') && args && (args.oldText || args.old_text) && (args.newText || args.new_text);
|
|
177
|
+
if (isEdit) {
|
|
178
|
+
body.appendChild(this.renderDiff(String(args.oldText || args.old_text || ''), String(args.newText || args.new_text || '')));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const argsJson = this.formatJson(args);
|
|
182
|
+
if (argsJson) {
|
|
183
|
+
const argsEl = document.createElement('div');
|
|
184
|
+
argsEl.className = 'tool-args';
|
|
185
|
+
argsEl.textContent = argsJson;
|
|
186
|
+
body.appendChild(argsEl);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const outputEl = document.createElement('div');
|
|
190
|
+
outputEl.className = 'tool-output';
|
|
191
|
+
body.appendChild(outputEl);
|
|
192
|
+
card.appendChild(body);
|
|
193
|
+
this.container.appendChild(card);
|
|
194
|
+
this.toolCards.set(String(toolCallId || ''), card);
|
|
195
|
+
return card;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Add result to a history card (stays collapsed)
|
|
199
|
+
*/
|
|
200
|
+
addHistoryResult(toolCallId, result, isError) {
|
|
201
|
+
const card = this.toolCards.get(toolCallId);
|
|
202
|
+
if (!card)
|
|
203
|
+
return;
|
|
204
|
+
if (isError) {
|
|
205
|
+
const statusEl = card.querySelector('.tool-status');
|
|
206
|
+
if (statusEl) {
|
|
207
|
+
statusEl.className = 'tool-status error';
|
|
208
|
+
statusEl.textContent = 'error';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const outputElement = card.querySelector('.tool-output');
|
|
212
|
+
if (outputElement && result) {
|
|
213
|
+
outputElement.textContent = this.formatResult(result);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Compact preview for the header line */
|
|
217
|
+
getArgsPreview(toolName, args) {
|
|
218
|
+
if (!args || Object.keys(args).length === 0)
|
|
219
|
+
return '';
|
|
220
|
+
// Show the most relevant arg inline
|
|
221
|
+
if (args.path)
|
|
222
|
+
return String(args.path);
|
|
223
|
+
if (args.command)
|
|
224
|
+
return String(args.command).substring(0, 80);
|
|
225
|
+
if (args.query)
|
|
226
|
+
return String(args.query).substring(0, 60);
|
|
227
|
+
if (args.url)
|
|
228
|
+
return String(args.url);
|
|
229
|
+
// Fallback: first string value
|
|
230
|
+
for (const val of Object.values(args)) {
|
|
231
|
+
if (typeof val === 'string' && val.length > 0) {
|
|
232
|
+
return val.substring(0, 60);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
formatJson(obj) {
|
|
238
|
+
if (!obj)
|
|
239
|
+
return '';
|
|
240
|
+
try {
|
|
241
|
+
if (Object.keys(obj).length === 0)
|
|
242
|
+
return '';
|
|
243
|
+
return JSON.stringify(obj, null, 2);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return String(obj);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Render a simple inline diff for Edit tool */
|
|
250
|
+
renderDiff(oldText, newText) {
|
|
251
|
+
const container = document.createElement('div');
|
|
252
|
+
container.className = 'tool-diff';
|
|
253
|
+
const oldLines = oldText.split('\n');
|
|
254
|
+
const newLines = newText.split('\n');
|
|
255
|
+
// Removed lines
|
|
256
|
+
for (const line of oldLines) {
|
|
257
|
+
const el = document.createElement('div');
|
|
258
|
+
el.className = 'diff-line diff-removed';
|
|
259
|
+
el.textContent = '- ' + line;
|
|
260
|
+
container.appendChild(el);
|
|
261
|
+
}
|
|
262
|
+
// Added lines
|
|
263
|
+
for (const line of newLines) {
|
|
264
|
+
const el = document.createElement('div');
|
|
265
|
+
el.className = 'diff-line diff-added';
|
|
266
|
+
el.textContent = '+ ' + line;
|
|
267
|
+
container.appendChild(el);
|
|
268
|
+
}
|
|
269
|
+
return container;
|
|
270
|
+
}
|
|
271
|
+
formatResult(result) {
|
|
272
|
+
if (!result)
|
|
273
|
+
return '';
|
|
274
|
+
if (result.content && Array.isArray(result.content)) {
|
|
275
|
+
return result.content
|
|
276
|
+
.map((block) => {
|
|
277
|
+
if (block.type === 'text')
|
|
278
|
+
return block.text;
|
|
279
|
+
return JSON.stringify(block);
|
|
280
|
+
})
|
|
281
|
+
.join('\n');
|
|
282
|
+
}
|
|
283
|
+
return JSON.stringify(result, null, 2);
|
|
284
|
+
}
|
|
285
|
+
escapeHtml(text) {
|
|
286
|
+
const div = document.createElement('div');
|
|
287
|
+
div.textContent = String(text ?? '');
|
|
288
|
+
return div.innerHTML;
|
|
289
|
+
}
|
|
290
|
+
scrollToBottom() {
|
|
291
|
+
if (this.container) {
|
|
292
|
+
const threshold = 100;
|
|
293
|
+
const isNear = this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < threshold;
|
|
294
|
+
if (isNear) {
|
|
295
|
+
requestAnimationFrame(() => {
|
|
296
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
expandAll() {
|
|
302
|
+
this.toolCards.forEach(card => {
|
|
303
|
+
card.querySelector('.tool-card-body')?.classList.add('expanded');
|
|
304
|
+
card.querySelector('.tool-card-chevron')?.classList.add('expanded');
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
collapseAll() {
|
|
308
|
+
this.toolCards.forEach(card => {
|
|
309
|
+
card.querySelector('.tool-card-body')?.classList.remove('expanded');
|
|
310
|
+
card.querySelector('.tool-card-chevron')?.classList.remove('expanded');
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
clear() {
|
|
314
|
+
this.toolCards.forEach((card) => card.remove());
|
|
315
|
+
this.toolCards.clear();
|
|
316
|
+
}
|
|
317
|
+
}
|