diff --git a/apps/code/20260130-000000/package.json b/apps/code/20260130-000000/package.json
index 0c83129..53d2806 100644
--- a/apps/code/20260130-000000/package.json
+++ b/apps/code/20260130-000000/package.json
@@ -9,7 +9,8 @@
"dev": "bun run --hot index.tsx"
},
"toes": {
- "icon": "🖥️"
+ "tool": true,
+ "icon": "💻"
},
"devDependencies": {
"@types/bun": "latest"
diff --git a/apps/todo/.npmrc b/apps/todo/.npmrc
new file mode 100644
index 0000000..6c57d5c
--- /dev/null
+++ b/apps/todo/.npmrc
@@ -0,0 +1 @@
+registry=https://npm.nose.space
diff --git a/apps/todo/bun.lock b/apps/todo/bun.lock
new file mode 100644
index 0000000..b136c8a
--- /dev/null
+++ b/apps/todo/bun.lock
@@ -0,0 +1,43 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "todo",
+ "dependencies": {
+ "@because/forge": "*",
+ "@because/howl": "*",
+ "@because/hype": "*",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5.9.2",
+ },
+ },
+ },
+ "packages": {
+ "@because/forge": ["@because/forge@0.0.1", "https://npm.nose.space/@because/forge/-/forge-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-QS5CK51gcWma91i4uECWe4HPJeNHcE+Af4SQHOcfEovyzOEa7VOTAjei+jIWr2i+abGWqQCEC9wIuFgPgyr2Bg=="],
+
+ "@because/howl": ["@because/howl@0.0.2", "https://npm.nose.space/@because/howl/-/howl-0.0.2.tgz", { "dependencies": { "lucide-static": "^0.555.0" }, "peerDependencies": { "@because/forge": "*", "typescript": "^5" } }, "sha512-Z4okzEa282LKkBk9DQwEUU6FT+PeThfQ6iQAY41LIEjs8B2kfXRZnbWLs7tgpwCfYORxb0RO4Hr7KiyEqnfTvQ=="],
+
+ "@because/hype": ["@because/hype@0.0.1", "https://npm.nose.space/@because/hype/-/hype-0.0.1.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-i92DNUXJOwt3J8dN1x8sh7i86blelcTCk8XDpwD839Ic8oe710lkDSVXJ7xYZb/i8YtzGhRg+L6eXDhaRiU2Pw=="],
+
+ "@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
+
+ "@types/node": ["@types/node@25.1.0", "https://npm.nose.space/@types/node/-/node-25.1.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
+
+ "bun-types": ["bun-types@1.3.8", "https://npm.nose.space/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
+
+ "hono": ["hono@4.11.7", "https://npm.nose.space/hono/-/hono-4.11.7.tgz", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
+
+ "kleur": ["kleur@4.1.5", "https://npm.nose.space/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "lucide-static": ["lucide-static@0.555.0", "https://npm.nose.space/lucide-static/-/lucide-static-0.555.0.tgz", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="],
+
+ "typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+ }
+}
diff --git a/apps/todo/index.tsx b/apps/todo/index.tsx
new file mode 100644
index 0000000..9b61e99
--- /dev/null
+++ b/apps/todo/index.tsx
@@ -0,0 +1 @@
+export { default } from './src/server'
diff --git a/apps/todo/package.json b/apps/todo/package.json
new file mode 100644
index 0000000..09b375c
--- /dev/null
+++ b/apps/todo/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "todo",
+ "module": "index.tsx",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "toes": "bun run --watch index.tsx",
+ "start": "bun toes",
+ "dev": "bun run --hot index.tsx"
+ },
+ "toes": {
+ "tool": true,
+ "icon": "🖥️"
+ },
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5.9.2"
+ },
+ "dependencies": {
+ "@because/hype": "*",
+ "@because/forge": "*",
+ "@because/howl": "*"
+ }
+}
diff --git a/apps/todo/pub/img/bite1.png b/apps/todo/pub/img/bite1.png
new file mode 100644
index 0000000..f452c1e
Binary files /dev/null and b/apps/todo/pub/img/bite1.png differ
diff --git a/apps/todo/pub/img/bite2.png b/apps/todo/pub/img/bite2.png
new file mode 100644
index 0000000..c075d53
Binary files /dev/null and b/apps/todo/pub/img/bite2.png differ
diff --git a/apps/todo/pub/img/burger.png b/apps/todo/pub/img/burger.png
new file mode 100644
index 0000000..bc5e222
Binary files /dev/null and b/apps/todo/pub/img/burger.png differ
diff --git a/apps/todo/src/client/App.tsx b/apps/todo/src/client/App.tsx
new file mode 100644
index 0000000..4cb5ef6
--- /dev/null
+++ b/apps/todo/src/client/App.tsx
@@ -0,0 +1,36 @@
+import { render, useState } from 'hono/jsx/dom'
+import { define } from '@because/forge'
+
+const Wrapper = define({
+ margin: '0 auto',
+ marginTop: 50,
+ width: '50vw',
+ border: '1px solid black',
+ padding: 24,
+ textAlign: 'center'
+
+})
+
+export default function App() {
+ const [count, setCount] = useState(0)
+
+ try {
+ return (
+
+ It works!
+ Count: {count}
+
+
+
+
+
+
+ )
+ } catch (error) {
+ console.error('Render error:', error)
+ return <>
Error
{error instanceof Error ? error : new Error(String(error))}>
+ }
+}
+
+const root = document.getElementById('root')!
+render(, root)
diff --git a/apps/todo/src/css/main.css b/apps/todo/src/css/main.css
new file mode 100644
index 0000000..62bcd77
--- /dev/null
+++ b/apps/todo/src/css/main.css
@@ -0,0 +1,40 @@
+section {
+ max-width: 500px;
+ margin: 0 auto;
+ text-align: center;
+ font-size: 200%;
+}
+
+h1 {
+ margin-top: 0;
+}
+
+.hype {
+ display: inline-block;
+ padding: 0.3rem 0.8rem;
+ background: linear-gradient(45deg,
+ #ff00ff 0%,
+ #00ffff 33%,
+ #ffff00 66%,
+ #ff00ff 100%);
+ background-size: 400% 400%;
+ animation: gradientShift 15s ease infinite;
+ color: black;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-weight: 700;
+ border-radius: 4px;
+}
+
+@keyframes gradientShift {
+ 0% {
+ background-position: 0% 50%;
+ }
+
+ 100% {
+ background-position: 100% 50%;
+ }
+}
+
+ul {
+ list-style-type: none;
+}
\ No newline at end of file
diff --git a/apps/todo/src/pages/index.tsx b/apps/todo/src/pages/index.tsx
new file mode 100644
index 0000000..126eeb5
--- /dev/null
+++ b/apps/todo/src/pages/index.tsx
@@ -0,0 +1,31 @@
+import { $ } from 'bun'
+
+const GIT_HASH = process.env.RENDER_GIT_COMMIT?.slice(0, 7)
+ || await $`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => 'unknown')
+
+export default () => <>
+
+
+ hype
+
+
+
+
+
+
+
+
+
+
+
+>
diff --git a/apps/todo/src/server/index.tsx b/apps/todo/src/server/index.tsx
new file mode 100644
index 0000000..99049e2
--- /dev/null
+++ b/apps/todo/src/server/index.tsx
@@ -0,0 +1,8 @@
+import { Hype } from '@because/hype'
+
+const app = new Hype({ layout: false })
+
+// custom routes go here
+// app.get("/my-custom-routes", (c) => c.text("wild, wild stuff"))
+
+export default app.defaults
diff --git a/apps/todo/src/shared/types.ts b/apps/todo/src/shared/types.ts
new file mode 100644
index 0000000..e69de29
diff --git a/apps/todo/tsconfig.json b/apps/todo/tsconfig.json
new file mode 100644
index 0000000..b16b513
--- /dev/null
+++ b/apps/todo/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
+ "allowJs": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+ "baseUrl": ".",
+ "paths": {
+ "$*": ["src/server/*"],
+ "#*": ["src/client/*"],
+ "@*": ["src/shared/*"]
+ }
+ }
+}
diff --git a/src/client/components/AppDetail.tsx b/src/client/components/AppDetail.tsx
index 5c51142..270c03f 100644
--- a/src/client/components/AppDetail.tsx
+++ b/src/client/components/AppDetail.tsx
@@ -2,7 +2,7 @@ import { define } from '@because/forge'
import type { App } from '../../shared/types'
import { restartApp, startApp, stopApp } from '../api'
import { openDeleteAppModal, openRenameAppModal } from '../modals'
-import { selectedTab } from '../state'
+import { apps, selectedTab } from '../state'
import {
ActionBar,
Button,
@@ -44,6 +44,9 @@ const OpenEmojiPicker = define('OpenEmojiPicker', {
})
export function AppDetail({ app, render }: { app: App, render: () => void }) {
+ // Find all tools
+ const tools = apps.filter(a => a.tool)
+
return (
@@ -57,7 +60,7 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) {
-
+
@@ -142,9 +145,29 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) {
-
- hardy har har
-
+ {tools.map(tool => {
+ const toolName = typeof tool.tool === 'string' ? tool.tool : tool.name
+ const isSelected = selectedTab === tool.name
+ return (
+
+
+ {toolName}
+ {tool.state !== 'running' && (
+
+ Tool is {stateLabels[tool.state].toLowerCase()}
+
+ )}
+ {/* Target for iframe overlay positioning */}
+ {tool.state === 'running' && (
+
+ )}
+
+
+ )
+ })}
)
diff --git a/src/client/components/Nav.tsx b/src/client/components/Nav.tsx
index 1305164..fc61933 100644
--- a/src/client/components/Nav.tsx
+++ b/src/client/components/Nav.tsx
@@ -1,16 +1,33 @@
-import { selectedTab, setSelectedTab } from '../state'
+import type { App } from '../../shared/types'
+import { apps, selectedTab, setSelectedTab } from '../state'
import { Tab, TabBar } from '../styles'
-export function Nav({ render }: { render: () => void }) {
- const handleTabClick = (tab: 'overview' | 'todo') => {
+export function Nav({ app, render }: { app: App; render: () => void }) {
+ const handleTabClick = (tab: string) => {
setSelectedTab(tab)
render()
}
+ // Find all tools
+ const tools = apps.filter(a => a.tool)
+
return (
- handleTabClick('overview')}>Overview
- handleTabClick('todo')}>TODO
+ handleTabClick('overview')}>
+ Overview
+
+ {tools.map(tool => {
+ const toolName = typeof tool.tool === 'string' ? tool.tool : tool.name
+ return (
+ handleTabClick(tool.name)}
+ >
+ {toolName}
+
+ )
+ })}
)
}
diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx
index d2bf315..d9dd3f4 100644
--- a/src/client/components/Sidebar.tsx
+++ b/src/client/components/Sidebar.tsx
@@ -1,5 +1,13 @@
import { openNewAppModal } from '../modals'
-import { apps, selectedApp, setSelectedApp, setSidebarCollapsed, sidebarCollapsed } from '../state'
+import {
+ apps,
+ selectedApp,
+ setSelectedApp,
+ setSidebarCollapsed,
+ setSidebarSection,
+ sidebarCollapsed,
+ sidebarSection,
+} from '../state'
import {
AppItem,
AppList,
@@ -7,7 +15,8 @@ import {
HamburgerLine,
Logo,
NewAppButton,
- SectionLabel,
+ SectionSwitcher,
+ SectionTab,
Sidebar as SidebarContainer,
SidebarFooter,
StatusDot,
@@ -24,6 +33,15 @@ export function Sidebar({ render }: { render: () => void }) {
render()
}
+ const switchSection = (section: 'apps' | 'tools') => {
+ setSidebarSection(section)
+ render()
+ }
+
+ const regularApps = apps.filter(app => !app.tool)
+ const toolApps = apps.filter(app => app.tool)
+ const activeApps = sidebarSection === 'apps' ? regularApps : toolApps
+
return (
@@ -34,9 +52,18 @@ export function Sidebar({ render }: { render: () => void }) {
- {!sidebarCollapsed && Apps}
+ {!sidebarCollapsed && toolApps.length > 0 && (
+
+ switchSection('apps')}>
+ Apps
+
+ switchSection('tools')}>
+ Tools
+
+
+ )}
- {apps.map(app => (
+ {activeApps.map(app => (
selectApp(app.name)}
diff --git a/src/client/index.tsx b/src/client/index.tsx
index bc26d7c..1b2ed60 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -1,16 +1,23 @@
import { render as renderApp } from 'hono/jsx/dom'
import { Dashboard } from './components'
-import { apps, selectedApp, setApps, setSelectedApp } from './state'
+import { apps, selectedApp, selectedTab, setApps, setSelectedApp } from './state'
import { initModal } from './components/modal'
+import { initToolIframes, updateToolIframes } from './tool-iframes'
import { initUpdate } from './update'
const render = () => {
renderApp(, document.getElementById('app')!)
+ // Update tool iframes after DOM settles
+ requestAnimationFrame(() => {
+ const tools = apps.filter(a => a.tool)
+ updateToolIframes(selectedTab, tools)
+ })
}
// Initialize render functions
initModal(render)
initUpdate(render)
+initToolIframes()
// Set theme based on system preference
const setTheme = () => {
diff --git a/src/client/state.ts b/src/client/state.ts
index df01bda..ddfeb36 100644
--- a/src/client/state.ts
+++ b/src/client/state.ts
@@ -3,12 +3,13 @@ import type { App } from '../shared/types'
// UI state (survives re-renders)
export let selectedApp: string | null = localStorage.getItem('selectedApp')
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
+export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps'
// Server state (from SSE)
export let apps: App[] = []
// Tab state
-export let selectedTab: 'overview' | 'todo' = 'overview'
+export let selectedTab: string = 'overview'
// State setters
export function setSelectedApp(name: string | null) {
@@ -25,10 +26,15 @@ export function setSidebarCollapsed(collapsed: boolean) {
localStorage.setItem('sidebarCollapsed', String(collapsed))
}
+export function setSidebarSection(section: 'apps' | 'tools') {
+ sidebarSection = section
+ localStorage.setItem('sidebarSection', section)
+}
+
export function setApps(newApps: App[]) {
apps = newApps
}
-export function setSelectedTab(tab: 'overview' | 'todo') {
+export function setSelectedTab(tab: string) {
selectedTab = tab
}
diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts
index 91b1721..4bb45eb 100644
--- a/src/client/styles/index.ts
+++ b/src/client/styles/index.ts
@@ -14,6 +14,8 @@ export {
MainHeader,
MainTitle,
SectionLabel,
+ SectionSwitcher,
+ SectionTab,
Sidebar,
SidebarFooter,
} from './layout'
diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts
index a87d1c2..0251454 100644
--- a/src/client/styles/layout.ts
+++ b/src/client/styles/layout.ts
@@ -60,6 +60,40 @@ export const SectionLabel = define('SectionLabel', {
letterSpacing: '0.05em',
})
+export const SectionSwitcher = define('SectionSwitcher', {
+ display: 'flex',
+ gap: 0,
+ padding: '12px 16px',
+ borderBottom: `1px solid ${theme('colors-border')}`,
+})
+
+export const SectionTab = define('SectionTab', {
+ base: 'button',
+ flex: 1,
+ padding: '6px 12px',
+ background: 'none',
+ border: 'none',
+ cursor: 'pointer',
+ fontSize: 12,
+ fontWeight: 600,
+ color: theme('colors-textMuted'),
+ textTransform: 'uppercase',
+ letterSpacing: '0.05em',
+ borderRadius: theme('radius-sm'),
+ selectors: {
+ '&:hover': {
+ background: theme('colors-bgHover'),
+ color: theme('colors-text'),
+ },
+ },
+ variants: {
+ active: {
+ background: theme('colors-bgSelected'),
+ color: theme('colors-text'),
+ },
+ },
+})
+
export const AppList = define('AppList', {
flex: 1,
overflow: 'auto',
diff --git a/src/client/tool-iframes.ts b/src/client/tool-iframes.ts
new file mode 100644
index 0000000..6263b0d
--- /dev/null
+++ b/src/client/tool-iframes.ts
@@ -0,0 +1,130 @@
+import { theme } from './themes'
+
+// Iframe cache - these never get recreated once loaded
+// Use a global to survive hot reloads
+const iframes: Map =
+ (window as any).__toolIframes ??= new Map()
+
+// Track current state to avoid unnecessary DOM updates
+// Also stored on window to survive hot reloads
+let currentTool: string | null = (window as any).__currentTool ?? null
+
+// Get the stable container (outside Hono-managed DOM)
+const getContainer = () => document.getElementById('tool-iframes')
+
+// Initialize the container styles
+export function initToolIframes() {
+ const container = getContainer()
+ if (!container) return
+
+ // Restore iframe cache from DOM if module was hot-reloaded
+ if (iframes.size === 0) {
+ const existingIframes = container.querySelectorAll('iframe')
+ existingIframes.forEach(iframe => {
+ const match = iframe.src.match(/localhost:(\d+)/)
+ if (match && match[1]) {
+ const port = parseInt(match[1], 10)
+ const name = iframe.dataset.toolName
+ if (name) {
+ iframes.set(name, { iframe, port })
+ }
+ }
+ })
+ }
+
+ container.style.cssText = `
+ display: none;
+ position: fixed;
+ background: ${theme('colors-bg')};
+ `
+}
+
+// Update which iframe is visible based on selected tab and tool state
+export function updateToolIframes(
+ selectedTab: string,
+ tools: Array<{ name: string; port?: number; state: string }>
+) {
+ const container = getContainer()
+ if (!container) return
+
+ // Find the selected tool
+ const selectedTool = tools.find(t => t.name === selectedTab)
+ const showIframe = selectedTool?.state === 'running' && selectedTool?.port
+
+ if (!showIframe) {
+ // Only update if state changed
+ if (currentTool !== null) {
+ container.style.display = 'none'
+ currentTool = null
+ ;(window as any).__currentTool = null
+ }
+ return
+ }
+
+ const tool = selectedTool!
+
+ // Skip if nothing changed
+ if (currentTool === tool.name) {
+ // Just update position in case of scroll/resize
+ const tabContent = document.querySelector('[data-tool-target]')
+ if (tabContent) {
+ const rect = tabContent.getBoundingClientRect()
+ container.style.top = `${rect.top}px`
+ container.style.left = `${rect.left}px`
+ container.style.width = `${rect.width}px`
+ container.style.height = `${rect.height}px`
+ }
+ return
+ }
+
+ // Position container over the tab content area
+ const tabContent = document.querySelector('[data-tool-target]')
+ if (!tabContent) {
+ container.style.display = 'none'
+ currentTool = null
+ return
+ }
+
+ const rect = tabContent.getBoundingClientRect()
+ container.style.cssText = `
+ display: block;
+ position: fixed;
+ top: ${rect.top}px;
+ left: ${rect.left}px;
+ width: ${rect.width}px;
+ height: ${rect.height}px;
+ background: ${theme('colors-bg')};
+ z-index: 100;
+ `
+
+ // Get or create the iframe for this tool
+ let cached = iframes.get(tool.name)
+
+ if (!cached || cached.port !== tool.port) {
+ // Create new iframe (first time or port changed)
+ const iframe = document.createElement('iframe')
+ iframe.src = `http://localhost:${tool.port}`
+ iframe.dataset.toolName = tool.name // For hot reload recovery
+ iframe.style.cssText = `
+ width: 100%;
+ height: 100%;
+ border: none;
+ `
+ cached = { iframe, port: tool.port! }
+ iframes.set(tool.name, cached)
+ // Add to container
+ container.appendChild(iframe)
+ }
+
+ // Show only the selected iframe, hide others
+ for (const [name, { iframe }] of iframes) {
+ const shouldShow = name === tool.name
+ if (shouldShow && iframe.parentElement !== container) {
+ container.appendChild(iframe)
+ }
+ iframe.style.display = shouldShow ? 'block' : 'none'
+ }
+
+ currentTool = tool.name
+ ;(window as any).__currentTool = tool.name
+}
diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts
index f3789d0..bedaa1f 100644
--- a/src/server/api/apps.ts
+++ b/src/server/api/apps.ts
@@ -19,14 +19,10 @@ function convert(app: BackendApp): SharedApp {
// SSE endpoint for real-time app state updates
router.sse('/stream', (send) => {
const broadcast = () => {
- const apps: SharedApp[] = allApps().map(({ name, state, icon, error, port, started, logs }) => ({
- name,
- state,
- icon,
- error,
- port,
- started,
- logs,
+ const apps: SharedApp[] = allApps().map(({
+ name, state, icon, error, port, started, logs, tool
+ }) => ({
+ name, state, icon, error, port, started, logs, tool,
}))
send(apps)
}
diff --git a/src/server/apps.ts b/src/server/apps.ts
index 15eba09..3f77a20 100644
--- a/src/server/apps.ts
+++ b/src/server/apps.ts
@@ -243,7 +243,8 @@ function discoverApps() {
const { pkg, error } = loadApp(dir)
const state: AppState = error ? 'invalid' : 'stopped'
const icon = pkg.toes?.icon ?? DEFAULT_EMOJI
- _apps.set(dir, { name: dir, state, icon, error })
+ const tool = pkg.toes?.tool
+ _apps.set(dir, { name: dir, state, icon, error, tool })
}
}
@@ -613,7 +614,8 @@ function watchAppsDir() {
const { pkg, error } = loadApp(dir)
const state: AppState = error ? 'invalid' : 'stopped'
const icon = pkg.toes?.icon
- _apps.set(dir, { name: dir, state, icon, error })
+ const tool = pkg.toes?.tool
+ _apps.set(dir, { name: dir, state, icon, error, tool })
update()
if (!error) {
runApp(dir, getPort(dir))
@@ -637,8 +639,9 @@ function watchAppsDir() {
const { pkg, error } = loadApp(dir)
- // Update icon and error from package.json
+ // Update icon, tool, and error from package.json
app.icon = pkg.toes?.icon
+ app.tool = pkg.toes?.tool
app.error = error
// App became valid - start it if stopped
diff --git a/src/server/shell.tsx b/src/server/shell.tsx
index bf9bf70..90a77bf 100644
--- a/src/server/shell.tsx
+++ b/src/server/shell.tsx
@@ -8,6 +8,7 @@ export const Shell = () => (
+