mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
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:
31
apps/story-book/index.html
Normal file
31
apps/story-book/index.html
Normal 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>
|
||||
45
apps/story-book/package.json
Normal file
45
apps/story-book/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
58
apps/story-book/src/index.tsx
Normal file
58
apps/story-book/src/index.tsx
Normal 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,
|
||||
);
|
||||
203
apps/story-book/src/mock-data.ts
Normal file
203
apps/story-book/src/mock-data.ts
Normal 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.",
|
||||
},
|
||||
};
|
||||
576
apps/story-book/src/story-book.tsx
Normal file
576
apps/story-book/src/story-book.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
4
apps/story-book/tsconfig.json
Normal file
4
apps/story-book/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../app/tsconfig.json",
|
||||
"include": ["src", "../app/src", "vite.config.ts"]
|
||||
}
|
||||
37
apps/story-book/vite.config.ts
Normal file
37
apps/story-book/vite.config.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
@@ -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
88
pnpm-lock.yaml
generated
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user