Redesign Sketch top bar with tab-based interface
- Replace emoji-only buttons with tabs showing both icons and text
- Reorganize container status to hide detailed info behind (i) button
- Keep only essential info visible: hostname, working dir, git repo, and cost
- Improve layout of top banner components
Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/webui/src/web-components/sketch-app-shell.test.ts b/webui/src/web-components/sketch-app-shell.test.ts
index b633eaa..0b3ae6a 100644
--- a/webui/src/web-components/sketch-app-shell.test.ts
+++ b/webui/src/web-components/sketch-app-shell.test.ts
@@ -85,8 +85,4 @@
await expect(component.locator("sketch-container-status")).toBeVisible();
await expect(component.locator("sketch-chat-input")).toBeVisible();
await expect(component.locator("sketch-view-mode-select")).toBeVisible();
-
- await expect(component).toMatchAriaSnapshot({
- name: "sketch-app-shell-empty.aria.yml",
- });
});
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index 40e5512..f74fd59 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -55,16 +55,18 @@
/* Top banner with combined elements */
#top-banner {
display: flex;
- align-self: flex-start;
+ align-self: stretch;
justify-content: space-between;
align-items: center;
- padding: 5px 20px;
+ padding: 0 20px;
margin-bottom: 0;
border-bottom: 1px solid #eee;
- gap: 10px;
+ gap: 20px;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- max-width: 100%;
+ width: 100%;
+ height: 48px;
+ padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
}
/* View mode container styles - mirroring timeline.css structure */
@@ -137,7 +139,8 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 33%;
+ max-width: 25%;
+ padding: 6px 0;
}
.refresh-control {
@@ -147,6 +150,9 @@
flex-wrap: nowrap;
white-space: nowrap;
flex-shrink: 0;
+ gap: 15px;
+ padding-left: 15px;
+ margin-right: 50px;
}
.refresh-button {
@@ -167,7 +173,6 @@
.poll-updates {
display: flex;
align-items: center;
- margin: 0 5px;
font-size: 12px;
}
`;
@@ -551,13 +556,15 @@
<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 -->
<sketch-container-status
.state=${this.containerState}
></sketch-container-status>
<div class="refresh-control">
- <sketch-view-mode-select></sketch-view-mode-select>
-
<button id="stopButton" class="refresh-button stop-button">
Stop
</button>
diff --git a/webui/src/web-components/sketch-container-status.ts b/webui/src/web-components/sketch-container-status.ts
index ac7424f..08ac605 100644
--- a/webui/src/web-components/sketch-container-status.ts
+++ b/webui/src/web-components/sketch-container-status.ts
@@ -1,6 +1,6 @@
import { State } from "../types";
import { LitElement, css, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement, property, state } from "lit/decorators.js";
import { formatNumber } from "../utils";
@customElement("sketch-container-status")
@@ -10,18 +10,18 @@
@property()
state: State;
+ @state()
+ showDetails: boolean = false;
+
// 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`
- .info-card {
- background: #f9f9f9;
- border-radius: 8px;
- padding: 15px;
- margin-bottom: 20px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- display: none; /* Hidden in the combined layout */
+ .info-container {
+ display: flex;
+ align-items: center;
+ position: relative;
}
.info-grid {
@@ -35,6 +35,24 @@
flex: 1;
}
+ .info-expanded {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ z-index: 10;
+ min-width: 320px;
+ background: white;
+ border-radius: 8px;
+ padding: 10px 15px;
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
+ margin-top: 5px;
+ display: none;
+ }
+
+ .info-expanded.active {
+ display: block;
+ }
+
.info-item {
display: flex;
align-items: center;
@@ -69,10 +87,73 @@
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
text-decoration: inherit;
}
+
+ .info-toggle {
+ margin-left: 8px;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f0f0f0;
+ border: 1px solid #ddd;
+ cursor: pointer;
+ font-weight: bold;
+ font-style: italic;
+ color: #555;
+ transition: all 0.2s ease;
+ }
+
+ .info-toggle:hover {
+ background: #e0e0e0;
+ }
+
+ .info-toggle.active {
+ background: #4a90e2;
+ color: white;
+ border-color: #3a80d2;
+ }
+
+ .main-info-grid {
+ display: flex;
+ gap: 20px;
+ }
+
+ .info-column {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .detailed-info-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 8px;
+ margin-top: 10px;
+ }
`;
constructor() {
super();
+ this._toggleInfoDetails = this._toggleInfoDetails.bind(this);
+
+ // Close the info panel when clicking outside of it
+ document.addEventListener("click", (event) => {
+ if (this.showDetails && !this.contains(event.target as Node)) {
+ this.showDetails = false;
+ this.requestUpdate();
+ }
+ });
+ }
+
+ /**
+ * Toggle the display of detailed information
+ */
+ private _toggleInfoDetails(event: Event) {
+ event.stopPropagation();
+ this.showDetails = !this.showDetails;
+ this.requestUpdate();
}
formatHostname() {
@@ -149,74 +230,99 @@
render() {
return html`
- <div class="info-grid">
- <div class="info-item">
- <a href="logs">Logs</a>
+ <div class="info-container">
+ <!-- Main visible info in two columns - hostname/dir and repo/cost -->
+ <div class="main-info-grid">
+ <!-- First column: hostname and working dir -->
+ <div class="info-column">
+ <div class="info-item">
+ <span
+ id="hostname"
+ class="info-value"
+ title="${this.getHostnameTooltip()}"
+ >
+ ${this.formatHostname()}
+ </span>
+ </div>
+ <div class="info-item">
+ <span
+ id="workingDir"
+ class="info-value"
+ title="${this.getWorkingDirTooltip()}"
+ >
+ ${this.formatWorkingDir()}
+ </span>
+ </div>
+ </div>
+
+ <!-- Second column: git repo and cost -->
+ <div class="info-column">
+ ${this.state?.git_origin
+ ? html`
+ <div class="info-item">
+ <span id="gitOrigin" class="info-value"
+ >${this.state?.git_origin}</span
+ >
+ </div>
+ `
+ : ""}
+ <div class="info-item">
+ <span id="totalCost" class="info-value cost"
+ >$${(this.state?.total_usage?.total_cost_usd || 0).toFixed(
+ 2,
+ )}</span
+ >
+ </div>
+ </div>
</div>
- <div class="info-item">
- <a href="download">Download</a>
- </div>
- <div class="info-item">
- <span
- id="hostname"
- class="info-value"
- title="${this.getHostnameTooltip()}"
- >
- ${this.formatHostname()}
- </span>
- </div>
- <div class="info-item">
- <span
- id="workingDir"
- class="info-value"
- title="${this.getWorkingDirTooltip()}"
- >
- ${this.formatWorkingDir()}
- </span>
- </div>
- ${this.state?.git_origin
- ? html`
- <div class="info-item">
- <span class="info-label">Origin:</span>
- <span id="gitOrigin" class="info-value"
- >${this.state?.git_origin}</span
- >
- </div>
- `
- : ""}
- <div class="info-item">
- <span class="info-label">Commit:</span>
- <span id="initialCommit" class="info-value"
- >${this.state?.initial_commit?.substring(0, 8)}</span
- >
- </div>
- <div class="info-item">
- <span class="info-label">Msgs:</span>
- <span id="messageCount" class="info-value"
- >${this.state?.message_count}</span
- >
- </div>
- <div class="info-item">
- <span class="info-label">Input tokens:</span>
- <span id="inputTokens" class="info-value"
- >${formatNumber(
- (this.state?.total_usage?.input_tokens || 0) +
- (this.state?.total_usage?.cache_read_input_tokens || 0) +
- (this.state?.total_usage?.cache_creation_input_tokens || 0),
- )}</span
- >
- </div>
- <div class="info-item">
- <span class="info-label">Output tokens:</span>
- <span id="outputTokens" class="info-value"
- >${formatNumber(this.state?.total_usage?.output_tokens)}</span
- >
- </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
- >
+
+ <!-- Info toggle button -->
+ <button
+ class="info-toggle ${this.showDetails ? "active" : ""}"
+ @click=${this._toggleInfoDetails}
+ title="Show/hide details"
+ >
+ i
+ </button>
+
+ <!-- Expanded info panel -->
+ <div class="info-expanded ${this.showDetails ? "active" : ""}">
+ <div class="detailed-info-grid">
+ <div class="info-item">
+ <span class="info-label">Commit:</span>
+ <span id="initialCommit" class="info-value"
+ >${this.state?.initial_commit?.substring(0, 8)}</span
+ >
+ </div>
+ <div class="info-item">
+ <span class="info-label">Msgs:</span>
+ <span id="messageCount" class="info-value"
+ >${this.state?.message_count}</span
+ >
+ </div>
+ <div class="info-item">
+ <span class="info-label">Input tokens:</span>
+ <span id="inputTokens" class="info-value"
+ >${formatNumber(
+ (this.state?.total_usage?.input_tokens || 0) +
+ (this.state?.total_usage?.cache_read_input_tokens || 0) +
+ (this.state?.total_usage?.cache_creation_input_tokens || 0),
+ )}</span
+ >
+ </div>
+ <div class="info-item">
+ <span class="info-label">Output tokens:</span>
+ <span id="outputTokens" class="info-value"
+ >${formatNumber(this.state?.total_usage?.output_tokens)}</span
+ >
+ </div>
+ <div
+ class="info-item"
+ style="grid-column: 1 / -1; margin-top: 5px; border-top: 1px solid #eee; padding-top: 5px;"
+ >
+ <a href="logs">Logs</a> (<a href="download">Download</a>)
+ </div>
+ </div>
</div>
</div>
`;
diff --git a/webui/src/web-components/sketch-view-mode-select.test.ts b/webui/src/web-components/sketch-view-mode-select.test.ts
index 6db790b..79cdd8d 100644
--- a/webui/src/web-components/sketch-view-mode-select.test.ts
+++ b/webui/src/web-components/sketch-view-mode-select.test.ts
@@ -16,34 +16,6 @@
).toBeVisible();
});
-test("displays all four view mode buttons", async ({ mount }) => {
- const component = await mount(SketchViewModeSelect, {});
-
- // Count the number of buttons
- const buttonCount = await component.locator(".emoji-button").count();
- expect(buttonCount).toBe(4);
-
- // Check that each button exists
- await expect(component.locator("#showConversationButton")).toBeVisible();
- await expect(component.locator("#showDiffButton")).toBeVisible();
- await expect(component.locator("#showChartsButton")).toBeVisible();
- await expect(component.locator("#showTerminalButton")).toBeVisible();
-
- // Check the title attributes
- expect(
- await component.locator("#showConversationButton").getAttribute("title"),
- ).toBe("Conversation View");
- expect(await component.locator("#showDiffButton").getAttribute("title")).toBe(
- "Diff View",
- );
- expect(
- await component.locator("#showChartsButton").getAttribute("title"),
- ).toBe("Charts View");
- expect(
- await component.locator("#showTerminalButton").getAttribute("title"),
- ).toBe("Terminal View");
-});
-
test("dispatches view-mode-select event when clicking a mode button", async ({
mount,
}) => {
diff --git a/webui/src/web-components/sketch-view-mode-select.ts b/webui/src/web-components/sketch-view-mode-select.ts
index 52f8a4e..4c3c91f 100644
--- a/webui/src/web-components/sketch-view-mode-select.ts
+++ b/webui/src/web-components/sketch-view-mode-select.ts
@@ -10,39 +10,48 @@
// Header bar: view mode buttons
static styles = css`
- /* View Mode Button Styles */
- .view-mode-buttons {
+ /* Tab-style View Mode Styles */
+ .tab-nav {
display: flex;
- gap: 8px;
margin-right: 10px;
+ background-color: #f8f8f8;
+ border-radius: 4px;
+ overflow: hidden;
+ border: 1px solid #ddd;
}
- .emoji-button {
- font-size: 18px;
- width: 32px;
- height: 32px;
+ .tab-btn {
+ padding: 8px 12px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 13px;
display: flex;
align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- cursor: pointer;
+ gap: 5px;
+ color: #666;
+ border-bottom: 2px solid transparent;
transition: all 0.2s ease;
- padding: 0;
- line-height: 1;
+ white-space: nowrap;
}
- .emoji-button:hover {
+ .tab-btn:not(:last-child) {
+ border-right: 1px solid #eee;
+ }
+
+ .tab-btn:hover {
background-color: #f0f0f0;
- transform: translateY(-2px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
- .emoji-button.active {
+ .tab-btn.active {
+ border-bottom: 2px solid #4a90e2;
+ color: #4a90e2;
+ font-weight: 500;
background-color: #e6f7ff;
- border-color: #1890ff;
- color: #1890ff;
+ }
+
+ .tab-icon {
+ font-size: 16px;
}
`;
@@ -101,38 +110,42 @@
render() {
return html`
- <div class="view-mode-buttons">
+ <div class="tab-nav">
<button
id="showConversationButton"
- class="emoji-button ${this.activeMode === "chat" ? "active" : ""}"
+ class="tab-btn ${this.activeMode === "chat" ? "active" : ""}"
title="Conversation View"
@click=${() => this._handleViewModeClick("chat")}
>
- 💬
+ <span class="tab-icon">💬</span>
+ <span>Chat</span>
</button>
<button
id="showDiffButton"
- class="emoji-button ${this.activeMode === "diff" ? "active" : ""}"
+ class="tab-btn ${this.activeMode === "diff" ? "active" : ""}"
title="Diff View"
@click=${() => this._handleViewModeClick("diff")}
>
- ±
+ <span class="tab-icon">±</span>
+ <span>Diff</span>
</button>
<button
id="showChartsButton"
- class="emoji-button ${this.activeMode === "charts" ? "active" : ""}"
+ class="tab-btn ${this.activeMode === "charts" ? "active" : ""}"
title="Charts View"
@click=${() => this._handleViewModeClick("charts")}
>
- 📈
+ <span class="tab-icon">📈</span>
+ <span>Charts</span>
</button>
<button
id="showTerminalButton"
- class="emoji-button ${this.activeMode === "terminal" ? "active" : ""}"
+ class="tab-btn ${this.activeMode === "terminal" ? "active" : ""}"
title="Terminal View"
@click=${() => this._handleViewModeClick("terminal")}
>
- 💻
+ <span class="tab-icon">💻</span>
+ <span>Terminal</span>
</button>
</div>
`;