webtap-tool 0.4.0__py3-none-any.whl → 0.5.1__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
 
@@ -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
 
@@ -1,9 +1,7 @@
1
1
  """JavaScript code execution in browser context."""
2
2
 
3
- import json
4
3
  from webtap.app import app
5
- from webtap.commands._errors import check_connection
6
- from webtap.commands._builders import info_response, error_response
4
+ from webtap.commands._builders import check_connection, info_response, error_response, code_result_response
7
5
  from webtap.commands._tips import get_mcp_description
8
6
 
9
7
 
@@ -17,35 +15,86 @@ mcp_desc = get_mcp_description("js")
17
15
  },
18
16
  fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"},
19
17
  )
20
- def js(state, code: str, wait_return: bool = True, await_promise: bool = False) -> dict:
21
- """Execute JavaScript in the browser.
18
+ def js(
19
+ state,
20
+ code: str,
21
+ selection: int = None,
22
+ persist: bool = False,
23
+ wait_return: bool = True,
24
+ await_promise: bool = False,
25
+ ) -> dict: # pyright: ignore[reportArgumentType]
26
+ """Execute JavaScript in the browser with optional element selection.
22
27
 
23
28
  Args:
24
- code: JavaScript code to execute
29
+ code: JavaScript code to execute (use 'element' variable if selection provided)
30
+ selection: Browser element selection number (e.g., 1 for #1) - makes element available
31
+ persist: Keep variables in global scope across calls (default: False, uses fresh scope)
25
32
  wait_return: Wait for and return result (default: True)
26
33
  await_promise: Await promises before returning (default: False)
27
34
 
28
35
  Examples:
36
+ # Default: fresh scope (no redeclaration errors)
37
+ js("const x = 1") # ✓ x isolated
38
+ js("const x = 2") # ✓ No error, fresh scope
29
39
  js("document.title") # Get page title
30
- js("document.body.innerText.length") # Get text length
31
- js("console.log('test')", wait_return=False) # Fire and forget
32
40
  js("[...document.links].map(a => a.href)") # Get all links
33
41
 
42
+ # With browser element selection (always fresh scope)
43
+ js("element.offsetWidth", selection=1) # Use element #1
44
+ js("element.classList", selection=2) # Use element #2
45
+ js("element.getBoundingClientRect()", selection=1)
46
+
47
+ # Persistent scope: keep variables across calls
48
+ js("var data = fetch('/api')", persist=True) # data persists
49
+ js("data.json()", persist=True) # Access data from previous call
50
+
34
51
  # Async operations
35
52
  js("fetch('/api').then(r => r.json())", await_promise=True)
36
53
 
37
54
  # DOM manipulation (no return needed)
38
55
  js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
39
56
 
40
- # Install interceptors
41
- js("window.fetch = new Proxy(window.fetch, {get: (t, p) => console.log(p)})", wait_return=False)
42
-
43
57
  Returns:
44
58
  The evaluated result if wait_return=True, otherwise execution status
45
59
  """
46
60
  if error := check_connection(state):
47
61
  return error
48
62
 
63
+ # Handle browser element selection
64
+ if selection is not None:
65
+ # Check if browser data exists
66
+ if not hasattr(state, "browser_data") or not state.browser_data:
67
+ return error_response(
68
+ "No browser selections available",
69
+ suggestions=[
70
+ "Use browser() to select elements first",
71
+ "Or omit the selection parameter to run code directly",
72
+ ],
73
+ )
74
+
75
+ # Get the jsPath for the selected element
76
+ selections = state.browser_data.get("selections", {})
77
+ sel_key = str(selection)
78
+
79
+ if sel_key not in selections:
80
+ available = ", ".join(selections.keys()) if selections else "none"
81
+ return error_response(
82
+ f"Selection #{selection} not found",
83
+ suggestions=[f"Available selections: {available}", "Use browser() to see all selections"],
84
+ )
85
+
86
+ js_path = selections[sel_key].get("jsPath")
87
+ if not js_path:
88
+ return error_response(f"Selection #{selection} has no jsPath")
89
+
90
+ # Wrap code with element variable in fresh scope (IIFE)
91
+ # Selection always uses fresh scope to avoid element redeclaration errors
92
+ code = f"(() => {{ const element = {js_path}; return ({code}); }})()"
93
+ elif not persist:
94
+ # Default: wrap in IIFE for fresh scope (avoids const/let redeclaration errors)
95
+ code = f"(() => {{ return ({code}); }})()"
96
+ # else: persist=True, use code as-is (global scope)
97
+
49
98
  result = state.cdp.execute(
50
99
  "Runtime.evaluate", {"expression": code, "returnByValue": wait_return, "awaitPromise": await_promise}
51
100
  )
@@ -60,23 +109,7 @@ def js(state, code: str, wait_return: bool = True, await_promise: bool = False)
60
109
  # Return based on wait_return flag
61
110
  if wait_return:
62
111
  value = result.get("result", {}).get("value")
63
-
64
- # Format the result in markdown
65
- elements = [
66
- {"type": "heading", "content": "JavaScript Result", "level": 2},
67
- {"type": "code_block", "content": code, "language": "javascript"}, # Full code
68
- ]
69
-
70
- # Add the result
71
- if value is not None:
72
- if isinstance(value, (dict, list)):
73
- elements.append({"type": "code_block", "content": json.dumps(value, indent=2), "language": "json"})
74
- else:
75
- elements.append({"type": "text", "content": f"**Result:** `{value}`"})
76
- else:
77
- elements.append({"type": "text", "content": "**Result:** _(no return value)_"})
78
-
79
- return {"elements": elements}
112
+ return code_result_response("JavaScript Result", code, "javascript", result=value)
80
113
  else:
81
114
  return info_response(
82
115
  title="JavaScript Execution",
@@ -1,8 +1,7 @@
1
1
  """Browser page navigation and history 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"})
@@ -3,7 +3,7 @@
3
3
  from typing import List
4
4
 
5
5
  from webtap.app import app
6
- from webtap.commands._errors import check_connection
6
+ from webtap.commands._builders import check_connection, table_response
7
7
  from webtap.commands._tips import get_tips
8
8
 
9
9
 
@@ -68,40 +68,22 @@ def network(state, limit: int = 20, filters: List[str] = None, no_filters: bool
68
68
  if limit and len(results) == limit:
69
69
  warnings.append(f"Showing first {limit} results (use limit parameter to see more)")
70
70
 
71
- # Get tips from TIPS.md with context
72
- tips = None
73
- if rows:
74
- example_id = rows[0]["ID"]
75
- tips = get_tips("network", context={"id": example_id})
76
-
77
- # Build response elements manually to add filter tip
78
- elements = []
79
- elements.append({"type": "heading", "content": "Network Requests", "level": 2})
80
-
81
- for warning in warnings or []:
82
- elements.append({"type": "alert", "message": warning, "level": "warning"})
71
+ # Get tips from TIPS.md with context, and add filter guidance
72
+ combined_tips = [
73
+ "Reduce noise with `filters()` - filter by type (XHR, Fetch) or domain (*/api/*)",
74
+ ]
83
75
 
84
76
  if rows:
85
- elements.append(
86
- {"type": "table", "headers": ["ID", "ReqID", "Method", "Status", "URL", "Type", "Size"], "rows": rows}
87
- )
88
- else:
89
- elements.append({"type": "text", "content": "_No data available_"})
90
-
91
- if f"{len(rows)} requests":
92
- elements.append({"type": "text", "content": f"_{len(rows)} requests_"})
93
-
94
- # Always show filter tip demonstrating both type AND domain filtering
95
- elements.append(
96
- {
97
- "type": "alert",
98
- "message": "Tip: Reduce noise with filters by type or domain. Example: `filters('update', {'category': 'api-only', 'mode': 'include', 'types': ['XHR', 'Fetch'], 'domains': ['*/api/*', '*/graphql/*']})`",
99
- "level": "info",
100
- }
77
+ example_id = rows[0]["ID"]
78
+ context_tips = get_tips("network", context={"id": example_id})
79
+ if context_tips:
80
+ combined_tips.extend(context_tips)
81
+
82
+ return table_response(
83
+ title="Network Requests",
84
+ headers=["ID", "ReqID", "Method", "Status", "URL", "Type", "Size"],
85
+ rows=rows,
86
+ summary=f"{len(rows)} requests" if rows else None,
87
+ warnings=warnings,
88
+ tips=combined_tips,
101
89
  )
102
-
103
- if tips:
104
- elements.append({"type": "heading", "content": "Next Steps", "level": 3})
105
- elements.append({"type": "list", "items": tips})
106
-
107
- return {"elements": elements}