bone-agent 1.4.0 → 2.0.1

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.
Files changed (126) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -201
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -144
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -50
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/skills.md +0 -3
  25. package/prompts/main/targeted_searching.md +0 -10
  26. package/prompts/main/task_lists_pattern.md +0 -8
  27. package/prompts/main/temp_folder.md +0 -9
  28. package/prompts/main/think_before_acting.md +0 -10
  29. package/prompts/main/tone_and_style.md +0 -4
  30. package/prompts/main/tool_preferences.md +0 -24
  31. package/prompts/main/trust_subagent_context.md +0 -21
  32. package/prompts/main/when_to_use_sub_agent.md +0 -7
  33. package/prompts/micro/ask_questions.md +0 -1
  34. package/prompts/micro/batch_independent_calls.md +0 -1
  35. package/prompts/micro/casual_interactions.md +0 -1
  36. package/prompts/micro/code_references.md +0 -1
  37. package/prompts/micro/communication_style.md +0 -1
  38. package/prompts/micro/context_reliability.md +0 -1
  39. package/prompts/micro/conversational_tool_calling.md +0 -1
  40. package/prompts/micro/editing_pattern.md +0 -1
  41. package/prompts/micro/error_handling.md +0 -1
  42. package/prompts/micro/exploration_pattern.md +0 -1
  43. package/prompts/micro/intro.md +0 -1
  44. package/prompts/micro/obsidian.md +0 -4
  45. package/prompts/micro/obsidian_project.md +0 -5
  46. package/prompts/micro/professional_objectivity.md +0 -1
  47. package/prompts/micro/skills.md +0 -1
  48. package/prompts/micro/targeted_searching.md +0 -1
  49. package/prompts/micro/task_lists_pattern.md +0 -1
  50. package/prompts/micro/temp_folder.md +0 -1
  51. package/prompts/micro/think_before_acting.md +0 -5
  52. package/prompts/micro/tone_and_style.md +0 -1
  53. package/prompts/micro/tool_preferences.md +0 -1
  54. package/prompts/micro/trust_subagent_context.md +0 -1
  55. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  56. package/requirements.txt +0 -9
  57. package/src/__init__.py +0 -11
  58. package/src/core/__init__.py +0 -1
  59. package/src/core/agentic.py +0 -1085
  60. package/src/core/chat_manager.py +0 -1577
  61. package/src/core/config_manager.py +0 -260
  62. package/src/core/cron.py +0 -578
  63. package/src/core/cron_allowlist.py +0 -118
  64. package/src/core/memory.py +0 -145
  65. package/src/core/metadata.py +0 -75
  66. package/src/core/retry.py +0 -71
  67. package/src/core/skills.py +0 -463
  68. package/src/core/sub_agent.py +0 -376
  69. package/src/core/tool_approval.py +0 -220
  70. package/src/core/tool_feedback.py +0 -789
  71. package/src/exceptions.py +0 -79
  72. package/src/llm/__init__.py +0 -1
  73. package/src/llm/client.py +0 -176
  74. package/src/llm/codex_provider.py +0 -350
  75. package/src/llm/config.py +0 -536
  76. package/src/llm/prompts.py +0 -494
  77. package/src/llm/providers.py +0 -438
  78. package/src/llm/streaming.py +0 -163
  79. package/src/llm/token_tracker.py +0 -399
  80. package/src/tools/__init__.py +0 -151
  81. package/src/tools/constants.py +0 -59
  82. package/src/tools/create_file.py +0 -136
  83. package/src/tools/directory.py +0 -389
  84. package/src/tools/edit.py +0 -549
  85. package/src/tools/file_reader.py +0 -322
  86. package/src/tools/helpers/__init__.py +0 -99
  87. package/src/tools/helpers/base.py +0 -599
  88. package/src/tools/helpers/converters.py +0 -44
  89. package/src/tools/helpers/file_helpers.py +0 -189
  90. package/src/tools/helpers/formatters.py +0 -411
  91. package/src/tools/helpers/loader.py +0 -145
  92. package/src/tools/helpers/parallel_executor.py +0 -231
  93. package/src/tools/helpers/path_resolver.py +0 -283
  94. package/src/tools/helpers/plugin_manifest.py +0 -185
  95. package/src/tools/obsidian.py +0 -96
  96. package/src/tools/review_sub_agent.py +0 -190
  97. package/src/tools/rg_search.py +0 -477
  98. package/src/tools/search_plugins.py +0 -177
  99. package/src/tools/select_option.py +0 -600
  100. package/src/tools/shell.py +0 -302
  101. package/src/tools/sub_agent.py +0 -139
  102. package/src/tools/task_list.py +0 -269
  103. package/src/tools/web_search.py +0 -61
  104. package/src/ui/__init__.py +0 -1
  105. package/src/ui/banner.py +0 -87
  106. package/src/ui/commands.py +0 -3131
  107. package/src/ui/displays.py +0 -239
  108. package/src/ui/loader.py +0 -284
  109. package/src/ui/main.py +0 -643
  110. package/src/ui/prompt_utils.py +0 -113
  111. package/src/ui/setting_selector.py +0 -590
  112. package/src/ui/setup_wizard.py +0 -294
  113. package/src/ui/sub_agent_panel.py +0 -234
  114. package/src/ui/tool_confirmation.py +0 -226
  115. package/src/utils/__init__.py +0 -1
  116. package/src/utils/citation_parser.py +0 -199
  117. package/src/utils/editor.py +0 -207
  118. package/src/utils/gitignore_filter.py +0 -149
  119. package/src/utils/logger.py +0 -254
  120. package/src/utils/paths.py +0 -30
  121. package/src/utils/result_parsers.py +0 -108
  122. package/src/utils/safe_commands.py +0 -243
  123. package/src/utils/settings.py +0 -195
  124. package/src/utils/user_message_logger.py +0 -120
  125. package/src/utils/validation.py +0 -201
  126. package/src/utils/web_search.py +0 -173
@@ -1,789 +0,0 @@
1
- """Tool result display functions for the agentic loop."""
2
-
3
- import re
4
- import textwrap
5
- from pathlib import Path
6
- from typing import Optional
7
-
8
- from rich.syntax import Syntax
9
-
10
- from utils.settings import MAX_COMMAND_OUTPUT_LINES, MonokaiDarkBGStyle
11
- from utils.result_parsers import extract_exit_code, extract_all_metadata, extract_multiple_metadata
12
- from tools.search_plugins import HEADER_MATCHES, HEADER_ALL
13
- from tools.task_list import _format_task_list, _strip_rich_markup
14
-
15
-
16
- # ---------------------------------------------------------------------------
17
- # Helpers
18
- # ---------------------------------------------------------------------------
19
-
20
- def vault_root_str() -> Optional[str]:
21
- """Return vault_root from the active VaultSession, or None."""
22
- try:
23
- from tools.obsidian import get_vault_session
24
- session = get_vault_session()
25
- return str(session.vault_root) if session else None
26
- except Exception:
27
- return None
28
-
29
-
30
- def _print_or_append(text, console, panel_updater, markup=True):
31
- """Print text to console or append to panel_updater.
32
-
33
- Args:
34
- text: Text to display
35
- console: Rich console
36
- panel_updater: Optional SubAgentPanel for live updates
37
- markup: If True, parse Rich markup (only used for console)
38
- """
39
- if panel_updater:
40
- panel_updater.append(text)
41
- else:
42
- console.print(text, markup=markup)
43
-
44
-
45
- # ---------------------------------------------------------------------------
46
- # Constants
47
- # ---------------------------------------------------------------------------
48
-
49
- # File extension to Pygments lexer name mapping for syntax highlighting
50
- LEXER_MAP = {
51
- 'py': 'python',
52
- 'js': 'javascript',
53
- 'ts': 'typescript',
54
- 'tsx': 'typescript',
55
- 'jsx': 'javascript',
56
- 'go': 'go',
57
- 'rs': 'rust',
58
- 'java': 'java',
59
- 'c': 'c',
60
- 'cpp': 'cpp',
61
- 'h': 'c',
62
- 'hpp': 'cpp',
63
- 'sh': 'bash',
64
- 'bash': 'bash',
65
- 'zsh': 'bash',
66
- 'yaml': 'yaml',
67
- 'yml': 'yaml',
68
- 'json': 'json',
69
- 'toml': 'toml',
70
- 'md': 'markdown',
71
- 'html': 'html',
72
- 'css': 'css',
73
- 'sql': 'sql',
74
- 'php': 'php',
75
- 'rb': 'ruby',
76
- 'swift': 'swift',
77
- 'kt': 'kotlin',
78
- 'scala': 'scala',
79
- 'lua': 'lua',
80
- 'r': 'r',
81
- }
82
-
83
-
84
- # ---------------------------------------------------------------------------
85
- # Label builders
86
- # ---------------------------------------------------------------------------
87
-
88
- def strip_leading_task_list_echo(content, task_list, title=None):
89
- """Remove a leading echoed task list from assistant content.
90
-
91
- Some models copy the task list tool output into the final response, which
92
- causes duplicate task list rendering in the CLI.
93
- """
94
- if not content or not isinstance(content, str) or not task_list:
95
- return content or ""
96
-
97
- expected = _strip_rich_markup(_format_task_list(task_list, title)).strip()
98
- if not expected:
99
- return content
100
-
101
- trimmed = content.lstrip()
102
- if trimmed.startswith(expected):
103
- remainder = trimmed[len(expected):]
104
- return remainder.lstrip("\n").lstrip()
105
-
106
- return content
107
-
108
-
109
- def build_read_file_label(path, start_line=None, max_lines=None, with_colon=False):
110
- """Build uniform read_file label.
111
-
112
- Args:
113
- path: File path
114
- start_line: Optional starting line number (unused in display)
115
- max_lines: Optional max lines to read (unused in display)
116
- with_colon: If True, use 'read_file: path' format (for batch mode labels)
117
-
118
- Returns:
119
- str: Formatted label
120
- """
121
- separator = ': ' if with_colon else ' '
122
- label = f"read_file{separator}{path}"
123
- return label
124
-
125
-
126
- def build_tool_label(function_name, arguments):
127
- """Build tool label with arguments for display.
128
-
129
- Args:
130
- function_name: Name of the tool function
131
- arguments: Dictionary of tool arguments
132
-
133
- Returns:
134
- str: Formatted label with arguments (e.g., "read_file: path/to/file", "rg: search_pattern")
135
- """
136
- if function_name == "rg":
137
- pattern = arguments.get('pattern', '')
138
- # Truncate long patterns for display
139
- return f"rg: {pattern[:40]}" if pattern else "rg"
140
- elif function_name == "read_file":
141
- path = arguments.get('path_str', '')
142
- return build_read_file_label(path, with_colon=True)
143
- elif function_name == "list_directory":
144
- path = arguments.get('path_str', '')
145
- return f"list_directory: {path}"
146
- elif function_name == "create_file":
147
- path = arguments.get('path_str', '')
148
- return f"create_file: {path}"
149
- elif function_name == "edit_file":
150
- path = arguments.get('path', '')
151
- return f"edit_file: {path}"
152
- elif function_name == "web_search":
153
- query = arguments.get('query', '')
154
- return f"web search | {query}"
155
- elif function_name == "execute_command":
156
- command = arguments.get('command', '')
157
- # Truncate long commands for display
158
- return f"execute_command: {command[:80]}" if command else "execute_command"
159
- else:
160
- return function_name
161
-
162
-
163
- # ---------------------------------------------------------------------------
164
- # Tool-specific feedback handlers
165
- # ---------------------------------------------------------------------------
166
-
167
- def handle_create_file_feedback(tool_result, console, panel_updater):
168
- """Handle feedback for create_file tool.
169
-
170
- Display syntax-highlighted file preview.
171
- """
172
- lines = tool_result.split('\n')
173
- # Extract path from metadata
174
- path_match = re.search(r'path=([^\s]+)', tool_result)
175
- path_str = path_match.group(1) if path_match else "file"
176
-
177
- # Find file content section
178
- content_start = None
179
- content_end = None
180
- for i, line in enumerate(lines):
181
- if line.startswith("=== FILE_CONTENT ==="):
182
- content_start = i + 1
183
- elif line.startswith("=== END_FILE_CONTENT ===") and content_start is not None:
184
- content_end = i
185
- break
186
-
187
- # Display summary and syntax-highlighted content
188
- if content_start is not None and content_end is not None:
189
- content_lines = lines[content_start:content_end]
190
- content = "\n".join(content_lines)
191
-
192
- # Get file extension for syntax highlighting
193
- file_ext = Path(path_str).suffix[1:] if Path(path_str).suffix else "text"
194
- lexer_name = LEXER_MAP.get(file_ext.lower(), 'text')
195
-
196
- # Create syntax object
197
- syntax = Syntax(
198
- content,
199
- lexer_name,
200
- theme=MonokaiDarkBGStyle,
201
- line_numbers=True,
202
- word_wrap=False
203
- )
204
-
205
- # Show with prefix for console
206
- if panel_updater:
207
- panel_updater.append(f"Created: {path_str}")
208
- panel_updater.append(str(syntax))
209
- else:
210
- console.print(f"Created: {path_str}", markup=False)
211
- console.print(syntax)
212
- else:
213
- # Fallback: just show path
214
- prefix = "╰─ " if not panel_updater else ""
215
- message = f"{prefix}Created: {path_str}"
216
- _print_or_append(message, console, panel_updater)
217
-
218
- if not panel_updater:
219
- console.print()
220
-
221
-
222
- def handle_list_directory_feedback(tool_result, console, panel_updater):
223
- """Handle feedback for list_directory tool.
224
-
225
- Display formatted directory tree with files and directories.
226
- """
227
- lines = tool_result.split('\n')
228
- # Extract items_count from metadata
229
- items_count = 0
230
- for line in lines:
231
- match = re.search(r'items_count=(\d+)', line)
232
- if match:
233
- items_count = int(match.group(1))
234
- break
235
-
236
- # Parse content lines (skip metadata lines)
237
- content_start = None
238
- for i, line in enumerate(lines):
239
- if line.startswith("FILE") or line.startswith("DIR"):
240
- content_start = i
241
- break
242
-
243
- if content_start is not None and items_count > 0:
244
- content_lines = lines[content_start:]
245
-
246
- # Parse entries: kind, path, line_count, size
247
- # Format: FILE path/to/file.py 123 lines 12345 bytes
248
- # DIR path/to/dir/ 0 lines
249
- entries = []
250
- for line in content_lines:
251
- # Use regex to extract parts - handles paths with spaces
252
- # Pattern: <KIND> <path> <line_count> lines [<size> bytes]
253
- file_match = re.match(r'^(FILE|DIR)\s+(.+?)\s+(\d+)\s+lines(?:\s+(\d+)\s+bytes)?$', line)
254
- if file_match:
255
- kind = file_match.group(1)
256
- path = file_match.group(2).strip()
257
- line_count = file_match.group(3)
258
- size = file_match.group(4) if file_match.group(4) else None
259
-
260
- if kind == "FILE":
261
- entries.append(("FILE", path, size if size else "?"))
262
- else: # DIR
263
- entries.append(("DIR", path))
264
-
265
- # Sort: directories first, then alphabetically
266
- entries.sort(key=lambda x: (0 if x[0] == "DIR" else 1, x[1]))
267
-
268
- # Build tree with truncation (max 10 items)
269
- max_display = 10
270
- display_entries = entries[:max_display]
271
- remaining = max(0, items_count - max_display)
272
-
273
- # Format tree lines
274
- tree_lines = []
275
- for i, entry in enumerate(display_entries):
276
- is_last = (i == len(display_entries) - 1) and (remaining == 0)
277
- # Use closing pipe (└─) for last item, otherwise middle pipe (├─)
278
- connector = "└─" if is_last else "├─"
279
- if entry[0] == "DIR":
280
- tree_lines.append(f" {connector} {entry[1]}")
281
- else: # FILE
282
- size_str = f"{int(entry[2]):,}" if entry[2].isdigit() else entry[2]
283
- tree_lines.append(f" {connector} {entry[1]} ({size_str} bytes)")
284
-
285
- # Add overflow indicator if needed (always use closing pipe)
286
- if remaining > 0:
287
- tree_lines.append(f" └─ ... and {remaining} more")
288
-
289
- # Build output with header
290
- path_match = re.search(r'path=([^\s]+)', tool_result)
291
- path_str = path_match.group(1) if path_match else "directory"
292
- header = f"{path_str}/ ({items_count} item{'s' if items_count != 1 else ''})"
293
-
294
- # Display with prefix
295
- prefix = "╰─ " if not panel_updater else ""
296
- output = f"{prefix}{header}\n"
297
- output += "\n".join(tree_lines)
298
-
299
- _print_or_append(output, console, panel_updater)
300
-
301
- if not panel_updater:
302
- console.print()
303
-
304
-
305
- def handle_search_plugins_feedback(tool_result, console, panel_updater):
306
- """Handle feedback for search_plugins tool.
307
-
308
- Display compact capability results in the same style as other tool output.
309
- """
310
- lines = tool_result.split('\n')
311
- prefix = "╰─ " if not panel_updater else ""
312
-
313
- query = ""
314
- capabilities = []
315
- in_capability_section = False
316
-
317
- i = 0
318
- while i < len(lines):
319
- raw_line = lines[i]
320
- line = raw_line.strip()
321
-
322
- if line.startswith(HEADER_MATCHES):
323
- query = line.replace(HEADER_MATCHES, "", 1)
324
- in_capability_section = True
325
- i += 1
326
- continue
327
-
328
- if line == HEADER_ALL:
329
- query = line
330
- in_capability_section = True
331
- i += 1
332
- continue
333
-
334
- if not in_capability_section:
335
- i += 1
336
- continue
337
-
338
- if line.startswith("Results: ") or line.startswith("Total: ") or not line:
339
- i += 1
340
- continue
341
-
342
- if line.startswith("Activated plugins:") or line.startswith("Loaded skills:") or line.startswith("Load issues:"):
343
- break
344
-
345
- item_match = re.match(r'^- (.+)$', line)
346
- if item_match:
347
- item = {
348
- "name": item_match.group(1),
349
- "type": "",
350
- "status": "",
351
- "summary": "",
352
- "tags": "",
353
- }
354
- i += 1
355
- while i < len(lines):
356
- raw_detail_line = lines[i]
357
- stripped = raw_detail_line.strip()
358
- if not stripped:
359
- i += 1
360
- continue
361
- if re.match(r'^- (.+)$', stripped):
362
- break
363
- if stripped.startswith("Activated plugins:") or stripped.startswith("Loaded skills:") or stripped.startswith("Load issues:"):
364
- break
365
- if stripped.startswith("type: "):
366
- item["type"] = stripped.replace("type: ", "", 1)
367
- elif stripped.startswith("status: "):
368
- item["status"] = stripped.replace("status: ", "", 1)
369
- elif stripped.startswith("summary: "):
370
- item["summary"] = stripped.replace("summary: ", "", 1)
371
- elif stripped.startswith("tags: "):
372
- item["tags"] = stripped.replace("tags: ", "", 1)
373
- i += 1
374
- capabilities.append(item)
375
- continue
376
-
377
- i += 1
378
-
379
- if not capabilities:
380
- msg_lines = [l for l in lines if l.strip() and not l.startswith("exit_code=")]
381
- output = prefix + "\n".join(msg_lines) if msg_lines else f"{prefix}No capability matches"
382
- _print_or_append(output, console, panel_updater)
383
- if not panel_updater:
384
- console.print()
385
- return
386
-
387
- def append_wrapped_detail(line_parts, text, indent, width=72):
388
- wrapped_lines = textwrap.wrap(text, width=width) or [text]
389
- line_parts.extend(f"{indent}[dim]{wrapped}[/dim]" for wrapped in wrapped_lines)
390
-
391
- max_display = 10
392
- display_capabilities = capabilities[:max_display]
393
- remaining = max(0, len(capabilities) - max_display)
394
-
395
- header = f"{query} ({len(capabilities)} match{'es' if len(capabilities) != 1 else ''})" if query else "capabilities"
396
- tree_lines = []
397
-
398
- for idx, capability in enumerate(display_capabilities):
399
- is_last = (idx == len(display_capabilities) - 1) and (remaining == 0)
400
- connector = "└─" if is_last else "├─"
401
-
402
- if capability["type"] == "plugin":
403
- meta = "[dim](plugin)[/dim]"
404
- if capability["status"]:
405
- meta += f" [dim][{capability['status']}][/dim]"
406
- else:
407
- meta = "[dim](skill)[/dim]"
408
-
409
- detail_indent = " │ " if not is_last else " "
410
- line_parts = [f" {connector} {capability['name']} {meta}"]
411
- if capability["summary"]:
412
- append_wrapped_detail(line_parts, capability["summary"], detail_indent)
413
- if capability["tags"]:
414
- append_wrapped_detail(line_parts, f"tags: {capability['tags']}", detail_indent)
415
- tree_lines.append("\n".join(line_parts))
416
-
417
- if remaining > 0:
418
- tree_lines.append(f" └─ ... and {remaining} more")
419
-
420
- output = f"{prefix}{header}\n" + "\n".join(tree_lines)
421
- _print_or_append(output, console, panel_updater)
422
-
423
- if not panel_updater:
424
- console.print()
425
-
426
-
427
- def handle_execute_command_feedback(tool_result, console, panel_updater):
428
- """Handle feedback for execute_command tool.
429
-
430
- Display command output with line truncation and exit code.
431
- """
432
- lines = tool_result.split('\n')
433
- if lines:
434
- # Extract exit code from first line
435
- exit_code = extract_exit_code(tool_result)
436
-
437
- # Get output (all lines after the exit_code line)
438
- output_lines = lines[1:] if exit_code is not None else lines
439
- output_lines = [line for line in output_lines if line.strip()]
440
-
441
- # Truncate if too many lines
442
- truncation_message = None
443
- if len(output_lines) > MAX_COMMAND_OUTPUT_LINES:
444
- displayed_lines = output_lines[:MAX_COMMAND_OUTPUT_LINES]
445
- omitted = len(output_lines) - MAX_COMMAND_OUTPUT_LINES
446
- output = '\n'.join(displayed_lines)
447
- truncation_message = f"[dim]... ({omitted} more lines truncated)[/dim]"
448
- else:
449
- output = '\n'.join(output_lines)
450
-
451
- # Build prefix
452
- prefix = "╰─ " if not panel_updater else ""
453
-
454
- # Show output
455
- if output:
456
- display_text = f"{prefix}{output}"
457
- _print_or_append(display_text, console, panel_updater, markup=False)
458
-
459
- # Show truncation message separately to preserve markup
460
- if truncation_message:
461
- _print_or_append(truncation_message, console, panel_updater)
462
-
463
- # Show exit code if non-zero
464
- if exit_code is not None and exit_code != 0:
465
- exit_text = f"[dim](exit code: {exit_code})[/dim]"
466
- _print_or_append(exit_text, console, panel_updater)
467
-
468
- if not panel_updater:
469
- console.print()
470
-
471
-
472
- # ---------------------------------------------------------------------------
473
- # Main display dispatcher
474
- # ---------------------------------------------------------------------------
475
-
476
- def display_tool_feedback(command, tool_result, console, indent=False, panel_updater=None):
477
- """Display user summary for read_file, rg, and list_directory.
478
-
479
- Args:
480
- command: Tool command string
481
- tool_result: Tool result string
482
- console: Rich console
483
- indent: If True, prefix with '│ ' (for sub-agent mode)
484
- panel_updater: Optional SubAgentPanel for live updates
485
- """
486
- if not tool_result:
487
- return
488
-
489
- # For sub-agent panel: add tool call with formatted message
490
- if panel_updater:
491
- # Extract tool name from command
492
- if command.startswith("read_file"):
493
- tool_name = "read_file"
494
- elif command.startswith("rg"):
495
- tool_name = "rg"
496
- elif command.startswith("list_directory"):
497
- tool_name = "list_directory"
498
- elif command.startswith("search_plugins"):
499
- tool_name = "search_plugins"
500
- elif command.startswith(("create_task_list", "complete_task", "show_task_list")):
501
- tool_name = command.split()[0]
502
- elif command.startswith("web search"):
503
- tool_name = "web_search"
504
- elif command.startswith("execute_command"):
505
- tool_name = "execute_command"
506
- else:
507
- tool_name = command.split()[0]
508
-
509
- # Pass to panel updater which will handle formatting
510
- panel_updater.add_tool_call(tool_name, tool_result, command)
511
-
512
- # For task list tools: show the list (bounded by MAX_TASKS / MAX_TASK_LEN)
513
- if command.startswith(("create_task_list", "complete_task", "show_task_list")):
514
- exit_code = extract_exit_code(tool_result)
515
- if exit_code == 0 or exit_code is None:
516
- # Successful task list - display without exit_code line, with Rich markup parsing.
517
- rendered = tool_result
518
- if rendered.startswith("exit_code="):
519
- rendered = "\n".join(rendered.splitlines()[1:])
520
- _print_or_append(rendered.strip(), console, panel_updater, markup=True)
521
- else:
522
- # Show single-line error if present
523
- first_two = "\n".join(tool_result.splitlines()[:2]).strip()
524
- _print_or_append(first_two or tool_result.strip(), console, panel_updater, markup=False)
525
- if not panel_updater:
526
- console.print()
527
- return
528
-
529
- # For read_file: parse lines_read and start_line from first line
530
- if command.startswith("read_file"):
531
- metadata = extract_multiple_metadata(tool_result, 'lines_read', 'start_line')
532
- count = metadata.get('lines_read')
533
- if count is not None:
534
- # Only add prefix for console, not for panel_updater
535
- prefix = "╰─ " if not panel_updater else ""
536
-
537
- # Build message with line range if start_line is present
538
- start_line = metadata.get('start_line')
539
- if start_line:
540
- if start_line > 1:
541
- end_line = start_line + count - 1
542
- message = f"{prefix}[dim]Read lines {start_line}-{end_line} ({count} line{'s' if count != 1 else ''})[/dim]"
543
- else:
544
- message = f"{prefix}[dim]Read {count} line{'s' if count != 1 else ''}[/dim]"
545
- else:
546
- message = f"{prefix}[dim]Read {count} line{'s' if count != 1 else ''}[/dim]"
547
-
548
- _print_or_append(message, console, panel_updater)
549
- if not panel_updater:
550
- console.print()
551
- return
552
-
553
- # For rg: parse matches/files from result
554
- if command.startswith("rg"):
555
- prefix = "╰─ " if not panel_updater else ""
556
- message = None
557
-
558
- # Check for "No matches found" message (0 results)
559
- lines = tool_result.split('\n')
560
- if any("No matches found" in line for line in lines):
561
- message = f"{prefix}[dim]No matches found[/dim]"
562
- # Check for matches=N or files=N pattern
563
- elif len(lines) > 1:
564
- metadata = extract_all_metadata(tool_result, line_index=1)
565
- count = metadata.get('matches') or metadata.get('files')
566
- if count is not None:
567
- label = 'matches' if 'matches' in metadata else 'files'
568
- if count == 0:
569
- message = f"{prefix}[dim]No {label} found[/dim]"
570
- else:
571
- message = f"{prefix}[dim]Found {count} {label}[/dim]"
572
- # Fallback: if exit_code=1 but no other info, show no matches
573
- elif any("exit_code=1" in line for line in lines):
574
- message = f"{prefix}[dim]No matches found[/dim]"
575
-
576
- if message:
577
- _print_or_append(message, console, panel_updater)
578
- if not panel_updater:
579
- console.print()
580
- return
581
-
582
- # For list_directory: parse and display directory tree
583
- if command.startswith("list_directory"):
584
- handle_list_directory_feedback(tool_result, console, panel_updater)
585
- return
586
-
587
- # For search_plugins: display tree of found plugins
588
- if command.startswith("search_plugins"):
589
- handle_search_plugins_feedback(tool_result, console, panel_updater)
590
- return
591
-
592
- # For create_file: display preview of created file
593
- if command.startswith("create_file"):
594
- handle_create_file_feedback(tool_result, console, panel_updater)
595
- return
596
-
597
- # For execute_command: display command output with line truncation
598
- if command.startswith("execute_command"):
599
- handle_execute_command_feedback(tool_result, console, panel_updater)
600
- return
601
-
602
- # For web_search: display results count and content fetch status
603
- if command.startswith("web search"):
604
- lines = tool_result.split('\n')
605
- if lines:
606
- summary = _parse_web_search_metadata(lines[0])
607
- if summary:
608
- prefix = "╰─ " if not panel_updater else ""
609
- message = f"{prefix}[dim]{summary}[/dim]"
610
- _print_or_append(message, console, panel_updater)
611
- if not panel_updater:
612
- console.print()
613
- return
614
-
615
-
616
- def _parse_web_search_metadata(first_line):
617
- """Parse web search metadata line into a human-readable summary.
618
-
619
- Args:
620
- first_line: The first line of web_search tool result containing metadata.
621
-
622
- Returns:
623
- str: Human-readable summary like "Found 5 results, 3 pages fetched"
624
- """
625
- match = re.search(r'results_found=(\d+)', first_line)
626
- if not match:
627
- return ""
628
-
629
- count = int(match.group(1))
630
- if count == 0:
631
- return "No results found"
632
-
633
- parts = [f"Found {count} result{'s' if count != 1 else ''}"]
634
-
635
- fetched = re.search(r'pages_fetched=(\d+)', first_line)
636
- if fetched:
637
- fc = int(fetched.group(1))
638
- if fc > 0:
639
- parts.append(f"{fc} page{'s' if fc != 1 else ''} fetched")
640
-
641
- failed = re.search(r'pages_failed=(\d+)', first_line)
642
- if failed:
643
- f = int(failed.group(1))
644
- if f > 0:
645
- parts.append(f"{f} failed")
646
-
647
- return ", ".join(parts)
648
-
649
-
650
- # ---------------------------------------------------------------------------
651
- # Panel message builder (used by SubAgentPanel.add_tool_call)
652
- # ---------------------------------------------------------------------------
653
-
654
- def build_panel_tool_message(tool_name, tool_result, command):
655
- """Build a formatted Rich markup message for a tool call in the sub-agent panel.
656
-
657
- This consolidates the formatting logic that was previously duplicated
658
- between SubAgentPanel.add_tool_call and display_tool_feedback.
659
-
660
- Args:
661
- tool_name: Name of the tool (e.g., "read_file", "rg")
662
- tool_result: Tool result string
663
- command: Optional command string for context (e.g., "read_file: path/to/file")
664
-
665
- Returns:
666
- str: Rich markup string for the panel message
667
- """
668
- if tool_result is None:
669
- return f"[grey]{tool_name}[/grey]"
670
-
671
- if tool_name == "read_file":
672
- path = ""
673
- if command:
674
- match = re.search(r'read_file:?\s+(.+)', command)
675
- if match:
676
- path = match.group(1).strip()
677
-
678
- metadata = extract_multiple_metadata(tool_result, 'lines_read', 'start_line')
679
- count = metadata.get('lines_read')
680
-
681
- if count is not None:
682
- start_line = metadata.get('start_line')
683
- suffix = f" ({count} line{'s' if count != 1 else ''})"
684
- if start_line and start_line > 1:
685
- end_line = start_line + count - 1
686
- suffix = f" lines {start_line}-{end_line}{suffix}"
687
- else:
688
- suffix = f" Read{suffix}"
689
- return f"[grey]read_file {path}[/grey]\n[dim]╰─{suffix}[/dim]"
690
- return f"[grey]read_file {path}[/grey]"
691
-
692
- if tool_name == "rg":
693
- pattern = ""
694
- if command:
695
- match = re.search(r'rg:?\s+(.+)', command)
696
- if match:
697
- pattern = match.group(1).strip()
698
-
699
- lines = tool_result.split('\n')
700
- if len(lines) > 1:
701
- metadata = extract_all_metadata(tool_result, line_index=1)
702
- count = metadata.get('matches') or metadata.get('files')
703
- if count is not None:
704
- label = 'matches' if 'matches' in metadata else 'files'
705
- if count == 0:
706
- return f"[grey]rg {pattern}[/grey]\n[dim]╰─ No {label} found[/dim]"
707
- return f"[grey]rg {pattern}[/grey]\n[dim]╰─ Found {count} {label}[/dim]"
708
- elif any("exit_code=1" in line for line in lines):
709
- return f"[grey]rg {pattern}[/grey]\n[dim]╰─ No matches found[/dim]"
710
-
711
- # Fallback: if exit_code=1 but no footer
712
- if tool_result and "exit_code=1" in tool_result:
713
- return f"[grey]rg {pattern}[/grey]\n[dim]╰─ No matches found[/dim]"
714
- return f"[grey]rg {pattern}[/grey]"
715
-
716
- if tool_name == "list_directory":
717
- path = "."
718
- if command:
719
- match = re.search(r'list_directory:?\s+(.+)', command)
720
- if match:
721
- path = match.group(1).strip()
722
-
723
- lines = tool_result.split('\n')
724
- items_count = 0
725
- for line in lines:
726
- match = re.search(r'items_count=(\d+)', line)
727
- if match:
728
- items_count = int(match.group(1))
729
- break
730
-
731
- if items_count > 0:
732
- return f"[grey]list_directory {path}[/grey]\n[dim]╰─ {items_count} item{'s' if items_count != 1 else ''}[/dim]"
733
- return f"[grey]list_directory {path}[/grey]\n[dim]╰─ No items[/dim]"
734
-
735
- if tool_name == "search_plugins":
736
- query = ""
737
- query_match = re.search(r"matching '([^']+)':", tool_result or "")
738
- if query_match:
739
- query = query_match.group(1)
740
-
741
- # Count plugin entries
742
- plugin_count = len(re.findall(r'^- \*\*.*?\*\*', tool_result or "", re.MULTILINE))
743
- if plugin_count > 0:
744
- label = f"plugins matching '{query}'" if query else "plugins"
745
- return f"[grey]search_plugins {query}[/grey]\n[dim]╰─ {plugin_count} {label}[/dim]"
746
- return f"[grey]search_plugins {query}[/grey]\n[dim]╰─ No plugins found[/dim]"
747
-
748
- if tool_name == "web_search":
749
- query = ""
750
- if command:
751
- if "|" in command:
752
- parts = command.split(" | ", 1)
753
- if len(parts) > 1:
754
- query = parts[1]
755
-
756
- lines = tool_result.split('\n')
757
- summary = _parse_web_search_metadata(lines[0]) if lines else ""
758
-
759
- if query:
760
- if summary:
761
- return f"[bold #5F9EA0]web search | {query}[/bold #5F9EA0]\n[dim]╰─ {summary}[/dim]"
762
- return f"[bold #5F9EA0]web search | {query}[/bold #5F9EA0]\n[dim]╰─ Search completed[/dim]"
763
- return f"[bold #5F9EA0]web_search[/bold #5F9EA0]\n[dim]╰─ Search completed[/dim]"
764
-
765
- if tool_name == "execute_command":
766
- cmd_display = ""
767
- if command:
768
- if command.startswith("execute_command"):
769
- parts = command.split(' ', 1)
770
- if len(parts) > 1:
771
- cmd_display = parts[1]
772
- else:
773
- cmd_display = command
774
- if cmd_display:
775
- return f"[grey]{cmd_display}[/grey]\n[dim]╰─ Command executed[/dim]"
776
- return f"[grey]execute_command[/grey]\n[dim]╰─ Command executed[/dim]"
777
-
778
- if tool_name in ("create_task_list", "complete_task", "show_task_list"):
779
- exit_code = extract_exit_code(tool_result)
780
- if exit_code == 0 or exit_code is None:
781
- rendered = tool_result
782
- if rendered.startswith("exit_code="):
783
- rendered = "\n".join(rendered.splitlines()[1:])
784
- return f"[grey]{tool_name}[/grey]\n[dim]╰─ {rendered.strip()}[/dim]"
785
- first_two = "\n".join(tool_result.splitlines()[:2]).strip()
786
- return f"[grey]{tool_name}[/grey]\n[dim]╰─ {first_two or tool_result.strip()}[/dim]"
787
-
788
- return f"[grey]{tool_name}[/grey]"
789
-