Skip to content

Commit 4160b95

Browse files
committed
fix: preserve control ui parity after rebase
1 parent f291f13 commit 4160b95

32 files changed

Lines changed: 726 additions & 192 deletions

test/ui.presenter-next-run.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// UI presenter next-run tests cover presenter scheduling output.
22
import { describe, expect, it } from "vitest";
33
import { t } from "../ui/src/i18n/index.ts";
4-
import { formatNextRun } from "../ui/src/ui/presenter.ts";
4+
import { formatNextRun } from "../ui/src/lib/presenter.ts";
55

66
describe("formatNextRun", () => {
77
it("returns localized n/a for nullish values", () => {

ui/src/api/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ export type GatewaySessionRow = {
475475
room?: string;
476476
space?: string;
477477
updatedAt: number | null;
478+
archived?: boolean;
479+
archivedAt?: number;
480+
pinned?: boolean;
481+
pinnedAt?: number;
478482
sessionId?: string;
479483
systemSent?: boolean;
480484
abortedLastRun?: boolean;
@@ -494,7 +498,6 @@ export type GatewaySessionRow = {
494498
totalTokens?: number;
495499
totalTokensFresh?: boolean;
496500
status?: SessionRunStatus;
497-
archived?: boolean;
498501
hasActiveRun?: boolean;
499502
subagentRunState?: SubagentRunState;
500503
hasActiveSubagentRun?: boolean;

ui/src/app-navigation.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ describe("inferBasePathFromPathname", () => {
227227
expect(inferBasePathFromPathname("/apps/openclaw/sessions")).toBe("/apps/openclaw");
228228
});
229229

230+
it("preserves mount roots without a route suffix", () => {
231+
expect(inferBasePathFromPathname("/__openclaw__/")).toBe("/__openclaw__");
232+
expect(inferBasePathFromPathname("/apps/openclaw/")).toBe("/apps/openclaw");
233+
expect(inferBasePathFromPathname("/typo")).toBe("");
234+
});
235+
230236
it("handles index.html suffix", () => {
231237
expect(inferBasePathFromPathname("/index.html")).toBe("");
232238
expect(inferBasePathFromPathname("/ui/index.html")).toBe("/ui");

ui/src/app-route-paths.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function routeIdFromPath(pathname: string, basePath = ""): RouteId | null
6666
}
6767

6868
export function inferBasePathFromPathname(pathname: string): string {
69+
const isMountRoot = pathname.trim().endsWith("/");
6970
const normalizedPath = normalizePath(pathname);
7071
if (normalizedPath.toLowerCase().endsWith("/index.html")) {
7172
return normalizeBasePath(normalizedPath.slice(0, -"/index.html".length));
@@ -95,7 +96,7 @@ export function inferBasePathFromPathname(pathname: string): string {
9596
}
9697
return index ? `/${segments.slice(0, index).join("/")}` : "";
9798
}
98-
return "";
99+
return isMountRoot && segments.length ? `/${segments.join("/")}` : "";
99100
}
100101

101102
export function locationForRoute(routeId: RouteId, basePath: string): RouteLocation {

ui/src/app/app-host.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import "../components/app-topbar.ts";
99
import "../components/exec-approval.ts";
1010
import "../components/gateway-url-confirmation.ts";
1111
import "../components/login-gate.ts";
12+
import "../components/terminal/terminal-panel.ts";
1213
import "../components/tooltip.ts";
1314
import "../components/update-banner.ts";
1415
import { APP_ROUTE_IDS, isRouteId, pathForRoute, type RouteId } from "../app-routes.ts";
@@ -18,6 +19,7 @@ import {
1819
type CommandPaletteTargetDetail,
1920
} from "../components/command-palette.ts";
2021
import type { ThemeModeChangeDetail } from "../components/theme-mode-toggle.ts";
22+
import { isGatewayMethodAdvertised } from "../lib/gateway-methods.ts";
2123
import { searchForSession } from "../lib/sessions/index.ts";
2224
import { resolveAgentIdFromSessionKey } from "../lib/sessions/session-key.ts";
2325
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../lib/string-coerce.ts";
@@ -27,6 +29,7 @@ import {
2729
type ApplicationContext,
2830
type ApplicationNavigationOptions,
2931
} from "./context.ts";
32+
import { hasOperatorAdminAccess } from "./operator-access.ts";
3033
import type { ApplicationOverlaySnapshot } from "./overlays.ts";
3134
import { selectRenderedRouteMatch } from "./router-outlet.ts";
3235

@@ -71,6 +74,23 @@ function resolveOnboardingMode(): boolean {
7174
return raw !== null && /^(?:1|true|yes|on)$/iu.test(raw.trim());
7275
}
7376

77+
function resolveTerminalThemeMode(): "dark" | "light" {
78+
return document.documentElement.dataset.themeMode === "light" ? "light" : "dark";
79+
}
80+
81+
function isTerminalAvailable(
82+
snapshot: ApplicationContext["gateway"]["snapshot"],
83+
terminalEnabled: boolean,
84+
): boolean {
85+
if (!snapshot.connected || !terminalEnabled) {
86+
return false;
87+
}
88+
return (
89+
hasOperatorAdminAccess(snapshot.hello?.auth ?? null) &&
90+
isGatewayMethodAdvertised(snapshot, "terminal.open") === true
91+
);
92+
}
93+
7494
export class OpenClawApp extends LitElement {
7595
@state() private gatewayConnected = false;
7696
@state() private gatewayLastError: string | null = null;
@@ -232,6 +252,8 @@ class OpenClawShell extends LitElement {
232252
@state() private recentSessionsCollapsed = false;
233253
@state() private navDrawerOpen = false;
234254
@state() private gatewayConnected = false;
255+
@state() private terminalAvailable = false;
256+
@state() private terminalClient: GatewayBrowserClient | null = null;
235257
@state() private activeSessionKey = "";
236258
@state() private agentLabel = "";
237259
@state() private routeState: ShellRouteState = {};
@@ -249,10 +271,12 @@ class OpenClawShell extends LitElement {
249271
private agentsListClient: GatewayBrowserClient | null = null;
250272
private sessionKeyClient: GatewayBrowserClient | null = null;
251273
private stopAgentsSubscription: (() => void) | undefined;
274+
private stopConfigSubscription: (() => void) | undefined;
252275
private stopGatewaySubscription: (() => void) | undefined;
253276
private stopNavigationSubscription: (() => void) | undefined;
254277
private stopRouteSubscription: (() => void) | undefined;
255278
private stopOverlaySubscription: (() => void) | undefined;
279+
private stopThemeSubscription: (() => void) | undefined;
256280

257281
override createRenderRoot() {
258282
return this;
@@ -275,10 +299,12 @@ class OpenClawShell extends LitElement {
275299
!runtime ||
276300
!context ||
277301
this.stopAgentsSubscription ||
302+
this.stopConfigSubscription ||
278303
this.stopGatewaySubscription ||
279304
this.stopNavigationSubscription ||
280305
this.stopRouteSubscription ||
281-
this.stopOverlaySubscription
306+
this.stopOverlaySubscription ||
307+
this.stopThemeSubscription
282308
) {
283309
return;
284310
}
@@ -288,13 +314,19 @@ class OpenClawShell extends LitElement {
288314
});
289315
this.updateGatewaySessionKey(context.gateway.snapshot);
290316
this.updateGatewayStatus(context.gateway.snapshot);
317+
this.updateTerminalSurface(context.gateway.snapshot);
291318
this.updateAgentLabel();
292319
this.stopGatewaySubscription = context.gateway.subscribe((snapshot) => {
293320
this.updateGatewaySessionKey(snapshot);
294321
this.updateGatewayStatus(snapshot);
322+
this.updateTerminalSurface(snapshot);
295323
this.updateAgentLabel();
296324
this.ensureAgentsList(snapshot);
297325
});
326+
this.stopConfigSubscription = context.config.subscribe(() => {
327+
this.updateTerminalSurface(context.gateway.snapshot);
328+
});
329+
this.stopThemeSubscription = context.theme.subscribe(() => this.requestUpdate());
298330
this.stopAgentsSubscription = context.agents.subscribe(() => {
299331
this.updateAgentLabel();
300332
});
@@ -316,6 +348,8 @@ class OpenClawShell extends LitElement {
316348
this.removeEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget);
317349
this.stopAgentsSubscription?.();
318350
this.stopAgentsSubscription = undefined;
351+
this.stopConfigSubscription?.();
352+
this.stopConfigSubscription = undefined;
319353
this.stopGatewaySubscription?.();
320354
this.stopGatewaySubscription = undefined;
321355
this.stopNavigationSubscription?.();
@@ -324,8 +358,11 @@ class OpenClawShell extends LitElement {
324358
this.stopRouteSubscription = undefined;
325359
this.stopOverlaySubscription?.();
326360
this.stopOverlaySubscription = undefined;
361+
this.stopThemeSubscription?.();
362+
this.stopThemeSubscription = undefined;
327363
this.agentsListClient = null;
328364
this.sessionKeyClient = null;
365+
this.terminalClient = null;
329366
this.navDrawerTrigger = null;
330367
super.disconnectedCallback();
331368
}
@@ -429,6 +466,14 @@ class OpenClawShell extends LitElement {
429466
this.gatewayConnected = snapshot.connected;
430467
};
431468

469+
private updateTerminalSurface(snapshot: ApplicationContext["gateway"]["snapshot"]) {
470+
this.terminalClient = snapshot.connected ? snapshot.client : null;
471+
this.terminalAvailable = isTerminalAvailable(
472+
snapshot,
473+
this.context?.config.current.terminalEnabled ?? false,
474+
);
475+
}
476+
432477
private ensureAgentsList(snapshot: { client: GatewayBrowserClient | null; connected: boolean }) {
433478
if (!snapshot.connected || !snapshot.client) {
434479
this.agentsListClient = null;
@@ -449,11 +494,11 @@ class OpenClawShell extends LitElement {
449494
client: GatewayBrowserClient | null;
450495
sessionKey: string;
451496
}) {
452-
if (snapshot.client === this.sessionKeyClient && this.activeSessionKey) {
497+
const sessionKey = snapshot.sessionKey.trim();
498+
if (snapshot.client === this.sessionKeyClient && sessionKey === this.activeSessionKey) {
453499
return;
454500
}
455501
this.sessionKeyClient = snapshot.client;
456-
const sessionKey = snapshot.sessionKey.trim();
457502
if (sessionKey) {
458503
this.activeSessionKey = sessionKey;
459504
}
@@ -533,6 +578,9 @@ class OpenClawShell extends LitElement {
533578
.themeMode=${context.theme.mode}
534579
.onboarding=${this.onboarding}
535580
.onOpenPalette=${this.openPalette}
581+
.terminalAvailable=${this.terminalAvailable}
582+
.onToggleTerminal=${() =>
583+
window.dispatchEvent(new CustomEvent("openclaw:terminal-toggle"))}
536584
.onToggleDrawer=${(trigger: HTMLElement) => this.toggleNavDrawer(trigger)}
537585
.onNavigate=${(routeId: string, options?: ApplicationNavigationOptions) =>
538586
this.navigate(routeId, options)}
@@ -593,6 +641,11 @@ class OpenClawShell extends LitElement {
593641
.onNotFound=${() => this.replaceChatWithCurrentSession()}
594642
></openclaw-router-outlet>
595643
</main>
644+
<openclaw-terminal-panel
645+
.client=${this.terminalClient}
646+
.available=${this.terminalAvailable}
647+
.themeMode=${resolveTerminalThemeMode()}
648+
></openclaw-terminal-panel>
596649
<openclaw-exec-approval
597650
.props=${{
598651
queue: this.overlaySnapshot.approvalQueue,

0 commit comments

Comments
 (0)