Skip to content

Commit 9d19dbb

Browse files
committed
fix(desktop): logo-only rail switcher, workspace name in status bar
Match Nexus UX: icon rail shows only the logo button (dropdown portal to the right with glass card, Workspaces header, active checkmark). Workspace basename and git branch live in the status bar as read-only context.
1 parent 8497412 commit 9d19dbb

6 files changed

Lines changed: 117 additions & 72 deletions

File tree

libs/naas-abi/naas_abi/apps/desktop/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ desktop/
124124

125125
A workspace is a **folder on disk** (VS Code / Cursor semantics), not a Nexus tenant.
126126

127-
- **UI**: icon rail top (Nexus sidebar-top pattern): ABI logo + workspace basename + chevron opens a dropdown downward (square corners). Hover shows the full path; the menu lists the current path (dimmed), recent workspaces, and **Open Folder…**. The status bar left shows the current workspace basename and git branch as read-only context (no switch action). Top bar and main body stay clean (panel toggle + section title only).
127+
- **UI**: icon rail top (Nexus sidebar-top pattern): **logo button only** opens a glass portal dropdown to the right (square corners). Hover on the logo shows the full path; the menu lists recent workspaces with a checkmark on the active entry and **Open Folder…**. The status bar left shows the current workspace basename and git branch as read-only context (no switch action). Top bar and main body stay clean (panel toggle + section title only).
128128
- **Switch / open**: `POST /api/workspaces/open` or `PUT /api/settings` with a new `workspace_root`. Triggers `ensure_workspace`, harness restart, terminal reconnect, file index refresh, org/model context reload, and graph rescaffold.
129129
- **Recent list**: `recent_workspaces` setting (JSON array, max 10 paths). Updated on every open/switch.
130130
- **First run**: `maybe_upgrade_workspace_setting()` auto-detects `~/abi` (git + `.env`) when still on the factory default.

libs/naas-abi/naas_abi/apps/desktop/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ uv run python libs/naas-abi/naas_abi/apps/desktop/run.py --browser-only
2727

2828
Open [http://127.0.0.1:54242](http://127.0.0.1:54242). Override the port with `ABI_DESKTOP_PORT` if needed.
2929

30-
Use the **workspace switcher** (icon rail top: logo + folder name + chevron) to open or switch IDE workspaces. The status bar shows the current workspace and git branch as read-only context. Each workspace is a folder on disk (e.g. `~/abi`). In browser dev, **Open Folder…** accepts a typed path; in the pywebview app, **Browse…** opens the native folder picker.
30+
Use the **workspace switcher** (icon rail top: logo only, Nexus-style) to open or switch IDE workspaces. The **status bar** (left) shows the current workspace basename and git branch as read-only context. Each workspace is a folder on disk (e.g. `~/abi`). In browser dev, **Open Folder…** accepts a typed path; in the pywebview app, **Browse…** opens the native folder picker.
3131

3232
Useful flags:
3333

libs/naas-abi/naas_abi/apps/desktop/gui/web/app.js

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2503,10 +2503,8 @@ function hasNativeFolderPicker() {
25032503

25042504
function renderWorkspaceSwitcher() {
25052505
const trigger = $("workspace-switcher");
2506-
const nameEl = $("workspace-name");
2507-
const pathEl = $("workspace-menu-path");
25082506
const listEl = $("workspace-menu-recent");
2509-
if (!trigger || !nameEl || !pathEl || !listEl) return;
2507+
if (!trigger || !listEl) return;
25102508

25112509
const active =
25122510
state.workspaces?.active ||
@@ -2518,35 +2516,35 @@ function renderWorkspaceSwitcher() {
25182516
}
25192517
: null);
25202518

2521-
const displayName = active?.name || "workspace";
25222519
const fullPath = active?.path || "Workspace root";
2523-
nameEl.textContent = displayName;
25242520
trigger.title = fullPath;
2525-
pathEl.textContent = active?.path || "";
25262521

25272522
listEl.innerHTML = "";
2528-
const recent = (state.workspaces?.recent || []).filter(
2529-
(item) => !active || item.path !== active.path
2530-
);
2531-
if (!recent.length && active) {
2523+
const items = [];
2524+
if (active) items.push(active);
2525+
for (const item of state.workspaces?.recent || []) {
2526+
if (!active || item.path !== active.path) items.push(item);
2527+
}
2528+
if (!items.length) {
25322529
const empty = document.createElement("p");
2533-
empty.className = "workspace-menu-path";
2534-
empty.textContent = "No other recent workspaces";
2530+
empty.className = "workspace-menu-empty";
2531+
empty.textContent = "No workspaces yet";
25352532
listEl.appendChild(empty);
25362533
}
2537-
for (const item of recent) {
2534+
for (const item of items) {
2535+
const isActive = Boolean(active && item.path === active.path);
25382536
const btn = document.createElement("button");
25392537
btn.type = "button";
25402538
btn.className = "workspace-menu-item";
25412539
if (!item.exists) btn.classList.add("missing");
2542-
if (active && item.path === active.path) btn.classList.add("active");
2540+
if (isActive) btn.classList.add("active");
25432541
btn.title = item.path;
25442542
btn.dataset.path = item.path;
25452543
const label = document.createElement("span");
25462544
label.className = "workspace-menu-item-name";
25472545
label.textContent = item.name;
25482546
btn.appendChild(label);
2549-
if (active && item.path === active.path) {
2547+
if (isActive) {
25502548
const check = document.createElement("span");
25512549
check.className = "workspace-menu-item-check";
25522550
check.innerHTML = icon("check", 14);
@@ -2571,9 +2569,9 @@ function positionWorkspaceMenu() {
25712569
const menu = $("workspace-menu");
25722570
if (!trigger || !menu) return;
25732571
const rect = trigger.getBoundingClientRect();
2574-
menu.style.top = `${rect.bottom + 6}px`;
2572+
menu.style.top = `${rect.top}px`;
25752573
menu.style.bottom = "auto";
2576-
menu.style.left = `${rect.left}px`;
2574+
menu.style.left = `${rect.right + 8}px`;
25772575
}
25782576

25792577
function openWorkspaceMenu() {

libs/naas-abi/naas_abi/apps/desktop/gui/web/index.html

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222
aria-expanded="false"
2323
>
2424
<img id="workspace-logo" src="/static/assets/abi-logo.png" alt="ABI" width="28" height="28" />
25-
<span class="workspace-switcher-label">
26-
<span id="workspace-name">workspace</span>
27-
<span class="workspace-chevron" data-icon="chevron-down" data-size="12"></span>
28-
</span>
2925
</button>
3026
</div>
3127
<div class="rail-nav">
@@ -395,7 +391,7 @@ <h2>Models</h2>
395391
<!-- Workspace switcher dropdown (portal, Nexus-inspired) -->
396392
<div id="workspace-menu-backdrop" class="workspace-menu-backdrop hidden"></div>
397393
<div id="workspace-menu" class="workspace-menu glass-card hidden" role="menu" aria-label="Workspaces">
398-
<p id="workspace-menu-path" class="workspace-menu-path"></p>
394+
<p class="workspace-menu-heading">Workspaces</p>
399395
<div id="workspace-menu-recent" class="workspace-menu-list"></div>
400396
<button type="button" id="workspace-menu-open-folder" class="workspace-menu-action">
401397
<span data-icon="folder" data-size="14"></span><span>Open Folder…</span>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Shell layout conventions for ABI Desktop web UI."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
WEB_DIR = Path(__file__).resolve().parent
8+
9+
10+
def _read(name: str) -> str:
11+
return (WEB_DIR / name).read_text(encoding="utf-8")
12+
13+
14+
def test_rail_logo_only_workspace_switcher() -> None:
15+
html = _read("index.html")
16+
assert 'id="workspace-switcher"' in html
17+
assert 'id="workspace-logo"' in html
18+
assert 'id="workspace-name"' not in html
19+
assert "workspace-switcher-label" not in html
20+
assert "workspace-chevron" not in html
21+
22+
23+
def test_status_bar_shows_workspace_name() -> None:
24+
html = _read("index.html")
25+
assert 'id="status-workspace"' in html
26+
assert 'id="status-git"' in html
27+
assert html.index('id="status-workspace"') < html.index('id="status-git"')
28+
29+
30+
def test_workspace_menu_nexus_style() -> None:
31+
html = _read("index.html")
32+
assert 'id="workspace-menu"' in html
33+
assert 'class="workspace-menu glass-card' in html
34+
assert "workspace-menu-heading" in html
35+
assert "Workspaces" in html
36+
assert 'id="workspace-menu-open-folder"' in html
37+
assert "workspace-menu-path" not in html
38+
39+
40+
def test_app_shell_status_bar_layout() -> None:
41+
html = _read("index.html")
42+
assert 'id="app-shell"' in html
43+
assert 'id="status-bar"' in html
44+
assert html.index('id="app-shell"') < html.index('id="status-bar"')
45+
46+
47+
def test_workspace_switcher_js_uses_status_not_rail_name() -> None:
48+
js = _read("app.js")
49+
assert "status-workspace" in js
50+
assert "workspace-name" not in js
51+
assert "workspace-menu-path" not in js
52+
assert "rect.right + 8" in js
53+
54+
55+
def test_workspace_switcher_css_logo_button() -> None:
56+
css = _read("style.css")
57+
assert ".glass-card" in css
58+
assert ".workspace-switcher" in css
59+
assert "workspace-switcher-label" not in css
60+
assert "#workspace-name" not in css

libs/naas-abi/naas_abi/apps/desktop/gui/web/style.css

Lines changed: 39 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ input, textarea, select { font: inherit; color: inherit; }
7676
-webkit-backdrop-filter: blur(12px);
7777
}
7878

79+
.glass-card {
80+
background: var(--glass-bg);
81+
backdrop-filter: blur(12px);
82+
-webkit-backdrop-filter: blur(12px);
83+
border: 1px solid var(--glass-border);
84+
box-shadow: var(--glass-shadow);
85+
border-radius: 0;
86+
}
87+
7988
#app {
8089
display: flex;
8190
flex-direction: column;
@@ -105,89 +114,71 @@ svg.icon { flex-shrink: 0; }
105114
height: 56px;
106115
flex-shrink: 0;
107116
display: flex;
108-
align-items: stretch;
117+
align-items: center;
109118
justify-content: center;
110119
padding: 0;
111120
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
112121
}
113122

114123
#workspace-logo {
115-
width: 26px;
116-
height: 26px;
124+
width: 100%;
125+
height: 100%;
117126
border-radius: 0;
118127
display: block;
119-
object-fit: contain;
128+
object-fit: cover;
120129
flex-shrink: 0;
121130
}
122131

123132
.workspace-switcher {
124133
display: flex;
125-
flex-direction: column;
126134
align-items: center;
127135
justify-content: center;
128-
gap: 3px;
129-
width: 100%;
130-
height: 100%;
131-
padding: 6px 4px;
136+
width: 36px;
137+
height: 36px;
138+
flex-shrink: 0;
139+
padding: 0;
140+
overflow: hidden;
132141
border-radius: 0;
133142
color: var(--foreground);
134-
background: transparent;
135-
border: 1px solid transparent;
143+
background: var(--workspace-accent);
144+
border: none;
136145
font: inherit;
137-
transition: background 0.15s, border-color 0.15s;
146+
transition: box-shadow 0.15s, background 0.15s;
138147
}
139148
.workspace-switcher:hover {
140-
background: var(--accent-10);
141-
border-color: color-mix(in srgb, var(--workspace-accent) 35%, transparent);
149+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--workspace-accent) 50%, transparent);
142150
}
143151
.workspace-switcher[aria-expanded="true"] {
144-
background: var(--accent-15);
145-
border-color: color-mix(in srgb, var(--workspace-accent) 45%, transparent);
146-
}
147-
.workspace-switcher-label {
148-
display: inline-flex;
149-
align-items: center;
150-
justify-content: center;
151-
gap: 1px;
152-
max-width: 100%;
153-
min-width: 0;
154-
}
155-
#workspace-name {
156-
font-size: 10px;
157-
font-weight: 600;
158-
max-width: 40px;
159-
overflow: hidden;
160-
text-overflow: ellipsis;
161-
white-space: nowrap;
162-
line-height: 1.1;
163-
}
164-
.workspace-chevron {
165-
display: inline-flex;
166-
color: var(--muted-foreground);
167-
flex-shrink: 0;
152+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--workspace-accent) 65%, transparent);
168153
}
169154

170155
.workspace-menu-backdrop {
171156
position: fixed;
172157
inset: 0;
173-
z-index: 198;
158+
z-index: 199;
174159
background: transparent;
175160
}
176161
.workspace-menu {
177162
position: fixed;
178-
z-index: 199;
179-
width: 280px;
163+
z-index: 200;
164+
width: 240px;
180165
padding: 4px 0;
181166
border-radius: 0;
182-
box-shadow: var(--glass-shadow);
183167
}
184-
.workspace-menu-path {
168+
.workspace-menu-heading {
185169
margin: 0;
186-
padding: 8px 12px 6px;
187-
font-size: 11px;
188-
line-height: 1.35;
170+
padding: 6px 12px;
171+
font-size: 10px;
172+
font-weight: 600;
173+
letter-spacing: 0.05em;
174+
text-transform: uppercase;
175+
color: var(--muted-foreground);
176+
}
177+
.workspace-menu-empty {
178+
margin: 0;
179+
padding: 8px 12px;
180+
font-size: 12px;
189181
color: var(--muted-foreground);
190-
word-break: break-all;
191182
}
192183
.workspace-menu-list {
193184
display: flex;
@@ -211,7 +202,7 @@ svg.icon { flex-shrink: 0; }
211202
background: var(--accent-10);
212203
}
213204
.workspace-menu-item.active {
214-
background: var(--accent-15);
205+
background: var(--accent-5);
215206
}
216207
.workspace-menu-item.missing {
217208
color: var(--muted-foreground);

0 commit comments

Comments
 (0)