Modernize and streamline Sketch top bar UI
Comprehensive improvements to the top bar interface:
Removed Elements:
- Poll checkbox and all polling UI controls
- Network status indicator and all status messages
- Dotted underlines from tooltip elements
Button Improvements:
- Added SVG icons to Restart and Stop buttons
- Made buttons responsive (text hides below 1400px)
- Stop button now disabled when no active calls
Layout Simplification:
- Simplified hostname display (outside hostname only)
- Simplified working directory display (outside directory only)
- Repositioned tab chooser for better layout
- Improved responsive behavior across viewport sizes
Enhanced Features:
- Added GitHub repo auto-detection with clickable links
- Improved VSCode integration with button styling
- Changed 'SSH Connection' to 'Connect to Container'
- Enhanced tooltips with more descriptive text
The UI is now cleaner, more responsive, and provides a better user experience
across different screen sizes.
Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index 7c618aa..157f2f2 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -230,7 +230,8 @@
margin-right: 50px;
}
- .restart-button {
+ .restart-button,
+ .stop-button {
background: #2196f3;
color: white;
border: none;
@@ -239,6 +240,9 @@
cursor: pointer;
font-size: 12px;
margin-right: 5px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
}
.restart-button:hover {
@@ -251,28 +255,43 @@
opacity: 0.6;
}
- .refresh-button {
- background: #4caf50;
+ .stop-button {
+ background: #dc3545;
color: white;
- border: none;
- padding: 4px 10px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- margin-right: 5px;
}
- .stop-button:hover {
- background-color: #c82333 !important;
+ .stop-button:hover:not(:disabled) {
+ background-color: #c82333;
}
- .poll-updates {
- display: flex;
- align-items: center;
- font-size: 12px;
- margin-right: 10px;
+ .stop-button:disabled {
+ background-color: #e9a8ad;
+ cursor: not-allowed;
+ opacity: 0.7;
}
+ .stop-button:disabled:hover {
+ background-color: #e9a8ad;
+ }
+
+ .button-icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ @media (max-width: 1400px) {
+ .button-text {
+ display: none;
+ }
+
+ .restart-button,
+ .stop-button {
+ padding: 6px;
+ }
+ }
+
+ /* Removed poll-updates class */
+
.notifications-toggle {
display: flex;
align-items: center;
@@ -320,9 +339,6 @@
@property()
connectionErrorMessage: string = "";
- @property()
- messageStatus: string = "";
-
// Chat messages
@property({ attribute: false })
messages: AgentMessage[] = [];
@@ -729,18 +745,8 @@
private handleDataChanged(eventData: {
state: State;
newMessages: AgentMessage[];
- isFirstFetch?: boolean;
}): void {
- const { state, newMessages, isFirstFetch } = eventData;
-
- // Check if this is the first data fetch or if there are new messages
- if (isFirstFetch) {
- this.messageStatus = "Initial messages loaded";
- } else if (newMessages && newMessages.length > 0) {
- this.messageStatus = "Updated just now";
- } else {
- this.messageStatus = "No new messages";
- }
+ const { state, newMessages } = eventData;
// Update state if we received it
if (state) {
@@ -768,7 +774,7 @@
this.updateLastCommitInfo(newMessages);
// Check for agent messages with end_of_turn=true and show notifications
- if (newMessages && newMessages.length > 0 && !isFirstFetch) {
+ if (newMessages && newMessages.length > 0) {
for (const message of newMessages) {
if (
message.type === "agent" &&
@@ -858,10 +864,9 @@
);
}
- this.messageStatus = "Stop request sent";
+ // Stop request sent
} catch (error) {
console.error("Error stopping operation:", error);
- this.messageStatus = "Failed to stop operation";
}
}
@@ -927,14 +932,14 @@
<h2 id="chatTitle" class="chat-title">${this.title}</h2>
</div>
- <!-- Views section with tabs -->
- <sketch-view-mode-select></sketch-view-mode-select>
-
- <!-- Container status info -->
+ <!-- Container status info moved above tabs -->
<sketch-container-status
.state=${this.containerState}
></sketch-container-status>
+ <!-- Views section with tabs - repositioned -->
+ <sketch-view-mode-select></sketch-view-mode-select>
+
${this.lastCommit
? html`
<div
@@ -963,23 +968,49 @@
?disabled=${this.containerState.message_count === 0}
@click=${this.openRestartModal}
>
- Restart
+ <svg
+ class="button-icon"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
+ <path d="M3 3v5h5" />
+ </svg>
+ <span class="button-text">Restart</span>
</button>
- <button id="stopButton" class="refresh-button stop-button">
- Stop
+ <button
+ id="stopButton"
+ class="stop-button"
+ ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
+ 0 &&
+ (this.containerState?.outstanding_tool_calls || []).length === 0}
+ >
+ <svg
+ class="button-icon"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <rect x="6" y="6" width="12" height="12" />
+ </svg>
+ <span class="button-text">Stop</span>
</button>
- <div class="poll-updates">
- <input type="checkbox" id="pollToggle" checked />
- <label for="pollToggle">Poll</label>
- </div>
-
<div
class="notifications-toggle"
@click=${this._handleNotificationsToggle}
title="${this.notificationsEnabled
? "Disable"
- : "Enable"} notifications"
+ : "Enable"} notifications when the agent completes its turn"
>
<div
class="bell-icon ${!this.notificationsEnabled
@@ -1002,7 +1033,6 @@
</div>
<sketch-network-status
- message=${this.messageStatus}
connection=${this.connectionStatus}
error=${this.connectionErrorMessage}
></sketch-network-status>
@@ -1096,18 +1126,8 @@
}
});
- const pollToggleCheckbox = this.renderRoot?.querySelector(
- "#pollToggle",
- ) as HTMLInputElement;
- pollToggleCheckbox?.addEventListener("change", () => {
- this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
- if (!pollToggleCheckbox.checked) {
- this.connectionStatus = "disabled";
- this.messageStatus = "Polling stopped";
- } else {
- this.messageStatus = "Polling for updates...";
- }
- });
+ // Always enable polling by default
+ this.dataManager.setPollingEnabled(true);
// Process any existing messages to find commit information
if (this.messages && this.messages.length > 0) {
diff --git a/webui/src/web-components/sketch-container-status.ts b/webui/src/web-components/sketch-container-status.ts
index f4c0f25..ba745cc 100644
--- a/webui/src/web-components/sketch-container-status.ts
+++ b/webui/src/web-components/sketch-container-status.ts
@@ -74,8 +74,7 @@
}
[title] {
- cursor: help;
- text-decoration: underline dotted;
+ cursor: default;
}
.cost {
@@ -180,11 +179,33 @@
}
.vscode-link {
+ color: white;
+ text-decoration: none;
+ background-color: #0066b8;
+ padding: 4px 8px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ transition: all 0.2s ease;
+ }
+
+ .vscode-link:hover {
+ background-color: #005091;
+ }
+
+ .vscode-icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ .github-link {
color: #2962ff;
text-decoration: none;
}
- .vscode-link:hover {
+ .github-link:hover {
text-decoration: underline;
}
`;
@@ -212,33 +233,25 @@
}
formatHostname() {
+ // Only display outside hostname
const outsideHostname = this.state?.outside_hostname;
- const insideHostname = this.state?.inside_hostname;
- if (!outsideHostname || !insideHostname) {
+ if (!outsideHostname) {
return this.state?.hostname;
}
- if (outsideHostname === insideHostname) {
- return outsideHostname;
- }
-
- return `${outsideHostname}:${insideHostname}`;
+ return outsideHostname;
}
formatWorkingDir() {
+ // Only display outside working directory
const outsideWorkingDir = this.state?.outside_working_dir;
- const insideWorkingDir = this.state?.inside_working_dir;
- if (!outsideWorkingDir || !insideWorkingDir) {
+ if (!outsideWorkingDir) {
return this.state?.working_dir;
}
- if (outsideWorkingDir === insideWorkingDir) {
- return outsideWorkingDir;
- }
-
- return `${outsideWorkingDir}:${insideWorkingDir}`;
+ return outsideWorkingDir;
}
getHostnameTooltip() {
@@ -298,6 +311,33 @@
return `sketch-${this.state?.session_id}`;
}
+ // Format GitHub repository URL to org/repo format
+ formatGitHubRepo(url) {
+ if (!url) return null;
+
+ // Common GitHub URL patterns
+ const patterns = [
+ // HTTPS URLs
+ /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
+ // SSH URLs
+ /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
+ // Git protocol
+ /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
+ ];
+
+ for (const pattern of patterns) {
+ const match = url.match(pattern);
+ if (match) {
+ return {
+ formatted: `${match[1]}/${match[2]}`,
+ url: `https://github.com/${match[1]}/${match[2]}`,
+ };
+ }
+ }
+
+ return null;
+ }
+
renderSSHSection() {
// Only show SSH section if we're in a Docker container and have session ID
if (!this.state?.session_id) {
@@ -312,7 +352,7 @@
if (!this.state?.ssh_available) {
return html`
<div class="ssh-section">
- <h3>SSH Connection</h3>
+ <h3>Connect to Container</h3>
<div class="ssh-warning">
SSH connections are not available:
${this.state?.ssh_error || "SSH configuration is missing"}
@@ -323,7 +363,7 @@
return html`
<div class="ssh-section">
- <h3>SSH Connection</h3>
+ <h3>Connect to Container</h3>
<div class="ssh-command">
<div class="ssh-command-text">${sshCommand}</div>
<button
@@ -343,7 +383,23 @@
</button>
</div>
<div class="ssh-command">
- <a href="${vscodeURL}" class="vscode-link">${vscodeURL}</a>
+ <a href="${vscodeURL}" class="vscode-link" title="${vscodeURL}">
+ <svg
+ class="vscode-icon"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="white"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <path
+ d="M16.5 9.4 7.55 4.24a.35.35 0 0 0-.41.01l-1.23.93a.35.35 0 0 0-.14.29v13.04c0 .12.07.23.17.29l1.24.93c.13.1.31.09.43-.01L16.5 14.6l-6.39 4.82c-.16.12-.38.12-.55.01l-1.33-1.01a.35.35 0 0 1-.14-.28V5.88c0-.12.07-.23.18-.29l1.23-.93c.14-.1.32-.1.46 0l6.54 4.92-6.54 4.92c-.14.1-.32.1-.46 0l-1.23-.93a.35.35 0 0 1-.18-.29V5.88c0-.12.07-.23.17-.29l1.33-1.01c.16-.12.39-.11.55.01l6.39 4.81z"
+ />
+ </svg>
+ <span>Open in VSCode</span>
+ </a>
</div>
</div>
`;
@@ -381,9 +437,30 @@
${this.state?.git_origin
? html`
<div class="info-item">
- <span id="gitOrigin" class="info-value"
- >${this.state?.git_origin}</span
- >
+ ${(() => {
+ const github = this.formatGitHubRepo(
+ this.state?.git_origin,
+ );
+ if (github) {
+ return html`
+ <a
+ href="${github.url}"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="github-link"
+ title="${this.state?.git_origin}"
+ >
+ ${github.formatted}
+ </a>
+ `;
+ } else {
+ return html`
+ <span id="gitOrigin" class="info-value"
+ >${this.state?.git_origin}</span
+ >
+ `;
+ }
+ })()}
</div>
`
: ""}
diff --git a/webui/src/web-components/sketch-network-status.test.ts b/webui/src/web-components/sketch-network-status.test.ts
index 45882a0..5c968d4 100644
--- a/webui/src/web-components/sketch-network-status.test.ts
+++ b/webui/src/web-components/sketch-network-status.test.ts
@@ -1,61 +1,26 @@
import { test, expect } from "@sand4rt/experimental-ct-web";
import { SketchNetworkStatus } from "./sketch-network-status";
-test("displays the correct connection status when connected", async ({
+// Test for when no error message is present - component should not render
+test("does not display anything when no error is provided", async ({
mount,
}) => {
const component = await mount(SketchNetworkStatus, {
props: {
connection: "connected",
- message: "Connected to server",
},
});
- await expect(component.locator(".polling-indicator")).toBeVisible();
- await expect(component.locator(".status-text")).toBeVisible();
- await expect(component.locator(".polling-indicator.active")).toBeVisible();
- await expect(component.locator(".status-text")).toContainText(
- "Connected to server",
- );
+ // The component should be empty
+ await expect(component.locator(".status-container")).not.toBeVisible();
});
-test("displays the correct connection status when disconnected", async ({
- mount,
-}) => {
- const component = await mount(SketchNetworkStatus, {
- props: {
- connection: "disconnected",
- message: "Disconnected",
- },
- });
-
- await expect(component.locator(".polling-indicator")).toBeVisible();
- await expect(component.locator(".polling-indicator.error")).toBeVisible();
-});
-
-test("displays the correct connection status when disabled", async ({
- mount,
-}) => {
- const component = await mount(SketchNetworkStatus, {
- props: {
- connection: "disabled",
- message: "Disabled",
- },
- });
-
- await expect(component.locator(".polling-indicator")).toBeVisible();
- await expect(component.locator(".polling-indicator.error")).not.toBeVisible();
- await expect(
- component.locator(".polling-indicator.active"),
- ).not.toBeVisible();
-});
-
+// Test that error message is displayed correctly
test("displays error message when provided", async ({ mount }) => {
const errorMsg = "Connection error";
const component = await mount(SketchNetworkStatus, {
props: {
connection: "disconnected",
- message: "Disconnected",
error: errorMsg,
},
});
diff --git a/webui/src/web-components/sketch-network-status.ts b/webui/src/web-components/sketch-network-status.ts
index 2a0e455..cf168fd 100644
--- a/webui/src/web-components/sketch-network-status.ts
+++ b/webui/src/web-components/sketch-network-status.ts
@@ -7,9 +7,6 @@
connection: string;
@property()
- message: string;
-
- @property()
error: string;
// See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
@@ -23,37 +20,6 @@
align-items: center;
}
- .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.error {
- background-color: #f44336;
- animation: pulse 1.5s infinite;
- }
-
- @keyframes pulse {
- 0% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
- 100% {
- opacity: 1;
- }
- }
-
.status-text {
font-size: 11px;
color: #666;
@@ -74,23 +40,15 @@
super.disconnectedCallback();
}
- indicator() {
- if (this.connection === "disabled") {
- return "";
- }
- return this.connection === "connected" ? "active" : "error";
- }
-
render() {
+ // Only render if there's an error to display
+ if (!this.error) {
+ return html``;
+ }
+
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
- >
+ <span id="statusText" class="status-text">${this.error}</span>
</div>
`;
}
diff --git a/webui/src/web-components/sketch-view-mode-select.ts b/webui/src/web-components/sketch-view-mode-select.ts
index 4c3c91f..3b94303 100644
--- a/webui/src/web-components/sketch-view-mode-select.ts
+++ b/webui/src/web-components/sketch-view-mode-select.ts
@@ -35,6 +35,16 @@
white-space: nowrap;
}
+ @media (max-width: 1400px) {
+ .tab-btn span:not(.tab-icon) {
+ display: none;
+ }
+
+ .tab-btn {
+ padding: 8px 10px;
+ }
+ }
+
.tab-btn:not(:last-child) {
border-right: 1px solid #eee;
}