Add container config tab skeleton
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 12s
CI / Detect changes (pull_request) Successful in 21s
CI / Python Lint & Test (pull_request) Successful in 12s
E2E Chat / detect-changes (pull_request) Successful in 30s
E2E API Smoke Test / detect-changes (pull_request) Successful in 31s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 24s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 15s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
gate-check-v3 / gate-check (pull_request) Successful in 15s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 11s
CI / Platform (Go) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m37s
E2E Chat / E2E Chat (pull_request) Successful in 23s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
Harness Replays / Harness Replays (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 9s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
CI / Canvas (Next.js) (pull_request) Successful in 6m57s
CI / all-required (pull_request) Successful in 9m8s
qa-review / approved (pull_request) Refired via /qa-recheck by core-qa
security-review / approved (pull_request) Refired via /security-recheck by core-security
CI / Canvas Deploy Reminder (pull_request) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 7/7
sop-checklist / na-declarations (pull_request) N/A: (none)
audit-force-merge / audit (pull_request) Successful in 14s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 12s
CI / Detect changes (pull_request) Successful in 21s
CI / Python Lint & Test (pull_request) Successful in 12s
E2E Chat / detect-changes (pull_request) Successful in 30s
E2E API Smoke Test / detect-changes (pull_request) Successful in 31s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 24s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 15s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
gate-check-v3 / gate-check (pull_request) Successful in 15s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 11s
CI / Platform (Go) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m37s
E2E Chat / E2E Chat (pull_request) Successful in 23s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
Harness Replays / Harness Replays (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 9s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
CI / Canvas (Next.js) (pull_request) Successful in 6m57s
CI / all-required (pull_request) Successful in 9m8s
qa-review / approved (pull_request) Refired via /qa-recheck by core-qa
security-review / approved (pull_request) Refired via /security-recheck by core-security
CI / Canvas Deploy Reminder (pull_request) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 7/7
sop-checklist / na-declarations (pull_request) N/A: (none)
audit-force-merge / audit (pull_request) Successful in 14s
This commit is contained in:
@@ -9,6 +9,7 @@ import { DetailsTab } from "./tabs/DetailsTab";
|
||||
import { SkillsTab } from "./tabs/SkillsTab";
|
||||
import { ChatTab } from "./tabs/ChatTab";
|
||||
import { ConfigTab } from "./tabs/ConfigTab";
|
||||
import { ContainerConfigTab } from "./tabs/ContainerConfigTab";
|
||||
import { DisplayTab } from "./tabs/DisplayTab";
|
||||
import { TerminalTab } from "./tabs/TerminalTab";
|
||||
import { FilesTab } from "./tabs/FilesTab";
|
||||
@@ -33,6 +34,7 @@ const TABS: { id: PanelTab; label: string; icon: string }[] = [
|
||||
{ id: "skills", label: "Plugins", icon: "✦" },
|
||||
{ id: "terminal", label: "Terminal", icon: "▸" },
|
||||
{ id: "display", label: "Display", icon: "▣" },
|
||||
{ id: "container-config", label: "Container", icon: "▤" },
|
||||
{ id: "config", label: "Config", icon: "⚙" },
|
||||
{ id: "schedule", label: "Schedule", icon: "⏲" },
|
||||
{ id: "channels", label: "Channels", icon: "⇌" },
|
||||
@@ -303,6 +305,7 @@ export function SidePanel() {
|
||||
{panelTab === "chat" && <ChatTab key={selectedNodeId} workspaceId={selectedNodeId} data={node.data} />}
|
||||
{panelTab === "terminal" && <TerminalTab key={selectedNodeId} workspaceId={selectedNodeId} data={node.data} />}
|
||||
{panelTab === "display" && <DisplayTab key={selectedNodeId} workspaceId={selectedNodeId} />}
|
||||
{panelTab === "container-config" && <ContainerConfigTab key={selectedNodeId} data={node.data} />}
|
||||
{panelTab === "config" && <ConfigTab key={selectedNodeId} workspaceId={selectedNodeId} />}
|
||||
{panelTab === "schedule" && <ScheduleTab key={selectedNodeId} workspaceId={selectedNodeId} />}
|
||||
{panelTab === "channels" && <ChannelsTab key={selectedNodeId} workspaceId={selectedNodeId} />}
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock("../tabs/DetailsTab", () => ({ DetailsTab: () => null }));
|
||||
vi.mock("../tabs/SkillsTab", () => ({ SkillsTab: () => null }));
|
||||
vi.mock("../tabs/ChatTab", () => ({ ChatTab: () => null }));
|
||||
vi.mock("../tabs/ConfigTab", () => ({ ConfigTab: () => null }));
|
||||
vi.mock("../tabs/ContainerConfigTab", () => ({ ContainerConfigTab: () => null }));
|
||||
vi.mock("../tabs/DisplayTab", () => ({ DisplayTab: () => null }));
|
||||
vi.mock("../tabs/TerminalTab", () => ({ TerminalTab: () => null }));
|
||||
vi.mock("../tabs/FilesTab", () => ({ FilesTab: () => null }));
|
||||
@@ -75,7 +76,7 @@ import { SidePanel } from "../SidePanel";
|
||||
|
||||
const TABS = [
|
||||
"chat", "activity", "details", "skills", "terminal",
|
||||
"display", "config", "schedule", "channels", "files", "memory", "traces", "events", "audit",
|
||||
"display", "container-config", "config", "schedule", "channels", "files", "memory", "traces", "events", "audit",
|
||||
];
|
||||
|
||||
describe("SidePanel — ARIA tablist pattern", () => {
|
||||
@@ -86,10 +87,10 @@ describe("SidePanel — ARIA tablist pattern", () => {
|
||||
expect(tablist.getAttribute("aria-label")).toBe("Workspace panel tabs");
|
||||
});
|
||||
|
||||
it("renders exactly 14 tab buttons", () => {
|
||||
it("renders exactly 15 tab buttons", () => {
|
||||
render(<SidePanel />);
|
||||
const tabs = screen.getAllByRole("tab");
|
||||
expect(tabs.length).toBe(14);
|
||||
expect(tabs.length).toBe(15);
|
||||
});
|
||||
|
||||
it("renders the Display tab", () => {
|
||||
@@ -97,6 +98,11 @@ describe("SidePanel — ARIA tablist pattern", () => {
|
||||
expect(document.getElementById("tab-display")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the Container Config tab", () => {
|
||||
render(<SidePanel />);
|
||||
expect(document.getElementById("tab-container-config")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("active tab (chat) has aria-selected='true'", () => {
|
||||
render(<SidePanel />);
|
||||
const chatTab = screen.getAllByRole("tab").find(
|
||||
@@ -105,11 +111,11 @@ describe("SidePanel — ARIA tablist pattern", () => {
|
||||
expect(chatTab?.getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("all other 13 tabs have aria-selected='false'", () => {
|
||||
it("all other 14 tabs have aria-selected='false'", () => {
|
||||
render(<SidePanel />);
|
||||
const tabs = screen.getAllByRole("tab");
|
||||
const inactive = tabs.filter((t) => t.id !== "tab-chat");
|
||||
expect(inactive.length).toBe(13);
|
||||
expect(inactive.length).toBe(14);
|
||||
for (const tab of inactive) {
|
||||
expect(tab.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
@@ -122,7 +128,7 @@ describe("SidePanel — ARIA tablist pattern", () => {
|
||||
const minusOnes = tabs.filter((t) => t.getAttribute("tabindex") === "-1");
|
||||
expect(zeros.length).toBe(1);
|
||||
expect(zeros[0].id).toBe("tab-chat");
|
||||
expect(minusOnes.length).toBe(13);
|
||||
expect(minusOnes.length).toBe(14);
|
||||
});
|
||||
|
||||
it("active tab has aria-controls='panel-chat' and id='tab-chat'", () => {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { runtimeDisplayName } from "@/lib/runtime-names";
|
||||
import type { WorkspaceNodeData } from "@/store/canvas";
|
||||
|
||||
type Props = {
|
||||
data: Pick<
|
||||
WorkspaceNodeData,
|
||||
"runtime" | "status" | "needsRestart" | "activeTasks" | "deliveryMode"
|
||||
| "workspaceAccess" | "maxConcurrentTasks"
|
||||
>;
|
||||
};
|
||||
|
||||
export function ContainerConfigTab({ data }: Props) {
|
||||
const runtime = data.runtime || "unknown";
|
||||
const workspaceAccess = formatAccess(data.workspaceAccess);
|
||||
const maxConcurrentTasks = data.maxConcurrentTasks ? String(data.maxConcurrentTasks) : "platform-managed";
|
||||
const mountedPath = "/workspace";
|
||||
const privilegeStatus = "standard";
|
||||
const deliveryMode = data.deliveryMode || "push";
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<section className="rounded-lg border border-line/50 bg-surface-card/40 p-4">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-ink">Container Config</h3>
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-1 gap-2 text-[11px]">
|
||||
<ConfigRow label="Runtime image" value={runtimeDisplayName(runtime)} detail={runtime} />
|
||||
<ConfigRow label="Workspace access" value={workspaceAccess} />
|
||||
<ConfigRow label="Max concurrent tasks" value={maxConcurrentTasks} />
|
||||
<ConfigRow label="Mounted workspace path" value={mountedPath} />
|
||||
<ConfigRow label="Container privileges" value={privilegeStatus} />
|
||||
<ConfigRow label="Delivery mode" value={deliveryMode} />
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-line/50 bg-surface-card/40 p-4">
|
||||
<h3 className="mb-3 text-sm font-semibold text-ink">Session Controls</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ReadOnlyAction label={data.needsRestart ? "Restart required" : "Restart"} />
|
||||
<ReadOnlyAction label="Reset session" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-line/50 bg-surface-card/40 p-4">
|
||||
<h3 className="mb-3 text-sm font-semibold text-ink">Status</h3>
|
||||
<dl className="grid grid-cols-1 gap-2 text-[11px]">
|
||||
<ConfigRow label="Container status" value={data.status} />
|
||||
<ConfigRow label="Active tasks" value={String(data.activeTasks ?? 0)} />
|
||||
<ConfigRow label="Mounted path access" value="available" />
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatAccess(value: string | null | undefined): string {
|
||||
if (!value) return "none";
|
||||
return value.replace(/_/g, "-");
|
||||
}
|
||||
|
||||
function ConfigRow({
|
||||
label,
|
||||
value,
|
||||
detail,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
detail?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 rounded-md bg-surface-sunken/40 px-3 py-2">
|
||||
<dt className="text-ink-mid">{label}</dt>
|
||||
<dd className="min-w-0 text-right">
|
||||
<div className="font-mono text-ink break-words">{value}</div>
|
||||
{detail && detail !== value && (
|
||||
<div className="mt-0.5 font-mono text-[10px] text-ink-mid break-words">{detail}</div>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyAction({ label }: { label: string }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="rounded-md border border-line/50 bg-surface-sunken/40 px-3 py-2 text-[11px] text-ink-mid disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/runtime-names", () => ({
|
||||
runtimeDisplayName: (runtime: string) => runtime,
|
||||
}));
|
||||
|
||||
import { ContainerConfigTab } from "../ContainerConfigTab";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("ContainerConfigTab", () => {
|
||||
it("renders read-only runtime and container settings separate from compute shape", () => {
|
||||
render(
|
||||
<ContainerConfigTab
|
||||
data={{
|
||||
runtime: "claude-code",
|
||||
status: "online",
|
||||
needsRestart: false,
|
||||
activeTasks: 2,
|
||||
maxConcurrentTasks: 3,
|
||||
workspaceAccess: "read_write",
|
||||
deliveryMode: "poll",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Runtime image")).toBeTruthy();
|
||||
expect(screen.getByText("claude-code")).toBeTruthy();
|
||||
expect(screen.getByText("Workspace access")).toBeTruthy();
|
||||
expect(screen.getByText("read-write")).toBeTruthy();
|
||||
expect(screen.getByText("Max concurrent tasks")).toBeTruthy();
|
||||
expect(screen.getByText("3")).toBeTruthy();
|
||||
expect(screen.getByText("/workspace")).toBeTruthy();
|
||||
expect(screen.getByText("Container privileges")).toBeTruthy();
|
||||
expect(screen.queryByText("Instance type")).toBeNull();
|
||||
expect(screen.queryByText("Root volume")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -513,6 +513,8 @@ export function buildNodesAndEdges(
|
||||
parentId: ws.parent_id,
|
||||
currentTask: ws.current_task || "",
|
||||
runtime: ws.runtime || "",
|
||||
workspaceAccess: ws.workspace_access,
|
||||
maxConcurrentTasks: ws.max_concurrent_tasks ?? null,
|
||||
needsRestart: false,
|
||||
budgetLimit: ws.budget_limit ?? null,
|
||||
budgetUsed: ws.budget_used ?? null,
|
||||
|
||||
@@ -88,6 +88,8 @@ export interface WorkspaceNodeData extends Record<string, unknown> {
|
||||
parentId: string | null;
|
||||
currentTask: string;
|
||||
runtime: string;
|
||||
workspaceAccess?: string | null;
|
||||
maxConcurrentTasks?: number | null;
|
||||
needsRestart: boolean;
|
||||
/** USD spend ceiling set by the user; null = unlimited. Added by issue #541. */
|
||||
budgetLimit: number | null;
|
||||
@@ -130,7 +132,7 @@ export interface WorkspaceNodeData extends Record<string, unknown> {
|
||||
deliveryMode?: string;
|
||||
}
|
||||
|
||||
export type PanelTab = "details" | "skills" | "chat" | "terminal" | "display" | "config" | "schedule" | "channels" | "files" | "memory" | "traces" | "events" | "activity" | "audit";
|
||||
export type PanelTab = "details" | "skills" | "chat" | "terminal" | "display" | "container-config" | "config" | "schedule" | "channels" | "files" | "memory" | "traces" | "events" | "activity" | "audit";
|
||||
|
||||
export interface ContextMenuState {
|
||||
x: number;
|
||||
|
||||
@@ -320,11 +320,13 @@ export interface WorkspaceData {
|
||||
url: string;
|
||||
parent_id: string | null;
|
||||
active_tasks: number;
|
||||
max_concurrent_tasks?: number | null;
|
||||
last_error_rate: number;
|
||||
last_sample_error: string;
|
||||
uptime_seconds: number;
|
||||
current_task: string;
|
||||
runtime: string;
|
||||
workspace_access?: string | null;
|
||||
x: number;
|
||||
y: number;
|
||||
collapsed: boolean;
|
||||
|
||||
Reference in New Issue
Block a user