webtap-tool 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

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.

Potentially problematic release.


This version of webtap-tool might be problematic. Click here for more details.

@@ -173,7 +173,7 @@ def command(state, param: int = 0)
173
173
  @app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "text/markdown"})
174
174
  def pages(state) -> dict:
175
175
  """List available pages."""
176
- return build_table_response(
176
+ return table_response(
177
177
  title="Chrome Pages",
178
178
  headers=["Index", "Title", "URL"],
179
179
  rows=rows,
@@ -187,7 +187,7 @@ def pages(state) -> dict:
187
187
  def navigate(state, url: str) -> dict:
188
188
  """Navigate to URL."""
189
189
  # Perform action
190
- return build_info_response(
190
+ return info_response(
191
191
  title="Navigation Complete",
192
192
  fields={"URL": url, "Status": "Success"}
193
193
  )
@@ -195,37 +195,75 @@ def navigate(state, url: str) -> dict:
195
195
 
196
196
  ## Error Handling
197
197
 
198
- Always use the error utilities from `_errors.py`:
198
+ Always use the error utilities from `_builders.py`:
199
199
 
200
200
  ```python
201
- from webtap.commands._errors import check_connection, error_response
201
+ from webtap.commands._builders import check_connection, error_response
202
202
 
203
203
  def my_command(state, ...):
204
204
  # Check connection first for commands that need it
205
205
  if error := check_connection(state):
206
206
  return error
207
-
207
+
208
208
  # Validate parameters
209
209
  if not valid:
210
- return error_response("invalid_param", "Parameter X must be Y")
211
-
212
- # Custom errors
213
- return error_response("custom", custom_message="Specific error message")
210
+ return error_response("Parameter X must be Y", suggestions=["Try this", "Or that"])
211
+
212
+ # Simple errors
213
+ return error_response("Specific error message")
214
214
  ```
215
215
 
216
- ## Utility Functions
216
+ ## Response Builders
217
217
 
218
- Use helpers from `_utils.py`:
218
+ Use builders from `_builders.py`:
219
219
 
220
220
  ```python
221
- from webtap.commands._utils import (
222
- build_table_response, # For tables
223
- build_info_response, # For key-value info
224
- parse_options, # Parse dict with defaults
225
- extract_option, # Extract single option
226
- truncate_string, # Truncate long strings
227
- format_size, # Format byte sizes
228
- format_id, # Format IDs
221
+ from webtap.commands._builders import (
222
+ # Table responses
223
+ table_response, # Tables with headers, warnings, tips
224
+
225
+ # Info displays
226
+ info_response, # Key-value pairs with optional heading
227
+
228
+ # Status responses
229
+ error_response, # Errors with suggestions
230
+ success_response, # Success messages with details
231
+ warning_response, # Warnings with suggestions
232
+
233
+ # Code results
234
+ code_result_response, # Code execution with result display
235
+ code_response, # Simple code block display
236
+
237
+ # Connection helpers
238
+ check_connection, # Helper for CDP connection validation
239
+ check_fetch_enabled, # Helper for fetch interception validation
240
+ )
241
+ ```
242
+
243
+ ### Usage Examples
244
+
245
+ ```python
246
+ # Table with tips
247
+ return table_response(
248
+ title="Network Requests",
249
+ headers=["ID", "URL", "Status"],
250
+ rows=rows,
251
+ summary=f"{len(rows)} requests",
252
+ tips=["Use body(ID) to fetch response body"]
253
+ )
254
+
255
+ # Code execution result
256
+ return code_result_response(
257
+ "JavaScript Result",
258
+ code="2 + 2",
259
+ language="javascript",
260
+ result=4
261
+ )
262
+
263
+ # Error with suggestions
264
+ return error_response(
265
+ "Not connected to Chrome",
266
+ suggestions=["Run pages()", "Use connect(0)"]
229
267
  )
230
268
  ```
231
269
 
@@ -300,15 +338,63 @@ Use explicit text instead of symbols for clarity:
300
338
  4. **MCP mode**: Test with `webtap --mcp` command
301
339
  5. **Markdown rendering**: Verify output displays correctly
302
340
 
341
+ ## TIPS.md Integration
342
+
343
+ All commands should be documented in `TIPS.md` for consistent help and MCP descriptions:
344
+
345
+ ```python
346
+ from webtap.commands._tips import get_tips, get_mcp_description
347
+
348
+ # Get MCP description (for fastmcp metadata)
349
+ mcp_desc = get_mcp_description("mycommand")
350
+
351
+ @app.command(
352
+ display="markdown",
353
+ fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"}
354
+ )
355
+ def mycommand(state, param: str) -> dict:
356
+ """My command description."""
357
+ # ... implementation ...
358
+
359
+ # Include tips in response
360
+ tips = get_tips("mycommand", context={"id": some_id})
361
+ return table_response(
362
+ headers=headers,
363
+ rows=rows,
364
+ tips=tips # Automatically shown as "Next Steps"
365
+ )
366
+ ```
367
+
368
+ ### Adding to TIPS.md
369
+
370
+ Add a section for your command:
371
+
372
+ ```markdown
373
+ ### mycommand
374
+ Brief description of what the command does.
375
+
376
+ #### Examples
377
+ \```python
378
+ mycommand("param1") # Basic usage
379
+ mycommand("param2", flag=True) # With options
380
+ \```
381
+
382
+ #### Tips
383
+ - **Related command:** `other_command()` - does related thing
384
+ - **Advanced usage:** Use with `yet_another()` for X
385
+ - **Context aware:** Tips support {id} placeholders from context dict
386
+ ```
387
+
303
388
  ## Checklist for New Commands
304
389
 
305
390
  - [ ] Use `@app.command()` decorator with `display="markdown"`
306
391
  - [ ] Add `fastmcp` metadata (type: "resource" or "tool")
307
392
  - [ ] Use simple types only (no unions, no Optional)
308
393
  - [ ] Add `# pyright: ignore[reportArgumentType]` for `dict = None`
309
- - [ ] Import utilities from `_utils.py` and `_errors.py`
310
- - [ ] Use `build_table_response()` or `build_info_response()`
394
+ - [ ] Import builders from `_builders.py`
395
+ - [ ] Use `table_response()`, `info_response()`, or `code_result_response()`
311
396
  - [ ] Check connection with `check_connection()` if needed
397
+ - [ ] Add command section to `TIPS.md` with examples and tips
398
+ - [ ] Use `get_tips()` to show tips in response
312
399
  - [ ] Document parameters clearly in docstring
313
- - [ ] Provide usage examples in docstring
314
400
  - [ ] Test in both REPL and MCP modes
webtap/commands/TIPS.md CHANGED
@@ -150,4 +150,27 @@ Show paused requests and responses.
150
150
  - **Analyze body:** `body({id})` - fetch and examine response body
151
151
  - **Resume request:** `resume({id})` - continue the request
152
152
  - **Modify request:** `resume({id}, modifications={'url': '...'})`
153
- - **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
153
+ - **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
154
+
155
+ ### selections
156
+ Browser element selections with prompt and analysis.
157
+
158
+ Access selected DOM elements and their properties via Python expressions. Elements are selected using the Chrome extension's selection mode.
159
+
160
+ #### Examples
161
+ ```python
162
+ selections() # View all selections
163
+ selections(expr="data['prompt']") # Get prompt text
164
+ selections(expr="data['selections']['1']") # Get element #1 data
165
+ selections(expr="data['selections']['1']['styles']") # Get styles
166
+ selections(expr="len(data['selections'])") # Count selections
167
+ selections(expr="{k: v['selector'] for k, v in data['selections'].items()}") # All selectors
168
+ ```
169
+
170
+ #### Tips
171
+ - **Extract HTML:** `selections(expr="data['selections']['1']['outerHTML']")` - get element HTML
172
+ - **Get CSS selector:** `selections(expr="data['selections']['1']['selector']")` - unique selector
173
+ - **Use with js():** `js("element.offsetWidth", selection=1)` - integrate with JavaScript execution
174
+ - **Access styles:** `selections(expr="data['selections']['1']['styles']['display']")` - computed CSS
175
+ - **Get attributes:** `selections(expr="data['selections']['1']['preview']")` - tag, id, classes
176
+ - **Inspect in prompts:** Use `@webtap:webtap://selections` resource in Claude Code for AI analysis
@@ -1,4 +1,36 @@
1
- """Response builders using ReplKit2 v0.10.0+ markdown elements."""
1
+ """Response builders using ReplKit2 v0.10.0+ markdown elements.
2
+
3
+ USAGE GUIDELINES:
4
+
5
+ Use builders for:
6
+ ✅ Simple responses (error, info, success, warning)
7
+ ✅ Tables with standard format
8
+ ✅ Code execution results
9
+ ✅ Repeated patterns across commands
10
+
11
+ Use manual building for:
12
+ ❌ Complex multi-section layouts (>20 lines)
13
+ ❌ Conditional sections with deep nesting
14
+ ❌ Custom workflows (wizards, dashboards)
15
+ ❌ Dual-mode resource views with tutorials
16
+
17
+ Examples:
18
+ - network() - Simple table → Use table_response()
19
+ - javascript() - Code result → Use code_result_response()
20
+ - server() - Custom dashboard → Manual OK
21
+ - selections() resource mode - Tutorial layout → Manual OK
22
+
23
+ Available builders:
24
+ - table_response() - Tables with headers, warnings, tips
25
+ - info_response() - Key-value pairs with optional heading
26
+ - error_response() - Errors with suggestions
27
+ - success_response() - Success messages with details
28
+ - warning_response() - Warnings with suggestions
29
+ - check_connection() - Helper for CDP connection validation
30
+ - check_fetch_enabled() - Helper for fetch interception validation
31
+ - code_result_response() - Code execution with result display
32
+ - code_response() - Simple code block display
33
+ """
2
34
 
3
35
  from typing import Any
4
36
 
@@ -125,3 +157,109 @@ def warning_response(message: str, suggestions: list[str] | None = None) -> dict
125
157
  elements.append({"type": "list", "items": suggestions})
126
158
 
127
159
  return {"elements": elements}
160
+
161
+
162
+ # Connection helpers
163
+
164
+
165
+ def check_connection(state):
166
+ """Check CDP connection, return error response if not connected.
167
+
168
+ Returns error_response() if not connected, None otherwise.
169
+ Use pattern: `if error := check_connection(state): return error`
170
+
171
+ Args:
172
+ state: Application state containing CDP session.
173
+
174
+ Returns:
175
+ Error response dict if not connected, None if connected.
176
+ """
177
+ if not (state.cdp and state.cdp.is_connected):
178
+ return error_response(
179
+ "Not connected to Chrome",
180
+ suggestions=[
181
+ "Run `pages()` to see available tabs",
182
+ "Use `connect(0)` to connect to first tab",
183
+ "Or `connect(page_id='...')` for specific tab",
184
+ ],
185
+ )
186
+ return None
187
+
188
+
189
+ def check_fetch_enabled(state):
190
+ """Check fetch interception, return error response if not enabled.
191
+
192
+ Args:
193
+ state: Application state containing fetch service.
194
+
195
+ Returns:
196
+ Error response dict if not enabled, None if enabled.
197
+ """
198
+ if not state.service.fetch.enabled:
199
+ return error_response(
200
+ "Fetch interception not enabled", suggestions=["Enable with `fetch('enable')` to pause requests"]
201
+ )
202
+ return None
203
+
204
+
205
+ # Code result builders
206
+
207
+
208
+ def code_result_response(
209
+ title: str,
210
+ code: str,
211
+ language: str,
212
+ result: Any = None,
213
+ ) -> dict:
214
+ """Build code execution result display.
215
+
216
+ Args:
217
+ title: Result heading (e.g. "JavaScript Result")
218
+ code: Source code executed
219
+ language: Syntax highlighting language
220
+ result: Execution result (supports dict/list/str/None)
221
+
222
+ Returns:
223
+ Markdown response with code and result
224
+ """
225
+ import json
226
+
227
+ elements = [
228
+ {"type": "heading", "content": title, "level": 2},
229
+ {"type": "code_block", "content": code, "language": language},
230
+ ]
231
+
232
+ if result is not None:
233
+ if isinstance(result, (dict, list)):
234
+ elements.append({"type": "code_block", "content": json.dumps(result, indent=2), "language": "json"})
235
+ else:
236
+ elements.append({"type": "text", "content": f"**Result:** `{result}`"})
237
+ else:
238
+ elements.append({"type": "text", "content": "**Result:** _(no return value)_"})
239
+
240
+ return {"elements": elements}
241
+
242
+
243
+ def code_response(
244
+ content: str,
245
+ language: str = "",
246
+ title: str | None = None,
247
+ ) -> dict:
248
+ """Build simple code block response.
249
+
250
+ Args:
251
+ content: Code content to display
252
+ language: Syntax highlighting language
253
+ title: Optional heading above code block
254
+
255
+ Returns:
256
+ Markdown response with code block
257
+ """
258
+ elements = []
259
+
260
+ if title:
261
+ elements.append({"type": "heading", "content": title, "level": 2})
262
+
263
+ elements.append({"type": "code_block", "content": content, "language": language})
264
+
265
+ return {"elements": elements}
webtap/commands/body.py CHANGED
@@ -3,8 +3,7 @@
3
3
  import json
4
4
  from webtap.app import app
5
5
  from webtap.commands._utils import evaluate_expression, format_expression_result
6
- from webtap.commands._errors import check_connection
7
- from webtap.commands._builders import info_response, error_response
6
+ from webtap.commands._builders import check_connection, info_response, error_response
8
7
  from webtap.commands._tips import get_mcp_description
9
8
 
10
9
 
@@ -1,8 +1,7 @@
1
1
  """Chrome browser connection management commands."""
2
2
 
3
3
  from webtap.app import app
4
- from webtap.commands._errors import check_connection
5
- from webtap.commands._builders import info_response, table_response, error_response
4
+ from webtap.commands._builders import check_connection, info_response, table_response, error_response
6
5
 
7
6
 
8
7
  @app.command(display="markdown", fastmcp={"type": "tool"})
@@ -1,8 +1,7 @@
1
1
  """Browser console message monitoring and display commands."""
2
2
 
3
3
  from webtap.app import app
4
- from webtap.commands._builders import table_response
5
- from webtap.commands._errors import check_connection
4
+ from webtap.commands._builders import check_connection, table_response
6
5
  from webtap.commands._tips import get_tips
7
6
 
8
7
 
webtap/commands/events.py CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  from webtap.app import app
4
4
  from webtap.cdp import build_query
5
- from webtap.commands._errors import check_connection
6
- from webtap.commands._builders import table_response
5
+ from webtap.commands._builders import check_connection, table_response
7
6
  from webtap.commands._tips import get_tips, get_mcp_description
8
7
 
9
8
 
webtap/commands/fetch.py CHANGED
@@ -1,8 +1,7 @@
1
1
  """HTTP fetch request interception and debugging commands."""
2
2
 
3
3
  from webtap.app import app
4
- from webtap.commands._errors import check_connection
5
- from webtap.commands._builders import error_response, info_response, table_response
4
+ from webtap.commands._builders import check_connection, error_response, info_response, table_response
6
5
  from webtap.commands._tips import get_tips
7
6
 
8
7
 
@@ -7,46 +7,19 @@ from webtap.commands._builders import info_response, error_response
7
7
  @app.command(display="markdown", fastmcp={"type": "tool"})
8
8
  def filters(state, action: str = "list", config: dict = None) -> dict: # pyright: ignore[reportArgumentType]
9
9
  """
10
- Manage network request filters.
11
-
12
- Filters are managed by the service and can be persisted to .webtap/filters.json.
13
-
14
- Args:
15
- action: Filter operation
16
- - "list" - List all filter categories (default)
17
- - "show" - Show specific category details
18
- - "add" - Add patterns to category
19
- - "remove" - Remove patterns from category
20
- - "update" - Update entire category
21
- - "delete" - Delete entire category
22
- - "enable" - Enable category
23
- - "disable" - Disable category
24
- - "save" - Save filters to disk
25
- - "load" - Load filters from disk
26
- config: Action-specific configuration
27
- - For show: {"category": "ads"}
28
- - For add: {"category": "ads", "patterns": ["*ad*"], "type": "domain"}
29
- - For remove: {"patterns": ["*ad*"], "type": "domain"}
30
- - For update: {"category": "ads", "domains": [...], "types": [...]}
31
- - For delete/enable/disable: {"category": "ads"}
32
-
33
- Examples:
34
- filters() # List all categories
35
- filters("list") # Same as above
36
- filters("show", {"category": "ads"}) # Show ads category details
37
- filters("add", {"category": "ads",
38
- "patterns": ["*doubleclick*"]}) # Add domain pattern
39
- filters("add", {"category": "tracking",
40
- "patterns": ["Ping"], "type": "type"}) # Add type pattern
41
- filters("remove", {"patterns": ["*doubleclick*"]}) # Remove pattern
42
- filters("update", {"category": "ads",
43
- "domains": ["*google*", "*facebook*"]}) # Replace patterns
44
- filters("delete", {"category": "ads"}) # Delete category
45
- filters("save") # Persist to disk
46
- filters("load") # Load from disk
47
-
48
- Returns:
49
- Current filter configuration or operation result
10
+ Manage network request filters with include/exclude modes.
11
+
12
+ CDP Types: Document, XHR, Fetch, Image, Stylesheet, Script, Font, Media, Other, Ping
13
+ Domains filter URLs, types filter Chrome's resource loading mechanism.
14
+
15
+ Actions:
16
+ list: Show all categories (default)
17
+ show: Display category - config: {"category": "api"}
18
+ add: Add patterns - config: {"category": "api", "patterns": ["*api*"], "type": "domain", "mode": "include"}
19
+ remove: Remove patterns - config: {"patterns": ["*api*"], "type": "domain"}
20
+ update: Replace category - config: {"category": "api", "mode": "include", "domains": [...], "types": ["XHR", "Fetch"]}
21
+ delete/enable/disable: Manage category - config: {"category": "api"}
22
+ save/load: Persist to/from disk
50
23
  """
51
24
  fm = state.service.filters
52
25
  cfg = config or {}
@@ -54,14 +27,35 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
54
27
  # Handle load operation
55
28
  if action == "load":
56
29
  if fm.load():
57
- # Convert display info to markdown
58
- display_info = fm.get_display_info()
59
- return {
60
- "elements": [
61
- {"type": "heading", "content": "Filters Loaded", "level": 2},
62
- {"type": "code_block", "content": display_info, "language": ""},
63
- ]
64
- }
30
+ # Build table data
31
+ categories = fm.get_categories_summary()
32
+ rows = []
33
+ for cat in categories:
34
+ mode = cat["mode"] or "exclude"
35
+ mode_display = "include" if mode == "include" else "exclude"
36
+ if cat["mode"] is None:
37
+ mode_display = "exclude*" # Asterisk indicates missing mode field
38
+
39
+ rows.append(
40
+ {
41
+ "Category": cat["name"],
42
+ "Status": "enabled" if cat["enabled"] else "disabled",
43
+ "Mode": mode_display,
44
+ "Domains": str(cat["domain_count"]),
45
+ "Types": str(cat["type_count"]),
46
+ }
47
+ )
48
+
49
+ elements = [
50
+ {"type": "heading", "content": "Filters Loaded", "level": 2},
51
+ {"type": "text", "content": f"From: `{fm.filter_path}`"},
52
+ {"type": "table", "headers": ["Category", "Status", "Mode", "Domains", "Types"], "rows": rows},
53
+ ]
54
+
55
+ if any(cat["mode"] is None for cat in categories):
56
+ elements.append({"type": "text", "content": "_* Mode not specified, defaulting to exclude_"})
57
+
58
+ return {"elements": elements}
65
59
  else:
66
60
  return error_response(f"No filters found at {fm.filter_path}")
67
61
 
@@ -82,6 +76,7 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
82
76
  category = cfg.get("category", "custom")
83
77
  patterns = cfg.get("patterns", [])
84
78
  pattern_type = cfg.get("type", "domain")
79
+ mode = cfg.get("mode") # Required, no default
85
80
 
86
81
  if not patterns:
87
82
  # Legacy single pattern support
@@ -94,11 +89,14 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
94
89
 
95
90
  added = []
96
91
  failed = []
97
- for pattern in patterns:
98
- if fm.add_pattern(pattern, category, pattern_type):
99
- added.append(pattern)
100
- else:
101
- failed.append(pattern)
92
+ try:
93
+ for pattern in patterns:
94
+ if fm.add_pattern(pattern, category, pattern_type, mode):
95
+ added.append(pattern)
96
+ else:
97
+ failed.append(pattern)
98
+ except ValueError as e:
99
+ return error_response(str(e))
102
100
 
103
101
  if added and not failed:
104
102
  return info_response(
@@ -107,6 +105,7 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
107
105
  "Type": "Domain pattern" if pattern_type == "domain" else "Resource type",
108
106
  "Patterns": ", ".join(added),
109
107
  "Category": category,
108
+ "Mode": mode,
110
109
  },
111
110
  )
112
111
  elif failed:
@@ -149,8 +148,10 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
149
148
  return error_response("'category' required for update action")
150
149
 
151
150
  category = cfg["category"]
152
- fm.update_category(category, domains=cfg.get("domains"), types=cfg.get("types"))
153
- return info_response(title="Category Updated", fields={"Category": category})
151
+ fm.update_category(category, domains=cfg.get("domains"), types=cfg.get("types"), mode=cfg.get("mode"))
152
+ return info_response(
153
+ title="Category Updated", fields={"Category": category, "Mode": cfg.get("mode", "exclude")}
154
+ )
154
155
 
155
156
  # Handle delete operation
156
157
  elif action == "delete":
@@ -193,10 +194,12 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
193
194
  if category in fm.filters:
194
195
  filters = fm.filters[category]
195
196
  enabled = "Enabled" if category in fm.enabled_categories else "Disabled"
197
+ mode = filters.get("mode", "exclude")
196
198
 
197
199
  elements = [
198
200
  {"type": "heading", "content": f"Category: {category}", "level": 2},
199
201
  {"type": "text", "content": f"**Status:** {enabled}"},
202
+ {"type": "text", "content": f"**Mode:** {mode}"},
200
203
  ]
201
204
 
202
205
  if filters.get("domains"):
@@ -212,13 +215,43 @@ def filters(state, action: str = "list", config: dict = None) -> dict: # pyrigh
212
215
 
213
216
  # Default list action: show all filters
214
217
  elif action == "list" or action == "":
215
- display_info = fm.get_display_info()
216
- return {
217
- "elements": [
218
- {"type": "heading", "content": "Filter Configuration", "level": 2},
219
- {"type": "code_block", "content": display_info, "language": ""},
220
- ]
221
- }
218
+ if not fm.filters:
219
+ return {
220
+ "elements": [
221
+ {"type": "heading", "content": "Filter Configuration", "level": 2},
222
+ {"type": "text", "content": f"No filters loaded (would load from `{fm.filter_path}`)"},
223
+ {"type": "text", "content": "Use `filters('load')` to load filters from disk"},
224
+ ]
225
+ }
226
+
227
+ # Build table data
228
+ categories = fm.get_categories_summary()
229
+ rows = []
230
+ for cat in categories:
231
+ mode = cat["mode"] or "exclude"
232
+ mode_display = "include" if mode == "include" else "exclude"
233
+ if cat["mode"] is None:
234
+ mode_display = "exclude*"
235
+
236
+ rows.append(
237
+ {
238
+ "Category": cat["name"],
239
+ "Status": "enabled" if cat["enabled"] else "disabled",
240
+ "Mode": mode_display,
241
+ "Domains": str(cat["domain_count"]),
242
+ "Types": str(cat["type_count"]),
243
+ }
244
+ )
245
+
246
+ elements = [
247
+ {"type": "heading", "content": "Filter Configuration", "level": 2},
248
+ {"type": "table", "headers": ["Category", "Status", "Mode", "Domains", "Types"], "rows": rows},
249
+ ]
250
+
251
+ if any(cat["mode"] is None for cat in categories):
252
+ elements.append({"type": "text", "content": "_* Mode not specified, defaulting to exclude_"})
253
+
254
+ return {"elements": elements}
222
255
 
223
256
  else:
224
257
  return error_response(f"Unknown action: {action}")
@@ -4,8 +4,7 @@ import json
4
4
 
5
5
  from webtap.app import app
6
6
  from webtap.commands._utils import evaluate_expression, format_expression_result
7
- from webtap.commands._errors import check_connection
8
- from webtap.commands._builders import error_response
7
+ from webtap.commands._builders import check_connection, error_response
9
8
  from webtap.commands._tips import get_mcp_description
10
9
 
11
10