diff --git a/desktop/src/apps/AppStudioApp.test.tsx b/desktop/src/apps/AppStudioApp.test.tsx new file mode 100644 index 00000000..9947b73e --- /dev/null +++ b/desktop/src/apps/AppStudioApp.test.tsx @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import React from "react"; +import { AppStudioApp } from "./AppStudioApp"; + +describe("AppStudioApp", () => { + it("renders the titlebar with App Studio", () => { + render(); + expect(screen.getByText("App Studio")).toBeInTheDocument(); + }); + + it("renders all rail items", () => { + const { container } = render(); + const nav = container.querySelector("nav[aria-label='App Studio views']"); + expect(nav).toBeInTheDocument(); + // Rail buttons are inside the nav + const railBtns = nav!.querySelectorAll("button"); + const labels = Array.from(railBtns).map((b) => b.getAttribute("aria-label")); + expect(labels).toContain("Build"); + expect(labels).toContain("Templates"); + expect(labels).toContain("Publish"); + expect(labels).toContain("SDK"); + }); + + it("shows Build view by default", () => { + render(); + expect(screen.getByRole("heading", { name: /^build$/i })).toBeInTheDocument(); + // checkerboard sandbox area has the live preview header text + expect(screen.getByText("Build log")).toBeInTheDocument(); + }); + + it("switches to Templates view and shows template cards", () => { + const { container } = render(); + const nav = container.querySelector("nav[aria-label='App Studio views']")!; + const templatesBtn = Array.from(nav.querySelectorAll("button")).find( + (b) => b.getAttribute("aria-label") === "Templates" + )!; + fireEvent.click(templatesBtn); + // hero heading + expect(screen.getByText(/build a taOS app in plain words/i)).toBeInTheDocument(); + // template card labels + expect(screen.getByText("Dashboard")).toBeInTheDocument(); + expect(screen.getByText("Tracker")).toBeInTheDocument(); + expect(screen.getByText("Kanban")).toBeInTheDocument(); + expect(screen.getByText("Blank")).toBeInTheDocument(); + }); + + it("switches to Publish view and shows capability rows", () => { + const { container } = render(); + const nav = container.querySelector("nav[aria-label='App Studio views']")!; + const publishBtn = Array.from(nav.querySelectorAll("button")).find( + (b) => b.getAttribute("aria-label") === "Publish" + )!; + fireEvent.click(publishBtn); + // app identity + expect(screen.getAllByText("Chore Quest").length).toBeGreaterThan(0); + // capability row labels + expect(screen.getByTestId("perm-row-workspace")).toBeInTheDocument(); + expect(screen.getByTestId("perm-row-notifications")).toBeInTheDocument(); + expect(screen.getByTestId("perm-row-household")).toBeInTheDocument(); + // publish button + expect(screen.getByRole("button", { name: /publish to my store/i })).toBeInTheDocument(); + }); +}); diff --git a/desktop/src/apps/AppStudioApp.tsx b/desktop/src/apps/AppStudioApp.tsx new file mode 100644 index 00000000..1a22179c --- /dev/null +++ b/desktop/src/apps/AppStudioApp.tsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +import { Sparkles, LayoutGrid, Share2, CircleDot } from "lucide-react"; +import { BuildView } from "./appstudio/BuildView"; +import { TemplatesView } from "./appstudio/TemplatesView"; +import { PublishView } from "./appstudio/PublishView"; + +/* ------------------------------------------------------------------ */ +/* App Studio -- shell */ +/* */ +/* Build taOS apps from plain words. An agent generates them against */ +/* the taOS SDK, sandboxed and safe. Publish to your Store or share */ +/* with family. */ +/* */ +/* Shell follows the canonical studio pattern from GameStudioApp: */ +/* 46px centered titlebar, 68px icon rail, per-view subfolder. */ +/* ------------------------------------------------------------------ */ + +type AppStudioView = "build" | "templates" | "publish" | "sdk"; + +const RAIL: { id: AppStudioView; label: string; icon: typeof Sparkles }[] = [ + { id: "build", label: "Build", icon: Sparkles }, + { id: "templates", label: "Templates", icon: LayoutGrid }, + { id: "publish", label: "Publish", icon: Share2 }, +]; + +const RAIL_BOTTOM: { id: AppStudioView; label: string; icon: typeof Sparkles }[] = [ + { id: "sdk", label: "SDK", icon: CircleDot }, +]; + +export function AppStudioApp({ windowId: _windowId }: { windowId: string }) { + const [view, setView] = useState("build"); + + function RailButton({ id, label, icon: Icon }: { id: AppStudioView; label: string; icon: typeof Sparkles }) { + const on = view === id; + return ( + + ); + } + + return ( +
+ {/* titlebar */} +
+ App Studio +
+ +
+ {/* left rail */} + + + {/* active surface */} +
+ {view === "build" && } + {view === "templates" && } + {view === "publish" && } + {view === "sdk" && ( +
+ SDK docs coming soon +
+ )} +
+
+
+ ); +} diff --git a/desktop/src/apps/CodingStudioApp.test.tsx b/desktop/src/apps/CodingStudioApp.test.tsx new file mode 100644 index 00000000..d899bced --- /dev/null +++ b/desktop/src/apps/CodingStudioApp.test.tsx @@ -0,0 +1,71 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { CodingStudioApp } from "./CodingStudioApp"; + +function renderApp() { + return render(); +} + +describe("CodingStudioApp", () => { + it("renders the app titlebar", () => { + renderApp(); + expect(screen.getByText("Coding Studio")).toBeDefined(); + }); + + it("renders all rail items", () => { + renderApp(); + // Rail buttons use aria-label for exact matching via the nav element + const nav = screen.getByRole("navigation", { name: "Coding Studio views" }); + expect(nav).toBeDefined(); + expect(screen.getByRole("button", { name: "Code" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Preview" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Templates" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Models" })).toBeDefined(); + }); + + it("shows Build view by default with Build rail item active", () => { + renderApp(); + // The rail Build button (inside nav) should be aria-current="page" + const nav = screen.getByRole("navigation", { name: "Coding Studio views" }); + const railBuildBtn = nav.querySelector('[aria-label="Build"]') as HTMLElement; + expect(railBuildBtn).toBeTruthy(); + expect(railBuildBtn.getAttribute("aria-current")).toBe("page"); + }); + + it("switches to Templates view on rail click", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Templates" })); + expect(screen.getByRole("button", { name: "Templates" }).getAttribute("aria-current")).toBe( + "page", + ); + expect(screen.getByText("Describe what you want to build.")).toBeDefined(); + }); + + it("Templates view shows all 8 template cards", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Templates" })); + const expectedNames = [ + "Web App", + "REST API", + "CLI Tool", + "Discord Bot", + "Static Site", + "Data Pipeline", + "Python Script", + "Browser Extension", + ]; + for (const name of expectedNames) { + expect(screen.getByText(name)).toBeDefined(); + } + }); + + it("switches to Preview view on rail click and shows preview header", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Preview" })); + expect(screen.getByRole("button", { name: "Preview" }).getAttribute("aria-current")).toBe( + "page", + ); + // Preview header h2 + expect(screen.getByRole("heading", { name: "Preview" })).toBeDefined(); + }); +}); diff --git a/desktop/src/apps/CodingStudioApp.tsx b/desktop/src/apps/CodingStudioApp.tsx new file mode 100644 index 00000000..7e7a35c2 --- /dev/null +++ b/desktop/src/apps/CodingStudioApp.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { Sparkles, Code2, Play, LayoutGrid, Settings2 } from "lucide-react"; +import { BuildView } from "./codingstudio/BuildView"; +import { TemplatesView } from "./codingstudio/TemplatesView"; +import { PreviewView } from "./codingstudio/PreviewView"; + +type CodingView = "build" | "code" | "preview" | "templates"; + +const RAIL: { id: CodingView; label: string; icon: typeof Sparkles }[] = [ + { id: "build", label: "Build", icon: Sparkles }, + { id: "code", label: "Code", icon: Code2 }, + { id: "preview", label: "Preview", icon: Play }, + { id: "templates", label: "Templates", icon: LayoutGrid }, +]; + +export function CodingStudioApp({ windowId: _windowId }: { windowId: string }) { + const [view, setView] = useState("build"); + + return ( +
+ {/* title strip */} +
+ Coding Studio +
+ +
+ {/* left rail */} + + + {/* active surface */} +
+ {view === "build" && } + {view === "code" && } + {view === "preview" && } + {view === "templates" && } +
+
+
+ ); +} diff --git a/desktop/src/apps/DesignStudioApp.test.tsx b/desktop/src/apps/DesignStudioApp.test.tsx new file mode 100644 index 00000000..9d450cba --- /dev/null +++ b/desktop/src/apps/DesignStudioApp.test.tsx @@ -0,0 +1,69 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { DesignStudioApp } from "./DesignStudioApp"; + +function renderApp() { + return render(); +} + +describe("DesignStudioApp", () => { + it("renders the app titlebar", () => { + renderApp(); + expect(screen.getByText("Design Studio")).toBeDefined(); + }); + + it("renders all rail items", () => { + renderApp(); + const nav = screen.getByRole("navigation", { name: "Design Studio views" }); + expect(nav).toBeDefined(); + expect(screen.getByRole("button", { name: "Design" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Templates" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Elements" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Magic" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Brand" })).toBeDefined(); + }); + + it("shows Design view by default with Design rail item active", () => { + renderApp(); + const nav = screen.getByRole("navigation", { name: "Design Studio views" }); + const designBtn = nav.querySelector('[aria-label="Design"]') as HTMLElement; + expect(designBtn).toBeTruthy(); + expect(designBtn.getAttribute("aria-current")).toBe("page"); + }); + + it("default Design view renders the canvas artboard", () => { + renderApp(); + expect(screen.getByText("Untitled poster")).toBeDefined(); + }); + + it("switches to Templates view and shows template cards", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Templates" })); + expect(screen.getByRole("button", { name: "Templates" }).getAttribute("aria-current")).toBe( + "page", + ); + const expectedCards = [ + "Instagram Post", + "Story", + "Poster", + "Presentation", + "Logo", + "Flyer", + "Banner", + "Business Card", + ]; + for (const name of expectedCards) { + expect(screen.getByText(name)).toBeDefined(); + } + }); + + it("switches to Magic view and shows prompt bar and result tiles", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Magic" })); + expect(screen.getByRole("button", { name: "Magic" }).getAttribute("aria-current")).toBe("page"); + expect(screen.getByText("Describe the design you need.")).toBeDefined(); + expect(screen.getByText("Bold, centered")).toBeDefined(); + expect(screen.getByText("Split layout")).toBeDefined(); + expect(screen.getAllByText("Editorial").length).toBeGreaterThan(0); + }); +}); diff --git a/desktop/src/apps/DesignStudioApp.tsx b/desktop/src/apps/DesignStudioApp.tsx new file mode 100644 index 00000000..bf569d75 --- /dev/null +++ b/desktop/src/apps/DesignStudioApp.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { PenLine, LayoutGrid, Plus, Sparkles, Circle } from "lucide-react"; +import { DesignView } from "./designstudio/DesignView"; +import { TemplatesView } from "./designstudio/TemplatesView"; +import { MagicView } from "./designstudio/MagicView"; + +type DesignStudioView = "design" | "templates" | "elements" | "magic"; + +const RAIL: { id: DesignStudioView; label: string; icon: typeof PenLine }[] = [ + { id: "design", label: "Design", icon: PenLine }, + { id: "templates", label: "Templates", icon: LayoutGrid }, + { id: "elements", label: "Elements", icon: Plus }, + { id: "magic", label: "Magic", icon: Sparkles }, +]; + +export function DesignStudioApp({ windowId: _windowId }: { windowId: string }) { + const [view, setView] = useState("design"); + + return ( +
+ {/* title strip */} +
+ Design Studio +
+ +
+ {/* left rail */} + + + {/* active surface */} +
+ {view === "design" && } + {view === "templates" && } + {view === "elements" && } + {view === "magic" && } +
+
+
+ ); +} diff --git a/desktop/src/apps/MusicStudioApp.test.tsx b/desktop/src/apps/MusicStudioApp.test.tsx new file mode 100644 index 00000000..15710296 --- /dev/null +++ b/desktop/src/apps/MusicStudioApp.test.tsx @@ -0,0 +1,80 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { MusicStudioApp } from "./MusicStudioApp"; + +function renderApp() { + return render(); +} + +describe("MusicStudioApp", () => { + it("renders the app titlebar with name", () => { + renderApp(); + expect(screen.getByText("Music Studio")).toBeDefined(); + }); + + it("renders all rail items", () => { + renderApp(); + const nav = screen.getByRole("navigation", { name: "Music Studio views" }); + expect(nav).toBeDefined(); + expect(screen.getByRole("button", { name: "Studio" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Compose" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Sounds" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Mixer" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Export" })).toBeDefined(); + }); + + it("shows Studio view by default with Studio rail item active", () => { + renderApp(); + const nav = screen.getByRole("navigation", { name: "Music Studio views" }); + const studioBtn = nav.querySelector('[aria-label="Studio"]') as HTMLElement; + expect(studioBtn).toBeTruthy(); + expect(studioBtn.getAttribute("aria-current")).toBe("page"); + }); + + it("Studio view shows transport controls", () => { + renderApp(); + expect(screen.getByRole("button", { name: "Stop" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Play" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Record" })).toBeDefined(); + }); + + it("Studio view shows a track in the track list", () => { + renderApp(); + expect(screen.getAllByText("Drums").length).toBeGreaterThan(0); + }); + + it("switches to Compose view on rail click", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Compose" })); + const nav = screen.getByRole("navigation", { name: "Music Studio views" }); + const composeBtn = nav.querySelector('[aria-label="Compose"]') as HTMLElement; + expect(composeBtn.getAttribute("aria-current")).toBe("page"); + expect(screen.getByRole("heading", { name: "Compose" })).toBeDefined(); + }); + + it("Compose view shows Generate button and style chips", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Compose" })); + expect(screen.getByRole("button", { name: "Generate" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Lo-fi" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Cinematic" })).toBeDefined(); + }); + + it("switches to Sounds view on rail click", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Sounds" })); + const nav = screen.getByRole("navigation", { name: "Music Studio views" }); + const soundsBtn = nav.querySelector('[aria-label="Sounds"]') as HTMLElement; + expect(soundsBtn.getAttribute("aria-current")).toBe("page"); + expect(screen.getByRole("heading", { name: "Sounds" })).toBeDefined(); + }); + + it("Sounds view shows filter pills and instrument cards", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Sounds" })); + expect(screen.getByRole("button", { name: "All" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Drums" })).toBeDefined(); + expect(screen.getByText("Boom Bap Kit")).toBeDefined(); + expect(screen.getByText("Rhodes Mk I")).toBeDefined(); + }); +}); diff --git a/desktop/src/apps/MusicStudioApp.tsx b/desktop/src/apps/MusicStudioApp.tsx new file mode 100644 index 00000000..6bf00718 --- /dev/null +++ b/desktop/src/apps/MusicStudioApp.tsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +import { LayoutList, Sparkles, Music2, LayoutGrid, Download } from "lucide-react"; +import { StudioView } from "./musicstudio/StudioView"; +import { ComposeView } from "./musicstudio/ComposeView"; +import { SoundsView } from "./musicstudio/SoundsView"; + +type MusicView = "studio" | "compose" | "sounds" | "mixer" | "export"; + +const RAIL_MAIN: { id: MusicView; label: string; icon: typeof LayoutList }[] = [ + { id: "studio", label: "Studio", icon: LayoutList }, + { id: "compose", label: "Compose", icon: Sparkles }, + { id: "sounds", label: "Sounds", icon: Music2 }, + { id: "mixer", label: "Mixer", icon: LayoutGrid }, +]; + +export function MusicStudioApp({ windowId: _windowId }: { windowId: string }) { + const [view, setView] = useState("studio"); + + return ( +
+ {/* title strip */} +
+ Music Studio +
+ +
+ {/* left rail */} + + + {/* active surface */} +
+ {view === "studio" && } + {view === "compose" && } + {view === "sounds" && } + {view === "mixer" && ( +
+ Mixer coming soon +
+ )} + {view === "export" && ( +
+ Export coming soon +
+ )} +
+
+
+ ); +} diff --git a/desktop/src/apps/OfficeSuiteApp.test.tsx b/desktop/src/apps/OfficeSuiteApp.test.tsx new file mode 100644 index 00000000..6f791607 --- /dev/null +++ b/desktop/src/apps/OfficeSuiteApp.test.tsx @@ -0,0 +1,67 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { OfficeSuiteApp } from "./OfficeSuiteApp"; + +function renderApp() { + return render(); +} + +describe("OfficeSuiteApp", () => { + it("renders the app titlebar", () => { + renderApp(); + expect(screen.getByText("Office Suite")).toBeDefined(); + }); + + it("renders all rail items", () => { + renderApp(); + const nav = screen.getByRole("navigation", { name: "Office Suite views" }); + expect(nav).toBeDefined(); + // query within nav to avoid ambiguity with toolbar buttons + expect(nav.querySelector('[aria-label="Write"]')).toBeTruthy(); + expect(nav.querySelector('[aria-label="Calc"]')).toBeTruthy(); + expect(nav.querySelector('[aria-label="Slides"]')).toBeTruthy(); + expect(nav.querySelector('[aria-label="Data"]')).toBeTruthy(); + expect(nav.querySelector('[aria-label="Assist"]')).toBeTruthy(); + }); + + it("shows Write view by default with Write rail item active", () => { + renderApp(); + const nav = screen.getByRole("navigation", { name: "Office Suite views" }); + const writeBtn = nav.querySelector('[aria-label="Write"]') as HTMLElement; + expect(writeBtn).toBeTruthy(); + expect(writeBtn.getAttribute("aria-current")).toBe("page"); + // Write view content + expect(screen.getByText("taOS Studios launch note")).toBeDefined(); + }); + + it("switches to Calc view and shows spreadsheet grid with Total row", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Calc" })); + expect(screen.getByRole("button", { name: "Calc" }).getAttribute("aria-current")).toBe("page"); + // spreadsheet table + expect(screen.getByRole("table", { name: "Spreadsheet" })).toBeDefined(); + // total row value + expect(screen.getByTestId("total-revenue")).toBeDefined(); + expect(screen.getByTestId("total-revenue").textContent).toBe("28,900"); + // Total label + expect(screen.getByText("Total")).toBeDefined(); + }); + + it("switches to Slides view and shows slide thumbnails", () => { + renderApp(); + fireEvent.click(screen.getByRole("button", { name: "Slides" })); + expect(screen.getByRole("button", { name: "Slides" }).getAttribute("aria-current")).toBe( + "page", + ); + // thumbnail rail + expect(screen.getByRole("complementary", { name: "Slide thumbnails" })).toBeDefined(); + // all 5 slide thumbnails + expect(screen.getByLabelText("Slide 1: Build it your way")).toBeDefined(); + expect(screen.getByLabelText("Slide 2: Ready today")).toBeDefined(); + expect(screen.getByLabelText("Slide 3: On the way")).toBeDefined(); + expect(screen.getByLabelText("Slide 4: Your hardware")).toBeDefined(); + expect(screen.getByLabelText("Slide 5: Get started")).toBeDefined(); + // slide content + expect(screen.getByText("Build it your way.")).toBeDefined(); + }); +}); diff --git a/desktop/src/apps/OfficeSuiteApp.tsx b/desktop/src/apps/OfficeSuiteApp.tsx new file mode 100644 index 00000000..89b245f0 --- /dev/null +++ b/desktop/src/apps/OfficeSuiteApp.tsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import { Sparkles, Type, Grid, Monitor, Database } from "lucide-react"; +import { WriteView } from "./officesuite/WriteView"; +import { CalcView } from "./officesuite/CalcView"; +import { SlidesView } from "./officesuite/SlidesView"; + +type OfficeView = "write" | "calc" | "slides" | "data"; + +const RAIL: { id: OfficeView; label: string; icon: typeof Sparkles }[] = [ + { id: "write", label: "Write", icon: Type }, + { id: "calc", label: "Calc", icon: Grid }, + { id: "slides", label: "Slides", icon: Monitor }, + { id: "data", label: "Data", icon: Database }, +]; + +export function OfficeSuiteApp({ windowId: _windowId }: { windowId: string }) { + const [view, setView] = useState("write"); + + return ( +
+ {/* title strip */} +
+ Office Suite +
+ +
+ {/* left rail */} + + + {/* active surface */} +
+ {view === "write" && } + {view === "calc" && } + {view === "slides" && } + {view === "data" && ( +
+ Data view coming soon +
+ )} +
+
+
+ ); +} diff --git a/desktop/src/apps/StoreApp/StudiosView.test.tsx b/desktop/src/apps/StoreApp/StudiosView.test.tsx new file mode 100644 index 00000000..bed98b59 --- /dev/null +++ b/desktop/src/apps/StoreApp/StudiosView.test.tsx @@ -0,0 +1,61 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { StudiosView } from "./StudiosView"; + +afterEach(() => { cleanup(); }); + +describe("StudiosView", () => { + it("renders the three section headings", () => { + render(); + expect(screen.getByText("taOS Studios")).toBeInTheDocument(); + expect(screen.getByText("Community Studios")).toBeInTheDocument(); + expect(screen.getByText("Studio layouts")).toBeInTheDocument(); + }); + + it("renders all taOS studio card names", () => { + render(); + const expected = [ + "Images Studio", + "Game Studio", + "Coding Studio", + "Design Studio", + "Music Studio", + "App Studio", + "Office Suite", + "Web Studio", + ]; + for (const name of expected) { + expect(screen.getAllByText(name).length).toBeGreaterThan(0); + } + }); + + it("shows a Soon badge on the unreleased studio", () => { + render(); + // Only Web Studio is still "soon"; Coding/Design/Music/App/Office are in beta. + const soonBadges = screen.getAllByText("Soon"); + expect(soonBadges.length).toBe(1); + }); + + it("shows Coding Studio in the hero section with the featured eyebrow", () => { + render(); + // The eyebrow text calls it out as featured + expect(screen.getByText(/Featured/i)).toBeInTheDocument(); + // Coding Studio name appears at least twice (hero heading + grid card) + expect(screen.getAllByText("Coding Studio").length).toBeGreaterThanOrEqual(2); + }); + + it("renders community studio names", () => { + render(); + expect(screen.getByText("Pixel Art Studio")).toBeInTheDocument(); + expect(screen.getByText("Lo-fi Beats Kit")).toBeInTheDocument(); + expect(screen.getByText("API Forge")).toBeInTheDocument(); + expect(screen.getByText("Retro FPS Kit")).toBeInTheDocument(); + }); + + it("renders layout chip names", () => { + render(); + expect(screen.getByText("Photo Retoucher")).toBeInTheDocument(); + expect(screen.getByText("Chiptune")).toBeInTheDocument(); + expect(screen.getByText("Static Site Kit")).toBeInTheDocument(); + }); +}); diff --git a/desktop/src/apps/StoreApp/StudiosView.tsx b/desktop/src/apps/StoreApp/StudiosView.tsx new file mode 100644 index 00000000..2bd7affa --- /dev/null +++ b/desktop/src/apps/StoreApp/StudiosView.tsx @@ -0,0 +1,573 @@ +import { useCallback, useState } from "react"; +import { Check, Loader2, Star } from "lucide-react"; +import type { CatalogApp } from "./types"; +import { useInstalledOptionalApps } from "@/hooks/use-installed-optional-apps"; +import { useProcessStore } from "@/stores/process-store"; +import { getApp } from "@/registry/app-registry"; +import { emitAppEvent, APP_OPTIONAL_CHANGED } from "@/lib/app-event-bus"; + +/* The studio catalog card id maps 1:1 to the registry app id, except Images + Studio whose registry app id is "images". */ +function studioAppId(studioId: string): string { + return studioId === "images-studio" ? "images" : studioId; +} + +/* ------------------------------------------------------------------ + Studio catalog entries (first-party, type "studio") + ------------------------------------------------------------------ */ + +const STUDIOS: CatalogApp[] = [ + { + id: "images-studio", + name: "Images Studio", + type: "studio", + category: "Creative", + version: "1.0.0", + description: "Make and edit images on your own GPU, NPU, or CPU.", + tagline: "Generate, edit, upscale", + installed: true, + compat: "green", + studioState: "installed", + cover: "radial-gradient(120% 120% at 30% 20%,#3a3357,transparent 60%),linear-gradient(140deg,#211d30,#14121b)", + }, + { + id: "game-studio", + name: "Game Studio", + type: "studio", + category: "Creative", + version: "1.0.0", + description: "Describe a 3D game and play it in the browser. Runs entirely offline.", + tagline: "Offline AI game maker", + installed: true, + compat: "green", + studioState: "installed", + cover: "radial-gradient(120% 120% at 70% 25%,#1f4a4f,transparent 60%),linear-gradient(140deg,#10242a,#0c181c)", + }, + { + id: "coding-studio", + name: "Coding Studio", + type: "studio", + category: "Dev", + version: "1.0.0", + description: "An agent writes, runs, and previews your app on the cluster.", + tagline: "Describe, build, preview", + installed: false, + compat: "green", + studioState: "available", + cover: "radial-gradient(120% 120% at 35% 25%,#34384a,transparent 60%),linear-gradient(140deg,#1b1d27,#121319)", + }, + { + id: "design-studio", + name: "Design Studio", + type: "studio", + category: "Creative", + version: "0.0.0", + description: "Canva-style design with AI, on a shared canvas engine.", + tagline: "Graphics and layouts", + installed: false, + compat: "green", + studioState: "available", + cover: "radial-gradient(120% 120% at 65% 20%,#4a3a4f,transparent 60%),linear-gradient(140deg,#241a27,#16121a)", + }, + { + id: "music-studio", + name: "Music Studio", + type: "studio", + category: "Creative", + version: "0.0.0", + description: "Compose, arrange, and generate audio on your hardware.", + tagline: "Web DAW with AI", + installed: false, + compat: "green", + studioState: "available", + cover: "radial-gradient(120% 120% at 30% 25%,#2f4a3a,transparent 60%),linear-gradient(140deg,#16271d,#101a14)", + }, + { + id: "app-studio", + name: "App Studio", + type: "studio", + category: "Dev", + version: "0.0.0", + description: "Build and share new taOS apps, sandboxed and safe.", + tagline: "taOS app builder", + installed: false, + compat: "green", + studioState: "available", + cover: "radial-gradient(120% 120% at 70% 25%,#3a4150,transparent 60%),linear-gradient(140deg,#1c1f28,#13151b)", + }, + { + id: "office-suite", + name: "Office Suite", + type: "studio", + category: "Productivity", + version: "0.0.0", + description: "Documents, spreadsheets, and presentations with AI.", + tagline: "Write, Calc, Slides", + installed: false, + compat: "green", + studioState: "available", + cover: "radial-gradient(120% 120% at 35% 20%,#4a4632,transparent 60%),linear-gradient(140deg,#262216,#181610)", + }, + { + id: "web-studio", + name: "Web Studio", + type: "studio", + category: "Dev", + version: "0.0.0", + description: "Build sites with templates, host them on your LAN.", + tagline: "AI website builder", + installed: false, + compat: "green", + studioState: "soon", + cover: "radial-gradient(120% 120% at 65% 25%,#324a47,transparent 60%),linear-gradient(140deg,#152624,#0f1817)", + }, +]; + +/* ------------------------------------------------------------------ + Community studios (static mock) + ------------------------------------------------------------------ */ + +interface CommunityStudio { + id: string; + name: string; + badge: "Fork" | "Layout"; + parent: string; + description: string; + stars: number; + cover: string; +} + +const COMMUNITY_STUDIOS: CommunityStudio[] = [ + { + id: "pixel-art-studio", + name: "Pixel Art Studio", + badge: "Fork", + parent: "Images Studio fork", + description: "Palette-locked, grid-snapped sprite workflow.", + stars: 1200, + cover: "radial-gradient(120% 120% at 30% 20%,#3f3357,transparent 60%),linear-gradient(140deg,#221c30,#15111b)", + }, + { + id: "lofi-beats-kit", + name: "Lo-fi Beats Kit", + badge: "Layout", + parent: "Music Studio layout", + description: "Boom-bap drum rack, vinyl FX, swing presets.", + stars: 860, + cover: "radial-gradient(120% 120% at 70% 25%,#2f4a3a,transparent 60%),linear-gradient(140deg,#16271d,#101a14)", + }, + { + id: "api-forge", + name: "API Forge", + badge: "Layout", + parent: "Coding Studio layout", + description: "FastAPI scaffold, request runner, schema view.", + stars: 2400, + cover: "radial-gradient(120% 120% at 35% 25%,#3a4150,transparent 60%),linear-gradient(140deg,#1c1f28,#13151b)", + }, + { + id: "retro-fps-kit", + name: "Retro FPS Kit", + badge: "Fork", + parent: "Game Studio fork", + description: "Doom-style raycaster templates and assets.", + stars: 970, + cover: "radial-gradient(120% 120% at 65% 20%,#4a3340,transparent 60%),linear-gradient(140deg,#271620,#180f14)", + }, +]; + +/* ------------------------------------------------------------------ + Layout chips (static mock) + ------------------------------------------------------------------ */ + +interface LayoutChip { + id: string; + name: string; + parent: string; + author: string; + iconGradient: string; +} + +const LAYOUT_CHIPS: LayoutChip[] = [ + { + id: "photo-retoucher", + name: "Photo Retoucher", + parent: "Images Studio", + author: "@mara", + iconGradient: "linear-gradient(135deg,#6f7687,#474d5e)", + }, + { + id: "chiptune", + name: "Chiptune", + parent: "Music Studio", + author: "@ben", + iconGradient: "linear-gradient(135deg,#5f8a6f,#456f54)", + }, + { + id: "static-site-kit", + name: "Static Site Kit", + parent: "Coding Studio", + author: "@ivo", + iconGradient: "linear-gradient(135deg,#5d7a8a,#46606c)", + }, +]; + +function formatStars(n: number): string { + if (n >= 1000) return `${(n / 1000).toFixed(1)}k`; + return String(n); +} + +/* ------------------------------------------------------------------ + Hero card for the featured studio (Coding Studio) + ------------------------------------------------------------------ */ + +function StudioHero({ + studio, + installed, + onOpen, + onInstall, +}: { + studio: CatalogApp; + installed: Set; + onOpen: (appId: string) => void; + onInstall: (appId: string) => Promise; +}) { + const isInstalled = installed.has(studio.id); + const [busy, setBusy] = useState(false); + const handleGet = useCallback(async () => { + setBusy(true); + try { + await onInstall(studio.id); + } catch { + /* leave the button as Get so the user can retry */ + } finally { + setBusy(false); + } + }, [studio.id, onInstall]); + + return ( +
+
+
+
+
+ Featured -- New Studio +
+

+ {studio.name} +

+

+ {studio.description} +

+
+ + + + {studio.category} -- runs on cluster +
+
+
+ ); +} + +/* ------------------------------------------------------------------ + Studio card (grid) + ------------------------------------------------------------------ */ + +function StudioCard({ + studio, + installed, + onOpen, + onInstall, +}: { + studio: CatalogApp; + installed: Set; + onOpen: (appId: string) => void; + onInstall: (appId: string) => Promise; +}) { + const appId = studioAppId(studio.id); + const isSoon = studio.studioState === "soon"; + // Bundled studios (Images, Game) ship with the OS; available ones become + // installed once they are in the optional-app set. + const isBundled = studio.studioState === "installed"; + const isInstalled = isBundled || installed.has(appId); + const [busy, setBusy] = useState(false); + const handleGet = useCallback(async () => { + setBusy(true); + try { + await onInstall(appId); + } catch { + /* leave the button as Get so the user can retry */ + } finally { + setBusy(false); + } + }, [appId, onInstall]); + + return ( +
+ {/* Cover */} +
+ + {studio.category} + + {isSoon && ( + + Soon + + )} +
+ {/* Meta */} +
+
+
{studio.name}
+
{studio.tagline}
+
+

{studio.description}

+
+ {isInstalled ? ( + + Installed + + ) : isSoon ? ( + In design + ) : ( + New + )} + {isInstalled ? ( + + ) : isSoon ? ( + + ) : ( + + )} +
+
+
+ ); +} + +/* ------------------------------------------------------------------ + Community studio card (horizontal scroll row) + ------------------------------------------------------------------ */ + +function CommunityStudioCard({ item }: { item: CommunityStudio }) { + return ( +
+
+ + {item.badge} + +
+
+
+
{item.name}
+
{item.parent}
+
+

{item.description}

+
+ + + {formatStars(item.stars)} + + +
+
+
+ ); +} + +/* ------------------------------------------------------------------ + Layout chip + ------------------------------------------------------------------ */ + +function LayoutChipCard({ chip }: { chip: LayoutChip }) { + return ( +
+ + ); +} + +/* ------------------------------------------------------------------ + Section header + ------------------------------------------------------------------ */ + +function SectionHeader({ + title, + sub, + action, +}: { + title: string; + sub?: string; + action?: string; +}) { + return ( +
+
+

{title}

+ {sub && {sub}} +
+ {action && ( + + )} +
+ ); +} + +/* ------------------------------------------------------------------ + StudiosView + ------------------------------------------------------------------ */ + +export function StudiosView() { + const hero = STUDIOS.find((s) => s.id === "coding-studio")!; + const installed = useInstalledOptionalApps(); + const openWindow = useProcessStore((s) => s.openWindow); + + const openStudio = useCallback( + (appId: string) => { + const app = getApp(appId); + if (app) openWindow(appId, app.defaultSize); + }, + [openWindow], + ); + + const installStudio = useCallback(async (appId: string) => { + const res = await fetch(`/api/apps/optional/${encodeURIComponent(appId)}/install`, { + method: "POST", + headers: { Accept: "application/json" }, + }); + if (!res.ok) throw new Error(`install failed (${res.status})`); + // Re-fetch the installed set so the card flips to Open and the launcher + // surfaces the studio at once. + emitAppEvent(APP_OPTIONAL_CHANGED, appId); + }, []); + + return ( +
+ {/* Hero */} + + + {/* taOS Studios grid */} +
+ +
+ {STUDIOS.map((s) => ( + + ))} +
+
+ + {/* Community Studios */} +
+ +
+ {COMMUNITY_STUDIOS.map((item) => ( + + ))} +
+
+ + {/* Studio layouts */} +
+ +
+ {LAYOUT_CHIPS.map((chip) => ( + + ))} +
+
+
+ ); +} + +export default StudiosView; diff --git a/desktop/src/apps/StoreApp/index.tsx b/desktop/src/apps/StoreApp/index.tsx index 92f4c6f5..7e2169cb 100644 --- a/desktop/src/apps/StoreApp/index.tsx +++ b/desktop/src/apps/StoreApp/index.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Search, Download, Trash2, Check, Package, Loader2, Server, Compass, Grid2x2, Bot, Brain, Plug, Wrench, Star, Globe, - ArrowDownToLine, RefreshCw, Users, Cpu, + ArrowDownToLine, RefreshCw, Users, Cpu, Sparkles, } from "lucide-react"; import { Input } from "@/components/ui"; import { fetchLatestFrameworks, LatestVersion } from "@/lib/framework-api"; @@ -19,6 +19,7 @@ import { TaosAppsSection } from "./TaosAppsSection"; import { useIsMobile } from "@/hooks/use-is-mobile"; import { MobileStore } from "./MobileStore"; import { AppIcon, StoreCover } from "./AppIcon"; +import { StudiosView } from "./StudiosView"; /* ------------------------------------------------------------------ Nav sections @@ -27,6 +28,7 @@ import { AppIcon, StoreCover } from "./AppIcon"; type NavId = | "discover" | "apps" + | "studios" | "agents" | "models" | "services" @@ -46,6 +48,7 @@ interface NavItem { const NAV: NavItem[] = [ { id: "discover", label: "Discover", icon: }, { id: "apps", label: "Apps", icon: }, + { id: "studios", label: "Studios", icon: }, { id: "agents", label: "Agents", icon: }, { id: "models", label: "Models", icon: }, { id: "services", label: "Services", icon: }, @@ -957,6 +960,7 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) { const NAV_TYPE_MAP: Record = { discover: [], apps: ["streaming-app", "ai-app", "productivity", "home", "monitoring", "automation", "image-gen", "voice", "video-gen", "plugin"], + studios: ["studio"], agents: ["agent-framework"], models: ["model", "llm-runtime"], services: ["service", "infrastructure"], @@ -1027,9 +1031,9 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) { const profileSub = primaryTarget?.tier_id ? primaryTarget.tier_id.replace(/-/g, " ") : "Connect a device"; // When the user is searching, show the results grid even on the curated - // Discover/Community views (which otherwise ignore the search box). + // Discover/Community/Studios views (which otherwise ignore the search box). const searching = search.trim().length > 0; - const showGrid = searching || (activeNav !== "discover" && activeNav !== "community"); + const showGrid = searching || (activeNav !== "discover" && activeNav !== "community" && activeNav !== "studios"); // Mobile reads like the Apple App Store: bottom tab bar, full-width feed, // snap-scroll carousels and a full-screen search. Same data and install @@ -1131,6 +1135,8 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) {
) : activeNav === "community" && !searching ? ( + ) : activeNav === "studios" && !searching ? ( + ) : activeNav === "discover" && !searching ? ( ) : showGrid ? ( diff --git a/desktop/src/apps/StoreApp/types.ts b/desktop/src/apps/StoreApp/types.ts index 3dbe9b6e..a98fbd43 100644 --- a/desktop/src/apps/StoreApp/types.ts +++ b/desktop/src/apps/StoreApp/types.ts @@ -33,6 +33,8 @@ export interface CatalogApp { coverImage?: string; /** True when an installed app has a newer version available (drives Updates). */ update_available?: boolean; + /** Studios-specific lifecycle state. "soon" hides install and shows a badge. */ + studioState?: "installed" | "available" | "soon"; } export interface InstallTarget { diff --git a/desktop/src/apps/appstudio/BuildView.tsx b/desktop/src/apps/appstudio/BuildView.tsx new file mode 100644 index 00000000..68ac919b --- /dev/null +++ b/desktop/src/apps/appstudio/BuildView.tsx @@ -0,0 +1,217 @@ +import { CheckCircle2, Folder, Bell, Loader2, Sparkles } from "lucide-react"; + +/* ------------------------------------------------------------------ */ +/* BuildView -- sandbox preview + build log + prompt bar */ +/* ------------------------------------------------------------------ */ + +const CHORES = [ + { who: "M", task: "Take out the bins", sub: "Mara · Mon", done: true }, + { who: "B", task: "Walk the dog", sub: "Ben · daily", pts: 5, done: false }, + { who: "I", task: "Load dishwasher", sub: "Ivo · today", pts: 3, done: false }, + { who: "M", task: "Water the plants", sub: "Mara · Wed", pts: 2, done: false }, +]; + +const STEPS = [ + { label: "App shell + routing", sub: "taOS SDK window", done: true }, + { label: "Chore list + points", sub: "data model + UI", done: true }, + { label: "Family members", sub: "reading workspace contacts...", done: false }, +]; + +const CAPS = [ + { icon: Folder, label: "Workspace files" }, + { icon: Bell, label: "Send notifications" }, +]; + +export function BuildView() { + return ( +
+ {/* view header */} +
+

Build

+ + Chore Quest · taOS app · sandboxed + +
+ + {/* build area */} +
+ {/* sandbox panel */} +
+ {/* chip row */} +
+ } label="Live preview" /> + + + taOS SDK 1.0 · no network + +
+ + {/* app window in window */} +
+ {/* mini titlebar */} +
+
+ {[0, 1, 2].map((i) => ( + + ))} +
+ Chore Quest +
+ + {/* app body */} +
+
+ This week +
+
+ 3 of 6 done · Team Henderson +
+ + {CHORES.map((c) => ( +
+
+ {c.who} +
+
+
+ {c.task} +
+
+ {c.sub} +
+
+ {c.done ? ( + + ) : ( + + +{c.pts} + + )} +
+ ))} +
+
+
+ + {/* build log panel */} +
+
+ + Build log +
+ +
+ {STEPS.map((s) => ( +
+
+ {s.done ? ( + + + + ) : ( + + + + )} +
+
+
{s.label}
+
{s.sub}
+
+
+ ))} +
+ + {/* capabilities */} +
+
+ Capabilities requested +
+ {CAPS.map(({ icon: Icon, label }) => ( +
+ + {label} + + Granted + +
+ ))} +
+ + {/* model pill */} +
+
+
+
+
Qwen2.5-Coder 7B
+
local · fedora-gpu
+
+
+
+
+
+ + {/* prompt bar */} +
+
+ give each person a weekly points total and a little leaderboard +
+ +
+
+ ); +} + +function Chip({ icon, label }: { icon?: React.ReactNode; label: string }) { + return ( +
+ {icon} + {label} +
+ ); +} diff --git a/desktop/src/apps/appstudio/PublishView.tsx b/desktop/src/apps/appstudio/PublishView.tsx new file mode 100644 index 00000000..2b40c73e --- /dev/null +++ b/desktop/src/apps/appstudio/PublishView.tsx @@ -0,0 +1,177 @@ +import { useState } from "react"; +import { Folder, Bell, Users, Shield, Upload, Share2, Download, CheckSquare } from "lucide-react"; + +/* ------------------------------------------------------------------ */ +/* PublishView -- app identity + capabilities + side publish panel */ +/* ------------------------------------------------------------------ */ + +interface PermRow { + key: string; + icon: React.ElementType; + label: string; + desc: string; + defaultOn: boolean; +} + +const PERMS: PermRow[] = [ + { + key: "workspace", + icon: Folder, + label: "Workspace files", + desc: "Read and write the app's own folder", + defaultOn: true, + }, + { + key: "notifications", + icon: Bell, + label: "Notifications", + desc: "Send reminders for due chores", + defaultOn: true, + }, + { + key: "household", + icon: Users, + label: "Household members", + desc: "See names and avatars of your people", + defaultOn: false, + }, +]; + +export function PublishView() { + const [enabled, setEnabled] = useState>( + Object.fromEntries(PERMS.map((p) => [p.key, p.defaultOn])) + ); + + const toggle = (key: string) => + setEnabled((prev) => ({ ...prev, [key]: !prev[key] })); + + return ( +
+ {/* view header */} +
+

Publish

+ Review, set permissions, share +
+ +
+ {/* main */} +
+
+ {/* app identity */} +
+
+ +
+
+
Chore Quest
+
+ A weekly chore tracker with points and a family leaderboard. +
+
+
+ + {/* capabilities section */} +
+ Capabilities +
+ + {PERMS.map((p) => { + const Icon = p.icon; + const on = enabled[p.key]; + return ( +
+
+ +
+
+
{p.label}
+
{p.desc}
+
+ +
+ ); + })} + + {/* safety note */} +
+ +

+ Runs sandboxed with no network access. It can only touch what you grant above, and + you can change these any time. +

+
+
+
+ + {/* side panel */} +
+ {/* preview tile */} +
+ Chore Quest +
+ + + + + + + +

+ Community submissions are reviewed before they appear in the public Store. +

+
+
+
+ ); +} diff --git a/desktop/src/apps/appstudio/TemplatesView.tsx b/desktop/src/apps/appstudio/TemplatesView.tsx new file mode 100644 index 00000000..0f3e32a5 --- /dev/null +++ b/desktop/src/apps/appstudio/TemplatesView.tsx @@ -0,0 +1,140 @@ +import { Sparkles, CheckSquare, AlignLeft, Columns2, Gamepad2, MessageSquare, Image, Clock, LayoutDashboard } from "lucide-react"; + +/* ------------------------------------------------------------------ */ +/* TemplatesView -- hero + template grid */ +/* ------------------------------------------------------------------ */ + +interface TplCard { + label: string; + desc: string; + icon: React.ElementType; + gradient: string; +} + +const TEMPLATES: TplCard[] = [ + { + label: "Dashboard", + desc: "Cards, charts, and live stats.", + icon: LayoutDashboard, + gradient: "linear-gradient(135deg,#6f7687,#474d5e)", + }, + { + label: "Tracker", + desc: "Lists, checkboxes, streaks.", + icon: CheckSquare, + gradient: "linear-gradient(135deg,#5f8a6f,#456f54)", + }, + { + label: "Form", + desc: "Collect input, store responses.", + icon: AlignLeft, + gradient: "linear-gradient(135deg,#5d7a8a,#46606c)", + }, + { + label: "Kanban", + desc: "Columns and draggable cards.", + icon: Columns2, + gradient: "linear-gradient(135deg,#7a7488,#5e596b)", + }, + { + label: "Mini-game", + desc: "Score, timer, simple loop.", + icon: Gamepad2, + gradient: "linear-gradient(135deg,#8a7a5d,#6c5e46)", + }, + { + label: "Agent Panel", + desc: "A custom UI for one of your agents.", + icon: MessageSquare, + gradient: "linear-gradient(135deg,#6f7687,#474d5e)", + }, + { + label: "Gallery", + desc: "A grid of media with detail view.", + icon: Image, + gradient: "linear-gradient(135deg,#5f8a6f,#456f54)", + }, + { + label: "Blank", + desc: "An empty taOS SDK app.", + icon: Clock, + gradient: "linear-gradient(135deg,#7a7488,#5e596b)", + }, +]; + +export function TemplatesView() { + return ( +
+ {/* view header */} +
+

New app

+ + Describe it, or start from a template + +
+ + {/* scrollable body */} +
+ {/* hero */} +
+

+ Build a taOS app in plain words. +

+

+ Describe what it should do. An agent builds it against the taOS SDK, sandboxed and safe, + and you can publish it to your Store or share it with family. +

+
+
+ a shared shopping list the whole house can add to from their phones... +
+ +
+
+ + {/* section label */} +
+ Start from a template +
+ + {/* template grid */} +
+ {TEMPLATES.map((t) => { + const Icon = t.icon; + return ( + + ); + })} +
+
+
+ ); +} diff --git a/desktop/src/apps/codingstudio/BuildView.tsx b/desktop/src/apps/codingstudio/BuildView.tsx new file mode 100644 index 00000000..95088773 --- /dev/null +++ b/desktop/src/apps/codingstudio/BuildView.tsx @@ -0,0 +1,384 @@ +import { + Sparkles, + Folder, + FileText, + ClipboardCheck, + Check, + Circle, + ChevronDown, + Play, +} from "lucide-react"; + +const CODE_LINES = [ + { + n: 1, + jsx: ( + <> + import + {" { useTodos } "} + from + {" "} + {"'./useTodos'"} + + ), + }, + { + n: 2, + jsx: ( + <> + import + {" { TodoList } "} + from + {" "} + {"'./TodoList'"} + + ), + }, + { + n: 3, + jsx: ( + + {"// added by taOS - persists to localStorage"} + + ), + }, + { n: 4, jsx: <> }, + { + n: 5, + jsx: ( + <> + export default function + {" "} + App + {"() {"} + + ), + }, + { + n: 6, + jsx: ( + <> + {" "} + const + {" { todos, add, toggle, remaining } = "} + useTodos + {"()"} + + ), + }, + { + n: 7, + jsx: ( + <> + {" "} + return + {" ("} + + ), + }, + { + n: 8, + jsx: ( + <> + {" <"} + main + {" "} + className + {"="} + "app" + {">"} + + ), + }, + { + n: 9, + jsx: ( + <> + {" <"} + header + {">My Tasks <"} + small + {">{remaining} leftsmall + {">header + {">"} + + ), + }, + { + n: 10, + jsx: ( + <> + {" <"} + TodoList + {" "} + items + {"={todos} "} + onToggle + {"={toggle} />"} + + ), + }, + { + n: 11, + jsx: ( + <> + {" <"} + AddTodo + {" "} + onAdd + {"={add} />"} + + ), + }, + { + n: 12, + jsx: ( + <> + {" main + {">"} + + ), + }, + { n: 13, jsx: <>{" )"} }, + { n: 14, jsx: <>{"}"} }, +]; + +const BUILD_STEPS = [ + { status: "done" as const, title: "Scaffold Vite + React + TS", meta: "7 files - 1.4s" }, + { status: "done" as const, title: "Write TodoList + AddTodo", meta: "components/ - 2 files" }, + { status: "running" as const, title: "Add useTodos hook", meta: "wiring localStorage persistence..." }, + { status: "queued" as const, title: "Install dependencies", meta: "npm i - queued" }, + { status: "queued" as const, title: "Start dev server + preview", meta: "queued" }, +]; + +function StepIcon({ status }: { status: "done" | "running" | "queued" }) { + if (status === "done") { + return ( +
+ +
+ ); + } + if (status === "running") { + return ( +
+
+
+ ); + } + return ( +
+ +
+ ); +} + +export function BuildView() { + return ( + <> + + + {/* view header */} +
+

Build

+ + todo-app - Node + React - runs on fedora-gpu + +
+ + Chat + + + Diff + +
+
+ + {/* three-column body */} +
+ {/* column 1: file tree */} +
+
+ todo-app +
+ + {/* src folder */} +
+ + src +
+ + {/* App.tsx - active */} +
+ + App.tsx +
+
+ +
+ + TodoList.tsx +
+ +
+ + useTodos.ts +
+ +
+ + styles.css +
+ + {/* root files */} +
+ + index.html +
+ +
+ + package.json +
+ +
+ + vite.config.ts +
+
+ + {/* column 2: editor */} +
+ {/* tabs bar */} +
+
+ App.tsx + x +
+
+ useTodos.ts + x +
+
+ + {/* code area */} +
+ {CODE_LINES.map(({ n, jsx }) => ( +
+ + {n} + + + {jsx} + {n === 10 && ( + + )} + +
+ ))} +
+ + {/* terminal strip */} +
+
+ ~/todo-app + {" $ npm run dev"} +
+
+ VITE v5.4 + {" ready in 412 ms"} +
+
+ {"> Local: "} + http://todo-app.taos.local + {" "} + live +
+
+
+ + {/* column 3: build log */} +
+ {/* header */} +
+ + Build log +
+ + {/* steps */} +
+ {BUILD_STEPS.map((step) => ( +
+ +
+
{step.title}
+
+ {step.meta} +
+
+
+ ))} +
+ + {/* footer */} +
+ {/* model pill */} +
+
+
+
Qwen2.5-Coder 7B
+
local - fedora-gpu
+
+ +
+ + {/* open preview button */} +
+ + Open live preview +
+
+
+
+ + {/* prompt bar */} +
+