Reviewed codebase for skill invocation and collapsible elements.
UnderstandConfirmed skill block renders correctly with spacer.
VerifyCommitted and pushed fix for issue #894.
EditClosed GitHub issue with confirmation comment.
CleanupAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/894
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
The user wants me to analyze GitHub issue #894. Let me fetch the issue details first.
>gh issue view 894 --repo badlogic/pi-mono --json title,body,comments,labels,stateThis is a feature request for making skill invocation messages collapsible in the chat output. Let me understand what's being asked:
/skill:name commands, the full skill content is displayedI need to:
Let me start by exploring the codebase to find the relevant files.
Let me explore the codebase to understand how skill invocations are currently handled and how collapsible elements are implemented elsewhere.
>cd /Users/badlogic/workspaces/pi-mono && find packages/coding-agent -type f -name "*.ts" | xargs grep -l -i "skill" | head -20>cd /Users/badlogic/workspaces/pi-mono && grep -r "skill:name\|/skill:" packages/coding-agent/src --include="*.ts" -l>cd /Users/badlogic/workspaces/pi-mono && grep -r "collaps\|expand" packages/coding-agent/src --include="*.ts" -lLet me look at how skill invocations are handled and where they display messages. I need to check agent-session.ts and also look at how collapsible components like branch-summary-message.ts work.
Let me read the relevant files to understand the current skill handling and collapsible components:
Let me continue reading agent-session.ts to see more context and find exactly where skill invocations create messages. I also need to see the _expandSkillCommand method more fully and understand how the skill message is displayed. Let me also look at other collapsible components like tool-execution.ts.
Let me look at more components to understand the pattern and find how skill messages are handled:
Now I understand the structure better. The skill command is expanded in _expandSkillCommand method which prepends a header and returns the full skill content as plain text. This becomes part of the user message content, not a separate message type.
Let me look at how the interactive mode renders user messages to understand the current behavior better:
Let me look at how user messages are rendered in interactive mode:
>cd /Users/badlogic/workspaces/pi-mono && grep -n "skill" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -40Let me continue looking at the interactive mode to see how user messages are rendered, and specifically how skill expansions are shown:
>cd /Users/badlogic/workspaces/pi-mono && grep -n "Skill\|skill" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -60Now I understand the current implementation. The key insight is:
In agent-session.ts, the _expandSkillCommand method (around line 623) expands /skill:name args commands into full text:
The user message is then rendered by UserMessageComponent which just renders markdown text without any collapsible behavior.
The issue is that skill invocations result in a regular user message with the full skill content, pushing important content out of view.
Looking at how other collapsible components work:
BranchSummaryMessageComponent: Has a separate message type (branchSummary) and renders with collapsed/expanded stateToolExecutionComponent: Shows tool output collapsed by default with expansion toggleCustomMessageComponent: Has a separate message type (custom) and supports collapsed/expanded stateFor skills, the most elegant solution would be to create a new message type skillInvocation that stores:
Then create a SkillInvocationMessageComponent that renders it collapsible.
Let me look at the message types and session manager to understand how to add a new message type:
Let me look at the session manager to understand how messages are persisted and the session entry types:
Now I have a clear picture. Let me summarize what I've found:
When a user types /skill:name args, _expandSkillCommand() in agent-session.ts reads the skill file and creates a user message containing:
Skill location: <path>\nReferences are relative to <baseDir>.---This expanded text becomes a regular user message rendered by UserMessageComponent, which just renders markdown without any collapsible behavior.
The issue requests that skill invocations be collapsible, similar to:
BranchSummaryMessageComponent - has [branch] label and collapsed by defaultToolExecutionComponent - shows collapsed output with expand toggleThere are two possible approaches:
skillInvocationSkillInvocationMessage type to messages.tsskill_invocation entry type to session-manager.ts (or use custom_message with customType: "skill")SkillInvocationMessageComponent in interactive mode_expandSkillCommand to return/emit a skill message instead of expanded textThis is a significant change affecting session persistence and message types.
CustomMessage with a special customTypeThe existing CustomMessage system already supports:
customTypeCustomMessageComponentWe could:
In _expandSkillCommand, instead of returning plain text, emit a CustomMessage with customType: "skill"
Store skill metadata in details (name, path, content, args)
Register a custom renderer for the "skill" customType
The renderer shows collapsed "[skill] pdf-tools (press X to expand)" by default
However, _expandSkillCommand runs during text expansion before the prompt is sent and returns text that becomes the user message content, making this approach more complex than it initially appears.
I could detect user messages that are skill invocations by checking if they start with the skill header pattern and render them with SkillInvocationMessageComponent in the UI layer. This is the least invasive approach.
The implementation would involve creating a new collapsible component for skill content, detecting skill invocations in interactive-mode.ts where user messages are rendered, and keeping session persistence unchanged since it remains a regular user message. I'd need to modify three files: a new component file for the collapsible wrapper, the interactive mode file to detect and use the component, and the agent session file to add a constant for the skill header pattern so detection works consistently. Let me check the exact skill message format from _expandSkillCommand to ensure proper detection.
Let me look at the exact format of the skill message header to understand what pattern to detect:
>cd /Users/badlogic/workspaces/pi-mono && grep -A 20 "_expandSkillCommand" packages/coding-agent/src/core/agent-session.ts | head -30>cd /Users/badlogic/workspaces/pi-mono && grep -n "message_start\|UserMessageComponent\|role === \"user\"" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -30Let me look at the full _expandSkillCommand method and see how messages are rendered in interactive mode:
Let me look at the addMessageToChat method to see how user messages are rendered:
>cd /Users/badlogic/workspaces/pi-mono && grep -n "addMessageToChat" packages/coding-agent/src/modes/interactive/interactive-mode.tsLet me read the addMessageToChat method in full:
Now I have a complete picture of the architecture. Let me read the full _expandSkillCommand method to understand the exact format of skill messages:
Let me continue reading to see the full _expandSkillCommand method:
Now I have all the information I need. Let me summarize my analysis:
When a user types /skill:name args:
_expandSkillCommand() in agent-session.ts expands the skill command to:
Skill location: <filePath>References are relative to <baseDir>.
<skill body content>
---
User: <args>This becomes a regular user message stored and displayed via UserMessageComponent
Long skill content pushes important content out of view
The most concise implementation is to:
Add a constant for detecting skill messages in agent-session.ts (export the prefix pattern)
Create a new component SkillInvocationMessageComponent in packages/coding-agent/src/modes/interactive/components/ that:
[skill] skill-name (press X to expand)Modify addMessageToChat in interactive-mode.ts to:
SkillInvocationMessageComponent for skill invocationsUserMessageComponent for regular user messagespackages/coding-agent/src/core/agent-session.ts - Export skill header constantpackages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts - NEW: collapsible skill componentpackages/coding-agent/src/modes/interactive/interactive-mode.ts - Detect and use skill component1. agent-session.ts:
Export SKILL_MESSAGE_PREFIX = "Skill location: " so the UI layer can identify skill messages.
2. skill-invocation-message.ts (new file):
Build this similar to BranchSummaryMessageComponent, parsing the skill name from the file path in the header. Start with expanded state as false, displaying [skill] skill-name (press X to expand) when collapsed, and the full content when expanded.
3. interactive-mode.ts:
In addMessageToChat, check if the message starts with the skill prefix and route to the appropriate component accordingly.
When handling user messages, extract the text content and check if it begins with SKILL_MESSAGE_PREFIX. If it does, instantiate SkillInvocationMessageComponent and apply the current toolOutputExpanded state. Otherwise, use the standard UserMessageComponent. Add the text to history if requested. Maintain a reference to skill components to support the expand/collapse toggle functionality.
When using /skill:name commands, the full skill content is displayed in chat. Long skill documentation pushes important content out of view, requiring scrolling to reach it.
In agent-session.ts, _expandSkillCommand() transforms skill commands into text:
Skill location: /path/to/skill/SKILL.mdReferences are relative to /path/to/skill.
<full skill body content>
---
User: <args>This expanded text becomes a regular user message rendered by UserMessageComponent, which has no collapsible behavior.
Create a UI-level detection of skill messages and render them with a collapsible component, similar to BranchSummaryMessageComponent.
Files to modify:
| File | Change |
|---|---|
packages/coding-agent/src/core/agent-session.ts | Export a constant SKILL_MESSAGE_PREFIX = "Skill location: " for detection |
packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts | NEW - Collapsible component for skill messages |
packages/coding-agent/src/modes/interactive/interactive-mode.ts | Detect skill messages in addMessageToChat() and use new component; track for expand/collapse toggle |
1. New SkillInvocationMessageComponent:
---\n\nUser: delimiter (if present)[skill] skill-name (press X to expand)2. Changes to interactive-mode.ts:
// In addMessageToChat(), case "user":if (textContent.startsWith(SKILL_MESSAGE_PREFIX)) { const component = new SkillInvocationMessageComponent(textContent, ...); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component);} else { const userComponent = new UserMessageComponent(textContent, ...); this.chatContainer.addChild(userComponent);}Also need to:
pendingTools) for expand/collapse togglehandleExpandToolOutputToggle() loop3. Export constant from agent-session.ts:
export const SKILL_MESSAGE_PREFIX = "Skill location: ";New message type: Add skillInvocation role with dedicated session entry type. More invasive, requires schema changes.
Use CustomMessage: Emit skill as CustomMessage with customType: "skill". Requires refactoring _expandSkillCommand to return structured data instead of text.
The UI detection approach is simplest because it keeps session persistence unchanged (still a regular user message) while providing the desired UX improvement.
ok, i think we should change expandSkillCommand so it wraps the skill shit in
followed by the uesr message. that's what gets sent to the llm. in interactive-mode.ts we then check if a user message has
ok, i think we should change expandSkillCommand so it wraps the skill shit in
References are relative to ${skill.baseDir}.... skill.md content ...
followed by the uesr message. that's what gets sent to the llm. we should also have an exported function in agent-session.ts that takes a string and checks if it has a skill block and extracts the structured data in interactive-mode.ts we then check if a user message has . or is there a way to add detail to a usermessage?
The user wants a cleaner approach:
_expandSkillCommand to wrap skill content in XML-like tags:<skill name="skillname" location="path">References are relative to ${skill.baseDir}.
... skill content ...</skill>
user message/args hereExport a utility function from agent-session.ts that parses this structure
In interactive-mode.ts, detect and extract the skill block for collapsible rendering
The user also asks if there's a way to add details to a user message. Let me check the types...
Looking at the code, Message from @mariozechner/pi-ai has:
There's no details field on user messages like there is on toolResult messages. So the XML wrapping approach is the cleanest solution since it:
Let me outline the implementation:
Good idea. The XML wrapper is cleaner and self-describing. Let me check if user messages support details...
>cd /Users/badlogic/workspaces/pi-mono && grep -A 20 "export interface Message" packages/ai/src/types.ts | head -25Let me try a different approach to find the Message type definition.
>cd /Users/badlogic/workspaces/pi-mono && grep -B 2 -A 15 "^export type Message" packages/ai/src/types.tsLet me check the UserMessage type to see if there's a details field.
>cd /Users/badlogic/workspaces/pi-mono && grep -B 2 -A 10 "UserMessage" packages/ai/src/types.ts | head -20So UserMessage has no details field. The XML wrapper approach is the cleanest solution. Here's the plan:
Changes:
agent-session.ts:
_expandSkillCommand to wrap content in <skill name="..." location="..."> tagsparseSkillBlock(text: string) function that returns { skillName, location, baseDir, content, userMessage } | nullinteractive-mode.ts:
addMessageToChat case "user", call parseSkillBlockSkillInvocationMessageComponentUserMessageComponentNew skill-invocation-message.ts:
The XML format will be:
<skill name="pdf-tools" location="/path/to/SKILL.md">References are relative to /path/to/skill/.
... skill markdown content ...</skill>
User args here (if any)No details field on UserMessage. The XML wrapper approach is the right call.
1. agent-session.ts changes:
// New format for skill expansionconst skillMessage = `<skill name="${skill.name}" location="${skill.filePath}">References are relative to ${skill.baseDir}.
${body}</skill>`;
return args ? `${skillMessage}\n\n${args}` : skillMessage;Export a parser:
export interface ParsedSkillBlock { name: string; location: string; content: string; // everything inside <skill>...</skill> userMessage: string | undefined; // text after </skill>}
export function parseSkillBlock(text: string): ParsedSkillBlock | null { const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/); if (!match) return null; return { name: match[1], location: match[2], content: match[3], userMessage: match[4]?.trim() || undefined, };}2. interactive-mode.ts changes:
In addMessageToChat(), case "user":
case "user": { const textContent = this.getUserMessageText(message); if (textContent) { const skillBlock = parseSkillBlock(textContent); if (skillBlock) { const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings()); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); } else { this.chatContainer.addChild(new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings())); } // ... history handling } break;}3. New skill-invocation-message.ts:
Similar to BranchSummaryMessageComponent:
[skill] pdf-tools (press X to expand)Files affected:
| File | Change |
|---|---|
packages/coding-agent/src/core/agent-session.ts | Change _expandSkillCommand format, export parseSkillBlock() and ParsedSkillBlock |
packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts | NEW |
packages/coding-agent/src/modes/interactive/interactive-mode.ts | Import parser, detect skill blocks, use new component, track for expand toggle |
Want me to implement this?
yes, implement
Let me implement this feature. I need to:
agent-session.ts to change the skill expansion format and export a parserSkillInvocationMessageComponentinteractive-mode.ts to use the new component for skill messagesLet me start by reading the full relevant sections of the files I need to modify.
Now let me look at the imports and see the interactive-mode.ts imports section to know what to add:
Now I have enough context. Let me implement the changes:
First, I'll modify agent-session.ts to:
ParsedSkillBlock interface and parseSkillBlock functionCreate the new SkillInvocationMessageComponent
Modify interactive-mode.ts to use the new component
Let me start with the implementation.
Let me implement all three changes:
Now let me update the _expandSkillCommand method to use the new XML format:
Now let me create the new SkillInvocationMessageComponent:
Now I need to update interactive-mode.ts to:
parseSkillBlock functionaddMessageToChat to use the new component for skill messagesLet me first add the imports:
Now let me update interactive-mode.ts to import and use the new component:
Now let me add the import for SkillInvocationMessageComponent:
Now I need to modify the addMessageToChat method for the "user" case. Let me find the exact location:
Now I need to modify the "user" case to detect skill blocks and use the new component. I also need to track skill components for expand/collapse functionality.
Let me also check if there's a tracking mechanism for expandable components that I need to update:
>cd /Users/badlogic/workspaces/pi-mono && grep -n "Expandable\|toolOutputExpanded\|setExpanded" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -30The code already has an Expandable interface and uses isExpandable() to check if a component can be expanded. Since my SkillInvocationMessageComponent has the setExpanded method, it will automatically work with the expand toggle logic.
Now let me modify the "user" case:
Now let me run the check to make sure everything compiles correctly:
Now let me run the type check:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1All checks pass. Let me also make sure the parseSkillBlock and ParsedSkillBlock are exported from the package's index file if needed. Let me check the exports:
Let me check if the exports are properly set up:
>cd /Users/badlogic/workspaces/pi-mono && grep -n "parseSkillBlock\|ParsedSkillBlock" packages/coding-agent/src/index.tsThe types aren't exported from the package index. Let me check what is exported:
I should export parseSkillBlock and ParsedSkillBlock from the package index so they're available to extensions and the SDK. Let me also add SkillInvocationMessageComponent to the components export.
Let me add the new exports to the index file:
Now let me add SkillInvocationMessageComponent to the components export. Let me find where that's defined:
Let me add the SkillInvocationMessageComponent export:
Now let me also add the export to the main index.ts file:
Let me run the check again to make sure everything still compiles:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1Biome fixed 1 file (probably formatting). Everything compiles correctly. Let me update the CHANGELOG:
Let me update the CHANGELOG:
Now let me add the changelog entry under the ### Added section in [Unreleased]:
Let me run the check one more time to make sure everything is good:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1All checks pass. Let me summarize what was implemented.
Implementation complete. Here's a summary of the changes:
1. packages/coding-agent/src/core/agent-session.ts
ParsedSkillBlock interface and parseSkillBlock() function for parsing skill blocks from user messages_expandSkillCommand() to wrap skill content in XML tags:
<skill name="skill-name" location="/path/to/SKILL.md">References are relative to /path/to/skill.
... skill content ...</skill>
user args (if any)2. packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts (NEW)
[skill] skill-name (press X to expand)3. packages/coding-agent/src/modes/interactive/interactive-mode.ts
parseSkillBlock and SkillInvocationMessageComponentaddMessageToChat() case "user": detect skill blocks and use SkillInvocationMessageComponent instead of UserMessageComponenttoolOutputExpanded toggle (Ctrl+E)4. packages/coding-agent/src/modes/interactive/components/index.ts
SkillInvocationMessageComponent5. packages/coding-agent/src/index.ts
parseSkillBlock, ParsedSkillBlock, and SkillInvocationMessageComponent6. packages/coding-agent/CHANGELOG.md
[Unreleased] → ### AddedWeb search and content extraction using the official Brave Search API. No browser required.
Requires a Brave Search API account with a free subscription. A credit card is required to create the free subscription (you won't be charged).
~/.profile or ~/.zprofile for zsh):
export BRAVE_API_KEY=[REDACTED]cd {baseDir}npm install{baseDir}/search.js "query" # Basic search (5 results){baseDir}/search.js "query" -n 10 # More results (max 20){baseDir}/search.js "query" --content # Include page content as markdown{baseDir}/search.js "query" --freshness pw # Results from last week{baseDir}/search.js "query" --freshness 2024-01-01to2024-06-30 # Date range{baseDir}/search.js "query" --country DE # Results from Germany{baseDir}/search.js "query" -n 3 --content # Combined options-n <num> - Number of results (default: 5, max: 20)--content - Fetch and include page content as markdown--country <code> - Two-letter country code (default: US)--freshness <period> - Filter by time:
pd - Past day (24 hours)pw - Past weekpm - Past monthpy - Past yearYYYY-MM-DDtoYYYY-MM-DD - Custom date range{baseDir}/content.js https://example.com/articleFetches a URL and extracts readable content as markdown.
--- Result 1 ---Title: Page TitleLink: https://example.com/pageAge: 2 days agoSnippet: Description from search resultsContent: (if --content flag used) Markdown content extracted from the page...
--- Result 2 ---...don't do anything, just trying the skill stuff
The user is testing the skill invocation feature. The message shows the new XML format for skill blocks that I just implemented. They're just testing it and explicitly said not to do anything.
I can
ok, /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/TemporaryItems/NSIRD_screencaptureui_MtCNsO/Screenshot\ 2026-01-22\ at\ 22.25.53.png this is not ideal
The user is showing me the current output and has two issues:
The user message (after the skill block) should be rendered as a separate UserMessageComponent, not inside the SkillInvocationMessageComponent. The skill component should only show the skill block itself.
The collapsed skill message should be minimal - all on one line: [skill] <name> (ctrl + o to expand) with the hint part dimmed.
Let me fix the SkillInvocationMessageComponent:
interactive-mode.ts to render the user message separately after the skill componentLet me look at the current implementation again and fix it.
Got it. Let me fix both issues:
Now let me update interactive-mode.ts to render the user message separately after the skill component:
Let me run the check to make sure everything compiles:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1All checks pass. The changes I made:
SkillInvocationMessageComponent:
[skill] skill-name (ctrl+o to expand) with the hint part dimmed[skill] label + skill name header + full contentinteractive-mode.ts:
UserMessageComponent if presentDone. Changes:
[skill] skill-name (ctrl+o to expand) with the hint part dimmedUserMessageComponent after the skill block (not inside it)Web search and content extraction using the official Brave Search API. No browser required.
Requires a Brave Search API account with a free subscription. A credit card is required to create the free subscription (you won't be charged).
~/.profile or ~/.zprofile for zsh):
export BRAVE_API_KEY=[REDACTED]cd {baseDir}npm install{baseDir}/search.js "query" # Basic search (5 results){baseDir}/search.js "query" -n 10 # More results (max 20){baseDir}/search.js "query" --content # Include page content as markdown{baseDir}/search.js "query" --freshness pw # Results from last week{baseDir}/search.js "query" --freshness 2024-01-01to2024-06-30 # Date range{baseDir}/search.js "query" --country DE # Results from Germany{baseDir}/search.js "query" -n 3 --content # Combined options-n <num> - Number of results (default: 5, max: 20)--content - Fetch and include page content as markdown--country <code> - Two-letter country code (default: US)--freshness <period> - Filter by time:
pd - Past day (24 hours)pw - Past weekpm - Past monthpy - Past yearYYYY-MM-DDtoYYYY-MM-DD - Custom date range{baseDir}/content.js https://example.com/articleFetches a URL and extracts readable content as markdown.
--- Result 1 ---Title: Page TitleLink: https://example.com/pageAge: 2 days agoSnippet: Description from search resultsContent: (if --content flag used) Markdown content extracted from the page...
--- Result 2 ---...test
yeah, that's great, please add a space before the skill custom message
The user wants a spacer before the skill message component, similar to how other components like BranchSummaryMessageComponent have a spacer before them. Let me check how it's done for branch summary and add the same for skill invocation.
Let me also verify it still compiles:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1Done. Added a Spacer(1) before the skill component, similar to how BranchSummaryMessageComponent and CompactionSummaryMessageComponent have spacers before them.
Done. Added Spacer(1) before the skill component.
Chrome DevTools Protocol tools for agent-assisted web automation. These tools connect to Chrome running on :9222 with remote debugging enabled.
Run once before first use:
cd {baseDir}/browser-toolsnpm install{baseDir}/browser-start.js # Fresh profile{baseDir}/browser-start.js --profile # Copy user's profile (cookies, logins)Launch Chrome with remote debugging on :9222. Use --profile to preserve user's authentication state.
{baseDir}/browser-nav.js https://example.com{baseDir}/browser-nav.js https://example.com --newNavigate to URLs. Use --new flag to open in a new tab instead of reusing current tab.
{baseDir}/browser-eval.js 'document.title'{baseDir}/browser-eval.js 'document.querySelectorAll("a").length'Execute JavaScript in the active tab. Code runs in async context. Use this to extract data, inspect page state, or perform DOM operations programmatically.
{baseDir}/browser-screenshot.jsCapture current viewport and return temporary file path. Use this to visually inspect page state or verify UI changes.
{baseDir}/browser-pick.js "Click the submit button"IMPORTANT: Use this tool when the user wants to select specific DOM elements on the page. This launches an interactive picker that lets the user click elements to select them. The user can select multiple elements (Cmd/Ctrl+Click) and press Enter when done. The tool returns CSS selectors for the selected elements.
Common use cases:
{baseDir}/browser-cookies.jsDisplay all cookies for the current tab including domain, path, httpOnly, and secure flags. Use this to debug authentication issues or inspect session state.
{baseDir}/browser-content.js https://example.comNavigate to a URL and extract readable content as markdown. Uses Mozilla Readability for article extraction and Turndown for HTML-to-markdown conversion. Works on pages with JavaScript content (waits for page to load).
Don't take screenshots to see page state. Do parse the DOM directly:
// Get page structuredocument.body.innerHTML.slice(0, 5000)
// Find interactive elementsArray.from(document.querySelectorAll('button, input, [role="button"]')).map(e => ({ id: e.id, text: e.textContent.trim(), class: e.className}))Wrap everything in an IIFE to run multi-statement code:
(function() { // Multiple operations const data = document.querySelector('#target').textContent; const buttons = document.querySelectorAll('button'); // Interactions buttons[0].click(); // Return results return JSON.stringify({ data, buttonCount: buttons.length });})()Don't make separate calls for each click. Do batch them:
(function() { const actions = ["btn1", "btn2", "btn3"]; actions.forEach(id => document.getElementById(id).click()); return "Done";})()(function() { const text = "HELLO"; for (const char of text) { document.getElementById("key-" + char).click(); } document.getElementById("submit").click(); return "Submitted: " + text;})()Extract structured state in one call:
(function() { const state = { score: document.querySelector('.score')?.textContent, status: document.querySelector('.status')?.className, items: Array.from(document.querySelectorAll('.item')).map(el => ({ text: el.textContent, active: el.classList.contains('active') })) }; return JSON.stringify(state, null, 2);})()If DOM updates after actions, add a small delay with bash:
sleep 0.5 && {baseDir}/browser-eval.js '...'Always start by understanding the page structure:
(function() { return { title: document.title, forms: document.forms.length, buttons: document.querySelectorAll('button').length, inputs: document.querySelectorAll('input').length, mainContent: document.body.innerHTML.slice(0, 3000) };})()Then target specific elements based on what you find.
The user is testing the skill invocation again. The skill block is showing correctly. They didn't add any user message after the skill command, so it's just the skill block alone. Looks like it's working as expected. I'll wait for their feedback.
Looks like it's working well with the spacer. The skill block displays as a single collapsible line, and since there's no user message after the skill command, nothing else is rendered below it.
jupp, looking good. commit and push, and close the issue with "Thanks for the suggestion, implemented in main2
The user wants me to commit and push the changes, then close the issue with a comment. Let me first check what files were changed.
>cd /Users/badlogic/workspaces/pi-mono && git statusI need to add the specific files I changed and then commit. Let me add all the relevant files.
>cd /Users/badlogic/workspaces/pi-mono && git add \
packages/coding-agent/CHANGELOG.md \
packages/coding-agent/src/core/agent-session.ts \
packages/coding-agent/src/index.ts \
packages/coding-agent/src/modes/interactive/components/index.ts \
packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts \
packages/coding-agent/src/modes/interactive/interactive-mode.ts && \
git commit -m "feat(coding-agent): make skill invocation messages collapsible
- Add ParsedSkillBlock interface and parseSkillBlock() function
- Change skill expansion to use XML-style <skill> tags
- Add SkillInvocationMessageComponent for collapsible display
- Collapsed: single line with skill name and expand hint
- User message rendered separately after skill block
Fixes #894"Now let me push and close the issue with a comment.
>cd /Users/badlogic/workspaces/pi-mono && git pushNow close the issue with the comment.
>cd /Users/badlogic/workspaces/pi-mono && gh issue close 894 --comment "Thanks for the suggestion, implemented in main"The issue was already closed automatically by the "Fixes #894" in the commit message. Let me add the comment separately.
>cd /Users/badlogic/workspaces/pi-mono && gh issue comment 894 --body "Thanks for the suggestion, implemented in main"Done. The issue was already closed by the commit (via "Fixes #894"), and I added the comment.
Done. Committed, pushed, and commented on the issue. The issue was already auto-closed by the Fixes #894 in the commit message.
Skill location: <filePath>References are relative to <baseDir>.
<skill body content>
---
User: <args>Skill location: /path/to/skill/SKILL.mdReferences are relative to /path/to/skill.
<full skill body content>
---
User: <args>// In addMessageToChat(), case "user":if (textContent.startsWith(SKILL_MESSAGE_PREFIX)) { const component = new SkillInvocationMessageComponent(textContent, ...); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component);} else { const userComponent = new UserMessageComponent(textContent, ...); this.chatContainer.addChild(userComponent);}export const SKILL_MESSAGE_PREFIX = "Skill location: ";<skill name="skillname" location="path">References are relative to ${skill.baseDir}.
... skill content ...</skill>
user message/args here<skill name="pdf-tools" location="/path/to/SKILL.md">References are relative to /path/to/skill/.
... skill markdown content ...</skill>
User args here (if any)// New format for skill expansionconst skillMessage = `<skill name="${skill.name}" location="${skill.filePath}">References are relative to ${skill.baseDir}.
${body}</skill>`;
return args ? `${skillMessage}\n\n${args}` : skillMessage;export interface ParsedSkillBlock { name: string; location: string; content: string; // everything inside <skill>...</skill> userMessage: string | undefined; // text after </skill>}
export function parseSkillBlock(text: string): ParsedSkillBlock | null { const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/); if (!match) return null; return { name: match[1], location: match[2], content: match[3], userMessage: match[4]?.trim() || undefined, };}case "user": { const textContent = this.getUserMessageText(message); if (textContent) { const skillBlock = parseSkillBlock(textContent); if (skillBlock) { const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings()); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); } else { this.chatContainer.addChild(new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings())); } // ... history handling } break;}<skill name="skill-name" location="/path/to/SKILL.md">References are relative to /path/to/skill.
... skill content ...</skill>
user args (if any)export BRAVE_API_KEY=[REDACTED]cd {baseDir}npm install{baseDir}/search.js "query" # Basic search (5 results){baseDir}/search.js "query" -n 10 # More results (max 20){baseDir}/search.js "query" --content # Include page content as markdown{baseDir}/search.js "query" --freshness pw # Results from last week{baseDir}/search.js "query" --freshness 2024-01-01to2024-06-30 # Date range{baseDir}/search.js "query" --country DE # Results from Germany{baseDir}/search.js "query" -n 3 --content # Combined options{baseDir}/content.js https://example.com/article--- Result 1 ---Title: Page TitleLink: https://example.com/pageAge: 2 days agoSnippet: Description from search resultsContent: (if --content flag used) Markdown content extracted from the page...
--- Result 2 ---...export BRAVE_API_KEY=[REDACTED]cd {baseDir}npm install{baseDir}/search.js "query" # Basic search (5 results){baseDir}/search.js "query" -n 10 # More results (max 20){baseDir}/search.js "query" --content # Include page content as markdown{baseDir}/search.js "query" --freshness pw # Results from last week{baseDir}/search.js "query" --freshness 2024-01-01to2024-06-30 # Date range{baseDir}/search.js "query" --country DE # Results from Germany{baseDir}/search.js "query" -n 3 --content # Combined options{baseDir}/content.js https://example.com/article--- Result 1 ---Title: Page TitleLink: https://example.com/pageAge: 2 days agoSnippet: Description from search resultsContent: (if --content flag used) Markdown content extracted from the page...
--- Result 2 ---...cd {baseDir}/browser-toolsnpm install{baseDir}/browser-start.js # Fresh profile{baseDir}/browser-start.js --profile # Copy user's profile (cookies, logins){baseDir}/browser-nav.js https://example.com{baseDir}/browser-nav.js https://example.com --new{baseDir}/browser-eval.js 'document.title'{baseDir}/browser-eval.js 'document.querySelectorAll("a").length'{baseDir}/browser-screenshot.js{baseDir}/browser-pick.js "Click the submit button"{baseDir}/browser-cookies.js{baseDir}/browser-content.js https://example.com// Get page structuredocument.body.innerHTML.slice(0, 5000)
// Find interactive elementsArray.from(document.querySelectorAll('button, input, [role="button"]')).map(e => ({ id: e.id, text: e.textContent.trim(), class: e.className}))(function() { // Multiple operations const data = document.querySelector('#target').textContent; const buttons = document.querySelectorAll('button'); // Interactions buttons[0].click(); // Return results return JSON.stringify({ data, buttonCount: buttons.length });})()(function() { const actions = ["btn1", "btn2", "btn3"]; actions.forEach(id => document.getElementById(id).click()); return "Done";})()(function() { const text = "HELLO"; for (const char of text) { document.getElementById("key-" + char).click(); } document.getElementById("submit").click(); return "Submitted: " + text;})()(function() { const state = { score: document.querySelector('.score')?.textContent, status: document.querySelector('.status')?.className, items: Array.from(document.querySelectorAll('.item')).map(el => ({ text: el.textContent, active: el.classList.contains('active') })) }; return JSON.stringify(state, null, 2);})()sleep 0.5 && {baseDir}/browser-eval.js '...'(function() { return { title: document.title, forms: document.forms.length, buttons: document.querySelectorAll('button').length, inputs: document.querySelectorAll('input').length, mainContent: document.body.innerHTML.slice(0, 3000) };})()