From 0c3cf77c6037df4012db3d3b0734bb732a79f99a Mon Sep 17 00:00:00 2001 From: Derek Bourgeois Date: Sun, 22 Mar 2026 08:46:29 -0400 Subject: [PATCH 1/2] feat: build the desktop MVP shell mockup --- app/Http/Controllers/HomeController.php | 563 +++++- composer.json | 3 +- composer.lock | 153 +- public/katra-logo-light.svg | 5 + public/katra-logo.svg | 5 + resources/css/app.css | 253 +++ .../components/desktop/feature-card.blade.php | 16 +- .../components/desktop/icon-button.blade.php | 16 + .../components/desktop/list-card.blade.php | 14 + .../desktop/message-bubble.blade.php | 34 + .../views/components/desktop/modal.blade.php | 30 + .../components/desktop/nav-item.blade.php | 55 + .../components/desktop/nav-section.blade.php | 64 + .../views/components/desktop/panel.blade.php | 47 + .../components/desktop/profile-menu.blade.php | 76 + .../desktop/runtime-status-card.blade.php | 29 - .../desktop/sidebar-block.blade.php | 32 - .../components/desktop/status-pill.blade.php | 19 + .../components/desktop/window-frame.blade.php | 14 +- .../desktop/workspace-status-card.blade.php | 14 - resources/views/welcome.blade.php | 1638 ++++++++++++++++- tests/Feature/DesktopShellTest.php | 104 +- tests/Feature/DesktopUiFeatureFlagTest.php | 15 +- 23 files changed, 2950 insertions(+), 249 deletions(-) create mode 100644 public/katra-logo-light.svg create mode 100644 public/katra-logo.svg create mode 100644 resources/views/components/desktop/icon-button.blade.php create mode 100644 resources/views/components/desktop/list-card.blade.php create mode 100644 resources/views/components/desktop/message-bubble.blade.php create mode 100644 resources/views/components/desktop/modal.blade.php create mode 100644 resources/views/components/desktop/nav-item.blade.php create mode 100644 resources/views/components/desktop/nav-section.blade.php create mode 100644 resources/views/components/desktop/panel.blade.php create mode 100644 resources/views/components/desktop/profile-menu.blade.php delete mode 100644 resources/views/components/desktop/runtime-status-card.blade.php delete mode 100644 resources/views/components/desktop/sidebar-block.blade.php create mode 100644 resources/views/components/desktop/status-pill.blade.php delete mode 100644 resources/views/components/desktop/workspace-status-card.blade.php diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index ff3f673..388a2c3 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,96 +2,527 @@ namespace App\Http\Controllers; +use App\Features\Desktop\AgentPresence; +use App\Features\Desktop\ArtifactSurfaces; +use App\Features\Desktop\ConversationChannels; +use App\Features\Desktop\MvpShell; +use App\Features\Desktop\TaskSurfaces; +use App\Features\Desktop\WorkspaceNavigation; use App\Models\Workspace; -use App\Services\Surreal\SurrealCliClient; -use App\Services\Surreal\SurrealConnection; use App\Services\Surreal\SurrealRuntimeManager; use App\Support\Features\DesktopUi; -use Illuminate\Support\Str; +use Illuminate\Http\Request; use Illuminate\View\View; use RuntimeException; use Throwable; class HomeController extends Controller { - public function __invoke(SurrealConnection $connection, SurrealCliClient $client, SurrealRuntimeManager $runtimeManager): View + /** + * @return array, + * notes: array, + * tasks: array, + * artifacts: array, + * decisions: array, + * messages: array + * }> + */ + private function workspaces(?Workspace $workspace, bool $localReady): array { - $workspace = null; - $surrealStatus = 'degraded'; - $surrealMessage = 'The Surreal-backed model layer is wired in, but the runtime is not available yet on this machine.'; - $runtimeReady = false; - $runtimeLabel = $this->runtimeLabel($connection, $client); - $surrealDetails = [ - ['label' => 'Runtime', 'value' => 'Unavailable'], - ['label' => 'Binary', 'value' => $this->binaryLabel($connection, $client)], - ['label' => 'Endpoint', 'value' => $connection->endpoint], + return [ + 'katra-local' => [ + 'slug' => 'katra-local', + 'label' => 'Katra Local', + 'meta' => 'Local', + 'prefix' => 'K', + 'summary' => $localReady + ? 'A local-first workspace on this device with the embedded Surreal runtime already available.' + : 'A local-first workspace on this device for conversations, tasks, artifacts, and decisions.', + 'room' => '# design-room', + 'roomStatus' => $localReady ? 'local' : 'draft', + 'participants' => [ + ['label' => 'You', 'meta' => 'Human'], + ['label' => 'Planner Agent', 'meta' => 'Worker'], + ['label' => 'Research Model', 'meta' => 'Model'], + ['label' => 'Context Agent', 'meta' => 'Worker'], + ], + 'notes' => [ + 'The core room should feel durable enough that work keeps accumulating here over time.', + 'Tasks, artifacts, and decisions should stay linked without overtaking the room itself.', + 'The first desktop shell should stay generic enough to seed other local-first products later on.', + ], + 'tasks' => [ + [ + 'label' => 'Shape the MVP shell', + 'status' => 'In review', + 'summary' => 'Tighten the room layout, spacing, and navigation so the shell feels like an app instead of a staged page.', + ], + [ + 'label' => 'Introduce connection switching', + 'status' => 'Queued', + 'summary' => 'Make room for local and remote instances without changing the top-level layout.', + ], + [ + 'label' => 'Refine linked work', + 'status' => 'Draft', + 'summary' => 'Keep tasks, artifacts, and decisions close to the room without turning the whole shell into a dashboard.', + ], + ], + 'artifacts' => [ + [ + 'label' => 'Room layout', + 'kind' => 'Note', + 'summary' => 'Current draft for the center room and right-side context split.', + ], + [ + 'label' => 'Brand guide', + 'kind' => 'Guide', + 'summary' => 'Nord palette, quieter type, and the current Katra identity rules.', + ], + ], + 'decisions' => [ + [ + 'label' => 'Persistent rooms', + 'owner' => 'Product', + 'summary' => 'One room per participant set should replace disposable transcript threads.', + ], + [ + 'label' => 'Diagnostics stay hidden', + 'owner' => 'Desktop', + 'summary' => 'Connection and runtime details belong in logs or a developer view, not in the main shell.', + ], + ], + 'messages' => [ + [ + 'speaker' => 'You', + 'role' => 'Human', + 'tone' => 'plain', + 'meta' => 'Just now', + 'body' => 'This room should feel like the place where work already lives, not like a landing page that still needs to explain itself.', + ], + [ + 'speaker' => 'Planner Agent', + 'role' => 'Agent', + 'tone' => 'accent', + 'meta' => 'Note', + 'body' => 'Keep the center focused on the room. Tasks, artifacts, and decisions can stay visible in the context rail without taking over the main flow.', + ], + [ + 'speaker' => 'Research Model', + 'role' => 'Model', + 'tone' => 'subtle', + 'meta' => 'Draft', + 'body' => 'The layout should stay generic enough to support other local-first products later: rooms on the left, active work in the center, linked context on the right.', + ], + ], + ], + 'design-lab' => [ + 'slug' => 'design-lab', + 'label' => 'Design Lab', + 'meta' => 'Local', + 'prefix' => 'D', + 'summary' => 'A quieter workspace for shell structure, navigation studies, and UI direction.', + 'room' => '# shell-studies', + 'roomStatus' => 'draft', + 'participants' => [ + ['label' => 'You', 'meta' => 'Human'], + ['label' => 'Visual Agent', 'meta' => 'Worker'], + ['label' => 'Layout Model', 'meta' => 'Model'], + ['label' => 'Critique Agent', 'meta' => 'Worker'], + ], + 'notes' => [ + 'Reduce surface noise until the room can carry the experience on its own.', + 'Treat navigation as durable structure, not as temporary marketing copy.', + 'Let component work stay atomic so the shell can evolve without rewrites.', + ], + 'tasks' => [ + [ + 'label' => 'Tighten sidebar density', + 'status' => 'Active', + 'summary' => 'Remove truncation pressure and make the workspace selector feel native to the app frame.', + ], + [ + 'label' => 'Calm the center room', + 'status' => 'Queued', + 'summary' => 'Strip away explanatory blocks until the room reads clearly without helping text.', + ], + ], + 'artifacts' => [ + [ + 'label' => 'Sidebar studies', + 'kind' => 'Mock', + 'summary' => 'Current workspace selector and room list direction.', + ], + [ + 'label' => 'Type rhythm', + 'kind' => 'Spec', + 'summary' => 'Reduced tracking and line-height targets for the desktop shell.', + ], + ], + 'decisions' => [ + [ + 'label' => 'No marketing copy in-app', + 'owner' => 'Design', + 'summary' => 'The desktop shell should assume the user already chose to be here.', + ], + ], + 'messages' => [ + [ + 'speaker' => 'You', + 'role' => 'Human', + 'tone' => 'plain', + 'meta' => 'Now', + 'body' => 'Let the workspace selector behave like a selector, not like a section accordion.', + ], + [ + 'speaker' => 'Visual Agent', + 'role' => 'Agent', + 'tone' => 'accent', + 'meta' => 'Draft', + 'body' => 'Wider rails and calmer spacing are helping, but the shell still needs less explanation and more structure.', + ], + ], + ], + 'relay-cloud' => [ + 'slug' => 'relay-cloud', + 'label' => 'Relay Cloud', + 'meta' => 'Remote', + 'prefix' => 'R', + 'summary' => 'A remote instance view for shared orchestration, worker presence, and linked team context.', + 'room' => '# relay-ops', + 'roomStatus' => 'remote', + 'participants' => [ + ['label' => 'You', 'meta' => 'Human'], + ['label' => 'Ops Agent', 'meta' => 'Worker'], + ['label' => 'Team Model', 'meta' => 'Model'], + ['label' => 'Routing Agent', 'meta' => 'Worker'], + ], + 'notes' => [ + 'Remote work should preserve the same room model as local work.', + 'The desktop app can be a client and a worker without splitting the product model in two.', + 'Shared AI workloads should flow through a dedicated worker queue, not the default queue.', + ], + 'tasks' => [ + [ + 'label' => 'Stage remote workers', + 'status' => 'Queued', + 'summary' => 'Make room for authenticated desktop workers contributing model capacity to a server instance.', + ], + [ + 'label' => 'Define secure payloads', + 'status' => 'Draft', + 'summary' => 'Keep worker jobs minimized, private, and cleaned up after completion.', + ], + ], + 'artifacts' => [ + [ + 'label' => 'Queue boundary notes', + 'kind' => 'Guide', + 'summary' => 'Remote AI work should use a dedicated worker queue, separate from normal Laravel jobs.', + ], + ], + 'decisions' => [ + [ + 'label' => 'Same product model', + 'owner' => 'Architecture', + 'summary' => 'Local and remote should be different runtimes of the same Katra model, not separate apps.', + ], + ], + 'messages' => [ + [ + 'speaker' => 'You', + 'role' => 'Human', + 'tone' => 'plain', + 'meta' => 'Latest', + 'body' => 'A remote instance should still feel like the same workspace model, not a separate product mode.', + ], + [ + 'speaker' => 'Ops Agent', + 'role' => 'Agent', + 'tone' => 'accent', + 'meta' => 'Queued', + 'body' => 'Desktop workers can stay available to authenticated instances while the main queue remains server-owned.', + ], + ], + ], ]; - $workspaceNavigationEnabled = DesktopUi::workspaceNavigationEnabled(); + } - try { - $runtimeReady = $runtimeManager->ensureReady(); - $runningProcessId = $runtimeManager->runningProcessId(); + /** + * @param array $workspaces + * @return array + */ + private function workspaceLinks(array $workspaces, string $activeWorkspace): array + { + return collect($workspaces) + ->map(fn (array $workspace): array => [ + 'label' => $workspace['label'], + 'meta' => $workspace['meta'], + 'active' => $workspace['slug'] === $activeWorkspace, + 'prefix' => $workspace['prefix'], + 'href' => route('home', ['workspace' => $workspace['slug']]), + ]) + ->values() + ->all(); + } - $surrealDetails[0]['value'] = $runtimeReady ? 'Running' : 'Unavailable'; + /** + * @return array + */ + private function favoriteLinks(string $activeRoom): array + { + return [ + ['label' => $activeRoom, 'active' => true, 'prefix' => '#', 'tone' => 'room'], + ['label' => 'Derek Bourgeois', 'prefix' => 'D', 'tone' => 'human'], + ['label' => 'Planner Agent', 'prefix' => '@', 'tone' => 'bot'], + ]; + } - if ($runningProcessId !== null) { - $surrealDetails[] = ['label' => 'Process', 'value' => sprintf('PID %d', $runningProcessId)]; - } + /** + * @return array + */ + private function roomLinks(string $activeRoom): array + { + return [ + ['label' => $activeRoom, 'active' => true, 'prefix' => '#', 'tone' => 'room'], + ['label' => '# product-direction', 'prefix' => '#', 'tone' => 'room'], + ['label' => 'Founders', 'prefix' => 'F', 'tone' => 'human'], + ]; + } - if (! $runtimeReady) { - $surrealMessage = $connection->usesLocalRuntime() - ? sprintf('The %s is configured for %s, but it is not responding yet.', $runtimeLabel, $connection->endpoint) - : sprintf('The remote Surreal runtime is not responding at %s.', $connection->endpoint); - - return view('welcome', [ - 'workspaceNavigationEnabled' => $workspaceNavigationEnabled, - 'workspace' => $workspace, - 'surrealStatus' => $surrealStatus, - 'surrealMessage' => $surrealMessage, - 'surrealDetails' => $surrealDetails, - ]); - } + /** + * @return array + */ + private function chatLinks(): array + { + return [ + ['label' => 'Derek Bourgeois', 'prefix' => 'D', 'tone' => 'human'], + ['label' => 'Planner Agent', 'prefix' => '@', 'tone' => 'bot'], + ['label' => 'Research Model', 'prefix' => '@', 'tone' => 'bot'], + ]; + } + + /** + * @return array + */ + private function chatContacts(): array + { + return [ + [ + 'label' => 'Derek Bourgeois', + 'value' => 'derek-bourgeois', + 'prefix' => 'D', + 'tone' => 'human', + 'subtitle' => 'Human', + ], + [ + 'label' => 'Planner Agent', + 'value' => 'planner-agent', + 'prefix' => '@', + 'tone' => 'bot', + 'subtitle' => 'Agent', + ], + [ + 'label' => 'Research Model', + 'value' => 'research-model', + 'prefix' => '@', + 'tone' => 'bot', + 'subtitle' => 'Model', + ], + ]; + } + + /** + * @return array + */ + private function workspaceTargets(): array + { + return [ + ['label' => 'Katra Local', 'value' => 'local'], + ['label' => 'Relay Cloud', 'value' => 'relay-cloud'], + ]; + } + + /** + * @param array{ + * tasks: array, + * artifacts: array, + * decisions: array + * } $workspace + * @return array + * }> + * }> + */ + private function conversationNodeTabs(array $workspace): array + { + $openTasks = collect($workspace['tasks']) + ->take(2) + ->map(fn (array $task): array => [ + 'label' => $task['label'], + 'meta' => $task['status'], + 'status' => $task['status'], + 'summary' => $task['summary'], + ]) + ->values() + ->all(); + + $openArtifacts = collect($workspace['artifacts']) + ->take(1) + ->map(fn (array $artifact): array => [ + 'label' => $artifact['label'], + 'meta' => $artifact['kind'], + 'status' => 'Open', + 'summary' => $artifact['summary'], + ]) + ->values() + ->all(); + + $openDecisions = collect($workspace['decisions']) + ->take(1) + ->map(fn (array $decision): array => [ + 'label' => $decision['label'], + 'meta' => $decision['owner'], + 'status' => 'Open', + 'summary' => $decision['summary'], + ]) + ->values() + ->all(); + + $closedTasks = collect($workspace['tasks']) + ->slice(2, 1) + ->map(fn (array $task): array => [ + 'label' => $task['label'], + 'meta' => 'Closed', + 'status' => 'Closed', + 'summary' => $task['summary'], + ]) + ->values() + ->all(); - $workspace = Workspace::desktopPreview(); - $surrealStatus = 'connected'; - $surrealMessage = sprintf('The preview workspace is persisted through the %s at %s.', $runtimeLabel, $connection->endpoint); + $closedArtifacts = collect($workspace['artifacts']) + ->slice(1, 1) + ->map(fn (array $artifact): array => [ + 'label' => $artifact['label'], + 'meta' => 'Archived', + 'status' => 'Archived', + 'summary' => $artifact['summary'], + ]) + ->values() + ->all(); + + $closedDecisions = collect($workspace['decisions']) + ->slice(1, 1) + ->map(fn (array $decision): array => [ + 'label' => $decision['label'], + 'meta' => 'Settled', + 'status' => 'Settled', + 'summary' => $decision['summary'], + ]) + ->values() + ->all(); + + return [ + [ + 'key' => 'open', + 'label' => 'Open', + 'groups' => [ + ['label' => 'Tasks', 'items' => $openTasks], + ['label' => 'Artifacts', 'items' => $openArtifacts], + ['label' => 'Decisions', 'items' => $openDecisions], + ], + ], + [ + 'key' => 'closed', + 'label' => 'Closed', + 'groups' => [ + ['label' => 'Tasks', 'items' => $closedTasks], + ['label' => 'Artifacts', 'items' => $closedArtifacts], + ['label' => 'Decisions', 'items' => $closedDecisions], + ], + ], + ]; + } + + public function __invoke(Request $request, SurrealRuntimeManager $runtimeManager): View + { + $workspace = null; + $localReady = false; + $desktopUiStates = DesktopUi::states(); + $mvpShellEnabled = DesktopUi::enabled($desktopUiStates, MvpShell::class); + $workspaceNavigationEnabled = DesktopUi::enabled($desktopUiStates, WorkspaceNavigation::class); + $conversationChannelsEnabled = DesktopUi::enabled($desktopUiStates, ConversationChannels::class); + $taskSurfacesEnabled = DesktopUi::enabled($desktopUiStates, TaskSurfaces::class); + $artifactSurfacesEnabled = DesktopUi::enabled($desktopUiStates, ArtifactSurfaces::class); + $agentPresenceEnabled = DesktopUi::enabled($desktopUiStates, AgentPresence::class); + + try { + if ($runtimeManager->ensureReady()) { + $workspace = Workspace::desktopPreview(); + $localReady = true; + } } catch (Throwable $exception) { - if ($exception instanceof RuntimeException) { - $surrealStatus = $runtimeReady ? 'runtime-ready' : 'degraded'; - $surrealMessage = $runtimeReady - ? sprintf('The %s is running at %s, but the preview workspace bootstrap failed: %s', $runtimeLabel, $connection->endpoint, Str::limit($exception->getMessage(), 160)) - : Str::limit($exception->getMessage(), 220); - } else { + if (! $exception instanceof RuntimeException) { report($exception); - $surrealMessage = 'The Surreal-backed model layer hit an unexpected error while loading the preview workspace.'; } } + $workspaces = $this->workspaces($workspace, $localReady); + $selectedWorkspace = $request->string('workspace')->value(); + $activeWorkspace = array_key_exists($selectedWorkspace, $workspaces) ? $selectedWorkspace : 'katra-local'; + $activeWorkspaceState = $workspaces[$activeWorkspace]; + return view('welcome', [ + 'mvpShellEnabled' => $mvpShellEnabled, 'workspaceNavigationEnabled' => $workspaceNavigationEnabled, + 'conversationChannelsEnabled' => $conversationChannelsEnabled, + 'taskSurfacesEnabled' => $taskSurfacesEnabled, + 'artifactSurfacesEnabled' => $artifactSurfacesEnabled, + 'agentPresenceEnabled' => $agentPresenceEnabled, 'workspace' => $workspace, - 'surrealStatus' => $surrealStatus, - 'surrealMessage' => $surrealMessage, - 'surrealDetails' => $surrealDetails, + 'previewState' => $activeWorkspaceState['roomStatus'], + 'activeWorkspace' => $activeWorkspaceState, + 'workspaceLinks' => $this->workspaceLinks($workspaces, $activeWorkspace), + 'workspaceTargets' => $this->workspaceTargets(), + 'favoriteLinks' => $this->favoriteLinks($activeWorkspaceState['room']), + 'roomLinks' => $this->roomLinks($activeWorkspaceState['room']), + 'chatLinks' => $this->chatLinks(), + 'chatContacts' => $this->chatContacts(), + 'conversationNodeTabs' => $this->conversationNodeTabs($activeWorkspaceState), + 'messages' => $activeWorkspaceState['messages'], + 'linkedTasks' => $activeWorkspaceState['tasks'], + 'linkedArtifacts' => $activeWorkspaceState['artifacts'], + 'decisions' => $activeWorkspaceState['decisions'], + 'feedbackGoals' => $activeWorkspaceState['notes'], + 'participants' => $activeWorkspaceState['participants'], ]); } - - private function runtimeLabel(SurrealConnection $connection, SurrealCliClient $client): string - { - return match (true) { - ! $connection->usesLocalRuntime() => 'remote Surreal runtime', - $client->usesBundledBinary() => 'bundled Surreal runtime', - default => 'local Surreal runtime', - }; - } - - private function binaryLabel(SurrealConnection $connection, SurrealCliClient $client): string - { - return match (true) { - ! $connection->usesLocalRuntime() => 'Remote endpoint', - $client->usesBundledBinary() => 'Bundled preview binary', - $client->isAvailable() => 'Machine-local CLI', - default => 'Missing', - }; - } } diff --git a/composer.json b/composer.json index 857e1ec..1ebff27 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/framework": "^13.0", "laravel/pennant": "^1.0", "laravel/tinker": "^3.0", - "nativephp/desktop": "dev-l13-compatibility" + "nativephp/desktop": "dev-l13-compatibility", + "postare/blade-mdi": "^1.0" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index f1eb851..11b6aac 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,89 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b1b28d182bb883b528f5356b08231224", + "content-hash": "26afd95ae23fb3e697e5ef21234b7e87", "packages": [ + { + "name": "blade-ui-kit/blade-icons", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/driesvints/blade-icons.git", + "reference": "caa92fde675d7a651c38bf73ca582ddada56f318" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/caa92fde675d7a651c38bf73ca582ddada56f318", + "reference": "caa92fde675d7a651c38bf73ca582ddada56f318", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^7.4|^8.0", + "symfony/console": "^5.3|^6.0|^7.0|^8.0", + "symfony/finder": "^5.3|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5.1", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.0|^10.5|^11.0" + }, + "bin": [ + "bin/blade-icons-generate" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "BladeUI\\Icons\\BladeIconsServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "BladeUI\\Icons\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dries Vints", + "homepage": "https://driesvints.com" + } + ], + "description": "A package to easily make use of icons in your Laravel Blade views.", + "homepage": "https://github.com/driesvints/blade-icons", + "keywords": [ + "blade", + "icons", + "laravel", + "svg" + ], + "support": { + "issues": "https://github.com/driesvints/blade-icons/issues", + "source": "https://github.com/driesvints/blade-icons" + }, + "funding": [ + { + "url": "https://github.com/sponsors/driesvints", + "type": "github" + }, + { + "url": "https://www.paypal.com/paypalme/driesvints", + "type": "paypal" + } + ], + "time": "2026-02-23T10:42:23+00:00" + }, { "name": "brick/math", "version": "0.14.8", @@ -2879,6 +2960,76 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "postare/blade-mdi", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/postare/blade-mdi.git", + "reference": "3988609c871e97bb8de8938791d724abfcca34db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/postare/blade-mdi/zipball/3988609c871e97bb8de8938791d724abfcca34db", + "reference": "3988609c871e97bb8de8938791d724abfcca34db", + "shasum": "" + }, + "require": { + "blade-ui-kit/blade-icons": "^1.1", + "php": "^8.0" + }, + "require-dev": { + "orchestra/database": "^7.0", + "orchestra/testbench": "^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Postare\\BladeMdi\\BladeMdiServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Postare\\BladeMdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "inerba", + "homepage": "https://github.com/inerba", + "role": "Developer" + } + ], + "description": "Material Design Icons for Laravel Blade views.", + "homepage": "https://github.com/renoki-co/blade-mdi", + "keywords": [ + "blade", + "design", + "icons", + "kit", + "laravel", + "material", + "mdi", + "php", + "ui" + ], + "support": { + "source": "https://github.com/postare/blade-mdi/tree/v1.0.0" + }, + "funding": [ + { + "url": "https://github.com/renoki-co", + "type": "github" + } + ], + "time": "2023-01-30T09:18:47+00:00" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/public/katra-logo-light.svg b/public/katra-logo-light.svg new file mode 100644 index 0000000..cc599f1 --- /dev/null +++ b/public/katra-logo-light.svg @@ -0,0 +1,5 @@ + + Katra + + + diff --git a/public/katra-logo.svg b/public/katra-logo.svg new file mode 100644 index 0000000..1d5703d --- /dev/null +++ b/public/katra-logo.svg @@ -0,0 +1,5 @@ + + Katra + + + diff --git a/resources/css/app.css b/resources/css/app.css index f89d156..f6df5d3 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -10,3 +10,256 @@ 'Segoe UI Symbol', 'Noto Color Emoji'; --font-mono: 'IBM Plex Mono', ui-monospace, monospace; } + +:root { + color-scheme: dark; + --shell-base: #2e3440; + --shell-panel: #3b4252; + --shell-surface: #434c5e; + --shell-elevated: #4c566a; + --shell-context: #353d4d; + --shell-input: #2e3440; + --shell-border: rgb(76 86 106 / 72%); + --shell-overlay: rgb(46 52 64 / 62%); + --shell-search-overlay: rgb(46 52 64 / 76%); + --shell-text: #eceff4; + --shell-text-muted: #d8dee9; + --shell-text-soft: rgb(216 222 233 / 72%); + --shell-text-faint: rgb(216 222 233 / 52%); + --shell-text-subtle: rgb(216 222 233 / 62%); + --shell-accent: #b48ead; + --shell-accent-hover: #c2a0ba; + --shell-accent-text: #2e3440; + --shell-accent-soft: rgb(180 142 173 / 18%); + --shell-bot: #81a1c1; + --shell-bot-text: #2e3440; + --shell-bot-soft: rgb(129 161 193 / 18%); + --shell-room: #88c0d0; + --shell-room-text: #2e3440; + --shell-room-soft: rgb(136 192 208 / 16%); + --shell-info: #88c0d0; + --shell-info-strong: #81a1c1; + --shell-info-soft: rgb(129 161 193 / 16%); + --shell-danger-soft: rgb(191 97 106 / 14%); + --shell-danger-hover: rgb(191 97 106 / 22%); + --shell-danger-text: #ebcb8b; + --shell-shadow: 0 28px 64px rgb(15 23 42 / 35%); +} + +html[data-shell-theme='light'] { + color-scheme: light; + --shell-base: #d6deea; + --shell-panel: #cad4e4; + --shell-surface: #bfcbde; + --shell-elevated: #b3c1d7; + --shell-context: #c2cde0; + --shell-input: #d0d9e7; + --shell-border: rgb(129 161 193 / 28%); + --shell-overlay: rgb(76 86 106 / 24%); + --shell-search-overlay: rgb(76 86 106 / 36%); + --shell-text: #3b4252; + --shell-text-muted: #4c566a; + --shell-text-soft: rgb(76 86 106 / 72%); + --shell-text-faint: rgb(76 86 106 / 54%); + --shell-text-subtle: rgb(76 86 106 / 62%); + --shell-accent: #b48ead; + --shell-accent-hover: #c2a0ba; + --shell-accent-text: #2e3440; + --shell-accent-soft: rgb(180 142 173 / 16%); + --shell-bot: #81a1c1; + --shell-bot-text: #2e3440; + --shell-bot-soft: rgb(129 161 193 / 16%); + --shell-room: #88c0d0; + --shell-room-text: #2e3440; + --shell-room-soft: rgb(136 192 208 / 14%); + --shell-info: #5e81ac; + --shell-info-strong: #5e81ac; + --shell-info-soft: rgb(94 129 172 / 16%); + --shell-danger-soft: rgb(191 97 106 / 10%); + --shell-danger-hover: rgb(191 97 106 / 18%); + --shell-danger-text: #bf616a; + --shell-shadow: 0 24px 56px rgb(76 86 106 / 18%); +} + +.shell-app { + background: var(--shell-base); + color: var(--shell-text); +} + +.shell-panel { + background: var(--shell-panel); +} + +.shell-surface { + background: var(--shell-surface); +} + +.shell-elevated { + background: var(--shell-elevated); +} + +.shell-input { + background: var(--shell-input); +} + +.shell-context-panel { + background: var(--shell-context); +} + +.shell-border { + border-color: var(--shell-border); +} + +.shell-text { + color: var(--shell-text); +} + +.shell-text-muted { + color: var(--shell-text-muted); +} + +.shell-text-soft { + color: var(--shell-text-soft); +} + +.shell-text-faint { + color: var(--shell-text-faint); +} + +.shell-text-subtle { + color: var(--shell-text-subtle); +} + +.shell-text-info { + color: var(--shell-info); +} + +.shell-text-info-strong { + color: var(--shell-info-strong); +} + +.shell-icon-button { + background: var(--shell-surface); + color: var(--shell-text); +} + +.shell-icon-button:hover { + background: var(--shell-elevated); +} + +.shell-active-row { + background: var(--shell-elevated); + color: var(--shell-text); +} + +.shell-accent-chip { + background: var(--shell-accent); + color: var(--shell-accent-text); +} + +.shell-accent-soft { + background: var(--shell-accent-soft); + color: var(--shell-text); +} + +.shell-human-chip { + background: var(--shell-accent); + color: var(--shell-accent-text); +} + +.shell-bot-chip { + background: var(--shell-bot); + color: var(--shell-bot-text); +} + +.shell-room-chip { + background: var(--shell-room-soft); + color: var(--shell-info-strong); +} + +.shell-theme-track { + background: var(--shell-elevated); +} + +.shell-theme-option { + color: var(--shell-text-subtle); +} + +.shell-theme-option:hover { + background: var(--shell-panel); + color: var(--shell-text); +} + +.shell-theme-option[data-theme-active='true'] { + background: var(--shell-accent); + color: var(--shell-accent-text); +} + +.shell-hover-surface:hover { + background: var(--shell-surface); +} + +.shell-hover-elevated:hover { + background: var(--shell-elevated); +} + +.shell-danger-button { + background: var(--shell-danger-soft); + color: var(--shell-danger-text); +} + +.shell-danger-button:hover { + background: var(--shell-danger-hover); +} + +.shell-overlay { + background: var(--shell-overlay); +} + +.shell-search-backdrop { + background: var(--shell-search-overlay); +} + +.shell-shadow { + box-shadow: var(--shell-shadow); +} + +.shell-logo-light { + display: none; +} + +html[data-shell-theme='light'] .shell-logo-dark { + display: none; +} + +html[data-shell-theme='light'] .shell-logo-light { + display: block; +} + +* { + scrollbar-color: var(--shell-accent) var(--shell-base); + scrollbar-width: thin; +} + +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +*::-webkit-scrollbar-track { + background: var(--shell-base); +} + +*::-webkit-scrollbar-thumb { + background: var(--shell-accent); + border: 2px solid var(--shell-base); + border-radius: 999px; +} + +*::-webkit-scrollbar-thumb:hover { + background: var(--shell-accent-hover); +} + +dialog::backdrop { + background: var(--shell-overlay); +} diff --git a/resources/views/components/desktop/feature-card.blade.php b/resources/views/components/desktop/feature-card.blade.php index ef8879b..b336fba 100644 --- a/resources/views/components/desktop/feature-card.blade.php +++ b/resources/views/components/desktop/feature-card.blade.php @@ -6,17 +6,17 @@ @php $toneClasses = match ($tone) { - 'cyan' => 'border-cyan-200/12 bg-cyan-300/8 text-cyan-100/70', - 'sky' => 'border-sky-200/12 bg-sky-300/8 text-sky-100/70', - 'emerald' => 'border-emerald-200/12 bg-emerald-300/8 text-emerald-100/70', - default => 'border-white/10 bg-white/5 text-slate-300/72', + 'cyan' => 'bg-[#3B4252] text-[#88C0D0]', + 'sky' => 'bg-[#3B4252] text-[#81A1C1]', + 'emerald' => 'bg-[#3B4252] text-[#A3BE8C]', + default => 'bg-[#3B4252] text-[#D8DEE9]/72', }; @endphp -
class(['rounded-[28px] border p-5', $toneClasses]) }}> -

{{ $eyebrow }}

-

{{ $title }}

-
+
class(['rounded-[22px] p-4', $toneClasses]) }}> +

{{ $eyebrow }}

+

{{ $title }}

+
{{ $slot }}
diff --git a/resources/views/components/desktop/icon-button.blade.php b/resources/views/components/desktop/icon-button.blade.php new file mode 100644 index 0000000..0353e64 --- /dev/null +++ b/resources/views/components/desktop/icon-button.blade.php @@ -0,0 +1,16 @@ +@props([ + 'label', + 'dialogId' => null, +]) + + diff --git a/resources/views/components/desktop/list-card.blade.php b/resources/views/components/desktop/list-card.blade.php new file mode 100644 index 0000000..9a952a7 --- /dev/null +++ b/resources/views/components/desktop/list-card.blade.php @@ -0,0 +1,14 @@ +@props([ + 'label', + 'meta', + 'summary', +]) + +
class(['shell-input rounded-[22px] px-4 py-4']) }}> +
+

{{ $label }}

+ {{ $meta }} +
+ +

{{ $summary }}

+
diff --git a/resources/views/components/desktop/message-bubble.blade.php b/resources/views/components/desktop/message-bubble.blade.php new file mode 100644 index 0000000..d1b16fb --- /dev/null +++ b/resources/views/components/desktop/message-bubble.blade.php @@ -0,0 +1,34 @@ +@props([ + 'speaker', + 'role', + 'meta', + 'tone' => 'plain', +]) + +@php + $wrapperClasses = match ($tone) { + 'accent' => 'shell-surface', + 'subtle' => 'shell-panel', + default => 'shell-input', + }; + + $roleClasses = match ($tone) { + 'accent' => 'text-[var(--shell-accent)]', + 'subtle' => 'shell-text-info', + default => 'shell-text-soft', + }; +@endphp + +
class(['rounded-[22px] px-4 py-4', $wrapperClasses]) }}> +
+
+

{{ $speaker }}

+ {{ $role }} +
+ {{ $meta }} +
+ +
+ {{ $slot }} +
+
diff --git a/resources/views/components/desktop/modal.blade.php b/resources/views/components/desktop/modal.blade.php new file mode 100644 index 0000000..69b59db --- /dev/null +++ b/resources/views/components/desktop/modal.blade.php @@ -0,0 +1,30 @@ +@props([ + 'id', + 'title', + 'description' => null, +]) + +class(['shell-panel shell-text shell-shadow shell-border m-auto w-full max-w-md rounded-[24px] border p-0']) }}> +
+
+
+
+

{{ $title }}

+ @if ($description) +

{{ $description }}

+ @endif +
+ +
+ +
+
+
+ + {{ $slot }} +
+
diff --git a/resources/views/components/desktop/nav-item.blade.php b/resources/views/components/desktop/nav-item.blade.php new file mode 100644 index 0000000..a429a48 --- /dev/null +++ b/resources/views/components/desktop/nav-item.blade.php @@ -0,0 +1,55 @@ +@props([ + 'label', + 'prefix' => '•', + 'meta' => null, + 'active' => false, + 'muted' => false, + 'href' => null, + 'tone' => 'room', +]) + +@php + $containerClasses = match (true) { + $active => 'shell-active-row', + $muted => 'bg-transparent shell-text-faint', + default => 'bg-transparent shell-text', + }; + + $prefixClasses = match (true) { + $active => 'shell-accent-chip', + $muted => 'shell-surface shell-text-faint', + default => match ($tone) { + 'human' => 'shell-human-chip', + 'bot' => 'shell-bot-chip', + default => 'shell-room-chip', + }, + }; + + $metaClasses = match (true) { + $active => 'shell-accent-soft', + $muted => 'shell-surface shell-text-faint', + default => 'shell-surface shell-text-soft', + }; +@endphp + +@if ($href) + class(['shell-hover-elevated flex items-center gap-3 rounded-2xl px-3 py-2 transition-colors', $containerClasses]) }}> + {{ $prefix }} +
+

{{ $label }}

+
+ @if ($meta) + {{ $meta }} + @endif +
+@else +
class(['flex items-center gap-3 rounded-2xl px-3 py-2', $containerClasses]) }}> + {{ $prefix }} +
+

{{ $label }}

+
+ @if ($meta) + {{ $meta }} + @endif +
+@endif diff --git a/resources/views/components/desktop/nav-section.blade.php b/resources/views/components/desktop/nav-section.blade.php new file mode 100644 index 0000000..c905f47 --- /dev/null +++ b/resources/views/components/desktop/nav-section.blade.php @@ -0,0 +1,64 @@ +@props([ + 'label', + 'collapsible' => false, + 'open' => false, + 'actionLabel' => null, + 'actionDialogId' => null, +]) + +@php + $sectionId = 'nav-section-'.md5($label.($actionDialogId ?? '')); +@endphp + +@if ($collapsible) +
+
+

{{ $label }}

+ +
+ + + @if ($actionLabel && $actionDialogId) + + @endif +
+
+ +
! $open])> + {{ $slot }} +
+
+@else +
+
+

{{ $label }}

+ + @if ($actionLabel && $actionDialogId) + + @endif +
+
+ {{ $slot }} +
+
+@endif diff --git a/resources/views/components/desktop/panel.blade.php b/resources/views/components/desktop/panel.blade.php new file mode 100644 index 0000000..eb189cc --- /dev/null +++ b/resources/views/components/desktop/panel.blade.php @@ -0,0 +1,47 @@ +@props([ + 'eyebrow' => null, + 'title' => null, + 'subtitle' => null, + 'tone' => 'slate', + 'contentClass' => 'space-y-4', +]) + +@php + $toneClasses = match ($tone) { + 'cyan' => 'shell-panel', + 'sky' => 'shell-panel', + 'emerald' => 'shell-panel', + 'fuchsia' => 'shell-panel', + 'amber' => 'shell-panel', + default => 'shell-panel', + }; + + $eyebrowClasses = match ($tone) { + 'cyan' => 'shell-text-info', + 'sky' => 'shell-text-info-strong', + 'emerald' => 'text-emerald-300', + 'fuchsia' => 'text-[var(--shell-accent)]', + 'amber' => 'text-amber-300', + default => 'shell-text-faint', + }; +@endphp + +
class(['rounded-[22px] p-4', $toneClasses]) }}> + @if ($eyebrow || $title || $subtitle) +
+ @if ($eyebrow) +

{{ $eyebrow }}

+ @endif + @if ($title) +

{{ $title }}

+ @endif + @if ($subtitle) +

{{ $subtitle }}

+ @endif +
+ @endif + +
+ {{ $slot }} +
+
diff --git a/resources/views/components/desktop/profile-menu.blade.php b/resources/views/components/desktop/profile-menu.blade.php new file mode 100644 index 0000000..eefc340 --- /dev/null +++ b/resources/views/components/desktop/profile-menu.blade.php @@ -0,0 +1,76 @@ +@props([ + 'name', + 'email', + 'initials', +]) + +
+ + + {{ $initials }} + + +
+

{{ $name }}

+

{{ $email }}

+
+ +
+ +
+
+ +
+
+
+ + {{ $initials }} + +
+

{{ $name }}

+

{{ $email }}

+
+
+ +
+ + + + + +
+ +
+

Theme

+ +
+ + + +
+
+ + +
+
+
diff --git a/resources/views/components/desktop/runtime-status-card.blade.php b/resources/views/components/desktop/runtime-status-card.blade.php deleted file mode 100644 index 696afa4..0000000 --- a/resources/views/components/desktop/runtime-status-card.blade.php +++ /dev/null @@ -1,29 +0,0 @@ -@props([ - 'status', - 'message', - 'details' => [], -]) - -@php - $statusTone = match ($status) { - 'connected' => 'text-emerald-200', - 'runtime-ready' => 'text-cyan-200', - default => 'text-amber-200', - }; -@endphp - -
-

Surreal Foundation

-

- {{ str_replace('-', ' ', $status) }} -

-

{{ $message }}

-
- @foreach ($details as $detail) -
-
{{ $detail['label'] }}
-
{{ $detail['value'] }}
-
- @endforeach -
-
diff --git a/resources/views/components/desktop/sidebar-block.blade.php b/resources/views/components/desktop/sidebar-block.blade.php deleted file mode 100644 index 7886dca..0000000 --- a/resources/views/components/desktop/sidebar-block.blade.php +++ /dev/null @@ -1,32 +0,0 @@ -@props([ - 'eyebrow', - 'title' => null, - 'tone' => 'slate', - 'contentClass' => 'text-sm leading-6 text-slate-200/78', -]) - -@php - $toneClasses = match ($tone) { - 'cyan' => 'border-cyan-200/12 bg-cyan-300/8', - 'fuchsia' => 'border-fuchsia-200/15 bg-fuchsia-300/10', - 'amber' => 'border-amber-200/12 bg-amber-300/8', - default => 'border-white/10 bg-white/5', - }; - - $eyebrowClasses = match ($tone) { - 'cyan' => 'text-cyan-100/72', - 'fuchsia' => 'text-fuchsia-100/78', - 'amber' => 'text-amber-100/78', - default => 'text-slate-300/72', - }; -@endphp - -
class(['rounded-[28px] border p-5', $toneClasses]) }}> -

{{ $eyebrow }}

- @if ($title) -

{{ $title }}

- @endif -
- {{ $slot }} -
-
diff --git a/resources/views/components/desktop/status-pill.blade.php b/resources/views/components/desktop/status-pill.blade.php new file mode 100644 index 0000000..7e4e84b --- /dev/null +++ b/resources/views/components/desktop/status-pill.blade.php @@ -0,0 +1,19 @@ +@props([ + 'label', + 'tone' => 'slate', +]) + +@php + $toneClasses = match ($tone) { + 'fuchsia' => 'bg-[#B48EAD]/16 text-[#ECEFF4]', + 'cyan' => 'bg-[#88C0D0]/14 text-[#E5E9F0]', + 'sky' => 'bg-[#81A1C1]/16 text-[#E5E9F0]', + 'emerald' => 'bg-[#A3BE8C]/14 text-[#E5E9F0]', + 'amber' => 'bg-[#EBCB8B]/14 text-[#E5E9F0]', + default => 'shell-surface shell-text-soft', + }; +@endphp + +class(['inline-flex items-center rounded-full px-3 py-1 font-mono text-[10px] uppercase tracking-[0.12em]', $toneClasses]) }}> + {{ $label }} + diff --git a/resources/views/components/desktop/window-frame.blade.php b/resources/views/components/desktop/window-frame.blade.php index ce1882a..4be06bb 100644 --- a/resources/views/components/desktop/window-frame.blade.php +++ b/resources/views/components/desktop/window-frame.blade.php @@ -3,19 +3,19 @@ 'badge', ]) -
-
+
+
- - - + + +
-

{{ $label }}

+

{{ $label }}

-
+
{{ $badge }}
diff --git a/resources/views/components/desktop/workspace-status-card.blade.php b/resources/views/components/desktop/workspace-status-card.blade.php deleted file mode 100644 index 3c88115..0000000 --- a/resources/views/components/desktop/workspace-status-card.blade.php +++ /dev/null @@ -1,14 +0,0 @@ -@props([ - 'workspace' => null, -]) - -
-

Preview Workspace

- @if ($workspace) -

{{ $workspace->name }}

-

{{ $workspace->id }}

-

{{ $workspace->summary }}

- @else -

The runtime is visible now, but no Surreal-backed preview workspace has been materialized on this machine yet.

- @endif -
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index 0d80f90..b43b41d 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -13,101 +13,1585 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) @endif - -
-
-
-
- -
- - -
-
-
-

NativePHP desktop shell

- -
-

- Katra is taking shape as a graph-native workspace for collaborative intelligence. -

- -

- This first shell proves the Laravel app can launch inside NativePHP while keeping the Katra v2 direction visible: - local-first workflows, graph-native context, multi-runtime delivery, and a desktop experience built intentionally from day one. -

+ +
+ @if (! $mvpShellEnabled) +
+ +
+ + +
+
+
+ @else + @php + $searchResults = [ + [ + 'label' => 'Conversations', + 'items' => [ + ['title' => '# design-room', 'meta' => 'Room', 'summary' => 'Current active room in Katra Local.'], + ['title' => '# shell-studies', 'meta' => 'Room', 'summary' => 'Design-focused room for layout and navigation work.'], + ], + ], + [ + 'label' => 'People and agents', + 'items' => [ + ['title' => 'Derek Bourgeois', 'meta' => 'Human', 'summary' => 'Direct conversation and workspace owner context.'], + ['title' => 'Planner Agent', 'meta' => 'Worker', 'summary' => 'Planning and structuring support for the active room.'], + ], + ], + [ + 'label' => 'Nodes', + 'items' => [ + ['title' => 'Shape the MVP shell', 'meta' => 'Task', 'summary' => 'Open node linked into the current conversation.'], + ['title' => 'Sidebar studies', 'meta' => 'Artifact', 'summary' => 'Mock artifact connected to the design room.'], + ], + ], + ]; + + $conversationSeedMessages = collect($messages) + ->values() + ->map(fn (array $message, int $index): array => [ + 'id' => 'seed-'.$index, + 'sender' => $message['speaker'], + 'role' => $message['role'], + 'meta' => $message['meta'], + 'body' => $message['body'], + 'direction' => $message['speaker'] === 'You' ? 'outgoing' : 'incoming', + 'attachments' => [], + ]) + ->all(); + + $conversationResponders = collect($participants) + ->filter(fn (array $participant): bool => $participant['meta'] !== 'Human') + ->values() + ->map(fn (array $participant): array => [ + 'label' => $participant['label'], + 'role' => $participant['meta'], + ]) + ->all(); + + $conversationMockReplies = [ + 'I can break this into a couple of linked nodes without changing the flow of the room.', + 'That looks good. I would tighten the interaction first, then let the linked work follow behind it.', + 'We can keep the room focused and still attach the next task, artifact, or decision from the context rail.', + 'This conversation feels clearer when the structure stays quiet and the actions stay close to the composer.', + 'I can take the next pass on this and keep the changes scoped to the active conversation.', + ]; + @endphp +
+
+ + +
+
+
+ + +
+
+ + +
+ +
-
- - + +
+ +
+
+

Conversation

+

{{ $activeWorkspace['room'] }}

+

+ Shared room for people, models, and agents working inside {{ $activeWorkspace['label'] }}. +

+
+
+
+ +
+
+
+ @foreach ($conversationSeedMessages as $message) + @php + $isOutgoing = $message['direction'] === 'outgoing'; + $messageRoleTone = match ($message['role']) { + 'Human' => 'shell-text-faint', + 'Agent' => 'text-[color:var(--shell-accent)]', + default => 'shell-text-info-strong', + }; + $messageBubbleTone = $isOutgoing ? 'shell-accent-soft' : 'shell-elevated'; + @endphp + +
+
+
+ {{ $message['sender'] }} + {{ $message['role'] }} + {{ $message['meta'] }} +
+ +
+

{{ $message['body'] }}

+
+
+
+ @endforeach +
+ +
+ + + + + + +
+
+ + + +
+ + +
-
+
+ +
- -
+
+ + + + + + @endif + + + +
+
+ + +
+ +
+ + +

+ Local is always available. Connected servers only appear here when this user can create workspaces on them. +

+
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+
+ + +
+ +
+

Selected

+
+

No contacts selected yet.

+
+
+ +
+

Available contacts

+
+ @foreach ($chatContacts as $contact) + + @endforeach +
+
+
+ +
+ + +
+
+
+ + diff --git a/tests/Feature/DesktopShellTest.php b/tests/Feature/DesktopShellTest.php index 3da4bbc..e5d19c9 100644 --- a/tests/Feature/DesktopShellTest.php +++ b/tests/Feature/DesktopShellTest.php @@ -10,17 +10,77 @@ $this->get('/') ->assertSuccessful() - ->assertSee('NativePHP desktop shell') - ->assertSee('composer native:dev') - ->assertSee('Katra is taking shape as a graph-native workspace') - ->assertSee('Surreal Foundation') - ->assertSee('Bundled preview') - ->assertSee('local Surreal runtime') - ->assertSee('Runtime') - ->assertSee('Binary') - ->assertSee('Endpoint') - ->assertSee('Unavailable') - ->assertDontSee('Workspace navigation pilot'); + ->assertSee('Katra') + ->assertSee('Workspaces') + ->assertSee('Favorites') + ->assertSee('Rooms') + ->assertSee('Chats') + ->assertSee('Create room') + ->assertSee('Create chat') + ->assertSee('Planner Agent') + ->assertSee('Research Model') + ->assertSee('# design-room') + ->assertSee('Composer') + ->assertSee('Create workspace') + ->assertSee('Workspace name') + ->assertSee('Create room') + ->assertSee('Room name') + ->assertSee('Create chat') + ->assertSee('Start conversation') + ->assertSee('Contacts') + ->assertSee('Search people, agents, and models') + ->assertSee('Selected') + ->assertSee('Available contacts') + ->assertSee('No contacts selected yet.') + ->assertSee('Server') + ->assertSee('Katra Local') + ->assertSee('Relay Cloud') + ->assertSee('Research Model') + ->assertSee('Collapse sidebar') + ->assertSee('Expand sidebar') + ->assertSee('Search conversations, people, and nodes') + ->assertSee('People and agents') + ->assertSee('Open context panel') + ->assertSee('Close context panel') + ->assertSee('Pin context panel') + ->assertSee('Resize context panel') + ->assertSee('Manage people') + ->assertSee('Nodes') + ->assertSee('Open') + ->assertSee('Closed') + ->assertSee('In review') + ->assertSee('Assign to agent') + ->assertSee('Assign') + ->assertSee('Choose an agent') + ->assertSee('Context Agent') + ->assertSee('Attach file') + ->assertSee('Toggle voice mode') + ->assertSee('Send message') + ->assertSee('Message # design-room') + ->assertSee('Voice mode selected') + ->assertSee('Tighten the room layout, spacing, and navigation so the shell feels like an app instead of a staged page.') + ->assertSee('Derek Bourgeois') + ->assertSee('derek@katra.io') + ->assertSee('Profile settings') + ->assertSee('Workspace settings') + ->assertSee('Administration') + ->assertSee('Manage connections') + ->assertSee('Light') + ->assertSee('Dark') + ->assertSee('System') + ->assertSee('Log out') + ->assertDontSee('desktop mvp preview') + ->assertDontSee('composer native:dev') + ->assertDontSee('Surreal Foundation') + ->assertDontSee('Runtime') + ->assertDontSee('Binary') + ->assertDontSee('Endpoint') + ->assertDontSee('single active session') + ->assertDontSee('Type') + ->assertDontSee('First note') + ->assertDontSee('Views') + ->assertDontSee('Workspace navigation pilot') + ->assertDontSee('Message input will live here.'); }); test('the desktop shell falls back to default feature flags before the Pennant table exists', function () { @@ -40,6 +100,24 @@ $this->get('/') ->assertSuccessful() - ->assertSee('NativePHP desktop shell') - ->assertDontSee('Workspace navigation pilot'); + ->assertSee('Katra') + ->assertSee('# design-room') + ->assertDontSee('Workspace navigation'); +}); + +test('the desktop shell can switch the active mock workspace from the selector', function () { + config()->set('pennant.default', 'array'); + config()->set('surreal.autostart', false); + config()->set('surreal.host', '127.0.0.1'); + config()->set('surreal.port', 18999); + config()->set('surreal.endpoint', 'ws://127.0.0.1:18999'); + config()->set('surreal.binary', 'surreal-missing-binary-for-desktop-shell-test'); + + $this->get('/?workspace=design-lab') + ->assertSuccessful() + ->assertSee('Design Lab') + ->assertSee('# shell-studies') + ->assertSee('Shared room for people, models, and agents working inside Design Lab.') + ->assertSee('Visual Agent') + ->assertSee('Critique Agent'); }); diff --git a/tests/Feature/DesktopUiFeatureFlagTest.php b/tests/Feature/DesktopUiFeatureFlagTest.php index fcf0617..375b0ae 100644 --- a/tests/Feature/DesktopUiFeatureFlagTest.php +++ b/tests/Feature/DesktopUiFeatureFlagTest.php @@ -74,5 +74,18 @@ $this->get('/') ->assertSuccessful() - ->assertSee('Workspace navigation pilot'); + ->assertSee('# design-room') + ->assertSee('Katra'); +}); + +test('the desktop shell can be hidden behind the mvp flag', function () { + config()->set('pennant.default', 'array'); + + Feature::for(DesktopUi::scope())->deactivate(MvpShell::class); + + $this->get('/') + ->assertSuccessful() + ->assertSee('The MVP workspace shell is currently hidden.') + ->assertDontSee('# design-room') + ->assertDontSee('Notes'); }); From f83d760c83fcdaaea8b691cfdcaa0fa002ea7264 Mon Sep 17 00:00:00 2001 From: Derek Bourgeois Date: Sun, 22 Mar 2026 09:50:00 -0400 Subject: [PATCH 2/2] fix: harden desktop shell component wiring --- app/Http/Controllers/HomeController.php | 28 ++---------- .../components/desktop/icon-button.blade.php | 2 +- .../components/desktop/nav-section.blade.php | 9 +--- resources/views/welcome.blade.php | 44 +++++++++++++++++-- tests/Feature/DesktopShellTest.php | 1 - 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 388a2c3..0530385 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,12 +2,7 @@ namespace App\Http\Controllers; -use App\Features\Desktop\AgentPresence; -use App\Features\Desktop\ArtifactSurfaces; -use App\Features\Desktop\ConversationChannels; use App\Features\Desktop\MvpShell; -use App\Features\Desktop\TaskSurfaces; -use App\Features\Desktop\WorkspaceNavigation; use App\Models\Workspace; use App\Services\Surreal\SurrealRuntimeManager; use App\Support\Features\DesktopUi; @@ -35,7 +30,7 @@ class HomeController extends Controller * messages: array * }> */ - private function workspaces(?Workspace $workspace, bool $localReady): array + private function workspaces(bool $localReady): array { return [ 'katra-local' => [ @@ -474,19 +469,13 @@ private function conversationNodeTabs(array $workspace): array public function __invoke(Request $request, SurrealRuntimeManager $runtimeManager): View { - $workspace = null; $localReady = false; $desktopUiStates = DesktopUi::states(); $mvpShellEnabled = DesktopUi::enabled($desktopUiStates, MvpShell::class); - $workspaceNavigationEnabled = DesktopUi::enabled($desktopUiStates, WorkspaceNavigation::class); - $conversationChannelsEnabled = DesktopUi::enabled($desktopUiStates, ConversationChannels::class); - $taskSurfacesEnabled = DesktopUi::enabled($desktopUiStates, TaskSurfaces::class); - $artifactSurfacesEnabled = DesktopUi::enabled($desktopUiStates, ArtifactSurfaces::class); - $agentPresenceEnabled = DesktopUi::enabled($desktopUiStates, AgentPresence::class); try { if ($runtimeManager->ensureReady()) { - $workspace = Workspace::desktopPreview(); + Workspace::desktopPreview(); $localReady = true; } } catch (Throwable $exception) { @@ -495,20 +484,13 @@ public function __invoke(Request $request, SurrealRuntimeManager $runtimeManager } } - $workspaces = $this->workspaces($workspace, $localReady); + $workspaces = $this->workspaces($localReady); $selectedWorkspace = $request->string('workspace')->value(); $activeWorkspace = array_key_exists($selectedWorkspace, $workspaces) ? $selectedWorkspace : 'katra-local'; $activeWorkspaceState = $workspaces[$activeWorkspace]; return view('welcome', [ 'mvpShellEnabled' => $mvpShellEnabled, - 'workspaceNavigationEnabled' => $workspaceNavigationEnabled, - 'conversationChannelsEnabled' => $conversationChannelsEnabled, - 'taskSurfacesEnabled' => $taskSurfacesEnabled, - 'artifactSurfacesEnabled' => $artifactSurfacesEnabled, - 'agentPresenceEnabled' => $agentPresenceEnabled, - 'workspace' => $workspace, - 'previewState' => $activeWorkspaceState['roomStatus'], 'activeWorkspace' => $activeWorkspaceState, 'workspaceLinks' => $this->workspaceLinks($workspaces, $activeWorkspace), 'workspaceTargets' => $this->workspaceTargets(), @@ -518,10 +500,6 @@ public function __invoke(Request $request, SurrealRuntimeManager $runtimeManager 'chatContacts' => $this->chatContacts(), 'conversationNodeTabs' => $this->conversationNodeTabs($activeWorkspaceState), 'messages' => $activeWorkspaceState['messages'], - 'linkedTasks' => $activeWorkspaceState['tasks'], - 'linkedArtifacts' => $activeWorkspaceState['artifacts'], - 'decisions' => $activeWorkspaceState['decisions'], - 'feedbackGoals' => $activeWorkspaceState['notes'], 'participants' => $activeWorkspaceState['participants'], ]); } diff --git a/resources/views/components/desktop/icon-button.blade.php b/resources/views/components/desktop/icon-button.blade.php index 0353e64..61ccd74 100644 --- a/resources/views/components/desktop/icon-button.blade.php +++ b/resources/views/components/desktop/icon-button.blade.php @@ -5,7 +5,7 @@