/tool redirects

This commit is contained in:
Chris Wanstrath 2026-01-30 20:38:46 -08:00
parent d1b7e973d3
commit a25088e723
7 changed files with 31 additions and 80 deletions

View File

@ -216,9 +216,12 @@ const fileMemoryScript = `
var version = params.get('version') || 'current';
if (!app) return;
var key = 'code-app:' + app + ':' + version + ':file';
if (file) {
localStorage.setItem(key, file);
if (params.has('file')) {
// Explicit file param (even empty) - save it
if (file) localStorage.setItem(key, file);
else localStorage.removeItem(key);
} else {
// No file param - restore saved location
var saved = localStorage.getItem(key);
if (saved) {
var url = '/?app=' + encodeURIComponent(app);
@ -318,7 +321,7 @@ function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) {
return (
<Breadcrumb>
{crumbs.length > 0 ? (
<BreadcrumbLink href={`/?app=${appName}${versionParam}`}>{appName}</BreadcrumbLink>
<BreadcrumbLink href={`/?app=${appName}${versionParam}&file=`}>{appName}</BreadcrumbLink>
) : (
<BreadcrumbCurrent>{appName}</BreadcrumbCurrent>
)}

View File

@ -6,6 +6,7 @@ import { join } from 'path'
import type { Child } from 'hono/jsx'
const APPS_DIR = process.env.APPS_DIR!
const TOES_URL = process.env.TOES_URL!
const app = new Hype({ prettyHTML: false })
@ -181,8 +182,7 @@ app.get('/', async c => {
{versions.map(v => (
<VersionItem>
<VersionLink
href="#"
onclick={`window.parent.postMessage({type:'navigate-tool',tool:'code',params:{app:'${appName}',version:'${v.name}'}}, '*'); return false;`}
href={`${TOES_URL}/tool/code?app=${appName}&version=${v.name}`}
>
{formatTimestamp(v.name)}
</VersionLink>

View File

@ -45,19 +45,17 @@ const appPath = join(APPS_DIR, appName, 'current')
Not `APPS_DIR/appName` directly.
## talking to the parent
## linking to tools
**Navigate to another tool:**
Use `/tool/:name` URLs to link directly to tools with params:
```js
window.parent.postMessage({
type: 'navigate-tool',
tool: 'code',
params: { app: 'my-app', version: '20260130-000000' }
}, '*')
```html
<a href="/tool/code?app=my-app&version=20260130-000000">
View in Code
</a>
```
**Resize your iframe:**
## resize iframe
```js
window.parent.postMessage({

View File

@ -1,8 +1,8 @@
import { render as renderApp } from 'hono/jsx/dom'
import { Dashboard } from './components'
import { apps, selectedApp, selectedTab, setApps, setSelectedApp, setSelectedTab } from './state'
import { apps, selectedApp, selectedTab, setApps, setSelectedApp } from './state'
import { initModal } from './components/modal'
import { initToolIframes, updateToolIframes, setNavigateToolHandler, navigateToTool } from './tool-iframes'
import { initToolIframes, updateToolIframes } from './tool-iframes'
import { initUpdate } from './update'
const render = () => {
@ -19,15 +19,6 @@ initModal(render)
initUpdate(render)
initToolIframes()
// Handle tool-to-tool navigation via postMessage
setNavigateToolHandler((tool, params) => {
const tools = apps.filter(a => a.tool)
navigateToTool(tool, params, tools, (tab) => {
setSelectedTab(tab)
render()
})
})
// Set theme based on system preference
const setTheme = () => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@ -12,13 +12,6 @@ let currentTool: string | null = (window as any).__currentTool ?? null
// Get the stable container (outside Hono-managed DOM)
const getContainer = () => document.getElementById('tool-iframes')
// Callback for tool navigation requests
let onNavigateTool: ((tool: string, params: Record<string, string>) => void) | null = null
export function setNavigateToolHandler(handler: (tool: string, params: Record<string, string>) => void) {
onNavigateTool = handler
}
// Listen for messages from iframes
function setupMessageListener() {
if ((window as any).__toolIframeMessageListener) return
@ -40,13 +33,6 @@ function setupMessageListener() {
}
}
}
if (type === 'navigate-tool') {
const { tool, params } = event.data
if (tool && onNavigateTool) {
onNavigateTool(tool, params || {})
}
}
})
}
@ -99,45 +85,6 @@ function buildCacheKey(toolName: string, params: Record<string, string>): string
return parts.join(':')
}
// Navigate to a tool with specific params (called from postMessage handler)
export function navigateToTool(
toolName: string,
params: Record<string, string>,
tools: Array<{ name: string; port?: number; state: string }>,
onSelectTab: (tab: string) => void
) {
const tool = tools.find(t => t.name === toolName)
if (!tool || tool.state !== 'running' || !tool.port) return
const container = getContainer()
if (!container) return
const cacheKey = buildCacheKey(toolName, params)
// Create iframe if needed
let cached = iframes.get(cacheKey)
if (!cached || cached.port !== tool.port) {
const iframe = document.createElement('iframe')
iframe.src = buildToolUrl(tool.port, params)
iframe.dataset.toolName = toolName
iframe.dataset.appName = params.app ?? ''
iframe.style.cssText = `width: 100%; border: none;`
cached = { iframe, port: tool.port }
iframes.set(cacheKey, cached)
container.appendChild(iframe)
}
// Switch to that tab
onSelectTab(toolName)
// Force show this specific iframe
currentTool = cacheKey
;(window as any).__currentTool = cacheKey
for (const [key, { iframe }] of iframes) {
iframe.style.display = key === cacheKey ? 'block' : 'none'
}
}
// Update which iframe is visible based on selected tab and tool state
export function updateToolIframes(
selectedTab: string,

View File

@ -433,7 +433,7 @@ async function runApp(dir: string, port: number) {
const proc = Bun.spawn(['bun', 'run', 'toes'], {
cwd,
env: { ...process.env, PORT: String(port), NO_AUTOPORT: 'true', APPS_DIR },
env: { ...process.env, PORT: String(port), NO_AUTOPORT: 'true', APPS_DIR, TOES_URL: `http://localhost:${process.env.PORT || 3000}` },
stdout: 'pipe',
stderr: 'pipe',
})

View File

@ -1,4 +1,4 @@
import { initApps } from '$apps'
import { allApps, initApps } from '$apps'
import appsRouter from './api/apps'
import syncRouter from './api/sync'
import { Hype } from '@because/hype'
@ -8,6 +8,18 @@ const app = new Hype({ layout: false })
app.route('/api/apps', appsRouter)
app.route('/api/sync', syncRouter)
// Tool URLs: /tool/code?app=todo&file=README.md -> redirect to tool port
app.get('/tool/:tool', c => {
const toolName = c.req.param('tool')
const tool = allApps().find(a => a.tool && a.name === toolName)
if (!tool || tool.state !== 'running' || !tool.port) {
return c.text(`Tool "${toolName}" not found or not running`, 404)
}
const params = new URLSearchParams(c.req.query()).toString()
const url = params ? `http://localhost:${tool.port}?${params}` : `http://localhost:${tool.port}`
return c.redirect(url)
})
console.log('🐾 Toes!')
initApps()