mirror of
https://github.com/hanzo-apps/tabs.git
synced 2026-09-15 19:32:29 +00:00
The workspace has one home and more than one host
tabs.hanzo.ai is one host for it. The desktop shell is another, and hanzo.ai a third, and the alternative to exporting it is three pane trees that look alike — a second set of answers to what a split does, which then drift. Almost nothing had to change, because the component was already pure: it takes `hosts` and `mint` as props, holds no token and calls no server. So this is packaging. Its own `@/lib` imports become relative, since a consumer has no such alias; the control plane's origin becomes a parameter defaulting to where the web app already pointed, so a host aims it at whichever cloud its identity belongs to without module state a second host must remember to set; and `index.ts` names what a host mounts. Source is published rather than a build. Both hosts compile TypeScript, so a build step here would be a second toolchain making something neither needs and one more thing to be stale. React, @hanzo/ui, @hanzo/gui and lucide are peers for the same reason a shell must not get a second copy of a context in one tree. Two things were already broken on this branch and are fixed with it. @hanzo/ui hard-imports react-hook-form, which it declares an OPTIONAL peer — a bundler resolves it statically either way, so the module has to be there and `next build` failed on every path without it. And Next 16 builds with Turbopack by default, which dies here with a worker error while webpack compiles in 9.6s; Next's own message asks callers to name the bundler, so the script names the one that works. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -73,12 +73,12 @@ import {
|
||||
splitPane,
|
||||
stableOrder,
|
||||
stackFor,
|
||||
} from '@/lib/tiles';
|
||||
} from '../lib/tiles';
|
||||
// One import, and it is a PURE READING of an error — no origin, no token, no
|
||||
// request. The workspace still holds no credential; what it gains is the
|
||||
// platform's own word for "no money", which is the one failure whose remedy
|
||||
// differs from every other.
|
||||
import { unfunded } from '@/lib/api';
|
||||
import { unfunded } from '../lib/api';
|
||||
import {
|
||||
DEADLINE,
|
||||
DOT,
|
||||
@@ -91,7 +91,7 @@ import {
|
||||
rescued,
|
||||
web,
|
||||
restore,
|
||||
} from '@/lib/panes';
|
||||
} from '../lib/panes';
|
||||
|
||||
/** The stack's own props, taken from the stack. @hanzo/ui is the one import
|
||||
* source, so a local alias beats reaching past it to @hanzo/gui for a type. */
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* The workspace, for whoever is hosting it.
|
||||
*
|
||||
* tabs.hanzo.ai is one host. The desktop shell is another, and hanzo.ai a
|
||||
* third — the same tiling of terminals, desktops and pages in each, because it
|
||||
* is the same component rather than three that look alike. That is the whole
|
||||
* reason this file exists: a second copy of a pane tree is a second set of
|
||||
* answers to what a split does, and they drift.
|
||||
*
|
||||
* WHAT A HOST SUPPLIES is the machines and a way to mint a pane's url. The
|
||||
* `Workspace` component holds no token and calls no server: `hosts` and `mint`
|
||||
* come in as props, so a host points them at whichever control plane its
|
||||
* identity belongs to. The functions in `lib/api` are the ones tabs.hanzo.ai
|
||||
* uses for that, offered here because another host most likely wants the same
|
||||
* ones — each taking its origin as a parameter.
|
||||
*
|
||||
* SOURCE IS PUBLISHED, not a build. Both hosts compile TypeScript already, so a
|
||||
* build step here would be a second toolchain producing something neither
|
||||
* needs, and one more thing to be stale.
|
||||
*/
|
||||
export { Workspace } from './components/workspace';
|
||||
|
||||
export {
|
||||
DEFAULT_SHELL,
|
||||
DEADLINE,
|
||||
DOT,
|
||||
READY,
|
||||
type Binding,
|
||||
type Shell,
|
||||
host,
|
||||
isReady,
|
||||
label,
|
||||
machineOf,
|
||||
mintName,
|
||||
proves,
|
||||
rescued,
|
||||
restore,
|
||||
safeName,
|
||||
shellUrl,
|
||||
web,
|
||||
} from './lib/panes';
|
||||
|
||||
export {
|
||||
type Dir,
|
||||
type Geometry,
|
||||
type Path,
|
||||
type Tile,
|
||||
closePane,
|
||||
geometry,
|
||||
pageGeometry,
|
||||
pane,
|
||||
paneIds,
|
||||
setRatio,
|
||||
splitPane,
|
||||
} from './lib/tiles';
|
||||
|
||||
export {
|
||||
API,
|
||||
type Class,
|
||||
type Door,
|
||||
type Machine,
|
||||
type SandboxMachine,
|
||||
type Session,
|
||||
Refusal,
|
||||
createSandbox,
|
||||
frameUrl,
|
||||
grant,
|
||||
machineName,
|
||||
machines,
|
||||
sandboxes,
|
||||
sessions,
|
||||
unfunded,
|
||||
} from './lib/api';
|
||||
+36
-13
@@ -7,7 +7,23 @@
|
||||
* party to a conversation that has two, and one more thing to keep running.
|
||||
*/
|
||||
|
||||
export const API = process.env.NEXT_PUBLIC_HANZO_API ?? 'https://api.hanzo.ai';
|
||||
/**
|
||||
* Where the control plane answers.
|
||||
*
|
||||
* A DEFAULT, not a constant, because this module is read by two hosts now: the
|
||||
* web app at tabs.hanzo.ai, and the desktop shell, which points at whichever
|
||||
* cloud its identity belongs to. Every function here already takes a token, so
|
||||
* it takes the origin the same way — a parameter with this as its default,
|
||||
* rather than module state a second host would have to remember to set before
|
||||
* the first call.
|
||||
*
|
||||
* `process.env` is read defensively: a bundler that does not define it leaves
|
||||
* the identifier undefined rather than an empty object, and reading a property
|
||||
* off that throws at module load.
|
||||
*/
|
||||
export const API =
|
||||
(typeof process !== 'undefined' ? process.env?.NEXT_PUBLIC_HANZO_API : undefined) ??
|
||||
'https://api.hanzo.ai';
|
||||
|
||||
export interface Machine {
|
||||
id: string;
|
||||
@@ -68,8 +84,8 @@ async function refusal(res: Response, fallback: string): Promise<Refusal> {
|
||||
: new Refusal(res.status, '', e || fallback);
|
||||
}
|
||||
|
||||
async function read<T>(path: string, token: string): Promise<T> {
|
||||
const res = await fetch(`${API}${path}`, {
|
||||
async function read<T>(path: string, token: string, origin = API): Promise<T> {
|
||||
const res = await fetch(`${origin}${path}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
cache: 'no-store',
|
||||
});
|
||||
@@ -77,11 +93,13 @@ async function read<T>(path: string, token: string): Promise<T> {
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export const machines = (token: string) =>
|
||||
read<{ targets: Machine[] }>('/v1/agents/targets', token).then((r) => r.targets ?? []);
|
||||
export const machines = (token: string, origin = API) =>
|
||||
read<{ targets: Machine[] }>('/v1/agents/targets', token, origin).then((r) => r.targets ?? []);
|
||||
|
||||
export const sessions = (token: string) =>
|
||||
read<{ sessions: Session[] }>('/v1/agents/sessions?limit=200', token).then((r) => r.sessions ?? []);
|
||||
export const sessions = (token: string, origin = API) =>
|
||||
read<{ sessions: Session[] }>('/v1/agents/sessions?limit=200', token, origin).then(
|
||||
(r) => r.sessions ?? [],
|
||||
);
|
||||
|
||||
/**
|
||||
* Sandboxes — the machines we start for you, rather than the ones you linked.
|
||||
@@ -109,8 +127,8 @@ export interface SandboxMachine {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const sandboxes = (token: string) =>
|
||||
read<{ sandboxes?: SandboxMachine[] }>('/v1/sandboxes?status=running', token).then((r) =>
|
||||
export const sandboxes = (token: string, origin = API) =>
|
||||
read<{ sandboxes?: SandboxMachine[] }>('/v1/sandboxes?status=running', token, origin).then((r) =>
|
||||
(r.sandboxes ?? []).filter((s) => s.status === 'running'),
|
||||
);
|
||||
|
||||
@@ -147,7 +165,11 @@ export type Class = 'dev' | 'desktop';
|
||||
* should live, and a `ttlSec` here would be a second answer to drift from the one
|
||||
* in the class table.
|
||||
*/
|
||||
export const createSandbox = async (token: string, kind: Class = 'dev'): Promise<SandboxMachine> => {
|
||||
export const createSandbox = async (
|
||||
token: string,
|
||||
kind: Class = 'dev',
|
||||
origin = API,
|
||||
): Promise<SandboxMachine> => {
|
||||
const taken = new Set((await sandboxes(token)).map(machineName));
|
||||
// Every machine carries its number, the first one included: `cloud` beside
|
||||
// `cloud-2` reads as a different kind of thing rather than the one before it.
|
||||
@@ -155,7 +177,7 @@ export const createSandbox = async (token: string, kind: Class = 'dev'): Promise
|
||||
const stem = STEM[kind];
|
||||
let project = `${stem}-1`;
|
||||
for (let n = 2; taken.has(project); n++) project = `${stem}-${n}`;
|
||||
const res = await fetch(`${API}/v1/sandboxes`, {
|
||||
const res = await fetch(`${origin}/v1/sandboxes`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ class: kind, project }),
|
||||
@@ -229,8 +251,9 @@ export const frameUrl = async (
|
||||
id: string,
|
||||
door: Door,
|
||||
arg?: string,
|
||||
origin = API,
|
||||
): Promise<string> => {
|
||||
const res = await fetch(`${API}/v1/sandboxes/${encodeURIComponent(id)}/${door}/ticket`, {
|
||||
const res = await fetch(`${origin}/v1/sandboxes/${encodeURIComponent(id)}/${door}/ticket`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
cache: 'no-store',
|
||||
@@ -238,5 +261,5 @@ export const frameUrl = async (
|
||||
if (!res.ok) throw new Error(`${door} ticket → ${res.status}`);
|
||||
const body = (await res.json()) as { url?: string };
|
||||
if (!body.url) throw new Error(`${door} ticket → empty answer`);
|
||||
return `${API}${body.url}` + (arg ? `&arg=${encodeURIComponent(arg)}` : '');
|
||||
return `${origin}${body.url}` + (arg ? `&arg=${encodeURIComponent(arg)}` : '');
|
||||
};
|
||||
|
||||
+33
-10
@@ -1,35 +1,58 @@
|
||||
{
|
||||
"name": "@hanzo/tabs",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"description": "Keep tabs on your agents. A browser terminal workspace for machines you have linked.",
|
||||
"description": "Keep tabs on your agents. A browser terminal workspace for machines you have linked \u2014 the app at tabs.hanzo.ai, and the component its other hosts mount.",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"test": "jest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:surface": "next build && playwright test"
|
||||
"test:surface": "next build --webpack && playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/gui": "catalog:",
|
||||
"@hanzo/iam": "catalog:",
|
||||
"@hanzo/ui": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-native": "catalog:",
|
||||
"react-native-web": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hanzo/gui": "catalog:",
|
||||
"@hanzo/ui": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@types/jest": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"jest": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"packageManager": "[email protected]"
|
||||
"packageManager": "[email protected]",
|
||||
"types": "./index.ts",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./workspace": "./components/workspace.tsx",
|
||||
"./panes": "./lib/panes.ts",
|
||||
"./tiles": "./lib/tiles.ts",
|
||||
"./api": "./lib/api.ts"
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"components",
|
||||
"lib",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"peerDependencies": {
|
||||
"@hanzo/gui": "catalog:",
|
||||
"@hanzo/ui": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+32
-15
@@ -42,6 +42,9 @@ catalogs:
|
||||
react-dom:
|
||||
specifier: 19.2.8
|
||||
version: 19.2.8
|
||||
react-hook-form:
|
||||
specifier: ^7.71.0
|
||||
version: 7.88.0
|
||||
react-native:
|
||||
specifier: ^0.87.1
|
||||
version: 0.87.1
|
||||
@@ -60,27 +63,15 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@hanzo/gui':
|
||||
specifier: 'catalog:'
|
||||
version: 8.3.1([email protected]([email protected]))([email protected](@babel/[email protected]([email protected]))(@react-native/[email protected](@babel/[email protected]([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected]))([email protected])
|
||||
'@hanzo/iam':
|
||||
specifier: 'catalog:'
|
||||
version: 0.21.17([email protected]([email protected]))([email protected])
|
||||
'@hanzo/ui':
|
||||
specifier: 'catalog:'
|
||||
version: 8.27.2(bebf53e2256154e0b6f66411048aeaaf)
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 1.44.0([email protected])
|
||||
next:
|
||||
specifier: 'catalog:'
|
||||
version: 16.3.3(@babel/[email protected]([email protected]))(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
react-hook-form:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.8
|
||||
react-dom:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.8([email protected])
|
||||
version: 7.88.0([email protected])
|
||||
react-native:
|
||||
specifier: 'catalog:'
|
||||
version: 0.87.1(@babel/[email protected]([email protected]))(@react-native/[email protected](@babel/[email protected]([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])
|
||||
@@ -88,6 +79,12 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 0.21.2([email protected]([email protected]))([email protected])
|
||||
devDependencies:
|
||||
'@hanzo/gui':
|
||||
specifier: 'catalog:'
|
||||
version: 8.3.1([email protected]([email protected]))([email protected](@babel/[email protected]([email protected]))(@react-native/[email protected](@babel/[email protected]([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected]))([email protected])
|
||||
'@hanzo/ui':
|
||||
specifier: 'catalog:'
|
||||
version: 8.27.2(2a4766a00c25c043c2e3b25c728d239e)
|
||||
'@playwright/test':
|
||||
specifier: 'catalog:'
|
||||
version: 1.62.1
|
||||
@@ -103,6 +100,15 @@ importers:
|
||||
jest:
|
||||
specifier: 'catalog:'
|
||||
version: 30.5.1(@types/[email protected])([email protected])
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 1.44.0([email protected])
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.8
|
||||
react-dom:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.8([email protected])
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.2
|
||||
@@ -3236,6 +3242,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-QRaLOWhX93YCnMiRfnOFRSwWXZNt8qhm2JTZwypoDvKpSffTJHmpzMXt8U6PV5UThvL3IiiLDUWe2nHMtz6Mmw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -4336,7 +4348,7 @@ snapshots:
|
||||
'@hanzogui/lucide-icons-2': 8.3.1([email protected]([email protected]))([email protected]([email protected](@babel/[email protected]([email protected]))(@react-native/[email protected](@babel/[email protected]([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected]))([email protected]))([email protected])
|
||||
react: 19.2.8
|
||||
|
||||
'@hanzo/[email protected](bebf53e2256154e0b6f66411048aeaaf)':
|
||||
'@hanzo/[email protected](2a4766a00c25c043c2e3b25c728d239e)':
|
||||
dependencies:
|
||||
'@hanzo/appearance': 0.2.2([email protected])
|
||||
'@hanzo/brand': 1.4.8([email protected])
|
||||
@@ -4358,6 +4370,7 @@ snapshots:
|
||||
react-native-web: 0.21.2([email protected]([email protected]))([email protected])
|
||||
optionalDependencies:
|
||||
next: 16.3.3(@babel/[email protected]([email protected]))(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
react-hook-form: 7.88.0([email protected])
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- prop-types
|
||||
@@ -7768,6 +7781,10 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -14,6 +14,7 @@ catalog:
|
||||
'next': '16.3.3'
|
||||
'react': '19.2.8'
|
||||
'react-dom': '19.2.8'
|
||||
'react-hook-form': '^7.71.0'
|
||||
'react-native': '^0.87.1'
|
||||
'react-native-web': '^0.21.2'
|
||||
'typescript': '7.0.2'
|
||||
|
||||
Reference in New Issue
Block a user