claude-code-templates 1.10.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,539 @@
1
+ /**
2
+ * ToolDisplay - Dedicated component for displaying tool uses and results safely
3
+ * Handles proper formatting, truncation, and escaping of tool content
4
+ */
5
+ class ToolDisplay {
6
+ constructor() {
7
+ this.maxContentLength = 500;
8
+ this.maxParamLength = 100;
9
+ }
10
+
11
+ /**
12
+ * Render a tool use block
13
+ * @param {Object} toolBlock - Tool use block
14
+ * @param {Array} toolResults - Associated tool results (optional)
15
+ * @returns {string} Safe HTML string
16
+ */
17
+ renderToolUse(toolBlock, toolResults = null) {
18
+ const toolName = this.escapeHtml(toolBlock.name || 'Unknown');
19
+ const toolId = toolBlock.id ? toolBlock.id.slice(-8) : 'unknown';
20
+
21
+ // Generate compact command representation
22
+ const commandSummary = this.generateCompactCommand(toolName, toolBlock.input);
23
+
24
+ // For ALL tools, ALWAYS add a "Show details" button
25
+ const contentId = toolName.toLowerCase() + '_' + toolId + '_' + Date.now();
26
+
27
+ // Try to find corresponding tool result in toolResults first
28
+ let matchingResult = null;
29
+ if (toolResults && Array.isArray(toolResults)) {
30
+ matchingResult = toolResults.find(result => result.tool_use_id === toolBlock.id);
31
+ }
32
+
33
+ // Prepare comprehensive modal content for all tools (without tool results, as they're shown inline now)
34
+ let modalContent = this.generateComprehensiveToolContent(toolName, toolBlock, null);
35
+
36
+ // Store the tool content for modal display
37
+ if (typeof window !== 'undefined') {
38
+ window.storedContent = window.storedContent || {};
39
+ window.storedContent[contentId] = modalContent;
40
+ }
41
+
42
+ // Always show "Show details" for ALL tools
43
+ const buttonClass = toolName === 'Bash' ? 'bash-cmd-btn' : 'tool-detail-btn';
44
+ let showResultsButton = ` <button class="show-results-btn ${buttonClass}" data-content-id="${contentId}">Show details</button>`;
45
+
46
+ let toolUseHtml = `
47
+ <div class="terminal-tool tool-use compact">
48
+ <span class="tool-command">${commandSummary}${showResultsButton}</span>
49
+ </div>
50
+ `;
51
+
52
+ // Render associated tool results if they exist, with proper truncation
53
+ if (matchingResult) {
54
+ toolUseHtml += this.renderToolResultWithTruncation(matchingResult);
55
+ }
56
+
57
+ return toolUseHtml;
58
+ }
59
+
60
+ /**
61
+ * Render a tool result block with truncation support
62
+ * @param {Object} toolResultBlock - Tool result block
63
+ * @returns {string} Safe HTML string
64
+ */
65
+ renderToolResultWithTruncation(toolResultBlock) {
66
+ const toolId = toolResultBlock.tool_use_id ? toolResultBlock.tool_use_id.slice(-8) : 'unknown';
67
+ const isError = toolResultBlock.is_error || false;
68
+
69
+ // Generate enhanced result content with metadata
70
+ const resultContent = this.generateEnhancedResultContent(toolResultBlock);
71
+ const compactOutput = this.generateCompactOutput(resultContent, isError);
72
+
73
+ return `
74
+ <div class="terminal-tool tool-result compact ${isError ? 'error' : 'success'}" data-tool-use-id="${toolResultBlock.tool_use_id}">
75
+ <span class="tool-prompt">⎿</span>
76
+ <span class="tool-output-compact">${compactOutput}</span>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ /**
82
+ * Render a tool result block (legacy method, kept for compatibility)
83
+ * @param {Object} toolResultBlock - Tool result block
84
+ * @returns {string} Safe HTML string
85
+ */
86
+ renderToolResult(toolResultBlock) {
87
+ return this.renderToolResultWithTruncation(toolResultBlock);
88
+ }
89
+
90
+ /**
91
+ * Generate enhanced result content including metadata
92
+ * @param {Object} toolResultBlock - Tool result block
93
+ * @returns {string} Enhanced result content
94
+ */
95
+ generateEnhancedResultContent(toolResultBlock) {
96
+ let content = '';
97
+
98
+ // Add return code interpretation if available
99
+ if (toolResultBlock.returnCodeInterpretation && toolResultBlock.returnCodeInterpretation !== 'none') {
100
+ content += `${toolResultBlock.returnCodeInterpretation}\n`;
101
+ }
102
+
103
+ // Add main content
104
+ if (toolResultBlock.content) {
105
+ if (content) content += '\n';
106
+ content += toolResultBlock.content;
107
+ }
108
+
109
+ // Add stdout if different from content
110
+ if (toolResultBlock.stdout && toolResultBlock.stdout !== toolResultBlock.content) {
111
+ if (content) content += '\n';
112
+ content += toolResultBlock.stdout;
113
+ }
114
+
115
+ // Add stderr if present
116
+ if (toolResultBlock.stderr && toolResultBlock.stderr.trim()) {
117
+ if (content) content += '\n';
118
+ content += `stderr: ${toolResultBlock.stderr}`;
119
+ }
120
+
121
+ return content || '[Empty result]';
122
+ }
123
+
124
+ /**
125
+ * Generate compact command representation for tool use
126
+ * @param {string} toolName - Tool name
127
+ * @param {Object} input - Tool input parameters
128
+ * @returns {string} Compact command
129
+ */
130
+ generateCompactCommand(toolName, input) {
131
+ if (!input || typeof input !== 'object') {
132
+ return `${toolName}()`;
133
+ }
134
+
135
+ switch (toolName) {
136
+ case 'Bash':
137
+ if (input.command) {
138
+ const command = this.escapeHtml(input.command);
139
+ return `<span class="tool-name-bold">Bash </span>(${command})`;
140
+ }
141
+ break;
142
+
143
+ case 'Read':
144
+ if (input.file_path) {
145
+ const fileName = input.file_path.split('/').pop();
146
+ return `<span class="tool-name-bold">Read </span>(${this.escapeHtml(fileName)})`;
147
+ }
148
+ break;
149
+
150
+ case 'Edit':
151
+ if (input.file_path) {
152
+ const fileName = input.file_path.split('/').pop();
153
+ return `<span class="tool-name-bold">Edit </span>(${this.escapeHtml(fileName)})`;
154
+ }
155
+ break;
156
+
157
+ case 'Write':
158
+ if (input.file_path) {
159
+ const fileName = input.file_path.split('/').pop();
160
+ return `<span class="tool-name-bold">Write </span>(${this.escapeHtml(fileName)})`;
161
+ }
162
+ break;
163
+
164
+ case 'Glob':
165
+ if (input.pattern) {
166
+ return `<span class="tool-name-bold">Glob </span>("${this.escapeHtml(input.pattern)}")`;
167
+ }
168
+ break;
169
+
170
+ case 'Grep':
171
+ if (input.pattern) {
172
+ return `<span class="tool-name-bold">Grep </span>("${this.escapeHtml(input.pattern)}")`;
173
+ }
174
+ break;
175
+
176
+ case 'TodoWrite':
177
+ const todoCount = Array.isArray(input.todos) ? input.todos.length : 0;
178
+ return `<span class="tool-name-bold">TodoWrite </span>(${todoCount} todos)`;
179
+ }
180
+
181
+ return `${toolName}()`;
182
+ }
183
+
184
+ /**
185
+ * Generate compact output representation for tool results
186
+ * @param {*} content - Tool result content
187
+ * @param {boolean} _isError - Whether this is an error result (unused)
188
+ * @returns {string} Compact output
189
+ */
190
+ generateCompactOutput(content, _isError) {
191
+ if (typeof content === 'string') {
192
+ // For JSON content, try to format it nicely
193
+ if (content.trim().startsWith('{') && content.trim().endsWith('}')) {
194
+ try {
195
+ const parsed = JSON.parse(content);
196
+ const formatted = JSON.stringify(parsed, null, 2);
197
+ const lines = formatted.split('\n');
198
+ if (lines.length > 5) {
199
+ const preview = lines.slice(0, 5).join('\n');
200
+ const remaining = lines.length - 5;
201
+ const contentId = 'json_' + Date.now() + '_' + Math.random().toString(36).substring(2, 11);
202
+
203
+ if (typeof window !== 'undefined') {
204
+ window.storedContent = window.storedContent || {};
205
+ window.storedContent[contentId] = formatted;
206
+ }
207
+
208
+ return `<pre class="json-output">${this.escapeHtml(preview)}\n<span class="continuation">… +${remaining} lines hidden <button class="show-results-btn text-expand-btn" data-content-id="${contentId}">Show +${remaining} lines</button></span></pre>`;
209
+ } else {
210
+ return `<pre class="json-output">${this.escapeHtml(formatted)}</pre>`;
211
+ }
212
+ } catch (e) {
213
+ // Fall through to regular text handling
214
+ }
215
+ }
216
+
217
+ // For multi-line content, show first few lines with continuation
218
+ const lines = content.split('\n');
219
+ if (lines.length > 5) {
220
+ const preview = lines.slice(0, 5).join('\n');
221
+ const remaining = lines.length - 5;
222
+ const contentId = 'content_' + Date.now() + '_' + Math.random().toString(36).substring(2, 11);
223
+
224
+ // Store content in global storage without inline scripts
225
+ if (typeof window !== 'undefined') {
226
+ window.storedContent = window.storedContent || {};
227
+ window.storedContent[contentId] = content;
228
+ }
229
+
230
+ return `<pre class="text-output">${this.escapeHtml(preview)}\n<span class="continuation">… +${remaining} lines hidden <button class="show-results-btn text-expand-btn" data-content-id="${contentId}">Show +${remaining} lines</button></span></pre>`;
231
+ } else {
232
+ return `<pre class="text-output">${this.escapeHtml(content)}</pre>`;
233
+ }
234
+ } else if (Array.isArray(content)) {
235
+ return `<span class="array-output">[${content.length} items]</span>`;
236
+ } else if (content && typeof content === 'object') {
237
+ const keys = Object.keys(content);
238
+ return `<span class="object-output">{${keys.length} properties}</span>`;
239
+ }
240
+
241
+ return '<span class="empty-output">[empty]</span>';
242
+ }
243
+
244
+ /**
245
+ * Generate tool summary based on tool type
246
+ * @param {string} toolName - Tool name
247
+ * @param {Object} input - Tool input parameters
248
+ * @returns {string} Tool summary
249
+ */
250
+ generateToolSummary(toolName, input) {
251
+ if (!input || typeof input !== 'object') return '';
252
+
253
+ switch (toolName) {
254
+ case 'TodoWrite':
255
+ const todoCount = Array.isArray(input.todos) ? input.todos.length : 0;
256
+ return `${todoCount} todo${todoCount !== 1 ? 's' : ''}`;
257
+
258
+ case 'Read':
259
+ if (input.file_path) {
260
+ const fileName = input.file_path.split('/').pop();
261
+ return this.escapeHtml(fileName);
262
+ }
263
+ break;
264
+
265
+ case 'Edit':
266
+ if (input.file_path) {
267
+ const fileName = input.file_path.split('/').pop();
268
+ const changeSize = input.old_string ? input.old_string.length : 0;
269
+ return `${this.escapeHtml(fileName)} (${changeSize}b)`;
270
+ }
271
+ break;
272
+
273
+ case 'Bash':
274
+ if (input.command) {
275
+ const command = this.truncateText(input.command, 40);
276
+ return `<span class="bash-command">${this.escapeHtml(command)}</span>`;
277
+ }
278
+ break;
279
+
280
+ case 'Write':
281
+ if (input.file_path) {
282
+ const fileName = input.file_path.split('/').pop();
283
+ const contentSize = input.content ? input.content.length : 0;
284
+ return `${this.escapeHtml(fileName)} (${contentSize}b)`;
285
+ }
286
+ break;
287
+
288
+ case 'Glob':
289
+ if (input.pattern) {
290
+ return `"${this.escapeHtml(input.pattern)}"`;
291
+ }
292
+ break;
293
+
294
+ case 'Grep':
295
+ if (input.pattern) {
296
+ return `"${this.escapeHtml(input.pattern)}"`;
297
+ }
298
+ break;
299
+ }
300
+
301
+ return '';
302
+ }
303
+
304
+
305
+ /**
306
+ * Format Bash command output with proper console styling
307
+ * @param {string} content - Bash output content
308
+ * @returns {string} Formatted HTML
309
+ */
310
+ formatBashOutput(content) {
311
+ if (!content) return '';
312
+
313
+ const lines = content.split('\n');
314
+ const formattedLines = lines.map(line => {
315
+ // Escape HTML first
316
+ line = this.escapeHtml(line);
317
+
318
+ // Highlight different types of output
319
+ if (line.includes('Error:') || line.includes('ERROR') || line.includes('❌')) {
320
+ return `<span class="console-error">${line}</span>`;
321
+ } else if (line.includes('Warning:') || line.includes('WARN') || line.includes('⚠️')) {
322
+ return `<span class="console-warning">${line}</span>`;
323
+ } else if (line.includes('✅') || line.includes('SUCCESS')) {
324
+ return `<span class="console-success">${line}</span>`;
325
+ } else if (line.startsWith('>')) {
326
+ return `<span class="console-command">${line}</span>`;
327
+ } else if (line.includes('📊') || line.includes('🔧') || line.includes('⚡')) {
328
+ return `<span class="console-info">${line}</span>`;
329
+ } else {
330
+ return `<span class="console-output">${line}</span>`;
331
+ }
332
+ });
333
+
334
+ return formattedLines.join('<br>');
335
+ }
336
+
337
+ /**
338
+ * Generate result preview
339
+ * @param {*} content - Tool result content
340
+ * @returns {string} Result preview
341
+ */
342
+ generateResultPreview(content) {
343
+ if (typeof content === 'string') {
344
+ if (content.length > 50) {
345
+ const preview = this.truncateText(content, 50);
346
+ return this.escapeHtml(preview);
347
+ }
348
+ return this.escapeHtml(content);
349
+ } else if (Array.isArray(content)) {
350
+ return `${content.length} items`;
351
+ } else if (content && typeof content === 'object') {
352
+ const keys = Object.keys(content);
353
+ return `${keys.length} props`;
354
+ }
355
+
356
+ return '';
357
+ }
358
+
359
+ /**
360
+ * Truncate text safely
361
+ * @param {string} text - Text to truncate
362
+ * @param {number} maxLength - Maximum length
363
+ * @returns {string} Truncated text
364
+ */
365
+ truncateText(text, maxLength) {
366
+ if (!text || text.length <= maxLength) return text;
367
+ return text.substring(0, maxLength - 3) + '...';
368
+ }
369
+
370
+ /**
371
+ * Escape HTML to prevent XSS
372
+ * @param {string} text - Text to escape
373
+ * @returns {string} Escaped text
374
+ */
375
+ escapeHtml(text) {
376
+ if (typeof text !== 'string') return String(text);
377
+
378
+ const div = document.createElement('div');
379
+ div.textContent = text;
380
+ return div.innerHTML;
381
+ }
382
+
383
+ /**
384
+ * Find tool result in globally stored messages
385
+ * @param {string} toolUseId - Tool use ID to find result for
386
+ * @returns {string|null} Tool result content if found
387
+ */
388
+ findToolResultInGlobalMessages(toolUseId) {
389
+ // Try to find tool result in cached messages
390
+ try {
391
+ if (typeof window !== 'undefined' && window.currentMessages) {
392
+
393
+ // First pass: Look for direct tool_result matches in any message
394
+ for (let i = 0; i < window.currentMessages.length; i++) {
395
+ const message = window.currentMessages[i];
396
+
397
+ if (Array.isArray(message.content)) {
398
+ for (let j = 0; j < message.content.length; j++) {
399
+ const block = message.content[j];
400
+
401
+ if (block.type === 'tool_result' && block.tool_use_id === toolUseId) {
402
+ return block.content;
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ // Second pass: Sequential search - look for tool_use then find matching tool_result
409
+ let foundToolUse = false;
410
+ let toolUseIndex = -1;
411
+
412
+ for (let i = 0; i < window.currentMessages.length; i++) {
413
+ const message = window.currentMessages[i];
414
+
415
+ if (Array.isArray(message.content)) {
416
+ for (const block of message.content) {
417
+ if (block.type === 'tool_use' && block.id === toolUseId) {
418
+ foundToolUse = true;
419
+ toolUseIndex = i;
420
+ break;
421
+ }
422
+ }
423
+ }
424
+
425
+ // If we found the tool_use, look for the result in subsequent messages
426
+ if (foundToolUse) {
427
+ for (let j = toolUseIndex; j < window.currentMessages.length; j++) {
428
+ const laterMessage = window.currentMessages[j];
429
+ if (Array.isArray(laterMessage.content)) {
430
+ for (const laterBlock of laterMessage.content) {
431
+ if (laterBlock.type === 'tool_result' && laterBlock.tool_use_id === toolUseId) {
432
+ return laterBlock.content;
433
+ }
434
+ }
435
+ }
436
+ }
437
+ break; // Stop after finding tool_use and searching subsequent messages
438
+ }
439
+ }
440
+ }
441
+ } catch (error) {
442
+ // Silently handle errors in tool result search
443
+ }
444
+
445
+ return null;
446
+ }
447
+
448
+ /**
449
+ * Generate comprehensive tool content for modal display
450
+ * @param {string} toolName - Name of the tool
451
+ * @param {Object} toolBlock - Tool use block with input parameters
452
+ * @param {string|null} resultContent - Tool result content if available
453
+ * @returns {string} Comprehensive tool information for modal
454
+ */
455
+ generateComprehensiveToolContent(toolName, toolBlock, resultContent) {
456
+ let content = `=== TOOL: ${toolName} ===\n\n`;
457
+
458
+ // Tool ID and basic info
459
+ content += `Tool ID: ${toolBlock.id || 'Unknown'}\n`;
460
+ content += `Short ID: ${toolBlock.id ? toolBlock.id.slice(-8) : 'Unknown'}\n\n`;
461
+
462
+ // Tool Input Parameters
463
+ content += `--- INPUT PARAMETERS ---\n`;
464
+ if (toolBlock.input && typeof toolBlock.input === 'object') {
465
+ Object.entries(toolBlock.input).forEach(([key, value]) => {
466
+ if (typeof value === 'string') {
467
+ // For long strings, show preview + length
468
+ if (value.length > 200) {
469
+ content += `${key}: "${value.substring(0, 200)}..." [${value.length} characters total]\n`;
470
+ } else {
471
+ content += `${key}: "${value}"\n`;
472
+ }
473
+ } else {
474
+ content += `${key}: ${JSON.stringify(value, null, 2)}\n`;
475
+ }
476
+ });
477
+ } else {
478
+ content += `No input parameters provided.\n`;
479
+ }
480
+
481
+ // Tool-specific details
482
+ content += `\n--- TOOL DETAILS ---\n`;
483
+ switch (toolName) {
484
+ case 'Bash':
485
+ content += `Command executed: ${toolBlock.input?.command || 'Unknown'}\n`;
486
+ content += `Description: ${toolBlock.input?.description || 'No description provided'}\n`;
487
+ content += `Timeout: ${toolBlock.input?.timeout || 'Default (120s)'}\n`;
488
+ break;
489
+ case 'Read':
490
+ content += `File path: ${toolBlock.input?.file_path || 'Unknown'}\n`;
491
+ content += `Offset: ${toolBlock.input?.offset || 'Start of file'}\n`;
492
+ content += `Limit: ${toolBlock.input?.limit || 'Entire file'}\n`;
493
+ break;
494
+ case 'Write':
495
+ content += `File path: ${toolBlock.input?.file_path || 'Unknown'}\n`;
496
+ const contentLength = toolBlock.input?.content ? toolBlock.input.content.length : 0;
497
+ content += `Content length: ${contentLength} characters\n`;
498
+ break;
499
+ case 'Edit':
500
+ content += `File path: ${toolBlock.input?.file_path || 'Unknown'}\n`;
501
+ content += `Replace all: ${toolBlock.input?.replace_all ? 'Yes' : 'No'}\n`;
502
+ const oldLength = toolBlock.input?.old_string ? toolBlock.input.old_string.length : 0;
503
+ const newLength = toolBlock.input?.new_string ? toolBlock.input.new_string.length : 0;
504
+ content += `Old string length: ${oldLength} characters\n`;
505
+ content += `New string length: ${newLength} characters\n`;
506
+ break;
507
+ case 'Glob':
508
+ content += `Pattern: ${toolBlock.input?.pattern || 'Unknown'}\n`;
509
+ content += `Search path: ${toolBlock.input?.path || 'Current directory'}\n`;
510
+ break;
511
+ case 'Grep':
512
+ content += `Pattern: ${toolBlock.input?.pattern || 'Unknown'}\n`;
513
+ content += `Include filter: ${toolBlock.input?.include || 'All files'}\n`;
514
+ content += `Search path: ${toolBlock.input?.path || 'Current directory'}\n`;
515
+ break;
516
+ case 'TodoWrite':
517
+ const todoCount = Array.isArray(toolBlock.input?.todos) ? toolBlock.input.todos.length : 0;
518
+ content += `Number of todos: ${todoCount}\n`;
519
+ break;
520
+ default:
521
+ content += `Tool-specific details not available for ${toolName}\n`;
522
+ }
523
+
524
+ return content;
525
+ }
526
+
527
+ /**
528
+ * Bind events for tool displays (simplified for terminal style)
529
+ * @param {Element} _container - Container element (unused in terminal style)
530
+ */
531
+ bindEvents(_container) {
532
+ // No expand/collapse needed for terminal style - everything is compact
533
+ }
534
+ }
535
+
536
+ // Export for module use
537
+ if (typeof module !== 'undefined' && module.exports) {
538
+ module.exports = ToolDisplay;
539
+ }