loop/webui: add prettier
diff --git a/loop/webui/src/data.ts b/loop/webui/src/data.ts
index 2130c21..9eea954 100644
--- a/loop/webui/src/data.ts
+++ b/loop/webui/src/data.ts
@@ -4,12 +4,12 @@
/**
* Event types for data manager
*/
-export type DataManagerEventType = 'dataChanged' | 'connectionStatusChanged';
+export type DataManagerEventType = "dataChanged" | "connectionStatusChanged";
/**
* Connection status types
*/
-export type ConnectionStatus = 'connected' | 'disconnected' | 'disabled';
+export type ConnectionStatus = "connected" | "disconnected" | "disabled";
/**
* State interface
@@ -44,14 +44,17 @@
private connectionStatus: ConnectionStatus = "disabled";
private messages: TimelineMessage[] = [];
private timelineState: TimelineState | null = null;
-
+
// Event listeners
- private eventListeners: Map<DataManagerEventType, Array<(...args: any[]) => void>> = new Map();
+ private eventListeners: Map<
+ DataManagerEventType,
+ Array<(...args: any[]) => void>
+ > = new Map();
constructor() {
// Initialize empty arrays for each event type
- this.eventListeners.set('dataChanged', []);
- this.eventListeners.set('connectionStatusChanged', []);
+ this.eventListeners.set("dataChanged", []);
+ this.eventListeners.set("connectionStatusChanged", []);
}
/**
@@ -108,7 +111,10 @@
/**
* Add an event listener
*/
- public addEventListener(event: DataManagerEventType, callback: (...args: any[]) => void): void {
+ public addEventListener(
+ event: DataManagerEventType,
+ callback: (...args: any[]) => void,
+ ): void {
const listeners = this.eventListeners.get(event) || [];
listeners.push(callback);
this.eventListeners.set(event, listeners);
@@ -117,7 +123,10 @@
/**
* Remove an event listener
*/
- public removeEventListener(event: DataManagerEventType, callback: (...args: any[]) => void): void {
+ public removeEventListener(
+ event: DataManagerEventType,
+ callback: (...args: any[]) => void,
+ ): void {
const listeners = this.eventListeners.get(event) || [];
const index = listeners.indexOf(callback);
if (index !== -1) {
@@ -131,7 +140,7 @@
*/
private emitEvent(event: DataManagerEventType, ...args: any[]): void {
const listeners = this.eventListeners.get(event) || [];
- listeners.forEach(callback => callback(...args));
+ listeners.forEach((callback) => callback(...args));
}
/**
@@ -139,7 +148,7 @@
*/
public setPollingEnabled(enabled: boolean): void {
this.isPollingEnabled = enabled;
-
+
if (enabled) {
this.startPolling();
} else {
@@ -152,7 +161,7 @@
*/
public startPolling(): void {
this.stopPolling(); // Stop any existing polling
-
+
// Start long polling
this.longPoll();
}
@@ -166,7 +175,7 @@
this.currentPollController.abort();
this.currentPollController = null;
}
-
+
// If polling is disabled by user, set connection status to disabled
if (!this.isPollingEnabled) {
this.updateConnectionStatus("disabled");
@@ -179,7 +188,7 @@
private updateConnectionStatus(status: ConnectionStatus): void {
if (this.connectionStatus !== status) {
this.connectionStatus = status;
- this.emitEvent('connectionStatusChanged', status);
+ this.emitEvent("connectionStatusChanged", status);
}
}
@@ -297,14 +306,18 @@
this.updateConnectionStatus("disconnected");
// Emit an event that we're disconnected with the error message
- this.emitEvent('connectionStatusChanged', this.connectionStatus, errorMessage);
+ this.emitEvent(
+ "connectionStatusChanged",
+ this.connectionStatus,
+ errorMessage,
+ );
}
}
/**
* Fetch timeline data
*/
- public async fetchData(): Promise<void> {
+ public async fetchData(): Promise<void> {
// If we're already fetching messages, don't start another fetch
if (this.isFetchingMessages) {
console.log("Already fetching messages, skipping request");
@@ -326,7 +339,7 @@
) {
// No new messages, early return
this.isFetchingMessages = false;
- this.emitEvent('dataChanged', { state, newMessages: [] });
+ this.emitEvent("dataChanged", { state, newMessages: [] });
return;
}
@@ -335,7 +348,7 @@
const messagesResponse = await fetch(
`messages?start=${this.nextFetchIndex}`,
);
- const newMessages = await messagesResponse.json() || [];
+ const newMessages = (await messagesResponse.json()) || [];
// Store messages in our array
if (this.nextFetchIndex === 0) {
@@ -363,7 +376,11 @@
}
// Emit an event that data has changed
- this.emitEvent('dataChanged', { state, newMessages, isFirstFetch: this.nextFetchIndex === newMessages.length });
+ this.emitEvent("dataChanged", {
+ state,
+ newMessages,
+ isFirstFetch: this.nextFetchIndex === newMessages.length,
+ });
} catch (error) {
console.error("Error fetching data:", error);
@@ -371,7 +388,11 @@
this.updateConnectionStatus("disconnected");
// Emit an event that we're disconnected
- this.emitEvent('connectionStatusChanged', this.connectionStatus, "Not connected");
+ this.emitEvent(
+ "connectionStatusChanged",
+ this.connectionStatus,
+ "Not connected",
+ );
} finally {
this.isFetchingMessages = false;
}
diff --git a/loop/webui/src/diff2.css b/loop/webui/src/diff2.css
index 5a7ad71..f716a01 100644
--- a/loop/webui/src/diff2.css
+++ b/loop/webui/src/diff2.css
@@ -61,7 +61,7 @@
font-size: 16px;
font-weight: bold;
cursor: pointer;
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
opacity: 0.9;
z-index: 100;
user-select: none;
diff --git a/loop/webui/src/sketch-app-shell.html b/loop/webui/src/sketch-app-shell.html
index 6a9f1c3..8d1a30c 100644
--- a/loop/webui/src/sketch-app-shell.html
+++ b/loop/webui/src/sketch-app-shell.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -12,8 +12,13 @@
overflow-y: auto;
}
body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
- Roboto, sans-serif;
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ sans-serif;
margin: 0;
padding: 0;
color: #333;
diff --git a/loop/webui/src/utils.ts b/loop/webui/src/utils.ts
index ff505f9..b60a0fa 100644
--- a/loop/webui/src/utils.ts
+++ b/loop/webui/src/utils.ts
@@ -42,7 +42,7 @@
for (let i = 0; i < 3; i++) {
// Generate more muted colors by using only part of the range
// and adding a base value to avoid very dark colors
- const value = ((hash >> (i * 8)) & 0xff);
+ const value = (hash >> (i * 8)) & 0xff;
const scaledValue = Math.floor(100 + (value * 100) / 255); // Range 100-200 for more muted colors
color += scaledValue.toString(16).padStart(2, "0");
}
diff --git a/loop/webui/src/web-components/demo/demo.css b/loop/webui/src/web-components/demo/demo.css
index bb9750e..11164a1 100644
--- a/loop/webui/src/web-components/demo/demo.css
+++ b/loop/webui/src/web-components/demo/demo.css
@@ -1,9 +1,14 @@
body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
- Roboto, sans-serif;
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ sans-serif;
margin: 0;
padding: 20px;
padding-bottom: 100px; /* Adjusted padding for chat container */
color: #333;
line-height: 1.4; /* Reduced line height for more compact text */
-}
\ No newline at end of file
+}
diff --git a/loop/webui/src/web-components/demo/sketch-app-shell.demo.html b/loop/webui/src/web-components/demo/sketch-app-shell.demo.html
index 092ad7c..ef335ed 100644
--- a/loop/webui/src/web-components/demo/sketch-app-shell.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-app-shell.demo.html
@@ -2,12 +2,14 @@
<head>
<title>sketch-app-shell demo</title>
<link rel="stylesheet" href="demo.css" />
- <script src="/dist/web-components/sketch-app-shell.js" type="module"></script>
+ <script
+ src="/dist/web-components/sketch-app-shell.js"
+ type="module"
+ ></script>
</head>
<body>
<h1>sketch-app-shell demo</h1>
<sketch-app-shell></sketch-app-shell>
-
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/loop/webui/src/web-components/demo/sketch-charts.demo.html b/loop/webui/src/web-components/demo/sketch-charts.demo.html
index b525785..d9b714d 100644
--- a/loop/webui/src/web-components/demo/sketch-charts.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-charts.demo.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
<html>
<head>
<meta charset="utf-8" />
@@ -12,8 +12,13 @@
}
body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
- Roboto, sans-serif;
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ sans-serif;
}
</style>
</head>
diff --git a/loop/webui/src/web-components/demo/sketch-chat-input.demo.html b/loop/webui/src/web-components/demo/sketch-chat-input.demo.html
index 4806035..99d581b 100644
--- a/loop/webui/src/web-components/demo/sketch-chat-input.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-chat-input.demo.html
@@ -2,24 +2,26 @@
<head>
<title>sketch-chat-input demo</title>
<link rel="stylesheet" href="demo.css" />
- <script src="/dist/web-components/sketch-chat-input.js" type="module"></script>
+ <script
+ src="/dist/web-components/sketch-chat-input.js"
+ type="module"
+ ></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
- const chatInput = document.querySelector('sketch-chat-input');
+ const chatInput = document.querySelector("sketch-chat-input");
console.log("chatInput: ", chatInput);
chatInput.content = "hi";
chatInput.addEventListener("send-chat", (evt) => {
- console.log('send chat event: ', evt);
+ console.log("send chat event: ", evt);
const msgDiv = document.querySelector("#chat-messages");
const newDiv = document.createElement("div");
- newDiv.innerText = evt.detail.message;
+ newDiv.innerText = evt.detail.message;
msgDiv.append(newDiv);
- chatInput.content = '';
+ chatInput.content = "";
});
});
</script>
-
</head>
<body>
<h1>sketch-chat-input demo</h1>
@@ -27,6 +29,5 @@
<div id="chat-messages"></div>
<sketch-chat-input></sketch-chat-input>
-
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/loop/webui/src/web-components/demo/sketch-container-status.demo.html b/loop/webui/src/web-components/demo/sketch-container-status.demo.html
index bd2544d..a35e881 100644
--- a/loop/webui/src/web-components/demo/sketch-container-status.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-container-status.demo.html
@@ -2,30 +2,32 @@
<head>
<title>sketch-container-status demo</title>
<link rel="stylesheet" href="demo.css" />
- <script src="/dist/web-components/sketch-container-status.js" type="module"></script>
+ <script
+ src="/dist/web-components/sketch-container-status.js"
+ type="module"
+ ></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
- const containerStatus = document.querySelector('#status-2');
+ const containerStatus = document.querySelector("#status-2");
containerStatus.state = {
- hostname: 'example.hostname',
- initial_commit: 'decafbad',
+ hostname: "example.hostname",
+ initial_commit: "decafbad",
message_count: 27,
- os: 'linux',
+ os: "linux",
total_usage: {
- start_time: 'around lunch',
- messages:1337,
+ start_time: "around lunch",
+ messages: 1337,
input_tokens: 3,
output_tokens: 1000,
cache_read_input_tokens: 28,
cache_creation_input_tokens: 12354,
total_cost_usd: 2.03,
- },
- working_dir: '/app',
+ },
+ working_dir: "/app",
};
});
</script>
-
</head>
<body>
<h1>sketch-container-status demo</h1>
@@ -35,6 +37,5 @@
With state fields set:
<sketch-container-status id="status-2"></sketch-container-status>
-
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/loop/webui/src/web-components/demo/sketch-diff-view.demo.html b/loop/webui/src/web-components/demo/sketch-diff-view.demo.html
index 63b5395..3a6cb35 100644
--- a/loop/webui/src/web-components/demo/sketch-diff-view.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-diff-view.demo.html
@@ -1,61 +1,69 @@
-<!DOCTYPE html>
+<!doctype html>
<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Sketch Diff Viewer Demo</title>
- <link rel="stylesheet" href="../../../node_modules/diff2html/bundles/css/diff2html.min.css">
- <script type="module" src="/dist/web-components/sketch-diff-view.js"></script>
- <style>
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
- }
-
- h1 {
- color: #333;
- margin-bottom: 2rem;
- }
-
- .control-panel {
- margin-bottom: 2rem;
- padding: 1rem;
- background-color: #f0f0f0;
- border-radius: 4px;
- }
-
- input {
- padding: 0.5rem;
- border-radius: 4px;
- border: 1px solid #ccc;
- width: 300px;
- }
-
- button {
- padding: 0.5rem 1rem;
- background-color: #2196f3;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- margin-left: 1rem;
- }
-
- button:hover {
- background-color: #0d8bf2;
- }
- </style>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Sketch Diff Viewer Demo</title>
+ <link
+ rel="stylesheet"
+ href="../../../node_modules/diff2html/bundles/css/diff2html.min.css"
+ />
+ <script
+ type="module"
+ src="/dist/web-components/sketch-diff-view.js"
+ ></script>
+ <style>
+ body {
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
+ Arial, sans-serif;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+ }
-<script>
- document.addEventListener('DOMContentLoaded', () => {
- const diffViewer = document.getElementById('diffViewer');
- const commitHashInput = document.getElementById('commitHash');
- const viewDiffButton = document.getElementById('viewDiff');
- let commit = false;
- viewDiffButton.addEventListener('click', () => {
- let diffContent = `diff --git a/sample.txt b/sample.txt
+ h1 {
+ color: #333;
+ margin-bottom: 2rem;
+ }
+
+ .control-panel {
+ margin-bottom: 2rem;
+ padding: 1rem;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+ }
+
+ input {
+ padding: 0.5rem;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ width: 300px;
+ }
+
+ button {
+ padding: 0.5rem 1rem;
+ background-color: #2196f3;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-left: 1rem;
+ }
+
+ button:hover {
+ background-color: #0d8bf2;
+ }
+ </style>
+
+ <script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const diffViewer = document.getElementById("diffViewer");
+ const commitHashInput = document.getElementById("commitHash");
+ const viewDiffButton = document.getElementById("viewDiff");
+ let commit = false;
+ viewDiffButton.addEventListener("click", () => {
+ let diffContent = `diff --git a/sample.txt b/sample.txt
index 1111111..2222222 100644
--- a/sample.txt
+++ b/sample.txt
@@ -67,12 +75,12 @@
-Another line to remove
+A completely new line
The last line is unchanged`;
- if (commit) {
+ if (commit) {
// For demo purposes, generate fake diff based on commit hash
- diffContent = `diff --git a/file-${commit.substring(0,5)}.txt b/file-${commit.substring(0,5)}.txt
+ diffContent = `diff --git a/file-${commit.substring(0, 5)}.txt b/file-${commit.substring(0, 5)}.txt
index 3333333..4444444 100644
---- a/file-${commit.substring(0,5)}.txt
-+++ b/file-${commit.substring(0,5)}.txt
+--- a/file-${commit.substring(0, 5)}.txt
++++ b/file-${commit.substring(0, 5)}.txt
@@ -1,4 +1,6 @@
File with commit: ${commit}
+This line was added in commit ${commit}
@@ -82,23 +90,23 @@
+Another new line added in this commit
Last line of the file`;
}
- diffViewer.diffText = diffContent;
- diffViewer.commitHash = commitHashInput.value.trim();
- });
- });
-</script>
+ diffViewer.diffText = diffContent;
+ diffViewer.commitHash = commitHashInput.value.trim();
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <h1>Sketch Diff Viewer Demo</h1>
-</head>
-<body>
- <h1>Sketch Diff Viewer Demo</h1>
-
- <div class="control-panel">
- <label for="commitHash">Commit Hash (leave empty for unstaged changes):</label>
- <input type="text" id="commitHash" placeholder="Enter commit hash">
- <button id="viewDiff">View Diff</button>
- </div>
-
- <sketch-diff-view id="diffViewer"></sketch-diff-view>
-
-</body>
-</html>
\ No newline at end of file
+ <div class="control-panel">
+ <label for="commitHash"
+ >Commit Hash (leave empty for unstaged changes):</label
+ >
+ <input type="text" id="commitHash" placeholder="Enter commit hash" />
+ <button id="viewDiff">View Diff</button>
+ </div>
+
+ <sketch-diff-view id="diffViewer"></sketch-diff-view>
+ </body>
+</html>
diff --git a/loop/webui/src/web-components/demo/sketch-network-status.demo.html b/loop/webui/src/web-components/demo/sketch-network-status.demo.html
index 9926b0e..d645840 100644
--- a/loop/webui/src/web-components/demo/sketch-network-status.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-network-status.demo.html
@@ -2,16 +2,24 @@
<head>
<title>sketch-network-status demo</title>
<link rel="stylesheet" href="demo.css" />
- <script src="/dist/web-components/sketch-network-status.js" type="module"></script>
+ <script
+ src="/dist/web-components/sketch-network-status.js"
+ type="module"
+ ></script>
</head>
<body>
<h1>sketch-network-status demo</h1>
Connected:
- <sketch-network-status connection="connected" message="connected"></sketch-network-status>
+ <sketch-network-status
+ connection="connected"
+ message="connected"
+ ></sketch-network-status>
Error:
- <sketch-network-status connection="error" error="error"></sketch-network-status>
-
+ <sketch-network-status
+ connection="error"
+ error="error"
+ ></sketch-network-status>
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html b/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html
index 466f910..cb2bdf3 100644
--- a/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html
@@ -53,10 +53,9 @@
messageEl.message = msg;
document.body.appendChild(messageEl);
});
- window.addEventListener(
- "show-commit-diff",
- (evt) => {console.log("show-commit-diff", evt)}
- );
+ window.addEventListener("show-commit-diff", (evt) => {
+ console.log("show-commit-diff", evt);
+ });
});
</script>
</head>
diff --git a/loop/webui/src/web-components/demo/sketch-timeline.demo.html b/loop/webui/src/web-components/demo/sketch-timeline.demo.html
index 427181d..f8b7ad4 100644
--- a/loop/webui/src/web-components/demo/sketch-timeline.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-timeline.demo.html
@@ -2,7 +2,10 @@
<head>
<title>sketch-timeline demo</title>
<link rel="stylesheet" href="demo.css" />
- <script src="/dist/web-components/sketch-timeline.js" type="module"></script>
+ <script
+ src="/dist/web-components/sketch-timeline.js"
+ type="module"
+ ></script>
<script>
const messages = [
@@ -32,16 +35,14 @@
},
];
document.addEventListener("DOMContentLoaded", () => {
- const timelineEl = document.querySelector('sketch-timeline');
+ const timelineEl = document.querySelector("sketch-timeline");
timelineEl.messages = messages;
});
</script>
-
</head>
<body>
<h1>sketch-timeline demo</h1>
<sketch-timeline></sketch-timeline>
-
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html b/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
index dac6831..0d71067 100644
--- a/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
@@ -3,29 +3,32 @@
<title>sketch-view-mode-select demo</title>
<link rel="stylesheet" href="demo.css" />
- <script src="/dist/web-components/sketch-view-mode-select.js" type="module"></script>
+ <script
+ src="/dist/web-components/sketch-view-mode-select.js"
+ type="module"
+ ></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
- const viewModeSelect = document.querySelector('sketch-view-mode-select');
+ const viewModeSelect = document.querySelector(
+ "sketch-view-mode-select",
+ );
const msgDiv = document.querySelector("#selected-mode");
msgDiv.innerText = `selected mode: ${viewModeSelect.activeMode}`;
console.log("viewModeSelect: ", viewModeSelect);
viewModeSelect.addEventListener("view-mode-select", (evt) => {
- console.log('view mode change event: ', evt);
+ console.log("view mode change event: ", evt);
const msgDiv = document.querySelector("#selected-mode");
msgDiv.innerText = `selected mode: ${evt.detail.mode}`;
});
});
- </script>
-
+ </script>
</head>
<body>
<h1>sketch-view-mode-select demo</h1>
<sketch-view-mode-select></sketch-view-mode-select>
<div id="selected-mode"></div>
-
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/loop/webui/src/web-components/sketch-app-shell.ts b/loop/webui/src/web-components/sketch-app-shell.ts
index 2c7b111..4dcb251 100644
--- a/loop/webui/src/web-components/sketch-app-shell.ts
+++ b/loop/webui/src/web-components/sketch-app-shell.ts
@@ -36,8 +36,13 @@
static styles = css`
:host {
display: block;
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
- Roboto, sans-serif;
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ sans-serif;
color: #333;
line-height: 1.4;
min-height: 100vh;
@@ -216,32 +221,30 @@
this.toggleViewMode(mode as ViewMode, false);
// Add popstate event listener to handle browser back/forward navigation
- window.addEventListener(
- "popstate",
- this._handlePopState as EventListener);
+ window.addEventListener("popstate", this._handlePopState as EventListener);
// Add event listeners
window.addEventListener(
"view-mode-select",
- this._handleViewModeSelect as EventListener
+ this._handleViewModeSelect as EventListener,
);
window.addEventListener(
"diff-comment",
- this._handleDiffComment as EventListener
+ this._handleDiffComment as EventListener,
);
window.addEventListener(
"show-commit-diff",
- this._handleShowCommitDiff as EventListener
+ this._handleShowCommitDiff as EventListener,
);
// register event listeners
this.dataManager.addEventListener(
"dataChanged",
- this.handleDataChanged.bind(this)
+ this.handleDataChanged.bind(this),
);
this.dataManager.addEventListener(
"connectionStatusChanged",
- this.handleConnectionStatusChanged.bind(this)
+ this.handleConnectionStatusChanged.bind(this),
);
// Initialize the data manager
@@ -253,30 +256,31 @@
super.disconnectedCallback();
window.removeEventListener(
"popstate",
- this._handlePopState as EventListener);
+ this._handlePopState as EventListener,
+ );
// Remove event listeners
window.removeEventListener(
"view-mode-select",
- this._handleViewModeSelect as EventListener
+ this._handleViewModeSelect as EventListener,
);
window.removeEventListener(
"diff-comment",
- this._handleDiffComment as EventListener
+ this._handleDiffComment as EventListener,
);
window.removeEventListener(
"show-commit-diff",
- this._handleShowCommitDiff as EventListener
+ this._handleShowCommitDiff as EventListener,
);
// unregister data manager event listeners
this.dataManager.removeEventListener(
"dataChanged",
- this.handleDataChanged.bind(this)
+ this.handleDataChanged.bind(this),
);
this.dataManager.removeEventListener(
"connectionStatusChanged",
- this.handleConnectionStatusChanged.bind(this)
+ this.handleConnectionStatusChanged.bind(this),
);
// Disconnect mutation observer if it exists
@@ -287,9 +291,7 @@
}
}
- updateUrlForViewMode(
- mode: "chat" | "diff" | "charts" | "terminal"
- ): void {
+ updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
// Get the current URL without search parameters
const url = new URL(window.location.href);
@@ -299,7 +301,9 @@
// Only add view parameter if not in default chat view
if (mode !== "chat") {
url.searchParams.set("view", mode);
- const diffView = this.shadowRoot?.querySelector(".diff-view") as SketchDiffView;
+ const diffView = this.shadowRoot?.querySelector(
+ ".diff-view",
+ ) as SketchDiffView;
// If in diff view and there's a commit hash, include that too
if (mode === "diff" && diffView.commitHash) {
@@ -376,7 +380,7 @@
this.currentCommitHash = commitHash;
// Switch to diff view
- this.toggleViewMode("diff", true);
+ this.toggleViewMode("diff", true);
// Wait for DOM update to complete
this.updateComplete.then(() => {
@@ -452,7 +456,7 @@
// Update view mode buttons
const viewModeSelect = this.shadowRoot?.querySelector(
- "sketch-view-mode-select"
+ "sketch-view-mode-select",
);
if (viewModeSelect) {
const event = new CustomEvent("update-active-mode", {
@@ -473,7 +477,7 @@
mergeAndDedupe(
arr1: TimelineMessage[],
- arr2: TimelineMessage[]
+ arr2: TimelineMessage[],
): TimelineMessage[] {
const mergedArray = [...arr1, ...arr2];
const seenIds = new Set<number>();
@@ -542,14 +546,14 @@
// Log information about the message update
if (this.messages.length > oldMessageCount) {
console.log(
- `Auto-scroll: Messages updated from ${oldMessageCount} to ${this.messages.length}, shouldScroll=${this.shouldScrollToBottom}`
+ `Auto-scroll: Messages updated from ${oldMessageCount} to ${this.messages.length}, shouldScroll=${this.shouldScrollToBottom}`,
);
}
}
private handleConnectionStatusChanged(
status: ConnectionStatus,
- errorMessage?: string
+ errorMessage?: string,
): void {
this.connectionStatus = status;
this.connectionErrorMessage = errorMessage || "";
@@ -604,7 +608,7 @@
// Update the timeline component's scroll state
const timeline = this.shadowRoot?.querySelector(
- "sketch-timeline"
+ "sketch-timeline",
) as any;
if (timeline && timeline.setShouldScrollToLatest) {
timeline.setShouldScrollToLatest(true);
@@ -746,10 +750,12 @@
// Initial scroll to bottom when component is first rendered
setTimeout(
() => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
- 50
+ 50,
);
- const pollToggleCheckbox = this.renderRoot?.querySelector("#pollToggle") as HTMLInputElement;
+ const pollToggleCheckbox = this.renderRoot?.querySelector(
+ "#pollToggle",
+ ) as HTMLInputElement;
pollToggleCheckbox?.addEventListener("change", () => {
this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
if (!pollToggleCheckbox.checked) {
diff --git a/loop/webui/src/web-components/sketch-charts.ts b/loop/webui/src/web-components/sketch-charts.ts
index 3bde418..a933c44 100644
--- a/loop/webui/src/web-components/sketch-charts.ts
+++ b/loop/webui/src/web-components/sketch-charts.ts
@@ -77,7 +77,7 @@
}
private calculateCumulativeCostData(
- messages: TimelineMessage[]
+ messages: TimelineMessage[],
): { timestamp: Date; cost: number }[] {
if (!messages || messages.length === 0) {
return [];
@@ -124,14 +124,14 @@
// Create unique indexes for all messages
const messageIndexMap = new Map<string, number>();
let messageIdx = 0;
-
+
// First pass: Process parent messages
allMessages.forEach((msg, index) => {
// Create a unique ID for each message to track its position
const msgId = msg.timestamp ? msg.timestamp.toString() : `msg-${index}`;
messageIndexMap.set(msgId, messageIdx++);
});
-
+
// Process tool calls from messages to account for filtered out tool messages
const toolCallData: any[] = [];
allMessages.forEach((msg) => {
@@ -140,35 +140,35 @@
if (toolCall.result_message) {
// Add this tool result message to our data
const resultMsg = toolCall.result_message;
-
+
// Important: use the original message's idx to maintain the correct order
// The original message idx value is what we want to show in the chart
if (resultMsg.idx !== undefined) {
// If the tool call has start/end times, add it to bar data, otherwise to point data
if (resultMsg.start_time && resultMsg.end_time) {
toolCallData.push({
- type: 'bar',
- index: resultMsg.idx, // Use actual idx from message
- message_type: 'tool',
- content: resultMsg.content || '',
- tool_name: resultMsg.tool_name || toolCall.name || '',
- tool_input: toolCall.input || '',
- tool_result: resultMsg.tool_result || '',
+ type: "bar",
+ index: resultMsg.idx, // Use actual idx from message
+ message_type: "tool",
+ content: resultMsg.content || "",
+ tool_name: resultMsg.tool_name || toolCall.name || "",
+ tool_input: toolCall.input || "",
+ tool_result: resultMsg.tool_result || "",
start_time: new Date(resultMsg.start_time).toISOString(),
end_time: new Date(resultMsg.end_time).toISOString(),
- message: JSON.stringify(resultMsg, null, 2)
+ message: JSON.stringify(resultMsg, null, 2),
});
} else if (resultMsg.timestamp) {
toolCallData.push({
- type: 'point',
- index: resultMsg.idx, // Use actual idx from message
- message_type: 'tool',
- content: resultMsg.content || '',
- tool_name: resultMsg.tool_name || toolCall.name || '',
- tool_input: toolCall.input || '',
- tool_result: resultMsg.tool_result || '',
+ type: "point",
+ index: resultMsg.idx, // Use actual idx from message
+ message_type: "tool",
+ content: resultMsg.content || "",
+ tool_name: resultMsg.tool_name || toolCall.name || "",
+ tool_input: toolCall.input || "",
+ tool_result: resultMsg.tool_result || "",
time: new Date(resultMsg.timestamp).toISOString(),
- message: JSON.stringify(resultMsg, null, 2)
+ message: JSON.stringify(resultMsg, null, 2),
});
}
}
@@ -262,21 +262,29 @@
message: JSON.stringify(msg, null, 2), // Full message for detailed inspection
};
});
-
+
// Add tool call data to the appropriate arrays
- const toolBarData = toolCallData.filter(d => d.type === 'bar').map(d => {
- delete d.type;
- return d;
- });
-
- const toolPointData = toolCallData.filter(d => d.type === 'point').map(d => {
- delete d.type;
- return d;
- });
+ const toolBarData = toolCallData
+ .filter((d) => d.type === "bar")
+ .map((d) => {
+ delete d.type;
+ return d;
+ });
+
+ const toolPointData = toolCallData
+ .filter((d) => d.type === "point")
+ .map((d) => {
+ delete d.type;
+ return d;
+ });
// Check if we have any data to display
- if (barData.length === 0 && pointData.length === 0 &&
- toolBarData.length === 0 && toolPointData.length === 0) {
+ if (
+ barData.length === 0 &&
+ pointData.length === 0 &&
+ toolBarData.length === 0 &&
+ toolPointData.length === 0
+ ) {
return null;
}
@@ -412,16 +420,16 @@
<div class="chart-section">
<h3>Dollar Usage Over Time</h3>
<div class="chart-content">
- ${this.chartData.length > 0 ?
- html`<vega-embed .spec=${costSpec}></vega-embed>`
- : html`<p>No cost data available to display.</p>`}
+ ${this.chartData.length > 0
+ ? html`<vega-embed .spec=${costSpec}></vega-embed>`
+ : html`<p>No cost data available to display.</p>`}
</div>
</div>
<div class="chart-section">
<h3>Message Timeline</h3>
<div class="chart-content">
- ${messagesSpec?.data ?
- html`<vega-embed .spec=${messagesSpec}></vega-embed>`
+ ${messagesSpec?.data
+ ? html`<vega-embed .spec=${messagesSpec}></vega-embed>`
: html`<p>No messages available to display.</p>`}
</div>
</div>
diff --git a/loop/webui/src/web-components/sketch-chat-input.test.ts b/loop/webui/src/web-components/sketch-chat-input.test.ts
index 7d93c17..2c5dde3 100644
--- a/loop/webui/src/web-components/sketch-chat-input.test.ts
+++ b/loop/webui/src/web-components/sketch-chat-input.test.ts
@@ -1,4 +1,11 @@
-import { html, fixture, expect, oneEvent, elementUpdated, fixtureCleanup } from "@open-wc/testing";
+import {
+ html,
+ fixture,
+ expect,
+ oneEvent,
+ elementUpdated,
+ fixtureCleanup,
+} from "@open-wc/testing";
import "./sketch-chat-input";
import { SketchChatInput } from "./sketch-chat-input";
@@ -13,7 +20,9 @@
`);
expect(el.content).to.equal("");
- const textarea = el.shadowRoot!.querySelector("#chatInput") as HTMLTextAreaElement;
+ const textarea = el.shadowRoot!.querySelector(
+ "#chatInput",
+ ) as HTMLTextAreaElement;
expect(textarea.value).to.equal("");
});
@@ -24,7 +33,9 @@
`);
expect(el.content).to.equal(testContent);
- const textarea = el.shadowRoot!.querySelector("#chatInput") as HTMLTextAreaElement;
+ const textarea = el.shadowRoot!.querySelector(
+ "#chatInput",
+ ) as HTMLTextAreaElement;
expect(textarea.value).to.equal(testContent);
});
@@ -33,12 +44,14 @@
<sketch-chat-input></sketch-chat-input>
`);
- const textarea = el.shadowRoot!.querySelector("#chatInput") as HTMLTextAreaElement;
+ const textarea = el.shadowRoot!.querySelector(
+ "#chatInput",
+ ) as HTMLTextAreaElement;
const newValue = "New message";
-
+
textarea.value = newValue;
textarea.dispatchEvent(new Event("input"));
-
+
expect(el.content).to.equal(newValue);
});
@@ -48,12 +61,14 @@
<sketch-chat-input .content=${testContent}></sketch-chat-input>
`);
- const button = el.shadowRoot!.querySelector("#sendChatButton") as HTMLButtonElement;
-
+ const button = el.shadowRoot!.querySelector(
+ "#sendChatButton",
+ ) as HTMLButtonElement;
+
// Setup listener for the send-chat event
setTimeout(() => button.click());
const { detail } = await oneEvent(el, "send-chat");
-
+
expect(detail.message).to.equal(testContent);
expect(el.content).to.equal("");
});
@@ -64,21 +79,23 @@
<sketch-chat-input .content=${testContent}></sketch-chat-input>
`);
- const textarea = el.shadowRoot!.querySelector("#chatInput") as HTMLTextAreaElement;
-
+ const textarea = el.shadowRoot!.querySelector(
+ "#chatInput",
+ ) as HTMLTextAreaElement;
+
// Setup listener for the send-chat event
setTimeout(() => {
const enterEvent = new KeyboardEvent("keydown", {
key: "Enter",
bubbles: true,
cancelable: true,
- shiftKey: false
+ shiftKey: false,
});
textarea.dispatchEvent(enterEvent);
});
-
+
const { detail } = await oneEvent(el, "send-chat");
-
+
expect(detail.message).to.equal(testContent);
expect(el.content).to.equal("");
});
@@ -89,26 +106,28 @@
<sketch-chat-input .content=${testContent}></sketch-chat-input>
`);
- const textarea = el.shadowRoot!.querySelector("#chatInput") as HTMLTextAreaElement;
-
+ const textarea = el.shadowRoot!.querySelector(
+ "#chatInput",
+ ) as HTMLTextAreaElement;
+
// Create a flag to track if the event was fired
let eventFired = false;
el.addEventListener("send-chat", () => {
eventFired = true;
});
-
+
// Dispatch the shift+enter keydown event
const shiftEnterEvent = new KeyboardEvent("keydown", {
key: "Enter",
bubbles: true,
cancelable: true,
- shiftKey: true
+ shiftKey: true,
});
textarea.dispatchEvent(shiftEnterEvent);
-
+
// Wait a short time to verify no event was fired
- await new Promise(resolve => setTimeout(resolve, 10));
-
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
expect(eventFired).to.be.false;
expect(el.content).to.equal(testContent);
});
@@ -119,19 +138,21 @@
`);
const newContent = "Updated content";
-
+
// Dispatch the update-content event
const updateEvent = new CustomEvent("update-content", {
detail: { content: newContent },
- bubbles: true
+ bubbles: true,
});
el.dispatchEvent(updateEvent);
-
+
// Wait for the component to update
await elementUpdated(el);
-
+
expect(el.content).to.equal(newContent);
- const textarea = el.shadowRoot!.querySelector("#chatInput") as HTMLTextAreaElement;
+ const textarea = el.shadowRoot!.querySelector(
+ "#chatInput",
+ ) as HTMLTextAreaElement;
expect(textarea.value).to.equal(newContent);
});
});
diff --git a/loop/webui/src/web-components/sketch-chat-input.ts b/loop/webui/src/web-components/sketch-chat-input.ts
index 3e75b52..989a2e6 100644
--- a/loop/webui/src/web-components/sketch-chat-input.ts
+++ b/loop/webui/src/web-components/sketch-chat-input.ts
@@ -79,7 +79,7 @@
// Update the textarea value directly, otherwise it won't update until next render
const textarea = this.shadowRoot?.querySelector(
- "#chatInput"
+ "#chatInput",
) as HTMLTextAreaElement;
if (textarea) {
textarea.value = content;
@@ -94,7 +94,7 @@
// Listen for update-content events
this.addEventListener(
"update-content",
- this._handleUpdateContent as EventListener
+ this._handleUpdateContent as EventListener,
);
}
@@ -105,7 +105,7 @@
// Remove event listeners
this.removeEventListener(
"update-content",
- this._handleUpdateContent as EventListener
+ this._handleUpdateContent as EventListener,
);
}
diff --git a/loop/webui/src/web-components/sketch-container-status.test.ts b/loop/webui/src/web-components/sketch-container-status.test.ts
index 3a898ee..1eae4ee 100644
--- a/loop/webui/src/web-components/sketch-container-status.test.ts
+++ b/loop/webui/src/web-components/sketch-container-status.test.ts
@@ -17,13 +17,15 @@
output_tokens: 2000,
cache_read_input_tokens: 300,
cache_creation_input_tokens: 400,
- total_cost_usd: 0.25
- }
+ total_cost_usd: 0.25,
+ },
};
it("renders with complete state data", async () => {
const el: SketchContainerStatus = await fixture(html`
- <sketch-container-status .state=${mockCompleteState}></sketch-container-status>
+ <sketch-container-status
+ .state=${mockCompleteState}
+ ></sketch-container-status>
`);
// Check that all expected elements exist
@@ -38,15 +40,33 @@
expect(el.shadowRoot!.querySelector("#totalCost")).to.exist;
// Verify content of displayed elements
- expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal("test-host");
- expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal("/test/dir");
- expect(el.shadowRoot!.querySelector("#initialCommit")!.textContent).to.equal("abcdef12"); // Only first 8 chars
- expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal("42");
- expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal("1000");
- expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal("2000");
- expect(el.shadowRoot!.querySelector("#cacheReadInputTokens")!.textContent).to.equal("300");
- expect(el.shadowRoot!.querySelector("#cacheCreationInputTokens")!.textContent).to.equal("400");
- expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal("$0.25");
+ expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal(
+ "test-host",
+ );
+ expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal(
+ "/test/dir",
+ );
+ expect(
+ el.shadowRoot!.querySelector("#initialCommit")!.textContent,
+ ).to.equal("abcdef12"); // Only first 8 chars
+ expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal(
+ "42",
+ );
+ expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal(
+ "1000",
+ );
+ expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal(
+ "2000",
+ );
+ expect(
+ el.shadowRoot!.querySelector("#cacheReadInputTokens")!.textContent,
+ ).to.equal("300");
+ expect(
+ el.shadowRoot!.querySelector("#cacheCreationInputTokens")!.textContent,
+ ).to.equal("400");
+ expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
+ "$0.25",
+ );
});
it("renders with undefined state", async () => {
@@ -56,12 +76,24 @@
// Elements should exist but be empty
expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#initialCommit")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal("$0.00");
+ expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal(
+ "",
+ );
+ expect(
+ el.shadowRoot!.querySelector("#initialCommit")!.textContent,
+ ).to.equal("");
+ expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal(
+ "",
+ );
+ expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal(
+ "",
+ );
+ expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal(
+ "",
+ );
+ expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
+ "$0.00",
+ );
});
it("renders with partial state data", async () => {
@@ -71,24 +103,40 @@
os: "linux",
title: "Partial Test",
total_usage: {
- input_tokens: 500
- }
+ input_tokens: 500,
+ },
};
const el: SketchContainerStatus = await fixture(html`
- <sketch-container-status .state=${partialState as State}></sketch-container-status>
+ <sketch-container-status
+ .state=${partialState as State}
+ ></sketch-container-status>
`);
// Check that elements with data are properly populated
- expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal("partial-host");
- expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal("10");
- expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal("500");
-
+ expect(el.shadowRoot!.querySelector("#hostname")!.textContent).to.equal(
+ "partial-host",
+ );
+ expect(el.shadowRoot!.querySelector("#messageCount")!.textContent).to.equal(
+ "10",
+ );
+ expect(el.shadowRoot!.querySelector("#inputTokens")!.textContent).to.equal(
+ "500",
+ );
+
// Check that elements without data are empty
- expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#initialCommit")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal("");
- expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal("$0.00");
+ expect(el.shadowRoot!.querySelector("#workingDir")!.textContent).to.equal(
+ "",
+ );
+ expect(
+ el.shadowRoot!.querySelector("#initialCommit")!.textContent,
+ ).to.equal("");
+ expect(el.shadowRoot!.querySelector("#outputTokens")!.textContent).to.equal(
+ "",
+ );
+ expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
+ "$0.00",
+ );
});
it("handles cost formatting correctly", async () => {
@@ -97,7 +145,7 @@
{ cost: 0, expected: "$0.00" },
{ cost: 0.1, expected: "$0.10" },
{ cost: 1.234, expected: "$1.23" },
- { cost: 10.009, expected: "$10.01" }
+ { cost: 10.009, expected: "$10.01" },
];
for (const testCase of testCases) {
@@ -105,47 +153,57 @@
...mockCompleteState,
total_usage: {
...mockCompleteState.total_usage,
- total_cost_usd: testCase.cost
- }
+ total_cost_usd: testCase.cost,
+ },
};
const el: SketchContainerStatus = await fixture(html`
- <sketch-container-status .state=${stateWithCost}></sketch-container-status>
+ <sketch-container-status
+ .state=${stateWithCost}
+ ></sketch-container-status>
`);
- expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(testCase.expected);
+ expect(el.shadowRoot!.querySelector("#totalCost")!.textContent).to.equal(
+ testCase.expected,
+ );
}
});
it("truncates commit hash to 8 characters", async () => {
const stateWithLongCommit = {
...mockCompleteState,
- initial_commit: "1234567890abcdef1234567890abcdef12345678"
+ initial_commit: "1234567890abcdef1234567890abcdef12345678",
};
const el: SketchContainerStatus = await fixture(html`
- <sketch-container-status .state=${stateWithLongCommit}></sketch-container-status>
+ <sketch-container-status
+ .state=${stateWithLongCommit}
+ ></sketch-container-status>
`);
- expect(el.shadowRoot!.querySelector("#initialCommit")!.textContent).to.equal("12345678");
+ expect(
+ el.shadowRoot!.querySelector("#initialCommit")!.textContent,
+ ).to.equal("12345678");
});
it("has correct link elements", async () => {
const el: SketchContainerStatus = await fixture(html`
- <sketch-container-status .state=${mockCompleteState}></sketch-container-status>
+ <sketch-container-status
+ .state=${mockCompleteState}
+ ></sketch-container-status>
`);
- const links = Array.from(el.shadowRoot!.querySelectorAll('a'));
+ const links = Array.from(el.shadowRoot!.querySelectorAll("a"));
expect(links.length).to.equal(2);
-
+
// Check for logs link
- const logsLink = links.find(link => link.textContent === 'Logs');
+ const logsLink = links.find((link) => link.textContent === "Logs");
expect(logsLink).to.exist;
- expect(logsLink!.getAttribute('href')).to.equal('logs');
-
+ expect(logsLink!.getAttribute("href")).to.equal("logs");
+
// Check for download link
- const downloadLink = links.find(link => link.textContent === 'Download');
+ const downloadLink = links.find((link) => link.textContent === "Download");
expect(downloadLink).to.exist;
- expect(downloadLink!.getAttribute('href')).to.equal('download');
+ expect(downloadLink!.getAttribute("href")).to.equal("download");
});
});
diff --git a/loop/webui/src/web-components/sketch-container-status.ts b/loop/webui/src/web-components/sketch-container-status.ts
index c0f9626..736e5ef 100644
--- a/loop/webui/src/web-components/sketch-container-status.ts
+++ b/loop/webui/src/web-components/sketch-container-status.ts
@@ -136,7 +136,9 @@
</div>
<div class="info-item">
<span class="info-label">Cost:</span>
- <span id="totalCost" class="info-value cost">$${(this.state?.total_usage?.total_cost_usd || 0).toFixed(2)}</span>
+ <span id="totalCost" class="info-value cost"
+ >$${(this.state?.total_usage?.total_cost_usd || 0).toFixed(2)}</span
+ >
</div>
</div>
`;
diff --git a/loop/webui/src/web-components/sketch-diff-view.ts b/loop/webui/src/web-components/sketch-diff-view.ts
index 562eb1e..6c4feb1 100644
--- a/loop/webui/src/web-components/sketch-diff-view.ts
+++ b/loop/webui/src/web-components/sketch-diff-view.ts
@@ -1,11 +1,11 @@
-import {css, html, LitElement, unsafeCSS} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import { css, html, LitElement, unsafeCSS } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
import * as Diff2Html from "diff2html";
-@customElement('sketch-diff-view')
+@customElement("sketch-diff-view")
export class SketchDiffView extends LitElement {
// Current commit hash being viewed
- @property({type: String})
+ @property({ type: String })
commitHash: string = "";
// Selected line in the diff for commenting
@@ -24,14 +24,14 @@
overflow: hidden;
height: 100%;
}
-
+
.diff-container {
height: 100%;
overflow: auto;
flex: 1;
padding: 0 1rem;
}
-
+
#diff-view-controls {
display: flex;
justify-content: flex-end;
@@ -39,21 +39,21 @@
background: #f8f8f8;
border-bottom: 1px solid #eee;
}
-
+
.diff-view-format {
display: flex;
gap: 10px;
}
-
+
.diff-view-format label {
cursor: pointer;
}
-
+
.diff2html-content {
font-family: var(--monospace-font);
position: relative;
}
-
+
/* Comment box styles */
.diff-comment-box {
position: fixed;
@@ -67,18 +67,18 @@
padding: 16px;
z-index: 1000;
}
-
+
.diff-comment-box h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
-
+
.selected-line {
margin-bottom: 10px;
font-size: 14px;
}
-
+
.selected-line pre {
padding: 6px;
background: #f5f5f5;
@@ -91,7 +91,7 @@
font-size: 13px;
white-space: pre-wrap;
}
-
+
#diffCommentInput {
width: 100%;
height: 100px;
@@ -102,13 +102,13 @@
font-family: inherit;
margin-bottom: 10px;
}
-
+
.diff-comment-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}
-
+
.diff-comment-buttons button {
padding: 6px 12px;
border-radius: 4px;
@@ -116,21 +116,21 @@
background: white;
cursor: pointer;
}
-
+
.diff-comment-buttons button:hover {
background: #f5f5f5;
}
-
+
.diff-comment-buttons button#submitDiffComment {
background: #1a73e8;
color: white;
border-color: #1a73e8;
}
-
+
.diff-comment-buttons button#submitDiffComment:hover {
background: #1967d2;
}
-
+
/* Styles for the comment button on diff lines */
.d2h-gutter-comment-button {
position: absolute;
@@ -149,98 +149,103 @@
color: #666;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
-
+
tr:hover .d2h-gutter-comment-button {
visibility: visible;
}
-
+
.d2h-gutter-comment-button:hover {
background: white;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
`;
-
+
constructor() {
super();
}
-
+
// See https://lit.dev/docs/components/lifecycle/
connectedCallback() {
super.connectedCallback();
-
+
// Load the diff2html CSS if needed
this.loadDiff2HtmlCSS();
}
-
+
// Load diff2html CSS into the shadow DOM
private async loadDiff2HtmlCSS() {
try {
// Check if diff2html styles are already loaded
- const styleId = 'diff2html-styles';
+ const styleId = "diff2html-styles";
if (this.shadowRoot?.getElementById(styleId)) {
return; // Already loaded
}
-
+
// Fetch the diff2html CSS
- const response = await fetch('static/diff2html.min.css');
-
+ const response = await fetch("static/diff2html.min.css");
+
if (!response.ok) {
- console.error(`Failed to load diff2html CSS: ${response.status} ${response.statusText}`);
+ console.error(
+ `Failed to load diff2html CSS: ${response.status} ${response.statusText}`,
+ );
return;
}
-
+
const cssText = await response.text();
-
+
// Create a style element and append to shadow DOM
- const style = document.createElement('style');
+ const style = document.createElement("style");
style.id = styleId;
style.textContent = cssText;
this.shadowRoot?.appendChild(style);
-
- console.log('diff2html CSS loaded into shadow DOM');
+
+ console.log("diff2html CSS loaded into shadow DOM");
} catch (error) {
- console.error('Error loading diff2html CSS:', error);
+ console.error("Error loading diff2html CSS:", error);
}
}
-
+
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
}
-
+
// Method called to load diff content
async loadDiffContent() {
// Wait for the component to be rendered
await this.updateComplete;
-
- const diff2htmlContent = this.shadowRoot?.getElementById("diff2htmlContent");
+
+ const diff2htmlContent =
+ this.shadowRoot?.getElementById("diff2htmlContent");
if (!diff2htmlContent) return;
-
+
try {
// Show loading state
diff2htmlContent.innerHTML = "Loading enhanced diff...";
-
+
// Build the diff URL - include commit hash if specified
- const diffUrl = this.commitHash ? `diff?commit=${this.commitHash}` : "diff";
-
+ const diffUrl = this.commitHash
+ ? `diff?commit=${this.commitHash}`
+ : "diff";
+
// Fetch the diff from the server
const response = await fetch(diffUrl);
-
+
if (!response.ok) {
throw new Error(
`Server returned ${response.status}: ${response.statusText}`,
);
}
-
+
const diffText = await response.text();
-
+
if (!diffText || diffText.trim() === "") {
diff2htmlContent.innerHTML =
"<span style='color: #666; font-style: italic;'>No changes detected since conversation started.</span>";
return;
}
-
+
// Render the diff using diff2html
const diffHtml = Diff2Html.html(diffText, {
outputFormat: this.viewFormat,
@@ -249,10 +254,10 @@
renderNothingWhenEmpty: false,
colorScheme: "light" as any, // Force light mode to match the rest of the UI
});
-
+
// Insert the generated HTML
diff2htmlContent.innerHTML = diffHtml;
-
+
// Add CSS styles to ensure we don't have double scrollbars
const d2hFiles = diff2htmlContent.querySelectorAll(".d2h-file-wrapper");
d2hFiles.forEach((file) => {
@@ -263,10 +268,9 @@
(contentElem as HTMLElement).style.maxHeight = "none";
}
});
-
+
// Add click event handlers to each code line for commenting
this.setupDiffLineComments();
-
} catch (error) {
console.error("Error loading diff2html content:", error);
const errorMessage =
@@ -274,7 +278,7 @@
diff2htmlContent.innerHTML = `<span style='color: #dc3545;'>Error loading enhanced diff: ${errorMessage}</span>`;
}
}
-
+
// Handle view format changes
private handleViewFormatChange(event: Event) {
const input = event.target as HTMLInputElement;
@@ -283,39 +287,42 @@
this.loadDiffContent();
}
}
-
+
/**
* Setup handlers for diff code lines to enable commenting
*/
private setupDiffLineComments(): void {
- const diff2htmlContent = this.shadowRoot?.getElementById("diff2htmlContent");
+ const diff2htmlContent =
+ this.shadowRoot?.getElementById("diff2htmlContent");
if (!diff2htmlContent) return;
-
+
console.log("Setting up diff line comments");
-
+
// Add plus buttons to each code line
this.addCommentButtonsToCodeLines();
-
+
// Use event delegation for handling clicks on plus buttons
diff2htmlContent.addEventListener("click", (event) => {
const target = event.target as HTMLElement;
-
+
// Only respond to clicks on the plus button
if (target.classList.contains("d2h-gutter-comment-button")) {
// Find the parent row first
const row = target.closest("tr");
if (!row) return;
-
+
// Then find the code line in that row
- const codeLine = row.querySelector(".d2h-code-side-line") || row.querySelector(".d2h-code-line");
+ const codeLine =
+ row.querySelector(".d2h-code-side-line") ||
+ row.querySelector(".d2h-code-line");
if (!codeLine) return;
-
+
// Get the line text content
const lineContent = codeLine.querySelector(".d2h-code-line-ctn");
if (!lineContent) return;
-
+
const lineText = lineContent.textContent?.trim() || "";
-
+
// Get file name to add context
const fileHeader = codeLine
.closest(".d2h-file-wrapper")
@@ -323,79 +330,80 @@
const fileName = fileHeader
? fileHeader.textContent?.trim()
: "Unknown file";
-
+
// Get line number if available
const lineNumElem = codeLine
.closest("tr")
?.querySelector(".d2h-code-side-linenumber");
const lineNum = lineNumElem ? lineNumElem.textContent?.trim() : "";
const lineInfo = lineNum ? `Line ${lineNum}: ` : "";
-
+
// Format the line for the comment box with file context and line number
const formattedLine = `${fileName} ${lineInfo}${lineText}`;
-
+
console.log("Comment button clicked for line: ", formattedLine);
-
+
// Open the comment box with this line
this.openDiffCommentBox(formattedLine);
-
+
// Prevent event from bubbling up
event.stopPropagation();
}
});
}
-
+
/**
* Add plus buttons to each table row in the diff for commenting
*/
private addCommentButtonsToCodeLines(): void {
- const diff2htmlContent = this.shadowRoot?.getElementById("diff2htmlContent");
+ const diff2htmlContent =
+ this.shadowRoot?.getElementById("diff2htmlContent");
if (!diff2htmlContent) return;
-
+
// Target code lines first, then find their parent rows
const codeLines = diff2htmlContent.querySelectorAll(
- ".d2h-code-side-line, .d2h-code-line"
+ ".d2h-code-side-line, .d2h-code-line",
);
-
+
// Create a Set to store unique rows to avoid duplicates
const rowsSet = new Set<HTMLElement>();
-
+
// Get all rows that contain code lines
- codeLines.forEach(line => {
- const row = line.closest('tr');
+ codeLines.forEach((line) => {
+ const row = line.closest("tr");
if (row) rowsSet.add(row as HTMLElement);
});
-
+
// Convert Set back to array for processing
const codeRows = Array.from(rowsSet);
-
+
codeRows.forEach((row) => {
const rowElem = row as HTMLElement;
-
+
// Skip info lines without actual code (e.g., "file added")
if (rowElem.querySelector(".d2h-info")) {
return;
}
-
+
// Find the code line number element (first TD in the row)
const lineNumberCell = rowElem.querySelector(
- ".d2h-code-side-linenumber, .d2h-code-linenumber"
+ ".d2h-code-side-linenumber, .d2h-code-linenumber",
);
-
+
if (!lineNumberCell) return;
-
+
// Create the plus button
const plusButton = document.createElement("span");
plusButton.className = "d2h-gutter-comment-button";
plusButton.innerHTML = "+";
plusButton.title = "Add a comment on this line";
-
+
// Add button to the line number cell for proper positioning
(lineNumberCell as HTMLElement).style.position = "relative"; // Ensure positioning context
lineNumberCell.appendChild(plusButton);
});
}
-
+
/**
* Open the comment box for a selected diff line
*/
@@ -403,13 +411,13 @@
// Make sure the comment box div exists
const commentBoxId = "diffCommentBox";
let commentBox = this.shadowRoot?.getElementById(commentBoxId);
-
+
// If it doesn't exist, create it
if (!commentBox) {
commentBox = document.createElement("div");
commentBox.id = commentBoxId;
commentBox.className = "diff-comment-box";
-
+
// Create the comment box contents
commentBox.innerHTML = `
<h3>Add a comment</h3>
@@ -426,49 +434,49 @@
<button id="submitDiffComment">Add Comment</button>
</div>
`;
-
+
this.shadowRoot?.appendChild(commentBox);
}
-
+
// Store the selected line
this.selectedDiffLine = lineText;
-
+
// Display the line in the comment box
const selectedLine = this.shadowRoot?.getElementById("selectedLine");
if (selectedLine) {
selectedLine.textContent = lineText;
}
-
+
// Reset the comment input
const commentInput = this.shadowRoot?.getElementById(
- "diffCommentInput"
+ "diffCommentInput",
) as HTMLTextAreaElement;
if (commentInput) {
commentInput.value = "";
}
-
+
// Show the comment box
if (commentBox) {
commentBox.style.display = "block";
}
-
+
// Add event listeners for submit and cancel buttons
const submitButton = this.shadowRoot?.getElementById("submitDiffComment");
if (submitButton) {
submitButton.onclick = () => this.submitDiffComment();
}
-
+
const cancelButton = this.shadowRoot?.getElementById("cancelDiffComment");
if (cancelButton) {
cancelButton.onclick = () => this.closeDiffCommentBox();
}
-
+
// Focus on the comment input
if (commentInput) {
commentInput.focus();
}
}
-
+
/**
* Close the diff comment box without submitting
*/
@@ -479,45 +487,45 @@
}
this.selectedDiffLine = null;
}
-
+
/**
* Submit a comment on a diff line
*/
private submitDiffComment(): void {
const commentInput = this.shadowRoot?.getElementById(
- "diffCommentInput"
+ "diffCommentInput",
) as HTMLTextAreaElement;
-
+
if (!commentInput) return;
-
+
const comment = commentInput.value.trim();
-
+
// Validate inputs
if (!this.selectedDiffLine || !comment) {
alert("Please select a line and enter a comment.");
return;
}
-
+
// Format the comment in a readable way
const formattedComment = `\`\`\`\n${this.selectedDiffLine}\n\`\`\`\n\n${comment}`;
-
+
// Dispatch a custom event with the formatted comment
- const event = new CustomEvent('diff-comment', {
+ const event = new CustomEvent("diff-comment", {
detail: { comment: formattedComment },
bubbles: true,
- composed: true
+ composed: true,
});
this.dispatchEvent(event);
-
+
// Close only the comment box but keep the diff view open
this.closeDiffCommentBox();
}
-
+
// Clear the current state
public clearState(): void {
this.commitHash = "";
}
-
+
// Show diff for a specific commit
public showCommitDiff(commitHash: string): void {
// Store the commit hash
@@ -525,7 +533,7 @@
// Load the diff content
this.loadDiffContent();
}
-
+
render() {
return html`
<div class="diff-view">
@@ -533,22 +541,24 @@
<div id="diff-view-controls">
<div class="diff-view-format">
<label>
- <input
- type="radio"
- name="diffViewFormat"
- value="side-by-side"
+ <input
+ type="radio"
+ name="diffViewFormat"
+ value="side-by-side"
?checked=${this.viewFormat === "side-by-side"}
@change=${this.handleViewFormatChange}
- > Side-by-side
+ />
+ Side-by-side
</label>
<label>
- <input
- type="radio"
- name="diffViewFormat"
+ <input
+ type="radio"
+ name="diffViewFormat"
value="line-by-line"
?checked=${this.viewFormat === "line-by-line"}
@change=${this.handleViewFormatChange}
- > Line-by-line
+ />
+ Line-by-line
</label>
</div>
</div>
diff --git a/loop/webui/src/web-components/sketch-network-status.test.ts b/loop/webui/src/web-components/sketch-network-status.test.ts
index a580a8f..04e3386 100644
--- a/loop/webui/src/web-components/sketch-network-status.test.ts
+++ b/loop/webui/src/web-components/sketch-network-status.test.ts
@@ -34,7 +34,6 @@
expect(indicator!.classList.contains("error")).to.be.true;
});
-
it("displays the correct connection status when disabled", async () => {
const el: SketchNetworkStatus = await fixture(html`
<sketch-network-status
diff --git a/loop/webui/src/web-components/sketch-network-status.ts b/loop/webui/src/web-components/sketch-network-status.ts
index 4b01e5e..835abb5 100644
--- a/loop/webui/src/web-components/sketch-network-status.ts
+++ b/loop/webui/src/web-components/sketch-network-status.ts
@@ -1,10 +1,10 @@
-import {css, html, LitElement} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {DataManager, ConnectionStatus} from '../data';
-import {State, TimelineMessage} from '../types';
-import './sketch-container-status';
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { DataManager, ConnectionStatus } from "../data";
+import { State, TimelineMessage } from "../types";
+import "./sketch-container-status";
-@customElement('sketch-network-status')
+@customElement("sketch-network-status")
export class SketchNetworkStatus extends LitElement {
// Header bar: view mode buttons
@@ -14,54 +14,54 @@
message: string;
@property()
error: string;
-
+
// 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`
-.status-container {
- display: flex;
- align-items: center;
-}
+ .status-container {
+ display: flex;
+ align-items: center;
+ }
-.polling-indicator {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- margin-right: 4px;
- background-color: #ccc;
-}
+ .polling-indicator {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 4px;
+ background-color: #ccc;
+ }
-.polling-indicator.active {
- background-color: #4caf50;
- animation: pulse 1.5s infinite;
-}
+ .polling-indicator.active {
+ background-color: #4caf50;
+ animation: pulse 1.5s infinite;
+ }
-.polling-indicator.error {
- background-color: #f44336;
- animation: pulse 1.5s infinite;
-}
+ .polling-indicator.error {
+ background-color: #f44336;
+ animation: pulse 1.5s infinite;
+ }
-@keyframes pulse {
- 0% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
- 100% {
- opacity: 1;
- }
-}
+ @keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+ }
-.status-text {
- font-size: 11px;
- color: #666;
-}
-`;
+ .status-text {
+ font-size: 11px;
+ color: #666;
+ }
+ `;
constructor() {
super();
@@ -74,22 +74,27 @@
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
- super.disconnectedCallback();
+ super.disconnectedCallback();
}
indicator() {
if (this.connection === "disabled") {
- return '';
+ return "";
}
- return this.connection === "connected" ? "active": "error";
+ return this.connection === "connected" ? "active" : "error";
}
render() {
return html`
- <div class="status-container">
- <span id="pollingIndicator" class="polling-indicator ${this.indicator()}"></span>
- <span id="statusText" class="status-text">${this.error || this.message}</span>
- </div>
+ <div class="status-container">
+ <span
+ id="pollingIndicator"
+ class="polling-indicator ${this.indicator()}"
+ ></span>
+ <span id="statusText" class="status-text"
+ >${this.error || this.message}</span
+ >
+ </div>
`;
}
}
@@ -98,4 +103,4 @@
interface HTMLElementTagNameMap {
"sketch-network-status": SketchNetworkStatus;
}
-}
\ No newline at end of file
+}
diff --git a/loop/webui/src/web-components/sketch-terminal.ts b/loop/webui/src/web-components/sketch-terminal.ts
index 788521d..106f7e2 100644
--- a/loop/webui/src/web-components/sketch-terminal.ts
+++ b/loop/webui/src/web-components/sketch-terminal.ts
@@ -1,13 +1,13 @@
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
-import {css, html, LitElement} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
-import {DataManager, ConnectionStatus} from '../data';
-import {State, TimelineMessage} from '../types';
-import './sketch-container-status';
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { DataManager, ConnectionStatus } from "../data";
+import { State, TimelineMessage } from "../types";
+import "./sketch-container-status";
-@customElement('sketch-terminal')
+@customElement("sketch-terminal")
export class SketchTerminal extends LitElement {
// Terminal instance
private terminal: Terminal | null = null;
@@ -23,24 +23,24 @@
private processingTerminalInput: boolean = false;
static styles = css`
-/* Terminal View Styles */
-.terminal-view {
- width: 100%;
- background-color: #f5f5f5;
- border-radius: 8px;
- overflow: hidden;
- margin-bottom: 20px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- padding: 15px;
- height: 70vh;
-}
+ /* Terminal View Styles */
+ .terminal-view {
+ width: 100%;
+ background-color: #f5f5f5;
+ border-radius: 8px;
+ overflow: hidden;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ padding: 15px;
+ height: 70vh;
+ }
-.terminal-container {
- width: 100%;
- height: 100%;
- overflow: hidden;
-}
-`;
+ .terminal-container {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ }
+ `;
constructor() {
super();
@@ -84,30 +84,32 @@
private async loadXtermlCSS() {
try {
// Check if diff2html styles are already loaded
- const styleId = 'xterm-styles';
+ const styleId = "xterm-styles";
if (this.shadowRoot?.getElementById(styleId)) {
return; // Already loaded
}
// Fetch the diff2html CSS
- const response = await fetch('static/xterm.css');
+ const response = await fetch("static/xterm.css");
if (!response.ok) {
- console.error(`Failed to load xterm CSS: ${response.status} ${response.statusText}`);
+ console.error(
+ `Failed to load xterm CSS: ${response.status} ${response.statusText}`,
+ );
return;
}
const cssText = await response.text();
// Create a style element and append to shadow DOM
- const style = document.createElement('style');
+ const style = document.createElement("style");
style.id = styleId;
style.textContent = cssText;
this.renderRoot?.appendChild(style);
- console.log('xterm CSS loaded into shadow DOM');
+ console.log("xterm CSS loaded into shadow DOM");
} catch (error) {
- console.error('Error loading xterm CSS:', error);
+ console.error("Error loading xterm CSS:", error);
}
}
@@ -116,7 +118,9 @@
* @param terminalContainer The DOM element to contain the terminal
*/
public async initializeTerminal(): Promise<void> {
- const terminalContainer = this.renderRoot.querySelector("#terminalContainer") as HTMLElement;
+ const terminalContainer = this.renderRoot.querySelector(
+ "#terminalContainer",
+ ) as HTMLElement;
if (!terminalContainer) {
console.error("Terminal container not found");
@@ -177,7 +181,7 @@
try {
// Connect directly to the SSE endpoint for terminal 1
// Use relative URL based on current location
- const baseUrl = window.location.pathname.endsWith('/') ? '.' : '.';
+ const baseUrl = window.location.pathname.endsWith("/") ? "." : ".";
const eventsUrl = `${baseUrl}/terminal/events/${this.terminalId}`;
this.terminalEventSource = new EventSource(eventsUrl);
@@ -194,7 +198,7 @@
const decoded = atob(event.data);
this.terminal.write(decoded);
} catch (e) {
- console.error('Error decoding terminal data:', e);
+ console.error("Error decoding terminal data:", e);
// Fallback to raw data if decoding fails
this.terminal.write(event.data);
}
@@ -221,7 +225,9 @@
} catch (error) {
console.error("Failed to connect to terminal:", error);
if (this.terminal) {
- this.terminal.write(`\r\n\x1b[1;31mFailed to connect: ${error}\x1b[0m\r\n`);
+ this.terminal.write(
+ `\r\n\x1b[1;31mFailed to connect: ${error}\x1b[0m\r\n`,
+ );
}
}
}
@@ -262,7 +268,7 @@
this.processingTerminalInput = true;
// Concatenate all available inputs from the queue into a single request
- let combinedData = '';
+ let combinedData = "";
// Take all currently available items from the queue
while (this.terminalInputQueue.length > 0) {
@@ -271,17 +277,22 @@
try {
// Use relative URL based on current location
- const baseUrl = window.location.pathname.endsWith('/') ? '.' : '.';
- const response = await fetch(`${baseUrl}/terminal/input/${this.terminalId}`, {
- method: 'POST',
- body: combinedData,
- headers: {
- 'Content-Type': 'text/plain'
- }
- });
+ const baseUrl = window.location.pathname.endsWith("/") ? "." : ".";
+ const response = await fetch(
+ `${baseUrl}/terminal/input/${this.terminalId}`,
+ {
+ method: "POST",
+ body: combinedData,
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ },
+ );
if (!response.ok) {
- console.error(`Failed to send terminal input: ${response.status} ${response.statusText}`);
+ console.error(
+ `Failed to send terminal input: ${response.status} ${response.statusText}`,
+ );
}
} catch (error) {
console.error("Error sending terminal input:", error);
@@ -303,32 +314,36 @@
try {
// Send resize message in a format the server can understand
// Use relative URL based on current location
- const baseUrl = window.location.pathname.endsWith('/') ? '.' : '.';
- const response = await fetch(`${baseUrl}/terminal/input/${this.terminalId}`, {
- method: 'POST',
- body: JSON.stringify({
- type: "resize",
- cols: this.terminal.cols || 80, // Default to 80 if undefined
- rows: this.terminal.rows || 24, // Default to 24 if undefined
- }),
- headers: {
- 'Content-Type': 'application/json'
- }
- });
+ const baseUrl = window.location.pathname.endsWith("/") ? "." : ".";
+ const response = await fetch(
+ `${baseUrl}/terminal/input/${this.terminalId}`,
+ {
+ method: "POST",
+ body: JSON.stringify({
+ type: "resize",
+ cols: this.terminal.cols || 80, // Default to 80 if undefined
+ rows: this.terminal.rows || 24, // Default to 24 if undefined
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ },
+ );
if (!response.ok) {
- console.error(`Failed to send terminal resize: ${response.status} ${response.statusText}`);
+ console.error(
+ `Failed to send terminal resize: ${response.status} ${response.statusText}`,
+ );
}
} catch (error) {
console.error("Error sending terminal resize:", error);
}
}
-
render() {
return html`
<div id="terminalView" class="terminal-view">
- <div id="terminalContainer" class="terminal-container"></div>
+ <div id="terminalContainer" class="terminal-container"></div>
</div>
`;
}
@@ -338,4 +353,4 @@
interface HTMLElementTagNameMap {
"sketch-terminal": SketchTerminal;
}
-}
\ No newline at end of file
+}
diff --git a/loop/webui/src/web-components/sketch-timeline-message.test.ts b/loop/webui/src/web-components/sketch-timeline-message.test.ts
index 3f30696..d768f02 100644
--- a/loop/webui/src/web-components/sketch-timeline-message.test.ts
+++ b/loop/webui/src/web-components/sketch-timeline-message.test.ts
@@ -5,7 +5,9 @@
describe("SketchTimelineMessage", () => {
// Helper function to create mock timeline messages
- function createMockMessage(props: Partial<TimelineMessage> = {}): TimelineMessage {
+ function createMockMessage(
+ props: Partial<TimelineMessage> = {},
+ ): TimelineMessage {
return {
idx: props.idx || 0,
type: props.type || "agent",
@@ -17,39 +19,37 @@
tool_calls: props.tool_calls || [],
commits: props.commits || [],
usage: props.usage,
- ...props
+ ...props,
};
}
it("renders with basic message content", async () => {
const message = createMockMessage({
type: "agent",
- content: "This is a test message"
+ content: "This is a test message",
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const messageContent = el.shadowRoot!.querySelector(".message-text");
expect(messageContent).to.exist;
- expect(messageContent!.textContent!.trim()).to.include("This is a test message");
+ expect(messageContent!.textContent!.trim()).to.include(
+ "This is a test message",
+ );
});
it("renders with correct message type classes", async () => {
const messageTypes = ["user", "agent", "tool", "error"];
-
+
for (const type of messageTypes) {
const message = createMockMessage({ type });
-
+
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
-
+
const messageElement = el.shadowRoot!.querySelector(".message");
expect(messageElement).to.exist;
expect(messageElement!.classList.contains(type)).to.be.true;
@@ -58,13 +58,11 @@
it("renders end-of-turn marker correctly", async () => {
const message = createMockMessage({
- end_of_turn: true
+ end_of_turn: true,
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const messageElement = el.shadowRoot!.querySelector(".message");
@@ -74,13 +72,11 @@
it("formats timestamps correctly", async () => {
const message = createMockMessage({
- timestamp: "2023-05-15T12:00:00Z"
+ timestamp: "2023-05-15T12:00:00Z",
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const timestamp = el.shadowRoot!.querySelector(".message-timestamp");
@@ -92,15 +88,14 @@
});
it("renders markdown content correctly", async () => {
- const markdownContent = "# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
+ const markdownContent =
+ "# Heading\n\n- List item 1\n- List item 2\n\n`code block`";
const message = createMockMessage({
- content: markdownContent
+ content: markdownContent,
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const contentElement = el.shadowRoot!.querySelector(".markdown-content");
@@ -116,25 +111,23 @@
input_tokens: 150,
output_tokens: 300,
cost_usd: 0.025,
- cache_read_input_tokens: 50
+ cache_read_input_tokens: 50,
};
-
+
const message = createMockMessage({
- usage
+ usage,
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const usageElement = el.shadowRoot!.querySelector(".message-usage");
expect(usageElement).to.exist;
- expect(usageElement!.textContent).to.include("In: 150");
- expect(usageElement!.textContent).to.include("Out: 300");
- expect(usageElement!.textContent).to.include("Cache: 50");
- expect(usageElement!.textContent).to.include("$0.03");
+ expect(usageElement!.textContent).to.include("150"); // In
+ expect(usageElement!.textContent).to.include("300"); // Out
+ expect(usageElement!.textContent).to.include("50"); // Cache
+ expect(usageElement!.textContent).to.include("$0.03"); // Cost
});
it("renders commit information correctly", async () => {
@@ -143,31 +136,29 @@
hash: "1234567890abcdef",
subject: "Fix bug in application",
body: "This fixes a major bug in the application\n\nSigned-off-by: Developer",
- pushed_branch: "main"
- }
+ pushed_branch: "main",
+ },
];
-
+
const message = createMockMessage({
- commits
+ commits,
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
const commitsContainer = el.shadowRoot!.querySelector(".commits-container");
expect(commitsContainer).to.exist;
-
+
const commitHeader = commitsContainer!.querySelector(".commits-header");
expect(commitHeader).to.exist;
- expect(commitHeader!.textContent).to.include("1 new commit");
-
+ expect(commitHeader!.textContent).to.include("1 new");
+
const commitHash = commitsContainer!.querySelector(".commit-hash");
expect(commitHash).to.exist;
expect(commitHash!.textContent).to.equal("12345678"); // First 8 chars
-
+
const pushedBranch = commitsContainer!.querySelector(".pushed-branch");
expect(pushedBranch).to.exist;
expect(pushedBranch!.textContent).to.include("main");
@@ -179,27 +170,27 @@
hash: "1234567890abcdef",
subject: "Fix bug in application",
body: "This fixes a major bug in the application",
- pushed_branch: "main"
- }
+ pushed_branch: "main",
+ },
];
-
+
const message = createMockMessage({
- commits
+ commits,
});
const el: SketchTimelineMessage = await fixture(html`
- <sketch-timeline-message
- .message=${message}
- ></sketch-timeline-message>
+ <sketch-timeline-message .message=${message}></sketch-timeline-message>
`);
- const diffButton = el.shadowRoot!.querySelector(".commit-diff-button") as HTMLButtonElement;
+ const diffButton = el.shadowRoot!.querySelector(
+ ".commit-diff-button",
+ ) as HTMLButtonElement;
expect(diffButton).to.exist;
-
+
// Set up listener for the event
setTimeout(() => diffButton!.click());
const { detail } = await oneEvent(el, "show-commit-diff");
-
+
expect(detail).to.exist;
expect(detail.commitHash).to.equal("1234567890abcdef");
});
@@ -208,13 +199,13 @@
// First message of a type should show icon
const firstMessage = createMockMessage({
type: "user",
- idx: 0
+ idx: 0,
});
-
+
// Second message of same type should not show icon
const secondMessage = createMockMessage({
type: "user",
- idx: 1
+ idx: 1,
});
// Test first message (should show icon)
diff --git a/loop/webui/src/web-components/sketch-timeline-message.ts b/loop/webui/src/web-components/sketch-timeline-message.ts
index cd2985a..ea32015 100644
--- a/loop/webui/src/web-components/sketch-timeline-message.ts
+++ b/loop/webui/src/web-components/sketch-timeline-message.ts
@@ -248,7 +248,7 @@
display: flex;
flex-direction: column;
}
-
+
.commit-preview {
padding: 8px 12px;
cursor: pointer;
@@ -256,38 +256,38 @@
background-color: #f6f8fa;
border-bottom: 1px dashed #d1d5da;
}
-
+
.commit-preview:hover {
background-color: #eef2f6;
}
-
+
.commit-hash {
color: #0366d6;
font-weight: bold;
}
-
+
.commit-details {
padding: 8px 12px;
max-height: 200px;
overflow-y: auto;
}
-
+
.commit-details pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
-
+
.commit-details.is-hidden {
display: none;
}
-
+
.pushed-branch {
color: #28a745;
font-weight: 500;
margin-left: 6px;
}
-
+
.commit-diff-button {
padding: 6px 12px;
border: 1px solid #ccc;
@@ -300,12 +300,12 @@
margin: 8px 12px;
display: block;
}
-
+
.commit-diff-button:hover {
background-color: #e7e7e7;
border-color: #aaa;
}
-
+
/* Tool call cards */
.tool-call-cards-container {
display: flex;
@@ -446,7 +446,13 @@
}
showCommit(commitHash: string) {
- this.dispatchEvent(new CustomEvent("show-commit-diff", {bubbles: true, composed: true, detail: {commitHash}}))
+ this.dispatchEvent(
+ new CustomEvent("show-commit-diff", {
+ bubbles: true,
+ composed: true,
+ detail: { commitHash },
+ }),
+ );
}
render() {
@@ -464,14 +470,35 @@
<div class="message-content">
<div class="message-header">
<span class="message-type">${this.message?.type}</span>
- <span class="message-timestamp">${this.formatTimestamp(this.message?.timestamp)} ${this.message?.elapsed ? html`(${(this.message?.elapsed / 1e9).toFixed(2)}s)` : ''}</span>
- ${this.message?.usage ? html`
- <span class="message-usage">
- <span title="Input tokens">In: ${this.message?.usage?.input_tokens}</span>
- ${this.message?.usage?.cache_read_input_tokens > 0 ? html`<span title="Cache tokens">[Cache: ${this.formatNumber(this.message?.usage?.cache_read_input_tokens)}]</span>` : ""}
- <span title="Output tokens">Out: ${this.message?.usage?.output_tokens}</span>
- <span title="Message cost">(${this.formatCurrency(this.message?.usage?.cost_usd)})</span>
- </span>` : ''}
+ <span class="message-timestamp"
+ >${this.formatTimestamp(this.message?.timestamp)}
+ ${this.message?.elapsed
+ ? html`(${(this.message?.elapsed / 1e9).toFixed(2)}s)`
+ : ""}</span
+ >
+ ${this.message?.usage
+ ? html` <span class="message-usage">
+ <span title="Input tokens"
+ >In: ${this.message?.usage?.input_tokens}</span
+ >
+ ${this.message?.usage?.cache_read_input_tokens > 0
+ ? html`<span title="Cache tokens"
+ >[Cache:
+ ${this.formatNumber(
+ this.message?.usage?.cache_read_input_tokens,
+ )}]</span
+ >`
+ : ""}
+ <span title="Output tokens"
+ >Out: ${this.message?.usage?.output_tokens}</span
+ >
+ <span title="Message cost"
+ >(${this.formatCurrency(
+ this.message?.usage?.cost_usd,
+ )})</span
+ >
+ </span>`
+ : ""}
</div>
<div class="message-text-container">
<div class="message-actions">
@@ -492,22 +519,31 @@
? html`
<div class="commits-container">
<div class="commits-header">
- ${this.message.commits.length} new commit${this.message.commits.length > 1 ? "s" : ""} detected
+ ${this.message.commits.length} new
+ commit${this.message.commits.length > 1 ? "s" : ""} detected
</div>
${this.message.commits.map((commit) => {
return html`
<div class="commit-boxes-row">
<div class="commit-box">
<div class="commit-preview">
- <span class="commit-hash">${commit.hash.substring(0, 8)}</span>
+ <span class="commit-hash"
+ >${commit.hash.substring(0, 8)}</span
+ >
${commit.subject}
<span class="pushed-branch"
- >→ pushed to ${commit.pushed_branch}</span>
+ >→ pushed to ${commit.pushed_branch}</span
+ >
</div>
<div class="commit-details is-hidden">
<pre>${commit.body}</pre>
</div>
- <button class="commit-diff-button" @click=${() => this.showCommit(commit.hash)}>View Changes</button>
+ <button
+ class="commit-diff-button"
+ @click=${() => this.showCommit(commit.hash)}
+ >
+ View Changes
+ </button>
</div>
</div>
`;
@@ -521,9 +557,11 @@
}
}
-function copyButton(textToCopy: string) {
+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) => {
+ 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
@@ -543,7 +581,7 @@
});
}}>Copy</button`;
- return ret
+ return ret;
}
declare global {
diff --git a/loop/webui/src/web-components/sketch-timeline.ts b/loop/webui/src/web-components/sketch-timeline.ts
index 8122db7..7471ded 100644
--- a/loop/webui/src/web-components/sketch-timeline.ts
+++ b/loop/webui/src/web-components/sketch-timeline.ts
@@ -1,10 +1,10 @@
-import {css, html, LitElement} from 'lit';
-import {repeat} from 'lit/directives/repeat.js';
-import {customElement, property} from 'lit/decorators.js';
-import {State, TimelineMessage} from '../types';
-import './sketch-timeline-message'
+import { css, html, LitElement } from "lit";
+import { repeat } from "lit/directives/repeat.js";
+import { customElement, property } from "lit/decorators.js";
+import { State, TimelineMessage } from "../types";
+import "./sketch-timeline-message";
-@customElement('sketch-timeline')
+@customElement("sketch-timeline")
export class SketchTimeline extends LitElement {
@property()
messages: TimelineMessage[] = [];
@@ -14,58 +14,58 @@
// 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`
- /* Hide views initially to prevent flash of content */
- .timeline-container .timeline,
- .timeline-container .diff-view,
- .timeline-container .chart-view,
- .timeline-container .terminal-view {
- visibility: hidden;
- }
-
- /* Will be set by JavaScript once we know which view to display */
- .timeline-container.view-initialized .timeline,
- .timeline-container.view-initialized .diff-view,
- .timeline-container.view-initialized .chart-view,
- .timeline-container.view-initialized .terminal-view {
- visibility: visible;
- }
+ /* Hide views initially to prevent flash of content */
+ .timeline-container .timeline,
+ .timeline-container .diff-view,
+ .timeline-container .chart-view,
+ .timeline-container .terminal-view {
+ visibility: hidden;
+ }
- .timeline-container {
- width: 100%;
- position: relative;
- }
-
- /* Timeline styles that should remain unchanged */
- .timeline {
- position: relative;
- margin: 10px 0;
- scroll-behavior: smooth;
- }
-
- .timeline::before {
- content: "";
- position: absolute;
- top: 0;
- bottom: 0;
- left: 15px;
- width: 2px;
- background: #e0e0e0;
- border-radius: 1px;
- }
-
- /* Hide the timeline vertical line when there are no messages */
- .timeline.empty::before {
- display: none;
- }
+ /* Will be set by JavaScript once we know which view to display */
+ .timeline-container.view-initialized .timeline,
+ .timeline-container.view-initialized .diff-view,
+ .timeline-container.view-initialized .chart-view,
+ .timeline-container.view-initialized .terminal-view {
+ visibility: visible;
+ }
+
+ .timeline-container {
+ width: 100%;
+ position: relative;
+ }
+
+ /* Timeline styles that should remain unchanged */
+ .timeline {
+ position: relative;
+ margin: 10px 0;
+ scroll-behavior: smooth;
+ }
+
+ .timeline::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 15px;
+ width: 2px;
+ background: #e0e0e0;
+ border-radius: 1px;
+ }
+
+ /* Hide the timeline vertical line when there are no messages */
+ .timeline.empty::before {
+ display: none;
+ }
`;
constructor() {
super();
-
+
// Binding methods
this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
}
-
+
/**
* Handle showCommitDiff event
*/
@@ -73,10 +73,10 @@
const { commitHash } = event.detail;
if (commitHash) {
// Bubble up the event to the app shell
- const newEvent = new CustomEvent('show-commit-diff', {
+ const newEvent = new CustomEvent("show-commit-diff", {
detail: { commitHash },
bubbles: true,
- composed: true
+ composed: true,
});
this.dispatchEvent(newEvent);
}
@@ -85,37 +85,49 @@
// See https://lit.dev/docs/components/lifecycle/
connectedCallback() {
super.connectedCallback();
-
+
// Listen for showCommitDiff events from the renderer
- document.addEventListener('showCommitDiff', this._handleShowCommitDiff as EventListener);
+ document.addEventListener(
+ "showCommitDiff",
+ this._handleShowCommitDiff as EventListener,
+ );
}
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
-
+
// Remove event listeners
- document.removeEventListener('showCommitDiff', this._handleShowCommitDiff as EventListener);
+ document.removeEventListener(
+ "showCommitDiff",
+ this._handleShowCommitDiff as EventListener,
+ );
}
messageKey(message: TimelineMessage): string {
// If the message has tool calls, and any of the tool_calls get a response, we need to
// re-render that message.
- const toolCallResponses = message.tool_calls?.filter((tc)=>tc.result_message).map((tc)=>tc.tool_call_id).join('-');
+ const toolCallResponses = message.tool_calls
+ ?.filter((tc) => tc.result_message)
+ .map((tc) => tc.tool_call_id)
+ .join("-");
return `message-${message.idx}-${toolCallResponses}`;
}
render() {
return html`
- <div class="timeline-container">
- ${repeat(this.messages, this.messageKey, (message, index) => {
- let previousMessage: TimelineMessage;
- if (index > 0) {
- previousMessage = this.messages[index-1];
- }
- return html`<sketch-timeline-message .message=${message} .previousMessage=${previousMessage}></sketch-timeline-message>`;
- })}
- </div>
+ <div class="timeline-container">
+ ${repeat(this.messages, this.messageKey, (message, index) => {
+ let previousMessage: TimelineMessage;
+ if (index > 0) {
+ previousMessage = this.messages[index - 1];
+ }
+ return html`<sketch-timeline-message
+ .message=${message}
+ .previousMessage=${previousMessage}
+ ></sketch-timeline-message>`;
+ })}
+ </div>
`;
}
}
@@ -124,4 +136,4 @@
interface HTMLElementTagNameMap {
"sketch-timeline": SketchTimeline;
}
-}
\ No newline at end of file
+}
diff --git a/loop/webui/src/web-components/sketch-tool-calls.ts b/loop/webui/src/web-components/sketch-tool-calls.ts
index a8d0acc..e7a5c74 100644
--- a/loop/webui/src/web-components/sketch-tool-calls.ts
+++ b/loop/webui/src/web-components/sketch-tool-calls.ts
@@ -400,7 +400,7 @@
max-width: 30%;
white-space: pre;
}
-
+
.thought-bubble .preview {
white-space: nowrap;
text-overflow: ellipsis;
@@ -408,7 +408,7 @@
}
.thought-bubble:before {
- content: '';
+ content: "";
position: absolute;
top: -8px;
left: -8px;
@@ -418,9 +418,9 @@
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
-
+
.thought-bubble:after {
- content: '';
+ content: "";
position: absolute;
top: -16px;
left: -16px;
@@ -430,7 +430,6 @@
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
-
.patch-input-preview {
color: #555;
@@ -537,68 +536,75 @@
switch (toolCall.name) {
case "title":
const titleInput = JSON.parse(toolCall.input);
- return html`
- <div class="tool-call-compact-view">
+ 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">
+ 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>`
- : ""}`
+ ? html` ${toolCall.result_message.tool_result
+ ? html` <pre class="tool-call-result-preview">
+${toolCall.result_message.tool_result}</pre
+ >`
+ : ""}`
: cancelButton}
</div>`;
case "codereview":
- return html`
- <div class="tool-call-compact-view">
+ 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>
+ <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">
+ 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>
+ <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">
+ 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>
+ <div class="patch-input-preview">
+ <span class="patch-path">${patchInput.path}</span>:
+ ${patchInput.patches.length}
+ edit${patchInput.patches.length > 1 ? "s" : ""}
+ </div>
${cancelButton}
</div>`;
case "done":
const doneInput = JSON.parse(toolCall.input);
- return html`
- <div class="tool-call-compact-view">
+ 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 = '🤷♂️';
+ let statusIcon = "⛔";
+ if (item.status == "yes") {
+ statusIcon = "👍";
+ } else if (item.status == "not applicable") {
+ statusIcon = "🤷♂️";
}
- return html`<div><span>${statusIcon}</span> ${key}:${item.status}</div>`;
+ return html`<div>
+ <span>${statusIcon}</span> ${key}:${item.status}
+ </div>`;
})}
</div>
${cancelButton}
@@ -606,20 +612,21 @@
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}
- `;
+ <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}
+ `;
}
}
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) => {
diff --git a/loop/webui/src/web-components/sketch-view-mode-select.test.ts b/loop/webui/src/web-components/sketch-view-mode-select.test.ts
index beb0b67..13f5a27 100644
--- a/loop/webui/src/web-components/sketch-view-mode-select.test.ts
+++ b/loop/webui/src/web-components/sketch-view-mode-select.test.ts
@@ -1,4 +1,11 @@
-import { html, fixture, expect, oneEvent, elementUpdated, fixtureCleanup } from "@open-wc/testing";
+import {
+ html,
+ fixture,
+ expect,
+ oneEvent,
+ elementUpdated,
+ fixtureCleanup,
+} from "@open-wc/testing";
import "./sketch-view-mode-select";
import type { SketchViewModeSelect } from "./sketch-view-mode-select";
@@ -46,12 +53,14 @@
<sketch-view-mode-select></sketch-view-mode-select>
`);
- const diffButton = el.shadowRoot!.querySelector("#showDiffButton") as HTMLButtonElement;
-
+ const diffButton = el.shadowRoot!.querySelector(
+ "#showDiffButton",
+ ) as HTMLButtonElement;
+
// Setup listener for the view-mode-select event
setTimeout(() => diffButton.click());
const { detail } = await oneEvent(el, "view-mode-select");
-
+
expect(detail.mode).to.equal("diff");
});
@@ -62,17 +71,17 @@
// Initially should be in chat mode
expect(el.activeMode).to.equal("chat");
-
+
// Dispatch the update-active-mode event to change to diff mode
const updateEvent = new CustomEvent("update-active-mode", {
detail: { mode: "diff" },
- bubbles: true
+ bubbles: true,
});
el.dispatchEvent(updateEvent);
-
+
// Wait for the component to update
await elementUpdated(el);
-
+
expect(el.activeMode).to.equal("diff");
const diffButton = el.shadowRoot!.querySelector("#showDiffButton");
expect(diffButton!.classList.contains("active")).to.be.true;
@@ -88,12 +97,10 @@
const chatButton = el.shadowRoot!.querySelector("#showConversationButton");
const diffButton = el.shadowRoot!.querySelector("#showDiffButton");
const chartsButton = el.shadowRoot!.querySelector("#showChartsButton");
-
+
expect(terminalButton!.classList.contains("active")).to.be.true;
expect(chatButton!.classList.contains("active")).to.be.false;
expect(diffButton!.classList.contains("active")).to.be.false;
expect(chartsButton!.classList.contains("active")).to.be.false;
});
-
-
});
diff --git a/loop/webui/src/web-components/sketch-view-mode-select.ts b/loop/webui/src/web-components/sketch-view-mode-select.ts
index b55282a..d67da0b 100644
--- a/loop/webui/src/web-components/sketch-view-mode-select.ts
+++ b/loop/webui/src/web-components/sketch-view-mode-select.ts
@@ -1,10 +1,10 @@
-import {css, html, LitElement} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
-import {DataManager, ConnectionStatus} from '../data';
-import {State, TimelineMessage} from '../types';
-import './sketch-container-status';
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { DataManager, ConnectionStatus } from "../data";
+import { State, TimelineMessage } from "../types";
+import "./sketch-container-status";
-@customElement('sketch-view-mode-select')
+@customElement("sketch-view-mode-select")
export class SketchViewModeSelect extends LitElement {
// Current active mode
@property()
@@ -17,45 +17,45 @@
// other components or the containing web page (...unless you want it to do that).
static styles = css`
-/* View Mode Button Styles */
-.view-mode-buttons {
- display: flex;
- gap: 8px;
- margin-right: 10px;
-}
+ /* View Mode Button Styles */
+ .view-mode-buttons {
+ display: flex;
+ gap: 8px;
+ margin-right: 10px;
+ }
-.emoji-button {
- font-size: 18px;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s ease;
- padding: 0;
- line-height: 1;
-}
+ .emoji-button {
+ font-size: 18px;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0;
+ line-height: 1;
+ }
-.emoji-button:hover {
- background-color: #f0f0f0;
- transform: translateY(-2px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
+ .emoji-button:hover {
+ background-color: #f0f0f0;
+ transform: translateY(-2px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
-.emoji-button.active {
- background-color: #e6f7ff;
- border-color: #1890ff;
- color: #1890ff;
-}
-`;
-
+ .emoji-button.active {
+ background-color: #e6f7ff;
+ border-color: #1890ff;
+ color: #1890ff;
+ }
+ `;
+
constructor() {
super();
-
+
// Binding methods
this._handleViewModeClick = this._handleViewModeClick.bind(this);
this._handleUpdateActiveMode = this._handleUpdateActiveMode.bind(this);
@@ -64,24 +64,27 @@
// See https://lit.dev/docs/components/lifecycle/
connectedCallback() {
super.connectedCallback();
-
+
// Listen for update-active-mode events
- this.addEventListener('update-active-mode', this._handleUpdateActiveMode as EventListener);
+ this.addEventListener(
+ "update-active-mode",
+ this._handleUpdateActiveMode as EventListener,
+ );
}
-
+
/**
* Handle view mode button clicks
*/
private _handleViewModeClick(mode: "chat" | "diff" | "charts" | "terminal") {
// Dispatch a custom event to notify the app shell to change the view
- const event = new CustomEvent('view-mode-select', {
+ const event = new CustomEvent("view-mode-select", {
detail: { mode },
bubbles: true,
- composed: true
+ composed: true,
});
this.dispatchEvent(event);
}
-
+
/**
* Handle updates to the active mode
*/
@@ -95,9 +98,12 @@
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
-
+
// Remove event listeners
- this.removeEventListener('update-active-mode', this._handleUpdateActiveMode as EventListener);
+ this.removeEventListener(
+ "update-active-mode",
+ this._handleUpdateActiveMode as EventListener,
+ );
}
render() {
@@ -105,33 +111,33 @@
<div class="view-mode-buttons">
<button
id="showConversationButton"
- class="emoji-button ${this.activeMode === 'chat' ? 'active' : ''}"
+ class="emoji-button ${this.activeMode === "chat" ? "active" : ""}"
title="Conversation View"
- @click=${() => this._handleViewModeClick('chat')}
+ @click=${() => this._handleViewModeClick("chat")}
>
💬
</button>
<button
id="showDiffButton"
- class="emoji-button ${this.activeMode === 'diff' ? 'active' : ''}"
+ class="emoji-button ${this.activeMode === "diff" ? "active" : ""}"
title="Diff View"
- @click=${() => this._handleViewModeClick('diff')}
+ @click=${() => this._handleViewModeClick("diff")}
>
±
</button>
<button
id="showChartsButton"
- class="emoji-button ${this.activeMode === 'charts' ? 'active' : ''}"
+ class="emoji-button ${this.activeMode === "charts" ? "active" : ""}"
title="Charts View"
- @click=${() => this._handleViewModeClick('charts')}
+ @click=${() => this._handleViewModeClick("charts")}
>
📈
</button>
<button
id="showTerminalButton"
- class="emoji-button ${this.activeMode === 'terminal' ? 'active' : ''}"
+ class="emoji-button ${this.activeMode === "terminal" ? "active" : ""}"
title="Terminal View"
- @click=${() => this._handleViewModeClick('terminal')}
+ @click=${() => this._handleViewModeClick("terminal")}
>
💻
</button>
@@ -144,4 +150,4 @@
interface HTMLElementTagNameMap {
"sketch-view-mode-select": SketchViewModeSelect;
}
-}
\ No newline at end of file
+}