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