webui: split sketch-tool-card into per-tool cards
diff --git a/loop/webui/src/web-components/demo/demo.css b/loop/webui/src/web-components/demo/demo.css
index 11164a1..08e02a2 100644
--- a/loop/webui/src/web-components/demo/demo.css
+++ b/loop/webui/src/web-components/demo/demo.css
@@ -12,3 +12,7 @@
color: #333;
line-height: 1.4; /* Reduced line height for more compact text */
}
+
+pre {
+ white-space: normal;
+}
diff --git a/loop/webui/src/web-components/demo/index.html b/loop/webui/src/web-components/demo/index.html
index 3f728c0..77df51e 100644
--- a/loop/webui/src/web-components/demo/index.html
+++ b/loop/webui/src/web-components/demo/index.html
@@ -20,6 +20,7 @@
</li>
<li><a href="sketch-timeline.demo.html">sketch-timeline</a></li>
<li><a href="sketch-tool-calls.demo.html">sketch-tool-calls</a></li>
+ <li><a href="sketch-tool-card.demo.html">sketch-tool-card</a></li>
<li>
<a href="sketch-view-mode-select.demo.html">sketch-view-mode-select</a>
</li>
diff --git a/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html b/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html
index a72babc..44b598a 100644
--- a/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html
@@ -14,7 +14,8 @@
{
name: "bash",
input: JSON.stringify({
- command: "ls -a",
+ command:
+ "docker ps -a --format '{{.ID}} {{.Image }} {{.Names}}' | grep sketch | awk '{print $1 }' | xargs -I {} docker rm {} && docker image prune -af",
}),
},
],
@@ -171,10 +172,6 @@
];
document.addEventListener("DOMContentLoaded", () => {
toolCalls.forEach((calls) => {
- const jsonEl = document.createElement("pre");
- jsonEl.innerText = `.toolCalls property: ${JSON.stringify(calls)}`;
- document.body.append(jsonEl);
-
const toolCallsEl = document.createElement("sketch-tool-calls");
toolCallsEl.toolCalls = calls;
document.body.append(toolCallsEl);
diff --git a/loop/webui/src/web-components/demo/sketch-tool-card.demo.html b/loop/webui/src/web-components/demo/sketch-tool-card.demo.html
new file mode 100644
index 0000000..17c64ae
--- /dev/null
+++ b/loop/webui/src/web-components/demo/sketch-tool-card.demo.html
@@ -0,0 +1,257 @@
+<html>
+ <head>
+ <title>sketch-tool-card demo</title>
+ <link rel="stylesheet" href="demo.css" />
+
+ <script
+ src="/dist/web-components/sketch-tool-card.js"
+ type="module"
+ ></script>
+
+ <script>
+ const toolCalls = [
+ {
+ name: "bash",
+ input: JSON.stringify({
+ command:
+ "docker ps -a --format '{{.ID}} {{.Image }} {{.Names}}' | grep sketch | awk '{print $1 }' | xargs -I {} docker rm {} && docker image prune -af",
+ }),
+ result_message: {
+ type: "tool",
+ tool_result: `Deleted Images:
+deleted: sha256:110d4aed8bcc76cb7327412504af8aef31670b816453a3088d834bbeefd11a2c
+deleted: sha256:042622460c913078901555a8a72de18e95228fca98b9ac388503b3baafafb683
+deleted: sha256:04ccf3d087e258ffd5f940f378c2aab3c0ed646fb2fb283f90e65397db304694
+deleted: sha256:877120aa3efd02b6afdad181c1cd75bbdc67e41a75dd770fbf781e4fe9c95fc7
+deleted: sha256:d96824c284e594acacc631458818d07842fd4cfa3a1037668a1b23abce077d7b
+deleted: sha256:d90eef6007f5782b59643eecb3edab38af6399d4142f0bb306742efa0e1cf6a4
+deleted: sha256:66b006b0d7570ccf7e2afa15e7b6e6385debba0e60e76eb314383215e480a664
+deleted: sha256:834ff90a57edf5c3987a3f21713310d189f209cec7b002a863c75a22e24cc114
+deleted: sha256:735be867a9939611842099b1131e23096fbde47bb326416382ff7a90a86ab687
+deleted: sha256:986792e96058cabe4452eab0fda2694fe2d5f0b951c446c9c1f94d86614f7bc6
+deleted: sha256:01539d19a06b87dd7a2268677c6beb06bc5aed3cde0c52691a684f4d085bc437
+deleted: sha256:d03b7602a43340d6d1e53ad1d7daa5b55740613ad969c360e1377b7af7597eba
+deleted: sha256:5a7310817c5fa3e29ebfe5b17031fdc5789543460c790ae2e1039226044a6109
+deleted: sha256:def65005e4b1e48e9531ce6ca6bea682bd8285e32b0748212fb8ace12976f920
+deleted: sha256:3b17b8e4e349ac09bac24da27ec4d65e3dec359645f73bd9a38bf015ca5f8a98
+deleted: sha256:1bef4e5c965c2fa2658954096dbe64dae8f3b1d7d595bdb370d54f4027a95603
+deleted: sha256:16e6b5b274b06916833d3f040ca045a12fe1a6a10bebf5f92338fe6b4c7dbbf7
+deleted: sha256:d90588879cc818bc3b3b575a291a3c4088d0ea1c61fad2c4a2f34160bdc86db6
+deleted: sha256:85903960027c7b9baf8bd0ee662571758ce8ffe83526839377284e2fccac558f
+untagged: sketch-94924d08c163:latest
+deleted: sha256:7c7c3957d3ba526a351d21e52a1aee0e72bb4a62d0422a0eb3a0e2b53391824f
+deleted: sha256:e4a1fe6a3369ca8f24baaba277bc9d97353992e9e051020c5a25e588a702e634
+deleted: sha256:28ccbe834ee66199498458f500b10cc9ea69460216982a537ea3294d6dfb0b63
+deleted: sha256:95c7d2956020039d92b546d6824c5d7fac163a6247be599160483d263094c047
+deleted: sha256:f87bc9eb655a06edd50d5a34e016175006c430ad129146b9b755169a3c318a57
+deleted: sha256:b455829fdcd5fe238567af2370f9fc021eb416ec2140f98b0ab59478febcfb2e
+deleted: sha256:ed64271d223807308a391a733fc556a6c16bfb87e6f9aed6d4ce394fcbb77ba6
+deleted: sha256:a5ce6521003bca24abcb4a0021837e789349fb3f44f7ceb00ef4af33ca01f84f
+deleted: sha256:57e05db1ff95deab5f5c3f38f9607a1c3bb21518133f4e0c137ffe6bb9cbfde9
+deleted: sha256:540194db01e12f59d19f7795ec9c8a1bb753df2de935469b21a10fc7ca1d25a5
+deleted: sha256:97519dae495c256597a9b7975a332e67edb21f93e306b72132ed2c30bb01b8aa
+deleted: sha256:162c7a942156fd5f16616c6fea4a26f2bfa01a53e499d59fdb8c68e815f5350e
+deleted: sha256:51b9d76df1fbcb277e4f22496ff661d4d748f499453a27a012629f78bb61107e
+deleted: sha256:7a1a595c3015a6b2f5e996988d094bcaca328ebeaafe37403e78322e10d6b859
+deleted: sha256:27631f63a84d9a524381a95168f24deb89612fb468e03bce724f352bb5ef7b3b
+deleted: sha256:58746669dff4a4051d05542e05109d57c94f867981b47bdb5800d62567a6280f
+untagged: golang:1.24.2-alpine3.21
+untagged: golang@sha256:7772cb5322baa875edd74705556d08f0eeca7b9c4b5367754ce3f2f00041ccee
+untagged: sketch-3c262c60c42c:latest
+deleted: sha256:fadf166900e61610d77d613ce52ca1c03711ce2a7bcd31f1f634529791c0c107
+deleted: sha256:8b719162dad84cddd630e1e943520041947ca91b3794417c0d2a03b3726ebaa4
+deleted: sha256:444f0e44dcaff517142f8aab35d35f08536d886a746f6858dac7052977ee2cff
+deleted: sha256:a95a3660958ed25a27ae7b0622b5426e046d4c5587693aa7c0098e050e057311
+deleted: sha256:edb781114acb505bbde5e4a3db68b7ab6f4a3c0da92ceed2d10f02c6278b93c8
+deleted: sha256:1429402020a73b7d5c1de32f9451c68e22508cc4238750f5a500e1d9737eedae
+deleted: sha256:3f749e03b0f5ef2dfc538581c92230f2cd6b844fe3c734c728fd3775865ed24c
+deleted: sha256:f62c6ba2d4f4b94796d4c4c111031fbbbaf22df24623a2d6729277dc1eaf8da8
+deleted: sha256:504579f990b8894755910252d3b401f86a589709efafb30b9ded67cb3edad80e
+deleted: sha256:2e22f953ef8cc5fac95fb0babc5042f5e2a7fefc9d5ec444429c490d54acb1ab
+deleted: sha256:afa0c23676c039532a39faa1f1506b19f34507b586796ea070dcaee30e6228ef
+deleted: sha256:5f176f397253734bdc726a505c84448f9b00e5652d9a28ef59de0581a2e8e923
+deleted: sha256:253afbfd579bc6daf71e42b0f1e369d2b6c9015028191af4478da0b77b8a85ed
+deleted: sha256:81f79e13183887f93db52268f00975f43613abc520c88e1090a1dbb3d09094e9
+deleted: sha256:3c0b6f56bdbec5bf995b818e8a67d2d6c3bd9aa3698c403b6dabc01a81a4cb52
+deleted: sha256:635f4ba57c6445e69cf8c6fba61c3690f76901e17334f6d2d165979b2d387dfa
+
+Total reclaimed space: 1.426GB`,
+ },
+ },
+ {
+ name: "bash",
+ input: JSON.stringify({
+ command: "ls -a",
+ }),
+ result_message: {
+ type: "tool",
+ tool_result: ".\n..",
+ },
+ },
+ {
+ name: "bash",
+ input: JSON.stringify({
+ command: "sleep 200",
+ }),
+ result_message: {
+ type: "tool",
+ tool_error: "the user canceled this operation",
+ },
+ },
+ {
+ name: "title",
+ input: JSON.stringify({
+ title: "a new title for this sketch",
+ }),
+ },
+ {
+ name: "codereview",
+ input: "{}",
+ tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
+ result_message: {
+ type: "tool",
+ end_of_turn: false,
+ content: "",
+ tool_name: "codereview",
+ input: "{}",
+ tool_result: "OK",
+ tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
+ timestamp: "2025-04-14T16:33:17.575759565Z",
+ conversation_id: "xsa-8hw0",
+ start_time: "2025-04-14T16:33:07.11793816Z",
+ end_time: "2025-04-14T16:33:17.57575719Z",
+ elapsed: 10457819031,
+ idx: 45,
+ },
+ },
+ {
+ name: "codereview",
+ input: "{}",
+ tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
+ result_message: {
+ type: "tool",
+ end_of_turn: false,
+ content: "",
+ tool_name: "codereview",
+ input: "{}",
+ tool_result: "Not OK",
+ tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
+ timestamp: "2025-04-14T16:33:17.575759565Z",
+ conversation_id: "xsa-8hw0",
+ start_time: "2025-04-14T16:33:07.11793816Z",
+ end_time: "2025-04-14T16:33:17.57575719Z",
+ elapsed: 10457819031,
+ idx: 45,
+ },
+ },
+ {
+ name: "think",
+ input:
+ '{"thoughts":"I\'m going to inspect a few key components to understand their purpose and relationships:\\n1. sketch-app-shell.ts - Appears to be the main container component\\n2. sketch-timeline.ts - Likely manages the chat timeline\\n3. sketch-view-mode-select.ts - Handles switching between different views\\n\\nThis will help me create a comprehensive README that explains the components and their relationships."}',
+ tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
+ result_message: {
+ type: "tool",
+ end_of_turn: false,
+ content: "",
+ tool_name: "think",
+ input:
+ '{"thoughts":"I\'m going to inspect a few key components to understand their purpose and relationships:\\n1. sketch-app-shell.ts - Appears to be the main container component\\n2. sketch-timeline.ts - Likely manages the chat timeline\\n3. sketch-view-mode-select.ts - Handles switching between different views\\n\\nThis will help me create a comprehensive README that explains the components and their relationships."}',
+ tool_result: "recorded",
+ tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
+ timestamp: "2025-04-14T16:32:14.12647133Z",
+ conversation_id: "xsa-8hw0",
+ start_time: "2025-04-14T16:32:14.126454329Z",
+ end_time: "2025-04-14T16:32:14.126468539Z",
+ elapsed: 14209,
+ idx: 18,
+ },
+ },
+ {
+ name: "patch",
+ input:
+ '{"path":"/app/loop/webui/src/web-components/README.md","patches":[{"operation":"overwrite","newText":"# Web Components\\n\\nThis directory contains the custom web components used in the Loop WebUI. These components are built using the [Lit](https://lit.dev/) library for creating fast, lightweight web components with a declarative template system.\\n\\n## Component Architecture\\n\\nThe components follow a hierarchical structure that creates a complete UI for interacting with the CodingAgent:\\n\\n```\\nsketch-app-shell (main container)\\n├── sketch-container-status\\n├── sketch-network-status\\n├── sketch-view-mode-select\\n├── sketch-timeline (chat view)\\n│ └── sketch-timeline-message\\n│ └── sketch-tool-calls\\n├── sketch-diff-view (code diff view)\\n└── sketch-chat-input\\n```\\n\\n## Component Overview\\n\\n### sketch-app-shell.ts\\nThe main container component that orchestrates the entire UI. It manages:\\n- View modes (chat, diff, charts, terminal)\\n- Network status and connection management\\n- Timeline data fetching and rendering\\n- Auto-scrolling behavior for chat messages\\n\\n### sketch-chat-input.ts\\nHandles user input for sending messages to the CodingAgent:\\n- Text input area with markdown support\\n- Send button and keyboard shortcuts (Enter to send, Shift+Enter for newline)\\n- Auto-focusing behavior\\n\\n### sketch-container-status.ts\\nDisplays information about the container environment:\\n- OS information\\n- Resource usage (CPU, memory)\\n- Container status indicators\\n\\n### sketch-diff-view.ts\\nProvides a visual diff viewer for code changes:\\n- Git commit display\\n- Side-by-side or unified diff viewing\\n- Syntax highlighting for code\\n- Comment creation for code review\\n\\n### sketch-network-status.ts\\nShows the current connection status to the server:\\n- Connected/disconnected indicators\\n- Error messages when connection issues occur\\n- Visual feedback on connection state\\n\\n### sketch-timeline.ts\\nDisplays the conversation history between user and CodingAgent:\\n- Message rendering\\n- Manages the sequence of messages\\n- Handles scrolling behavior\\n\\n### sketch-timeline-message.ts\\nRenders individual messages in the timeline:\\n- Different styling for user vs. agent messages\\n- Markdown rendering with syntax highlighting\\n- Handles special message types\\n\\n### sketch-tool-calls.ts\\nDisplays tool call information within messages:\\n- Tool call parameters and outputs\\n- Expandable/collapsible sections for tool details\\n- Syntax highlighting for code in tool outputs\\n\\n### sketch-view-mode-select.ts\\nProvides UI for switching between different views:\\n- Chat view for conversation\\n- Diff view for code changes\\n- Charts view for data visualization\\n- Terminal view for command execution\\n\\n## Development\\n\\n### Creating New Components\\n\\nWhen creating new components, follow these patterns:\\n\\n1. Use the `@customElement` decorator to define the component\\n2. Encapsulate styles using the static `styles` property\\n3. Use Lit\'s reactive properties (`@property`, `@state`) to manage component state\\n4. Implement lifecycle methods as needed (`connectedCallback`, `disconnectedCallback`, etc.)\\n\\n### Example\\n\\n```typescript\\nimport { css, html, LitElement } from \'lit\';\\nimport { customElement, property } from \'lit/decorators.js\';\\n\\n@customElement(\'sketch-new-component\')\\nexport class SketchNewComponent extends LitElement {\\n @property()\\n myProperty: string = \'default\';\\n\\n static styles = css`\\n :host {\\n display: block;\\n }\\n `;\\n\\n render() {\\n return html`\\n <div>\\n ${this.myProperty}\\n </div>\\n `;\\n }\\n}\\n\\ndeclare global {\\n interface HTMLElementTagNameMap {\\n \'sketch-new-component\': SketchNewComponent;\\n }\\n}\\n```\\n\\n### Best Practices\\n\\n1. Maintain a single responsibility for each component\\n2. Use properties and events for parent-child communication\\n3. Document the purpose and API of each component\\n4. Write tests for components where possible\\n5. Keep styles encapsulated within components\\n6. Use type definitions for strong typing\\n\\n## Related Resources\\n\\n- [Lit Documentation](https://lit.dev/docs/)\\n- [Web Components Introduction](https://developer.mozilla.org/en-US/docs/Web/Web_Components)\\n- See `/app/loop/webui/readme.md` for more information on the Loop WebUI project\\n"}]}',
+ tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
+ result_message: {
+ type: "tool",
+ end_of_turn: false,
+ content: "",
+ tool_name: "patch",
+ input:
+ '{"path":"/app/loop/webui/src/web-components/README.md","patches":[{"operation":"overwrite","newText":"# Web Components\\n\\nThis directory contains the custom web components used in the Loop WebUI. These components are built using the [Lit](https://lit.dev/) library for creating fast, lightweight web components with a declarative template system.\\n\\n## Component Architecture\\n\\nThe components follow a hierarchical structure that creates a complete UI for interacting with the CodingAgent:\\n\\n```\\nsketch-app-shell (main container)\\n├── sketch-container-status\\n├── sketch-network-status\\n├── sketch-view-mode-select\\n├── sketch-timeline (chat view)\\n│ └── sketch-timeline-message\\n│ └── sketch-tool-calls\\n├── sketch-diff-view (code diff view)\\n└── sketch-chat-input\\n```\\n\\n## Component Overview\\n\\n### sketch-app-shell.ts\\nThe main container component that orchestrates the entire UI. It manages:\\n- View modes (chat, diff, charts, terminal)\\n- Network status and connection management\\n- Timeline data fetching and rendering\\n- Auto-scrolling behavior for chat messages\\n\\n### sketch-chat-input.ts\\nHandles user input for sending messages to the CodingAgent:\\n- Text input area with markdown support\\n- Send button and keyboard shortcuts (Enter to send, Shift+Enter for newline)\\n- Auto-focusing behavior\\n\\n### sketch-container-status.ts\\nDisplays information about the container environment:\\n- OS information\\n- Resource usage (CPU, memory)\\n- Container status indicators\\n\\n### sketch-diff-view.ts\\nProvides a visual diff viewer for code changes:\\n- Git commit display\\n- Side-by-side or unified diff viewing\\n- Syntax highlighting for code\\n- Comment creation for code review\\n\\n### sketch-network-status.ts\\nShows the current connection status to the server:\\n- Connected/disconnected indicators\\n- Error messages when connection issues occur\\n- Visual feedback on connection state\\n\\n### sketch-timeline.ts\\nDisplays the conversation history between user and CodingAgent:\\n- Message rendering\\n- Manages the sequence of messages\\n- Handles scrolling behavior\\n\\n### sketch-timeline-message.ts\\nRenders individual messages in the timeline:\\n- Different styling for user vs. agent messages\\n- Markdown rendering with syntax highlighting\\n- Handles special message types\\n\\n### sketch-tool-calls.ts\\nDisplays tool call information within messages:\\n- Tool call parameters and outputs\\n- Expandable/collapsible sections for tool details\\n- Syntax highlighting for code in tool outputs\\n\\n### sketch-view-mode-select.ts\\nProvides UI for switching between different views:\\n- Chat view for conversation\\n- Diff view for code changes\\n- Charts view for data visualization\\n- Terminal view for command execution\\n\\n## Development\\n\\n### Creating New Components\\n\\nWhen creating new components, follow these patterns:\\n\\n1. Use the `@customElement` decorator to define the component\\n2. Encapsulate styles using the static `styles` property\\n3. Use Lit\'s reactive properties (`@property`, `@state`) to manage component state\\n4. Implement lifecycle methods as needed (`connectedCallback`, `disconnectedCallback`, etc.)\\n\\n### Example\\n\\n```typescript\\nimport { css, html, LitElement } from \'lit\';\\nimport { customElement, property } from \'lit/decorators.js\';\\n\\n@customElement(\'sketch-new-component\')\\nexport class SketchNewComponent extends LitElement {\\n @property()\\n myProperty: string = \'default\';\\n\\n static styles = css`\\n :host {\\n display: block;\\n }\\n `;\\n\\n render() {\\n return html`\\n <div>\\n ${this.myProperty}\\n </div>\\n `;\\n }\\n}\\n\\ndeclare global {\\n interface HTMLElementTagNameMap {\\n \'sketch-new-component\': SketchNewComponent;\\n }\\n}\\n```\\n\\n### Best Practices\\n\\n1. Maintain a single responsibility for each component\\n2. Use properties and events for parent-child communication\\n3. Document the purpose and API of each component\\n4. Write tests for components where possible\\n5. Keep styles encapsulated within components\\n6. Use type definitions for strong typing\\n\\n## Related Resources\\n\\n- [Lit Documentation](https://lit.dev/docs/)\\n- [Web Components Introduction](https://developer.mozilla.org/en-US/docs/Web/Web_Components)\\n- See `/app/loop/webui/readme.md` for more information on the Loop WebUI project\\n"}]}',
+ tool_result: "- Applied all patches\n",
+ tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
+ timestamp: "2025-04-14T16:32:47.10299854Z",
+ conversation_id: "xsa-8hw0",
+ start_time: "2025-04-14T16:32:47.101660683Z",
+ end_time: "2025-04-14T16:32:47.102994206Z",
+ elapsed: 1333524,
+ idx: 34,
+ },
+ },
+ {
+ name: "done",
+ input:
+ '{"checklist_items":{"code_reviewed":{"status":"yes","description":"If any commits were made, the codereview tool was run and its output was addressed.","comments":"Since this commit only adds a documentation file (README.md) with no functional code changes, a formal code review is not essential, but I\'ve verified the content for accuracy and completeness."},"wrote_tests":{"status":"not applicable","description":"If code was changed, tests were written or updated.","comments":"No code was changed; only documentation was added. The README file is purely informational and doesn\'t require tests."},"passes_tests":{"status":"not applicable","description":"If any commits were made, tests pass.","comments":"No functional code changes were made that would impact existing tests. The addition of a README file is a documentation-only change."},"git_commit":{"status":"yes","description":"Create git commits for any code changes you made. Match the style of recent commit messages. Include \'Co-Authored-By: sketch\' and the original user prompt. Use GIT_AUTHOR_NAME=\\"Sean McCullough\\" GIT_AUTHOR_EMAIL=\\"banksean@gmail.com\\" (not git config).","comments":"Created a commit with a descriptive message that follows the requested format, including \'Co-Authored-By: sketch\' attribution."}}}',
+ tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
+ result_message: {
+ type: "tool",
+ end_of_turn: false,
+ content: "",
+ tool_name: "done",
+ input:
+ '{"checklist_items":{"code_reviewed":{"status":"yes","description":"If any commits were made, the codereview tool was run and its output was addressed.","comments":"Since this commit only adds a documentation file (README.md) with no functional code changes, a formal code review is not essential, but I\'ve verified the content for accuracy and completeness."},"wrote_tests":{"status":"not applicable","description":"If code was changed, tests were written or updated.","comments":"No code was changed; only documentation was added. The README file is purely informational and doesn\'t require tests."},"passes_tests":{"status":"not applicable","description":"If any commits were made, tests pass.","comments":"No functional code changes were made that would impact existing tests. The addition of a README file is a documentation-only change."},"git_commit":{"status":"yes","description":"Create git commits for any code changes you made. Match the style of recent commit messages. Include \'Co-Authored-By: sketch\' and the original user prompt. Use GIT_AUTHOR_NAME=\\"Sean McCullough\\" GIT_AUTHOR_EMAIL=\\"banksean@gmail.com\\" (not git config).","comments":"Created a commit with a descriptive message that follows the requested format, including \'Co-Authored-By: sketch\' attribution."}}}',
+ tool_result:
+ "codereview tool has not been run for commit 0b1f45dc17fbe7800f5164993ec99d6564256787",
+ tool_error: true,
+ tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
+ timestamp: "2025-04-14T16:33:04.639179373Z",
+ conversation_id: "xsa-8hw0",
+ start_time: "2025-04-14T16:33:04.616273148Z",
+ end_time: "2025-04-14T16:33:04.639173456Z",
+ elapsed: 22900309,
+ idx: 43,
+ },
+ },
+ ];
+ document.addEventListener("DOMContentLoaded", () => {
+ toolCalls.forEach((toolCall) => {
+ const h2El = document.createElement("h2");
+ h2El.innerText = toolCall.name;
+ document.body.append(h2El);
+
+ let toolCardEl = document.createElement("sketch-tool-card-generic");
+ switch (toolCall.name) {
+ case "bash":
+ toolCardEl = document.createElement("sketch-tool-card-bash");
+ break;
+ case "codereview":
+ toolCardEl = document.createElement(
+ "sketch-tool-card-codereview",
+ );
+ break;
+ case "done":
+ toolCardEl = document.createElement("sketch-tool-card-done");
+ break;
+ case "patch":
+ toolCardEl = document.createElement("sketch-tool-card-patch");
+ break;
+ case "think":
+ toolCardEl = document.createElement("sketch-tool-card-think");
+ break;
+ case "title":
+ toolCardEl = document.createElement("sketch-tool-card-title");
+ break;
+ }
+ toolCardEl.toolCall = toolCall;
+ toolCardEl.open = true;
+ document.body.append(toolCardEl);
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <h1>sketch-tool-calls demo</h1>
+
+ <sketch-tool-calls></sketch-tool-calls>
+ </body>
+</html>
diff --git a/loop/webui/src/web-components/sketch-timeline-message.ts b/loop/webui/src/web-components/sketch-timeline-message.ts
index ea32015..88e9f14 100644
--- a/loop/webui/src/web-components/sketch-timeline-message.ts
+++ b/loop/webui/src/web-components/sketch-timeline-message.ts
@@ -559,27 +559,31 @@
function copyButton(textToCopy: string) {
// Add click event listener to handle copying
- const ret = html`<button class="copy-button" title="Copy text to clipboard" @click=${(
- e: Event,
- ) => {
- e.stopPropagation();
- const copyButton = e.currentTarget as HTMLButtonElement;
- navigator.clipboard
- .writeText(textToCopy)
- .then(() => {
- copyButton.textContent = "Copied!";
- setTimeout(() => {
- copyButton.textContent = "Copy";
- }, 2000);
- })
- .catch((err) => {
- console.error("Failed to copy text: ", err);
- copyButton.textContent = "Failed";
- setTimeout(() => {
- copyButton.textContent = "Copy";
- }, 2000);
- });
- }}>Copy</button`;
+ const ret = html`<button
+ class="copy-button"
+ title="Copy text to clipboard"
+ @click=${(e: Event) => {
+ e.stopPropagation();
+ const copyButton = e.currentTarget as HTMLButtonElement;
+ navigator.clipboard
+ .writeText(textToCopy)
+ .then(() => {
+ copyButton.textContent = "Copied!";
+ setTimeout(() => {
+ copyButton.textContent = "Copy";
+ }, 2000);
+ })
+ .catch((err) => {
+ console.error("Failed to copy text: ", err);
+ copyButton.textContent = "Failed";
+ setTimeout(() => {
+ copyButton.textContent = "Copy";
+ }, 2000);
+ });
+ }}
+ >
+ Copy
+ </button>`;
return ret;
}
diff --git a/loop/webui/src/web-components/sketch-tool-calls.ts b/loop/webui/src/web-components/sketch-tool-calls.ts
index e7a5c74..2b4c426 100644
--- a/loop/webui/src/web-components/sketch-tool-calls.ts
+++ b/loop/webui/src/web-components/sketch-tool-calls.ts
@@ -1,172 +1,27 @@
import { css, html, LitElement } from "lit";
-import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { repeat } from "lit/directives/repeat.js";
import { customElement, property } from "lit/decorators.js";
import { State, ToolCall } from "../types";
import { marked, MarkedOptions } from "marked";
+import "./sketch-tool-card";
@customElement("sketch-tool-calls")
export class SketchToolCalls extends LitElement {
@property()
toolCalls: ToolCall[] = [];
- // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
- // Note that these styles only apply to the scope of this web component's
- // shadow DOM node, so they won't leak out or collide with CSS declared in
- // other components or the containing web page (...unless you want it to do that).
static styles = css`
/* Tool calls container styles */
.tool-calls-container {
- /* Removed dotted border */
+ /* Container for all tool calls */
}
- .tool-calls-toggle {
- cursor: pointer;
- background-color: #f0f0f0;
- padding: 5px 10px;
- border: none;
- border-radius: 4px;
- text-align: left;
- font-size: 12px;
- margin-top: 5px;
- color: #555;
- font-weight: 500;
- }
-
- .tool-calls-toggle:hover {
- background-color: #e0e0e0;
- }
-
- .tool-calls-details {
- margin-top: 10px;
- transition: max-height 0.3s ease;
- }
-
- .tool-calls-details.collapsed {
- max-height: 0;
- overflow: hidden;
- margin-top: 0;
- }
-
- .tool-call {
- background: #f9f9f9;
- border-radius: 4px;
- padding: 10px;
- margin-bottom: 10px;
- border-left: 3px solid #4caf50;
- }
-
- .tool-call-header {
- margin-bottom: 8px;
- font-size: 14px;
- padding: 2px 0;
- }
-
- /* Compact tool display styles */
- .tool-compact-line {
- font-family: monospace;
- font-size: 12px;
- line-height: 1.4;
- padding: 4px 6px;
- background: #f8f8f8;
- border-radius: 3px;
- position: relative;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
- display: flex;
- align-items: center;
- }
-
- .tool-result-inline {
- font-family: monospace;
- color: #0066bb;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 400px;
- display: inline-block;
- vertical-align: middle;
- }
-
- .copy-inline-button {
- font-size: 10px;
- padding: 2px 4px;
- margin-left: 8px;
- background: #eee;
- border: none;
- border-radius: 3px;
- cursor: pointer;
- opacity: 0.7;
- }
-
- .copy-inline-button:hover {
- opacity: 1;
- background: #ddd;
- }
-
- .tool-input.compact,
- .tool-result.compact {
- margin: 2px 0;
- padding: 4px;
- font-size: 12px;
- }
-
- /* Removed old compact container CSS */
-
- /* Ultra-compact tool call box styles */
+ /* Header for tool calls section */
.tool-calls-header {
/* Empty header - just small spacing */
}
- .tool-call-boxes-row {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 8px;
- }
-
- .tool-call-wrapper {
- display: flex;
- flex-direction: column;
- margin-bottom: 4px;
- }
-
- .tool-call-box {
- display: inline-flex;
- align-items: center;
- background: #f0f0f0;
- border-radius: 4px;
- padding: 3px 8px;
- font-size: 12px;
- cursor: pointer;
- max-width: 320px;
- position: relative;
- border: 1px solid #ddd;
- transition: background-color 0.2s;
- }
-
- .tool-call-box:hover {
- background-color: #e8e8e8;
- }
-
- .tool-call-box.expanded {
- background-color: #e0e0e0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- border-bottom: 1px solid #ccc;
- }
-
- .tool-call-input {
- color: #666;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-family: monospace;
- font-size: 11px;
- }
-
+ /* Card container */
.tool-call-card {
display: flex;
flex-direction: column;
@@ -175,42 +30,7 @@
cursor: pointer;
}
- /* Compact view (default) */
- .tool-call-compact-view {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 0.9em;
- white-space: nowrap;
- overflow: visible; /* Don't hide overflow, we'll handle text truncation per element */
- position: relative; /* For positioning the expand icon */
- }
-
- /* Expanded view (hidden by default) */
- .tool-call-card.collapsed .tool-call-expanded-view {
- display: none;
- }
-
- .tool-call-expanded-view {
- display: flex;
- flex-direction: column;
- border-top: 1px solid #eee;
- }
-
- .tool-call-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 6px 10px;
- background-color: #f0f0f0;
- border-bottom: 1px solid #ddd;
- font-weight: bold;
- }
-
- .tool-call-name {
- color: gray;
- }
-
+ /* Status indicators for tool calls */
.tool-call-status {
margin-right: 4px;
text-align: center;
@@ -222,70 +42,6 @@
width: 1em;
}
- .tool-call-time {
- margin-left: 8px;
- font-size: 0.85em;
- color: #666;
- font-weight: normal;
- }
-
- .tool-call-input-preview {
- color: #555;
- font-family: var(--monospace-font);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 30%;
- background-color: rgba(240, 240, 240, 0.5);
- padding: 2px 5px;
- border-radius: 3px;
- font-size: 0.9em;
- }
-
- .tool-call-result-preview {
- color: #28a745;
- font-family: var(--monospace-font);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 40%;
- background-color: rgba(240, 248, 240, 0.5);
- padding: 2px 5px;
- border-radius: 3px;
- font-size: 0.9em;
- }
-
- .tool-call-expand-icon {
- position: absolute;
- right: 10px;
- font-size: 0.8em;
- color: #888;
- }
-
- .tool-call-input {
- padding: 6px 10px;
- border-bottom: 1px solid #eee;
- font-family: var(--monospace-font);
- font-size: 0.9em;
- white-space: pre-wrap;
- word-break: break-all;
- background-color: #f5f5f5;
- }
-
- .tool-call-result {
- padding: 6px 10px;
- font-family: var(--monospace-font);
- font-size: 0.9em;
- white-space: pre-wrap;
- max-height: 300px;
- overflow-y: auto;
- }
-
- .tool-call-result pre {
- margin: 0;
- white-space: pre-wrap;
- }
-
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -294,344 +50,73 @@
transform: rotate(360deg);
}
}
-
- /* Standalone tool messages (legacy/disconnected) */
- .tool-details.standalone .tool-header {
- border-radius: 4px;
- background-color: #fff3cd;
- border-color: #ffeeba;
- }
-
- .tool-details.standalone .tool-warning {
- margin-left: 10px;
- font-size: 0.85em;
- color: #856404;
- font-style: italic;
- }
-
- /* Tool call expanded view with sections */
- .tool-call-section {
- border-bottom: 1px solid #eee;
- }
-
- .tool-call-section:last-child {
- border-bottom: none;
- }
-
- .tool-call-section-label {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 10px;
- background-color: #f5f5f5;
- font-weight: bold;
- font-size: 0.9em;
- }
-
- .tool-call-section-content {
- padding: 0;
- }
-
- .tool-call-copy-btn {
- background-color: #f0f0f0;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 2px 8px;
- font-size: 0.8em;
- cursor: pointer;
- transition: background-color 0.2s;
- }
-
- .tool-call-copy-btn:hover {
- background-color: #e0e0e0;
- }
-
- /* Override for tool call input in expanded view */
- .tool-call-section-content .tool-call-input {
- margin: 0;
- padding: 8px 10px;
- border: none;
- background-color: #fff;
- max-height: 300px;
- overflow-y: auto;
- }
-
- .tool-call-card .tool-call-input-preview,
- .tool-call-card .tool-call-result-preview {
- font-family: monospace;
- background: black;
- padding: 1em;
- }
- .tool-call-input-preview {
- color: white;
- }
- .tool-call-result-preview {
- color: gray;
- }
-
- .tool-call-card.title {
- font-style: italic;
- }
-
- .cancel-button {
- background: rgb(76, 175, 80);
- color: white;
- border: none;
- padding: 4px 10px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- margin: 5px;
- }
-
- .cancel-button:hover {
- background: rgb(200, 35, 51) !important;
- }
-
- .thought-bubble {
- position: relative;
- background-color: #eee;
- border-radius: 8px;
- padding: 0.5em;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
- margin-left: 24px;
- margin-top: 24px;
- margin-bottom: 12px;
- max-width: 30%;
- white-space: pre;
- }
-
- .thought-bubble .preview {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- }
-
- .thought-bubble:before {
- content: "";
- position: absolute;
- top: -8px;
- left: -8px;
- width: 15px;
- height: 15px;
- background-color: #eee;
- border-radius: 50%;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
- }
-
- .thought-bubble:after {
- content: "";
- position: absolute;
- top: -16px;
- left: -16px;
- width: 8px;
- height: 8px;
- background-color: #eee;
- border-radius: 50%;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
- }
-
- .patch-input-preview {
- color: #555;
- font-family: monospace;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 30%;
- background-color: rgba(240, 240, 240, 0.5);
- padding: 2px 5px;
- border-radius: 3px;
- font-size: 0.9em;
- }
-
- .codereview-OK {
- color: green;
- }
`;
constructor() {
super();
}
- // See https://lit.dev/docs/components/lifecycle/
connectedCallback() {
super.connectedCallback();
}
- // See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
}
- renderMarkdown(markdownContent: string): string {
- try {
- // Set markdown options for proper code block highlighting and safety
- const markedOptions: MarkedOptions = {
- gfm: true, // GitHub Flavored Markdown
- breaks: true, // Convert newlines to <br>
- async: false,
- // DOMPurify is recommended for production, but not included in this implementation
- };
- return marked.parse(markdownContent, markedOptions) as string;
- } catch (error) {
- console.error("Error rendering markdown:", error);
- // Fallback to plain text if markdown parsing fails
- return markdownContent;
- }
- }
-
- _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
- console.log("cancelToolCall", tool_call_id, button);
- button.innerText = "Cancelling";
- button.disabled = true;
- try {
- const response = await fetch("cancel", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- tool_call_id: tool_call_id,
- reason: "user requested cancellation",
- }),
- });
- if (response.ok) {
- console.log("cancel", tool_call_id, response);
- button.parentElement.removeChild(button);
- } else {
- button.innerText = "Cancel";
- console.log(`error trying to cancel ${tool_call_id}: `, response);
- }
- } catch (e) {
- console.error("cancel", tool_call_id, e);
- }
- };
-
- toolCard(toolCall: ToolCall) {
- const toolCallStatus = toolCall.result_message
- ? toolCall.result_message.tool_error
- ? "❌"
- : ""
- : "⏳";
-
- const cancelButton = toolCall.result_message
- ? ""
- : html`<button
- class="cancel-button"
- title="Cancel this operation"
- @click=${(e: Event) => {
- e.stopPropagation();
- const button = e.target as HTMLButtonElement;
- this._cancelToolCall(toolCall.tool_call_id, button);
- }}
- >
- Cancel
- </button>`;
-
- const status = html`<span
- class="tool-call-status ${toolCall.result_message ? "" : "spinner"}"
- >${toolCallStatus}</span
- >`;
-
+ cardForToolCall(toolCall: ToolCall, open: boolean) {
switch (toolCall.name) {
- case "title":
- const titleInput = JSON.parse(toolCall.input);
- return html` <div class="tool-call-compact-view">
- I've set the title of this sketch to <b>"${titleInput.title}"</b>
- </div>`;
case "bash":
- const bashInput = JSON.parse(toolCall.input);
- return html` <div class="tool-call-compact-view">
- ${status}
- <span class="tool-call-name">${toolCall.name}</span>
- <pre class="tool-call-input-preview">${bashInput.command}</pre>
- ${toolCall.result_message
- ? html` ${toolCall.result_message.tool_result
- ? html` <pre class="tool-call-result-preview">
-${toolCall.result_message.tool_result}</pre
- >`
- : ""}`
- : cancelButton}
- </div>`;
+ return html`<sketch-tool-card-bash
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-bash>`;
case "codereview":
- return html` <div class="tool-call-compact-view">
- ${status}
- <span class="tool-call-name">${toolCall.name}</span>
- ${cancelButton}
- <code
- class="codereview-preview codereview-${toolCall.result_message
- ?.tool_result}"
- >${toolCall.result_message?.tool_result == "OK"
- ? "✔️"
- : "⛔ " + toolCall.result_message?.tool_result}</code
- >
- </div>`;
- case "think":
- const thinkInput = JSON.parse(toolCall.input);
- return html` <div class="tool-call-compact-view">
- ${status}
- <span class="tool-call-name">${toolCall.name}</span>
- <div class="thought-bubble">
- <div class="preview">${thinkInput.thoughts}</div>
- </div>
- ${cancelButton}
- </div>`;
- case "patch":
- const patchInput = JSON.parse(toolCall.input);
- return html` <div class="tool-call-compact-view">
- ${status}
- <span class="tool-call-name">${toolCall.name}</span>
- <div class="patch-input-preview">
- <span class="patch-path">${patchInput.path}</span>:
- ${patchInput.patches.length}
- edit${patchInput.patches.length > 1 ? "s" : ""}
- </div>
- ${cancelButton}
- </div>`;
+ return html`<sketch-tool-card-codereview
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-codereview>`;
case "done":
- const doneInput = JSON.parse(toolCall.input);
- return html` <div class="tool-call-compact-view">
- ${status}
- <span class="tool-call-name">${toolCall.name}</span>
- <div class="done-input-preview">
- ${Object.keys(doneInput.checklist_items).map((key) => {
- const item = doneInput.checklist_items[key];
- let statusIcon = "⛔";
- if (item.status == "yes") {
- statusIcon = "👍";
- } else if (item.status == "not applicable") {
- statusIcon = "🤷♂️";
- }
- return html`<div>
- <span>${statusIcon}</span> ${key}:${item.status}
- </div>`;
- })}
- </div>
- ${cancelButton}
- </div>`;
-
- default: // Generic tool card:
- return html`
- <div class="tool-call-compact-view">
- ${status}
- <span class="tool-call-name">${toolCall.name}</span>
- <code class="tool-call-input-preview">${toolCall.input}</code>
- ${cancelButton}
- <code class="tool-call-result-preview"
- >${toolCall.result_message?.tool_result}</code
- >
- </div>
- ${toolCall.result_message?.tool_result}
- `;
+ return html`<sketch-tool-card-done
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-done>`;
+ case "patch":
+ return html`<sketch-tool-card-patch
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-patch>`;
+ case "think":
+ return html`<sketch-tool-card-think
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-think>`;
+ case "title":
+ return html`<sketch-tool-card-title
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-title>`;
}
+ return html`<sketch-tool-card-generic
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-generic>`;
}
+
render() {
- return html` <div class="tool-calls-container">
+ return html`<div class="tool-calls-container">
<div class="tool-calls-header"></div>
<div class="tool-call-cards-container">
- ${this.toolCalls?.map((toolCall) => {
- return html`<div class="tool-call-card ${toolCall.name}">
- ${this.toolCard(toolCall)}
+ ${this.toolCalls?.map((toolCall, idx) => {
+ let lastCall = false;
+ if (idx == this.toolCalls?.length - 1) {
+ lastCall = true;
+ }
+ return html`<div
+ id="${toolCall.tool_call_id}"
+ class="tool-call-card ${toolCall.name}"
+ >
+ ${this.cardForToolCall(toolCall, lastCall)}
</div>`;
})}
</div>
diff --git a/loop/webui/src/web-components/sketch-tool-card.ts b/loop/webui/src/web-components/sketch-tool-card.ts
new file mode 100644
index 0000000..800c665
--- /dev/null
+++ b/loop/webui/src/web-components/sketch-tool-card.ts
@@ -0,0 +1,592 @@
+import { css, html, LitElement } from "lit";
+import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { repeat } from "lit/directives/repeat.js";
+import { customElement, property } from "lit/decorators.js";
+import { State, ToolCall } from "../types";
+import { marked, MarkedOptions } from "marked";
+
+function renderMarkdown(markdownContent: string): string {
+ try {
+ // Set markdown options for proper code block highlighting and safety
+ const markedOptions: MarkedOptions = {
+ gfm: true, // GitHub Flavored Markdown
+ breaks: true, // Convert newlines to <br>
+ async: false,
+ // DOMPurify is recommended for production, but not included in this implementation
+ };
+ return marked.parse(markdownContent, markedOptions) as string;
+ } catch (error) {
+ console.error("Error rendering markdown:", error);
+ // Fallback to plain text if markdown parsing fails
+ return markdownContent;
+ }
+}
+
+@customElement("sketch-tool-card")
+export class SketchToolCard extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css`
+ .tool-call {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ white-space: nowrap;
+ }
+
+ .tool-call-status {
+ margin-right: 4px;
+ text-align: center;
+ }
+
+ .tool-call-status.spinner {
+ animation: spin 1s infinite linear;
+ display: inline-block;
+ width: 1em;
+ }
+
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
+ .title {
+ font-style: italic;
+ }
+
+ .cancel-button {
+ background: rgb(76, 175, 80);
+ color: white;
+ border: none;
+ padding: 4px 10px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ margin: 5px;
+ }
+
+ .cancel-button:hover {
+ background: rgb(200, 35, 51) !important;
+ }
+
+ .codereview-OK {
+ color: green;
+ }
+
+ details {
+ border-radius: 4px;
+ padding: 0.25em;
+ margin: 0.25em;
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ }
+
+ details summary {
+ list-style: none;
+ &::before {
+ cursor: hand;
+ font-family: monospace;
+ content: "+";
+ color: white;
+ background-color: darkgray;
+ border-radius: 1em;
+ padding-left: 0.5em;
+ margin: 0.25em;
+ min-width: 1em;
+ }
+ [open] &::before {
+ content: "-";
+ }
+ }
+
+ details summary:hover {
+ list-style: none;
+ &::before {
+ background-color: gray;
+ }
+ }
+ summary {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: baseline;
+ }
+
+ summary .tool-name {
+ font-family: monospace;
+ color: white;
+ background: rgb(124 145 160);
+ border-radius: 4px;
+ padding: 0.25em;
+ margin: 0.25em;
+ white-space: pre;
+ }
+
+ .summary-text {
+ padding: 0.25em;
+ display: flex;
+ max-width: 50%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ details[open] .summary-text {
+ /*display: none;*/
+ }
+
+ .tool-error-message {
+ font-style: italic;
+ color: #aa0909;
+ }
+ `;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
+ console.log("cancelToolCall", tool_call_id, button);
+ button.innerText = "Cancelling";
+ button.disabled = true;
+ try {
+ const response = await fetch("cancel", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ tool_call_id: tool_call_id,
+ reason: "user requested cancellation",
+ }),
+ });
+ if (response.ok) {
+ console.log("cancel", tool_call_id, response);
+ button.parentElement.removeChild(button);
+ } else {
+ button.innerText = "Cancel";
+ console.log(`error trying to cancel ${tool_call_id}: `, response);
+ }
+ } catch (e) {
+ console.error("cancel", tool_call_id, e);
+ }
+ };
+
+ render() {
+ const toolCallStatus = this.toolCall?.result_message
+ ? this.toolCall?.result_message.tool_error
+ ? html`❌
+ <span class="tool-error-message"
+ >${this.toolCall?.result_message.tool_error}</span
+ >`
+ : ""
+ : "⏳";
+
+ const cancelButton = this.toolCall?.result_message
+ ? ""
+ : html`<button
+ class="cancel-button"
+ title="Cancel this operation"
+ @click=${(e: Event) => {
+ e.stopPropagation();
+ const button = e.target as HTMLButtonElement;
+ this._cancelToolCall(this.toolCall?.tool_call_id, button);
+ }}
+ >
+ Cancel
+ </button>`;
+
+ const status = html`<span
+ class="tool-call-status ${this.toolCall?.result_message ? "" : "spinner"}"
+ >${toolCallStatus}</span
+ >`;
+
+ const ret = html`<div class="tool-call">
+ <details ?open=${this.open}>
+ <summary>
+ <span class="tool-name">${this.toolCall?.name}</span>
+ <span class="summary-text"><slot name="summary"></slot></span>
+ ${status} ${cancelButton}
+ </summary>
+ <slot name="input"></slot>
+ <slot name="result"></slot>
+ </details>
+ </div> `;
+ if (true) {
+ return ret;
+ }
+ }
+}
+
+@customElement("sketch-tool-card-bash")
+export class SketchToolCardBash extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css`
+ pre {
+ background: black;
+ color: white;
+ padding: 0.5em;
+ border-radius: 4px;
+ }
+ .summary-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: monospace;
+ }
+ .input {
+ display: flex;
+ }
+ .input pre {
+ width: 100%;
+ margin-bottom: 0;
+ border-radius: 4px 4px 0 0;
+ }
+ .result pre {
+ margin-top: 0;
+ color: gray;
+ border-radius: 0 0 4px 4px;
+ }
+ `;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ render() {
+ return html`
+ <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">${JSON.parse(this.toolCall?.input)?.command}</span>
+ <div slot="input" class="input"><pre>${JSON.parse(this.toolCall?.input)?.command}</pre></div>
+ ${
+ this.toolCall?.result_message
+ ? html` ${this.toolCall?.result_message.tool_result
+ ? html`<div slot="result" class="result">
+ <pre class="tool-call-result">
+${this.toolCall?.result_message.tool_result}</pre
+ >
+ </div>`
+ : ""}`
+ : ""
+ }</div>
+ </sketch-tool-card>`;
+ }
+}
+
+@customElement("sketch-tool-card-codereview")
+export class SketchToolCardCodeReview extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css``;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+ render() {
+ return html` <sketch-tool-card
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ >
+ <span slot="summary" class="summary-text">
+ ${this.toolCall?.result_message?.tool_result == "OK" ? "✔️" : "⛔"}
+ </span>
+ <div slot="result">
+ <pre>${this.toolCall?.result_message?.tool_result}</pre>
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+@customElement("sketch-tool-card-done")
+export class SketchToolCardDone extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css``;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ render() {
+ const doneInput = JSON.parse(this.toolCall.input);
+ return html` <sketch-tool-card
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ >
+ <span slot="summary" class="summary-text"> </span>
+ <div slot="result">
+ ${Object.keys(doneInput.checklist_items).map((key) => {
+ const item = doneInput.checklist_items[key];
+ let statusIcon = "⛔";
+ if (item.status == "yes") {
+ statusIcon = "👍";
+ } else if (item.status == "not applicable") {
+ statusIcon = "🤷♂️";
+ }
+ return html`<div>
+ <span>${statusIcon}</span> ${key}:${item.status}
+ </div>`;
+ })}
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+@customElement("sketch-tool-card-patch")
+export class SketchToolCardPatch extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css`
+ .summary-text {
+ color: #555;
+ font-family: monospace;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-radius: 3px;
+ }
+ `;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ render() {
+ const patchInput = JSON.parse(this.toolCall?.input);
+ return html` <sketch-tool-card
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ >
+ <span slot="summary" class="summary-text">
+ ${patchInput?.path}: ${patchInput.patches.length}
+ edit${patchInput.patches.length > 1 ? "s" : ""}
+ </span>
+ <div slot="input">
+ ${patchInput.patches.map((patch) => {
+ return html` Patch operation: <b>${patch.operation}</b>
+ <pre>${patch.newText}</pre>`;
+ })}
+ </div>
+ <div slot="result">
+ <pre>${this.toolCall?.result_message?.tool_result}</pre>
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+@customElement("sketch-tool-card-think")
+export class SketchToolCardThink extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css`
+ .thought-bubble {
+ overflow-x: auto;
+ margin-bottom: 3px;
+ font-family: monospace;
+ padding: 3px 5px;
+ background: rgb(236, 236, 236);
+ border-radius: 6px;
+ user-select: text;
+ cursor: text;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ font-size: 13px;
+ line-height: 1.3;
+ }
+ .summary-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: monospace;
+ max-width: 50%;
+ }
+ `;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ render() {
+ return html`
+ <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text"
+ >${JSON.parse(this.toolCall?.input)?.thoughts}</span
+ >
+ <div slot="input" class="thought-bubble">
+ <div class="markdown-content">
+ ${unsafeHTML(
+ renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
+ )}
+ </div>
+ </div>
+ </sketch-tool-card>
+ `;
+ }
+}
+
+@customElement("sketch-tool-card-title")
+export class SketchToolCardTitle extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ static styles = css`
+ .summary-text {
+ font-style: italic;
+ }
+ `;
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ render() {
+ return html`
+ <span class="summary-text"
+ >I've set the title of this sketch to
+ <b>"${JSON.parse(this.toolCall?.input)?.title}"</b></span
+ >
+ `;
+ }
+}
+
+@customElement("sketch-tool-card-generic")
+export class SketchToolCardGeneric extends LitElement {
+ @property()
+ toolCall: ToolCall;
+
+ @property()
+ open: boolean;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ render() {
+ return html` <sketch-tool-card
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ >
+ <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
+ <div slot="input">
+ Input:
+ <pre>${this.toolCall?.input}</pre>
+ </div>
+ <div slot="result">
+ Result:
+ ${this.toolCall?.result_message
+ ? html` ${this.toolCall?.result_message.tool_result
+ ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
+ : ""}`
+ : ""}
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card": SketchToolCard;
+ "sketch-tool-card-generic": SketchToolCardGeneric;
+ "sketch-tool-card-bash": SketchToolCardBash;
+ "sketch-tool-card-codereview": SketchToolCardCodeReview;
+ "sketch-tool-card-done": SketchToolCardDone;
+ "sketch-tool-card-patch": SketchToolCardPatch;
+ "sketch-tool-card-think": SketchToolCardThink;
+ "sketch-tool-card-title": SketchToolCardTitle;
+ }
+}