Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7d57205
feat(manifest): a phone side, mobile, built by Expo
MoSofi Oct 11, 2026
d9e4bf2
feat(public-api): an app key, a phone app's own kind of key
MoSofi Oct 11, 2026
84bcfb0
feat(apps): an app with a phone side gets an app key at install
MoSofi Oct 11, 2026
f387841
fix(public-api): the app key's review: links, the update check, kinds
MoSofi Oct 11, 2026
0689fc7
feat(apps): a phone side's starter, its build with Expo and its preview
MoSofi Oct 11, 2026
865378d
feat(designer): the phone door, and a phone side added from the page
MoSofi Oct 11, 2026
a785cb4
feat(designer): the Mobile target, the phone skill and its write rules
MoSofi Oct 11, 2026
7926f93
feat(designer): the Phone side of the preview, and Open on a phone
MoSofi Oct 11, 2026
e0870bf
docs: a phone app for an app, its key and its skill
MoSofi Oct 11, 2026
7512685
feat(apps): the Phone app card in an app's settings, and tests of the…
MoSofi Oct 11, 2026
9372bba
test(apps): the phone side, its build, its preview and its door
MoSofi Oct 11, 2026
c211312
chore: changeset for the phone app
MoSofi Oct 11, 2026
796a9e2
fix(apps): the phone starter's entry is a template like the rest
MoSofi Oct 11, 2026
e1e4919
chore(designer): say what the phone panel is built from
MoSofi Oct 11, 2026
1f5c038
fix(apps): close what the review of the phone side found
MoSofi Oct 11, 2026
068ac74
test(server): fix three source tests the phone side tripped
MoSofi Oct 11, 2026
4f48542
fix(llm): run on OpenAI's current models, with recordings of both
MoSofi Oct 11, 2026
5a11afa
fix(apps): close what the second review found in the phone side
MoSofi Oct 11, 2026
22d4d30
fix(server): look a phone preview's app up among the project's own
MoSofi Oct 11, 2026
2533c9a
fix(server): serve a phone preview's file by the build's own listing
MoSofi Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .changeset/openai-current-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@adminium/llm': patch
'@adminium/dashboard': patch
---

OpenAI's current models work again. The Designer's requests with tools were refused by every OpenAI model that reasons (`gpt-6-luna` and its family); it now asks with the model's reasoning off, which is how OpenAI's Chat Completions address takes tools from such a model. The assistant and schema enrichment were refused too (`max_tokens`, then temperature 0): they now send `max_completion_tokens`, and keep temperature 0 by switching the reasoning off where a model asks for that. OpenAI-compatible servers are sent what they were sent before.

The model lists shown before a provider answers are the current ones (Claude Sonnet 5.5, Haiku 5.5, Opus 5.5, Fable 5.1; GPT-6.1 Sol, GPT-6 Luna, GPT-6 Astra), and OpenAI's list no longer offers its models that do not chat (speech, live audio, pictures, search). Both were tried through the Designer on real requests: see "Models tried" in the Designer's guide.
15 changes: 15 additions & 0 deletions .changeset/phone-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@adminium/server': patch
'@adminium/dashboard': patch
'@adminium/manifest': patch
'@adminium/meta': patch
'@adminium/i18n': patch
---

An app can have a phone app for its customers: a third side, `mobile`, an Expo (React Native) project in `apps/<key>/mobile/`. `adminium app new <key> --mobile` writes one, and the Designer's "what to build" menu has **Mobile**. Adding one installs Expo's packages (about 350 MB), and a person is asked first; `adminium app approve-build` gives that yes for a phone side that came from elsewhere.

In the Designer the preview has a **Phone app** side: the app in a phone's frame, built for the web by Expo. **Open on a phone** shows a code that a phone with Expo Go, on the same network, scans to run the real app against this computer's data. Nothing is sent to anyone else, and the Designer itself stays on this computer. A phone side is a preview of a project being designed or developed: `adminium start`, a packed app and an installed app never serve it.

An app with a phone side gets an **app key** (`adm_app_…`) beside its customer key when it is installed. It reaches what the customer key reaches, needs no `Origin`, and is refused from a browser. Studio's API keys page lists it as "PHONE APP"; an update that adds a phone side asks before making it. A server key is now also refused when a request carries `Sec-Fetch-Dest`.

New skill for coding agents: `adminium-mobile`.
32 changes: 30 additions & 2 deletions apps/dashboard/src/designer/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,29 @@ import { api, ApiError, csrfHeaders } from '../app/api.js';

const BASE = '/api/v1/designer';

export type DesignerTarget = 'auto' | 'dashboard' | 'web';
export type DesignerTarget = 'auto' | 'dashboard' | 'web' | 'mobile';

/** How an app's phone side stands (mirrors the server's `designerPhoneReply`). */
export type PhoneStanding =
| { state: 'none' }
| { state: 'shape' | 'no-packages' | 'failed'; words: string }
/** `packages` are what its package.json asks for, by name: what the yes installs. */
| { state: 'unapproved'; words: string; packages: string[] }
| { state: 'built'; build: string }
| { state: 'working'; step: 'adding' | 'installing' | 'building' };

/** The door a real phone comes through. `url` carries the code a phone scans. */
export type PhoneDoorState =
| { state: 'off' }
| { state: 'starting'; app: string }
| { state: 'open'; app: string; url: string; qr: string; address: string; sdk: string | null; connected: boolean; phoneSdk: string | null; lastSeen: number | null }
| { state: 'closed'; app: string; why: 'stopped' | 'idle' | 'network' | 'failed'; words: string };

export interface PhoneStatus {
phone: PhoneStanding;
door: PhoneDoorState;
addresses: { address: string; name: string }[];
}

export interface DesignerSession {
id: string;
Expand Down Expand Up @@ -247,7 +269,7 @@ export type FolderHold = 'save' | 'style' | 'restore' | 'start';
/** `by: 'person'`: caused from a page, outside any turn. It carries the last turn's number and belongs to none. */
export type DesignerEvent = DesignerEventBody & { seq: number; turn: number; at: number; by?: 'person' };

export type DesignerFileGroup = 'customer' | 'staff' | 'dashboard' | 'settings';
export type DesignerFileGroup = 'customer' | 'staff' | 'mobile' | 'dashboard' | 'settings';
/** A file a person may open: where it is in the project, what it is called on the page, and what it holds now (by hash). */
export interface DesignerFile {
path: string;
Expand Down Expand Up @@ -408,6 +430,12 @@ export const designerApi = {
saveFiles: (id: string, files: readonly { path: string; content: string; base: string }[]) => api.put<DesignerFilesSaved>(`${BASE}/sessions/${id}/files`, { files }),
/** One of the app's files a person may open, whole, with the hash it has now. */
fileContent: (id: string, path: string) => api.get<{ path: string; content: string; hash: string }>(`${BASE}/sessions/${id}/files/content?path=${encodeURIComponent(path)}`),
/** The app's phone side: how it stands, the door a phone comes through, this computer's addresses. */
phone: (id: string) => api.get<PhoneStatus>(`${BASE}/sessions/${id}/phone`),
/** A person's yes: add the phone app (or get its packages), build it and apply the app. Answers at once; watch `phone`. */
phoneAdd: (id: string) => api.post<PhoneStatus>(`${BASE}/sessions/${id}/phone/add`, {}),
phoneOpen: (id: string, address: string) => api.post<PhoneStatus>(`${BASE}/sessions/${id}/phone/open`, { address }),
phoneStop: (id: string) => api.post<PhoneStatus>(`${BASE}/sessions/${id}/phone/stop`, {}),
previewTicket: (id: string, to: string) => api.post<{ url: string; origin: string; seenAs?: string[] }>(`${BASE}/sessions/${id}/preview-ticket`, { to }),
/** A one-use link that signs this project's local owner in from the system's browser, landing on `to` (a path). */
browserLink: (to: string) => api.post<{ data: { url: string } }>('/api/v1/auth/design-link', { to }),
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/src/designer/build/AddressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export function addressLabel(side: PreviewSide): string {
return t('designer:preview.addressStaff', 'Address on the staff side');
case 'customer':
return t('designer:preview.addressCustomer', 'Address on the customer side');
// A phone app has no addresses: its bar is not drawn, and this is never read.
case 'mobile':
return '';
}
}

Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/designer/build/BuildPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export function BuildPage({ sessionId }: { sessionId: string }): ReactNode {
void queryClient.invalidateQueries({ queryKey: designerKeys.apps });
void queryClient.invalidateQueries({ queryKey: designerKeys.architecture(sessionId) });
void queryClient.invalidateQueries({ queryKey: designerKeys.files(sessionId) });
// A turn may have added the phone app, or built it again.
void queryClient.invalidateQueries({ queryKey: ['designer', 'phone', sessionId] });
// The preview's list of installed apps: a live `app-changed` can be missed, a turn's end cannot.
void queryClient.invalidateQueries({ queryKey: ['designer', 'installed'] });
};
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/src/designer/build/CodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { CodeFiles, CodeLock } from './useCodeFiles.js';
function groupTitle(key: DesignerFileGroup): string {
if (key === 'customer') return t('designer:code.groupCustomer', 'Customer side');
if (key === 'staff') return t('designer:code.groupStaff', 'Staff side');
if (key === 'mobile') return t('designer:code.groupMobile', 'Phone app');
if (key === 'dashboard') return t('designer:code.groupDashboard', 'Dashboard side');
return t('designer:code.groupSettings', 'Design and settings');
}
Expand Down
5 changes: 4 additions & 1 deletion apps/dashboard/src/designer/build/MoreMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export function MoreMenu({
onSide: (side: string) => void;
seen: { label: string; tip: string } | null;
sees: boolean;
onSees: (on: boolean) => void;
/** Absent for a side the Designer does not look at (a phone app): the switch is then not in the menu. */
onSees?: ((on: boolean) => void) | undefined;
/** Absent where there is nowhere to open it. */
onNewTab?: (() => void) | undefined;
}): ReactNode {
Expand Down Expand Up @@ -69,6 +70,7 @@ export function MoreMenu({
<DropdownMenuSeparator />
</>
) : null}
{onSees === undefined ? null : (
<DropdownMenuItem
role="menuitemcheckbox"
aria-checked={sees}
Expand All @@ -83,6 +85,7 @@ export function MoreMenu({
>
{t('designer:preview.sees', 'The Designer looks at the page after it builds')}
</DropdownMenuItem>
)}
{rows && onNewTab !== undefined ? (
<DropdownMenuItem icon={<ExternalLink className="rtl:-scale-x-100" />} onSelect={onNewTab} className={MENU_ROW}>
{t('designer:preview.newTab', 'Open in a new tab')}
Expand Down
Loading
Loading