feat: add mocked story-book design gallery (#1069)

* feat(ui): add story-book playground app for mocked UI development

* refactor(ui): slim story-book down to a thin wrapper

* feat(ui): add mocked story-book design gallery

* refactor(story-book): use real session shell panels
This commit is contained in:
ben
2026-03-20 13:33:28 -07:00
committed by GitHub
parent 9603be37d2
commit ddd2e2bb34
9 changed files with 1043 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenWork Story Book</title>
<link rel="icon" type="image/svg+xml" href="/openwork-mark.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<script>
(function () {
try {
var mode = localStorage.getItem("openwork.themePref") || "system";
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var resolved = mode === "dark" || (mode === "system" && prefersDark) ? "dark" : "light";
document.documentElement.dataset.theme = resolved;
document.documentElement.style.colorScheme = resolved;
} catch (e) {
document.documentElement.dataset.theme = "light";
document.documentElement.style.colorScheme = "light";
}
})();
</script>
</head>
<body class="bg-dls-surface text-dls-text">
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,45 @@
{
"name": "@openwork/story-book",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"tailwindcss": "^4.1.18",
"typescript": "^5.6.3",
"vite": "^6.0.1",
"vite-plugin-solid": "^2.11.0"
},
"packageManager": "pnpm@10.27.0",
"dependencies": {
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.0",
"@opencode-ai/sdk": "^1.1.31",
"@radix-ui/colors": "^3.0.0",
"@solid-primitives/event-bus": "^1.1.2",
"@solid-primitives/storage": "^4.3.3",
"@solidjs/router": "^0.15.4",
"@tanstack/solid-virtual": "^3.13.19",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-deep-link": "^2.4.7",
"@tauri-apps/plugin-dialog": "~2.6.0",
"@tauri-apps/plugin-http": "~2.5.6",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-process": "~2.3.1",
"@tauri-apps/plugin-updater": "~2.9.0",
"fuzzysort": "^3.1.0",
"jsonc-parser": "^3.2.1",
"lucide-solid": "^0.562.0",
"marked": "^17.0.1",
"solid-js": "^1.9.0"
}
}

View File

@@ -0,0 +1,58 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "../../app/src/app/index.css";
import { PlatformProvider, type Platform } from "../../app/src/app/context/platform";
import { bootstrapTheme } from "../../app/src/app/theme";
import { isTauriRuntime } from "../../app/src/app/utils";
import { initLocale } from "../../app/src/i18n";
import StoryBookApp from "./story-book";
bootstrapTheme();
initLocale();
const root = document.getElementById("root");
if (!root) {
throw new Error("Root element not found");
}
const platform: Platform = {
platform: isTauriRuntime() ? "desktop" : "web",
openLink(url: string) {
if (isTauriRuntime()) {
void import("@tauri-apps/plugin-opener")
.then(({ openUrl }) => openUrl(url))
.catch(() => undefined);
return;
}
window.open(url, "_blank");
},
restart: async () => {
if (isTauriRuntime()) {
const { relaunch } = await import("@tauri-apps/plugin-process");
await relaunch();
return;
}
window.location.reload();
},
notify: async () => undefined,
storage: (name) => {
const prefix = name ? `${name}:` : "";
return {
getItem: (key) => window.localStorage.getItem(prefix + key),
setItem: (key, value) => window.localStorage.setItem(prefix + key, value),
removeItem: (key) => window.localStorage.removeItem(prefix + key),
};
},
fetch,
};
render(
() => (
<PlatformProvider value={platform}>
<StoryBookApp />
</PlatformProvider>
),
root,
);

View File

@@ -0,0 +1,203 @@
import type { WorkspaceInfo } from "../../app/src/app/lib/tauri";
export type StoryScreen = "session" | "settings" | "components" | "onboarding";
export type StoryStep = {
label: string;
detail: string;
state: "done" | "active" | "queued";
};
export type StoryMessage = {
role: "user" | "assistant";
title: string;
detail: string;
body: string[];
steps?: StoryStep[];
tags?: string[];
};
export const storyWorkspaces: WorkspaceInfo[] = [
{
id: "local-foundation",
name: "Local Foundation",
displayName: "OpenWork App",
path: "~/OpenWork/app",
preset: "starter",
workspaceType: "local",
},
{
id: "remote-worker",
name: "Remote Worker",
displayName: "Ops Worker",
path: "remote://ops-worker",
preset: "automation",
workspaceType: "remote",
remoteType: "openwork",
baseUrl: "https://worker.openworklabs.com/opencode",
openworkHostUrl: "https://worker.openworklabs.com",
openworkWorkspaceName: "Ops Worker",
sandboxBackend: "docker",
sandboxContainerName: "openwork-ops-worker",
},
];
export const sessionList = [
{ title: "Refresh den cloud worker states", meta: "6m ago", active: true },
{ title: "Polish mobile workspace connect flow", meta: "31m ago", active: false },
{ title: "Audit scheduler screenshots", meta: "Yesterday", active: false },
{ title: "Tighten status copy in settings", meta: "Yesterday", active: false },
];
export const progressItems = [
{ label: "Connect provider", done: true },
{ label: "Review shell layout", done: true },
{ label: "Mock key session states", done: false },
{ label: "Capture PR screenshots", done: false },
];
export const sessionMessages: StoryMessage[] = [
{
role: "user",
title: "Prompt",
detail: "Design review",
body: [
"Build a faithful story-book for the OpenWork app so we can iterate on the shell, session timeline, settings cards, and onboarding without touching the live runtime.",
],
tags: ["/design-review", "@openwork", "3 surfaces"],
},
{
role: "assistant",
title: "OpenWork",
detail: "Core shell recreation",
body: [
"The story-book should preserve the current app DNA: restrained chrome, pale shell surfaces, dense operational cards, and a strong bottom status rail.",
"I recreated the main session surface with mock workspaces, timeline steps, artifacts, and a composer so design changes can happen in one isolated place.",
],
steps: [
{
label: "Audit current shell",
detail: "left rail widths, center reading column, right utility rail",
state: "done",
},
{
label: "Rebuild with mocked data",
detail: "session transcript, queue state, artifacts, composer",
state: "done",
},
{
label: "Layer design states",
detail: "settings, onboarding, component primitives",
state: "active",
},
],
tags: ["Session", "Mocked data", "Operational UI"],
},
{
role: "assistant",
title: "Artifacts",
detail: "Design deliverables",
body: [
"The gallery keeps the real tokens and primitive buttons from apps/app, but swaps runtime state for stable mocked snapshots.",
],
steps: [
{
label: "Token fidelity",
detail: "shared colors, typography, radii, shadows",
state: "done",
},
{
label: "Scenario coverage",
detail: "session, settings, components, onboarding",
state: "queued",
},
],
tags: ["Design system", "Reusable shell"],
},
];
export const artifactItems = [
{ title: "Session shell", detail: "Primary flow with activity, tools, and composer" },
{ title: "Settings cards", detail: "Runtime, provider, and update states" },
{ title: "Onboarding canvas", detail: "First-run decisions and worker connect" },
{ title: "Primitive kit", detail: "Buttons, chips, inputs, and status rail" },
];
export const settingsTabs = ["General", "Cloud", "Model", "Advanced", "Debug"] as const;
export const settingsCards = [
{
title: "Runtime",
eyebrow: "Core services",
body: "Status for your local engine and OpenWork server with versioning, connection health, and repair actions.",
points: [
"OpenCode engine ready on localhost:4096",
"OpenWork server proxied for remote workers",
"Developer mode enabled for design QA",
],
action: "Reconnect runtime",
},
{
title: "Providers",
eyebrow: "Models + auth",
body: "Compact surface for provider connection state, default model choice, and reasoning depth defaults.",
points: [
"Anthropic connected",
"OpenAI connected",
"Default model: Claude Sonnet 4",
],
action: "Manage providers",
},
{
title: "Remote worker",
eyebrow: "Cloud worker",
body: "Connection card for hosted workspaces with URL, token state, and reconnect controls.",
points: [
"Worker URL copied into the shell",
"Last heartbeat 18s ago",
"Sandbox container detected",
],
action: "Refresh worker",
},
{
title: "Updates",
eyebrow: "Desktop",
body: "Patch notes and delivery state for the desktop app, orchestrator, and router sidecars.",
points: [
"Auto-check weekly",
"Download on Wi-Fi only",
"Restart banner prepared",
],
action: "Check for updates",
},
];
export const onboardingChoices = [
{
title: "Create local workspace",
detail: "Spin up a local OpenWork folder with starter automations and project memory.",
},
{
title: "Connect remote worker",
detail: "Attach to a hosted worker using OpenWork URL + token for shared remote execution.",
},
];
export const screenCopy: Record<StoryScreen, { title: string; detail: string }> = {
session: {
title: "Session shell",
detail: "The full operational canvas: left rail, timeline, composer, utility rail, and status bar.",
},
settings: {
title: "Settings stack",
detail: "Dense control cards for runtime health, providers, remote workers, and update handling.",
},
components: {
title: "Core components",
detail: "Buttons, inputs, chips, cards, status rail, and other primitives pulled from the live app language.",
},
onboarding: {
title: "Onboarding",
detail: "First-run surfaces for theme choice, workspace creation, and remote worker connection.",
},
};

View File

@@ -0,0 +1,576 @@
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
import type { Component, JSX } from "solid-js";
import "@radix-ui/colors/gray.css";
import "@radix-ui/colors/gray-alpha.css";
import "@radix-ui/colors/blue.css";
import "@radix-ui/colors/blue-alpha.css";
import "@radix-ui/colors/orange.css";
import "@radix-ui/colors/orange-alpha.css";
import "@radix-ui/colors/red.css";
import "@radix-ui/colors/red-alpha.css";
import {
Box,
ChevronLeft,
ChevronRight,
Command,
History,
Layers3,
MessageCircle,
Moon,
Search,
SlidersHorizontal,
Sun,
Zap,
} from "lucide-solid";
import Button from "../../app/src/app/components/button";
import DenSettingsPanel from "../../app/src/app/components/den-settings-panel";
import OpenWorkLogo from "../../app/src/app/components/openwork-logo";
import StatusBar from "../../app/src/app/components/status-bar";
import ArtifactsPanel from "../../app/src/app/components/session/artifacts-panel";
import Composer from "../../app/src/app/components/session/composer";
import InboxPanel from "../../app/src/app/components/session/inbox-panel";
import MessageList from "../../app/src/app/components/session/message-list";
import WorkspaceSessionList from "../../app/src/app/components/session/workspace-session-list";
import { createWorkspaceShellLayout } from "../../app/src/app/lib/workspace-shell-layout";
import {
applyThemeMode,
getInitialThemeMode,
persistThemeMode,
subscribeToSystemTheme,
type ThemeMode,
} from "../../app/src/app/theme";
import type {
ComposerDraft,
McpStatusMap,
MessageWithParts,
SlashCommandOption,
WorkspaceConnectionState,
WorkspaceSessionGroup,
} from "../../app/src/app/types";
import { sessionMessages, storyWorkspaces } from "./mock-data";
type RightRailNav = "automations" | "skills" | "extensions" | "messaging" | "advanced";
const themeModes: ThemeMode[] = ["system", "light", "dark"];
const localWorkspace = storyWorkspaces[0] ?? {
id: "local-foundation",
name: "Local Foundation",
displayName: "OpenWork App",
path: "~/OpenWork/app",
preset: "starter",
workspaceType: "local" as const,
};
const remoteWorkspace = storyWorkspaces[1] ?? {
id: "remote-worker",
name: "Remote Worker",
displayName: "Ops Worker",
path: "remote://ops-worker",
preset: "automation",
workspaceType: "remote" as const,
remoteType: "openwork" as const,
baseUrl: "https://worker.openworklabs.com/opencode",
openworkHostUrl: "https://worker.openworklabs.com",
openworkWorkspaceName: "Ops Worker",
sandboxBackend: "docker" as const,
sandboxContainerName: "openwork-ops-worker",
};
const now = Date.now();
const workspaceSessionGroups: WorkspaceSessionGroup[] = [
{
workspace: localWorkspace,
status: "ready",
sessions: [
{
id: "sb-session-shell",
title: "Story shell parity with session.tsx",
slug: "story-shell-parity",
time: { updated: now - 2 * 60 * 1000, created: now - 22 * 60 * 1000 },
},
{
id: "sb-session-provider",
title: "Provider states and status rail",
slug: "provider-states",
time: { updated: now - 18 * 60 * 1000, created: now - 56 * 60 * 1000 },
},
{
id: "sb-session-mobile",
title: "Mobile shell spacing pass",
slug: "mobile-shell-pass",
time: { updated: now - 56 * 60 * 1000, created: now - 3 * 60 * 60 * 1000 },
},
],
},
{
workspace: remoteWorkspace,
status: "ready",
sessions: [
{
id: "sb-session-remote",
title: "Remote worker onboarding",
slug: "remote-worker-onboarding",
time: { updated: now - 7 * 60 * 1000, created: now - 2 * 60 * 60 * 1000 },
},
{
id: "sb-session-inbox",
title: "Inbox upload behavior",
slug: "inbox-upload",
time: { updated: now - 35 * 60 * 1000, created: now - 6 * 60 * 60 * 1000 },
},
],
},
];
const workspaceConnectionStateById: Record<string, WorkspaceConnectionState> = {
[localWorkspace.id]: { status: "connected", message: "Local engine ready" },
[remoteWorkspace.id]: { status: "connected", message: "Connected via token" },
};
const sessionStatusById: Record<string, string> = {
"sb-session-shell": "running",
"sb-session-provider": "idle",
"sb-session-mobile": "idle",
"sb-session-remote": "idle",
"sb-session-inbox": "idle",
};
const mcpStatuses: McpStatusMap = {
browser: { status: "connected" },
notion: { status: "connected" },
linear: { status: "needs_auth" },
};
const workingFiles = [
"apps/story-book/src/story-book.tsx",
"apps/app/src/app/pages/session.tsx",
"apps/app/src/app/components/session/workspace-session-list.tsx",
"apps/app/src/app/components/session/inbox-panel.tsx",
];
const artifactFiles = [
"apps/story-book/pr/mock-shell-wireframe.md",
"apps/story-book/pr/right-rail-context.png",
"apps/story-book/pr/status-bar-provider-state.png",
"apps/story-book/pr/session-layout-notes.md",
];
const commandOptions: SlashCommandOption[] = [
{ id: "design-review", name: "design-review", description: "Open a design review pass", source: "command" },
{ id: "test-flow", name: "test-flow", description: "Run shell flow checks", source: "skill" },
];
function toMessageParts(id: string, role: "user" | "assistant", text: string): MessageWithParts {
return {
info: {
id,
sessionID: "story-shell-session",
role,
time: { created: Date.now() },
} as MessageWithParts["info"],
parts: [{ type: "text", text } as MessageWithParts["parts"][number]],
};
}
function initialStoryMessages(): MessageWithParts[] {
return sessionMessages.map((message, index) => {
const header = `${message.title}${message.detail ? ` - ${message.detail}` : ""}`;
const body = [header, ...message.body].filter(Boolean).join("\n\n");
return toMessageParts(`sb-msg-${index + 1}`, message.role, body);
});
}
const RightRailButton: Component<{
label: string;
icon: JSX.Element;
active: boolean;
expanded: boolean;
onClick: () => void;
}> = (props) => (
<button
type="button"
class={`flex h-10 w-full items-center rounded-xl px-2.5 text-sm transition-colors ${
props.active
? "bg-dls-surface text-dls-text shadow-[var(--dls-card-shadow)]"
: "text-gray-10 hover:bg-gray-2/70 hover:text-dls-text"
}`}
onClick={props.onClick}
title={props.label}
aria-label={props.label}
>
<span class={`inline-flex h-8 w-8 shrink-0 items-center justify-center ${props.active ? "text-dls-text" : "text-gray-9"}`}>
{props.icon}
</span>
<Show when={props.expanded}>
<span class="truncate">{props.label}</span>
</Show>
</button>
);
export default function StoryBookApp() {
const [activeWorkspaceId, setActiveWorkspaceId] = createSignal(localWorkspace.id);
const [selectedSessionId, setSelectedSessionId] = createSignal<string | null>("sb-session-shell");
const [rightRailNav, setRightRailNav] = createSignal<RightRailNav>("automations");
const [themeMode, setThemeMode] = createSignal<ThemeMode>(getInitialThemeMode());
const [composerPrompt, setComposerPrompt] = createSignal(
"Use this mock shell to design layout changes before touching the live session runtime.",
);
const [composerToast, setComposerToast] = createSignal<string | null>(null);
const [selectedAgent, setSelectedAgent] = createSignal<string | null>(null);
const [agentPickerOpen, setAgentPickerOpen] = createSignal(false);
const [messageRows, setMessageRows] = createSignal<MessageWithParts[]>(initialStoryMessages());
const [expandedStepIds, setExpandedStepIds] = createSignal(new Set<string>());
const {
leftSidebarWidth,
rightSidebarExpanded,
rightSidebarWidth,
startLeftSidebarResize,
toggleRightSidebar,
} = createWorkspaceShellLayout({ expandedRightWidth: 320 });
createEffect(() => {
const mode = themeMode();
persistThemeMode(mode);
applyThemeMode(mode);
});
createEffect(() => {
const unsubscribeSystemTheme = subscribeToSystemTheme(() => {
if (themeMode() === "system") {
applyThemeMode("system");
}
});
onCleanup(() => unsubscribeSystemTheme());
});
const selectedSessionTitle = createMemo(() => {
const target = selectedSessionId();
if (!target) return "New session";
for (const group of workspaceSessionGroups) {
const found = group.sessions.find((session) => session.id === target);
if (found) return found.title;
}
return "New session";
});
const showingSettings = createMemo(() => rightRailNav() === "advanced");
const agentLabel = createMemo(() => (selectedAgent() ? `@${selectedAgent()}` : "Auto"));
const handleDraftChange = (draft: ComposerDraft) => {
setComposerPrompt(draft.text);
};
const handleSend = (draft: ComposerDraft) => {
const text = (draft.resolvedText ?? draft.text ?? "").trim();
if (!text) return;
const nowStamp = Date.now();
setMessageRows((current) => [
...current,
toMessageParts(`sb-user-${nowStamp}`, "user", text),
toMessageParts(
`sb-assistant-${nowStamp}`,
"assistant",
"Story-book mock response: message accepted. This uses app MessageList + Composer with local mock state.",
),
]);
setComposerPrompt("");
};
const renderRightRail = (expanded: boolean) => (
<div class="flex h-full w-full flex-col overflow-hidden rounded-[24px] border border-dls-border bg-dls-sidebar p-3 transition-[width] duration-200">
<div class={`flex items-center pb-3 ${expanded ? "justify-end" : "justify-center"}`}>
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-[16px] text-gray-10 transition-colors hover:bg-dls-surface hover:text-dls-text"
onClick={toggleRightSidebar}
title={expanded ? "Collapse sidebar" : "Expand sidebar"}
aria-label={expanded ? "Collapse sidebar" : "Expand sidebar"}
>
<Show when={expanded} fallback={<ChevronLeft size={18} />}>
<ChevronRight size={18} />
</Show>
</button>
</div>
<div class={`flex-1 overflow-y-auto ${expanded ? "space-y-5 pt-1" : "space-y-3 pt-1"}`}>
<div class="space-y-1 mb-2">
<RightRailButton
label="Automations"
icon={<History size={18} />}
active={rightRailNav() === "automations"}
expanded={expanded}
onClick={() => setRightRailNav("automations")}
/>
<RightRailButton
label="Skills"
icon={<Zap size={18} />}
active={rightRailNav() === "skills"}
expanded={expanded}
onClick={() => setRightRailNav("skills")}
/>
<RightRailButton
label="Extensions"
icon={<Box size={18} />}
active={rightRailNav() === "extensions"}
expanded={expanded}
onClick={() => setRightRailNav("extensions")}
/>
<RightRailButton
label="Messaging"
icon={<MessageCircle size={18} />}
active={rightRailNav() === "messaging"}
expanded={expanded}
onClick={() => setRightRailNav("messaging")}
/>
<RightRailButton
label="Advanced"
icon={<SlidersHorizontal size={18} />}
active={rightRailNav() === "advanced"}
expanded={expanded}
onClick={() => setRightRailNav("advanced")}
/>
</div>
<Show when={expanded && activeWorkspaceId() === remoteWorkspace.id}>
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)]">
<InboxPanel
id="sidebar-inbox"
client={null}
workspaceId={null}
onToast={(message) => setComposerToast(message)}
/>
</div>
</Show>
<Show when={expanded}>
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)]">
<ArtifactsPanel
id="sidebar-artifacts"
files={artifactFiles}
workspaceRoot="/Users/benjaminshafii/openwork-enterprise/_repos/openwork"
/>
</div>
</Show>
</div>
</div>
);
return (
<div class="h-[100dvh] min-h-screen w-full overflow-hidden bg-[var(--dls-app-bg)] p-3 md:p-4 text-gray-12 font-sans">
<div class="flex h-full w-full gap-3 md:gap-4">
<aside
class="relative hidden lg:flex shrink-0 flex-col overflow-hidden rounded-[24px] border border-dls-border bg-dls-sidebar p-2.5"
style={{
width: `${leftSidebarWidth()}px`,
"min-width": `${leftSidebarWidth()}px`,
}}
>
<div class="min-h-0 flex-1">
<WorkspaceSessionList
workspaceSessionGroups={workspaceSessionGroups}
activeWorkspaceId={activeWorkspaceId()}
selectedSessionId={selectedSessionId()}
showSessionActions
sessionStatusById={sessionStatusById}
connectingWorkspaceId={null}
workspaceConnectionStateById={workspaceConnectionStateById}
newTaskDisabled={false}
importingWorkspaceConfig={false}
onActivateWorkspace={(workspaceId) => {
setActiveWorkspaceId(workspaceId);
return true;
}}
onOpenSession={(workspaceId, sessionId) => {
setActiveWorkspaceId(workspaceId);
setSelectedSessionId(sessionId);
}}
onCreateTaskInWorkspace={(workspaceId) => {
setActiveWorkspaceId(workspaceId);
}}
onOpenRenameSession={() => undefined}
onOpenDeleteSession={() => undefined}
onOpenRenameWorkspace={() => undefined}
onShareWorkspace={() => undefined}
onRevealWorkspace={() => undefined}
onRecoverWorkspace={() => true}
onTestWorkspaceConnection={() => true}
onEditWorkspaceConnection={() => undefined}
onForgetWorkspace={() => undefined}
onOpenCreateWorkspace={() => undefined}
onOpenCreateRemoteWorkspace={() => undefined}
onImportWorkspaceConfig={() => undefined}
/>
</div>
<div
class="absolute right-0 top-3 hidden h-[calc(100%-24px)] w-2 translate-x-1/2 cursor-col-resize rounded-full bg-transparent transition-colors hover:bg-gray-6/40 lg:block"
onPointerDown={startLeftSidebarResize}
title="Resize workspace column"
aria-label="Resize workspace column"
/>
</aside>
<main class="min-w-0 flex-1 flex flex-col overflow-hidden rounded-[24px] border border-dls-border bg-dls-surface shadow-[var(--dls-shell-shadow)]">
<header class="z-10 flex h-12 shrink-0 items-center justify-between border-b border-dls-border bg-dls-surface px-4 md:px-6">
<div class="flex min-w-0 items-center gap-3">
<OpenWorkLogo size={18} />
<span class="shrink-0 rounded-md bg-dls-hover px-2 py-1 text-[11px] font-medium text-dls-secondary">
Workspace
</span>
<h1 class="truncate text-[15px] font-semibold text-dls-text">
{showingSettings() ? "Settings" : selectedSessionTitle()}
</h1>
</div>
<div class="flex items-center gap-1.5 text-gray-10">
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-md transition-colors hover:bg-gray-2/70 hover:text-dls-text"
title="Search conversation"
aria-label="Search conversation"
>
<Search size={16} />
</button>
<button
type="button"
class="hidden h-9 w-9 items-center justify-center rounded-md transition-colors hover:bg-gray-2/70 hover:text-dls-text sm:flex"
title="Command palette"
aria-label="Command palette"
>
<Command size={16} />
</button>
<div class="hidden items-center gap-1 sm:flex">
<For each={themeModes}>
{(mode) => (
<button
type="button"
onClick={() => setThemeMode(mode)}
class={`inline-flex h-8 items-center gap-1.5 rounded-full border px-3 text-xs transition-colors ${
themeMode() === mode
? "border-dls-border bg-dls-hover text-dls-text"
: "border-transparent text-dls-secondary hover:bg-gray-2/70 hover:text-dls-text"
}`}
>
<Show
when={mode === "light"}
fallback={<Show when={mode === "dark"} fallback={<Layers3 size={12} />}><Moon size={12} /></Show>}
>
<Sun size={12} />
</Show>
<span class="capitalize">{mode}</span>
</button>
)}
</For>
</div>
</div>
</header>
<div class="flex-1 min-h-0 overflow-hidden">
<div class="h-full overflow-y-auto bg-dls-surface px-4 pt-6 pb-4 sm:px-6 lg:px-10">
<div class="mx-auto w-full max-w-[800px]">
<Show
when={showingSettings()}
fallback={
<MessageList
messages={messageRows()}
developerMode
showThinking={false}
isStreaming={false}
expandedStepIds={expandedStepIds()}
setExpandedStepIds={(updater) => setExpandedStepIds((current) => updater(current))}
workspaceRoot="/Users/benjaminshafii/openwork-enterprise/_repos/openwork"
/>
}
>
<div class="space-y-4">
<div class="rounded-[20px] border border-dls-border bg-dls-sidebar p-4 text-sm text-dls-secondary">
This is the real `DenSettingsPanel` from the app mounted inside story-book.
</div>
<DenSettingsPanel
developerMode
connectRemoteWorkspace={async () => true}
/>
</div>
</Show>
</div>
</div>
</div>
<Show when={!showingSettings()}>
<Composer
prompt={composerPrompt()}
developerMode
busy={false}
isStreaming={false}
onSend={handleSend}
onStop={() => undefined}
onDraftChange={handleDraftChange}
selectedModelLabel="Claude Sonnet 4"
onModelClick={() => undefined}
modelVariantLabel="Reasoning"
modelVariant="medium"
onModelVariantChange={() => undefined}
agentLabel={agentLabel()}
selectedAgent={selectedAgent()}
agentPickerOpen={agentPickerOpen()}
agentPickerBusy={false}
agentPickerError={null}
agentOptions={[]}
onToggleAgentPicker={() => setAgentPickerOpen((current) => !current)}
onSelectAgent={(agent) => {
setSelectedAgent(agent);
setAgentPickerOpen(false);
}}
setAgentPickerRef={() => undefined}
showNotionBanner={false}
onNotionBannerClick={() => undefined}
toast={composerToast()}
onToast={(message) => setComposerToast(message)}
listAgents={async () => []}
recentFiles={workingFiles}
searchFiles={async (query) => {
const normalized = query.trim().toLowerCase();
if (!normalized) return workingFiles.slice(0, 8);
return workingFiles.filter((path) => path.toLowerCase().includes(normalized)).slice(0, 8);
}}
isRemoteWorkspace={activeWorkspaceId() === remoteWorkspace.id}
isSandboxWorkspace={activeWorkspaceId() === remoteWorkspace.id}
attachmentsEnabled
attachmentsDisabledReason={null}
listCommands={async () => commandOptions}
/>
</Show>
<StatusBar
clientConnected
openworkServerStatus="connected"
developerMode
settingsOpen={false}
onSendFeedback={() => undefined}
onOpenSettings={() => undefined}
onOpenMessaging={() => undefined}
onOpenProviders={() => undefined}
onOpenMcp={() => undefined}
providerConnectedIds={["anthropic", "openai"]}
mcpStatuses={mcpStatuses}
statusLabel="Session Ready"
statusDetail="Story shell mode · app components mounted with mock data"
/>
</main>
<aside
class="hidden shrink-0 md:flex"
style={{
width: `${rightSidebarWidth()}px`,
"min-width": `${rightSidebarWidth()}px`,
}}
>
{renderRightRail(rightSidebarExpanded())}
</aside>
</div>
</div>
);
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../app/tsconfig.json",
"include": ["src", "../app/src", "vite.config.ts"]
}

View File

@@ -0,0 +1,37 @@
import os from "node:os";
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import solid from "vite-plugin-solid";
const portValue = Number.parseInt(process.env.PORT ?? "", 10);
const devPort = Number.isFinite(portValue) && portValue > 0 ? portValue : 5176;
const allowedHosts = new Set<string>();
const envAllowedHosts = process.env.VITE_ALLOWED_HOSTS ?? "";
const addHost = (value?: string | null) => {
const trimmed = value?.trim();
if (!trimmed) return;
allowedHosts.add(trimmed);
};
envAllowedHosts.split(",").forEach(addHost);
addHost(process.env.OPENWORK_PUBLIC_HOST ?? null);
const hostname = os.hostname();
addHost(hostname);
const shortHostname = hostname.split(".")[0];
if (shortHostname && shortHostname !== hostname) {
addHost(shortHostname);
}
export default defineConfig({
publicDir: "../app/public",
plugins: [tailwindcss(), solid()],
server: {
port: devPort,
strictPort: true,
...(allowedHosts.size > 0 ? { allowedHosts: Array.from(allowedHosts) } : {}),
},
build: {
target: "esnext",
},
});

View File

@@ -5,6 +5,7 @@
"scripts": {
"dev": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/desktop dev",
"dev:ui": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/app dev",
"dev:story": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/story-book dev",
"dev:web": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork-ee/den-web dev",
"dev:web-local": "OPENWORK_DEV_MODE=1 bash scripts/dev-web-local.sh",
"dev:den-docker": "bash packaging/docker/den-dev-up.sh",

88
pnpm-lock.yaml generated
View File

@@ -258,6 +258,94 @@ importers:
specifier: ^5.9.3
version: 5.9.3
apps/story-book:
dependencies:
'@codemirror/commands':
specifier: ^6.8.0
version: 6.10.2
'@codemirror/lang-markdown':
specifier: ^6.3.3
version: 6.5.0
'@codemirror/language':
specifier: ^6.11.0
version: 6.12.1
'@codemirror/state':
specifier: ^6.5.2
version: 6.5.4
'@codemirror/view':
specifier: ^6.38.0
version: 6.39.14
'@opencode-ai/sdk':
specifier: ^1.1.31
version: 1.1.39
'@radix-ui/colors':
specifier: ^3.0.0
version: 3.0.0
'@solid-primitives/event-bus':
specifier: ^1.1.2
version: 1.1.2(solid-js@1.9.9)
'@solid-primitives/storage':
specifier: ^4.3.3
version: 4.3.3(solid-js@1.9.9)
'@solidjs/router':
specifier: ^0.15.4
version: 0.15.4(patch_hash=1db11a7c28fe4da76187d42efaffc6b9a70ad370462fffb794ff90e67744d770)(solid-js@1.9.9)
'@tanstack/solid-virtual':
specifier: ^3.13.19
version: 3.13.19(solid-js@1.9.9)
'@tauri-apps/api':
specifier: ^2.0.0
version: 2.10.1
'@tauri-apps/plugin-deep-link':
specifier: ^2.4.7
version: 2.4.7
'@tauri-apps/plugin-dialog':
specifier: ~2.6.0
version: 2.6.0
'@tauri-apps/plugin-http':
specifier: ~2.5.6
version: 2.5.6
'@tauri-apps/plugin-opener':
specifier: ^2.5.3
version: 2.5.3
'@tauri-apps/plugin-process':
specifier: ~2.3.1
version: 2.3.1
'@tauri-apps/plugin-updater':
specifier: ~2.9.0
version: 2.9.0
fuzzysort:
specifier: ^3.1.0
version: 3.1.0
jsonc-parser:
specifier: ^3.2.1
version: 3.3.1
lucide-solid:
specifier: ^0.562.0
version: 0.562.0(solid-js@1.9.9)
marked:
specifier: ^17.0.1
version: 17.0.1
solid-js:
specifier: ^1.9.0
version: 1.9.9
devDependencies:
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
tailwindcss:
specifier: ^4.1.18
version: 4.1.18
typescript:
specifier: ^5.6.3
version: 5.9.3
vite:
specifier: ^6.0.1
version: 6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
vite-plugin-solid:
specifier: ^2.11.0
version: 2.11.10(solid-js@1.9.9)(vite@6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
ee/apps/den-controller:
dependencies:
'@daytonaio/sdk':